Udemy
    •  
    •  
    •  
    •  
    •  
    •  
    •  
    •  
Turn what you know into an opportunity and reach millions around the world.
Learn More
Your cart is empty.
Keep shopping
Building AI-Powered Blazor Applications
Rating: 4.5 out of 5(1 rating)
23 students

Building AI-Powered Blazor Applications

A Practical and Beginner-Friendly Guide to Modern .NET AI Development using OpenAI, Local LLMs and MS Agent Framework
Last updated 9/2026
English
English [Auto],

What you'll learn

  • Learn to create simple Blazor Hybrid (.NET MAUI) applications , extending the capabilities of .NET developers beyond the Web into desktop and mobile development
  • Learn about the concept of Localization. The the process of customizing applications to display and operate in the culture of the user.
  • Create simple applications that integrates Localization into Blazor WebAssembly, Server and .NET MAUI Blazor
  • Learn about the latest features and enhancements to Blazor in .NET 9
  • Learn how to Integrate the OpenAI API within Blazor Applications and Implement the .NET Smart Components - AI powered UI Controls
  • Learn how to use Open Source Tools (Ollama) to Run Large Language Models in Blazor
  • Learn how to perform AI Image Analysis and connect the results to a Database
  • Learn how to perform AI Video Analysis from various sources including using a Webcam
  • Learn how to Record Audio and convert it to Text (transcription/translation) and take Text and convert it to Speech using OpenAI's Whisper model
  • Learn how to implement real-time speech-to-text capabilities in a Blazor Web App and send the transcribed text to other users via Email
  • Learn how to Scrape a Website and analyze the contents using AI in a Blazor Application
  • Learn how to implement AI in Blazor Hybrid Applications (Windows Desktop/Android/IOS)
  • Learn how to Generate SQL Queries via AI for use in Blazor Database Applications (Natural Language to SQL)
  • Learn how to implement RAG (Retrieval Augmented Generation) within a simple Blazor AI application
  • Learn how to implement the new Microsoft Agent Framework and work with Multi-Agent Workflows scenarios
  • Learn how to implementing Human in the Loop AI Agents using the Microsoft Agent Framework
  • Learn how to implement the new OpenAI Responses API
  • Learn how to add custom memory to the Microsoft Agent Framework. It enables AI agents to recall user preferences, and past interactions
  • Learn how to implement the Google Gemini LLM into a .NET MAUI Blazor Hybrid application which then allows you to integrate the Google Maps Tool
  • Learn about Authentication and Authorization in a .NET MAUI Blazor Hybrid Application
  • Learn about AG-UI (Agent User Interaction Protocol) and how to implement it in a Blazor Web App
  • Learn how to integrate a Blazor Web App with n8n Automations
  • Learn how to integrate Blazor and n8n with a Telegram bot (mobile and desktop)

Course content

1 section • 31 lectures • 23h 7m total length
  • .NET 9 and Blazor ... A First Look39:26

    In this Lecture we will

    • Give a brief overview of some the new features and improvements specific to Blazor in the new .NET 9 Framework update

      • NET 9 Blazor WebAssembly Apps now startup 25% faster

      • Blazor Server Apps are more responsive due to web socket message compression

      • Static web assets (usually stored in the wwwroot folder)  are now optimised so that your static assets can be efficiently downloaded and cached (this occurs when your Application is Published)

      • You can now Detect the component render mode at runtime (See BlazorWebAppNET9InterativeAuto and RegistrationNET9 application)

      • The EditForm component InputNumber now supports a slider ... type=range (See RegistrationNET9 application)

      • The QuickGrid component now supports and OverscanCount property

        • <QuickGrid Items"@people" Virtualize OverscanCount="3" >

        • Using Virtualize allows the Grid to only displays data necessary for what is currently rendered with the OverscanCount property adding rows before and after  to pad out the scroll a little bit

      • Blazor Hybrid applications now have a new Template called .NET MAUI Blazor Hybrid and Web App (See BlazorHybridWebAppNET9  and HoneyDoNET9)

        • Recall that Blazor Hybrid apps take the best of .NET MAUI and Blazor and allow you to create Native experiences with web UI and code reuse across devices (desktop and mobile) and web browsers.

    • Create a simple Blazor Web App (Interactive Render mode : Auto Server and WebAssembly)  using .NET 9 (BlazorWebAppNET9InteractiveAuto) ... hands on

      • In the Counter page we implement the RenderInfo commands to detect the components render mode at runtime

        • <h2>Render mode: @RendererInfo.Name</h2>


          <p role="status">Current count: @currentCount</p>


          @*During Pre-Rendering there may be a delay...here is how we deal with it*@


          @if(RendererInfo.IsInteractive)

          {

              <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

          }

          else

          {

              <p>... Loading please wait</p>

          }

        • Execute application and observe results ... Initially RenderMode is Static ( Static Server Side Rendering ... which means that there is no interactivity at the moment because the page is rendered on the server and returned to the client without interactivity ... non-interactive render mode)  then it switches to either Server/WebAssembly Interactive Render Mode

          • The Auto render mode decides how to render the component at runtime. First, the component is rendered using InteractiveServer. In the background, the .NET runtime and the app bundle are downloaded and cached client-side.

            Next time the component renders, it will use the client executable and render the component using InteractiveWebAssembly.

    • RegistrationNET9 (demo only)

      • This is an update (Blazor WebAssembly Standalone App using .NET 9)  to the Application created in Lecture 125 where we create a more sophisticated Registration type form (Registration) which implemented among other things

        • A drop down list for name title (Mr./Mrs/Miss/Ms)

        • A confirm password check

        • An "Accept Terms" checkbox

        • The use of EditContext. Its the way EditForm keeps track of the current state of the form (which fields have been modified) and any validation errors which have been triggered. You can use it instead of the Model attribute assignment.

        • Storing the entered information in JSON format by calling the  JsonSerializer and re-displaying it with a simple alert command called from a Javascript function.

      • Note on the Home.razor page the RendererInfo commands ... You can now through code detect the component render mode at runtime.

        • Welcome to your new .NET 9 app 

          <br />

          Render Mode: @RendererInfo.Name   @(RendererInfo.IsInteractive? "Interactive RenderMode":"No Interactive RenderMode")

        • Name ... returns the location (Server/Client...WebAssembly) where the component is executing

        • IsInteractive indicates if the component supports interactivity at the time of rendering

      • Note in the link "Registration Form App" (RegForm.razor)  the addition of the InputNumber component with type=range .... But unlike the input html tag where we could add oninput for a real time update effect , this does not work now.

        • Basically bind-Value:event="oninput" is not supported in InputNumber or any other Blazor EditForm Component, but you can easily derive a new control from 'InputNumber' which does update on input.

        • You create a new custom Razor Component ... called MyInputNumber.razor , have it inherit Forms.InputNumber and then use the basic html input with additional attributes ... See the RegForm.razor page and MyInputNumber.razor page for all the details

        • MyInputNumber.razor

          • @inherits Microsoft.AspNetCore.Components.Forms.InputNumber<int>

            <input type="range" max="200" min="10" step="10" @attributes="@AdditionalAttributes" class="@CssClass" @bind="@CurrentValueAsString" @bind:event="oninput" />

          • By inheriting from InputNumber<int>, the MyInputNumber component gains all the functionality of the InputNumber<int> base class. This base class is a Blazor component designed to handle numeric input (int in this case)

          • @attributes="@AdditionalAttributes":

            This allows additional attributes to be passed dynamically to the <input> element. For example, if you pass id="mySlider" or data-custom="value" from the parent component, they will be added to the rendered <input> element.

          • class="@CssClass":

            This binds the CssClass property (inherited from InputNumber<int>) to the class attribute of the <input> element. It allows you to dynamically apply CSS classes to style the slider.

          • @bind="@CurrentValueAsString":

            This binds the slider's value to the CurrentValueAsString property (also inherited from InputNumber<int>). When the user moves the slider, the value is updated in the component, and vice versa.

        • RegForm.razor

          • <div class="form-group row">

                <div class="form-group col-5">

                    <label class="mt-2">Age ... using InputNumber:@(model.Age == 0 ? "" : model.Age)</label>

                    <InputNumber type="range" max="100" min="1" step="1" @bind-Value="model.Age" />

                    <ValidationMessage For="@(() => model.Age)" />

                </div>

                <div class="form-group col-5">

                    <label class="mt-2">Age ... using MyInputNumber:@(model.Age == 0 ? "" : model.Age)</label>

                    <MyInputNumber @bind-Value="model.Age" />

                    <ValidationMessage For="@(() => model.Age)" />

                </div>

            </div>

      • RegNET9InputNumberOninputTime

        • Note the addition of the time picker (MyInputTime) in the Registration Form App. The HTML input type=time is not available in Blazor Edit Components

      • RegNET9MarkdownEditor

        • In this update ... Note the addition of Simple Markdown Editor in the Registration Form App. SimpleMDE ... a simple and embeddable Javascript editor... You may need to run it without the Debugger (Ctrl+ F5) to see it in action)

          Here are the Details

          • In the wwwroot folder we have updated the index.html file to point to the js code and stylesheet

            • We are using an External URL: Instead of hosting the JavaScript file locally in your project (e.g., in a js folder), you are pointing to a Content Delivery Network (CDN) URL.

            • We also point to the local js file (located in the js folder) . This file is used to create an instance of the Editor for use in the BlazorSimpleMde.razor component.

          • In the wwwroot folder we have added a javascript file blazor-simplemde.js

          • In the Pages folder we have added a new Razor Component called BlazorSimpleMde.razor

          • In this BlazorSimpleMde.razor file we have added a textarea element and a code block to initialize the markdown editor and subsequently get the value (contents) of the editor

          • ... and finally we add BlazorSimpleMde component (the Markdown Editor) to our form

    • Take a look at the new Blazor Hybrid Template

      • .NET 9 now has a new template called .NET MAUI Blazor Hybrid and Web App

      • This new Blazor Hybrid template gives you with a single click an automatically configured project with .NET MAUI, Blazor Web App, and a Razor class library all setup! ... plus a Client Project if you pick WebAssembly Render Mode

      • ... Recall Lecture 166 Introduction to Blazor Hybrid ... Where we implemented the .NET MAUI Blazor Hybrid App which consisted of a Single Project (.MAUI) and we manually added a Razor Class Library Project and Server/WASM project after the fact (HybridTodoLibNET8updated)

      • BlazorHybridWebAppNET9  ... hands on

        • Start up Visual Studio 2022 and choose .NET MAUI Blazor Hybrid and Web App ...NET 9 ...Interactive Render Mode WebAssembly

        • The Solution now contains

          • The .NET MAUI project  ... Blazor Hybrid App  (MauiApp1)

            • Windows Machine (Windows Desktop App)

            • Mobile

          • A Web Project (MauiApp1.Web) ... standard Blazor Web App (Server)

          • A Shared Project (MauiApp1.Shared) ... Razor Class Library

            • Most of the UI lives in the Shared project

            • Note how all the Razor Component pages live here in the Pages subfolder

              • Counter and Weather pages are basically the same as previous versions

              • Home page has some new additions

                • @page "/"

                  @using ToDoHybridNET9.Shared.Services

                  @inject IFormFactor FormFactor


                  <PageTitle>Home</PageTitle>


                  <h1>Hello, world!</h1>


                  Welcome to your new app running on <em>@factor</em> using <em>@platform</em>.


                  @code {

                      private string factor => FormFactor.GetFormFactor();

                      private string platform => FormFactor.GetPlatform();

                  }

                • The Interface IFormFactor is located in the Services folder of this Shared project

                • This Interface is registered in the Program.cs file of the Web project and in MauiProgram.cs of the MAUI project
                  // Add device-specific services used by the ToDoHybridNET9.Shared project

                  builder.Services.AddSingleton<IFormFactor, FormFactor>();

                • The Implementation of FormFactor is located in the Services folder of the MAUI project
                  using ToDoHybridNET9.Shared.Services;


                  namespace ToDoHybridNET9.Services

                  {

                      public class FormFactor : IFormFactor

                      {

                          public string GetFormFactor()

                          {

                              return DeviceInfo.Idiom.ToString();

                          }


                          public string GetPlatform()

                          {

                              return DeviceInfo.Platform.ToString() + " - " + DeviceInfo.VersionString;

                          }

                      }

                  }

                • .... and the Services folder of the Web Project
                  using ToDoHybridNET9.Shared.Services;


                  namespace ToDoHybridNET9.Web.Services

                  {

                      public class FormFactor : IFormFactor

                      {

                          public string GetFormFactor()

                          {

                              return "Web";

                          }


                          public string GetPlatform()

                          {

                              return Environment.OSVersion.ToString();

                          }

                      }

                  }


            • Note how all the Layout pages live here (Shared project)  in the Layouts subfolder

          • ... Client Project (MauiApp1.Web.Client) ... WebAssembly Project .. since we chose WebAssembly Render Mode

        • When you execute the application it will default to the Blazor Hybrid App (Windows Machine / Mobile ).

          • Welcome to your new app running on Desktop using WinUI - 10.0.26100.2314.

          • Welcome to your new app running on Phone using Andriod-15

        • You can change the Startup Project and have it point to the Web App and it will run as a typical Blazor Web App with Rendermode InteractiveWebAssembly

          • Welcome to your new app running on WebAssembly using Other 1.0.0.0.

          • Check out App.razor in MauiApp.Web folder
            <body>

                <Routes @rendermode="InteractiveWebAssembly" />

                <script src="_framework/blazor.web.js"></script>

            </body>

      • HoneyDoNET9 (demo only ... Optional Enrichment )

        • Simple To Do application with different views/UI depending on device or Web

          • Card View (Mobile) or QuickGrid (Desktop or Web)

          • Note the use of the #if preprocessor conditional directive (on Home.razor/All.razor and Edit.razor pages)
            @{ #if ANDROID || IOS }

                <MyCardView HoneyDos="@filteredHoneyDos" />

            @{ #else }

                <MyQuickGrid HoneyDos="@filteredHoneyDos" />

            @{ #endif }

          • #if is a preprocessor directive to control conditional compilation.  Opens a conditional compilation where code is compiled only if the specified symbol is defined .The C# compiler compiles the code between the #if directive and #endif directive


        • Key Highlights

          • Most of the coding happens in the Shared project

            • In the Models folder ... HoneyDoModel.cs class

            • In the Interface folder

              • ILocalStorage.cs
                public interface ILocalStorage

                {

                    public string GetItem(string key);

                    public void SetItem(string key, string value);

                    public void RemoveItem(string key);

                }

              • IPhotoManager.cs
                public interface IPhotoManager

                {

                    public Task<string> TakePhotoAsync();

                    public Task<string> PickPhotoAsync();

                }

            • In the Services folder

              • LocalStorage

              • PhotoManager.cs

              • HoneyDoServices
                internal class HoneyDoService

                {

                    private static List<HoneyDoModel>? myModel = null;

                    private const string ModelName = "HoneyDoModel";


                    public static List<HoneyDoModel> GetModel(ILocalStorage storage)

                    {

                        if (myModel == null)

                        {

                            string jsonString = storage.GetItem(ModelName);

                            if (jsonString != null)

                            {

                                myModel = JsonSerializer.Deserialize<List<HoneyDoModel>>(jsonString);

                            }

                        }

                        if (myModel == null)

                        {

                            myModel = new List<HoneyDoModel>();

                            myModel.Add(new HoneyDoModel { Id = 1, Task = "Task 1", Description = "Description 1", IsComplete = false, DueDate = DateTime.Now.AddDays(1), AssignedTo = "Nick", CreatedBy = "Beth", CreatedDate = DateTime.Now, UpdatedDate = DateTime.Now });

                            myModel.Add(new HoneyDoModel { Id = 2, Task = "Task 2", Description = "Description 2", IsComplete = false, DueDate = DateTime.Now.AddDays(2), AssignedTo = "Nick", CreatedBy = "Beth", CreatedDate = DateTime.Now, UpdatedDate = DateTime.Now });

                            myModel.Add(new HoneyDoModel { Id = 3, Task = "Task 3", Description = "Description 3", IsComplete = false, DueDate = DateTime.Now.AddDays(2), AssignedTo = "Nick", CreatedBy = "Beth", CreatedDate = DateTime.Now, UpdatedDate = DateTime.Now });

                        }

                        return myModel;

                    }

                    public static HoneyDoModel GetModelById(int id, ILocalStorage storage)

                    {

                        if (myModel == null)

                        {

                            myModel = GetModel(storage);

                        }

                        var item = myModel?.FirstOrDefault(m => m.Id == id);

                        return item;

                    }

                    public static void SaveModel(ILocalStorage storage)

                    {

                        if (myModel == null)

                        {

                            return;

                        }

                        string jsonString = JsonSerializer.Serialize(myModel);

                        storage.SetItem(ModelName, jsonString);

                    }

                    public static HoneyDoModel AddItem()

                    {

                        var model = new HoneyDoModel();

                        if (myModel != null)

                        {

                            myModel.Add(model);

                            if (model.Id == 0)

                            {

                                model.Id = myModel.Max(m => m.Id) + 1;

                            }

                        }

                        return model;

                    }

                    public static void AddItem(HoneyDoModel model)

                    {

                        if (myModel != null)

                        {

                            myModel.Add(model);

                            if (model.Id == 0)

                            {

                                model.Id = myModel.Max(m => m.Id) + 1;

                            }

                        }

                    }

                    public static void RemoveItem(HoneyDoModel model)

                    {

                        myModel?.Remove(model);

                    }


                    public static List<HoneyDoModel> ReloadModel(ILocalStorage storage)

                    {

                        myModel = null;

                        return GetModel(storage);

                    }

                }

            • Components folder

              • MyCardView.razor
                @using Shared.Models

                @using Shared.Interfaces

                @using Shared.Services

                @inject NavigationManager NavigationManager


                @if (honeyDos == null || honeyDos.Count() == 0)

                {

                    <p>No tasks due. Enjoy the day! ?</p>

                }

                @foreach (var honeyDo in honeyDos)

                {

                    <div class="card @GetCardBorderColor(honeyDo)" >

                        <div class="card-body @GetCardBodyColor(honeyDo)">

                            <h4 class="card-title">@honeyDo.Task</h4>

                            <p class="card-text">@honeyDo.Description</p>

                            <p class="card-text">Due: @honeyDo.DueDate.ToShortDateString()</p>

                            @if (honeyDo.IsComplete)

                            {

                                <p class="card-text">Completed</p>

                            }

                            else

                            {

                                <p class="card-text">Not completed</p>

                            }

                            <img class="card-img-bottom img-thumbnail" style="width:100px" src="@honeyDo.Image" alt="Card image">

                            <a href="#" class="stretched-link" @onclick="() => Edit(honeyDo.Id)"></a>

                        </div>

                    </div>

                }

                @code {

                    [Parameter]

                    public IQueryable<Shared.Models.HoneyDoModel>? honeyDos { get; set; }


                    private void Edit(int id)

                    {

                        NavigationManager.NavigateTo($"/edit/{id}");

                    }

                    public string GetCardBorderColor(HoneyDoModel honeyDo)

                    {

                        if (honeyDo.IsComplete)

                        {

                            return " border-secondary mb-3";

                        }

                        else if (honeyDo.DueDate.Date < DateTime.Now.Date)

                        {

                            return " border-danger mb-3";

                        }

                        else

                        {

                            return "";

                        }

                    }

                    public string GetCardBodyColor(HoneyDoModel honeyDo)

                    {

                        if (honeyDo.IsComplete)

                        {

                            return " text-secondary";

                        }

                        else if (honeyDo.DueDate.Date < DateTime.Now.Date)

                        {

                            return " text-danger";

                        }

                        // else if (honeyDo.DueDate.Date == DateTime.Now.Date)

                        // {

                        //     return " text-primary";

                        // }

                        else

                        {

                            return "";

                        }

                    }

                }

              • MyQuickGrid.razor
                @using Shared.Models

                @using Shared.Interfaces

                @using Shared.Services

                @using Microsoft.AspNetCore.Components.QuickGrid

                @inject ILocalStorage LocalStorage

                @inject NavigationManager NavigationManager


                @if (honeyDos == null || honeyDos.Count() == 0)

                {

                    @*emoji character are added in Windows 11 by

                        pressing Window logo key and . (period)

                        this will display the Emoji keyboard ?

                    *@
                        <p>No tasks due. Enjoy the day! ?</p>

                }

                <QuickGrid Items="@honeyDos" Class="table table-sm table-bordered table-striped">

                @{#if ANDROID || IOS || WINDOWS || MACCATALYST }

                    <TemplateColumn Title="Image">

                        <img class="img-thumbnail" style="width:100px" src="@context.Image" alt="Card image">

                        <div class="click-area" @onclick="() => Edit(context)"></div>

                    </TemplateColumn>

                @{#endif}

                    <TemplateColumn Title="Task" Sortable="true">

                        <div class="@GetTaskColor(context)">@context.Task</div>

                        <div class="click-area" @onclick="() => Edit(context)"></div>

                    </TemplateColumn>

                    <PropertyColumn Property="@(item => item.DueDate)" Format="yyyy-MM-dd" Sortable="true" />

                     <TemplateColumn Title="Complete?">

                        <input type="checkbox" @bind="@context.IsComplete" @onclick="() => Complete(context)" />

                    </TemplateColumn>

                    <PropertyColumn Property="@(item => item.Description)" Sortable="true"/>

                    <PropertyColumn Property="@(item => item.AssignedTo)" Sortable="true"/>

                </QuickGrid>

                @code {

                    [Parameter]

                    public IQueryable<Shared.Models.HoneyDoModel>? honeyDos { get; set; }


                    private void Edit(HoneyDoModel model)

                    {

                        NavigationManager.NavigateTo($"/edit/{model.Id}");

                    }

                    private void Save()

                    {

                        HoneyDoService.SaveModel(LocalStorage);

                    }

                    private void Complete(HoneyDoModel model)

                    {

                        model.IsComplete = !model.IsComplete;

                        Save();

                    }

                    public string GetTaskColor(HoneyDoModel honeyDo)

                    {

                        if (honeyDo.IsComplete)

                        {

                            return " text-secondary";

                        }

                        else if (honeyDo.DueDate.Date < DateTime.Now.Date)

                        {

                            return " text-danger";

                        }

                        else

                        {

                            return "";

                        }

                    }

                }

            • Pages folder

              • All.razor

                @page "/all"

                @using HoneyDo.Shared.Services

                @using HoneyDo.Shared.Interfaces

                @using HoneyDo.Shared.Models

                @using HoneyDo.Shared.Components

                @inject ILocalStorage LocalStorage

                @inject NavigationManager NavigationManager


                <PageTitle>All "Honey Do" Items</PageTitle>

                <h3>All Tasks</h3>

                <p>

                    Show completed tasks: <input type="checkbox" @onclick="FilterChanged" />

                </p>

                @{ #if ANDROID || IOS }

                    <MyCardView HoneyDos="@filteredHoneyDos" />

                @{ #else }

                    <MyQuickGrid HoneyDos="@filteredHoneyDos" />

                @{ #endif }


                @code {


                    private IQueryable<HoneyDoModel> honeyDos;

                    private IQueryable<HoneyDoModel> filteredHoneyDos;

                    private bool showCompleted = false;


                    protected override void OnInitialized()

                    {

                        honeyDos = HoneyDoService.GetModel(LocalStorage)

                                                  .AsQueryable()

                                                  .OrderBy(item => item.DueDate);


                        filteredHoneyDos = honeyDos.Where(item => !item.IsComplete);

                    }           

                    private void FilterChanged()

                    {       

                        showCompleted = !showCompleted;

                        if (showCompleted)

                        {

                            //Show all

                            filteredHoneyDos = honeyDos.Where(item => true);

                        }

                        else

                        {

                            filteredHoneyDos = honeyDos.Where(item => !item.IsComplete);

                        }       

                    }

                }

              • Home.razor

              • Edit.razor (uses Route Parameters ... so that it handle handle edits and new ToDos)
                @page "/edit/{Id}"

                @page "/add"

                @using HoneyDo.Shared.Services

                @using HoneyDo.Shared.Interfaces

                @using HoneyDo.Shared.Models

                @inject NavigationManager NavigationManager

                @inject ILocalStorage LocalStorage

                @inject IPhotoManager PhotoManager


                <PageTitle>"Honey Do"" Task</PageTitle>

                <h3>Edit Task</h3>


                <EditForm class="form-control-sm" Model="@honeyDo" @ref=form>

                    <DataAnnotationsValidator />

                    <ValidationSummary />


                    <div class="form-group">

                        <label class="col-form-label" for="Task">Task</label>

                        <InputText id="Task" @ref="taskBox" class="form-control" @bind-Value="@honeyDo.Task" />

                    </div>

                    <div class="form-group">

                        <label class="col-form-label" for="Description">Description</label>

                        <InputTextArea id="Description" class="form-control" @bind-Value="@honeyDo.Description" />

                    </div>

                    <div class="form-group">

                        <label class="col-form-label" for="DueDate">Due Date</label>

                        <InputDate id="DueDate"  class="form-control" @bind-Value="@honeyDo.DueDate" />

                    </div>

                    <div class="form-group">

                        <label class="col-form-label" for="IsComplete">Completed?</label>

                        <InputCheckbox id="IsComplete" class="form-check-inline" @bind-Value="@honeyDo.IsComplete" />

                    </div>

                    @{#if ANDROID || IOS || WINDOWS }

                    <table>

                        <tr>

                            <td >

                                @{#if ANDROID || IOS }

                                <button class="btn btn-secondary" @onclick="@TakePhoto">Photo</button>

                                @{#endif}

                            </td>

                            <td><button class="btn btn-secondary" @onclick="@PickPhoto">Pick</button></td>

                            <td><button class="btn btn-secondary" @onclick="@ClearPhoto">Clear</button></td>

                        </tr>

                        <tr>

                            <td colspan="3" >                   

                                <img src="@honeyDo.Image" width="250" />

                            </td>

                        </tr>     

                    </table> 

                @{#endif}

                    <p></p>       

                    <button class="btn btn-primary" @onclick="@Save">Save</button>

                    <button class="btn btn-danger" @onclick="@Cancel">Cancel</button>

                </EditForm>


                @code {

                    private HoneyDoModel honeyDo;

                    [Parameter]

                    public string? Id { get; set; }

                    private EditForm? form { get; set; }

                    private InputText taskBox;


                    protected override async Task OnAfterRenderAsync(bool firstRender)

                    {

                        if (firstRender)

                        {

                            await taskBox.Element.Value.FocusAsync();

                        }

                   }

                    protected override void OnInitialized()

                    {

                        if (Id == null)

                        {

                            Id = "0";

                        }

                //When adding a NEW ToDo



                //When editing uri will NOT contain add


                        if (!NavigationManager.Uri.Contains("add"))

                        {           

                            var id = int.Parse(Id);

                            honeyDo = HoneyDoService.GetModelById(id, LocalStorage);       

                        } 

                        if (honeyDo == null)

                        {

                            honeyDo = HoneyDoService.AddItem();

                        }

                    }

                    private async void TakePhoto()

                    {

                        honeyDo.Image = await PhotoManager.TakePhotoAsync();

                        StateHasChanged();

                    }

                    private async void PickPhoto()

                    {

                        honeyDo.Image = await PhotoManager.PickPhotoAsync();

                        StateHasChanged();

                    }

                    private void ClearPhoto()

                    {   

                        honeyDo.Image = "_content/HoneyDo.Shared/placeholder-image.jpg";

                        StateHasChanged();

                    }

                    private void Save()

                    {     

                        if (form.EditContext.Validate())

                        {

                            HoneyDoService.SaveModel(LocalStorage);

                            NavigationManager.NavigateTo("/");

                        }

                    }

                    private void Cancel()

                    {

                        HoneyDoService.ReloadModel(LocalStorage);

                        NavigationManager.NavigateTo("/");

                    }

                }


          • Note the use of 

            • Blazored LocalStorage to save ToDos in the Web and Client Projects

              • added to the Dependencies in the Web Project and Client Project

              • ... and Program.cs
                using HoneyDo.Shared.Interfaces;

                using HoneyDo.Web.Components;

                using HoneyDo.Web.Services;

                using Blazored.LocalStorage;


                var builder = WebApplication.CreateBuilder(args);


                // Add services to the container.

                builder.Services.AddRazorComponents()

                    .AddInteractiveServerComponents()

                    .AddInteractiveWebAssemblyComponents();


                //Use Blazored OSS library for browser-local storage

                builder.Services.AddBlazoredLocalStorage();


                // Add device-specific services used by the HoneyDo.Shared project

                builder.Services.AddScoped<ILocalStorage, LocalStorage>();

                builder.Services.AddSingleton<IPhotoManager, PhotoManager>();

              • In Services folder LocalStorage.cs contains version specific to using Blazored LocalStorage on a typical Web App

                • using Blazored.LocalStorage;


                  namespace HoneyDo.Web.Services

                  {

                      public class LocalStorage : Shared.Services.LocalStorage

                      {

                          private ILocalStorageService LocalStorageService;


                          public LocalStorage(ILocalStorageService _localStorageService)

                          {

                              LocalStorageService = _localStorageService;

                          }


                          protected override string ReadFromStorage()

                          {

                              ValueTask<string> task = LocalStorageService.GetItemAsync<string>(StorageKeyName);

                              return task.GetAwaiter().GetResult();

                          }


                          protected override void SaveToStorage(string jsonString)

                          {               

                              LocalStorageService.SetItemAsync(StorageKeyName, jsonString);           

                          }

                      }

                  }

              • Preferences to store data locally in .MAUI Project (Windows Desktop, Mobile Apps)

                • public class LocalStorage : HoneyDo.Shared.Services.LocalStorage

                  {

                      protected override string ReadFromStorage()

                      {

                          return Preferences.Get(StorageKeyName, "");

                      }

                      protected override void SaveToStorage(string jsonString)

                      {           

                          Preferences.Set(StorageKeyName, jsonString);            

                      }

                  }

            • QuickGrid to display results in Web app and Windows Desktop

              • added to the Shared Project and the Web Project

    • Supplementary Demos

      • ToDoNET9WasmNewLayout

        • This is a simple To Do application that uses the Blazor WebAssembly Standalone Template and .NET 9.0. This version is a bit more stylized with a Top Navigation Menu instead of the classical Side Navigation Menu. The application stores the "Todos" locally using Javascript. This is in contrast to the HoneyDo application in this Lecture , which stored "Todos" locally by implementing the NuGet Package BlazoredLocalStorage for the Web App and the .MAUI Preferences command for the Mobile and Desktop Apps.

          Here are some the key teaching points/features of the application related to the Coding:

          • In the Models folder the class TaskItem.cs is declared. A simple class with 3 properties ... Id,Name,IsComplete

          • In the Interface folder we declared a simple Interface called ITaskService.cs ... 4 Tasks are declared ... GetTaskItemsAsync,SaveTaskItemsAsync,AddTaskItem,RemoveTaskItem

          • Now in the Services folder we create the Service/Class called TaskService which implements the Interface above

            • GetTaskItemsAsync ... returns a List of TaskItems from Local Storage using Javascript ... localStorage.getItem

            • SaveTaskItemsAsync ... saves the List of TaskItems to local Storage using Javascript ... localStorage.setItem

            • ... Recall we need to declare this Service in Program.cs so it can be injected in our desired Pages

          Now we focus on the UI Development

          • We will not use the traditional Side menu , but rather a Top Navigation Technique.

          • First in App.razor we change the DefaultLayout to TopNavLayout.razor

          • Now we go into the Layout folder and create TopNavLayout consisting of a TopNavMenu and Footer layout

          • TopNavMenu.razor will contain the Home and To Do Task Links

          • Footer.razor will contain 3 columns ... Id Section, Udemy section,Social Media links

          • Now we focus on the Pages folder

            • The Home page is updated with a new Welcome

            • The TaskPage is the focus. It implements an EditForm to display an InputText control and an Add Button. Once a Todo is entered we cycle through the entire list and display the current Todos in a separate component called TaskLine. This TaskLine component (located in the UI folder) will have a Delete button and a Checkbox to mark the Todo as complete. Note the use EventCallbacks in the TaskLine component since Delete and CheckBox actions will occur in the Parent component (TaskPage.razor)

            • The Styling for each of these pages occurs in local stylesheets (isolated stylesheet) connected to each Razor Component Page
              ... TaskLine.razor.css/Taskpage.razor.css
              ... TopNavMenu.razor.css/Footer.razor.css

      • FormValidDisplayNameNET9

        • This is a simple demo application which reviews and extends our knowledge of how to use forms and validations in Blazor WebAssembly.

          Click on the Forms and Validations link in the navigation bar to see the demo.

          ... Features to note are:

          • DisplayName="Car Drivers Age" hard coded right in the InputNumber component

          • The Addition of some SEO (Search Engine Optimization ... Check out the index.html file in the wwwroot folder

          • The use of C# conditional preprocessor directives and predefined constants you can use to check the Blazor target framework

            • @code {

                 

                  private string? blazorVersion{ get; set; }


                  protected override void OnInitialized()

                  {

                      #if NET6_0

                          blazorVersion = "NET6.0";   

                      #elif NET7_0

                          blazorVersion = "NET7.0";   

                      #elif NET8_0

                          blazorVersion = "NET8.0";   

                      #elif NET9_0

                          blazorVersion = "NET9.0";   

                      #endif

                  }   

              }

      • BlazorAutoCompleteNET9

        • A demonstration of the concept of AutoComplete , implementing a simple Service (although I called it SuggestionsApi) which we inject into this page to fetch matching names from a list

          Here are the Details

          • When you type a name in the input box, the service is called to fetch matching names

          • When you click on a suggestion, the name is selected and added to the input box and the suggestions are cleared

          • First we create a class called DataSeed which will contain a list of name suggestions ... not exhaustive but a fair size... See MoreNames for a large list of Male only names (2700+)

          • Then we create a class called SuggestionsApi (Service) which will have a method FindAsync to fetch matching names

          • Next we register the service in the Program.cs file

          • Finally we inject the service into this page and use it to fetch matching names

          • On the main page, IntroduceYourself.razor the key ideas are: The use of bind:after (in the inputbox) to execute the task AutocompleteAsync (accesses the FindAsyc Task from the injected Service),and making the suggestion names list clickable in the foreach statement.

      • BlazorMusicPadUpdated

        • This project is a simple music pad that plays sounds when the corresponding key is pressed or button clicked. This application reviews and extends our knowledge of Interfaces, Dependency Injection, and JS Interop.

        • First we created a Models folder to hold our classes,and interface

          • Our base class is called PadModel. A class containing 4 properties Id, SoundName,SoundUrl,Key and a constructor

        • Next we jump over to the wwwroot folder and create a folder called js to holder our javascript code

          • Our javascript code is called audioplayer.js and it contains a function called playaudio

          • Make sure you add a reference to audioplayer.js in the index.html file

        • Now we go back to our Models folder and create a simple Interface called ISoundPlayer with a Task called Play

          • ... and finally we create a class/service called SoundPlayer. This class implements the ISoundPlayer interface and uses IJSRuntime to call the JavaScript function playaudio . Remember we are going to inject this service into our MusicPage and MusicPad components so we need to register it in the Program.cs file

        • Now we focus on the UI

          • We create a non-routable Razor Component called MusicPad which implements a parameter referencing the PadModel class. This component is used to display the pads on the screen. Each pad has a name, sound url, and key. The key is the key on the keyboard that will trigger the sound. Note the use of a css file to style the pads. Also note how the sound is played when the pad is clicked using the OnPadClicked method which is called when the pad is clicked. The sound is played using the injected soundPlayer

          • Finally we create a routable Razor Component called MusicPage which is the main page of the application. This page contains a list of pads of type PadModel created using the PadModel constructor. Each pad has a name, sound url, and key. The pads are displayed on the screen using the MusicPad component referenced in the foreach loop in the HTML section. Note how on this page we are listening for Keypresses and playing the sound of the pad using the injected soundPlayer

      • HySqLiteN9profile

        • .NET MAUI Blazor Hybrid .NET 9 Application with full CRUD ability using an SQLite DB

        • Displays a table of user profiles (Id, Name, Email) and also a QuickGrid display

        • Supports creating, loading, editing and deleting profiles (with confirmation dialogs).

        • Responsive UI: Action buttons in Table Display and QuickGrid (Edit/Delete) are hidden on Android, instead we us row taps for options which pops up a MAUI DisplayActionSheet

        • Navigation to add/edit profile page (UpdatePage.razor)

        • UpdatePage uses route parameters ie page "/addOrEdit/{id:int}"

  • Integrating the OpenAI API into a Blazor Application ... Part 135:08

    In this Lecture we will

    • Discuss how the integration of ChatGPT in modern web applications adds to engagement and a more personalized experience for users.

    • Learn how to implement (integrate) OpenAI (ChatGPT) into a Blazor Web App

      • Learn that the first step in the process is creating an account on the OpenAI Developer Platform site


      • Learn how to generate an API key that we need to use in our Blazor app ... once you login go here:

        • Newer OpenAI keys can be as long as 164 characters in size

          • Make sure to make a copy of your key right away because you will not be able to see it again after it is created.

          • You can of course delete this key and create a new one if necessary

        • Next make note of  the OpenAI’s API endpoint for Chat Completions: ... choose API Reference ...  Chat Completions )

          • we will need this endpoint when we use the HttpClient technique (one of two techiques) to access the API ... recall Lectures 112-113 "Accessing Data Services that Implement the JSON:API"

          • Then click on the </> Code button and select curl ... you will see the Endpoint right at the top

          • You will also see a sample Request body which gives us a clue how we will code the request later in Blazor

          • You will also see a sample Response

          • Note: Circa May 2025 OpenAI now recommends trying Responses instead of Chat Completions (See Lecture 213)

        • Take a look at a number of the OpenAI API models available

          • GPT-4o

          • GPT-4o mini

        • Pricing

          • Accessing  the OpenAI API is not free and cost varies depending on usage and on the model being accessed.

          • Go to Settings (Cog wheel image) ... then Billing

            • Here you can use "Pay as you go" ... you add your payment method and setup an initial amount as little as 5-10 dollars which can be recharged when your credit balance reaches zero.

            • See the Pricing link   ... note there is a separate cost for input and output based on 1 million tokens (a basic unit of text used by the model to process information ... a token is a chunk of text that can be a whole word, part of a word or even a single character depending on how the text is tokenized)

            • Check out Usage (Dashboard)

      • Create our first simple Blazor application which utilizes the OpenAI API (key removed in demos in Resources ... you supply your own)

        • BlazorAInet9 (WebAssemblyStandalone App)

          • Introductory example with an input box and button. Users can type a prompt into the input field and hit the button to get a response

          • This first application uses the HttpClient technique which calls the OpenAI endpoint (we determined this endpoint in the introductory steps above)

          • The key here is we need to know the format of the "Response" from the API just like we have done before when accessing other data service APIs

            • Go to API Reference (top right menu) ... Chat Completions (left side menu) ... Create Chat Completion

            • I copied the JSON response (bottom right side display)  and then returned to my  application, created a Models page, added a class called ChatResponse and performed     Paste special -> Paste JSON as classes

              Below is the FULL response ... BUT I only selected some of the Key elements (properties) from each class

              ChatbotResponse .... public Choice[] choices {get;set;}

              Choice ... public Message message {get;set;}

              Message ... public string role {get;set;} and public string content {get;set;}


              //public class ChatbotResponse

              //{

              //    public string id { get; set; }

              //    public string _object { get; set; }

              //    public int created { get; set; }

              //    public string model { get; set; }

              //    public string system_fingerprint { get; set; }

              //    public Choice[] choices { get; set; }

              //    public string service_tier { get; set; }

              //    public Usage usage { get; set; }

              //}


              //public class Usage

              //{

              //    public int prompt_tokens { get; set; }

              //    public int completion_tokens { get; set; }

              //    public int total_tokens { get; set; }

              //    public Completion_Tokens_Details completion_tokens_details { get; set; }

              //}


              //public class Completion_Tokens_Details

              //{

              //    public int reasoning_tokens { get; set; }

              //    public int accepted_prediction_tokens { get; set; }

              //    public int rejected_prediction_tokens { get; set; }

              //}


              //public class Choice

              //{

              //    public int index { get; set; }

              //    public Message message { get; set; }

              //    public object logprobs { get; set; }

              //    public string finish_reason { get; set; }

              //}


              //public class Message

              //{

              //    public string role { get; set; }

              //    public string content { get; set; }


              //    // used to instruct the JSON serializer to ignore the marked property

              //    // when serializing or deserializing JSON data.

              //    [JsonIgnore]

              //    public bool IsUser => role == "user";

              //}

          • ChatGPTpage

            • All the remaining key coding is done in this razor component ... no Services or appsettings.json are used. (I highlight some more  advanced applications in the next lecture which implement these techniques)

            • Again one of the key concepts is how to talk to the OpenAI API ... the clue is here:

              • Go to API Reference (top right menu) ... Chat (left side menu) ... Create Chat Completion

                • Look at the format of the JSON Request ... we will use a slightly modified version of this written in C#

                  completion = client.chat.completions.create(

                    model="gpt-4o",

                    messages=[

                      {"role": "developer", "content": "You are a helpful assistant."},

                      {"role": "user", "content": "Hello!"}

                    ]

                  )

                  print(completion.choices[0].message)

                • Notice all the Request parameters that are available. We will use Temperature.

            • Let's start off with the code section and some simple declarations

              @code {

                  private string userPrompt=string.Empty;

                  private string? responseText;

            • Now let's go up and set up our UI in the HTML section
              @page "/chatgptpage"

              @using System.Text.Json

              @using System.Text

              @inject HttpClient Http


              <h3>ChatGPT API Integration</h3>


              <div class="mb-3">

                  <label for="userInput">Enter your Question</label>

                  <input @bind="userPrompt" id="userInput" class="form-control"/>

                  <button @onclick="SubmitPrompt" class="btn btn-primary mt-3">@buttonText</button>

              </div>


              @if (responseText !=null)

              {

                  <div class="alert alert-info">

                      <p>@responseText</p>       

                  </div>

              }

            • Finally we focus on the remaining code

              private readonly string apiKey = "yourKey";


              private async Task SubmitPrompt()

              {

                  buttonText = "Sending ... ";


                  //Sending a Request to the ChatGPT API endpoint for chat completions

                  var url = "";


                  //The requestData object specifies:

                  //the model (gpt-4o-mini),

                  //the role of each message (system/user)

                  //and the user's input

                  //The system message is used to define ChatGPT's role as a helpful assistant

                  //temperature is set to 0.7 to control the randomness of responses

                  //... lower values are more deterministic, higher values make the responses more creative


                  //Key ideas came from the OpenAI API documentation (See Request Body)

               


                  var requestData = new

                  {

                      model = "gpt-4o-mini",

                      messages = new[]

                      {

                          new{role="system",content="Your are a helpful assistant"},

                          new{role="user",content=userPrompt}

                      },

                      temperature=0.7

                  };       


                  //Here we are adding an Authorization header to the HTTP request message.

                  //The header includes a Bearer token, which is a type of access token used for authentication.

                  //The token is stored in the apiKey variable.

                  //...and finally we serialize the request data as JSON file using UTF-8 (Unicode Transformation Format - 8-bit)

                  //which is a variable-width character encoding used for electronic communication.

                  // It can represent every character in the Unicode character set.

                  var requestMessage = new HttpRequestMessage(HttpMethod.Post, url);

                  requestMessage.Headers.Add("Authorization", $"Bearer {apiKey}");

                  requestMessage.Content = new StringContent(JsonSerializer.Serialize(requestData), Encoding.UTF8, "application/json");


                  try

                  {

                      //Send the request to the OpenAI API

                      var response = await Http.SendAsync(requestMessage);


                      //if the request succeeds we use ReadAsStringAsync() to serialize the content to a string

                      //then we parse the response (Deserialize) using the ChatResponse class

                      //and display the first response message from the choices array

                      //This array contains possible responses from ChatGPT and we show the one

                      //with the highest likelihood

                      if (response.IsSuccessStatusCode)

                      {

                          var responseContent = await response.Content.ReadAsStringAsync();

                          var completion = JsonSerializer.Deserialize<ChatResponse>(responseContent);

                          responseText = completion.choices[0].message.content;

                      }

                      else

                      {

                          responseText = " Error: " + response.StatusCode;

                      }

                  }

                  catch (Exception ex)

                  {

                      responseText = "An error occured: " + ex.Message;

                  }

                  finally

                  {

                      buttonText = "Send";

                  }

                  //This block is executed after the try block and any associated catch blocks,

                  // regardless of whether an exception was thrown or caught.

                  // It is used for cleanup code that must run no matter what.

        • Test application with various inputs

          • note that the response is not nicely formatted ... here's the fix

            • BlazorAInet9HTMLupdate

            • var requestData = new

              {

                   model = "gpt-4o-mini",

                   messages = new[]

                   {

                       new{role="system",content="Your are a helpful assistant" + "format every response in HTML"},

                       new{role="user",content=userPrompt}

                   },

                   temperature=0.7

              };   

            • @if (responseText !=null)

              {

                  <div class="alert alert-info">

                      <p>@((MarkupString)responseText)</p>       

                  </div>

              }

    • Supplementary Demos

      • BlazorAIwasmImagesHttpClientTechn/BlazorAIwasmImagesSpeechHttpClientTechnUpdate

        • This example reviews and extends our knowledge of connecting to the OpenAI API using Blazor WebAssembly and the HttpClient Technique. In this demo we implemented a Service (OpenAIServices) to handle all the API calls

          • One to handle the Text Response  GenerateTextAsync and one to handle the Image Generation GenerateImageAsync and one to handle Text to Speech Generation ConvertTextToSpeechAsync

          • Then we used the service in our Blazor component (Home) to generate the required responses from the OpenAI API.

        • Also note that we did not create any classes to represent the API text response,instead using
          var json = await response.Content.ReadFromJsonAsync<JsonElement>();

          json.GetProperty("choices")[0].GetProperty("message").GetProperty("content").GetString();

          • Since JsonElement is used, the code does not rely on a predefined class like ChatResponse. Instead, it manually accesses properties in the JSON response using methods like GetProperty().

            and then simply storing the response in a string on the Home page.

        • ... Also, Check out the AI Image  and Text to Speech Links as an additional Enrichment demo

          • For the OpenAI image generation API, you do not need to specify the model in the request body.

            The model is implicitly defined by the endpoint you are using.

            The  endpoint is specifically for generating images, and it does not require a model parameter.

            • var requestBody = new

              {

                   prompt = prompt,

                   n = 3, //number of images to generate

                   size = "512x512"

              };

              var requestContent = new StringContent(JsonSerializer.Serialize(requestBody), Encoding.UTF8, "application/json");
              requestContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
              httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _apiKey);
              var response = await _httpClient.PostAsync("", requestContent);

              var json = await response.Content.ReadFromJsonAsync<JsonElement>();

              if (json.TryGetProperty("data", out var dataArray) && dataArray.ValueKind == JsonValueKind.Array)

              {


                  //Extracts the image URLs from the JSON response and returns them as a LIST of strings.

                  var imageUrls = dataArray.EnumerateArray()

                      .Select(img => img.GetProperty("url").GetString())

                      .Where(url=> !string.IsNullOrEmpty(url))

                      .ToList();


                  return imageUrls;

              }

          • For the Text to Speech Generation again you do specify a model in the requestBody plus we reference the endpoint in our Post request.

            The endpoint is specifically for generating speech, and it does not require a model parameter.

            • var requestBody = new

              {

                  model = "tts-1",

                  input = text,

                  voice = voice

              };

              var jsonBody = JsonSerializer.Serialize(requestBody);
              _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _apiKey);
              var content = new StringContent(jsonBody, Encoding.UTF8, "application/json");
              var response = await _httpClient.PostAsync("", content);

              var audioBytes = await response.Content.ReadAsByteArrayAsync();//Asynchronously reads the HTTP content and stores it in a byte array.

              var base64Audio = Convert.ToBase64String(audioBytes);          //Converts the byte array to a base64-encoded string.

              var audioUrl = "data:audio/mp3;base64," + base64Audio;         //Constructs a data URL for the audio file.

              return audioUrl;

            • Note the use of Voices in the SpeechPage.razor (we use a drop down list ... select) to display all the choices
              //

              //This list is used to populate the dropdown list of available voices

                  private readonly List<string> AvailableVoices = new List<string> { "alloy", "echo", "fable","onyx","nova","shimmer","ash","sage","coral" };   

            • Note the use of Javascript to play the AudioUrl
              //fetch the new speech URL

              AudioUrl = await OpenAIServices.ConvertTextToSpeechAsync(TextInput,SelectedVoice);
              await JSRuntime.InvokeVoidAsync("eval", "new Audio('" + AudioUrl + "').play();");

            • Note in the HTML section where we allow the user to download/save the audio generated
              @if (!string.IsNullOrEmpty(AudioUrl))

              {

                  @*Here the we download instead of link to href ... download location depends on browser default*@

                  <div class="text-center">

                      <a href="@AudioUrl" download="output.mp3" class="btn btn-success mt-3">Download Speech</a>

                  </div>

              }

      • BlazorHybridAIimagesHttpClientTechn-2026

        • .NET MAUI Blazor Hybrid App version of previous demo focusing on Image Generation

        • Uses a Tabbed Menu along the top (modified in MainPage.xaml)  with the link OpenAI Image API ... this a Native .MAUI page (NewPage2.xaml)  not a basic Razor

          • We use a bit of XAML to update the NewPage2.xaml and add some Text (Labels) , some Buttons, a Image container , an Entry container (like a text box) and a Switch.

          • Note the associated code-behind necessary to sense the Clicked events

          • We also added A Text to Speech command button which details the purpose of this page (Image Generation)

          • We have the Switch respond to a state change (Toggled) by making the loaded in image appear and disappear

          • Added ...VerticalOptions="Center" ...HorizontalOptions="Center" to VerticalStack and BackgroundColor to ContentPage to tweak appearance somewhat

            • <ContentPage
              BackgroundColor="LightBlue">

          • Added a new button to Close MAUI page if called Modally from Counter page ... does not work if page is simply accessed via Tabbed Menu

            • Associated code behind for button uses command ... Navigation.PopModalAsync();

            • Code on Counter page uses ...
              App.Current.MainPage.Navigation.PushModalAsync(new Pages.NewPage2());

      • BlazorAInet9UsingOpenAInuget

        • A redo of today's Lecture demo application (BlazorAInet9) using the OpenAI .NET Library


        • The OpenAI .NET library provides convenient access to the OpenAI REST API from .NET applications. It implements a number of classes/methods which reduce the coding necessary to send and receive responses from the OpenAI API.

          • First we must install the OpenAI NuGet Package

        • Next we store the OpenAI Key in a class called Constants.cs

          • public class Constants

            {

                public const string OpenAIKey = "yourKey";

            }   

        • The rest of the coding is in the razor component OpenAInuget.razor

          • @page "/openainuget"

            @using OpenAI.Chat 


            <h5>Implementing the OpenAI .NET Library (NuGet Package)</h5>


            <div class="mb-3">

                <label for="userInput">Enter your Question</label>

                <input @bind="userPrompt" id="userInput" class="form-control" />

                <button @onclick="SubmitPrompt" class="btn btn-primary mt-3">@buttonText</button>

            </div>


            @if (responseText != null)

            {

                <div class="alert alert-info">

                    <p>@((MarkupString)responseText)</p>

                </div>

            }


            @code {


                private string userPrompt = string.Empty;

                private string? responseText;

                private string buttonText = "Send to ChatGPT";


                private async Task SubmitPrompt()

                {

                    buttonText = "Sending ... ";


                    if (!string.IsNullOrEmpty(userPrompt))

                    {

                        var modelName = "gpt-4o-mini";

                        var client = new ChatClient(modelName, Constants.OpenAIKey);

                        var response = await client.CompleteChatAsync(userPrompt + " format every response in HTML");

                        string answer = response.Value.Content[0].Text;

                        responseText = answer;

                        buttonText = "Send to ChatGPT";

                    }

                    else

                    {

                        buttonText = "Send to ChatGPT";

                    }

                }

            }

      • BlazorAInet9UsingOpenAInugetEnhancedUpd

        • This updated version keeps track of our entire conversation with ChatGPT. This way the Bot has a frame of reference if a followup question is asked.

          ... for instance ... What is the tallest building in the world? ... whats the next?

        • Key updates in Code Section

          • //The ChatMessage class is typically used to represent a message in a chat conversation.

            // In the context of the OpenAI .NET library, it is used to structure the messages exchanged with the OpenAI API.

            // Each ChatMessage object usually contains information about the sender (e.g., user or assistant)

            // and the content of the message.

            List<ChatMessage> chatMessages = new List<ChatMessage>();   

          • private async Task SubmitPrompt()

            {

                buttonText = "Sending ... ";



                if (!string.IsNullOrEmpty(userPrompt))

                {

                    // Add the User's message to the chatMessages list so we can keep track of the conversation history.

                    // This helps the OpenAI API to maintain context across multiple interactions.

                    // Input messages from the caller are typically paired with the assistant's responses.... See below

                     chatMessages.Add(new UserChatMessage("Make sure to format every response in HTML"));       
                      chatMessages.Add(new UserChatMessage(userPrompt));

                               

                    var modelName = "gpt-4o-mini";

                    var client = new ChatClient(modelName, Constants.OpenAIKey);

                   

                    // Send the entire conversation to the OpenAI API ... instead of just current userPrompt

                    var response = await client.CompleteChatAsync(chatMessages);   

                    //var response = await client.CompleteChatAsync(userPrompt + " format every response in HTML");

                   

                    string answer = response.Value.Content[0].Text;

                    chatMessages.Add(new AssistantChatMessage(answer));
            // Add the Assistant's response to the chatMessages list

            // Input messages from the caller are typically paired with the assistant's responses.

            // This way the OpenAI API can maintain context across multiple interactions.

                    responseText += "<b>ChatGPT:</b>" + answer + "<br><br>";

                    buttonText = "Send to ChatGPT";

                }

                else

                {

                    buttonText = "Send to ChatGPT";

                }


            }

      • BlazorAutoGenOpenAInugetNBA

        • NBA Teams (History) is a new menu option in this application, designed to showcase the power of AI-generated content. Our .NET App Generates Content for itself via AI.

          • AI-Powered Summaries: The application uses advanced AI to generate detailed histories for NBA teams, providing unique and insightful content.

          • JSON Data Storage: All initial NBA team data is stored in a JSON file, structured according to the GameItem.cs model. This ensures data consistency and easy updates.

          • Service Architecture:

            • Service folder contains the IOpenAIService interface and its implementation OpenAIService, which handles communication with the AI backend. We are using the OpenAI nuget package for this purpose.

            • GameItemService is responsible for loading and updating the NBA teams data from the JSON file.

          • Main Page: The GameItems.razor page displays all NBA teams and allows users to request an AI-generated history for any team.

          • Team History Modal: The generated history is shown in a non-routable component called ItemsDescriptionModal, which receives parameters to display the selected team's details.

  • Integrating the OpenAI API into a Blazor Application ... Part 243:11

    In this Lecture we will

    • Extend our AI implementation skills through a series of Hands On and Demo applications

    • BlazorChatbotAInet9 (demo)

      • Advanced version of a Chatbot specifically used to answer questions related to C# programming and only C# programming.

      • Note how the class ChatbotResponse in the Models folder was developed

        • copied JSON response from OpenAI API (Paste special ... Paste JSON as classes)


        • A second class called ChatOptions is also added to the Models folder

          • public class ChatOptions

            {

                public string ApiKey { get; set; } 

                public string ApiUrl { get; set; } 

                public string GtpModel { get; set; }   

            }

      • We store our key values in the file appsettings.json in the  wwwroot folder
        {

          "OpenAI": {

            "ApiKey": "yourKey",

            "ApiUrl": "",

            "GtpModel": "gpt-4o-mini"

          }

      • This application still uses the HttpClient technique but puts most of the coding in a Service (class) called OpenAIService in the Services folder
        public class OpenAIService

        {


            private readonly HttpClient _httpClient;

           

            private readonly IOptions<ChatOptions> _options;

            //IOptions<T> is a feature in ASP.NET Core used for accessing configuration

            //settings. It provides a way to retrieve configuration values that are bound to a specific class.

            //Recall that ChatOptions is a class that holds configuration settings for the ApiKey,ApiUrl and GtpModel. 

           

            //The actual values are stored in the appsettings.json file.   

            //In the Program.cs we have ChatOptions refer to the OpenAI (key) section in the

            //appsettings.json configuration file. 

            //builder.Services.Configure<ChatOptions>(o => builder.Configuration.GetSection("OpenAI").Bind(o));


            //Class constructor that initializes a new instance of the class

            //with the required HttpClient and IOptions<ChatOptions> objects. 

            public OpenAIService(HttpClient httpClient,IOptions<ChatOptions> options)

            {

                _httpClient = httpClient;

                _options = options;           

            }


            //Note how CreateChatCompletion method is used in the ChatBot.razor.cs file

            //private void AddUserQuestionToConversation()

            //{

            //    _conversationHistory.Add(new Message { role = "user", content = _userQuestion });

            //}


            //private async Task CreateCompletion()

            //{

            //    _isSendingMessage = true;

            //    var assistantResponse = await OpenAIService.CreateChatCompletion(_conversationHistory);

            //    _conversationHistory.Add(assistantResponse);

            //    _isSendingMessage = false;

            //}

            public async Task<Message>CreateChatCompletion(List<Message>messages)

            {

                //Method used in BlazorAInet9 app (from previous Lecture)

                //var requestData = new

                //{

                //    model = "gpt-4o-mini",

                //    messages = new[]

                //{

                //    new{role="system",content="Your are a helpful assistant"},

                //    new{role="user",content=userPrompt}

                //},

                //    temperature = 0.7

                //};

                //---------------------------------------------------------------------------------

                var request = new { model = _options.Value.GtpModel, messages = messages.ToArray() };


                //Method used in BlazorAInet9 app (from previous Lecture)

                //var requestMessage = new HttpRequestMessage(HttpMethod.Post, url);

                //requestMessage.Headers.Add("Authorization", $"Bearer {apiKey}");

                //requestMessage.Content = new StringContent(JsonSerializer.Serialize(requestData), Encoding.UTF8, "application/json");

                //var response = await Http.SendAsync(requestMessage);

                //---------------------------------------------------------------------------------

                _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _options.Value.ApiKey);

                var response = await _httpClient.PostAsJsonAsync(_options.Value.ApiUrl, request);


                //EnsureSuccessStatusCode throws an exception if the IsSuccessStatusCode property for the HTTP response is false.

                response.EnsureSuccessStatusCode();


                //Method used in BlazorAInet9 app (from previous Lecture)

                //var chatCompletionResponseString = await response.Content.ReadAsStringAsync();

                //var chatCompletionResponse = System.Text.Json.JsonSerializer.Deserialize<ChatbotResponse>(chatCompleteResponseString);

                //---------------------------------------------------------------------------------

                var chatCompletionResponse = await response.Content.ReadFromJsonAsync<ChatbotResponse>();


                //return chatCompletionResponse?.choices[0].message;   

                return chatCompletionResponse?.choices.First().message;

            }


        }

      • Next we declare the Service in Program.cs
        / this line ensures that the settings defined under the "OpenAI" section in your configuration files

        // are loaded into an instance of the ChatOptions class, which can then be injected and used throughout

        // your application. This is useful for centralizing and managing configuration settings in a structured way.

        builder.Services.Configure<ChatOptions>(o => builder.Configuration.GetSection("OpenAI").Bind(o));

        builder.Services.AddScoped<OpenAIService>();

      • Now we focus on the main UI page ... ChatBot.razor/ChatBot.razor.cs

        • @page "/"   


          <div class="container d-flex flex-column vh-100">

             

              @*Only display the Opening Prompt at the start when no questions have been asked *@

              @if (Messages.Count==0)

              {

                  <div class="text-center">

                          <h3>Welcome to the Chat Bot</h3>

                          <p>I'm your C# learning assistant ... ask me anything</p>

                   </div>

              } 

             

              @*Cycle through all the questions and responses

                  The user's questions are displayed in a blue long oval bubble with a person image on the left

                  The bot's responses are displayed in a white long oval textarea with a robot image on the left

                  The Messages (list) is a filtered _conversationHistory list that contains the user's questions and the bot's responses

              *@

              <div class="flex-grow-1">


                  @foreach (var message in Messages)

                  {

                      <div class="row p-2">

                          <div class="col-1 rounded-circle bg-dark-subtle text-center m-2 p-0" style="width:50px;height:50px;">

                              <span class="bi fs-2 @(message.IsUser ? "bi-person-fill": "bi-robot")" ></span>

                          </div>

                          <div class="col-11 rounded p-3 shadow-sm @(message.IsUser ? "text-bg-primary" : "text-bg-light")">

                             @if(message.IsUser)

                             {

                                 @message.content

                             }

                             else

                             {

                                 @( (MarkupString)message.content)

                             }

                          </div>

                      </div>


                      @*MarkupString forces Blazor to render the string as HTML rather than encoding it as plain text.

                      This is useful when you want to include HTML content in your Blazor components.*@

                  }


              </div>


              @*The sticky-bottom class ensures that the input area is always at the bottom of the screen

                The input area consists of a button to clear the conversation, a textarea to enter the user's question

                and a button to send the question to the bot*@   

              <div class="sticky-bottom w-50 p-3 rounded mx-auto">

                 

                  @if (_isSendingMessage)

                  {

                      <div class="text-center">

                          <div class="spinner-border spinnder-border-sm" role="status">

                              <span class="visually-hidden">Loading ...</span>

                          </div>

                      </div>

                  }

                  <div class="input-group">       

                      <button class="btn btn-primary" @onclick="ClearConversation">

                          <span class="bi bi-eraser"></span>

                      </button>       

                      <textarea rows="2" class="form-control" placeholder="Ask me anything ..."

                      @bind-value="_userQuestion" @bind-value:event="oninput" @onkeypress="HandleKeyPress"

                      disabled="@_isSendingMessage">

                      </textarea>

                  </div>


              </div>


          </div>

        • using BlazorChatbotAInet9.Models;

          using BlazorChatbotAInet9.Services;

          using Microsoft.AspNetCore.Components;

          using Microsoft.AspNetCore.Components.Web;

          using System.Security.Principal;

          using System.Text.Json.Serialization;


          namespace BlazorChatbotAInet9.Pages

          {

              public partial class ChatBot

              {

                  private string _userQuestion = "";

                  private readonly List<Message> _conversationHistory = new List<Message>();

                  private bool _isSendingMessage;

                  private readonly string _chatBotKnowledgeScope = "" +

                     "Your name is CsharpBot, You are an assistant that help users learn C#." +

                     "When user's question is not related to C# or the .NET framework, reply politely that you can not answer" +

                     "format every response in HTML.";


                  //The _chatBotKnowledgeScope field above defines the initial context and behavior for the chatbot.

                  //It sets the chatbot's identity and scope of knowledge. Specifically, it instructs the chatbot to:

                  //1. Identify itself as "CsharpBot".

                  //2. Act as an assistant that helps users learn C#.

                  //3. Politely decline to answer questions that are not related to C# or the .NET framework.

                  //4. Format every response in HTML.


                  protected override Task OnInitializedAsync()

                  {

                      _conversationHistory.Add(new Message { role="system",content=_chatBotKnowledgeScope});

                       return Task.CompletedTask;

                  }


                  //The purpose of this property is to provide a list of messages that excludes any messages with the role "system".

                  //This is useful for displaying only user and assistant messages in the chat interface,

                  //ignoring any system messages that might be used for internal purposes.


                  public List<Message> Messages => _conversationHistory.Where(c=>c.role is not "system").ToList();


                  //Recall the Message class from the ChatbotResponse.cs file

                  //public class Message

                  //{

                  //    public string role { get; set; }

                  //    public string content { get; set; }


                  //    // used to instruct the JSON serializer to ignore the marked property

                  //    // when serializing or deserializing JSON data.

                  //    [JsonIgnore]

                  //    public bool IsUser => role == "user";

                  //}


                  [Inject]

                  public OpenAIService OpenAIService { get; set; }


                  private async Task HandleKeyPress(KeyboardEventArgs e)

                  {

                      if (e.Key is not "Enter") return;

                      await SendMessage();   

                  }


                  private async Task SendMessage()

                  {

                      if (string.IsNullOrWhiteSpace(_userQuestion)) return;

                      AddUserQuestionToConversation();

                      StateHasChanged();

                      await CreateCompletion();

                      ClearInput();

                      StateHasChanged(); 

                  }


                  private void AddUserQuestionToConversation()

                  {

                      _conversationHistory.Add(new Message { role="user", content = _userQuestion });

                  }

                 

                  private async Task CreateCompletion()

                  {

                      _isSendingMessage = true;

                      var assistantResponse= await OpenAIService.CreateChatCompletion(_conversationHistory);

                      _conversationHistory.Add(assistantResponse);

                      _isSendingMessage = false;

                  }


                  private void ClearInput()

                  {

                      _userQuestion = "";

                  }   


                  private void ClearConversation()

                  {

                      ClearInput();

                      _conversationHistory.Clear();   

                  }

              }

          }

      • Note the update to the MainLayout.razor page which has removed the reference to the NavMenu.razor ... so it will default to "/" ... this page directive has been added to the ChatBot.razor page
        @inherits LayoutComponentBase

        <main>

            @Body

        </main>

    • BlazorWASMAppOpenAI (.NET 8 WebAssembly Standalone App)

      • Blazor WASM Application using the Azure OpenAI NuGet package

        • Simple "Tourist Recommendation" application that prompts for the input of  a City and then allows you choose between Restaurants,Hotels or Attractions.

      • Lets first add an appsettings.json file to the wwwroot of the project. This will automatically be picked up by Blazor. The content should look like this.
        {

          "OpenAIKey": "yourKey",

          "OpenAIEndpoint": null

        }

      • Next we install the Azure.AI.OpenAI nuget Package (use 1.0.0-beta.8)

      • Now create a Services folder and create a class called OpenAIService

        • using Azure.AI.OpenAI;

          using Azure;

          namespace OpenAIRecommendationAppBlazor.Services;

          public class OpenAIService

          {

              //HttpClient vs OpenAIClient   

             

              //HttpClient is a more general-purpose class for sending HTTP requests and receiving HTTP responses from a resource identified by a URI.

          //Can be used to interact with any web service or API by manually constructing HTTP requests.

          //Highly flexible, but requires more boilerplate code to handle specific API interactions, including setting headers,

              //handling authentication, and parsing responses.


              //OpenAIClient is a class provided by the Azure.AI.OpenAI library

              //and is specifically designed to interact with OpenAI's API services.

              //Provides higher-level abstractions for OpenAI-specific operations like generating completions, chat completions, etc.

              //Simplifies the process of making requests to OpenAI by encapsulating the details of the

              //API endpoints and request/response handling.


              OpenAIClient client;

             

              //static readonly char[] trimChars = new char[] { '\n', '?' };


              //This method is responsible for initializing the OpenAIClient instance, which is used to interact with OpenAI's API services

              //OpenAIService is initialized with an API key and a custom endpoint.

              //If the custom endpoint is not needed, we just use the API key.

             

              //This Initialize method is called from the Program.cs file to set up the OpenAIService instance before running the Blazor app.

              //Program.cs is the entry point for the Blazor WebAssembly application and is responsible for setting up the app's configuration and services.

             

              // Set up the OpenAI client

              //OpenAIService svc = new OpenAIService();

              //builder.Services.AddSingleton<OpenAIService>(svc);

              //var app = builder.Build();

              //var config = builder.Configuration;

              //var openAIKey = config["OpenAIKey"]!;

              //var openAIEndpoint = config["OpenAIEndpoint"];

              //svc.Initialize(openAIKey, openAIEndpoint);

              //await app.RunAsync();


              public void Initialize(string openAIKey, string? openAIEndpoint = null)

              {

                  client = !string.IsNullOrWhiteSpace(openAIEndpoint)

                      ? new OpenAIClient(

                          new Uri(openAIEndpoint),

                          new AzureKeyCredential(openAIKey))

                      : new OpenAIClient(openAIKey);

              }


              //This Task is called from the TouristPage.razor file to generate a chat completion based on the user's input.

              //async void OnSubmit()

              //{

              //    responseText = "Working on it...This can take a little while based on the model selected.";

              //    var message = await openAIService.CallOpenAIChat(recommendation.RecommendationType, recommendation.City);

              //responseText = message;

              //    this.StateHasChanged();

              //}


              internal async Task<string> CallOpenAIChat(string recommendationType, string location)

              {

                  //This is the prompt we want to send to the OpenAI API ...

                  //for example ... Give me 5 recommendations for "restaurant" near "New York" format every response in HTML

                  string prompt = GeneratePrompt(recommendationType, location);

                 

                  ChatCompletionsOptions options = new ChatCompletionsOptions();


                  //options.ChoiceCount = 1;


                  //Recall from BlazorAInet9 example how we set up our requestData object

                  //var requestData = new

                  //{

                  //    model = "gpt-4o-mini",

                  //    messages = new[]

                  //    {

                  //        new{role="system",content="Your are a helpful assistant" + "format every response in HTML"},

                  //        new{role="user",content=userPrompt}

                  //    },

                  //    temperature = 0.7

                  //};


                  //Here is how we set up your request data using Azure.AI.OpenAI

                  options.Messages.Add(new ChatMessage(ChatRole.User, prompt));

                  var response = await client.GetChatCompletionsAsync("gpt-4o-mini",options);


                  // StringWriter is used to accumulate the responses from the OpenAI API into a single string,

                  // which is then returned by the CallOpenAIChat method.

                  StringWriter sw = new StringWriter();


                  foreach (ChatChoice choice in response.Value.Choices)

                  {

                      //var text = choice.Message.Content.TrimStart(trimChars);

                      var text = choice.Message.Content;

                      sw.WriteLine(text);

                  }

                  var message = sw.ToString();

                  return message;

              }


              private static string GeneratePrompt(string recommendationType, string location)

              {

                  //return $"What is a recommendation for  {recommendationType} near {location}";

                  //return "What is a recommendation for  " + recommendationType + " near " + location;

                  //return "Give me 5 recommendations for " + recommendationType + " near " + location + " format every response in HTML";

                  return "Give me recommendations for " + recommendationType + " near " + location + " format every response in HTML";   

              }

          }

      • We'll register the OpenAI service we just created. This is done in Program.cs.

        • Replace the builder.Build().RunAsync call with the following, the config isn't available until the builder is built, so we register the service then initialize it based on the config once it's available.

        • // Set up the OpenAI client

          OpenAIService svc = new OpenAIService();

          builder.Services.AddSingleton<OpenAIService>(svc);


          var app = builder.Build();

          var config = builder.Configuration;

          var openAIKey = config["OpenAIKey"]!;           //gets key from appsettings.json

          var openAIEndpoint = config["OpenAIEndpoint"];  //get endpoint from appsettings.json   

          svc.Initialize(openAIKey, openAIEndpoint);      //calls Initialize method (in Service-OpenAIService) to set up OpenAIService instance

          await app.RunAsync();

      • ... and lastly we create a simple UI called TouristPage.razor ( use TouristPageCodeSnippet.txt )

        • @page "/touristpage"


          @using OpenAIRecommendationAppBlazor.Services;

          @using System.ComponentModel.DataAnnotations;

          @inject OpenAIService openAIService;


          <PageTitle>Index</PageTitle>


          <h1>Local AI Recommendations</h1>


          <EditForm OnValidSubmit="OnSubmit" Model="@recommendation">

              <DataAnnotationsValidator />

              <ValidationSummary />


              <div class="form-group row">


                  <div class="form-group col-5">

                      <label>City</label>

                      <InputText @bind-Value="@recommendation.City" class="form-control"></InputText>

                  </div>


                  <div class="form-group col-5">

                      <label>I'd like a recommendation for ...</label>

                      <InputSelect @bind-Value="@recommendation.RecommendationType" class="form-select">

                          <option value="">--Select--</option>

                          <option value="restaurant">Restaurants</option>

                          <option value="hotel">Hotels</option>

                          <option value="attractions">Attractions</option>

                      </InputSelect>

                  </div>


                  <div class="form-group col-2">

                      <button type="submit" class="btn btn-success mt-4">

                          Submit

                      </button>

                  </div>


              </div>


          </EditForm>


          @* <div>

              <p>@responseText</p>   

          </div> *@


          <div>

              <p>@((MarkupString)responseText)</p>

          </div>


          @code

          {

              Recommendation recommendation = new Recommendation();

              string responseText = "";


              async void OnSubmit()

              {

                  responseText = "Working on it...This can take a little while based on the model selected.";

                  var message = await openAIService.CallOpenAIChat(recommendation.RecommendationType, recommendation.City);

                  responseText = message;

                  this.StateHasChanged();

              }


              //Here we defined our class locally instead of in a separate file

              public class Recommendation

              {

                  [Required]

                  public string City { get; set; } = "";

                  [Required]

                  public string RecommendationType { get; set; } = "";

              }

          }

    • BlazorHybridOpenAI /BlazorHybridOpenAIappShellXAMLupdate (demo)

      • .NET MAUI Blazor Hybrid version of the Recommendation App using the Azure OpenAI package

        • ... as of Nov 2025 works on .NET 8 but not yet on .NET 9 ... so will not work with Visual Studio 2026

      • Key Highlights

        • No appsettings.json .... so we created a class called Constants.cs

          • //static means that the class and its members can be

            //accessed without creating an instance of the class.

            static internal class Constants

            {

                public const string OpenAIKey = "yourKey";

                public const string OpenAIEndpoint = null;

            }

        • We register the Service (OpenAIService) and call the Initialize method from MauiProgram.cs

          • builder.Services.AddTransient<MainPage>();

            OpenAIService svc = new OpenAIService();

            svc.Initialize(Constants.OpenAIKey, Constants.OpenAIEndpoint);

            builder.Services.AddSingleton<OpenAIService>(svc);

        • We added a xaml page called AppShell.xaml  which routes to the MainPage on startup and adds a Title

          • <Shell

                x:Class="OpenAIRecommendationAppMaui.AppShell"

                xmlns=""

                xmlns:x=""

                xmlns:local="clr-namespace:OpenAIRecommendationAppMaui"

                Shell.FlyoutBehavior="Disabled"

                Title="OpenAIRecommendationAppMaui"

                BackgroundColor="LightGrey">


                <ShellContent

                    Title="Chiarelli OpenAI API Demo"       

                    ContentTemplate="{DataTemplate local:MainPage}"

                    Route="MainPage" />

            </Shell>

        • Most of the UI is created in  MainPage.xaml with the code behind in MainPage.xaml.cs

          • <ContentPage xmlns=""

                         xmlns:x=""

                         x:Class="OpenAIRecommendationAppMaui.MainPage">


                <ScrollView

                    BackgroundColor="LightGray"

                    Padding="10"

                    VerticalScrollBarVisibility="Always"

                    HorizontalScrollBarVisibility="Never">

                   

                    <VerticalStackLayout

                        Padding="30,0"

                        Spacing="25">

                        <Label

                            Text="Local AI recommendations"

                            SemanticProperties.HeadingLevel="Level1"

                            FontSize="32"

                            HorizontalOptions="Center" />

                        <Entry

                            x:Name="LocationEntry"

                            Placeholder="Enter your location"

                            PlaceholderColor="Yellow"

                            SemanticProperties.Hint="Enter the location for recommendations"

                            HorizontalOptions="Center"/>

                        <Button

                            x:Name="RestaurantBtn"

                            Text="Get restaurant recommendations"

                            SemanticProperties.Hint="Gets restaurant recommendations when you click"

                            Clicked="OnRestaurantClicked"

                            HorizontalOptions="Center" />


                        <Button

                            x:Name="HotelBtn"

                            Text="Get hotel recommendations"

                            SemanticProperties.Hint="Gets hotel recommendations when you click"

                            Clicked="OnHotelClicked"

                            HorizontalOptions="Center" />


                        <Button

                            x:Name="AttractionBtn"

                            Text="Get attraction recommendations"

                            SemanticProperties.Hint="Gets attraction recommendations when you click"

                            Clicked="OnAttractionClicked"

                            HorizontalOptions="Center" />


                        <Label x:Name="SmallLabel"

                            Text="Click a button for recommendations!"

                            SemanticProperties.HeadingLevel="Level2"

                            FontSize="18"

                            HorizontalOptions="Center" />

                    </VerticalStackLayout>

                </ScrollView>


            </ContentPage>

          • using OpenAIRecommendationAppMaui.Services;


            namespace OpenAIRecommendationAppMaui

            {

                public partial class MainPage : ContentPage

                {

                    OpenAIService openAIService;

                    public MainPage(OpenAIService svc)

                    {

                        openAIService = svc;

                        InitializeComponent();

                    }

                    private async void OnRestaurantClicked(object sender, EventArgs e)

                    {

                        await GetRecommendation("restaurant");

                    }


                    private async void OnHotelClicked(object sender, EventArgs e)

                    {

                        await GetRecommendation("hotel");

                    }


                    private async void OnAttractionClicked(object sender, EventArgs e)

                    {

                        await GetRecommendation("attraction");

                    }


                    private async Task GetRecommendation(string recommendationType)

                    {

                        if (string.IsNullOrWhiteSpace(LocationEntry.Text))

                        {

                            await DisplayAlert("Empty location", "Please enter a location (city or postal code)", "OK");

                            return;

                        }

                        SmallLabel.Text = "Working on it...This can take a little while based on the model selected.";

                        var message = await openAIService.CallOpenAIChat(recommendationType, LocationEntry.Text);

                        SmallLabel.Text = message;

                    }

                }

            }

    • Supplementary Demos

      • BlazorAIsemanticKernel

        • This is a Blazor WebAapp .NET 9  (Render Mode Server) that uses the Microsoft Semantic Kernel SDK to connect to the OpenAI API.

          • Just when you thought the  Azure OpenAI NuGet package or the OpenAI .NET Library (from last lecture) made working with the OpenAI API easier than the HttpClient technique along comes the Microsoft Semantic Kernel SDK

        • Key Features and Implementation Steps

          • Install the Microsoft.SemanticKernel NuGet Package

          • Update Program.cs
            using Microsoft.SemanticKernel;

            var builder = WebApplication.CreateBuilder(args);

            builder.Services.AddKernel().AddOpenAIChatCompletion(modelId:"gpt-4o-mini",apiKey: "yourKey");

          • Add a simple class called ChatMessage in the Models folder
            public class ChatMessage

            {

                public string Text { get; set; }   

                public bool IsUserMessage { get; set; }

               
               //Constructor
               //isUserMessage is a boolean that indicates whether the message
               //is from the user (true) or the bot (false)

                public ChatMessage(string text, bool isUserMessage)

                {

                    Text = text;

                    IsUserMessage = isUserMessage;

                }

            }

          • KernelPage.razor Page

            • Code Section

              private string buttonText = "Send";

              private string progressWidth = "0%";

              private bool isLoading = false;

              private string userInput = "";

              private List<ChatMessage> chatMessages = new List<ChatMessage>();

              private string apiResponse = "";


              private async Task SendMessage()

              {

                  buttonText = "Sending...";

                  isLoading = true;

                  StateHasChanged();


                  try

                  {

                      //Add the user message to the chat messages list true indicates it's a user message
                       chatMessages.Add(new ChatMessage(userInput, true));


                      apiResponse = "";

                      int messageIndex = -1;


                      await foreach (var message in kernel.InvokePromptStreamingAsync<string>(userInput))

                      {

                          apiResponse += message.Replace("\n", @"<br>").Replace("\t", @"<tab>");

                         
                         //If the messageIndex is -1, it means that the current message is the first message        //from the API, so we add a new ChatMessage to the chatMessages list with the //apiResponse and set the IsUserMessage property to false to indicate that it is a bot //message. If the messageIndex is not -1, it means that the message is a continuation
                         
                         if (messageIndex == -1)

                          {

                              chatMessages.Add(new ChatMessage(apiResponse, false));

                              messageIndex++;

                          }

                          else

                          {

                              chatMessages.LastOrDefault().Text = apiResponse;

                          }


                          this.StateHasChanged();


                      }


                      userInput = "";


                  }

                  catch (Exception ex)

                  {

                      apiResponse = "API call failed: " + ex.Message;

                  }

                  finally

                  {

                      buttonText = "Send";

                      isLoading = false;

                      StateHasChanged();

                  }

              }

            • HTML Section
              @page "/kernelpage"


              @using Microsoft.SemanticKernel

              @inject Kernel kernel

              @rendermode InteractiveServer


              <PageTitle>Home</PageTitle>


              <h5>Using Microsoft Semantic Kernel SDK</h5>


              @*Note the reference to a number of classes ... check out KernelPage.razor.css

                  for the styling of these classes

              *@

              <div class="chat-container">

                  @foreach (var message in chatMessages)

                  {

                      <div class="chat-message @(message.IsUserMessage ? "user-message" : "bot-message")">

                          @((MarkupString)message.Text)

                      </div>

                  }

              </div>


              <div class="message-input">   

                 

                  <input disabled="@isLoading" type="text" @bind="userInput" placeholder="Type your message here..." />


              @*Lots going on in this one button ... lots of ternary operators

                  to set the class and the disabled attribute.

                  The button has a span for the text and a div for the loading bar

                  The loading bar is shown when isLoading is true

              *@

                  <button disabled="@string.IsNullOrEmpty(userInput)" class="@(string.IsNullOrEmpty(userInput)||isLoading? "disabled":"enabled")"

                          @onclick="SendMessage">

                      <span>@buttonText</span>

                      <div class="loading-bar @(isLoading ? "show":"")"></div>

                  </button>


                  <div class="loading-bar @(isLoading ? "show":"")" style="width:@progressWidth"></div>   


              </div>

            • Check out KernelPage.razor for css coding

      • BlazorKernelVisionWebAppNET9auto/BlazorKernelVisionWebAppUpdate... PreviewImgUpd

        • These series of applications are an extension to the previous one that introduced using the Kernel to perform a simple chat completion task.

        • This time, we will use the Kernel to Analyze Images using the OpenAI API Vision.

        • This is a Blazor Web App Application ... NET 9 Auto Render Mode.This application has a Server Project and WebAssembly Project ... but all the coding is located in the Server Project

        • Make sure to check out the references added to _Imports.razor and Program.cs. The rest of the coding is located in the KernelVisionServer page

          • @*Required for Vision Implementation*@

            @using Microsoft.SemanticKernel.ChatCompletion

            @using Microsoft.SemanticKernel.Connectors.OpenAI

          • using Microsoft.SemanticKernel;

            app.MapRazorComponents<App>()

                .AddInteractiveServerRenderMode()

                .AddInteractiveWebAssemblyRenderMode()

                .AddAdditionalAssemblies(typeof(BlazorKernelVisionWebApp.Client._Imports).Assembly);

        • BlazorKernelVisionWebAppNET9auto

          • This version analyzes a pre-set URL image or allows the user to use the InputFile component to pick any local image

          • Key Elements Code Section

            • ChatMessageContent reply;

              //This will hold the response from the API where //ChatMessageContent is from //Microsoft.SemanticKernel.AI.ChatMessages and is used

              //to represent the content of a chat message.


              // This is the method that gets triggered when the user selects an image

              private async Task OnFileChanged(InputFileChangeEventArgs e)

              {

                  isTooLarge = false;

                 

                  var file = e.File;

                  imageFileSize = file.Size;


                  if (imageFileSize > 65520) // This is the maximum size that the .NET implementation of Uri can handle

                  {

                      isTooLarge = true;                       

                  }


                  var buffer = new byte[512000]; //initialize buffer to 512000 bytes


                  if (file.Size > 512000) // This is a separate check for the file size just to display the image on screen

                  {

                      // The file is too large

                      resizedFile = await file.RequestImageFileAsync(file.ContentType, 300, 300);

                      buffer = new byte[resizedFile.Size];


                      //OpenReadStream max allowed 512000 bytes ... so we need to read in a reduced sized image file

                      var stream = await resizedFile.OpenReadStream().ReadAsync(buffer);

                  }

                  else

                  {

                      buffer = new byte[file.Size];

                      var stream = await file.OpenReadStream().ReadAsync(buffer);           

                  }


                  image = Convert.ToBase64String(buffer);

                 

                  imageBase64 = "data:" + file.ContentType + ";base64," + image;   

              }

               


              private async Task AnalyzeImage(int type)

              {

                  //Inject the ChatCompletionService to get the API response from the Kernel

                  var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();


                  //Prompt messages for the chat completion

                  string systemPrompt = "You are a friendly chatbot that helps me analyze images. I will show you an image and you will tell me what you see in it.";

                  string userPrompt = "Analyze the image and tell me what you see in it.";


                  //Create a new ChatHistory object with the system prompt

                  var chatHistory = new ChatHistory(systemPrompt);


                  //type 1 is for image URL and type 2 is for local image

                  if (type==1)

                  {

                      isImageURL = true;

                     

                      chatHistory.AddUserMessage(new ChatMessageContentItemCollection

                      {

                          new TextContent(userPrompt),

                          new ImageContent(new Uri(imageUri))                 

                      });

                  }

                  else

                  {

                      isImageURL = false;

                     

                      chatHistory.AddUserMessage(new ChatMessageContentItemCollection

                      {

                          new TextContent(userPrompt),

                          new ImageContent(imageBase64) //.NET implementation of Uri has a length limitation (65520 bytes) so it can only process small images.

                         

                       });

                  }   

                 

                  reply = await chatCompletionService.GetChatMessageContentAsync(chatHistory);

              }

            • Key Elements HTML

              • @*This button is used to Analyze an image using it's URL address ... which is hard coded as imageUri in the code section *@

                <button class="btn btn-primary me-3" @onclick="@(() => AnalyzeImage(1))" >Analyze URL Image</button>


                @*This button is used to load a local image file.

                  The standard InputFile component displays a simple button

                  ... by surrounding it with the label using the class btn we now have a nice bootstrap button  *@

                <label class="btn btn-primary">

                    Choose Local Image

                    <InputFile OnChange="OnFileChanged" accept="image/png,image/jpeg" style="display: none;" />

                </label>

              • @if (imageBase64 != null)

                {

                    @*Here we are displaying the local image that was selected by the user

                     ... But the .NET implementation of Uri has a length limitation (65520 bytes) so it can only process small images.*@

                    <p><strong>Selected Image:@imageFileSize</strong><i>bytes</i> ... NET implementation has a length limitation of 65520 bytes </p>

                    <img src="@imageBase64" alt="Selected Image" style="max-width: 300px; max-height: 300px;" />


                    @*If the image is too big to analyze disable the button *@

                    <button class="btn btn-primary" disabled="@isTooLarge" @onclick="@(() => AnalyzeImage(2))" style="margin-left: 10px;">Analyze Local Image</button>

                }


                @if (reply != null)

                {

                    <div class="alert alert-info mt-3">

                        <h5 >API Response:</h5>

                        @if(isImageURL)

                        {

                            <img src="@imageUri" alt="Selected Image" style="max-width: 300px; max-height: 300px;" />

                            <br />

                        }       

                        @reply

                    </div>

                }

        • BlazorKernelVisionWebAppUpdate

          • In this version we display a list of image links located in the wwwroot folder and allow the user to select (click) the image to analyze.

          • Key Updates Code Section

            //List of image files in the wwwroot/images directory

            List<string> fileList = new List<string>();

            //create a method to automatically load a set of images on program startup

            protected override async Task OnInitializedAsync()

            {       

                //C# lets you add the @ symbol in front of a string to create a

                //verbatim string literal where backslashes are not interpreted as escape characters

                //... so you can use them to specify file paths               

                string path = Directory.GetCurrentDirectory() + @"\wwwroot\images";

               

                //GetFiles ... returns an array of the full names (including paths) for the files in the specified directory

                //[0] = "C:\\Temp\\Blazor\\Vision\\4-BlazorKernelVisionWebAppUpdate\\BlazorKernelVisionWebApp\\BlazorKernelVisionWebApp\\wwwroot\\images\\beach.jpg"       

                var files = Directory.GetFiles(path);


                //GetFileName returns the file name and extension of the specified path string ... beach.jpg       

                foreach (var file in files)

                {

                    fileList.Add(Path.GetFileName(file));

                }

            }   

            //The ReadFile method is reading a file from the local directory (wwwroot/images) that you have clicked on from a list on screen

            //and it accepts a simple STRING parameter (beach.jpg) for instance


            //The OnFileChanged method is reading the file from ANYWHERE on the user's computer

            //The selected file is stored in the File property of the InputFileChangeEventArgs OBJECT, which is of type IBrowserFile

            //IBrowserFile is an interface . It provides properties and methods to access the file's metadata and content like Size, ContentType, OpenReadStream, etc.


            private void ReadFile(string file)

            {

                var path = Directory.GetCurrentDirectory() + @"\wwwroot\images\" + file;

                var buffer = File.ReadAllBytes(path);

                image = Convert.ToBase64String(buffer);

                imageBase64 = "data:image/jpeg;base64," + image;


                //determine the image file size

                FileInfo fi = new FileInfo(path);

                imageFileSize = fi.Length;


                isTooLarge = false;


                StateHasChanged();

            }   

          • Key Updates HTML
            <div class="col-md-3">

                @if (fileList.Count > 0)

                {           

                    <h5>Select Image </h5>

                    <span>wwwroot folder</span>

                    <ul style="list-style-type: none;padding:0px;margin:0px;">

                        @foreach (var file in fileList)

                        {

                            <li><span @onclick="@( () => ReadFile(file))" style="cursor:pointer; text-decoration:underline;color:blue">@file</span></li>                   

                            @*Here we are using a Lambda expression since we are calling a method and passing a parameter to it*@

                        }

                    </ul>

                }

            </div>

        • BlazorKernelVisionWebAppUpdatePreviewImgUpd

          • In this last version we display a list of image links + the actual images located in the wwwroot folder and allow the user to select (click) the image to analyze.

          • Key update

            <li class="mb-2"><span @onclick="@( () => ReadFile(file))" style="cursor:pointer; text-decoration:underline;color:blue"><img src="/images/@file" height="50" width="50" style="padding:5px" />@file</span></li>

      • BlazorSmartComponents

        • This is a simple application which uses the Smart Components NuGet Package to add 3 different AI powered features to a Blazor Web App (.NET 9 WebAssembly RenderMode)

          The 3 components are:

          • SmartTextArea ... Allows you to configure how text will be autocompleted including tone and URLs

          • SmartComboBox ...upgrades the traditional combobox by making suggestions based on semantic matching ... helps users find what they are looking for faster (as you type a value an autocompleting dropdown appears). The traditional combo box suggests values only based on eact substring matches, but SmartCombobox upgrades this by suggesting matches closely related to meanings.

          • SmartPasteButton ... fills out forms automatically using data pasted in from the user's clipboard ... it just knows where to put the data

        • Here are the Implementation Steps:

          • Smart Components work in .NET 6 or higher ... Here we are using .NET 9 Web App with Render Mode WASM ... you do need to have an ASP.NET Core Server so you cannot use a Blazor WASM Standalone App. This is because you need a server to hold your API keys.

          • In the Server project add the NuGet Package SmartComponents.AspNetCore

          • In the WebAssembly project add the NuGet Package SmartComponents.AspNetCore.Components (not required if you only have a Server project)

          • Update Program.cs in the Server Project with builder.Services.AddSmartComponents();

          • To specifically use SmartPaste and SmartTextArea you need to provide access to a language model backend ... so we need to Configure an OpenAI backend in the Server Project

            • Add the SmartComponents.Inference.OpenAI NuGet Package

            • In Program.cs update the call to AddSmartComponents ... builder.Services.AddSmartComponents().WithInferenceBackend OpenAIInferenceBackend ();

          • Using Azure OpennAI or OpenAI endpoints ... we need to Configure API keys in appsettings.json ... "SmartComponents":{"ApiKey":"your key","DeploymentName":"gpt-4o-mini","Endpoint":null}

        • Now you can use the Smart Components in your Blazor Web App

          • Smart Paste you simplying add SmartPasteButton DefaultIcon

          • Smart ComboBox ... SmartComboBox Url="api/accounting-categories" ... Note here you need to set up an API endpoint matching the Url Setting up the API endpoint

            • In the server's Program.cs file you add an array of candidates

            • In the server's Program.cs you add app.MapSmartComboBox("api/accounting-categories",request=>candidates)

            • This version is not very smart since it will always use the fixed suggestions (stored in the array)

            • ... Adding Semantic Matching by using embeddings ... a way of converting natural language strings into numerical vectors.

            • First we reference and register the embedder Service

            • Install the NuGet Package .. SmartComponents.LocalEmbeddings in the Server project

            • In Program.cs register it ... builder.Services.AddSingleton LocalEmbedder

  • Using Open Source Tools to Run Large Language Models in Blazor56:09

    In this Lecture we will

    • Learn about  an open-source tool called Ollama that simplifies running large language models (LLMs) locally on your computer, offering a user-friendly way to access and interact with powerful AI models without relying on cloud services.

      • Ollama acts as a local model manager and runtime, handling the complexities of downloading, installing, and managing LLMs, allowing users to focus on interacting with them

        • Why it's useful:

          • Local AI: It enables running LLMs directly on your own hardware, providing greater control over data and privacy.

          • Open-Source Focus: Ollama primarily works with open-source models, fostering transparency and customization.

          • Simplified Workflow: It streamlines the process, making it easier to get started and experiment with LLMs.

          • Cost-effective: Eliminates the need for cloud infrastructure, potentially saving on recurring costs.

        • Platform Support:

          Ollama supports macOS, Windows, and Linux.

    • Learn about Microsoft.Extensions.AI a set of core .NET libraries developed in collaboration with developers across the .NET ecosystem, including Semantic Kernel. These libraries provide a unified layer of C# abstractions for interacting with AI services, such as small and large language models (OpenAI,Azure,Ollama)

    • Create a simple Blazor Application that implements  basic Image Recognition functionality (Vision) BlazorImageRecognitionMSextensionsAI (NET 9 Blazor WebApp Interactive Render Mode Server) ... A redo a similar application from the previous Lecture Supplementary Demos that used the Microsoft Semantic Kernel to perform Image Analysis.

      • Leverage Microsoft.Extensions.AI library for image recognition tasks

        • A new package from Microsoft that provides the scaffolding and tools to build AI-powered applications without starting from scratch.

        • Think of Microsoft.Extensions.AI as the evolution of the Microsoft. SemanticKernel Abstractions package that we highlighted in the Supplementary Demos in the last Lecture.

        • The .NET AI Chat template (See Lecture 207 for full details) was built using the Microsoft.Extensions.AI

      • Integrate Ollama in our .NET application to process visual data using a local LLM (llava) eliminating the need for cloud API calls while maintaining privacy and control over our data.

      • First we set up the development environment

        • install Microsoft.Extensions.AI  (NuGet Package)

        • install Microsoft.Extensions.AI.Ollama  (NuGet Package)

          • this library will allow us to interact with an Ollama model we will install locally

        • install Ollama (free open source tool) ... then

          • note all the models supported ... filter based on Vision

          • We are going to use the LLaVA model. It  is a novel end-to-end trained large multimodal model that combines a vision encoder and Vicuna for general-purpose visual and language understanding....

          • we are going to pick the 7b version that has a size of 4.7 GB

          • install model 

            • open up the console  (in windows enter "cmd")

            • We use the command ollama pull llava:7b to download the model from the Ollama Library

            • since we have Ollama installed locally we simply type  ollama run llava:7b to start an interactive chat session with the model

      • Now it's time to add this model to our application ... we do that in the Program.cs file

        • using Microsoft.Extensions.AI;
          // Add Ollama AI service to the container.

          builder.Services.AddChatClient(new OllamaChatClient(new Uri("http://localhost:11434"), "llava:7b"));

          • The port number 11434 is the specific port on which the Ollama AI service is listening for incoming HTTP requests.

          • The Blazor application will communicate with the Ollama AI service at this address to perform tasks such as chat operations using the specified AI model ("llava:7b").

      • Next we add an images folder in the wwwroot folder and add a number of high quality png/jpg files

        • See images.rar in the Resources or use your own

      • All the remaining coding will occur in the Home.razor page (removing all original code)

        • we start off by adding some required declarations

          @rendermode InteractiveServer


          @using Microsoft.Extensions.AI;

          @inject IChatClient ChatClient


          @*necessary declaration... allows us to use Directory and Path commands in the code section*@

          @using System.IO

        • We are going to have two methods to pick images ... one method will display on screen the contents of the default images folder or we let the user go anywhere on their computer and click on a image file (InputFile component)

          • Lets prep some of the code first

            private string responseMessage;

            private string status; //either Processing or Results


            private string image; //used to form imageBase64 below

            private string imageBase64;

               

            private bool isTooLarge = false; //for the Analyze Local Image button


            private long imageFileSize;

            IBrowserFile resizedFile; //Used in the OnFileChanged method to store the resized image file if the original is too large to display/process


            //List of image files in the wwwroot/images directory

            List<string> fileList = new List<string>();

          • Next lets create the task that will automatically load the contents of the images folder for us to display on screen for possible selection (one of two techniques ... this method uses hardcoded images)

            protected override async Task OnInitializedAsync()

            {

                //C# lets you add the @ symbol in front of a string to create a

                //verbatim string literal where backslashes are not interpreted as escape characters

                //... so you can use them to specify file paths

                string path = Directory.GetCurrentDirectory() + @"\wwwroot\images";


                //GetFiles ... returns an array of the full names (including paths) for the files in the specified directory

                //[0] = "C:\\Temp\\Blazor\\Vision\\4-BlazorKernelVisionWebAppUpdate\\BlazorKernelVisionWebApp\\BlazorKernelVisionWebApp\\wwwroot\\images\\beach.jpg"

                var files = Directory.GetFiles(path);


                //GetFileName returns the file name and extension of the specified path string ... beach.jpg

                foreach (var file in files)

                {

                    fileList.Add(Path.GetFileName(file));

                }

            }

          • Now we are going to create two methods to load in the selected image and store it as a Base64  string. The one method will be for when we use the InputFile component and the second method is when we click on the pre-displayed images in the images folder.

            • private void ReadFile(string file)

              {

                  var path = Directory.GetCurrentDirectory() + @"\wwwroot\images\" + file;

                  var buffer = File.ReadAllBytes(path);

                  image = Convert.ToBase64String(buffer);

                  imageBase64 = "data:image/jpeg;base64," + image;


                  //determine the image file size

                  FileInfo fi = new FileInfo(path);

                  imageFileSize = fi.Length;


                  isTooLarge = false; //no issues with this technique so Recognize button fully enabled all the time


                  StateHasChanged();

              }

            • private async Task OnFileChanged(InputFileChangeEventArgs e)

              {

                 

                  var buffer = new byte[512000]; //initialize buffer to 512000 bytes


                  isTooLarge = false;


                  //The selected file is stored in the File property of the InputFileChangeEventArgs object, which is of type IBrowserFile

                  var file = e.File;

                  imageFileSize = file.Size;


                  if (imageFileSize > 65520) // This is the maximum size that the .NET implementation of Uri can handle

                  {

                      isTooLarge = true;


                      // The file is too large

                      resizedFile = await file.RequestImageFileAsync(file.ContentType, 300, 300);

                      buffer = new byte[resizedFile.Size];       

                      var stream = await resizedFile.OpenReadStream().ReadAsync(buffer);                 

                  }

                  else

                  {

                      buffer = new byte[file.Size];

                      var stream = await file.OpenReadStream().ReadAsync(buffer);
                      //This line reads the contents of the file into the buffer array

                  }

                  //Convert the buffer to a Base64 string and then create the data URI scheme for the image

                  image = Convert.ToBase64String(buffer);

                  imageBase64 = "data:" + file.ContentType + ";base64," + image; 


                  StateHasChanged();

              }

            • The ReadFile method vs OnFileChanged method

              The ReadFile method is reading a file from the local directory (wwwroot/images) that you have clicked on from a list on screen

              and it accepts a simple STRING parameter (beach.jpg) for instance


              The OnFileChanged method is reading the file from ANYWHERE on the user's computer

              The selected file is stored in the File property of the InputFileChangeEventArgs OBJECT, which is of type IBrowserFile

              IBrowserFile is an interface . It provides properties and methods to access the file's metadata and content like Size, ContentType, OpenReadStream, etc.

          • Now lets setup our UI before we focus on our last task to actually Recognize the Image

            • <h5>Image Recognition with Microsoft AI Extensions</h5>


              @*This button is used to load a local image file.

                The standard InputFile component displays a simple button

                ... by surrounding it with the label using the class btn we now have a nice bootstrap button  *@

              <label class="btn btn-primary">

                 
                    Choose Local Image

                  <InputFile OnChange="OnFileChanged" accept="image/png,image/jpeg" style="display: none;" />


              </label>


              <button class="btn btn-primary" disabled="@isTooLarge" @onclick="RecognizeImage" style="margin-left:10px;">Recognize Image</button>


              <hr/>


              @*Here we are going to divide the screen into two sections

                  on the left we will display a list of images from the wwwroot folder

                  on the right we will display the selected image*@

              <div class="row">

                 

                  <div class="col-md-3">

                      @if (fileList.Count > 0)

                      {

                          <span style="color:purple; font-size:small;"><b>Select Image from wwwroot</b></span>


                          <ul style="list-style-type: none;padding:0px;margin:0px;">

                              @foreach (var file in fileList)

                              {

                                  <li class="mb-2"><span @onclick="@( () => ReadFile(file))" style="cursor:pointer; text-decoration:underline;color:blue"><img src="/images/@file" height="50" width="50" style="padding:5px" />@file</span></li>

                                  @*Here we are using a Lambda expression since we are calling a method and passing a parameter to it*@

                              }

                          </ul>

                      }

                  </div>   

                 

                 

                  <div class="col-md-9 mt-3">

                      @if (imageBase64 != null)

                      {           

                          @*Here we are displaying the local image that was selected by the user *@           

                              <strong>Selected Image Original Size:@imageFileSize</strong><i>bytes</i>

                              <br />
                         

                          <img src="@imageBase64" alt="Selected Image" style="max-width: 300px; max-height: 300px;" />

                      }       

                  </div>   

              </div>


              @status


              <br/>


              @if (responseMessage != null)

              {

                  <div class="alert alert-info mt-3">

                      <h5>API Response</h5>

                      <p>@responseMessage</p>

                  </div>   

              }

          • ... and finally the key AI Task  RecognizeImage()

            • private async Task RecognizeImage()

              {   

                  if (imageBase64 == null)

                  {

                      responseMessage = "Please select an image first.";

                      return;

                  }


                  status = "Processing";


                  //Create a new ChatMessage object with the ChatRole.User role and the message "What's in this image?"

                  //Add a new DataContent object to the message's Contents collection with the image data and the MIME type "image/jpg"

                  //Call the GetResponseAsync method of the ChatClient object with the message as the parameter

                  //and store the result in the response variable

                  //Set the responseMessage variable to the response's Text property


              var message = new ChatMessage(ChatRole.User, "What's in this image?");

                  message.Contents.Add(new DataContent(imageBase64, "image/jpg"));

                  var response = await ChatClient.GetResponseAsync(message);

                  responseMessage = response.Text;


                  status = "Results";

              }

          • Test out the application

            • NOTE: Do NOT shut down Ollama ... it MUST be running in the background for proper execution.

          • Compare the speed of execution to a similiar application from the previous lecture implementing the Semantic Kernel and accessing the OpenAI API

            • BlazorKernelVisionWebAppUpdateNoKey

        • Let's experiment a little bit with what we can ask our image (vision)  model

          • Count the number of people in an image (BlazorImageRecognitionMSextensionsAIUpdated)
            var message = new ChatMessage(ChatRole.User, "How many people are in this image?");

          • Get back a Structured response from the model ... ie return an object defined by a class (BlazorImageRecognitionMSextensionsAIUpdatedClass) ... demo only

            • Structured Data Extraction in .NET: Microsoft.Extensions.AI allows us to  extract and deserialize structured information from freeform listings into typed C# objects.

            • we are going to try to determine the number of adults and children in the images and store the results

            • public class PictureResult

              {       

                  public int TotalNumberOfPeople { get; set; }     

                  public int NumberOfAdults { get; set; }

                  public int NumberOfChildren { get; set; }           

              }

            • In  Task RecognizeImage()
              ///Change prompt to try and get a more structured response

              //*******************************************************

              var message = new ChatMessage(ChatRole.User, "What's in this image . Also count the total number of people in the image and how many are adults and how many are children");


              message.Contents.Add(new DataContent(imageBase64, "image/jpg"));

              var response1 = await ChatClient.GetResponseAsync(message);

              var response2 = await ChatClient.GetResponseAsync<PictureResult>(message);
              //Note:The method overload ... unique to Microsoft.Extensions.AI ... allows you to specify the type of response you are expecting ie an object of type PictureResult

              //The key is using the .Result property to access the PictureResult object

                     

              //Structured response
              //note here we are displaying the full response message and the structured response

              //also note the use of <br/> to format the response message so that the structured response is displayed on a new line

              //... because of this we are using the MarkupString type to display the response message on the HTML section

              //****************************

              responseMessage =  " <b>Standard Response</b> ... response1.Text" + "<br/>" + response1.Text + "<br/>" + "<br/>"+

                                  "<b>New Response using PictureResult class </b> ... response2.Text " + "<br/>" + response2.Text + "<br/>"+ "<br/>" +

                                  "<b>New Response using PictureResult class </b> ... response2.Result.TotalNumberOfPeople ... " +

                                  "<br/> Total number of People: " + response2.Result.TotalNumberOfPeople + " Adults: " + response2.Result.NumberOfAdults + " Children: " + response2.Result.NumberOfChildren;

            • In the HTML section
              @if (responseMessage != null)

              {

                  <div class="alert alert-info mt-3">

                      <h5>AI Response</h5>

                      ... to user Query : "What's in this image . Also count the total number of people in the image and how many are adults and how many are children"

                      <hr />

                      <p>@((MarkupString)responseMessage)</p>

                  </div>   

              }

    • Supplementary Demos

      • BlazorImgRecogMSextensionsAIClassUsingOpenAI

        • This application leverages the Microsoft.Extensions.AI Library for image recognition tasks. BUT, we replace our open source model with the OpenAI API model gpt-4o-mini for better results. Almost all the coding remains the same.

        • The key difference is the use of the Microsoft.Extensions.AI.OpenAI NuGet Library to interact with the OpenAI API. So the only program updates occur in the Program.cs file where we reference OpenAIChatClient instead of OllamaChatClient.

          • using Microsoft.Extensions.AI;

            using OpenAI;

            // Add AI service to the container ... OpenAI

            var openAiClient = new OpenAIClient("yourKey");

            builder.Services.AddChatClient(new OpenAIChatClient(openAiClient, "gpt-4o-mini"));

        • Biggest difference is the SPEED and somewhat the accuracy compared to the last Lecture demo BlazorImageRecognitionMSextensionsUpdatedClass

      • BlazorHybridMSExtensionsAI (Windows Machine version)

        • .NET MAUI Blazor Hybrid version of previous demo with the addition of a new FormResult class which contains the properties ReportBy,  ReportDate, ReportPhone, AttendantName, AttendantPhone, RecievedDate, EnteredDate. These will be used to store all the components of a given image which displays a business form.

        • Key Highlights

          • MauiProgram.cs
            using Microsoft.Extensions.AI;

            using OpenAI;
            // Add AI service to the container ... OpenAI

            var openAiClient = new OpenAIClient("yourKey");

            builder.Services.AddChatClient(new OpenAIChatClient(openAiClient, "gpt-4o-mini"));

          • We replace this Web App command ...with the .NET MAUI specific line below in both the ReadFile and OnInitializedAsync Tasks
            //var path = Directory.GetCurrentDirectory() + @"\wwwroot\images\" + file;

            //.NET MAUI uses the AppDomain.CurrentDomain.BaseDirectory property to get the directory where the application is running   (add @using System.IO in HTML section)

            string rootpath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "wwwroot");

            string path = rootpath + @"\images\" + file;

          • Note the additional support for Image URLs

            • //Use HttpClient to download the image from the URL and then convert it to a base64 string for processing

              var httpClient = new HttpClient();

              var imageBytes = await httpClient.GetByteArrayAsync(imageUrl);

              var base64Image = Convert.ToBase64String(imageBytes);

              var imageData = $"data:image/jpg;base64,{base64Image}";

          • The last and most important update is the implementation of the class FormResult and the extraction of the key elements of a business form image.

            • public class FormResult

              {

                  public string ReportBy { get; set; }

                  public string ReportDate { get; set; }

                  public string ReportPhone { get; set; }

                  public string AttendantName { get; set; }

                  public string AttendantPhone { get; set; }

                  public string ReceivedDate { get; set; }

                  public string EnteredDate { get; set; }

              }

            • private async Task RecognizeImage ()

              var message = new ChatMessage(ChatRole.User, "What's in this image . Pull out all the information from all the required questions and place them in the related properties called ReportBy, ReportDate,ReportPhone,AttendantName,AttendantPhone,RecievedDate,EnteredDate. If you are unable to extract specific information like that from the image please describe the visual content");


              message.Contents.Add(new DataContent(imageBase64, "image/jpg"));

              var response1 = await ChatClient.GetResponseAsync(message);

              var response2 = await ChatClient.GetResponseAsync<FormResult>(message);

              //Structured response ...

              //note here we are displaying the full response message and the structured response

              //also note the use of <br/> to format the response message so that the structured response is displayed on a new line

              //... because of this we are using the MarkupString type to display the response message on the HTML section

              //****************************

              responseMessage = " <b>Standard Response</b> ... response1.Text" + "<br/>" + response1.Text + "<br/>" + "<br/>" +

                                 "<b>New Response using FormResult class </b> ... response2.Text " + "<br/>" + response2.Text + "<br/>" + "<br/>" +

                                 "<b>New Response using FormResult class </b> ... response2.Result.ReportBy ... " + "<br/>" + "<hr/>" +

                                 " Report By: " + response2.Result.ReportBy + " <br/> Report Date: " + response2.Result.ReportDate

                                 + " <br/>  Report Phone: " + response2.Result.ReportPhone + " <br/> Attendant Name: " + response2.Result.AttendantName +

                                  "  <br/> Attendant Phone: " + response2.Result.AttendantPhone + " <br/> Received Date: " + response2.Result.ReceivedDate +

                                  " <br/> Entered Date: " + response2.Result.EnteredDate;

            • In the HTML section
              @if (responseMessage != null)

              {

                  <div class="alert alert-info mt-3">

                      <h5>AI Response</h5>

                      @if (isImageURL)

                      {

                          <span>... to user Query : "What's in this image"</span>

                      }

                      else

                      {

                          <span>... to user Query : "What's in this image . Pull out all the information from all the required questions and place them in the related properties called ReportBy, ReportDate,ReportPhone,AttendantName,AttendantPhone,RecievedDate,EnteredDate"</span>

                      }


                      <hr />

                      <p>@((MarkupString)responseMessage)</p>

                  </div>

              }

      • BlazorAIformImageToDB (updated version of  previous demo with Database implementation ... .NET 9 Blazor Web App using Interactive Render Mode WebAssembly ) 

        • Proof of Concept (Home.razor page ... in Server Project)

          • The Home page is our PROOF OF CONCEPT. It implements a class called FormResult (located in Shared Project ... more details below)  to further analyze an image of a form and provide a structured response. To do this we use the GetResponseAsync method with a FormResult type parameter. Note:The method overload ... unique to Microsoft.Extensions.AI ... allows you to specify the type of response you are expecting ie an object of type FormResult .The key is using the .Result property to access the FormResult object properties.The FormResult class has the following properties: ReportBy, ReportDate, ReportPhone, AttendantName, AttendantPhone, ReceivedDate, EnteredDate

        • The Next step is to take the structured response and store the results in a Database

          • Check out the View Current Form DB link in the Navigation Menu and Image AI page for all the details.

          • Note: This application is a .NET 9 Blazor Web App using Interactive Render Mode WebAssembly ... which includes a Server Project where this Home.razor page is located and a Client Project where FormPage.razor (View Current Form DB) and ImageAI.razor (new version of AI Image Recognition) pages are located + a FormEdit.razor page (for editing and deleting form data).

          • The key class FormResult is located in the Shared Project (Class Library).
            public class FormResult

            {

                public int Id { get; set; }

                public string? ReportBy { get; set; }

                public string? ReportDate { get; set; }

                public string? ReportPhone { get; set; }

                public string? AttendantName { get; set; }

                public string? AttendantPhone { get; set; }

                public string? ReceivedDate { get; set; }

                public string? EnteredDate { get; set; }

            }

          • The Microsoft.Extensions.AI and Microsoft.Extensions.AI.OpenAI librarys have been added to both the Server and Client projects

        • Database Implementation Key Teaching Points

          • We added a class library project (BlazorAIforms.Shared) which holds our FormResult class ... then make sure to add a Reference to this Shared  project in the Client and Server Projects

          • In the Server Project we added a folder called Controllers and then right clicked and add an API Controller with actions using Entity Framework. We also updated the appsettings.json (Server Project) and changed name of the Database=BlazorAIforms

          • Then we performed the migration and database update ... add-migration initial ... update-database

          • Check out the database in the Server Object Explorer and add a few records by hand

          • Before running the app we updated the Program.cs file (Server Project) so that it is aware of our use of a Controller api ... builder.Services.AddControllers() ... app.MapControllers()

          • We can now start up the app and enter the controller name (FormsResults) to view the rough Json text file output

          • .. All the prep is now done so we focus on the UI and coding for the remaining pages ...

            • ImageAI.razor (this page ... very similar to Home.razor)

              • HTML Section

                @rendermode @(new InteractiveWebAssemblyRenderMode(prerender:false))


                @inject HttpClient Http 

                @*Added to Program.cs in client project to implement HttpClient

                builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });*@


                <button class="btn btn-primary" @onclick="SaveDB" style="margin-left:10px;">Save To DB</button>

              • Code Section
                //an object of type FormResult to store the structured response

                //which will be saved to the database

                private FormResult formResult;

                private async Task RecognizeImage()
                { ...
                var message = new ChatMessage(ChatRole.User, "What's in this image . Pull out all the information from all the required questions and place them in the related properties called ReportBy, ReportDate,ReportPhone,AttendantName,AttendantPhone,RecievedDate,EnteredDate");


                message.Contents.Add(new DataContent(imageBase64, "image/jpg"));

                var response1 = await ChatClient.GetResponseAsync(message);

                var response2 = await ChatClient.GetResponseAsync<FormResult>(message);
                //Store the structured response in the formResult object for saving to the database

                formResult = response2.Result;
                }

                private async Task SaveDB()

                {

                    //reponse is an HttpResponseMessage object that is used to store the response from the server

                    //response is initialized to the result of the PostAsJsonAsync method of the Http object

                    HttpResponseMessage response;                     

                   

                    if (formResult == null)

                    {

                        status = "Please recognize the image first.";

                        return;

                    }


                    //The PostAsJsonAsync method sends a POST request to the specified URI (our Controller) with the specified value serialized as JSON in the request body

                    //and returns a Task<HttpResponseMessage> that represents the asynchronous operation

                    response = await Http.PostAsJsonAsync("FormResults", formResult);


                    if (response.IsSuccessStatusCode)

                    {

                        status = "Form Result Saved to Database";

                    }

                    else

                    {

                        status = "Error Saving Form Result to Database";

                    }

                }

            • FormPage.razor (basic display of the FormResult data in a table format)

              • Code Section
                //List of FormResult objects to store the form data from the database

                List<FormResult> FormList = new List<FormResult>();


                protected override async Task OnInitializedAsync()

                {           

                    //this line fetches data from the "FormResults" endpoint (Controller) in the API project.

                    //Converts the JSON response into a list of FormResult objects, and stores it in the FormList variable.

                    // This allows the Blazor component to use this data for rendering the form list in the UI.

                    FormList = await Http.GetFromJsonAsync<List<FormResult>>("FormResults");

                }


                private async Task HandleEdit(int id)

                {

                    string requestUri = "form-edit/" + id.ToString();

                    Nav.NavigateTo(requestUri);

                }

              • HTML Section
                @page "/formpage"

                @using BlazorAIforms.Shared

                @inject HttpClient Http

                @rendermode @(new InteractiveWebAssemblyRenderMode(prerender:false))

                @inject NavigationManager Nav


                @*Stream rendering allows the component to start rendering and sending parts of the response to the client as soon as they are ready,

                  rather than waiting for the entire component to be rendered*@

                @attribute [StreamRendering]


                <h5>Database Application using AI Form Image Recognition </h5>



                <div class="row mt-4">

                    <div class="col-6">

                        <h5 class="card-title mt-3">FORM LIST</h5>

                    </div>

                    <div class="col-4 offset-2">

                        <a href="/imageai" class="btn btn-info form-control">Add New Data Via Vision AI</a>

                    </div>

                </div>


                <br />


                @if (FormList == null)

                {

                    <div class="text-center">

                        <p><em>Loading ...</em></p>       

                    </div>

                }

                else

                {

                    <table class="table table-sm table-bordered table-striped table-hover">

                        <thead>

                            <tr>

                                <th>Action</th>

                                <th>Report By</th>

                                <th>Report Date</th>

                                <th>Report Phone</th>

                                <th>Attendant Name</th>

                                <th>Attendant Phone</th>

                                <th>Received Date</th>

                                <th>Entered Time</th>

                            </tr>

                        </thead>

                        <tbody>

                            @foreach (var form in FormList.OrderBy(m => m.ReportBy))

                            {

                                <tr @onclick="@(() => HandleEdit(form.Id))">

                                    <td>

                                        <a class="btn btn-success" href="/form-edit/@form.Id">Edit/Delete</a>

                                    </td>

                                    <td>@form.ReportBy</td>

                                    <td>@form.ReportDate</td>

                                    <td>@form.ReportPhone</td>

                                    <td>@form.AttendantName</td>

                                    <td>@form.AttendantPhone</td>

                                    <td>@form.ReceivedDate</td>

                                    <td>@form.EnteredDate</td>                   

                                </tr>

                            }

                        </tbody>

                    </table>

                }

            • FormEdit.razor (edit and delete functionality)

              • HTML Section
                @page "/form-edit/{id:int}"

                @rendermode @(new InteractiveWebAssemblyRenderMode(prerender:false))

                @using BlazorAIforms.Shared

                @inject HttpClient Http

                @inject NavigationManager Nav

              • Code Section
                [Parameter]

                public int id { get; set; }

                // FormResult is a class in the Shared project that represents the form data from the database

                // and is used to bind the form data to the form fields

                private FormResult  form;

                protected override async Task OnInitializedAsync()

                {

                    // Get the form data from the database using the id parameter from the URL

                    // and store it in the form object

                form = await Http.GetFromJsonAsync<FormResult>("FormResults/" + id);

                OpeningTitle = "Edit/Delete Current Form";


                ready = true;

                }

                private async Task HandleValidSubmit()

                {

                HttpResponseMessage response;

                //This line constructs the URI for the HTTP PUT request.

                // It concatenates the base URI "FormResults/" with the Id property of the form object.

                // This Id uniquely identifies the form data that needs to be updated on the server.

                string requestUri = "FormResults/" + form.Id;

                response = await Http.PutAsJsonAsync(requestUri, form);


                if (response.IsSuccessStatusCode)

                {

                Nav.NavigateTo("formpage");

                }

                else

                {

                error = response.ReasonPhrase;

                }

                }

                private async Task HandleReset()

                {

                Nav.NavigateTo("formpage");

                }


                private async Task DeleteForm()

                {

                string requestUri = "FormResults/" + form.Id;

                var response = await Http.DeleteAsync(requestUri);


                if (response.IsSuccessStatusCode)

                {

                Nav.NavigateTo("formpage");

                }

                else

                {

                error = response.ReasonPhrase;

                }

                }

      • BlazorAIformImageToDBupdateURL

        • Adds a couple of extra features to previous demo

        • On the Home.razor page we added the option to enter and analyze an Image URL (not crucial to DB implementation)

        • On the  FormEdit.razor page we added the option to Add New Data Via Vision AI (goes back to the ImageAI.razor page)

        • Added a little more error checking. If we analyze an image that is not a proper form then we don't want a null record added to the Database in the SaveDB Task
          if (formResult.ReportBy != null)

          {

              //The PostAsJsonAsync method sends a POST request to the specified URI (our Controller) with the specified value serialized as JSON in the request body

              //and returns a Task<HttpResponseMessage> that represents the asynchronous operation

              response = await Http.PostAsJsonAsync("FormResults", formResult);


              if (response.IsSuccessStatusCode)

              {

                  status = "Form Result Saved to Database";

              }

              else

              {

                  status = "Error Saving Form Result to Database";

              }

          }

          else

          {

              status = "Image does not conform to the Form required";

          }   

      • BlazorAIformImageToDBandTableImage/BlazorAIformImageToDBandTableImageQuickGrid

        • This latest version demostrates how to extract structured data from an image of a table that includes various fields like age, job , marital status, education, balance and housing.

          • First we add a new class in the Shared folder called Table with properties that match the table in the image

          • Then in the ImageAI.razor page we declare a new object called tableList ... a list of type Table

            • //a List of objects of type Table to store the structured response (from an image of  tabular data)

              private List<Table> tableList = new List<Table>(); 

          • We then create a new ChatMessage object with the ChatRole.User role and the message "What's in this image . Extract the data from this table into a structured including fields for age, job, marital status, education, balance, and housing."

          • Our GetResponseAsync command now uses a reference to a List of objects of type Table

            • //Added to extract the data from the image of a table

              var newmessage = new ChatMessage(ChatRole.User, "What's in this image . Extract the data from this table into a structured format including fields for age, job, marital status, education, balance, and housing.");

              //Added to extract the data from the image of a table ... note the use of List<Table> in the GetResponseAsync method

              newmessage.Contents.Add(new DataContent(imageBase64, "image/jpg"));

              var response3 = await ChatClient.GetResponseAsync(newmessage); 

              var response4 = await ChatClient.GetResponseAsync<List<Table>>(newmessage); 


              //Store the structured response in the tableList object for displaying in a table format

              tableList = response4.Result;


          • We then display the Result in a table format using a foreach loop

          • Use the image table.jpg in the images folder of the wwwroot folder to test out

      • BlazorAIformImageToDBandTableImageQGvideo

      • BlazorAIformImageToDBandTableImageQGvideoUpdate (1)

      • BlazorAIformImageToDBandTableImageQGvideoUpdTab (2)

      • BlazorAIformImageToDBandTableImageQGvideoUpdTabAudio (3)

      • BlazorAIformImageToDBandTableImageQGvideoUpdTabAudioUpd (4)

      • BlazorAIformImageToDBandTableImageQGvideoUpdTabAudioUpdWbcm (5)

      • BlazAIformImgToDBtableImgQGvidTabAudWbcmImgSimilaritySSIM(6)

        • These newest Updates adds the OpenCvSharp4 and OpenCvSharp4.runtime.win libraries to allow us to easily incorporate Computer Vision and Image processing.

        • This library allows us to connect  to cameras, open video files and save frames easily.

        • In this update we demonstrate how to easily connect to a Webcam , take a picture , save it to the wwwroot folder and analyze it. We also show how to extract frames from a video and analyze them using the Microsoft.Extensions.AI library.

        • We can apply this technology to real-world problems like insurance claim analysis. Automate report generation for vehicle damage videos, detailing the car model, year, damage specifics, and incident descriptions. This demonstrates AI’s transformative potential.

        • The first Updated version extracts frames evenly spaced across the entire video instead of the first 10 consecutive frames and then allows the user to analyze any one of them

        • The second Updated version allows the user to send all extracted frames to OpenAI for analysis at the same time. It also implements a Tabbed Menu.

        • The third Updated version allows you to record audio, save it, and convert it to text (English) using OpenAI's Whisper API. This is basic Transciption. The application will also take non-English audio and convert it to text in the language spoken (See French in wwwroot folder ... food-french.mp3) and also convert it (Translation)  to English Text.

        • The fourth Updated version takes the converted text and adds the capability to read it back in a number of user selected voices.

        • The fifth Updated version modifies the  Home.razor where we have been working on AI Video Analysis. Now we have added the ability to Record full mp4 videos via the Webcam.

        • The sixth Updated version adds the ability to compare images for Similarity using the OpenCvSharp4 library. Specifically we compare the first and last frames of any selected video using two techniques

          • The first technique calculates the mean difference for each color channel (B, G, R) and averages out these values to get a rudimentary score ... low is better high is worse

          • The second more advanced technique uses the Structural Similarity Index (SSIM), which compares images based on luminance, contrast, and structure, providing a value between -1 and 1 (where 1 means identical images).

      • BlazorMEAIchat

        • This application is a redo of several AI Chat Implementations from previous Lectures (.NET 9 Blazor WebApp Interactive Render Mode Server)

          • Recall BlazorAInet9HTMLupdate (HttpClient Technique covered in Lecture 198)

          • Recall BlazorAISemanticKernal (Microsoft Semantic Kernel Library covered in Lecture 199)

        • Here we use the Microsoft.Extensions.AI library to implement a Basic Chat page and a Conversational Chat page

          • Install the Microsoft.Extensions.AI Nuget Package

          • Install the Microsoft.Extensions.AI.OpenAI Nuget Package

          • In Program.cs add the AI Service

            • var openAiClient = new OpenAIClient("yourKey");

            • builder.Services.AddChatClient(new OpenAIChatClient(openAiClient, "gpt-4o-mini"));

          • The rest of the coding is in the ChatPage.razor and ChatConversation.razor (and assocated .css file)

      • BlazorSpeechToTextDemoJS

        • This demo showcases real-time speech-to-text capabilities in a Blazor WebAssembly Standalone app.
          There are many options for speech-to-text:
          Cloud APIs (Azure Cognitive Services, Google, etc.). Native libraries wrapped with gRPC or REST. Browser Web Speech API.
          For a Blazor app that runs in the browser, the Web Speech API is the easiest place to start:
          No server round trips for audio. No external billing. Works directly in the user’s browser. The downside: not all browsers support it (mainly Chromium based ones do: Chrome, Edge, some versions of Opera). You will add a simple IsSupported check and show a friendly message when support is missing.

          The solution is built using:

          • A small JavaScript helper speech-to-text.js that wraps the Web Speech API for browser-based voice input.

          • Real-time transcription with interim and final results.

          • Error handling and browser support checks.

          • A reusable Blazor component (SpeechToText) to drive speech recognition and integrate with your forms.

          • A simple binding pattern (bind-Value) to plug speech-to-text into any form field.

          User Benefits:
          This feature makes your app feel more modern and friendly, especially on mobile devices or for long forms. Users can dictate feedback or other text, making data entry faster and more accessible.

          Technical Highlights:

          • Works entirely in the browser—no server round-trips for speech recognition.

          • Easy to reuse: drop the SpeechToText component into any form.

          • Real-time interim and final transcript display.

          • Status line: listening / idle / error message.

          • Clear integration pattern for parent-child communication (e.g., clearing form fields).

          • All the magic will happen in JavaScript, but Blazor will control it via JS interop, and you will get typed callbacks on the .NET side.


          Try it out: Enter your name and feedback. You can type or use your voice. Click Clear All to reset the form and start again!

      • BlazorSpeechToTextEmailWebAppServerJS

        • This Updated demo showcases real-time speech-to-text capabilities in a Blazor Web App Interactive Render Mode Server with the additional capability of sending the transcribed text to other users via Email
          For speech-to-text we are using:
          Browser Web Speech API.

          This updated solution implements:

          • The Email Sending Library called MailKit

          • MailKit: A popular .NET library for sending and receiving emails using standard protocols like SMTP, IMAP, and POP3

          Technical Highlights:

          • Sending email in a Blazor Server application is generally easier than in a Blazor WebAssembly application due to the fundamental differences in their execution environments.

          • In Blazor Server, your application code runs entirely on the server. This means you have direct access to server-side resources and libraries, including those for sending emails.

          • Direct SMTP Client Access: You can directly use the SmtpClient class from System.Net.Mail or any other .NET email library (e.g., MailKit) to connect to an SMTP server and send emails.

          • To test out our implementation we used the Ethereal Site

          • Ethereal is a fake SMTP service, mostly aimed at Nodemailer and EmailEngine users (but not limited to). It's a completely free anti-transactional email service where messages never get delivered. Instead, you can generate a vanity email account right from Nodemailer, send an email using that account just as you would with any other SMTP provider and finally preview the sent message here as no emails are actually delivered.


          Try it out: Enter your Name, Receipent Email, Subject Heading and Body Text (Speech to Text Transcribed). You can also type your body content. Click Clear All to reset the form and start again!

  • Web Scraping and AI Analysis in a Blazor Application47:50

    In this Lecture we will

    • Learn how to Scrape a Website using the Html Agility Pack NuGet package within a Blazor Web App (Interactive Render Mode Server)

      • Web scraping is a technique used in many language like C# to extract data from a website.

      • Is web scraping legal?

        That's a gray zone! There is no official answer about it, and almost every company has some web scraping program. In short, do polite crawling and don't spam a website and everything will be fine. Also some sites block scraping tools.

      • The Html Agility Pack is a .NET library that is used to parse and manipulate HTML documents. It is commonly used in .NET applications for tasks such as web scraping, reading and updating HTML files, and more. Because Blazor is a web framework within the .NET ecosystem, you can use HAP in Server-Side Blazor applications without any issues.

      • However, Html Agility Pack cannot be used in Blazor WebAssembly (Blazor WASM) directly because it relies on .NET APIs that may not be supported or could cause issues when running in the browser's WebAssembly environment. For Blazor WebAssembly, you would need to make an API call to a server-side endpoint (which could be a Blazor Server app, an ASP.NET Core app, Azure Function, etc.) that performs the HTML parsing using HAP and then returns the results to the Blazor WebAssembly application.

    • Learn how to feed a Scraped website to an AI model (using Microsoft.Extensions.AI library)  and have it return specific details about the site ... including site overview ,specifics about images and more

      • The Scraped data cannot be added to the message.Contents command like it was done with images
        message.Contents.Add(new DataContent(imageBase64, "image/jpg")) in the previous lecture

      • ... so instead you must  attach (concatenate) the scraped site data (extractedData) right in the ChatMessage command
        var message = new ChatMessage(ChatRole.User, "Give me an overall idea what this site is about nicely format your response in HTML " + extractedData);

    • Create a simple application that implements the stated objectives above  BlazorServerWebScrapeAI (Blazor .NET 9 Web App Interactive Render Mode Server)

      • We start off by installing the HTML Agility Pack NuGet Package . It is a .NET code library that allows you to parse  HTML pages.

      • Next we install the Microsoft NuGet Packages

        • Microsoft.Extensions.AI and Microsoft.Extensions.AI.OpenAI

        • ... back to Program.cs
          using Microsoft.Extensions.AI;

          using OpenAI;

          // Add AI service to the container ... OpenAI

          var openAiClient = new OpenAIClient("yourKey");

          builder.Services.AddChatClient(new OpenAIChatClient(openAiClient, "gpt-4o-mini"));

          builder.Services.AddHttpClient();

      • Let's focus on implementing our Web Scraper first

        • We create a folder called Services and create a class (service) called HtmlScraperService.cs

          using HtmlAgilityPack;

          using System.Net.Http;

          using System.Threading.Tasks;   


          namespace BlazorServerWebScrapeAI.Services

          {

              public class HtmlScraperService

              {

                  private readonly HttpClient _httpClient;


                  //Constructor
                  public HtmlScraperService(HttpClient httpClient)

                  {

                      _httpClient = httpClient;

                  }



          //The result of the GetAsync call is an HttpResponseMessage object, which is stored in the        //response variable. The HttpResponseMessage contains details about the HTTP response, such as //the status code, headers, and the response content.
          //ReadAsStringAsync reads the HTTP response body (content) as a string and ensures it is fully //converted into a string format which is useful for processing the HTML

                  public async Task<HtmlDocument> LoadHtmlFromUrlAsync(string url)

                  {

                      var response = await _httpClient.GetAsync(url);

                      var pageContent = await response.Content.ReadAsStringAsync();


          //These commands are part of the HtmlAgilityPack library
          //Here we create a new instance of the HtmlDocument, which is used to parse and manipulate        //HTML content in a structured way
          //The LoadHtml method takes a string containing raw HTML and parses it into the HtmlDocument object. After this step the object will contain a tree-like structure of the HTML allowing you to query or manipulate specific elements (eg. tags, attribures, text...called ParsedText)

                      var htmlDocument = new HtmlDocument();

                      htmlDocument.LoadHtml(pageContent);


                      return htmlDocument;

                  }

              }

          }

        • Now we go back to Program.cs
          //declare the HtmlScraperService for future dependency injection

          builder.Services.AddScoped<HtmlScraperService>();   


      • The rest of our code will be on the Home.razor page

        • First some variable declarations in our code section

          private string targetUrl = "";


          private string extractedData = "";

          private MarkupString ExtractedDataMarkup => new MarkupString(extractedData); // Convert to MarkupString for HTML rendering

          // ... this is a second method you can use

          // instead of the (casting) technique used in the HTML above


          private bool isLoading = false; //used to disable and enable buttons


          private string? responseMessage;

          private string? statusScrape;

          private string? statusAI;


          private string? imageBase64;

          private long imageFileSize;

          private string? imageUrl;

          private bool isImageURL = false;

          //This is the list of image URLs extracted from the scraped data

          //used in the Task ServiceScrapeAndProcess()

          private List<string> imageUrls = new List<string>();

        • Now we work on our Web Scraper Task and the associated ExtractImageUrls method (used as a proof of concept to extract every image on the webpage ...  will be fully utilized  in the Supplementary Demo  BlazorServerWebScrapeAIupdated where we will allow the user to  analyze  any selected image)

          protected async Task ServiceScrapeAndProcess()

          {

              if (string.IsNullOrEmpty(targetUrl))

              {

                  statusScrape = "Please enter a URL to scrape.";

                  return;

              }

              else

              {

                  isLoading = true;

                  responseMessage = "";

                  statusAI = "";

                  statusScrape = "Processing...";

             

                  //call the HtmlScraperService (in Service folder) to scrape the URL

                  var ScraperResponse = await HtmlScraperService.LoadHtmlFromUrlAsync(targetUrl);

                 

                  //Extract data from the response so we can display it on our HTML page

                  extractedData = ScraperResponse.ParsedText;   

             

                  // Extract image URLs using regular expressions

                  // These will be added to the end of the scraped data (extractedData) so they can be displayed

                  imageUrls = ExtractImageUrls(ScraperResponse.ParsedText);

                 
                 foreach (var url in imageUrls)

                  {

                      extractedData += " <br/><img src=' " + url + " ' width='100' height='100' /> ";

                  }


                  imageUrl = imageUrls.FirstOrDefault(); // Get the first image URL to display and Analyze (ProcessUrLImage method below)


                  StateHasChanged(); // Refresh the UI to show the new data


                  isLoading = false;

                  statusScrape = "Done";

              }   

          }

          //Generated with help from GitHub Copilot (see ExtractImageUrls-codesnippet)
          ... live demo of using GitHub Copilot with  query "How do I extract all the images from the scraped site and store them into a list "

          private List<string> ExtractImageUrls(string htmlContent)

          {

              //This method uses a regular expression to extract image URLs from the HTML content and returns them as a list of strings.

              //The regular expression looks for <img> tags and captures the value of the src attribute.

              //The regex pattern "<img[^>]+?src=[\"'](?<url>.*?)[\"']" is used to match the <img> tags and extract the URLs.

              //The pattern uses a named group "url" to capture the URL value.

              //The RegexOptions.IgnoreCase option is used to make the regex case-insensitive.


              var urls = new List<string>();

              var regex = new Regex("<img[^>]+?src=[\"'](?<url>.*?)[\"']", RegexOptions.IgnoreCase);

              var matches = regex.Matches(htmlContent);

              foreach (Match match in matches)

              {

                  urls.Add(match.Groups["url"].Value);

              }

              return urls;

          }

        • Next we add some HTML so that we can test out the first part of our application ... Web Scraping

          • Some initial declarations ... some we need now some we will use later

            @page "/"


            @using HtmlAgilityPack

            @using System.Net.Http

            @using System.Text.Json

            @using System.Threading.Tasks

            @using System.Text


            @using BlazorServerWebScrapeAI.Services


            @rendermode InteractiveServer


            @*Implements HTML Agility Pack*@

            @inject HtmlScraperService HtmlScraperService   


            @using Microsoft.Extensions.AI;

            @inject IChatClient ChatClient


            @*Required for Regex commands in ExtractImageUrls method below*@

            @using System.Text.RegularExpressions

          • Now the UI  (see HTMLcodesnippet)  ... some of the HTML we need now, and  some we will use later ... before pasting in the snippet ... create a folder called images in the wwwroot folder and copy over loading.gif (in Resources)

            <PageTitle>Home</PageTitle>


            <h3>Web Scraping AI ... Implementing HTML Agility Pack and Microsoft.Extensions.AI</h3>


            <br/>


            <h5 style="text-align:center">

                <b>Generate Responses using the OpenAI API</b>

            </h5>


            <div class="row">

                <textarea @bind="targetUrl" rows="2" placeholder="Enter Website Address Here"></textarea>

            </div>


            <div style="text-align:center">

                <button @onclick="ServiceScrapeAndProcess" class="btn btn-primary mt-3" disabled="@isLoading">Web Scrape URL</button>

                <button @onclick="@( () => ProcessUrLImage(imageUrl))" class="btn btn-primary mt-3" disabled="@isLoading">First Image AI Analysis</button>

                <button @onclick="ScrapeAndProcessMSextensions" class="btn btn-primary mt-3" disabled="@isLoading">Generate Webpage AI Analysis</button>


                @if (statusAI == "Processing...")

                {

                    <img src="images/loading.gif" width="40" height="40" style="margin-left: 10px;" />

                    <em>@statusAI</em>

                }

                @if (isImageURL)

                {       

                    <br />

                    <img src="@imageUrl" alt="Selected Image" style="max-width: 300px; max-height: 300px;" />

                    <strong>Selected Image Original Size:@imageFileSize</strong>

                    <i>bytes</i>

                }


            </div>


            <div class="alert alert-info mt-3">

                <p>

                    AI Analysis of Scraped Data using MS Extensions:@statusAI

                    <br />

                    @((MarkupString)responseMessage)

                </p>

            </div>


            <div class="alert alert-light mt-3" style="width: 800px;">

                <p>

                    Scraped Data:@statusScrape

                    <br/>

                    @ExtractedDataMarkup

                </p>

            </div>

          • OK time to test out our Web Scraper (comment out the two buttons that reference ProcessUrLImage and ScrapeAndProcessMSextensions ... for now)

        • Now that we have our extractedData and imageUrl (first image only) we are going to feed these into our OpenAI API model via our Microsoft.Extensions.AI library of commands.

          • First we will analyze the image stored in imageUrl ... leveraging our knowledge of image (Vision) analysis from the previous lecture. (see ProcessImageCodeSnippet.txt)

            private async Task ProcessUrLImage( string imageUrl)

            {

                isImageURL = false;

                imageBase64 = null;

                isLoading = true;            


                if (imageUrl == null)

                {

                    responseMessage = "Need a valid Image URL ...Try Scraping first";

                    isLoading = false;

                    return;

                }


                //check if valid URL

                if (!Uri.IsWellFormedUriString(imageUrl, UriKind.Absolute))

                {

                    responseMessage = "Not a valid Image URL.";

                    isLoading = false;

                    return;

                }


                isImageURL = true;

                statusAI = "Processing...";

                responseMessage = null;

                       

                var message = new ChatMessage(ChatRole.User, "What's in this image");


                //Use HttpClient to download the image from the URL and then convert it to a base64 string for processing

                var httpClient = new HttpClient();


            //This method sends an HTTP GET request to the specified imageUrl (a string containing the URL of an image). It retrieves the response body as a byte array. This is useful for downloading binary data, such as images, files, or other non-text content.

                var imageBytes = await httpClient.GetByteArrayAsync(imageUrl);

                var base64Image = Convert.ToBase64String(imageBytes);

                var imageData = $"data:image/jpg;base64,{base64Image}";


                //determine the image file size

                imageFileSize = imageBytes.Length;


                message.Contents.Add(new DataContent(imageData, "image/jpg"));

                var response1 = await ChatClient.GetResponseAsync(message);

               

                responseMessage = response1.Text;


                statusAI = "Done";

                isLoading = false;

               

            }

          • ... and finally we Analyze the entire scraped site

            private async Task ScrapeAndProcessMSextensions()

            {

                if (!string.IsNullOrEmpty(extractedData))

                {

                    isLoading = true;

                    isImageURL = false;

                    responseMessage = "";

                    statusAI = "Processing...";


                    //Create a new ChatMessage object with the ChatRole.User role and an appropriate message

                    //Call the GetResponseAsync method of the ChatClient object with the message as the parameter

                    //and store the result in the response variable

                    //Set the responseMessage variable to the response's Text property           


                    //Change prompt to try and get a more detailed or relevant response

                    //*****************************************************************


                    //var message = new ChatMessage(ChatRole.User, "Tell me what's on this webpage and format the response in HTML " + extractedData );

                    //var message = new ChatMessage(ChatRole.User, "Tell me how many pictures of bread there are on this site " + extractedData);

                    //var message = new ChatMessage(ChatRole.User, "Tell me how many times the word Sourdough appears on the site " + extractedData);

                    //var message = new ChatMessage(ChatRole.User, "Give me an overall idea what this site is about " + extractedData);

                    //var message = new ChatMessage(ChatRole.User, "Count the number of images on the site by counting the number of img tags " + extractedData);       

                   

                    var message = new ChatMessage(ChatRole.User, "Give me an overall idea what this site is about nicely format your response in HTML " + extractedData);

                   

                    // ***** I Tried here to add the Scraped data to the message but not allowed ... so I fed scraped site data above right in the ChatMessage *********

                    //message.Contents.Add(new DataContent(imageBase64, "image/jpg"));


                    var response1 = await ChatClient.GetResponseAsync(message);

                    responseMessage = response1.Text;


                    isLoading = false;

                    statusAI = "Done";           

                }

                else

                {

                    responseMessage = "Please Scrape the URL first ";

                }

            }

      • Test out application ...

        • ... Some sites block users from web scraping .

    • Supplementary Demos

      • BlazorServerWebScrapeAIupdated

        ... Updated with Complete Site Image Analysis and enhanced error checking

        • HTML updates

          • <button @onclick="@( () => ProcessUrLImage(imageUrl,1))" class="btn btn-primary mt-3" disabled="@isLoading">First Image AI Analysis</button>

          • @*This handles the hard coded capture of the first image on the site*@

            @if (isImageURL1)

            {       

                <br />

                <img src="@imageUrl" alt="Selected Image" style="max-width: 300px; max-height: 300px;" />

                <br/>

                <strong>Selected Image Original Size:@imageFileSize</strong>

                <i>bytes</i>

            }

            @*This handles any image clicked on from the entire list displayed in the div section below*@

            else if (isImageURL2)

            {

                <br />

                <img src="@newImageURL" alt="Selected Image" style="max-width: 300px; max-height: 300px;" />

                <br/>

                <strong>Selected Image Original Size:@imageFileSize</strong>

                <i>bytes</i>

            }

          • @*This displays all the images on the site that was scraped ...

                Some may be placeholder images or not relevant to the site ... but they are all displayed

                They are all clickable and will call the ProcessUrLImage method to analyze the image

            *@

            <div class="alert alert-light">

                @if (imageUrls.Count > 0)

                {           

                    <span>Click on Images below for AI Analysis</span>

                    <br/>


                    @foreach (var img in imageUrls) 

                    { 

                        if (!string.IsNullOrEmpty(img)) 

                        { 

                            <span @onclick="@(() => ProcessUrLImage(img,2))" style="cursor:pointer;"> 

                                <img src="@img" height="50" width="50" style="padding:5px" /> 

                            </span> 

                        } 

                    }

                }

            </div>

        • Code Update

          • Note the use of try catch  constructs in the ServiceScrapeAndProcess Task ... here we are worried about a site whose url is well formed BUT  may not actually exist .

            try

            {

                // Call the HtmlScraperService to scrape the URL

                var ScraperResponse = await HtmlScraperService.LoadHtmlFromUrlAsync(targetUrl);


                // Extract data from the response so we can display it on our HTML page

                extractedData = ScraperResponse.ParsedText;


                // Extract image URLs using regular expressions

                imageUrls = ExtractImageUrls(ScraperResponse.ParsedText);


                foreach (var url in imageUrls)

                {

                    extractedData += "<br/><img src='" + url + "' width='100' height='100' />";

                }


                imageUrl = imageUrls.FirstOrDefault(); // Get the first image URL to display and analyze


                statusScrape = "Done";

            }

            catch (Exception ex)

            {

                // Handle any errors that occur during the scraping process

                statusScrape = "An error occurred while scraping the URL...." + ex.Message;

                extractedData = "";

                responseMessage = ""; // Optionally, you can log the exception message

                statusAI = "";

                imageUrls.Clear(); // Clear the list of image URLs

                imageUrl = null; // Clear the image URL

                isImageURL1 = false;

                isImageURL2 = false;

            }

            finally

            {

                isLoading = false;

                StateHasChanged(); // Refresh the UI to show the new data

            }

          • ProcessUrLImage Task now has two parameters
            private async Task ProcessUrLImage(string imageUrl,int type)

            { .....
              if (type==1) //First Image Analysis button was clicked

            {

                isImageURL1 = true;

            }

            else if (type == 2) //Image clicked on from the list of images displayed ... store the URL for analysis in a different variable

            {

                isImageURL2 = true;

                newImageURL = imageUrl;

            }
            ..... }

          • Note the use of try catch construct in the ScrapeAndProcessMSextensions Task ...here we may run into an issue with the Openai API due to a token limit error ... HTTP 429 (tokens: rate_limit_exceeded) Request too large for gpt-4o-mini ...

            try

            {

                var response1 = await ChatClient.GetResponseAsync(message);

                responseMessage = response1.Text;

            }

            catch (Exception ex)

            {

                // Handle the exception and provide feedback to the user

                responseMessage = $"An error occurred while processing the AI response: {ex.Message}";

                statusAI = "Error";

            }

            finally

            {

                isLoading = false;

                statusAI = "Done";

                StateHasChanged(); // Refresh the UI to show the new data

            }

      • BlazorServerWebScrapeAIupdatedOtherElements 

        • Instead of just pulling out images this Updated Version illustrates how to extract any desired part of a site.

          • For this demo app we use the site  which contains a table listing selected cities in the world by nominal gross domestic product (GDP). The table has four columns ... City, Country, GDP, Population

        • We must take a look at the site before hand via the DevTools (F12) and specifically look through the page Elements to get a good idea how the site is structured (html tags) ... we will use this knowledge later with our query selectors when we extract objects ... like city, population etc

        • Next we create a class to mirror what objects we want to extract ... CityByGdpData
          public class CityByGdpData

          {       

              public string City { get; set; }

              public string Country { get; set; }

              public int GdpByBillionUsd { get; set; }

              public int Population { get; set; }           

          }

        • Then we create a new Task/Method (public List GetCitiesByGdpDatas(string url)) within our current Service to scrape the site and use Query Selector(s) to pinpoint where the desired objects we want to extract are located. Note: to use the Query Selector commands you must additionally install the HtmlAgilityPack.CssSelectors.NetCore NuGet Package

          • //This Task below is for scraping the GDP data from the Wikipedia page

            //Before using the QuerySelector(s) you must take a deep dive into the HTML structure of the page

            //You can do this by right clicking on the page and selecting "Inspect" in your browser.

            //This should open the developer tools and show you the HTML structure of the page ... from there the fun begins

            //Narrow in on the table you want to scrape and check out the HTML tags and classes

            public List<CityByGdpData> GetCitiesByGdpDatas(string url)

            {

                 //Here is declare the list of CityByGdpData objects ... which will be used to store the scraped data (specific to the GDP data)

                 var citiesByGdpDataList = new List<CityByGdpData>();


                 //Load the HTML document from the specified URL using HtmlAgilityPack load command and then query the document for the table rows

                 //... because the information we want from the Wikipedia page is in a table format

                 //The QuerySelectorAll method is used to select all the table rows (tr) in the HTML document

                var htmlDoc = new HtmlWeb().Load(url);

                 var rows = htmlDoc.QuerySelectorAll("table tr");


                 //The first three rows are headers and we want to skip them ... the actual header for the table is obvious

                 //but it appears that the two boxed sections on the page are also part of the row structure

                 //The way I discovered this was to add a break here and hover over the rows variable above and observe the contents of rows[0] rows[1] rows[2]

                 for (int i=3;i<rows.Count;i++)

                 {

                    var cells = rows[i].QuerySelectorAll("td"); //here I grab the table cells (td) in the row (city,country,gdp,population)


                     if (cells.Count==4) //parse data only if there are 4 cells in a row for city,country,gdp,population

                     {

                         //Here I am using the ReplaceNonBreakingSpace method to replace non-breaking spaces with regular spaces

                         //The ReplaceNonBreakingSpace method is defined below and is used to clean up the data

                         //.. this is only discovered after exectuting the code and seeing the results

                        var city = ReplaceNonBreakingSpace(cells[0].InnerText.Trim());

                         var country = ReplaceNonBreakingSpace(cells[1].InnerText.Trim());


                         //Here I am using the ExtractNumericPart method to extract the numeric part of the GDP and population data

                         //The ExtractNumericPart method is defined below and is used to clean up the data

                         //.. this is only discovered after exectuting the code and seeing the results

                         //We use TryParse to convert the string to an integer and handle any parsing errors

                        int.TryParse(ExtractNumericPart(cells[2].InnerText.Trim()), out int gdpByBillionUsd);                   

                         int.TryParse(ExtractNumericPart(cells[3].InnerText.Trim()),out int population);


                         //Here I am creating a new CityByGdpData object and adding it to the list (citiesByGdpDataList)

                         //The CityByGdpData class is defined in the Models folder and contains the properties for the city, country, GDP, and population

                         //The CityByGdpData class is used to store the scraped data in a structured way

                        citiesByGdpDataList.Add(new CityByGdpData

                         {

                             City = city,

                             Country = country,

                             GdpByBillionUsd = gdpByBillionUsd,

                             Population = population                         

                         });

                     }


                     //The ReplaceNonBreakingSpace method is used to replace non-breaking spaces with regular spaces

                    string ReplaceNonBreakingSpace(string input)

                     {

                         return input.Replace("&#160;", " ");//non-breaking space characters (encoded as &#160; in HTML)

                     }


                     //The ExtractNumericPart method is used to extract the numeric part of the GDP and population data

                    string ExtractNumericPart(string input)

                     {

                         //^: Ensures the match starts at the beginning of the string.

                         //[\d,.]: Matches any digit(\d), a comma(,), or a period(.).

                         //+ Ensures one or more of the preceding characters(digits, commas, or periods) are matched.

                         var match = Regex.Match(input, @"^[\d,.]+");

                         if (match.Success)

                         {

                             return match.Value.Replace(",", string.Empty).Replace(".", string.Empty);

                         }


                         return null;

                     }

                 }


                 //Sort the list of CityByGdpData objects by country name and return the sorted list

                 return citiesByGdpDataList.OrderBy(x => x.Country).ToList(); 

            }

        • ... and finally we extract and process (in this case) the GDP table data from Wikipedia on the Home.razor page.

          • HTML Section
            @using Microsoft.AspNetCore.Components.QuickGrid

            <h5 style="text-align:center">

                <b>Advanced Scraping ... </b>

            </h5>


            <div class="row">

                <textarea @bind="newtargetUrl" rows="2" placeholder="Enter Website Address Here"></textarea>

            </div>


            <button @onclick="NewServiceScrapeAndProcess" class="btn btn-primary mt-3" disabled="@isLoading">Web Scrape URL ... Other Data Points (Objects)</button>

            <button @onclick="@( () => GetCityWithTheHighestPopulation(cityByGdpData))" class="btn btn-primary mt-3" disabled="@isLoading">Highest Population </button>

            <br/>

            <div class="alert alert-light mt-3" style="width: 800px;">

                <p>

                    Scraped Data:@statusScrape ... @HighestPopulationCity

                    <br/>

                    @ExtractedDataMarkup

                </p>

            </div>

            @if (cityByGdpData.Count >0)

            {       

                <QuickGrid Class="table table-sm table-bordered table-striped table-hover" Virtualize="true" Items="@CityQ"  Pagination="@pagination">


                    <PropertyColumn Property="@(p => p.City)" Title="City" Sortable="true" />

                    <PropertyColumn Property="@(p => p.Country)" Title="Country" Sortable="true" />

                    <PropertyColumn Property="@(p => p.GdpByBillionUsd.ToString("C", System.Globalization.CultureInfo.CurrentCulture))" Title="GDP" Sortable="true" />

                    <PropertyColumn Property="@(p => p.Population)" Title="Population" Sortable="true" />


                </QuickGrid>


                <Paginator State="@pagination"/>

            }

            //Note the implementation of the QuickGrid with column sorting and page due to the size of the data scraped

          • Code Section

            //Wikipedia URL

            private string newtargetUrl = "";

            //Wikipedia URL ... List for the newtargetURL

            private List<CityByGdpData> cityByGdpData = new List<CityByGdpData>();

            //Required for the use of QuickGrid

            IQueryable<CityByGdpData>? CityQ;

            PaginationState pagination = new PaginationState { ItemsPerPage = 10 };


            //used in the GetCityWithTheHighestPopulation method below

            private string HighestPopulationCity = string.Empty;   


            //This method is called when the user clicks the "Web Scrape URL ... Other Data Points (Objects)" button

            protected async Task NewServiceScrapeAndProcess()

            {

                if (string.IsNullOrEmpty(newtargetUrl))

                {

                    statusScrape = "Please enter a URL to scrape.";

                    extractedData = "";

                    responseMessage = "";

                    statusAI = "";

                    imageUrls.Clear(); // Clear the list of image URLs

                    imageUrl = null; // Clear the image URL

                    isImageURL1 = false;

                    isImageURL2 = false;


                    return;

                }

                else if (!Uri.IsWellFormedUriString(newtargetUrl, UriKind.Absolute))

                {

                    statusScrape = "Not a valid URL.";

                    extractedData = "";

                    responseMessage = "";

                    statusAI = "";

                    imageUrls.Clear(); // Clear the list of image URLs

                    imageUrl = null; // Clear the image URL

                    isImageURL1 = false;

                    isImageURL2 = false;

                    return;

                }   

                else

                {

                    isLoading = true;


                    responseMessage = "";

                    extractedData = ""; // Clear the extracted data


                    imageUrls.Clear(); // Clear the list of image URLs

                    imageUrl = null; // Clear the image URL

                    isImageURL1 = false;

                    isImageURL2 = false;


                    statusAI = "";

                    statusScrape = "Processing...";

               

                    try

                    {

                        //This Method call within the Service was used to extract the GDP data from the Wikipedia page

                        //it returns a List of CityByGdpData objects

                        cityByGdpData = HtmlScraperService.GetCitiesByGdpDatas(newtargetUrl);

                        CityQ = cityByGdpData.AsQueryable(); // Convert the list to an IQueryable for QuickGrid   


                        statusScrape = "Done";

                    }

                    catch (Exception ex)

                    {

                        // Handle any errors that occur during the scraping process

                        statusScrape = "An error occurred while scraping the URL...." + ex.Message;

                        extractedData = "";

                        responseMessage = ""; // Optionally, you can log the exception message

                        statusAI = "";

                        imageUrls.Clear(); // Clear the list of image URLs

                        imageUrl = null; // Clear the image URL

                        isImageURL1 = false;

                        isImageURL2 = false;

                    }

                    finally

                    {

                        isLoading = false;

                        StateHasChanged(); // Refresh the UI to show the new data

                    }


                }

            }


            public void GetCityWithTheHighestPopulation(List<CityByGdpData> data) 

            { 

               if (data == null || data.Count == 0) 

               { 

                   return; // Return if the list is empty or null 

               } 


                // Filter city with highest population using LINQ and lambda expression

               var cityWithHighestPopulation = data           

                   .OrderByDescending(city => city.Population) 

                   .FirstOrDefault(); 


               // Store the result in the HighestPopulationCity variable 

               HighestPopulationCity = cityWithHighestPopulation != null 

                   ? "City with Highest Population: " + cityWithHighestPopulation.City + " Population: " + cityWithHighestPopulation.Population.ToString("N0")

                   : "No data available for cities in the United States."; 

            }


      • BlazorScrapingAndDB/ tabbed menu layout BlazorScrapingAndDBupdated

        • Further updated  Web Scraping demo application  now with a Database implementation

          • These New Versions illustrates how to extract any desired part of a site and feed the results into a DB with full CRUD Ability.

          • We will be again be using the Wikipedia page "List of cities by GDP"

            • Recall we must take a look at the site before hand via the DevTools (F12) (right click on table and choose inspect) and specifically look through the page Elements to get a good idea how the site is structured (html tags) ... we will use this knowledge later with our query selectors when we extract objects ... like city, population etc

          • This version is a .NET 9 Blazor WebApp using Interactive Render Mode WebAssembly

            • Recall that this template creates a Server Project and Client (WebAssembly) project. We must manually add a new Class Library project which we will call BlazorScrapingAIandDB.Shared

          • First we create a class to mirror what objects we want to extract ... CityByGdpData with the addition of an Id property so we can implement a DB ... See the Shared project
            public  class CityByGdpData

            {

                public int Id { get; set; }

                public string City { get; set; }

                public string Country { get; set; }

                public int GdpByBillionUsd { get; set; }

                public int Population { get; set; }

            }

          • Then we create a new Task/Method (public List GetCitiesByGdpDatas(string url)) within our current Service to scrape the site and use the SelectNodes command instead of Query Selector(s) to pinpoint where the desired objects we want to extract are located (to use Query Selectors you must additionally install the HtmlAgilityPack.CssSelectors.NetCore NuGet Package
            public async Task<List<CityByGdpData>> GetCitiesByGdpDatas(string url)

            {

                //Here we declare a new object ... which will be a List of type CityByGdpData class

                //we will use it to store the data we scrape from the website

                var citiesByGdpDataList = new List<CityByGdpData>();


                //Here we load the HTML document from the URL

                var htmlDoc = new HtmlWeb().Load(url);


                // Replace QuerySelectorAll with SelectNodes

                //var rows = htmlDoc.QuerySelectorAll("table tr");           

               var rows = htmlDoc.DocumentNode.SelectNodes("//table//tr");


                if (rows != null)

                {

                    for (int i = 3; i < rows.Count; i++)

                    {

                        //var cells = rows[i].QuerySelectorAll("td");  ... old technique

                        //here a grab the table cells (td) in the row (city,country,gdp,population)//

                        var cells = rows[i].SelectNodes("td");


                        if (cells != null && cells.Count == 4)

                        {

                            var city = ReplaceNonBreakingSpace(cells[0].InnerText.Trim());

                            var country = ReplaceNonBreakingSpace(cells[1].InnerText.Trim());


                            int.TryParse(ExtractNumericPart(cells[2].InnerText.Trim()), out int gdpByBillionUsd);

                            int.TryParse(ExtractNumericPart(cells[3].InnerText.Trim()), out int population);

                           

                            citiesByGdpDataList.Add(new CityByGdpData

                            {

                                City = city,

                                Country = country,

                                GdpByBillionUsd = gdpByBillionUsd,

                                Population = population

                            });

                        }

                    }

                }


                return citiesByGdpDataList.OrderBy(x => x.Country).ToList();



                // Helper methods to clean up the data

                // These are local functions ... methods that are only used in this class

                // and are not accessible outside of this class


                string ReplaceNonBreakingSpace(string input)

                {

                    return input.Replace("&#160;", " ");

                }


                string ExtractNumericPart(string input)

                {

                    var match = Regex.Match(input, @"^[\d,.]+");

                    if (match.Success)

                    {

                        return match.Value.Replace(",", string.Empty).Replace(".", string.Empty);

                    }


                    return null;

                }

            }

          • Here are the key updates to the Program.cs file in the Server Project
            // Add services to the container... InteractiveServerComponents ... after the fact

            builder.Services.AddRazorComponents()

                .AddInteractiveWebAssemblyComponents()

                .AddInteractiveServerComponents();


            //added to implement HttpClient for Service HtmlScraperService 

            builder.Services.AddHttpClient();


            //added so that the app can use the database via the controller

            builder.Services.AddControllers();


            //inject the HtmlScraperService

            builder.Services.AddScoped<HtmlScraperService>();

            //added after the fact for the controller to work

            app.MapControllers();

          • Database Implementation Key Teaching Points

            • We added a class library project which holds our CityByGdpData class ... make sure to add a Reference to this Shared  project in the Client and Server Projects

            • In the Server Project we added a folder called Controllers and then right clicked and add an API Controller with actions using Entity Framework. We also updated the appsettings.json (Server Project) and changed name of the Database = BlazorScrapingAIandDB

            • Then we performed the migration and database update ... add-migration initial ... update-database

            • Check out the database in the Server Object Explorer and add a few records by hand

            • Before running the app we updated the Program.cs file (Server Project) so that it is aware of our use of a Controller api ... builder.Services.AddControllers() ... app.MapControllers()

            • We can now start up the app and enter the controller name (CityByGdpDatas) to view the rough Json text file output

            • .. All the prep is now done so we focus on the UI and coding for the remaining pages ...

          • Home.razor page

            • HTML code
              <div class="row">

                   <textarea @bind="newtargetUrl" rows="2" placeholder="Enter Website Address Here"></textarea>

              </div>

                 

              <button @onclick="@( () => NewServiceScrapeAndProcess(1))" class="btn btn-primary mt-3" disabled="@isLoading">Web Scrape URL and Save ALL to DB</button>

              <button @onclick="@( () => NewServiceScrapeAndProcess(0))" class="btn btn-primary mt-3" disabled="@isLoading">JUST ... Web Scrape URL </button>   

              <button @onclick="@( () => GetCityWithTheHighestPopulation(cityByGdpData))" class="btn btn-primary mt-3" disabled="@isLoading">Highest Population </button>

              <br/>

              @if (cityByGdpData.Count >0)

              {       

                  <QuickGrid Class="table table-sm table-bordered table-striped table-hover" Virtualize="true" Items="@CityQ"  Pagination="@pagination">


                      <PropertyColumn Property="@(p => p.City)" Title="City" Sortable="true" />

                      <PropertyColumn Property="@(p => p.Country)" Title="Country" Sortable="true" />

                      <PropertyColumn Property="@(p => p.GdpByBillionUsd.ToString("C", System.Globalization.CultureInfo.CurrentCulture))" Title="GDP" Sortable="true" />

                      <PropertyColumn Property="@(p => p.Population)" Title="Population" Sortable="true" />


                  </QuickGrid>


                  <Paginator State="@pagination"/>

              }

            • Code Section

              protected override void OnInitialized()

              {

                  // Set the base address for the API request in the NewServiceScrapeAndProcess Task below

                  // var response = await Http.PostAsJsonAsync("CityByGdpDatas", city);

                  //... solved issue with the "API URI not set correctly" error message in this Server Project

                  //This was not required in the Blazor WebAssembly project razor pages

                  Http.BaseAddress = new Uri(Nav.BaseUri);

              }
              //This method is called when the user clicks either the "Web Scrape URL and Save to DB" ... OR ..."JUST scrape URL" button

              //... the scrapeFlag is used to determine if we want to save the data to the DB or not

              protected async Task NewServiceScrapeAndProcess(int scrapeFlag)

              {

                  if (string.IsNullOrEmpty(newtargetUrl))

                  {

                      statusScrape = "Please enter a URL to scrape.";

                      return;

                  }

                  else if (!Uri.IsWellFormedUriString(newtargetUrl, UriKind.Absolute))

                  {

                      statusScrape = "Not a valid URL.";

                      return;

                  }   

                  else

                  {

                      isLoading = true;

                      statusScrape = "Processing...";


                      //----------- Updated Technique to handle possibly Scraper Errors -----------

                      try

                      {         


                          //This Method call within the Service was used to extract the GDP data from the Wikipedia page

                          //it returns a List of CityByGdpData objects

                          cityByGdpData = await HtmlScraperService.GetCitiesByGdpDatas(newtargetUrl);

                          CityQ = cityByGdpData.AsQueryable(); // Convert the list to an IQueryable for QuickGrid


                          //We only want to save to the Database once! it is not necessary to save the same data multiple times

                          if (scrapeFlag==1)

                          {

                              foreach(var city in cityByGdpData)

                              {

                                  //save to database

                                  if(city.Id==0)

                                   {

                                      //This line would not work until the API URI was set correctly in the OnInitialized method above

                                       var response = await Http.PostAsJsonAsync("CityByGdpDatas", city);                       

                                   }                     

                              }

                          }               


                          statusScrape = "Done";

                      }

                      catch (Exception ex)

                      {

                          // Handle any errors that occur during the scraping process

                          statusScrape = "An error occurred while scraping the URL...." + ex.Message;               

                      }

                      finally

                      {

                          isLoading = false;

                          StateHasChanged(); // Refresh the UI to show the new data

                      }

                  }

              }

          • In the Client project we have the WikiPage.razor (used to Display the current contents of the database with the option to Edit/Delete) and the  GdpEdit.razor page which is used to actually edit and delete records.

          • WikiPage.razor

            • HTML section
              @page "/wikipage"

              @using BlazorScrapingAIandDB.Shared

              @inject HttpClient Http

              @rendermode @(new InteractiveWebAssemblyRenderMode(prerender:false))

              @inject NavigationManager Nav

              @using Microsoft.AspNetCore.Components.QuickGrid

              @if (cityByGdpData.Count > 0)

              {

                  <QuickGrid Class="table table-sm table-bordered table-striped table-hover" Virtualize="true" Items="@CityQ" Pagination="@pagination">


                      <TemplateColumn Context="p" Title="Action">

                          <a class="btn btn-warning" href="/gdpedit/@p.Id">Edit/Delete</a>

                      </TemplateColumn>

                     

                      <PropertyColumn Property="@(p => p.City)" Title="City" Sortable="true" />

                      <PropertyColumn Property="@(p => p.Country)" Title="Country" Sortable="true" />

                      <PropertyColumn Property="@(p => p.GdpByBillionUsd.ToString("C", System.Globalization.CultureInfo.CurrentCulture))" Title="GDP" Sortable="true" />

                      <PropertyColumn Property="@(p => p.Population)" Title="Population" Sortable="true" />


                  </QuickGrid>


                  <Paginator State="@pagination" />

              }

            • Code Section
              protected override async Task OnInitializedAsync()

              {   

                  cityByGdpData = await Http.GetFromJsonAsync<List<CityByGdpData>>("CityByGdpDatas");

                  CityQ = cityByGdpData.AsQueryable();

              }

          • GdpEdit.razor

            • HTML Section
              @page "/gdpedit"

              @page "/gdpedit/{id:int}"
              ...
              <EditForm Model="@gdp" OnValidSubmit="@HandleValidSubmit" @onreset="HandleReset">

              <div class="text-center">

              <button type="submit" class="btn btn-primary" style="margin:2px;">Save</button>

                  @if (id != 0) // if we are editing an existing database record we show the delete button

              {

              <button class="btn btn-danger " title="Delete task" style="margin:2px;" @onclick="@DeleteForm">

              <i class="bi bi-fire"></i>

              Delete

              </button>

              }

              <button type="reset" class="btn btn-secondary" style="margin:2px;">Cancel</button>

              </div>

            • Code Section
              [Parameter]

              public int id { get; set; }

              private CityByGdpData gdp;

              protected override async Task OnInitializedAsync()

              {

              // Get the form data from the database using the id parameter from the URL

              // and store it in the form object


              // If the id is 0, create a new form object

              // If the id is not 0, get the form data from the database using the id parameter
              if (id==0)

              {

              gdp = new CityByGdpData();

                  OpeningTitle = "Create New Form";

              }
              else

              {

              gdp = await Http.GetFromJsonAsync<CityByGdpData>("CityByGdpDatas/" + id);

              OpeningTitle = "Edit/Delete Current Form";

              }
              }

        • Suggested Exercises: (WebScrapingExercise.pdf)

          • Scrape the 100 best American movies list from the Wikipedia site


            • BlazorScrapingAndDBupdatedMovies

          • Scrape a fictional E-Commerce Site and extract the product name, price, image URL and product URL


            • BlazorScrapingAndDBupdateNextEcom (1 page scrape)

            • BlazorScrapingAndDBupdatedNextEcomFull (full site scrape)

  • An Introduction to using GitHub Copilot Agent Mode in Visual Studio41:24

    In this Lecture we will

    • Learn that in the latest release of Visual Studio (17.4) we now can enable Agent mode for GitHub Copilot. Now GitHub Copilot not only works with you (synchronous), but works for you (asynchronous).

    • Learn that Copilot agent mode is the next evolution in AI-assisted coding. Acting as an autonomous peer programmer, it performs multi-step coding tasks at your command — analyzing your codebase, reading relevant files, proposing file edits,  and implementing the changes that you agree on right in your code. It's like a real time coding partner. Not only will the AI recommend changes, it will do some testing to validate that the changes actually work and even run terminal commands. A major step up from simple code completions.

      • Agent mode will actually look in your all files including all your components pages( .razor)  stylesheets (.css) , class (.cs) and more. 

      • ... But don't blindly accept all changes, that's why it is still necessary to have a good grounding in C#, HTML,  CSS  and ASP.NET core concepts.

        • There is growing gap between those who learned to code before AI tools and those who started with them. The former group tends to value methodical, hands-on coding, while the latter sometimes uses AI to bypass deeper learning. Despite these differences, most developers agree on one point: responsibility lies with the human, not the machine.

          GitHub Copilot can be a powerful ally, but only when used with intention and care. In the end, AI should enhance human decision-making—not replace it.

        • Experienced programmers can take AI and use it as a tool to help them with mundane parts of their coding assignments. They will have the expertise to fix things that the AI gets wrong and be better prepared to make cogent requests that will get results closer to the desired outcome. Attempting to code complex systems without such background/knowledge with AI would be like watching a full season of some TV medical drama and thinking you’re ready to do surgery.

    • Learn how to activate Agent Mode

      • Tools...Options...GitHub...Enable Agent mode in the chat pane

      • In the GitHub Copilot Chat Window you will now see under Ask, two options

        • Ask ... this will give you a direct answer (similar to chat) in the side bar and then you can use this information in your application. You may end up copying and pasting specific code  where necessary.

        • Edit ... focuses on code suggestions and improvements within a file, where you provide context and prompts for edits  ( actually blended into Agent functionality) ... so Ask and Agent are the two key Modes now in GitHub Copilot

        • Agent ... this is a more autonomous mode that can handle more complex tasks. In this mode it will do all the steps necessary to implement your request right inside your application, modifying all the required files , installing dependencies if necessary and actually building the project. If will determine the "context" of a request (if none is provided)  and search over your entire code base.

      • Resetting Copilot back to Ask mode

    • Practice using Agent mode and some more complex GitHub Copilot techniques in some Blazor Applications

      • In an ideal world, you would just care about the final output of Copilot agent mode, but it can sometimes make mistakes and go off track. To easily intervene and undo in those situations, every  invocation is transparently displayed in the UI, and each solution response requires approval, or you can undo and revert to the state before the last edit was applied.

      • Instead of relying on one long, detailed prompt to yield a perfect solution, you should iterate with Copilot – the UI is built for iterations, giving you full control over the process ensuring the final result is better.

      • AiAgentModeDemoApp1

        • Blazor Web App (.NET 9 Render Mode WebAssembly) ... working with the default template

        • Remove About Heading

          • Please remove the top border that contains the About Link

        • Change Colour Theme (see Color Hunt for possible color palette or use the discretion of Agent with some prompting)

          • Change the colour scheme of the entire site to your discretion please do not use the blue scheme at all including the left side menu

        • Add a Tab Style menu along a top bar (... again some trial and error until the desired menu look is achieved)

          • Update the application menu system, do not use the NavMenu but rather implement a tabbed menu style in the MainLayout using bootstrap

          • That's really good but I want the menu all on the top bar by itself and the display of the component pages directly below

      • AiAgentModelDemoApp2

        • Things don't always work out ... adding QuickGrid to  Weather page ... issues

          • Note: Pre- Rendering in Server Project Weather.razor page to prevent the page from loading twice

            • @rendermode @(new InteractiveServerRenderMode(prerender:false))

        • On the home.razor page put a nice summary of what this application can do section it off with an alert-info bootstarp box

      • Prompting with Images Using GitHub Copilot Vision (still using our current demo as the base of operations)

        • AiAgentModelDemoApp3a/AiAgentModelDemoApp3b/AiAgentModelDemoApp3c

          • using pizzaTemplate.png as image prompt

            • Can you help me build a page that looks like this (3a)

              • using pizza.png in images folder for mainpage (Home.razor)

            • Please keep the original tabbed menu along the top intact and add a completely separate menu right in the Home.razor page along the top with the choices Menu Order Online Book a Table About Hours & Locations (3b)

            • Now please create generic pages for the menu links Menu, Order Online, Book a Table, About , Hours & Locations. Place these new pages in the Client project pages folder (3c)

            • Make sure to make the links active on the Home page to these newly create razor component pages

          Copilot can understand all kinds of image content including for instance a rough drawing of a database layout ie Tables and connections and it will attempt to create this database

      • Using Agents to Fix and Style an existing Blazor Application

        • ToDoStatusUpdateConditionalOriginal

        • ToDoStatusUpdateConditionalAgentUpdate1

          • Redo the Todo.razor page to a more styled and colourful display when the user enters a Todo display the status in a table format with 3 columns the first column called Remove and second column description and the third column IsComplete The is complete column should contain a checkbox when clicked it would inidcate the todo is complete and then would automatically display a garbage pail icon in the Remove column which would allow the user to delete the current todo

          • Choose a different Agent Model (GPt-4.1 ... Claude 3.7 Sonnet)

        • ToDoStatusUpdateConditionalAgentUpdate2

          • small tweak using Ask mode : "Use the iconic icon class to display the trash can".

          • Can't use Bootstrap icon class here because this application was created in .NET 3.2

      • Create a Blazor Hybrid Application ... that mirrors the Song Database (SQLite) application (Lecture 169)

        • HybridSQLiteSongNET8Original

        • HybdSngAgent

          • I want to create an application that helps me tracks  my favourite songs , artists . I want to update the main page so it's a grid and on the top of the grid it lists Songs and Artist Name. On top of the grid I want a section for Song entry and Artist entry and a button to Add the latest entry. I want these new entries to add into a SQLite database. I want the model ie class to be in a separate file  in a folder called models and the database to be in its own separate file in a folder called database

          • Update the current application so that a user can edit a record and delete a record

          • When a user presses the Delete button please prompt the user with "Are you sure" and give them the option to cancel the delete

    • Suggested Exercise

      • Create a simple calculator application (Blazor WebAssembly Standalone App) . Add a new razor component page called Calc make sure you can get to it from the navmenu. The application should have two input boxes for the user entry and 4 buttons add, subtract, multiply and divide. The result should appear in a final result text box

      • Take a previously create CRUD application and recreate a similar version using GitHub Copilot Agent Mode

        • ToDoListDbNET8Lect180


    • Summary ... Here are some of the ways developers are using GitHub Agent Mode

      • Refactoring code

      • Migrating a project to another programming language or to another tech stack

      • Writing tests

      • Modernizing legacy code (maybe so you don’t have to learn an outdated language)

      • Autofixing code gen errors

      • Adding new features to an application

      • Prototyping a new app based on a functional spec or UI sketch

      • Implementing non-functional requirements or boilerplate code

      • Scoping and planning the work for implementing a new feature

      • Non-code generation tasks, like documentation

    One important thing to note: Copilot is powered by LLMs, which are nondeterministic, and that means its suggestions may differ even with the same prompt and context.

  • A Deeper Dive into Blazor Hybrid Apps with AI Implementations Part 129:21

    In this Lecture we will

    • Highlight a number of ways to access specific functions/capabilities of the Android Phone (Emulator) and the Windows Machine within a  .NET MAUI Blazor Hybrid App. We then leverage some of  these capabilities  to implement AI Analysis of Images, Videos (frame images) and Audio (transcribed text) ...

      HyPhone (.NET MAUI Blazor Hybrid App) ... deep dive into demo no hands on ... but suggested exercises (See Lecture 205 Resource Links )

    • Part 1 ...  Phone specific (Andriod Emulator) applications

      • Begin our deep dive into Blazor Hybrid applications  focusing on Android Emulator specific implementations including :

        • Accessing phone contacts lists

        • Creating Push Notifications

        • Biometric Authentication

        • Simulating Swipe Gestures

    • Contacts (TheContacts.razor)

      • Access the full Contact list on the Android Phone (Emulator) and allow the user to select a specific contact's full details

        • Note: The use of a service (ContactService) to access device contacts. The Nuget Package Microsoft.Maui.Essentials was installed but with the latest version of .NET MAUI, the Contacts API is now part of the Microsoft.Maui.Devices namespace... actually Microsoft.Maui.Essentials will lead to errors so it has been removed from the .csproj for all the demos in this 3 part Lecture series

          • MyContact.cs (in Model folder)
            public  class MyContact

            {

                //This class represents a contact in the phone book.
               //The Contact API returns many properties, but we will only use a few of them.

                public string? Name { get; set; }   

                public string? PhoneNumber { get; set; }

                public string? Email { get; set; }   

            }

          • IContact.cs (in Services folder)
            public interface IContact

            {

                Task<List<MyContact>> GetAll();

                Task<MyContact> SelectContact();

            }

          • ContactService (in Services folder)
            using Microsoft.Maui.ApplicationModel.Communication;
            //This service implements the IContact interface to provide functionality for accessing contacts on the device.

            public class ContactService : IContact

            {

                // This method retrieves all contacts from the device and returns them as a list of MyContact objects.

                public async Task<List<MyContact>> GetAll()

                {

                    bool permission = await IsGranted();    // Check if the app has permission to access contacts (method below)

                    if (!permission)

                    {

                        throw new UnauthorizedAccessException("You are not allowed");

                    }


                    // Get all contacts from the device using the Contacts API in .NET MAUI.

                    var allContacts = await Microsoft.Maui.ApplicationModel.Communication.Contacts.Default.GetAllAsync();

                   
                   // Map the retrieved contacts to a list of MyContact objects.

                  // MyContact is a custom class that represents a contact in the phone book.

                  // Here we only take values for the properties Name, PhoneNumber, and Email.
                 // We use the FirstOrDefault() method to get the first email and phone number if they exist,   otherwise we use an empty string.

                    var contactList = allContacts.Select(x => new MyContact

                    {

                        Email = x.Emails.FirstOrDefault()?.EmailAddress ?? string.Empty,       

                        PhoneNumber = x.Phones.FirstOrDefault()?.PhoneNumber ?? string.Empty,   

                        Name = x.GivenName ?? string.Empty                                     

                    }).ToList();


                    return contactList;

                }


                // This method allows the user to select a single contact from the device's contacts.

                public async Task<MyContact?> SelectContact()

                {

                    bool permission = await IsGranted();

                    if (!permission)

                    {

                        throw new UnauthorizedAccessException("You are not allowed");

                    }


                    // Open the contact picker to allow the user to select a contact

                    var contact = await Microsoft.Maui.ApplicationModel.Communication.Contacts.Default.PickContactAsync(); 

                 

                    return contact is null ? null : new MyContact

                    {

                        Email = contact.Emails.FirstOrDefault()?.EmailAddress ?? string.Empty,

                        PhoneNumber = contact.Phones.FirstOrDefault()?.PhoneNumber ?? string.Empty,

                        Name = contact.GivenName ?? string.Empty

                    };

                    //We need to make sure we return a non-nullable MyContact object otherwise the compiler will complain.

                }


                // This method checks if the app has permission to access contacts.

                // The Permissions class is part of the Microsoft.Maui.Essentials namespace.

                // In.NET MAUI, Essentials APIs(like permissions, contacts, device info, etc.) are integrated directly into the Microsoft.Maui libraries.

                // So you no longer need to install a separate NuGet package for Essentials in .NET MAUI projects, as you did with Xamarin.Essentials in Xamarin projects.

                // The Permissions API is available via using Microsoft.Maui.ApplicationModel;

                private async Task<bool> IsGranted()

                {

                    var permission = await Permissions.RequestAsync<Permissions.ContactsRead>();    // Request permission to read contacts

                    return permission == PermissionStatus.Granted ? true : false;                   // Return true if permission is granted, otherwise false

                }


                // Don't forget to declare the ContactService in your MauiProgram.cs file to register it as a service.

                // builder.Services.AddScoped<IContact, ContactService>();   

            }

        • TheContacts.razor
          @page "/thecontacts"
          <h6><strong>Phone Contacts</strong></h6>

          @*Key service referenced in the code section below *@

          @inject IContact contactService 


          <button class="btn btn-primary" style="font-size:0.85rem;" @onclick ="SelectContact">Select Contact</button>


          @*This will display key info specific to chosen contact*@

          @if (SelectedContact != null) 

          { 

              <ul> 

                  <li>Name: @SelectedContact.Name</li> 

                  <li>Phone: @SelectedContact.PhoneNumber</li> 

                  <li>Email: @SelectedContact.Email</li> 

              </ul> 

          } 


          @*This displays all the contacts in your phone ... just the names and phone numbers*@

          @if (Contacts.Count > 0) 

          { 

              <h3>Contacts</h3> 

              <ul> 

                  @foreach (var contact in Contacts) 

                  { 

                      <li>@contact.Name - @contact.PhoneNumber</li> 

                  } 

              </ul> 

          } 

          else 

          { 

              <p>No contacts available.</p> 

          }


          @status



          @code {

                 

              MyContact? SelectedContact = new();                 // Selected contact from the phone where MyContact is a class that holds contact details

              List<MyContact> Contacts = new List<MyContact>();   // List to hold all contacts


              private string status = "";


              async Task SelectContact() 

              {

                  status = "";


                  if (Microsoft.Maui.Devices.DeviceInfo.Platform == Microsoft.Maui.Devices.DevicePlatform.Android)              // Check if the platform is Android ... don't want to run this on iOS or Windows

                  {

                      SelectedContact = await contactService.SelectContact();         // Call the service to select a contact from the phone (contact picker dialog)

                      StateHasChanged();                                              // Refresh the UI to show the selected contact 

                  }

                  else

                  {

                      status = "Only Available when running Android Emulator";

                  }


              } 


               protected override async Task OnInitializedAsync() 

               { 

                  Contacts = await contactService.GetAll();                          // Get all contacts from the phone using the service

               } 


          }

        • For Android implementation you must also go to Platforms/Android/AndroidManifest.xml and check off "READ_CONTACTS"

        • You must connect to your Google Mail account to actually see your  contacts

    • Local Push Notifications (Notify Button ...on Home.razor page of Android Emulator)

      • using the Plugin.LocalNotication Nuget Package...Only for Android Emulator (not visible in Win App)

        • MauiProgram.cs
          var builder = MauiApp.CreateBuilder();

          builder

               .UseMauiApp<App>()     

               .UseLocalNotification()

               .ConfigureFonts(fonts =>

               {

                   fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");

               });

        • Home.razor
          @using Plugin.LocalNotification

          @inject INotificationService notificationService

        • @*Make Notify button only visible in Mobile apps ... not visible in WinUI *@

          @if (DeviceInfo.Platform == DevicePlatform.Android || DeviceInfo.Platform == DevicePlatform.iOS)

          {

              <button class="btn btn-info" @onclick="NotifyUser">Notify</button>

          }

        • private void NotifyUser()

          {       

              //Display an alert here to inform the user that they must turn on the notifications in the Android Emulator settings for the notification to work

              //Specifically they need to allow notifications for the BlazHybridPhoneDemos app in the Android Emulator settings

              App.Current.Windows[0].Page.DisplayAlert("Warning", "You need to allow notifications for the BlazHybridPhoneDemos app in the Android Emulator settings before the Notify button will work. If set properly you will hear a little sound. To actually view the Notification pull down the top of the screen on the Emulator" , "OK");

             

              // Show a notification with a title, description, and scheduled time

              // The notification will appear in the Android Emulator's notification bar after 5 seconds

              notificationService.Show(new NotificationRequest

              {

                  NotificationId = 1,

                  Title = "BlazHybridPhoneDemos",

                  Description = "This is a test notification from BlazHybridPhoneDemos",

                  Schedule = new NotificationRequestSchedule

                  {

                      NotifyTime = DateTime.Now.AddSeconds(5)

                  }

              });

          }

        • Need to add new Platform permission POST_NOTIFICATIONS in AndroidManifest.xml file

        • When executing the Android Emulator , after you press Notify button , you will see a notification in the Notification Bar of the Emulator (you may need to swipe down to see it)

        • All the coding is in the method NotifyUser() on the Home.razor page

    • Biometric Authentication

      • Use the Plugin.Maui.Biometric Nuget Package to add Biometric Authentication to the Blazor Hybrid App.

      • The Biometric Authentication is only for the Android Emulator

        • You will need to add the Biometric permission (USE_BIOMETRIC) in the AndroidManifest.xml file

      • Almost all of the coding is in the MainPage.xaml (BlazorWebView initially invisible) and MainPage.xaml.cs files (most of the coding is here where PIN verification is performed)

        • MainPage.xaml 
          <BlazorWebView

               x:Name="blazorWebView"

               IsVisible="False"

               HostPage=""> <!-- The HTML host page for Blazor -->

               <BlazorWebView.RootComponents>

                   <!-- Register the root Blazor component to render at the #app selector -->

                   <RootComponent

                       Selector="#app"

                       ComponentType="{x:Type components:Routes}" />

               </BlazorWebView.RootComponents>

          </BlazorWebView>

        • Since the Android Emulator does not have a fingerprint sensor or face recognition we will prompt the user to enter a PIN code (12345) to simulate the biometric authentication process.

        • MainPage.xaml.cs
          using Plugin.Maui.Biometric;
          protected override async void OnAppearing()

          {

               base.OnAppearing();


               //To ensure the biometric check only happens the very first time the app is run (an issue when closing XAML pages),

               //we use a persistent flag (e.g., in Preferences) to track if authentication has already occurred.

               //Preferences is a class provided by the Microsoft.Maui.Storage namespace in .NET MAUI.

               //It allows you to store and retrieve simple key-value pairs of data (such as strings, numbers, or booleans) in a persistent way on the device.

               //This is commonly used for saving user settings, flags, or small pieces of data that need to persist between app launches.
          //This is slightly different from Cookies which are typically used for web applications to store session data in the browser.


              //Preferences.Get("HasAuthenticated", false) tries to retrieve a value associated with the key "HasAuthenticated" from the app's persistent storage.

               //If the key does not exist(for example, the first time the app runs), the method returns the default value you provide in this case, false.

               //So the second parameter is the default value to return if the key does not exist.

               //But if the key exists and has a value, it will return that value (true) instead of false.
          //Note: in .NET MAUI, values set using Preferences.Set are persisted across app restarts and device reboots.

          //on the Home.razor page I have added a button to clear the Preferences, so you can test the authentication flow again.

          //Preferences.Remove("HasAuthenticated");


               if (Preferences.Get("HasAuthenticated", false))

               {

                   blazorWebView.IsVisible = true;

                   return;

               }


               // Only check authentication on Android if not running on Android, show the BlazorWebView directly and exit.

               if (DeviceInfo.Platform != DevicePlatform.Android)

               {

                   blazorWebView.IsVisible = true;

                   return;

               }


               // If running on Android, proceed with biometric authentication

               var biometric = BiometricAuthenticationService.Default; // Get the default biometric authentication service

               bool isAuthenticated = false;                           // Flag to track if the user is authenticated


               if (biometric.IsPlatformSupported)

               {

                   var authenticationRequest = new AuthenticationRequest   // Create an authentication request

                   {

                       Title = "Unlock to continue",

                       NegativeText = "Cancel",

                       Subtitle = "Subtitle",

                       Description = "Test",

                       AllowPasswordAuth = true,

                   };


                   // Attempt to authenticate using biometrics

                   AuthenticationResponse response = await biometric.AuthenticateAsync(authenticationRequest, CancellationToken.None);


                   // Check the response status

                   // If the response status is Success, show a success alert and set the authenticated flag

                   // If the response status is not Success, fall back to PIN authentication

                   if (response.Status == BiometricResponseStatus.Success)

                   {

                       await DisplayAlert("Success", "Auth success", "OK");

                       isAuthenticated = true;

                   }

                   else

                   {

                       // Fallback to PIN for any non-successful biometric authentication

                       while (true)

                       {

                           string pin = await DisplayPromptAsync("PIN Required", "Enter your PIN to continue:", "OK", "Cancel", "PIN", maxLength: 10, keyboard: Keyboard.Numeric);

                           if (string.IsNullOrEmpty(pin))

                           {

                               // User cancelled

                               break;

                           }

                           if (pin == "12345")

                           {

                               await DisplayAlert("Success", "PIN correct. Auth success", "OK");

                               isAuthenticated = true;

                               break;

                           }

                           else

                           {

                               await DisplayAlert("Auth Failed", "Incorrect PIN", "OK");

                           }

                       }

                   }

               }

               else

               {

                   // No biometric support at all, fallback to PIN

                   while (true)

                   {

                       string pin = await DisplayPromptAsync("PIN Required", "Enter your PIN to continue:", "OK", "Cancel", "PIN", maxLength: 10, keyboard: Keyboard.Numeric);

                       if (string.IsNullOrEmpty(pin))

                       {

                           // User cancelled

                           break;

                       }

                       if (pin == "12345")

                       {

                           await DisplayAlert("Success", "PIN correct. Auth success", "OK");

                           isAuthenticated = true;

                           break;

                       }

                       else

                       {

                           await DisplayAlert("Auth Failed", "Incorrect PIN", "OK");

                       }

                   }

               }


               // Set the visibility of the BlazorWebView based on authentication status

               // If authenticated, the BlazorWebView will be visible; otherwise, it will be hidden.

               // BlazorWevView displays the Blazor content in the app


              if (isAuthenticated)

               {

                   Preferences.Set("HasAuthenticated", true);  // Store the authentication status in preferences

               }


               blazorWebView.IsVisible = isAuthenticated;

          }

        • Home.razor
          @if (DeviceInfo.Platform == DevicePlatform.Android || DeviceInfo.Platform == DevicePlatform.iOS)

          {

              <button class="btn btn-info btn-sm mb-2" @onclick="NotifyUser">Notify</button>

              <button class="btn btn-info btn-sm mb-2" @onclick="ClearAuth">Clear HasAuthenicated Preference </button>

          }

          private void ClearAuth()

          {       

              // Clear the authenticated preference for testing purposes

              Preferences.Remove("HasAuthenticated");

              App.Current.MainPage.DisplayAlert("Info", "HasAuthenticated preference cleared for testing purposes.", "OK");

          }

    • Swipe Gestures (Swipe.razor)

      • Swipe left to display other button actions

        • Uses the Javascript library hammer.min.js to handle swipe gestures on the Android Emulator or Windows Machine App.

        • The js is kickstarted with the file script.js  located in the scripts folder

          • from there it will create a Hammer instance and attach the swipe event to the swipe-item div located on the Swipe.razor page

          • // Adds a swipe left gesture handler to a DOM element with the given swipeItemId.

            // When swiped left, the element is translated left and associated action buttons are shown.

            function addSwipeHandler(swipeItemId, buttonsId) {

                const swipeItem = document.getElementById(swipeItemId); // Get the swipeable item element

                const actionButtons = document.getElementById(buttonsId); // Get the action buttons element


                if (!swipeItem || !actionButtons)

                    return;


                const hammertime = new Hammer(swipeItem); // Initialize Hammer.js on the swipe item

                hammertime.on("swipeleft", function () {

                    swipeItem.style.transform = "translateX(-150px)"; // Move the item left

                    swipeItem.style.transition = "transform 0.3s ease-in-out"; // Animate the movement

                    actionButtons.classList.add("visible"); // Show the action buttons

                });

            }


            // Restores the swipeable item and hides the action buttons.

            // Resets the transform and transition styles to their original state.

            function restoreSwipe(swipeItemId, buttonsId) {

                const swipeItem = document.getElementById(swipeItemId); // Get the swipeable item element

                const actionButtons = document.getElementById(buttonsId); // Get the action buttons element


                if (!swipeItem || !actionButtons)

                    return;


                swipeItem.style.transform = "translateX(0)"; // Reset the item's position

                swipeItem.style.transition = "transform 0.3s ease-in-out"; // Animate the reset

                actionButtons.classList.remove("visible"); // Hide the action buttons

            }

        • Swipe.razor
          @page "/swipe" 

          @inject IJSRuntime js 

          @inject NavigationManager NavigationManager 


          <div class="alert alert-primary" role="alert" style="font-size: 0.85em;">

              <strong>Purpose:</strong> This page demonstrates how to implement <b>Swipe Gestures</b> in a .NET MAUI Blazor Hybrid app.<br />

              <strong>How it works:</strong>

              <ol class="mb-0">

                  <li>Swipe left on the item to reveal action buttons.</li>

                  <li>Click <b>Click Here</b> to trigger a sample alert.</li>

                  <li>Click <b>Go to Counter Page</b> to navigate to the Counter demo.</li>

                  <li>Click <b>Cancel</b> to restore the swipe item to its original state.</li>

                  <li>Swipe gestures are handled via JavaScript (Hammer.js) and custom logic in <code>script.js</code>.</li>

              </ol>

          </div>


          @*swipe-container and swipe-item are defined in the css file Swipe.razor.css

              swipe-item is the div that contains the swipe item and is the element that will be swiped

              action-buttons is the div that contains the action buttons and will be shown when the swipe item is swiped left

              these are both referenced in the JavaScript function addSwipeHandler and restoreSwipe in the OnAfterRenderAsync Task below

          *@

          <div id="swipe-container" class="swipe-container"> 

              <div id="swipe-item" class="swipe-item"> 

                  <strong>Swipe left for all the options</strong>

              </div> 


              <div id="action-buttons" class="action-buttons"> 

                  <button id="delete-button" class="delete-button btn-sm" @onclick="SampleItem" style="padding-top: 0.25rem; padding-bottom: 0.25rem; font-size: 0.70em;">Click Here</button> 

                  <button id="link-button" class="btn btn-secondary btn-sm" @onclick="LinkItem" style="padding-top: 0.25rem; padding-bottom: 0.25rem; font-size: 0.70em;">Go to Counter Page</button> 

                  <button id="cancel-button" class="cancel-button btn-sm" @onclick="CancelItem" style="padding-top: 0.25rem; padding-bottom: 0.25rem; font-size: 0.70em;">Cancel</button>

              </div> 

          </div> 


          @code { 

              private async Task SampleItem() 

              { 

                  await App.Current.Windows[0].Page.DisplayAlert("Info", "You could execute a method here", "OK"); 

              } 


              private async Task LinkItem() 

              { 

                  NavigationManager.NavigateTo("/counter"); 

              } 


              // This method is called when the cancel button is clicked

              // It will restore the swipe item to its original state

              // swipe-item is the id of the div above that contains the swipe item

              // action-buttons is the id of the div above that contains the action buttons

              private async Task CancelItem() 

              { 

                  await js.InvokeVoidAsync("restoreSwipe", "swipe-item", "action-buttons"); 

              } 


              //Here we are using the OnAfterRenderAsync method to add the swipe handler after the component has rendered for the first time

              //this is necessary to ensure that the JavaScript function is available when the component is rendered

              //addSwipeHandler is a JavaScript function defined in the script.js file that handles the swipe gestures

              //from there it will create a Hammer instance and attach the swipe event to the swipe-item div

              //script.js should be included in the index.html file along with the hammer.min.js library


              protected override async Task OnAfterRenderAsync(bool firstRender) 

              { 

                  if (firstRender) 

                  { 

                      await js.InvokeVoidAsync("addSwipeHandler", "swipe-item", "action-buttons"); 

                  } 

              } 

          }

  • A Deeper Dive into Blazor Hybrid Apps with AI Implementations Part 256:06

    In this Lecture we will

    • Part 2 ... Windows Machine
      Continue our deep dive into Blazor Hybrid applications using Razor pages focusing on Windows only applications with AI implementations dealing with:

      • Images (Camera/Webcam access) + AI Analysis returning a structured response (class based)

      • Audio (Microphone access) with  Transcription and  AI Analysis of Translation

      • Video (Webcam access) with AI Analysis of Video frames and Similarity scoring


    •   Camera (TheCamera.razor)  ... primarily a Windows specific  App with some features available in  the Android Emulator implementation.

      • Access the Camera / Webcam , Take a Photo, Save,Load and perform AI Analysis  (including a custom class for specific property extraction if image conforms )

        • For Android implementation you must also go to:

          Platforms/Android/AndroidManifest.xml and check off "CAMERA" and "WRITE_EXTERNAL_STORAGE"

        • For Windows implementation we are using the FileSystem.AppDataDirectory as our default saving/loading directory

          Photo saved in C:\Users\chiar\AppData\Local\User Name\com.companyname.blazhybridphonedemos\Data\images

        • Also note the use of the CameraService and the Tasks PickAPhotoAsync (loads in any selected photo using MediaPicker ... In Android will open up the Phone Photo Gallery) and TakePhotoAsync (in Windows this will open the local Webcam if available to take snapshot ... in Android Emulator you may get a response that no camera is available to take a picture). Try the Camera (XAML) page for a better implementation which uses the CommunityToolkit.Maui.Camera NuGet Package and the CameraView.

          •   //This service provides a method to load photos via a media picker

                    //In Windows you can pick photos from the file system

                    //In Android you can pick photos from the phone/camera photo gallery

                    public async Task<FileResult> PickAPhotoAsync()

                    {

            #if ANDROID || IOS || MACCATALYST || WINDOWS

                        if (MediaPicker.Default.IsCaptureSupported)

                        {

                            return await MediaPicker.Default.PickPhotoAsync(); 

                        }

                        else

                        {

                            throw new NotSupportedException("Photo access is not supported on this device.");

                        }

            #else

                        throw new NotSupportedException("Photo access is not supported on this device.");

            #endif

                    }

          •         // This method captures a photo using the device's camera.

                   // In Android, it checks for camera and storage permissions.

                  // In iOS, macOS, and Windows, it uses the MediaPicker to capture a photo.
                    public async Task<FileResult> TakePhotoAsync()

                    {

            #if ANDROID

                        var cameraStatus = await Permissions.RequestAsync<Permissions.Camera>();

                        var storageStatus = await Permissions.RequestAsync<Permissions.StorageWrite>();

                        if (cameraStatus != PermissionStatus.Granted || storageStatus != PermissionStatus.Granted)

                            throw new Exception("Camera or storage permission not granted.");

                        var context = Android.App.Application.Context;

                        if (!context.PackageManager.HasSystemFeature(PackageManager.FeatureCameraAny))

                            throw new NotSupportedException("No camera available on this device.");

                        var photo = await MediaPicker.Default.CapturePhotoAsync(new MediaPickerOptions

                        {

                            Title = $"photo_{DateTime.Now:yyyyMMdd_HHmmss}.jpg"

                        });

            #elif IOS || MACCATALYST || WINDOWS

                        if (!MediaPicker.Default.IsCaptureSupported)

                            throw new NotSupportedException("Camera capture is not supported on this device.");

                        var photo = await MediaPicker.Default.CapturePhotoAsync();

            #else

                        throw new NotSupportedException("Camera capture is not supported on this device.");

            #endif

                        if (photo == null)

                            throw new OperationCanceledException("Photo capture was canceled.");

                        return photo;

                    }

          • Don't forget to declare the service in MauiProgram.cs so it can be injected in TheCamera.razor page
            builder.Services.AddScoped<CameraService>();

          • TheCamera.razor
            @inject CameraService cameraService 
            @inject BlazHybridPhoneDemos.Model.WebCamState webCamState 

            private async Task TakePhoto()

            {   


                ClearAll(); // Clear any previous state before capturing a new photo


                if (Microsoft.Maui.Devices.DeviceInfo.Platform == Microsoft.Maui.Devices.DevicePlatform.WinUI)

                {

                    if (App.Current?.Windows?.Count > 0 && App.Current.Windows[0]?.Page != null)

                    {

                        await App.Current.Windows[0].Page.DisplayAlert("Info", "You are running a Windows Machine App ... I will try to open a Webcam if available", "OK");

                    }

                    else

                    {

                        errorMessage = "Unable to display alert. The application window or page is null.";

                    }

                    try

                    {

                        var photo = await cameraService.TakePhotoAsync();       // TakePhotoAsync opens the Webcam on Windows and allows user to take a photo

                        if (photo != null)

                        {

                            lastPhoto = photo;                                  // Store the captured photo in lastPhoto ... used by CanSavePhoto property above and SavePhoto method below

                            photoDataUrl = await GetPhotoDataUrlAsync(photo);   // Convert the photo to a data URL for display

                            webCamState.IsCameraAvailable = true;               // Set the WebCam state to active

                            webCamState.NotifyStateChanged();                   // Notify NavMenu or other  subscribers of the state change

                        }

                    }

                    catch (Exception ex)

                    {

                        errorMessage = "Error taking Photo: " + "<br /><hr />" + ex.Message;

                        webCamState.IsCameraAvailable = false;      // Set the WebCam state to inactive if an error occurs

                        webCamState.NotifyStateChanged();           // Notify NavMenu or other subscribers of the state change


                    }

                }       

                else if (Microsoft.Maui.Devices.DeviceInfo.Platform == Microsoft.Maui.Devices.DevicePlatform.Android)

                {

                    if (App.Current?.Windows?.Count > 0 && App.Current.Windows[0]?.Page != null)

                    {

                        await App.Current.Windows[0].Page.DisplayAlert("Info", "You are running on a mobile device ( Android Emulator) ... so taking a photo with the Camera may not be available using MediaPicker.Default.CapturePhotoAsync ... try the Camera (XAML) version which implements the CommunityToolkit.Maui.Camera NuGet Package and the CamerView", "OK");

                    }

                    else

                    {

                        errorMessage = "Unable to display alert. The application window or page is null.";

                    }

                    try

                    {

                        var photo = await cameraService.TakePhotoAsync();       // TakePhotoAsync opens the Camera on Android if available

                        if (photo != null)

                        {

                            lastPhoto = photo;                                  // Store the captured photo in lastPhoto ... used by CanSavePhoto property above and SavePhoto method

                            photoDataUrl = await GetPhotoDataUrlAsync(photo);   // Convert the photo to a data URL for display

                        }

                    }

                    catch (Exception ex)

                    {

                        errorMessage = "Error taking Photo: " + "<br /><hr />" + ex.Message;

                    }

                }

            }

          • WebCamState.cs
            //used to make Camera(XAML) page available (for Windows) only if camera is active on //computer running application. There is no issue when running Android Emulator.
            public class WebCamState

            {

                public bool IsCameraAvailable { get; set; } = false; // Default to false, assuming camera is not available


                //See NavMenu.razor and TheCamera.razor for Implementation


                // Event to notify subscribers when the camera availability changes

                public event Action? OnChange;


                // Method to set the camera availability and notify subscribers

                public void NotifyStateChanged() => OnChange?.Invoke();
            }

        • Note: The AI Image Analysis uses Microsoft.Extensions.AI and is done using the ChatClient and the GetResponseAsync method. The AI will analyze the image and try to return a structured response (based on the FormResult class ... declared within the Code Section) with the information extracted from the image.

          Note: The AI analysis is done using a custom prompt to extract specific information from the image. 

          • Don't forget to install the Nuget Packages

            • Microsoft.Extensions.AI
              Microsoft.Extensions.AI.OpenAI

          • Don't forget the necessary declarations in the MauiProgram.cs    


            using Microsoft.Extensions.AI;

            using OpenAI;

            // Add AI service to the container ... OpenAI

            var openAiClient = new OpenAIClient("yourKey");

            builder.Services.AddChatClient(new OpenAIChatClient(openAiClient, "gpt-4o-mini"));

            // Register OpenAIServices with HttpClient factory

            builder.Services.AddHttpClient<OpenAIServices>();

          • TheCamera.razor
            @using Microsoft.Extensions.AI

            @inject IChatClient ChatClient


            @*IChatClient is an interface provided by the Microsoft.Extensions.AI library.

            It represents a client for interacting with AI-powered chat services, such as large language models (LLMs).

            In the context of our .NET MAUI Blazor app, it is used to send messages (including text and images) to an AI service and receive responses.*@

    • Audio ... Windows only (Sound.razor)

      • This page allows you to record audio using your device's microphone, play back recordings, save them as MP3 files (file System AppDir) , and load existing audio files (from any local directory) . You can also select and play MP3 files from the app's audio folder (file system App Directory).

      • It also implements some AI integration by Converting Audio to Text (Transcribing), Converting Foreign Audio to English Text (Translating) and Playing back text to audio in a number of voices. For point of comparison the built-in MAUI TextToSpeech.Default.SpeakAsync command is also demonstrated.

      • Make note of how we are leverage the transcribe/translation capabilities to Analyze the audio-to-text (via AI) and pull out specific information according to some predefined properties we declared in a class called MyTasks (see the Analyze Audio button ). We are basically taking unstructured voice input and producing structured data.

      • Programming Details

        • Audio recording and playback is powered by the audioRecorder.js JavaScript module (in the scripts folder of wwwroot).

          • The script provides the microphone access, recording(pause/resume/stop), playback

            • audioRecorder.initRecorder is used by Task InitRecorder()

            • audioRecorder.startRecording is used by Task StartRecording()

            • audioRecorder.pause is used by Task PauseRecording()

            • audioRecorder.resumeRecording is used by Task ResumeRecording()

            • audioRecorder.stopRecording is used by Task StopRecording()

            • audioRecorder.getAudioDataUrl is used by Task SetAudioPlayerSource()  and returns the recorded audio as a base64 data URL (data:audio/mp3;base64,...)

            • audioRecorder.playAudio is used by Task PlayAudio() to play audio that was just recorded as opposed to selected audio from a list where we use
              await JS.InvokeVoidAsync("eval", "document.getElementById('audioPlayer').play();");

          • Note the reference in the index.html
            <script src="scripts/lame.min.js"></script>

            <script src="scripts/audioRecorder.js"></script>

        • Sound.razor

          • Note: This page make a number of Javascript calls to the audioRecorder.js script , implements AI for Analysis (Microsoft.Extensions.AI) and  uses a Text to Speech OpenAI endpoint (stored in the OpenAIServices.cs file in the Services folder)
            @inject OpenAIServices OpenAIServices

            @using Microsoft.JSInterop

            @using Microsoft.Extensions.AI

            @inject IChatClient ChatClient

          • Key Methods/Tasks

            • private async Task StopRecording()

              {

                  await JS.InvokeVoidAsync("audioRecorder.stopRecording");

                   isRecording = false;

                   isPaused = false;

                   hasAudio = true;                //referenced in the PlayAudio method below to indicate that we have recorded audio to play and this also enables the Save button

                   canPlayAudio = true;

                   isPlayingSelectedMp3 = false;   // Reset the isPlayingSelectedMp3 flag since we are not playing a selected MP3 file but rather the recorded audio

                   isSaved = false;                // Reset the isSaved flag, this will be turned on when we reload the audio file after saving it (in the PickAndLoadAudio method)

                   // for AI Transcribing and Translating

                   // The issue is, that after we record, we have a Blob URL eg.

                   // but we need to have a base64 URL in Blazor. Unfortunately C# cannot directly access browser memory blobs so we need to

                   // re-access the audio file after saving it to the File System App Directory ... using the PickAndLoadAudio method below (clicking the Load Audio File button)


                   status = "Stopped. Ready to play or save.";

                   await SetAudioPlayerSource();   // Set the audio player source to the recorded audio

              }

            • // Converts the current Recorded audio to a base64 data URL and sets mUrl ... only used when we record audio

              private async Task SetAudioPlayerSource()

              {       

                  mUrl = await JS.InvokeAsync<string>("audioRecorder.getAudioDataUrl");                       // Use the new JS function to get a base64 data URL

                  await JS.InvokeVoidAsync("eval", $"document.getElementById('audioPlayer').src='{mUrl}';");  // Set the audio player source to the recorded audio data URL

                  canPlayAudio = true;

                  isPlayingSelectedMp3 = false;

                  //<audio id="audioPlayer" src="@mUrl" controls style="display:@(canPlayAudio ? "inline-block" : "none"); vertical-align: middle; max-width: 220px;"></audio>

              } 

            • // This method plays the selected MP3 file or the Audio Recording. 

              private async Task PlayAudio()

              {

                   if (isPlayingSelectedMp3 && !string.IsNullOrEmpty(selectedMp3)) // Check if we are playing a selected MP3 file from the list

                   {

                       //This is a standard JavaScript method for HTML audio/video elements.

                       //It tells the browser to start playing the audio from its current source (src) which is stored in mUrl that came from the SelectMp3 method.

                       await JS.InvokeVoidAsync("eval", "document.getElementById('audioPlayer').play();");

                       status = $"Playing: {selectedMp3}";

                   }

                   else if (hasAudio)                                              // Check if we have recorded audio to play

                   {

                       //This line tells the browser to play the audio that was recorded using the JavaScript audio recorder module.

                       //It's typically used after recording audio, so the user can listen to their recording.

                       await JS.InvokeVoidAsync("audioRecorder.playAudio");

                       status = "Playing recorded audio...";

                   }

              }

            • private async Task SaveSysMp3()

              {

                  //This method saves the recorded audio to the File System App Directory

                  //If the directory does not exist, it will be created and named audio

                  //The audio file will be saved with the name provided in the fileName variable

                  //It saves the audio file in mp3 format

            • private async Task ConvertAudioToText(int type)

              • //The OpenAI API endpoints for audio transcription and translation

                if (type == 1)      // Check if the type is 1 for transcription

                {           

                    response = await httpClient.PostAsync("transcriptions", content);   //This is the endpoint for audio transcription

                    Heading = "Transcription"; // Set the heading for transcription

                }

                else if (type == 2) // Check if the type is 2 for translation

                {           

                    response = await httpClient.PostAsync("translations", content);     //This is the endpoint for audio translation

                    Heading = "Translation"; // Set the heading for translation   

                } 

            • // This method plays the audio from the transcription text using the selected voice

              private async Task PlaySpeech()
              //fetch the new speech URL based on the transcription text (obtained from the ConvertAudioToText method) and selected voice

              AudioUrl = await OpenAIServices.ConvertTextToSpeechAsync(mTranscription,SelectedVoice);

            • public  class OpenAIServices

              //The POST request is sent to the OpenAI API using the HttpClient.PostAsync method.

              //This will use the image api model called dall-e-2 to generate images

              var response = await _httpClient.PostAsync("generations", requestContent);   


    • Video ... Windows only (VideoPage.razor)

      • Create, Save and Load Videos and single rame Images from a live feed  video in a Windows Machine App using a local Webcam . Pull out single frames (images) or multiple frames from the video for AI Analysis and Similarity Scoring (a numerical representation of how alike two images are)

        • There are no video AI analysis buttons on screen (except for the basic single frame/image AI button)

        • Once a user saves and then re-loads a video , 10 evenly spaced frames of the video appear and anyone of them can be clicked on for Analysis or a new button label "Analyze All Captured Frames" can be clicked.

      • This application implements  the OpenCvSharp4 NuGet Package ... specifically  the OpenCvSharp4.runtime.win library which allows us to easily incorporate Computer Vision and Image processing.

        • This library allows us to connect  to cameras/Webcams, take a picture , save the picture (image/frame) , record videos, save videos and re-open video and image files

        • We demonstrate how to  analyze the video (frames) using the Microsoft.Extensions.AI library.

          • We extracts frames evenly spaced across the entire video and then allow the user to analyze any one of them

          • We also allow  the user to send all extracted frames to OpenAI for analysis at the same time.

          • We also add the ability to compare images for Similarity using the OpenCvSharp4 library. Specifically we compare the first and last frames of any selected video using two techniques

            • The first technique calculates the mean difference for each color channel (B, G, R) and averages out these values to get a rudimentary score ... low is better high is worse

            • The second more advanced technique uses the Structural Similarity Index (SSIM), which compares images based on luminance, contrast, and structure, providing a value between -1 and 1 (where 1 means identical images).

        • We can apply this technology to real-world problems like insurance claim analysis. Automate report generation for vehicle damage videos, detailing the car model, year, damage specifics, and incident descriptions. This demonstrates AI’s transformative potential.

      • Programming Details

        • Install the NuGet Package ... OpenCvSharp4.Windows

        • All the Coding is located in the VideoPager.razor file

          • Initial Declarations
            @using Microsoft.Extensions.AI;

            @inject IChatClient ChatClient


            @*allows us to use Directory and Path commands in the code section*@

            @using System.IO


            @*allows us to use the OpenCvSharp4 library*@

            @using OpenCvSharp

          • Key Initial Variable Declarations in the Code Section
            //Variables to check if the application is running on Android used in the

            //InitializeMediaAsync Task to display a warning message

            private bool isAndroid = false;

            private string? androidWarning;   


            //Image related variables

            //---------------------------------------------------------------------------------

            //image  is used only in the ReadFile method:Stores the base64-encoded string of the image file's bytes.

            //Immediately after, imageBase64 is set to a data URL using the value of image.

            private string? image;                                               

            private string? imageBase64; 


            private string? selectedImageBase64;        //used to store the selected image as a base64 string  ... in RecognizeFrameImage(string img) method

                                                        //and HTML section where we display it beside the first frame image


            List<string> fileList = new List<string>(); //List of image files in the wwwroot/images directory (debug folder)

            private long imageFileSize;                 //used to store the image file size   


            //----------------------------------------------------------------------------------


            //Video related variables   

            private string? firstFrameBase64; //used to store the first frame of the video ... used in ReadVideoFile(string file) method and then copied to imageBase64 variable for display and possible AI Analysis

            private string? videoBase64;      //used to store the video as a base64 string ... used in OnFileChangedVideo(InputFileChangeEventArgs e) method and ReadVideoFile(string file) method

            private long videoFileSize;       //used to store the video file size

            private List<string> videoFileList = new List<string>(); //used to store the list of video files in the wwwroot/videos directory (debug folder)

            private string? VideoFileName;    //used to store the name of the video file


            //Camera related variables

            private Mat? currentFrame;          // used to store the current frame from the camera (OpenCvSharp Mat type)

            private string? currentFrameBase64; // used to store the current frame as a base64 string

            private VideoCapture? videoCapture; // used to capture video from the camera (OpenCvSharp VideoCapture type)

            private bool isCameraActive = false;// used to track if the camera is active

            private string? capturedImageBase64; //used to store the captured image from the camera in base64 format

            private List<string> videoFramesBase64 = new List<string>(); // List to store frames as base64 strings

            private string? frameStatus;        //used to display the number of frames captured from the video

            private bool isFrame = false;       //used to track if the image is a frame from a video or a regular image selected from the left side list or taken from the camera

                                                //Set to false when analyzing a regular image, Set to true when analyzing a frame from a video


            //Camera Recording related variables

            private bool isRecording = false;   //used to track if the camera is recording

            private VideoWriter? videoWriter;   // used to write video to file (OpenCvSharp VideoWriter type) ... used in RecordVideo() and StopRecording() methods

            private string? recordedVideoFileName; // used to store the name of the recorded video file   


            //Variables for the Image Recognition AI Form

            private string? responseMessage;

            private string? status;


            //Class to store the structured response from the AI located in the Shared Project

            private FormResult? formResult;

          • Pre-loading currently saved videos and images
            private async Task InitializeMediaAsync()

            {

                //C# lets you add the @ symbol in front of a string to create a

                //verbatim string literal where backslashes are not interpreted as escape characters ... so you can use them to specify file paths


                // Detect Android at startup

                isAndroid = OperatingSystem.IsAndroid();

                if (isAndroid)

                {

                    androidWarning = "Warning: Webcam access only available in Windows Machine ... Not Android";

                }

                else

                {

                    videoFileList.Clear(); // Clear the Video list to avoid duplicates

                    fileList.Clear();      // Clear the Image list to avoid duplicates


                    //Video specific code

                    //----------------------------------------------------------------------------------

                    string path = Directory.GetCurrentDirectory() + @"\wwwroot\videos";           

                    var files = Directory.GetFiles(path, "*.*")

                                         .Where(file => file.EndsWith(".mp4") || file.EndsWith(".mov"))

                                         .ToArray();           


                    //Here we are storing the video file names in a list that we can use to display the video files on the left side of the screen

                    //and to play the video when the user clicks on the file name

                    foreach (var file in files)

                    {

                        videoFileList.Add(Path.GetFileName(file));

                    }


                    //GetFiles ... returns an array of the full names (including paths) for the files in the specified directory

                    //[0] = "C:\\Temp\\Blazor\\Vision\\4-BlazorKernelVisionWebAppUpdate\\BlazorKernelVisionWebApp\\BlazorKernelVisionWebApp\\wwwroot\\images\\beach.jpg"

                    //GetFileName returns the file name and extension of the specified path string ... beach.jpg


                    //Image specific code

                    //----------------------------------------------------------------------------------

                    string Imgpath = Directory.GetCurrentDirectory() + @"\wwwroot\images";

                    var Imgfiles = Directory.GetFiles(Imgpath);


                    //Here we are storing the image file names in a list that we can use to display the image files on the left side of the screen

                    //and display the images when the user clicks on the file name

                    foreach (var file in Imgfiles)

                    {

                        fileList.Add(Path.GetFileName(file));

                    }             

                }       

            }   

          • Key Tasks/Methods

            • //This Task/method is used to handle the video file chosen via the Blazor InputFile component 

              //It is called when the user clicks on the "Blazor InputFile ... not .NET MAUI MediaPicker" button

              private async Task OnFileChangedVideo(InputFileChangeEventArgs e)

            • //This Task used the .NET MAUI FilePicker
              private async Task PickAndLoadVideo()

            • //The ReadFile method is reading a file from the local directory (wwwroot/images) that you have clicked on from a list on screen

              //and it accepts a simple STRING parameter (beach.jpg) for instance
              private void ReadFile(string file)   

            • //The ReadVideoFile method is reading a file from the local directory (wwwroot/video)

              //that you have clicked on from the list on the left side of the screen

              //it accepts a simple STRING parameter (cake.mp4) for instance

              private void ReadVideoFile(string file)

              {

                   Clear();

                   frameStatus = ""; //used to display the number of frames captured from the video

                   
                   //var path = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "videos", file);

                   var path = Directory.GetCurrentDirectory() + @"\wwwroot\videos\" + file;

                   var buffer = File.ReadAllBytes(path);

                   videoBase64 = "data:video/mp4;base64," + Convert.ToBase64String(buffer); //Convert the byte array to a base64 string and set the videoBase64 variable to the base64 string


                   FileInfo fi = new FileInfo(path);

                   videoFileSize = fi.Length;


                   // Extract the first frame of the video

                   //-------------------------------------------------------------------------------------------

                   using var videoCapture = new VideoCapture();            // Create a new VideoCapture object (part of OpenCvSharp library)

                   videoCapture.Open(path, VideoCaptureAPIs.ANY);          // Open the video file ... expecting a file path or a stream, NOT a base64 string or a web URL.

                   //videoCapture.Open(videoBase64, VideoCaptureAPIs.ANY);


                   //declare firstFrameBytes here so it is in scope for the file size calculation

                   byte[] firstFrameBytes = null; //used to store the first frame of the video


                   if (videoCapture.IsOpened())        // Check if the video file is opened successfully

                   {

                       using var frame = new Mat();    // Create a new Mat object to store the frame ... Mat is a class in OpenCvSharp that represents an image or a video frame

                       if (videoCapture.Read(frame))   // Read the first frame

                       {

                           firstFrameBytes = frame.ToBytes(".jpg");                                        // Convert the frame to byte array

                           firstFrameBase64 = "data:image/jpeg;base64," + Convert.ToBase64String(firstFrameBytes); // Convert to base64 string   

                           imageBase64= firstFrameBase64;                                                  //copy first frame to imageBase64 variable for possible Image Analysis (see RecognizeImage Task)

                           imageFileSize= firstFrameBytes.Length;                                          //determine the image file size

                       }             

                   }

                   else

                   {

                       responseMessage = "Unable to open the video file.";

                       return;

                   }


                   // Evenly Spaced Frame Extraction

                   //There was an issue here with videoCapture.Read(frame) not working properly with the base64 string

                   //OpenCvSharp.VideoCapture.Open() ... located above ...  expects a file path or a stream, not a base64 string or a web URL.

                   //Solution:

                   //Pass the actual file path to the video file, not the base64 string.

                   //Where path is declare at the top of the method ... var path = Directory.GetCurrentDirectory() + @"\wwwroot\images\" + file;

                   //videoCapture.Open(path, VideoCaptureAPIs.ANY);       


                   int totalFrames = (int)videoCapture.Get(VideoCaptureProperties.FrameCount); // Get the total number of frames in the video

                   int framesToExtract = 10;                                                   // Number of frames to extract evenly spaced from the video

                   videoFramesBase64.Clear();


                   if (totalFrames > 0 && framesToExtract > 0)

                   {

                       for (int i = 1; i < framesToExtract; i++)

                       {

                           int frameIndex = (int)((i * (totalFrames - 1)) / (framesToExtract - 1));                // Calculate the frame index to extract

                           videoCapture.Set(VideoCaptureProperties.PosFrames, frameIndex);                         // Set the frame position to extract

                           using var newframe = new Mat();


                           if (videoCapture.Read(newframe) && !newframe.Empty())                                   // Read the frame

                           {

                               var frameBytes = newframe.ToBytes(".jpg");                                          // Convert frame to byte array

                               var frameBase64 = "data:image/jpeg;base64," + Convert.ToBase64String(frameBytes);   // Convert to base64

                               videoFramesBase64.Add(frameBase64);                                                 // Add to list 

                           }

                       }

                   }

                   frameStatus = "<b>Captured:</b> " + videoFramesBase64.Count.ToString() + " evenly spaced frames from video...<b>Click on any of these images for simple AI Analysis</b> <br/>You can also choose the <b>Analyze First Frame </b> button to analyze the larger image (first frame) below";

                   frameStatus += "<hr/>";


                   //Checking frame images for Similarity using OpenCvSharp

                   //Compare the first and last images in the list videoFramesBase64 using OpenCvSharp

                   //This will give us a simple similarity score based on the absolute difference between the two images

                   //This is a simple metric; you might want to use a more sophisticated method for real applications

                   //For example, you could use structural similarity index (SSIM) or other metrics depending on your needs


                   if (videoFramesBase64.Count >= 2)

                   {

                       // Decode base64 to byte arrays

                       

                       //... for instance , videoFramesBase64[0] could be a string like:

                       //data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD... (Base64 data)

                       //.Split(',')[1] Splits the string at the comma.

                       //[0] is "data:image/jpeg;base64" (the header)

                       //[1] is the actual Base64-encoded image data


                       var firstImageBytes = Convert.FromBase64String(videoFramesBase64[0].Split(',')[1]);

                       var lastImageBytes = Convert.FromBase64String(videoFramesBase64[^1].Split(',')[1]); // Using ^1 to get the last item in the list


                       // Load as Mat objects

                       using var firstMat = Cv2.ImDecode(firstImageBytes, ImreadModes.Color);

                       using var lastMat = Cv2.ImDecode(lastImageBytes, ImreadModes.Color);


                       // Resize to same size if needed

                       if (firstMat.Size() != lastMat.Size())

                       {

                           Cv2.Resize(lastMat, lastMat, firstMat.Size());

                       }


                       //Method 1 - using a simple absolute difference metric

                       //---------------------------------------------------------------------------------------


                       // Calculate absolute difference between the first and last frames

                       // This will give us a Mat object that contains the absolute difference between the two images

                       using var diff = new Mat();

                       Cv2.Absdiff(firstMat, lastMat, diff);


                       // Calculate a simple similarity metric (mean difference)

                       // This will give us a Scalar object that contains the mean difference for each channel (B, G, R)

                       var meanDiff = Cv2.Mean(diff);


                       // The meanDiff is a Scalar, so we access its values using Val0, Val1, Val2

                       // The Val0, Val1, Val2 properties correspond to the B, G, R channels respectively

                       // and we calculate the average of these values to get a similarity score 

                       // A low number indicates high similarity, while a high number indicates low similarity

                       // Note: This is a simple metric; you might want to use a more sophisticated method for real applications

                       // For example, you could use structural similarity index (SSIM) or other metrics depending on your needs


                       double similarityScore = (meanDiff.Val0 + meanDiff.Val1 + meanDiff.Val2) / 3.0;


                       // Optionally, display or log the similarity score ... attach to current contents of frameStatus

                       frameStatus += $"<b> ... Checking Images for Similarity : Similarity score (first vs last frame):</b> {similarityScore:F2}";

                       frameStatus += "(low number high similarity, a high number low similarity)"; // Add explanation


                       // Method 2 - Using Structural Similarity Index (SSIM)

                       //--------------------------------------------------------------------------

                       double ssimScore = CalculateSSIM(firstMat, lastMat);

                       frameStatus += $"<br/><b> ... Structural Similarity Index (SSIM) similarity score (first vs last frame):</b> {ssimScore:F4}";

                       frameStatus += "(1.0 = identical, closer to 0 = less similar)";

                   }

                   StateHasChanged();


              }

            • // Structural Similarity Index (SSIM), which compares images based on luminance, contrast, and structure,

              // providing a value between -1 and 1 (where 1 means identical images).

              // Helper method to compute SSIM between two Mat images

              private double CalculateSSIM(Mat img1, Mat img2)

            • //This method is called when the user clicks on the "Analyze First Frame from Captured Video or Image from Photo Taken or Selected Image from wwwroot" button

              //It is used to analzye a selected Image (from the list) OR from the Take Picture and Save (button) Or the Default first frame of a selected Video

              //all these sources store the image in the variable imageBase64 so we can use it here

              private async Task RecognizeImage()   

            • //This method is called when the user clicks one of the video frames displayed on the screen (form approx 9-10 frames)

              private async Task RecognizeFrameImage(string img)

            • //This method is called when the user clicks the "Analyze All Captured Frames of Video" button which is located to the right of the  9/10 frames of the video

              //Note that this method is similar to the RecognizeFrameImage method but it processes a List of images (frames) instead of a single image

              //The list of images (frames) is passed to this method as a parameter ... List<string> frameImgs

              //RecognizeFrameImages(videoFramesBase64))

              private async Task RecognizeFrameImages(List<string> frameImgs)

              {         

                  status = "";


                  if (frameImgs == null || frameImgs.Count==0)

                  {

                      responseMessage = "Please select an video first to break apart into frames.";

                      status = "Error1";

                      return;

                  }


                  isFrame = true;

                  status = "Processing";

                  responseMessage = null;


                  //Remember here we are sending a LIST of images (frames) to the AI service not a single image

                  var message = new ChatMessage(ChatRole.User, "Look through all the frame images and summarize the contents, display the results in nicely formatted HTML");


                  // Add all images in frameImgs to the message contents

                  foreach (var img in frameImgs)

                  {

                      message.Contents.Add(new DataContent(img, "image/jpg"));

                  }

                  //message.Contents.Add(new DataContent(img, "image/jpg"));       


                  var response1 = await ChatClient.GetResponseAsync(message);

                  responseMessage= response1.Text; //display the response message


                  status = "Results"; //update the status to show results       


              }   

            • private async Task StartCamera()

              {

                  if (isCameraActive)         //this is a boolean flag to track if the camera is already active it is set to true when the camera is started below

                  {

                      return;

                  }

                  Clear();


                  isCameraActive = true;

                  videoCapture = new VideoCapture(0); // Open the default camera


                  if (!videoCapture.IsOpened())       // Check if the camera is opened successfully

                  {

                      status = "Unable to access the camera";

                      isCameraActive = false;

                      return;

                  }


                  //This loop will run until the camera is stopped

                  //it will read frames from the camera and convert them to base64 strings for display on the screen

                  while (isCameraActive)

                  {

                      //The Mat type is from the OpenCvSharp library, which is a .NET wrapper for the popular OpenCV computer vision library.

                      using var frame = new Mat();

                      videoCapture.Read(frame);   // Read a frame from the camera


                      if (!frame.Empty())

                      {

                          currentFrame = frame.Clone();                                                           // Clone the frame to avoid memory issues

                          var frameBytes = currentFrame.ToBytes(".jpg");                                          // Convert the frame to a byte array

                          currentFrameBase64 = "data:image/jpeg;base64," + Convert.ToBase64String(frameBytes);    // Convert the byte array to a Base64 string to display it in the browser

                          StateHasChanged();                                                                      // Update the UI with the new frame

                      }


                      await Task.Delay(33); // ~30 FPS

                  }

              }

            • private void StopCamera()

            • //This method is called when the user clicks the "Take Picture" button

              //It captures the current frame from the camera and saves it to a file in the wwwroot/images directory

              private async Task TakePicture()

              {       


                  //Make sure we have a valid filename for the image file

                  if (string.IsNullOrEmpty(VideoFileName))

                  {

                      status = "Please enter Image File Name to Save";

                      await App.Current.Windows[0].Page.DisplayAlert("Warning", "Please Enter a File Name for the Image", "OK");

                      return;

                  }


                  if (currentFrame == null)

                  {

                      return;

                  }


                  var frameBytes = currentFrame.ToBytes(".jpg");                                       // Capture the current frame and convert it to a base64 string

                  capturedImageBase64 = "data:image/jpeg;base64," + Convert.ToBase64String(frameBytes);// Convert the byte array to a Base64 string

                  imageBase64 = capturedImageBase64;                                                   //copy current frame to imageBase64 variable for display and possible Img Analysis


                  imageFileSize = frameBytes.Length;


                  // Save the captured image to a file in the wwwroot/images directory (debug folder) with a unique name  and a timestamp and the prefix "captured_"
              //As opposed to the Sound.razor page where we saved to the AppData folder

              //Path.Combine(FileSystem.AppDataDirectory, "audio");

                  var fileName = "captured_" + VideoFileName + "-" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".jpg";         

                  var filePath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "images", fileName);

                  await File.WriteAllBytesAsync(filePath, frameBytes);    // Save the image to the file


                  await InitializeMediaAsync(); // Reinitialize media files to include the newly captured image

                  StateHasChanged();

              }

            • //This method is called when the user clicks the "Record Video" button

              //It captures video from the camera and saves it to a file (with a user provided filename) in the wwwroot/videos directory

              private async Task RecordVideo()

              {

                  //We go through a number of error checks first before we start recording


                  //this is a boolean flag to track if the camera is already recording it is set to true when the camera is started below

                  if (isRecording)           

                      return;


                  //Make sure we have a valid filename for the video file

                  if (string.IsNullOrEmpty(VideoFileName))

                  {

                      status = "Please enter Video File Name to Save";

                      return;

                  }

                  imageBase64 = null; //clear the image base64 variable to avoid confusion with the video recording

                  StateHasChanged();


                  status = "";


                  //1.Check if the camera is already open:

                  //videoCapture == null checks if the videoCapture object hasn't been created yet.

                  //!videoCapture.IsOpened() checks if the camera isn't currently open.

                 

                  //2.Open the default camera:

                  //videoCapture = new VideoCapture(0); tries to open the default camera (usually the first webcam).

                 

                  //3.Handle failure to open:

                  //If the camera still isn't open after trying, it sets a status message:

                  //status = "Unable to access the camera"; and exits the method with return;.

                 
              if (videoCapture == null || !videoCapture.IsOpened())

                  {

                      videoCapture = new VideoCapture(0);

                      if (!videoCapture.IsOpened())

                      {

                          status = "Unable to access the camera";

                          return;

                      }

                  }


                  isRecording = true;


                  //You use VideoWriter to save a sequence of images (frames) as a video file (e.g., .mp4).

                  //You must specify the output file path, codec(like H.264), frame rate, and frame size when creating it.

                  //Each time you call .Write(frame), it adds that frame to the video.

                  //When done, you call.Release() or.Dispose() to finish and close the file.

                 

                  // H.264 offers better performance and fewer crashes compared to some older codecs .

                  // The resulting .mp4 files are widely compatible.

                 

                  //var fourcc = VideoWriter.FourCC('M', 'P', '4', 'V'); // MP4 codec

                  var fourcc = VideoWriter.FourCC('X', '2', '6', '4'); // H.264 codec ... better performance no crashes       

                 

                  int frameWidth = (int)videoCapture.Get(VideoCaptureProperties.FrameWidth);

                  int frameHeight = (int)videoCapture.Get(VideoCaptureProperties.FrameHeight);

                  double fps = videoCapture.Get(VideoCaptureProperties.Fps);

                  if (fps <= 0) fps = 20; // fallback if FPS not detected


                  // Generate a unique name for the recorded video file including the date and the file name and the prefix "recorded_"

                  recordedVideoFileName = "recorded_" + VideoFileName + "-" + DateTime.Now.ToString("yyyyMMdd") + ".mp4";

                  var filePath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "videos", recordedVideoFileName);


                  videoWriter = new VideoWriter(filePath, fourcc, fps, new OpenCvSharp.Size(frameWidth, frameHeight));


                  // Check if the video writer is opened successfully ... VideoWriter is part of OpenCvSharp library       

                  if (!videoWriter.IsOpened())

                  {

                      status = "Unable to create video file";

                      isRecording = false;

                      return;

                  }


                  status = "Recording started...";

                  StateHasChanged();



                  // Capture frames from the camera and write them to the video file

                  // This loop will run until the camera is stopped or the recording is stopped

                  // The Task.Run is used to run the video capture in a separate thread to avoid blocking the UI thread

                 

                  await Task.Run(async () =>

                  {

                      using var frame = new Mat();

                      while (isRecording && videoCapture.IsOpened())  //if we press the Stop Recording button, isRecording will be set to false

                      {

                          videoCapture.Read(frame);

                          if (!frame.Empty())

                          {

                              videoWriter.Write(frame);               // Write the frame to the video file

                          }

                          await Task.Delay(33); // ~30 FPS

                      }

                  });


                  // Stop the recording and release the video writer

                  videoWriter.Release();

                  videoWriter.Dispose();

                  videoWriter = null;


                  // Add to video list for UI

                  await InvokeAsync(() =>

                   {

                      if (!string.IsNullOrEmpty(recordedVideoFileName))

                          videoFileList.Add(recordedVideoFileName);     // Add the recorded video file to the list of video files

                      status = "Recording stopped. Video saved.";


                      InitializeMediaAsync(); // Reinitialize media files to include the newly recorded video

                      StateHasChanged();

                  });

              }

            • private void StopRecording()

            • private void Clear()

            • //class used in the RecognizeImage method to hold the result of the possible form image that was recognized by the AI.

              public class FormResult

              {

                  public string? ReportBy { get; set; }

                  public string? ReportDate { get; set; }

                  public string? ReportPhone { get; set; }

                  public string? AttendantName { get; set; }

                  public string? AttendantPhone { get; set; }

                  public string? ReceivedDate { get; set; }

                  public string? EnteredDate { get; set; }

              }

          • Key HTML (See HTML snippet)
             

  • A Deeper Dive into Blazor Hybrid Apps with AI Implementations Part 358:17

    In this Lecture we will

    • Part 3 ... XAML pages and Android
      Continue our deep dive into Blazor Hybrid applications this time using  XAML pages  and focusing on Android Emulator execution (most work on Windows Machine mode also). We again look at implementations that cover:

      • Images

      • Image Generation

      • Audio and Video Playback

    • We will  re-visit how to create and work with .NET MAUI ContentPage XAML pages, and  how to edit/update the MainPage.xaml and MauiProgram.cs files.

    • Camera (XAML CameraMAUI.xaml)

      • Access the camera , take a picture, save (AppDirectory images folder) and load a picture on a XAML page (.NET MAUI ContentPage). Also displays a ListView of the contents of the AppDirectory for user selection.

        • This page is  available for Android but only available in Windows if a webcam is active. If no webcam is available when running application as a Desktop application it may crash. So we implemented State Management by creating a class called WebCamState with the  property IsCameraAvailable. The property is initially false. You must link to the Camera Image + AI page first and initiate Take a Photo . If a Webcam is present we set the IsCameraAvailable to true. The NavMenu is then modified to make the Line to Camera (XAML) available.

        • Check the NavMenu.razor for full details

      • For this implementation we use the CameraView for .NET MAUI which is made available by installing  the  CommunityToolkit.Maui.Camera Nuget package

        • Make sure to declare the Toolkit in MauiProgram.cs
          var builder = MauiApp.CreateBuilder();

          builder

              .UseMauiApp<App>()

              .UseMauiCommunityToolkitMediaElement()

              .UseMauiCommunityToolkitCamera()

              .UseLocalNotification()

              .ConfigureFonts(fonts =>

              {

                  fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");

              });

        • In order to make use of the toolkit within XAML you add this namespace to the CameraMAUI.xaml page
          <ContentPage xmlns=""

                       xmlns:x=""

                       xmlns:toolkit=""

                       x:Class="BlazHybridPhoneDemos.Components.Pages.CameraMAUI"

                       Title="CameraMAUI"

                       BackgroundColor="Gray">

        • One last prep ... we need to add some permissions in the AndroidManifest.xml file

          • check off Camera

      • CameraMAUI.xaml (add the CameraView)

        <ContentPage.Resources>

            <Style TargetType="Button" x:Key="BlueWhiteButtonStyle">

                <Setter Property="BackgroundColor" Value="Blue" />

                <Setter Property="TextColor" Value="White" />

            </Style>

        </ContentPage.Resources>


        <ScrollView Orientation="Vertical" Padding="20">

           

            <VerticalStackLayout

             VerticalOptions="Center" 

             HorizontalOptions="Center" 

             Spacing="10">


                <Label 

                 x:Name="myLabel" 

                 Text=".NET MAUI Camera Implementation" 

                 TextColor="Yellow" 

                 VerticalOptions="Center" 

                 HorizontalOptions="Center" />


               

                <!--When the camera successfully captures media (like a photo or video), the MyCamera_MediaCaptured method in the code-behind (C# file) will be called automatically.-->

               

                <Frame

                    BackgroundColor="Gray"

                    BorderColor="Blue"

                    CornerRadius="8"

                    Padding="8"

                    HorizontalOptions="Center"

                    VerticalOptions="Center"

                    HasShadow="True">


                    <toolkit:CameraView

                        x:Name="MyCamera"

                        MediaCaptured="MyCamera_MediaCaptured"

                        WidthRequest="250"

                        HeightRequest="250"

                        HorizontalOptions="Center"

                        VerticalOptions="Center"

                        BackgroundColor="Gray"

                        IsVisible="False" />


                </Frame>


                <HorizontalStackLayout HorizontalOptions="Center" Spacing="10">

                    <Button Text="Turn Camera ON/OFF" Clicked="CameraOnButton" Style="{StaticResource BlueWhiteButtonStyle}" WidthRequest="170" />

                    <Button Text="Take Picture" Clicked="TakePictureButton_Clicked" Style="{StaticResource BlueWhiteButtonStyle}" WidthRequest="170" />

                </HorizontalStackLayout>


                <!-- ListView for displaying image names -->

                <Frame

                    BorderColor="Blue"

                    CornerRadius="8"

                    Padding="8"

                    BackgroundColor="White"

                    HasShadow="True"

                    HorizontalOptions="Center"

                    VerticalOptions="Center"

                    Margin="0,10">


                    <VerticalStackLayout Spacing="6">

                        <Label

                            Text="AppDirectory Images"

                            FontAttributes="Bold"

                            FontSize="16"

                            TextColor="Green"

                            HorizontalOptions="Center" />

                        <BoxView

                            HeightRequest="1"

                            WidthRequest="180"

                            Color="Gray"

                            HorizontalOptions="Center"

                            Margin="0,4" />

                       <ListView x:Name="ImagesListView"

                  ItemsSource="{Binding ImageNames}"

                  SelectionMode="Single"

                  HeightRequest="120"

                  WidthRequest="200"

                  BackgroundColor="White"

                  Margin="0,0,0,0"

                  ItemSelected="ImagesListView_ItemSelected"

                  RowHeight="18">

            <ListView.ItemTemplate>

                <DataTemplate>

                    <ViewCell>

                        <StackLayout Orientation="Horizontal" Padding="2,0" Spacing="4">

                            <Label Text="{Binding .}" TextColor="Blue" FontSize="10" VerticalOptions="Center" />

                        </StackLayout>

                    </ViewCell>

                </DataTemplate>

            </ListView.ItemTemplate>

        </ListView>

                    </VerticalStackLayout>

                </Frame>


                <Frame

                BorderColor="Blue"

                CornerRadius="8"

                Padding="4"

                HasShadow="False"

                HorizontalOptions="Center"

                VerticalOptions="Center">

                   

                    <Image

                    x:Name="MyImage"

                    Source="{x:Null}"

                    WidthRequest="150"

                    HeightRequest="150"

                    Aspect="AspectFit"

                    HorizontalOptions="Center"/>

                   

                </Frame>


                <HorizontalStackLayout HorizontalOptions="Center" Spacing="10">

                    <Button Text="Save" Clicked="Button_Save" Style="{StaticResource BlueWhiteButtonStyle}" WidthRequest="100" BackgroundColor="Blue" />

                    <Button Text="Load" Clicked="Button_Load" Style="{StaticResource BlueWhiteButtonStyle}" WidthRequest="100" BackgroundColor="Blue" />

                </HorizontalStackLayout>



                <HorizontalStackLayout HorizontalOptions="Center" Spacing="10">

                    <Button Text="Clear All" Clicked="Button_Clear" Style="{StaticResource BlueWhiteButtonStyle}" WidthRequest="100" BackgroundColor="Red" />

                    <Button Text="Close" Clicked="Button_Close" Style="{StaticResource BlueWhiteButtonStyle}" WidthRequest="100" BackgroundColor="Red" />

                </HorizontalStackLayout>


            </VerticalStackLayout>


        </ScrollView>

        • CameraMAUI.xaml .cs

          using CommunityToolkit.Maui.Core;

          using CommunityToolkit.Maui.Core.Primitives;

          using Microsoft.Maui.Controls;

          using System.Collections.ObjectModel;

          using System.IO;

          using System.Linq;

          public partial class CameraMAUI : ContentPage

          {

              public ObservableCollection<string> ImageNames { get; set; } = new();
             
          // Cache for last captured image bytes ...necessary because of issue with a closed stream in    MediaCaptured event before saving the image.

          private byte[] _lastCapturedImageBytes;


              public CameraMAUI()

              {

                  InitializeComponent();

                  BindingContext = this;

                  LoadImageNames();

              }


              private void LoadImageNames()

              {

                  string imagesDir = Path.Combine(FileSystem.AppDataDirectory, "images");

                  if (!Directory.Exists(imagesDir))

                      Directory.CreateDirectory(imagesDir);


                  var files = Directory.GetFiles(imagesDir)

                      .Where(f => f.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) || f.EndsWith(".png", StringComparison.OrdinalIgnoreCase))

                      .Select(Path.GetFileName)

                      .ToList();


                  ImageNames.Clear();

                  foreach (var file in files)

                      ImageNames.Add(file);

              }


              private void ImagesListView_ItemSelected(object sender, SelectedItemChangedEventArgs e)

              {

                  //This line checks if the selected item in the ListView is a string and, if so, assigns it to the variable fileName:

                  //If the selected item is not a string or is null, this line will not execute.

                  if (e.SelectedItem is string fileName)

                  {

                      string imagesDir = Path.Combine(FileSystem.AppDataDirectory, "images");

                      string filePath = Path.Combine(imagesDir, fileName);

                      MyImage.Source = ImageSource.FromFile(filePath);                       

                  }

              }


              // This event is triggered when the camera captures an image in the TakePictureButton_Clicked method.

          // The MediaCapturedEventArgs contains the captured media stream.
                private void MyCamera_MediaCaptured(object sender, CommunityToolkit.Maui.Views.MediaCapturedEventArgs e)

              {

                 // This method sets the captured image to the Image control and caches the image bytes. We do both because we need to display the image immediately and also save it later.

          // Before using this technique we had an issue where the stream was closed before we could save the image, so we cache the bytes in a field.


          // This local method handles the image setting and caching.

          // It is called inside the Dispatcher to ensure it runs on the main thread or directly if the Dispatcher is not required


              //The CameraView control starts the camera preview automatically when it is visible ... we have set it to not visible by default.

              //This method was added because .NET MAUI does not have a built-in button to toggle the camera on and off.

              //Instead, we create a button that toggles the visibility of the camera view   


              private async void CameraOnButton(object sender, EventArgs e)

              {

                  //if the camera is currently visible, hide it and stop the preview else show it and start the preview.

                  if (MyCamera.IsVisible)

                  {

                      MyCamera.IsVisible = false;

                      if (MyCamera is ICameraView cameraView) 

                      {

                          //This ensures you only call methods defined in ICameraView

                          //(like StartCameraPreview or StopCameraPreview) if MyCamera actually supports them.

                          cameraView.StopCameraPreview();

                      }

                  }

                  else

                  {

                      MyCamera.IsVisible = true;

                      if (MyCamera is ICameraView cameraView)

                      {

                          // Starting the camera preview and passing CancellationToken.None to indicate that you don't intend to cancel this operation.

                          await cameraView.StartCameraPreview(CancellationToken.None);

                      }

                  }

              }


              private async void TakePictureButton_Clicked(object sender, EventArgs e)

              {

                  // Trigger the camera to take a picture

                  if (!MyCamera.IsVisible)

                  {

                      await DisplayAlert("Camera", "Please turn on the camera first.", "OK");

                      return;

                  }

                  try

                  {

                      await MyCamera.CaptureImage(CancellationToken.None);    // This will trigger the MediaCaptured event above

                  }

                  catch (Exception ex)

                  {

                      // Handle or log the exception as needed

                      await DisplayAlert("Error", $"Failed to capture image: {ex.Message}", "OK");

                  }

              }


           // This method is called when the user clicks the Save button ...   

          // It uses the cached image bytes (photo just taken) or retrieves the image from the ImageSource to save it to the device.

             private async void Button_Save(object sender, EventArgs e)

          {

              // Check if there is an image to save

              if (MyImage.Source == null)

              {

                  await DisplayAlert("Error", "No image to save.", "OK");

                  return;

              }


              string userFileName = await DisplayPromptAsync("Save Image", "Enter a name for the image file:");

              if (string.IsNullOrWhiteSpace(userFileName))

              {

                  await DisplayAlert("Error", "File name cannot be empty.", "OK");

                  return;

              }


              // _lastCapturedImageBytes is a private field that stores the raw bytes of the most recently captured image from the camera.

              //• imageData is a local variable(in the Button_Save method) that will hold the image data to be saved to disk.

              //• By assigning _lastCapturedImageBytes to imageData, the code ensures that the most recent photo taken by the camera is used for saving, if available.

              //Why is this important ?

              //• This approach avoids issues with closed streams(which can happen if you try to access the camera image stream after the event has finished).

              //• It ensures the image being saved is exactly what was just captured, without needing to re-fetch or re-convert the image.


              try

              {

                  byte[] imageData = null;


                  // Use cached image bytes if available (best and most correct option for recently take photo) or retrieve from ImageSource

                  if (_lastCapturedImageBytes != null && _lastCapturedImageBytes.Length > 0)

                  {

                      imageData = _lastCapturedImageBytes;

                  }

                  else if (MyImage.Source is StreamImageSource streamImageSource)

                  {

                      var streamCallback = streamImageSource.Stream;

                      if (streamCallback != null)

                      {

                          using (var stream = await streamCallback.Invoke(CancellationToken.None))

                          {

                              if (stream != null)

                              {

                                  using (var ms = new MemoryStream())

                                  {

                                      await stream.CopyToAsync(ms);

                                      imageData = ms.ToArray();

                                  }

                              }

                          }

                      }

                  }

                  else if (MyImage.Source is FileImageSource fileImageSource)

                  {

                      var fileImagePath = fileImageSource.File;

                      if (File.Exists(fileImagePath))

                      {

                          imageData = File.ReadAllBytes(fileImagePath);

                      }

                  }


                  if (imageData == null || imageData.Length == 0)

                  {

                      await DisplayAlert("Error", "Could not access image data.", "OK");

                      return;

                  }


                  // Save the image data

                  string imagesDir = Path.Combine(FileSystem.AppDataDirectory, "images");

                  if (!Directory.Exists(imagesDir))

                      Directory.CreateDirectory(imagesDir);


                  string fileName = userFileName + "_" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".jpg";

                  string filePath = Path.Combine(imagesDir, fileName);


                  // Write the data to the file

                  await File.WriteAllBytesAsync(filePath, imageData);


                  // Add the saved image to the collection

                  ImageNames.Add(fileName);


                  await DisplayAlert("Success", "Image saved successfully.", "OK");

              }

              catch (Exception ex)

              {

                  await DisplayAlert("Error", $"Failed to save image: {ex.Message}", "OK");

              }

          }

             

              private async void Button_Load(object sender, EventArgs e)

              {

                  // Load an image from the device's photo library (Android/iOS) or file system (Windows)

                  try

                  {

                       //result is of type FileResult which contains the path to the selected image
                     var result = await MediaPicker.PickPhotoAsync(new    MediaPickerOptions

                      {

                          Title = "Please select a photo"

                      });

                      if (result != null)

                      {

                          // Display the selected image in the Image control

                          MyImage.Source = ImageSource.FromFile(result.FullPath);
                         //we use FullPath to get the absolute path

                      }

                  }

                  catch (Exception ex)

                  {

                      // Handle or log the exception as needed

                      await DisplayAlert("Error", $"Failed to load image: {ex.Message}", "OK");

                  }

              }      

          }

    • Generate an Image ( on new separate .NET MAUI ContentPage XAML ... NewPage2.xaml)

      • User can enter an idea for an image and the page generates the image. You also have the ability to save the image to the AppDirectory generated folder. A ListView of the contents of previous generated images is also displayed with the ability to chosen any for re-display.

      • Link is available right in TheCamera.razor page (Generate Image) and NavMenu.razor (Gen Images AI)

        • <div class="nav-item px-3">

              <a class="nav-link" href="#" @onclick="GoMaui">

                  <span class="bi bi-brush" aria-hidden="true"></span> Gen Images AI (XAML)

              </a>

          </div>

        • @code {

              private void GoMaui()

              {

                  App.Current.MainPage.Navigation.PushModalAsync(new Pages.NewPage2());

              }
          }

      • NewPage2.xaml
        <ContentPage xmlns=""

                     xmlns:x=""

                     x:Class="BlazHybridPhoneDemos.Components.Pages.NewPage2"

                     Title="OpenAI Image API"

                     BackgroundColor="Gray">


            <ContentPage.Resources>

                <Style TargetType="Button" x:Key="BlueWhiteButtonStyle">

                    <Setter Property="BackgroundColor" Value="Blue" />

                    <Setter Property="TextColor" Value="White" />

                </Style>

            </ContentPage.Resources>


            <VerticalStackLayout

            VerticalOptions="Center"

            HorizontalOptions="Center"

            Spacing="10">


                <Label

                x:Name="myLabel"

                Text="Welcome to .NET MAUI!"

                TextColor="Yellow"

                VerticalOptions="Center"

                 HorizontalOptions="Center" />


                <Button Text="Click Here for Application Info" Clicked="OnButtonClicked" Style="{StaticResource BlueWhiteButtonStyle}" />

               


                <!-- IsToggled="True": The switch starts in the ON position by default.

                     Toggled="OnToggled": When the user toggles the switch,

                     it triggers the OnToggled event handler in your code-behind (C# file). -->

                <Switch OnColor="Orange" ThumbColor="Green" IsToggled="True" Toggled="OnToggled" />


                <Label

                x:Name="PromptLabel"

                Text="Enter idea for image"

                TextColor="Yellow"   

                SemanticProperties.HeadingLevel="Level1"

                FontSize="22"

                HorizontalOptions="Center" />

               

                <Entry

                x:Name="userPrompt"

                Placeholder="Enter your idea for image"

                PlaceholderColor="Black"

                SemanticProperties.Hint="Enter image to generate"

                HorizontalOptions="Center"

                WidthRequest="300"

                HeightRequest="50"/>


                <HorizontalStackLayout HorizontalOptions="Center">

                    <HorizontalStackLayout HorizontalOptions="Center" Spacing="15">

                        <Button

                            x:Name="ImageBtn"

                            Text="Generate"

                            SemanticProperties.Hint="Generate Images"

                            Clicked="OnImageClicked"

                            HorizontalOptions="Center"

                            Style="{StaticResource BlueWhiteButtonStyle}" />

                       

                </HorizontalStackLayout>


                <!-- The Image source defaults to the Resources folder Images -->

                <Image

                x:Name="myImage"

                Source="dotnet_bot.png"

                SemanticProperties.Description="Test"

                HeightRequest="200"

                HorizontalOptions="Center"/>


                <Button Text="Close" Clicked="Button_Close" Style="{StaticResource BlueWhiteButtonStyle}" />


            </VerticalStackLayout>


        </ContentPage>

      • NewPage2.xaml.cs
        using BlazHybridPhoneDemos.Services;

        namespace BlazHybridPhoneDemos.Components.Pages;


        public partial class NewPage2 : ContentPage

        {

        OpenAIServices openAIServices;

        private List<string> imageUrls = new List<string>();



            public NewPage2()

        {

        InitializeComponent();

        openAIServices = new OpenAIServices(new HttpClient()); // Initialize OpenAIServices with HttpClient

            }

           

            async void OnButtonClicked(object sender, EventArgs args)

            {

                await TextToSpeech.Default.SpeakAsync("This Blazor Hybrid Application demonstrates how to connect to the " +

                    "OpenAI Image API given an idea for an image");

            }


            // When the Button 'Generate" is pressed ... call the method to get generated images based on user input (userPrompt.Text)

            private async void OnImageClicked(object sender, EventArgs e)

            {

                await GetGeneratedImages(userPrompt.Text);             

            }


            // This method is called when the Clear button is clicked

            private async void OnClearClicked(object sender, EventArgs e)

            {

                PromptLabel.Text = "Enter your idea for image";

                userPrompt.Text = string.Empty;

                myImage.Source = null;

            }


            // This method is called when the toggle switch is toggled

            // It sets the visibility of the image based on the toggle state

            void OnToggled(object sender, ToggledEventArgs e)

            {

                if (e.Value == true)

                {

                    myImage.IsVisible = true;

                }

                else

                {

                    myImage.IsVisible = false;

                }

            }   


            private async Task GetGeneratedImages(string prompt)

            {

                if (string.IsNullOrWhiteSpace(prompt))

                {

                    await DisplayAlert("Error", "Please enter an image idea", "OK");

                    return;

                }


                PromptLabel.Text = "Working on it ...";


                //imageUrls will contain a List of image URLs as Strings.

                //These URLs are typically direct links to image files(e.g., PNG or JPEG) hosted by the API provider.

                imageUrls = await openAIServices.GenerateImageAsync(prompt);


                if (imageUrls.Count > 0)

                {

                    myImage.Source = imageUrls[0]; //just take the first generated image

                    PromptLabel.Text = "Image generated! ... ";

                }

                else

                {

                    await DisplayAlert("Error", "No images found", "OK");

                }

            }

        }


        /Originally got  error "Could not access image data" this occured because the current save logic only handles StreamImageSource and FileImageSource,

            //But when you set myImage.Source to a URL (string), it becomes a UriImageSource. This is the case when you Generate Images from the OpenAI API.

            //To fix this, you need to handle UriImageSource when downloading the image data from the URL before saving.

            //Here’s what was be changed:

            //• Add a check for UriImageSource.

            //• Download the image data from the URL if needed.

            private async void OnSaveClicked(object sender, EventArgs e)

             else if (myImage.Source is UriImageSource uriImageSource)

            {

             var uri = uriImageSource.Uri;

             if (uri != null)

             {

                 using (var httpClient = new System.Net.Http.HttpClient())

                 {

                     imageData = await httpClient.GetByteArrayAsync(uri);      // Download the image data from the URL 

                 }

             }

        }

      • OpenAIServices.cs (used for image generation and later for text-to-speech in voices)
        using System.Text;

        using System.Threading.Tasks;

        using System.Net.Http;

        using System.Net.Http.Json;

        using System.Text.Json;

        using System.Net.Http.Headers;

        public  class OpenAIServices

        {

            private readonly HttpClient _httpClient;

            private readonly string _apiKey = "yourKey";


            //constructor for OpenAIServices

            //The constructor is used to initialize a new instance of the OpenAIServices class

            public OpenAIServices(HttpClient httpClient)

            {

                _httpClient = httpClient;

            }


            //This method converts text to speech using the OpenAI text-to-speech API.

            public async Task<string> ConvertTextToSpeechAsync(string text, string voice)

            {

                if (string.IsNullOrWhiteSpace(text))

                {

                    throw new ArgumentException("Text cannot be empty");

                }

                //

                //This is the required request body for the OpenAI text-to-speech API.

                //This model converts text into natural sounding spoken audio and is optimized for speed.

                var requestBody = new

                {

                    model = "tts-1",

                    input = text,

                    voice = voice

                };


                //The request body is serialized to a JSON string using the System.Text.Json.JsonSerializer class.

                var jsonBody = JsonSerializer.Serialize(requestBody);


                //The Authorization header is added to the request to authenticate the API call.

                _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _apiKey);


                //The request body is converted to a StringContent object with the appropriate encoding and media type.

                var content = new StringContent(jsonBody, Encoding.UTF8, "application/json");


                //The POST request is sent to the OpenAI API using the HttpClient.PostAsync method.

                //This will use the text-to-speech api model called tts-1 to convert text to speech

                var response = await _httpClient.PostAsync("");


                //A base64 encoded string is a way to represent binary data (such as images, audio files, or any other binary content)

                //in an ASCII string format. This encoding is useful for transmitting binary data over text-based protocols like HTTP, email, or JSON.

                if (response.IsSuccessStatusCode)

                {

                    var audioBytes = await response.Content.ReadAsByteArrayAsync();//Asynchronously reads the HTTP content and stores it in a byte array.

                    var base64Audio = Convert.ToBase64String(audioBytes);          //Converts the byte array to a base64-encoded string.

                    var audioUrl = "data:audio/mp3;base64," + base64Audio;         //Constructs a data URL for the audio file.

                    return audioUrl;

                }

                else

                {

                    throw new Exception("Failed to convert text to speech.");

                }

            }


            //This task will return a list of strings (image URLs)

            public async Task<List<string>> GenerateImageAsync(string prompt)

            {

                //The request body is a JSON object that contains the prompt, the number of images to generate, and the size of the images.


                //For the OpenAI image generation API, you do not need to specify the model in the request body.

                //The model is implicitly defined by the endpoint you are using.

             


                var requestBody = new

                {

                    prompt = prompt,

                    n = 3, //number of images to generate

                    size = "512x512"

                };


                //The request body is Serialized to a JSON string and then converted to a StringContent object.

                var requestContent = new StringContent(JsonSerializer.Serialize(requestBody), Encoding.UTF8, "application/json");


                //ContentType: This header specifies the media type of the content being sent.

                //In this case, it is set to "application/json", indicating that the content is in JSON format.

                //MediaTypeHeaderValue: This is a class that represents a media type used in a content-type header.

                requestContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");


                //The Authorization header is added to the request to authenticate the API call.   

                _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _apiKey);


                //The POST request is sent to the OpenAI API using the HttpClient.PostAsync method.

                //This will use the image api model called dall-e-2 to generate images

                var response = await _httpClient.PostAsync("", requestContent);

             


                if (response.IsSuccessStatusCode)

                {

                    // Asynchronously reads the HTTP content and deserializes it into a JsonElement.

                    // The JsonElement type is part of the System.Text.Json namespace and represents a JSON value,

                    // which can be an object, array, string, number, etc.

                    var json = await response.Content.ReadFromJsonAsync<JsonElement>();


                    //Checks if JSON response contains a property named "data" and if that property is an array.

                    if (json.TryGetProperty("data", out var dataArray) && dataArray.ValueKind == JsonValueKind.Array)

                    {

                        //Extracts the image URLs from the JSON response and returns them as a LIST of strings.

                        var imageUrls = dataArray.EnumerateArray()

                            .Select(img => img.GetProperty("url").GetString())

                            .Where(url => !string.IsNullOrEmpty(url))

                            .ToList();

                        return imageUrls;

                    }

                    else

                    {

                        throw new Exception("API response does not contain 'data' array");

                    }

                }

                else

                {

                    var errorMessage = await response.Content.ReadAsStringAsync();

                    throw new Exception("API request failed with status code " + response.StatusCode + " Error message: " + errorMessage);

                }

            }

        }


    • Audio   (AudioMAUI.xaml) ... with Audio and  Video Playback

      • Record , Play back  and Save recorded  audio using the Plugin.Maui.Audio Nuget Package . Also has the capability to play back locally stored audio (Resources\raw folder)

        • We save recorded audio for  Windows execution in mp3 format and Android in wav format.

      • This page also implements MediaElement for .NET MAUI (CommunityToolkit.Maui.MediaElements) which can playback most audio and video in a nice Player (toolkit:MediaElement) that has playback controls. You can use hard coded uri's that point to mp3 or mp4 files or use a FilePicker

      • ListViews of  Audio and Video files are displayed for user selection and playback also.

      • Programming Details

        • Install the NuGet Package ... Plugin.Maui.Audio

          • Register this package in MauiProgram.cs

            builder.Services.AddSingleton(AudioManager.Current);

          • In AudioMAUI.xaml.cs we inject the AudioManager and prepare it for implementation.

            using Plugin.Maui.Audio;

            using Microsoft.Maui.Storage;

            public partial class AudioMAUI : ContentPage

            {

                //Some audio files have been added to the Resources/Raw folder in the project.


                private readonly IAudioManager _audioManager;

                private readonly IAudioRecorder _audioRecorder;

                IAudioSource recordedAudio;

                private IAudioPlayer? _samplePlayer;

                public ObservableCollection<string> AudioFileNames { get; set; } = new();

                public ObservableCollection<string> VideoFileNames { get; set; } = new();


                public AudioMAUI(IAudioManager audioManager)

                {

                    InitializeComponent();

                    _audioManager = audioManager;                       // audioManager is injected via Dependency Injection         

                    _audioRecorder = _audioManager.CreateRecorder();    // Create an audio recorder instance
                   BindingContext = this;
                  LoadAudioFileNames();

                  LoadVideoFileNames();

                }  

          • In AudioMAUI.xaml (play  locally stored audio)
            <HorizontalStackLayout 

                HorizontalOptions="Center" 

                Spacing="10">   


                <Button x:Name="playResourceSoundButton" 

                    Text="Play MP3 (Audio Manager Res-Raw folder)"   

                    Clicked="OnPlaySampleSounds"   

                    Style="{StaticResource BlueWhiteButtonStyle}"   

                    WidthRequest="300" /> 


                <Button x:Name="stopResourceSoundButton" 

                    Text="Stop MP3"   

                    Clicked="OnStopSampleSounds"   

                    Style="{StaticResource BlueWhiteButtonStyle}"   

                    WidthRequest="90"   

                    BackgroundColor="Red"/> 

            </HorizontalStackLayout> 

            In AudioMAUI.xaml.cs (corresponding code behind)
            private async void OnPlaySampleSounds(object sender, EventArgs e)

            {

                // Ensure any previous sample player is stopped before creating a new one

                if (_samplePlayer != null && _samplePlayer.IsPlaying)

                {

                    _samplePlayer.Stop();

                }

                // Create a new player for the sample audio file

                _samplePlayer = _audioManager.CreatePlayer(await FileSystem.OpenAppPackageFileAsync("colours-german.mp3")); // Loads the audio file from the Resources/Raw folder

                //_samplePlayer = _audioManager.CreatePlayer(await FileSystem.OpenAppPackageFileAsync("cctest.mp3"));         

                _samplePlayer.Volume = .75f;

                _samplePlayer.Play();

            }


            private async void OnStopSampleSounds(object sender, EventArgs e)

            {

                // Stop the sample player if it is currently playing

                if (_samplePlayer != null && _samplePlayer.IsPlaying)

                {

                    _samplePlayer.Stop();

                }

            }

          • Note on the Home.razor and NavMenu.razor pages the use of onclick instead of href to link to the AudioMAUI.xaml page

            • private void GoAudioMaui()

              {

                  // Resolve IAudioManager from the service provider

                  var audioManager = App.Current?.Handler?.MauiContext?.Services.GetService<IAudioManager>();

                  if (audioManager is not null)

                  {

                      App.Current.MainPage.Navigation.PushModalAsync(new Pages.AudioMAUI(audioManager));

                  }

                  else

                  {

                      // Optionally handle the error (e.g., show a message)

                  }

              }

          • In AudioMAUI.xaml (record and playback a recorded sound)
            <HorizontalStackLayout 

                HorizontalOptions="Center" 

                Spacing="10"> 


                <Button x:Name="recordAudioButton" 

                        Text="Record Audio"   

                        Clicked="OnRecordButtonClicked"   

                        Style="{StaticResource BlueWhiteButtonStyle}"   

                        WidthRequest="120" /> 


                <Button x:Name="stopAudioButton" 

                    Text="Stop Recording"   

                    Clicked="OnStopButtonClicked"   

                    Style="{StaticResource BlueWhiteButtonStyle}"   

                    WidthRequest="120" 

                    BackgroundColor="Red" /> 


                <Button x:Name="playAudioButton" 

                    Text="Play Recorded Audio"   

                    Clicked="OnPlayButtonClicked"   

                    Style="{StaticResource BlueWhiteButtonStyle}"   

                    WidthRequest="160" /> 
            </HorizontalStackLayout> 

            In AudioMAUI.xaml.cs (corresponding code behind)
            private async void OnRecordButtonClicked(object sender,EventArgs e)

            {

                if (await Permissions.RequestAsync<Permissions.Microphone>() != PermissionStatus.Granted)

                {

                    await DisplayAlert("Permission Denied", "Unable to record audio without microphone permission.", "OK");

                    return;

                }

                if (!_audioRecorder.IsRecording)

                {

                    await _audioRecorder.StartAsync();           

                }       

            }


            private async void OnStopButtonClicked(object sender, EventArgs e)

            {

                if (_audioRecorder.IsRecording)

                {

                    recordedAudio = await _audioRecorder.StopAsync();           

                }

                else

                {

                    await DisplayAlert("Recording Stopped", "No audio was recorded.", "OK");

                }

            }


            private async void OnPlayButtonClicked(object sender, EventArgs e)

            {

                if (_audioRecorder.IsRecording)

                {

                    await DisplayAlert("Recording in Progress", "Please stop the recording before playing audio.", "OK");

                    return;

                }

               

                if (recordedAudio != null)

                {

                    await DisplayAlert("Info", "Recorded audio plays with no issues in Windows but may not play in Andriod Emulator", "OK");


                    var player = AudioManager.Current.CreatePlayer(recordedAudio.GetAudioStream());             

                    player.Play();

                }

                else

                {

                    await DisplayAlert("No Audio", "No audio was recorded to play.", "OK");

                }

            }

        • Install the NuGet Package ... CommunityToolkit.Maui.MediaElement

          • Register this package in MauiProgram.cs

            var builder = MauiApp.CreateBuilder();

            builder

                .UseMauiApp<App>()

                .UseMauiCommunityToolkitMediaElement()

                .UseMauiCommunityToolkitCamera()

                .UseLocalNotification()

                .ConfigureFonts(fonts =>

                {

                    fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");

                });

          • In AudioMAUI.xaml ... add
            <ContentPage xmlns="" 

                         xmlns:x="" 

                         x:Class="BlazHybridPhoneDemos.Components.Pages.AudioMAUI"              xmlns:toolkit=""   

                         Title="AudioMAUI" 

                         BackgroundColor="Gray"> 

            <toolkit:MediaElement 

                 x:Name="mediaElement" 

                 WidthRequest="400" 

                 HeightRequest="300"                 

                 Source="" 

                 ShouldShowPlaybackControls="True"/> 


            <!--Source=""--> 

            <HorizontalStackLayout 

                 HorizontalOptions="Center" 

                 Spacing="10">             


                 <Button x:Name="playAudioMediaElementButton" 

                     Text="Play Audio (MediaElement)"   

                    Clicked="OnPlayAudio"   

                     Style="{StaticResource BlueWhiteButtonStyle}"   

                     WidthRequest="200" />       


                 <Button x:Name="playVideoButton" 

                     Text="Play Video (MediaElement)"   

                     Clicked="OnPlayVideos"   

                     Style="{StaticResource BlueWhiteButtonStyle}"   

                     WidthRequest="200" /> 


            </HorizontalStackLayout>


            <Button Text="File Picker" Clicked="OnFilePicker" Style="{StaticResource BlueWhiteButtonStyle}" WidthRequest="100" BackgroundColor="Blue" />

            In AudioMAUI.xaml.cs (corresponding code behind)

            using CommunityToolkit.Maui.Core.Primitives;

            using CommunityToolkit.Maui.Views;

            private async void OnPlayVideos(object sender, EventArgs e)

            {

                mediaElement.Source = null; // Clear any previous media source


                if (mediaElement.CurrentState == MediaElementState.Playing)

                {

                    mediaElement.Pause(); // Pause if currently playing

                    playVideoButton.Text = "Play Video (MediaElement)"; // Change button text to indicate play     

                }

                else

                {           

                    mediaElement.Source = new Uri("");    // Set video source           

                    //mediaElement.Source = new Uri(""); // Set video source

                   

                    mediaElement.Play();

                    playVideoButton.Text = "Pause Video"; // Change button text to indicate pause           

                }

            }


            private async void OnPlayAudio(object sender, EventArgs e)

            {

                mediaElement.Source = null; // Clear any previous media source


                if (mediaElement.CurrentState == MediaElementState.Playing)

                {

                    mediaElement.Pause(); // Pause if currently playing

                    playAudioMediaElementButton.Text = "Play Audio (MediaElement)"; // Change button text to indicate play     

                }

                else

                {           

                    mediaElement.Source = new Uri("");// Set audio source

                    //mediaElement.Source = MediaSource.FromFile("colours-german.mp3");           


                    mediaElement.Play();

                    playAudioMediaElementButton.Text = "Pause Audio"; // Change button text to indicate pause           

                }

            }

    • Suggested Exercises and Updated Applications (HyPhMoDetNoKey/HyPhMoDetQRnokey/HyPhMoDetQRupdateNoKey/LiveLinkMore/Update) 
      </

  • Generating SQL Queries via AI for use in Blazor Database Applications42:55

    In this Lecture we will

    • Learn how to use AI to generate SQL Queries for a Local Database and use those generated queries in a Blazor Web App to return the corresponding Data

      • The amount of data available today on any given subject is staggering. We need powerful, flexible tools to help us query and analyze this data and make sense of it. Structured query language (SQL) is among the most popular data query languages.

      • However, SQL can be challenging for some users because it requires precise, syntactically correct queries to work properly. Such situations can be a barrier for users unfamiliar with SQL or those who have difficulty writing SQL queries. This is where natural language processing comes into play.

      • ‍Natural language processing is a field of computer science and artificial intelligence (AI) that deals with the computers’ ability to understand human language. In recent years, significant AI advancements have enabled computers to parse and interpret human language with greater accuracy.

      • This Lecture acts as a beginner’s look at converting Natural Language to SQL queries using the OpenAI API and the Microsoft Semantic Kernel.

    • Create a simple Blazor Web App .NET 9 Interactive Render Mode Server (BlazorAIsemanticKernelDBnet9)

      • This application will connect to the local database SongListBase from the Blazor application (BlazorSongListWasmLect145) from Lecture 145 ... Basic CRUD application with a simple Filter by Artist capability

        • Note: We don't actually use the application BlazorSongListWasmLect145 ... we JUST want access to it's database

      • This application uses the Microsoft Semantic Kernel SDK to connect to the OpenAI API as opposed to the Microsoft.Extensions.AI which we have used most recently. Recall Lecture 199 Supplementary Demos (BlazorAIsemanticKernelLect199) ... Simple AI Chat application which displays user prompts and corresponding bot messages in stylized formats.

    • The Details

      • This application builds on (updates) the original application BlazorAIsemanticKernelLect199 -> BlazorAIsemanticKernelDBnet9

        • The actual SQL Server Database we will be using is actually not part of the application we are working in but is available to us ... in Visual Studio make sure the SQL Server Object Explorer is viewable and then  check out all the available Databases, from previous Lectures . I will be using the Database called SongListBase and the Table called Song

        • Of course first you need the Microsoft Semantic Kernel NuGet Package ... already installed in our base application

          • Program.cs
            using Microsoft.SemanticKernel;


            // Add Kernel services to the container (OpenAI Chat Completion Service)

            // along with desired OpenAI API key and model ID

            builder.Services.AddKernel().AddOpenAIChatCompletion(modelId:"gpt-4o-mini",apiKey: "yourKey");


        • Also need to install Microsoft.Data.SqlClient NuGet Package ... used to help us make a connection to our SQL Server Database ... See DataService.cs in Models folder

          using Microsoft.Data.SqlClient;

          namespace BlazorAIsemanticKernel.Models

          {

              public static class DataService

              {

                  public static List<List<string>> GetDataTable(string sqlQuery)

                  {

                      var rows = new List<List<string>>();


                      using (SqlConnection connection = new SqlConnection("Server=(localdb)\\mssqllocaldb;Database=SongListBase;Trusted_Connection=True;MultipleActiveResultSets=true"))

                      {

                          connection.Open();

                          using (SqlCommand command = new SqlCommand(sqlQuery, connection))

                          {

                              using (SqlDataReader reader = command.ExecuteReader())

                              {

                                  int count = 0;

                                  bool headersAdded = false;


                                  while (reader.Read())

                                  {

                                      count++;

                                      var cols = new List<string>();

                                      var headerCols = new List<string>();

                                      if (!headersAdded)

                                      {

                                          for (int i = 0; i < reader.FieldCount; i++)

                                          {

                                              headerCols.Add(reader.GetName(i).ToString());

                                          }

                                          rows.Add(headerCols);

                                          headersAdded = true;

                                      }

                                      for (int i = 0; i <= reader.FieldCount - 1; i++)

                                      {

                                          try

                                          {

                                              cols.Add(reader.GetValue(i).ToString());

                                          }

                                          catch (Exception ex)

                                          {

                                              cols.Add("DataTypeConversionError");

                                          }

                                      }

                                      rows.Add(cols);

                                  }

                              }

                          }

                      }

                      return rows;

                  }

              }

          }

      • Eventually our prompt to OpenAI API will include a reference to the Database Schema , you can determine this manually or automatically (See the GenerateSchema.razor). At the beginners level we simply use the manual technique but the coding for the automatic technique is also provided for enrichment.

        • You can determine the schema manually by taking a deep dive in the SQL Server Object Explorer. In this particular case we look for the SongListBase database and then the Song table. Right click on the table and select "View Designer". This will show you the columns and data types. You can then manually create a schema representation like the one below.

          -SongListBase.dbo.Song (Id (int), Title (nvarchar), Artist (nvarchar), Year (nvarchar), Chart1 (bit))

      • Next look at the supporting classes in the Models folder

        • Message.cs
          // This class represents a message in a conversation, with a role (e.g., user, assistant) and content.

          public class Message

          {

              public string role { get; set; }

              public string content { get; set; }

          }

        • Rootobject.cs
          //This class represents the root object for an OpenAI API AI model request, containing the model name and an array of Message objects

          //Where each Message object represents a message in the conversation with a role and content.

          public class Rootobject

          {

              public string model { get; set; }   

              public Message[] messages { get; set; }         

          }

        • AIQuery.cs
          // Used to store the apiResponse as a Deserialized  JSON string

          // Summary = response.summary;

          // Query = response.query;

          public class AIQuery

          {

              public string summary { get; set; }

              public string query { get; set; }   

          }

      • ... and finally the Sqlai.razor page

        • HTML section Initializations
          @page "/sqlai"


          @using Microsoft.SemanticKernel

          @inject Kernel kernel


          @rendermode InteractiveServer

        • Code Section Initializations
          // This property is bound to the textarea input where the user types their prompt ... eg. "List all songs after 1990"

          public string UserPrompt { get; set; } = string.Empty;


          // This is a LIST of LISTS of strings. Think of it as a 2D table:

          // The outer List represents all the rows.

          // Each inner List<string> represents a single row, where each string is a cell value (column).

          private List<List<string>> RowData;


          private string Summary;     // This string holds the summary generated by the AI

          private string Query;       // This string holds the SQL query generated by the AI ... this is passed to GetDataTable (DataService) to get the actual data


          private string Error;       // This string holds any error messages that occur during processing


          // This string accumulates the raw response from the AI API

          private string apiResponse = string.Empty;


          // This flag indicates whether the application is currently loading the API response

          // It is used to show/hide the loading animation

          private bool isLoading = false;

        • Task SendMessages()
          try

          {

              isLoading = true;                   // Show the loading animation

              apiResponse = string.Empty;

              Error = string.Empty;

              Summary = string.Empty;

              Query = string.Empty;


              RowData = new List<List<string>>(); // Clear previous data results

             

              //This is the system message that provides context to the AI about the database schema and how to format its response

              //Note: We don't use the dbo.Song prefix in the schema because the database only has a single table called Song

             

              var systemMessage = @"Your are a helpful assistant that helps people find information in a SQL database.

                                  Use the following database schema when creating your answers:

                                  - Song (Id (int), Title (nvarchar), Artist (nvarchar), Year (nvarchar), Chart1 (bit))

                                  Include column names headers in the query results.

                                  Always provide your answer in the JSON format: {""summary"": ""your-summary"", ""query"": ""your-query""} 

                                  Output only JSON

                                  In the preceding JSON response, substitute ""your-query"" with MS SQL Server Query to retrieve the requested data

                                  In the preceding JSON response, substitute ""your-summary"" with a sumary of the query

                                  Always include all columns in the table

                                  If the resulting query is non-executable , replace ""your-query"" with NA, but still substitute ""your-query"" with a summary of the query

                                  Do not use MySQL syntax

                                  Do not use LIMIT syntax";


              // Construct the prompt object with the model, system message, and user prompt

              // Note: We use gpt-4o-mini which is optimized for cost and performance

              var prompt = new Rootobject

              {

                  model = "gpt-4o-mini",

                  messages = new Message[]

                  {

                      new Message { role = "system", content = systemMessage },

                      new Message { role = "user", content = UserPrompt }

                  }, 

              };


              // Serialize the prompt object to a JSON string ... this is what we will send to the AI API

              var promptstring = System.Text.Json.JsonSerializer.Serialize(prompt);


              // Call the AI API using the Semantic Kernel's InvokePromptStreamingAsync method

              await foreach (var result in kernel.InvokePromptStreamingAsync<string>(promptstring))

              {

                  // Accumulate the result into apiResponse, cleaning up any unwanted characters

                  // such as triple single quotes, newlines, and tabs.

                  // apiResponse represents the full RAW response from the AI

                  apiResponse += result.Replace("'''"," ").Replace("\n"," ").Replace("\t"," ");

                  StateHasChanged();

              }   


              // Clean up apiResponse: trim whitespace and remove leading non-JSON characters

              apiResponse = apiResponse.Trim();

             

              int firstBrace = apiResponse.IndexOf('{');

              if (firstBrace > 0)

              {

                  apiResponse = apiResponse.Substring(firstBrace);

              }


              int lastBrace = apiResponse.LastIndexOf('}');

              if (lastBrace > 0 && lastBrace < apiResponse.Length - 1)

              {

                  apiResponse = apiResponse.Substring(0, lastBrace + 1);

              }           


              // Parse the JSON response from the AI ... Deserialize the JSON string into a response Object of type AIQuery

              AIQuery response = null;

              try

              {

                  response = System.Text.Json.JsonSerializer.Deserialize<AIQuery>(apiResponse);

              }

              catch (Exception jsonEx)

              {

                  Error = $"JSON parse error: {jsonEx.Message}. Raw response: {apiResponse}";

                  return;

              }

              if (response == null)

              {

                  Error = $"Failed to parse AI response. Raw response: {apiResponse}";

                  return;

              }


              Summary = response.summary;     // This is the summary generated by the AI

              Query = response.query;         // This is the SQL query generated by the AI ... we will pass this to GetDataTable to get the actual data


              // RowData is a List of Lists of strings (2D table) returned from GetDataTable (DataService)

              // We use RowData to render the HTML table in the UI

              RowData = DataService.GetDataTable(response.query);

             

              isLoading = false;

              this.StateHasChanged();


          }

          catch (Exception ex)

          {

              apiResponse = "API call failed with exception: " + ex.Message;

              Error = ex.Message;

          }   

        • HTML Section
          <div class="row">   


              <div class="col-md-12">


                  @if (!string.IsNullOrEmpty(apiResponse))

                  {

                       <h5>Raw API Response (for debugging purposes):</h5>

                       <h5 class="alert alert-secondary" style="font-size:medium;">@apiResponse</h5>

                  }       


                  <div class="form-group mb-3">

                      <h5 for="name">Your Prompt:</h5>

                      For example ... "List all songs ", or "List all songs after 1990 "

                      <textarea type="text" @bind="UserPrompt" class="form-control"></textarea>

                  </div>


                  <button type="submit"

                      class="btn btn-success"

                      @onclick="SendMessage">

                      <span>Submit</span>

                  </button>

                  <button class="btn btn-secondary" @onclick="ClearAll">Clear</button>


                  @if (isLoading)

                  {

                      <img src="images/running.gif" width="25" height="25" />

                  }

                  <hr/>

                  @if (!string.IsNullOrEmpty(Error))

                  {

                      <p><b>Error:</b>@Error</p>   

                  }

                  @if (!string.IsNullOrEmpty(Summary))

                  {

                      <p><b>AI generated summary:</b>@Summary</p>

                  }

                  @if (!string.IsNullOrEmpty(Query))

                  {

                      <p><b>AI generated query:</b>@Query</p>

                  }

                  @if (RowData!=null && RowData.Count >0)

                  {

                      <p><b>Data results:</b></p>

                  }


                  <div class="overflow-scroll">                   

                         

                      @if (RowData != null && RowData.Count > 0)

                      {

                          <table class="table table-bordered table-striped table-hover">

                             

                              <thead class="table-dark">

                                  <tr>

                                      @foreach (var col in RowData[0])

                                      {

                                          <th>@col</th>

                                      }

                                  </tr>

                              </thead>

                             

                              <tbody>

                                  @for (int i = 1; i < RowData.Count; i++)

                                  {

                                      <tr>

                                          @foreach (var col in RowData[i])

                                          {

                                              if (col == "True" || col == "False")

                                              {

                                                  <td>@(col == "True" ? "Yes" : "No")</td>

                                              }

                                              else

                                              {

                                                  <td>@col</td>

                                              }

                                          }

                                      </tr>

                                  }

                              </tbody>


                          </table>

                      }

                     

                  </div>


              </div>


          </div>

    • Sample Prompts

      • List all songs

      • List all songs after 1990

      • Sort by Artist Name

      • Which artist has the most Chart 1 (this demostrates the real power of the AI implementation)

        • produces a table with Artist and Chart Count

      • How many chart #1s are there in the  database

    • Suggested Exercises

      • Add the AI capabilities covered in this Lecture  to the BlazorSongListWasmLect145 demo -> BlazorSongListWasmLect145withAInk

      • Add the AI capabilities covered in this Lecture to a Relational Database (Lecture 158) SongWasmRelDB6withImagesRowClickEdit -> SongWasmRelDBaiNoKey

        • Note: Your Data Schema will now have at least 2 sets of entries

    • Supplementary Demos

      • MovieWasmRelDBai

      • BlazorScrapeDBwithSQLaiEcomFull

        • In this application we have added Natural Language capabilities to the Wiki GDP DB page. This allows the user to ask questions about the data scraped from the page. The ChatGPT model is used to process the UserPrompt and return the appropriate SQL statement . This is then used to filter the scraped data further and display the new results in a table. Note: We are implementing the Microsoft Semantic Kernel is this implementation

      • BlazorExcelAi

        • This application seamlessly imports Excel (.xlsx) files into a Blazor WebAssembly application, displaying data in a responsive, virtualized table format with advanced AI filtering capabilities using  Natural Language prompts Note: we are implementing a basic HttpClient technique here to connect to the AI service.

    • Supplementary Demos (Microsoft Agent Framework ... not Semantic Kernel)

      • These are Redos of previous applications now using the new Framework and implement  Natural Language Querying using Real Time Speech to Text 

        • BlazorAIsemKernExternalConnectToDBnet9ServerMAFspchTxt

          • This is a Blazor WebAapp .NET 9 (Render Mode Server) that uses the new Microsoft Agent Framework to connect to the OpenAI API. This is a redo of a similar demo covered in this Lecture

            Key Implementation Steps:

            • Install the Microsoft.Agents.AI.OpenAI NuGet Package

            • Install the OpenAI NuGet Package

            • Install the Microsoft.Data.SqlClient NuGet Package

          • This application will connect to the local database SongListBase from the Blazor application (BlazorSongListWASM) from Lecture 145. This application did NOT create the database. It is accessing it from afar so to speak.

          • Note how much more compact and straightforward the code is using MAF compared to the Semantic Kernel. Check out the Sqlai.razor page to compare.

          • ... and finally This demo showcases real-time speech-to-text capabilities .There are many options for speech-to-text:

            Cloud APIs (Azure Cognitive Services, Google, etc.). Native libraries wrapped with gRPC or REST. Browser Web Speech API.

            For a Blazor app that runs in the browser, the Web Speech API is the easiest place to start:No server round trips for audio. No external billing. Works directly in the user’s browser. The downside: not all browsers support it (mainly Chromium based ones do: Chrome, Edge, some versions of Opera). You will add a simple IsSupported check and show a friendly message when support is missing. Check out all the specifics in the SqlaiSpeechText.razor page

        • SongListWasmFromLect145InternalConnectDBaiMAFspchTxt

          • This is a redo of a similar demo covered in this Lecture.  Again we are implementing the new Microsoft Agent Framework and Real Time Speech to Text

          • In this example our application is a full CRUD application

          • All necessary updates and changes related to the framework have been implemented in the AIQueryController.

          • Note:  All the AI processing, that is,  sending the userprompt to the ai to get an SQL statement, and then taking that SQL statement and doing a database query (via the DataService in the Data folder)  is ALL done in the Controller

        • SongWasmInternalConnectRelDBaiMAFspchTxt

          • This is a redo of the Relational ( 2 table) suggested Exercise in this Lecture. This updated version  now implements the new Microsoft Agent Framework and uses  Real Time Text to Speech

        • MovieWasmInternalConnectRelDBaiMAFspchTxt

          • This is a redo of the Supplementary  Relational DB demo called MovieWasmRelDBai. This updated version now implements the new Microsoft Agent Framework and uses  Real Time Text to Speech

        • BlazorScrapeDBwithSQLaiEcomFullMAFspchTxt

          • This is a redo of the Supplementary demo called BlazorScrapeDBwithSQLaiEcomFull.This updated version now implements the new Microsoft Agent Framework and uses  Real Time Text to Speech

        • BlazorExcelAiUpdatedMAFspchTxt

          • This is a redo of the Supplementary demo called BlazorExcelAi. This updated version now implements the new Microsoft Agent Framework and uses  Real Time Text to Speech


  • RAG (Retrieval Augmented Generation) and Blazor ... A First Look42:29

    In this Lecture we will

    • Learn that RAG ... Retrieval-Augmented Generation  is used to provide answers based on your custom data.

      • Retreival ... The AI searches for relevant information from connected sources such as policy documents, product manuals or internal knowledge bases

      • Augmentation ... The retrieved content is fed into the AI model as additional context. This step is what makes RAG special, the model doesn’t rely solely on what it “knows” from training, but gets enriched, real-time domain-specific information to base its answer on leading to more accurate , factual, and trustworthy outputs (no hallucinations)

      • Generation ... using this combination, its internal language capabilities plus the retrieved data, the AI produces a natural-language response. 

      • Imagine asking a chatbot, “What did the President say in his speech yesterday?” Most large language models, like GPT, are trained only up to a certain date and lack up-to-date knowledge. This is where RAG comes in. It could query an external database, updated daily with the latest news articles, to find relevant information. The database returns a match, which is then incorporated into the query before being passed to the generative AI model. By framing the query to include this new information, such as “Consider the text of the following news article in your response,” the AI can generate a response that accounts for the latest data. This method allows for the integration of external, possibly proprietary, data, effectively creating a sophisticated search engine.

    • Learn that the a new Visual Studio Blazor template called AI Chat Web App (.NET 9) is designed to help you quickly build an AI-powered chat application which uses  RAG to chat with your own Local Custom Data , like pdf documents  (stored in the Data folder located in wwwroot) . You can also make your own tools (functions) available to the LLM ... Local Custom Functions .

      • This document-based AI assistant  allows users to query local PDFs using natural language with citations, powered by OpenAI and a vector store.The responses are specific to your own documents which might be out of reach for most AI models that are scraping the web. The generated code also provides follow-up suggestions

      • To get started with the new template, you install the Microsoft.Extensions.AI.Templates from your Terminal. Just run: dotnet new install Microsoft.Extensions.AI.Templates ... Once installed, the template is available in Visual Studio (AI Chat Web App)

        • Create a New Project and choose AI Chat Web App

          • .NET 9.0 (by default Interactive RenderMode Server)

          • AI Service Provider ... choose OpenAI Platform or Ollama (Local Development)

          • Vector Embedding Store ... Local on-disk (uses a JSON file on disk ) great for prototyping

            • This is how the LLM "ingests" data. For each document you provide it, the LLM creates an array of numbers ... Embedding Vector (numerical representations of the semantic meaning and relationships of real-world concepts or objects that are easily utilized by AI models ) that are stored in a database (Vector database)  or in a JSON file in the local case. The LLM can then run queries against the database and find "similar" data.

          • Before running this project you need to configure the API keys or endpoints for the providers you have chosen.

            • If you chose OpenAI, configure your API key for this project, using .NET User Secrets:

              • In Visual Studio, right-click on your project in the Solution Explorer and select "Manage User Secrets".

              • This will open a secrets.json file where you can store your API key without them being tracked in source control. Add the following key and value to the file:
                {

                   "OpenAI:Key": "YOUR-API-KEY"

                }

            • If you chose Ollama the Program.cs files has been automatically updated with the required language models  ... BUT 

              • if you don't have Ollama installed on your computer you must install it first

              • and then install the language models used by the project (in your terminal)
                ollama pull llama3.2
                ollama pull all-minilm

          • Test out the application ... try asking about something related to the example documents displayed on screen

        • Check out the Packages folder or Project File (right click Project)

          • It is referencing many of the Libraries/Packages we have covered in this new AI Section
            <PackageReference Include="Azure.AI.OpenAI" Version="2.2.0-beta.1" />

            <PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.3.0-preview.1.25114.11" />

            <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.2" />

            <PackageReference Include="Microsoft.Extensions.AI" Version="9.3.0-preview.1.25114.11" />

            <PackageReference Include="Microsoft.SemanticKernel.Core" Version="1.37.0" />

            <PackageReference Include="PdfPig" Version="0.1.9" />

            <PackageReference Include="System.Linq.Async" Version="6.0.1" />

        • In Program.cs note the key Services that are registered

          var chatClient = openAIClient.AsChatClient("gpt-4o-mini");

          var embeddingGenerator =

          openAIClient.AsEmbeddingGenerator("text-embedding-3-small");

          var vectorStore = new

          JsonVectorStore(Path.Combine(AppContext.BaseDirectory, "vector-store"));

          • An IChatClient service to chat back and forth with the generative AI model

          • An IEmbeddingGenerator service that's used to generate embeddings (vector representations of text ), which are essential for vector search functionality

          • A JsonVectorStore to act as an in-memory vector store (JSON files)

          • Registers a SQLite database context service to handle ingesting documents. The app is preconfigured to ingest whatever documents you add to the Data folder of the project, including the provided example files.
            builder.Services.AddDbContext<IngestionCacheDbContext>(options =>

                options.UseSqlite("Data Source=ingestioncache.db"));

          • // By default, we ingest PDF files from the /wwwroot/Data directory.

            await DataIngestor.IngestDataAsync(

                app.Services,

                new PDFDirectorySource(Path.Combine(builder.Environment.WebRootPath, "Data")));

        • In Chat.razor note the key elements

          • <ChatCitation File="EmergencyKit.pdf"/>  ... displays files located in  Data folder, clickable, will pop-up pdf viewer

          • private const string SystemPrompt = @"

                You are an assistant who answers questions about information you retrieve.

                Do not answer questions about anything else.

                Use only simple markdown to format your responses.

                ....

                Use the search tool to find relevant information. When you do this, end your

                reply with citations in the special XML format:

          • Notice how we add the SearchAsync function as a tool available to the LLM. This enables Retrieval Augmented Generation (RAG). The chatOptions is passed to GetStreamingResponseAsync in AddUserMessageAsync (executed/called from ChatInput)

            protected override void OnInitialized()

            {

                 messages.Add(new(ChatRole.System, SystemPrompt)); 

                chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)];

            }

          • private async Task AddUserMessageAsync(ChatMessage userMessage)

            {

                CancelAnyCurrentResponse();


                // Add the user message to the conversation

                messages.Add(userMessage);

                chatSuggestions?.Clear();

                await chatInput!.FocusAsync();


                // Stream and display a new response from the IChatClient

                var responseText = new TextContent("");

                currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]);

                currentResponseCancellation = new();

            // Stream the response from the chat client (LLM) and append each chunk to the responseText

            // Note that the chatOptions includes the SearchAsync tool for RAG ... this enables the LLM to call SearchAsync when it determines it's needed

            // SearchAsync will give the LLM relevant information from the local data store to include in its response

                await foreach (var chunk in ChatClient.GetStreamingResponseAsync(messages, chatOptions, currentResponseCancellation.Token))

                {

                    responseText.Text += chunk.Text;

                    ChatMessageItem.NotifyChanged(currentResponseMessage);

                }


                // Store the final response in the conversation, and begin getting suggestions

                messages.Add(currentResponseMessage!);

                currentResponseMessage = null;

                chatSuggestions?.Update(messages);

            }

          • //This is an example of RAG (Retrieval Augmented Generation)

            //This C# function Searches the local data (ingested) store for relevant information  it is made available to the LLM as a tool

            // The Description Attribute helps the AI model understand:

            // What the function does("Searches for information using a phrase or keyword")

            // What parameters it expects and their purpose

            [Description("Searches for information using a phrase or keyword")]

            private async Task<IEnumerable<string>> SearchAsync(

                 [Description("The phrase to search for.")] string searchPhrase,

                 [Description("Whenever possible, specify the filename to search that file only. If not provided, the search includes all files.")] string? filenameFilter = null)

            {

                 // Limit to top 5 results to reduce context size sent to LLM

                // SearchAsync is called by the LLM via the chatOptions.Tools added in OnInitialized

               // Search.SearchAsync uses the SemanticSearch service registered in Program.cs and defined    in Services/SemanticSearch.cs 
            // The SemanticSearch service uses the embeddings model to convert the searchPhrase into a vector

            // Search was injected at the top of this file   

                await InvokeAsync(StateHasChanged);

                 var results = await Search.SearchAsync(searchPhrase, filenameFilter, maxResults: 5);

                 return results.Select(result =>

                     $"<result filename=\"{result.FileName}\" page_number=\"{result.PageNumber}\">{result.Text}</result>");

            } 

          • In Services take a look at SemanticSearch.cs  and specifically the Task SearchAsync

            • Generate the embedding vector for the search text

            • Get the vector collection from the vector store (Where "data-blazornetainewtemplate-ingested" is the name of the collection we ingested data into)

            • Perform the vectorized search with the query embedding and filter

        • On Startup of the app, the data ingestion code (located in /Services/Ingestion/DataIngestor.cs) will compare the contents of the Data folder; it will remove old files from the configured vector store and add new ones. Note: depending on how many files you have, and how big they are, you may run into quota and rate limits with your configured AI model provider.

          • code is kickstarted in Program.cs
            await DataIngestor.IngestDataAsync(

                app.Services,

                new PDFDirectorySource(Path.Combine(builder.Environment.WebRootPath, "Data")));

          • In Services/Ingestion folder .... DataIngestor.cs
            public static async Task IngestDataAsync(IServiceProvider services, IIngestionSource source)

            {

                using var scope = services.CreateScope();

                var ingestor = scope.ServiceProvider.GetRequiredService<DataIngestor>();

                await ingestor.IngestDataAsync(source);

            }


            public async Task IngestDataAsync(IIngestionSource source)

            {

                var vectorCollection = vectorStore.GetCollection<string, SemanticSearchRecord>("data-blazornetainewtemplate-ingested");

                await vectorCollection.CreateCollectionIfNotExistsAsync();


                var documentsForSource = ingestionCacheDb.Documents

                    .Where(d => d.SourceId == source.SourceId)

                    .Include(d => d.Records);
            ......

      • Recap: The RAG AI Flow

        • Enter prompt/query

        • Get Embedding Vector for the query  (separate AI model that specializes in generating embedding) ... this will send back a series of numbers that represents the prompt from the user (similar idea to previous lecture where AI returned proper SQL statement)

        • Take the Embedding Vector query and Run a Vector search against a Vector database (pre-populated database)

        • The database will return a set of records that are similar to the query embedding ... the results are semantically related to the embedding of the original prompt.

        • Then the original prompt is combined with the Vector result and sent back to the AI model

        • The AI model uses this context to generate a friendly response that incoporates the retreived information

    • Look at a couple of simple implementations

      • BlazorNETaiNewTemplate/BlazorNETaiOpenSourceOllama

        • When testing my demos  erase the ingestion cache files (ingestioncache.db ...) ... these will be recreated in your location

        • Also anytime you move your app to a different location, erase the ingestion cache files before executing the program to reduce the chance of runtime errors.

        • Try asking about something related to the example documents (stored in the data folder located in wwwroot)

          • The chat assistant interprets your question and decides what phrases to search for in the documents. It then answers your question based on the details it finds in the documents, and even provides a link to the file that contains the answer.

          • Notice the followup questions

        • You can add your own local documents (pdfs)

          • copy the desired pdfs to the Data folder (wwwroot)

          • Add new references to the files in the Chat.razor page

            • <ChatCitation File="VS2022IssueFix.pdf" />

              <ChatCitation File="ASPNETvsWebform.pdf" />

              <ChatCitation File="Lecture198.pdf" />

              <ChatCitation File="Lecture199.pdf" />

          • This general technique of providing "sources" for the LLM to use, instead of relying on the built-in knowledge is the basis for  retrieval-augmented generation (RAG), and is one way to try to ensure that the LLM provides answers grounded in facts.

            • Ingest source data,

            • Encode it as vectors in a vector store

            • Make this store available to the LLM.

      • BlazorNETaiNewTemplateUpdated

        • Go to a Webpage and highlight the information on any desired page  and then Choose print ... Save as PDF.  Here we are going to create our own private Chatbot to answer questions specifically about the document you capture.


          • Take the saved pdf ... CarcinogensSunscreen.pdf and copy it to the Data folder in wwwroot.

          • update the Chat.razor page  ... add
            <ChatCitation File="CarcinogensSunscreen.pdf" />

        • A deeper dive in Chat.razor page

          • Check out the SystemPrompt

          • Modify the SystemPrompt
            If you cannot find any relevant information, say 'I could not find any information on that topic.'

            Please answer in a voice of a super hero like Batman or Spiderman

            • "How do I tie my skates" ... should report ... "No info"

            • "What are the 5 key things to have in your emergency kit" ... "Alright, here's the intel on what you should arm yourself with in your emergency kit. You need the following five key items: "

      • BlazorNETaiNewTemplateUpdatedNext

        • Add new Custom function(s) /tools that can be made available to the LLM besides the default SearchAsync

        • We will add functions to determine the weather for any given city

          • //This is an example of a custom function that could be used as a tool by the LLM

            //The first function gets the latitude and longitude for a specified location using a geocoding API

            //The second function gets the current temperature for the specified latitude and longitude using a weather API


            //These functions are made available to the LLM as tools via the chatOptions.Tools added in OnInitialized

            // The AI Model can call these functions when it determines they are needed to answer the user's question

            // and it will know to call GetLatitudeLongitude first to get the lat/long before calling GetTemperature

            [Description("Gets the current temperature for a specified location.")]

            private async Task<GeocodingResult?> GetLatitudeLongitude(string location)

            {

             

            }


            record GeocodingResponse(GeocodingResult[] Results);

            record GeocodingResult(string Name,float Latitude,float Longitude);


            private async Task<float?> GetTemperature(float latitude,float longitude)

            {

               

                return response?.Daily.Temperature_2m_Max.FirstOrDefault();

            }


            record WeatherResponse(WeatherReports Daily);

            record WeatherReports(float[] Temperature_2m_Max);

          • protected override void OnInitialized()

            {

                // Start the conversation with a System message containing the SystemPrompt

                // System: Sets the rules, instructions, or context for the conversation. It's not a participant, but more like a moderator or guide for the AI.

                // User: Represents the person interacting with the AI (you or your app's users).

                // Assistant: Represents the AI itself — the entity that responds to user messages.

                messages.Add(new(ChatRole.System, SystemPrompt));


                // Add the SearchAsync function as a tool available to the LLM

                // This enables Retrieval Augmented Generation (RAG)

                // The chatOptions is passed to GetStreamingResponseAsync in AddUserMessageAsync

                chatOptions.Tools = [

                        AIFunctionFactory.Create(SearchAsync),

                        AIFunctionFactory.Create(GetLatitudeLongitude),

                        AIFunctionFactory.Create(GetTemperature)

                ];

            }

        • Test out several Cities

    • What's Next?

      • The AI landscape is evolving at an unprecedented pace. AI agents are a step up from RAG, with Agentic RAG the future of AI. It combines retrieval, generation, and dynamic actions while continuously learning and refining itself through feedback. With very high autonomy, Agentic RAG systems are designed for complex, high-stakes environments like supply chain management, financial modeling, and advanced AI coordination. This is where AI moves beyond automation and into true intelligence.

  • An Introduction to the Microsoft Agent Framework (Introduction) Part 130:38

    In this Lecture we will

    • Learn that the Microsoft Agent Framework is a runtime model for building stateful, multi-agent systems directly in .NET 

      • In plain English?

        Your Blazor app can now think, remember, and collaborate. AI agents go beyond basic chatbots. They handle complex goals by reasoning, utiliziing multiple tools and APIs, coordinating with other agents, executing several tasks simultaneously, and incorporating human oversight. Enterprises apply them in areas like audit automation at KPMG and real-time vehicle data analysis at BMW

      • How is this different from other approaches?

        Unlike using just the OpenAI NuGet package, Semantic Kernel, or Microsoft.Extensions, the Agent Framework offers a more opinionated, higher-level model that simplifies building complex, multi-turn, and stateful conversational agents. It handles orchestration, memory, and extensibility out of the box, so you can focus on your application's unique logic.

      • The two core building blocks of agent systems are Agents and Workflows.

        • Agents are systems that combine reasoning, context and tools to pursue objectives

          • Reasoning and Decision making ... usually powered by LLMs

          • Context awareness ... external data that informs decision making like conversation history and knowledge bases

          • Tools ... APIs, Model Context Protocol (MCP) tools. They extend what a system can do but do not make the decisions themselves. For example a Weather API provides data but does not decide how that data is used.

        • Workflows structure complex objectives into sequences of steps, coordinating tasks across people or systems to reach a goal efficiently.

        • Agents + Workflows

          • While workflows can function purely as predetermined sequences, integrating agents adds dynamic decision-making and adaptability, enabling more intelligent and autonomous process management.

          • Agents can call multiple tools

          • Workflows can sequence agents, tools and other workflows

      • Meet Microsoft Agent Framework

        • Microsoft Agent Framework is a comprehensive set of .NET libraries that reduces the complexity of agent development. Whether you’re building a simple chatbot or orchestrating multiple AI agents in complex workflows, Microsoft Agent Framework provides the tools you need to:

          • Build agents with minimal boilerplate code

          • Orchestrate multi-agent workflows with ease

          • Host and deploy agents using familiar .NET patterns

          • Monitor and observe agent behavior in production

    • Discuss the evolution of  Microsoft's AI visions from  Semantic Kernel vs Microsoft.Extensions.AI vs Microsoft Agent Framework

      • Semantic Kernel 2023 (provides robust orchestration) ... MEAI 2024 (delivers standardized AI building blocks for .NET)  ...  AuotGen (enables advanced multi-agent collaboration ) ... MAF 2025

      • MAF takes the best of all these frameworks

    • UPDATE: Jan 21, 2026 MAF General Availability Changes for Release Candidate

      • The .CreateAIAgent() that creates the ChatCompletionAgent is now called .AsAIAgent

        • ChatClientAgent agent = client.GetChatClient("model").CreateAIAgent();

        • ChatClientAgent agent = client.GetChatClient("model").AsAIAgent();

      • The method agent .GetNewThread() is now async and is called agent.GetNewThreadAsync()

        • AgentThread thread = agent.GetNewThread();

        • AgentThread thread = await agent.GetNewThreadAsync();

      • The return object of the .RunAsync(...) has been renamed from AgentRunResponse to AgentResponse

        • AgentRunResponse response = await agent.RunAsync(message);

        • AgentResponse response = await agent.RunAsync(message);

    • Learn to create several simple applications which implement the new Microsoft.Agents.AI.OpenAI library along with supporting libraries like OpenAI and OllamaSharp

      • Intro (BlazorMSagentFrameworkIntro) ... Blazor WebAssembly Standalone App Template in .NET 10 ... A simple basic Chat example (Hands on)

        • Coding Specifics

          • First in the HTML section add some necessary usings
            @using OpenAI

            @using Microsoft.Agents.AI

          • Coding Section

            • First some initial variable declarations
              private string userPrompt = string.Empty;

              private string? responseText ;

              private string buttonText = "Send to ChatGPT";


              const string apiKey = "yourKey";

              const string model = "gpt-4o-mini";

            • Next we create our main Task called SubmitPrompt and a simple ClearAll method
              private async Task SubmitPrompt()

              {

                   buttonText = "Sending...";


                   //The AIAgent Manages the conversation flow with the AI model.

                   //Sends user prompts to the model.

                   //Receives responses, either as a complete message or as a stream of updates.

                   //Simplifies handling of streaming responses, so you can update the UI as new text arrives.

                  OpenAI.OpenAIClient client = new OpenAIClient(apiKey);

                   AIAgent agent = client.GetChatClient(model).CreateAIAgent();


                  //Regular response - waits for full response ... we do this version FIRST

                   //AgentRunResponse response = await agent.RunAsync(userPrompt);

                   //responseText = response.Text;       


                  //Streaming response ... we do this version SECOND

                   //A streaming response means the server sends back pieces of the result as soon as they are available,

                   // instead of waiting for the whole answer to be ready. This is useful for things like chatbots,

                   // where you want to show the answer as it's being generated.


                   await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(userPrompt + " format every response in HTML"))

                   {

              responseText += update.Text;

              StateHasChanged(); // Refresh UI with new content

                   }


                   buttonText = "Send to ChatGPT";

                   

              }


              private void ClearAll()

              {

                   userPrompt = string.Empty;

                   responseText = null;

              }

          • ... and finally back to the HTML to complete the UI
            <h3>Agent Page</h3>


            <div class="mb-3">

                <label for="userInput">Enter your Question</label>

                <input @bind="userPrompt" id="userInput" class="form-control" />

                <button @onclick="SubmitPrompt" class="btn btn-primary mt-3">@buttonText</button>

                <button @onclick="ClearAll" class="btn btn-secondary mt-3">Clear All</button>

            </div>


            @if (responseText != null)

            {

                <div class="alert alert-info">

                    <p>@((MarkupString)responseText)</p>

                </div>

            }

      • Intro (BlazorMSagentFrameworkIntroWebAppServer)

        • This application was created using a Blazor Web App template Interactive Render Mode ... Server with .NET 10 . (demo only ... exactly the same application as the previous one)

      • Intro (BlazorMSagentFrameworkIntroWebAppServerOllama)  (hands on)

        • This application was created using a Blazor Web App template Interactive Render Mode ...  Server with .NET 10 . Here we are implementing a local LLM using  Ollama

        • We must install the NuGet Package OllamaSharp and the  model llama3.2:latest within Ollama

          • in the HTML section we added
            @using OllamaSharp
            @using Microsoft.Extensions.AI

          • in the Code Section the key changes were :
            //OpenAI and Agent setup

            //-----------------------------------------------------------

            //OpenAI.OpenAIClient client = new OpenAIClient(apiKey);

            //AIAgent agent = client.GetChatClient(model).CreateAIAgent();


            //Ollama and Agent setup ... note you need to have Ollama running locally and the model downloaded and available

            //-----------------------------------------------------------

            IChatClient client = new OllamaApiClient("http://localhost:11434", "llama3.2:latest");

            AIAgent agent = new ChatClientAgent(client);

          • Note:

            • OpenAIClient is a specific client implementation for interacting with OpenAI's API, typically using their hosted models (like GPT-3, GPT-4) via HTTP and an API key.

            • IChatClient is an interface (abstraction) defined in the Microsoft.Extensions.AI package. It allows you to interact with any chat model provider (OpenAI, Ollama, Azure OpenAI, etc.) in a generic way.

            • Why use IChatClient instead?

              • OllamaApiClient implements IChatClient, allowing you to use local models (like those running on Ollama) instead of OpenAI's cloud models.

              • The agent framework (AIAgent, ChatClientAgent) expects an IChatClient for maximum flexibility and provider-agnostic code.

              • This makes it easy to swap between providers (OpenAI, Ollama, etc.) without changing your agent logic.

        • Update

          IChatClient client = new OllamaApiClient("http://localhost:11434", "llama3.2:latest");


          //AIAgent agent = new ChatClientAgent(client);

                 

          AIAgent ollamaAgent = client.CreateAIAgent(

              name: "Ollama Agent",

              instructions: "You are a useful agent");         

    • Learn to get Token Info

      • BlazorMSagentFrameworkTokens ... demo only

        • Displays AI response + Input Tokens used and Output Tokens used for informational purposes.

        • Code Section:
          //Regular response - waits for full response

          AgentRunResponse response = await agent.RunAsync(userPrompt +" format every response in HTML");

          responseText = response.Text;     

          responseText += "<br/><b>-- Response complete --</b>";

          responseText += "<br/><i>Input Tokens used: " + response.Usage.InputTokenCount + "</i>";   

          responseText += "<br/><i>Output Tokens used: " + response.Usage.OutputTokenCount + "</i>";

  • An Introduction to the Microsoft Agent Framework (Custom Tools and MCP) Part 238:12

    In this Lecture we will

    • Learn to Implement Persistence ... AI has a point of reference from previous queries

      • BlazorMSagentFrameworkPersistence (Blazor WebAssembly Standalone App Template in .NET 10) ... demo only

        • Coding Specifics (Code Section)

          • // Store chat history as a list of (role, text) pairs ie Tuple

            // Here we are using a tuple as the element type for the list as opposed to defining a separate class for chat messages

            private List<(string Role, string Text)> chatHistory = new();

            private AgentThread? thread;    //The thread instance ... which holds the conversation state

            private AIAgent? agent;             //The agent instance ... which holds the agent configuration

          • private async Task SubmitPrompt()

            {

                buttonText = "Sending...";


            // Ignore empty prompts

                if (string.IsNullOrWhiteSpace(userPrompt))

                {

                    buttonText = "Send to ChatGPT";

                    return;

                }


                // Initialize thread and agent if first message

                if (thread == null)

                {

                    OpenAI.OpenAIClient client = new OpenAIClient(apiKey);

                    agent = client.GetChatClient(model).CreateAIAgent(instructions: "You are a Friendly AI Bot, answering questions");

                    thread = agent.GetNewThread();

                }


            // Add user message to chat history ... where chatHistory is a List<(string Role, string Text)>

                // chatHistory list is used to display the conversation history to the user,

                // but it is not directly used to provide context to the chatbot (the AI agent) for follow-up questions.

            // that is done by the thread object which maintains the conversation state internally.

                chatHistory.Add(("User", userPrompt));           


            // Create a ChatMessage with the user's prompt, appending instruction to format response in HTML

            // This message is sent to the AI agent for processing.

               ChatMessage message = new ChatMessage(ChatRole.User, userPrompt + " format every response in HTML");

                string responseText = string.Empty;


                //The thread object keeps track of the conversation history for the AI model, so it can answer follow-up questions with context.

               await foreach (AgentRunResponseUpdate update in agent!.RunStreamingAsync(message, thread))

                {

                    responseText += update.Text;

                    StateHasChanged();

                }


                // Add assistant response to chat history for display

                chatHistory.Add(("Assistant", responseText));


                userPrompt = string.Empty;

                buttonText = "Send to ChatGPT";

            }

        • Coding Specifics (HTML)

          @using Microsoft.Extensions.AI  @*Needed for ChatMessage*@

          .....
          @if (chatHistory.Any())

          {

              <div class="alert alert-info">

                  @foreach (var entry in chatHistory)

                  {

                      <div>

                          <strong>@entry.Role:</strong>

                          <span>@((MarkupString)entry.Text)</span>

                      </div>

                  }

              </div>

          }

    • Implement Custom Tools (Functions) ... augmented retrieval based on local fuction(s)

      • BlazorMSagentFrameworkTools (Blazor WebAssembly Standalone App Template in .NET 10) ... hands on

      • Sometimes, AI models cannot answer certain questions directly—such as "What is the current time?"—because they lack real-time or external data access. This page demonstrates how to extend an AI agent with custom tools that the agent can call to fetch information like the current date, time, or timezone, enabling more useful and context-aware responses.

      • Coding Specifics

        • First we create a static class called Tools.cs in the Models folder that will hold several static methods that can be called from our main page (ToolPage.razor)
          public static class Tools

          {

              public static DateTime CurrentDateAndTime(TimeType type)

              {

                  return type switch

                  {

                      TimeType.Local => DateTime.Now,

                      TimeType.Utc => DateTime.UtcNow,

                      _ => throw new ArgumentOutOfRangeException(nameof(type), type, null)

                  };

                  //Alternate simple switch statement
                  //switch (type)

                  //{

                  //     case TimeType.Local:

                  //          return DateTime.Now;

                  //     case TimeType.Utc:

                  //           return DateTime.UtcNow;

                  //    default:

                  //           throw new ArgumentOutOfRangeException(nameof(type), type,   null);

                  // }


              }


              public static string CurrentTimezone()

              {

                  return TimeZoneInfo.Local.DisplayName;

              }


              public enum TimeType

              {

                  Local,

                  Utc

              }   

          }

        • Toolpage.razor

          • A typical user prompt may be "Please tell me the time right here"

          • Code Section

            private async Task SubmitTimePrompt()

            {

            // Ignore empty prompts

            if (string.IsNullOrWhiteSpace(userPrompt))

            {

            return;

            }


            buttonText = "Sending...";

            //Here we are making calls to two simple tools that return the current date/time and timezone

            //These are custom tools are defined in the Tools.cs file

            //The agent is configured with these tools and can call them as needed to answer user questions

            //This is similar to what we did in Lecture 207 when we took a look at the RAG pattern

            //There we called an external knowledge base to get information ie tools to call Latitude/Longitude and then call a weather API


            OpenAI.OpenAIClient client = new OpenAIClient(apiKey);

            AIAgent agent = client.GetChatClient(model)

            .CreateAIAgent(

            instructions: "You are an AI Bot that can call tools to get information. You are a Time Expert",

            tools:

            [

            AIFunctionFactory.Create(Tools.CurrentDateAndTime,"current_date_and_time"),

            AIFunctionFactory.Create(Tools.CurrentTimezone,"current_timezone")

            ]

            );

            //When the AI model decides to call the tool, it will generate a function call with the required parameters (e.g., TimeType: "local" or "utc").

            //If the user prompt is ambiguous, the agent may ask the user to clarify which time type is needed.


            //In this single-turn use case, the thread is not strictly necessary.

            //However, it is a good practice for future extensibility and aligns with how conversational AI agents are typically managed.

            AgentThread thread = agent.GetNewThread();


            ChatMessage message = new ChatMessage(ChatRole.User, userPrompt);

            AgentRunResponse response = await agent.RunAsync(message,thread);


            responseText = response.Text;


            buttonText = "Send to ChatGPT";

            }

    • Learn to use an MCP Server (GitHub) ... Model Context Protocol ... (demo only)

      • This page demonstrates how to connect to and utilize a Model Context Protocol (MCP) server, specifically GitHub's Copilot MCP endpoint.

      • By entering a prompt (query) , you can interact with the GitHub MCP server using an AI agent that leverages available tools and models. It enables AI applications to access real-time, real-world information and perform tasks, similar to how a web browser uses HTTP to connect to websites. MCP standardizes this connection, allowing AI agents to be more context-aware, perform actions, and overcome the limitations of their training data.

      • Think of pre-MCP AI like a computer without internet -- powerful but isolated. Adding MCP is like not just giving it internet access, but also an app store where each new app comes with a clear instruction manual.

        When AI uses an MCP server, it's like having a menu at a restaurant. The menu (server) tells you what's available (tools) and what each thing is in plain language. You don't need to know how the kitchen works -- you just need to know what you want and what goes into it.

      • You still need API keys (Personal Access Tokens) and proper security (it's not magic), but what makes MCP special is how it creates a standard way for AI to discover and use tools without needing to know every technical detail. I see MCP as the moment AI tools went from being smart-but-isolated to actually being able to interact with the world.

      • Example Prompts

        • How many open issues are there on microsoft/agent-framework repository

        • “Generate a C# function that takes a list of integers and returns only the even numbers. Also, summarize in one paragraph how this function works and provide a unit test for it.”

          • Code generation

          • Explanation/summarization

          • Test creation

          This prompt demonstrates how you can request code, explanations, and tests all in a single request, leveraging the advanced tools and models available via the MCP server.

      • BlazorAgentWebAppServerMCP (Blazor Web App template Interactive Render Mode ... Server with .NET 10 )

        • We use a Blazor Web App with Render Mode Interactive Server to facilitate the interaction with the MCP server.

        • Coding Specifics

          • First we need to install the NuGet Package ModelContextProtocol

          • Next we need a GitHub Personal Access Token (PAT) for authentication

            • GitHub Settings -> Developer Settings -> Personal Access Tokens

          • Now in our MCPpage.razor we start off by adding two new usings
            @using ModelContextProtocol.Client

          • Code Section

            private async Task SubmitMcpPrompt()

            {

            // Ignore empty prompts

            if (string.IsNullOrWhiteSpace(userPrompt))

            {

            return;

            }


            buttonText = "Sending...";


            // Create MCP Client pointing to GitHub Copilot MCP endpoint

            // with Personal Access Token (PAT) for authentication (GitHub Settings -> Developer Settings -> Personal Access Tokens)

            await using McpClient gitHubMcpClient = await McpClient.CreateAsync(new HttpClientTransport(new HttpClientTransportOptions

            {

            TransportMode = HttpTransportMode.StreamableHttp,

            Endpoint = new Uri(""),

            AdditionalHeaders = new Dictionary<string, string>

            {

            { "Authorization", "yourPat" }

            }

            }));


            // List available tools in GitHub MCP

            // The returned toolsInGitHubMcp collection contains metadata about each tool (such as its name, capabilities, and how to invoke it).

            // These tools are then provided to the AI agent, enabling it to use them when responding to user prompts.

            IList<McpClientTool> toolsInGitHubMcp = await gitHubMcpClient.ListToolsAsync();


            OpenAI.OpenAIClient client = new OpenAIClient(apiKey);


            // Create AI Agent with tools from GitHub MCP server

            // The agent is configured with specific instructions and the tools retrieved from the MCP server.

            AIAgent agent = client

            .GetChatClient(model)

            .CreateAIAgent(

            instructions: "You are a GitHub Expert",

            tools: toolsInGitHubMcp.Cast<AITool>().ToList()

            )

            .AsBuilder()

            .Build();


            // In this single-turn use case, the thread is not strictly necessary.

            // However, it is a good practice for future extensibility and aligns with how conversational AI agents are typically managed.

            AgentThread thread = agent.GetNewThread();


            ChatMessage message = new ChatMessage(ChatRole.User, userPrompt +" format every response in HTML");

            AgentRunResponse response = await agent.RunAsync(message, thread);


            responseText = response.Text;


            buttonText = "Send to ChatGPT";

            }

    • Supplementary Demos

      • BlazorMSagentFrameworkToolsTravelAgent

        • This application demonstrates how to build an intelligent travel planning agent using the Microsoft Agent Framework for .NET. The agent can automatically generate personalized day-trip itineraries for random destinations around the world.

        • Capabilities:

          • Random Destination Selection: Uses a custom tool to pick vacation spots

          • Intelligent Trip Planning: Creates detailed day-by-day itineraries

          • Real-time Streaming: Supports both immediate and streaming responses

          • Custom Tool Integration: Demonstrates how to extend agent capabilities

        • Tool.cs
          // Tool Function: Random Destination Generator

          // This static method will be available to the agent as a callable tool

          // The [Description] attribute helps the AI understand when to use this function

          [Description("Provides a random vacation destination.")]

          public static string GetRandomDestination()

          {

              // List of popular vacation destinations around the world

              // The agent will randomly select from these options

              var destinations = new List<string>

              {

                  "Paris, France",         // European cultural capital

                  "Tokyo, Japan",          // Asian modern metropolis

                  "New York City, USA",    // American urban experience

                  "Sydney, Australia",     // Oceanic coastal beauty

                  "Rome, Italy",           // Historical European city

                  "Barcelona, Spain",      // Mediterranean cultural hub

                  "Cape Town, South Africa", // African scenic destination

                  "Rio de Janeiro, Brazil",  // South American beach city

                  "Bangkok, Thailand",        // Southeast Asian cultural center

                  "Vancouver, Canada"         // North American natural beauty

              };


              // Generate random index and return selected destination

              // Uses System.Random for simple random selection

              var random = new Random();

              int index = random.Next(destinations.Count);

              return destinations[index];

          }

        • TravelAgentPage.razor (code section)
          OpenAI.OpenAIClient client = new OpenAIClient(apiKey);


          //Create the AI Agent with the custom tool GetRandomDestination which is defined in the Tools class

          AIAgent agent = client.GetChatClient(model)

          .CreateAIAgent(

          instructions: "You are a helpful AI Agent that can help plan vacations for customers at random destinations",

          tools:

          [

          AIFunctionFactory.Create(Tools.GetRandomDestination)

          ]

          );


          //In this single-turn use case, the thread is not strictly necessary.

          //However, it is a good practice for future extensibility and aligns with how conversational AI agents are typically managed.

          AgentThread thread = agent.GetNewThread();


          //--------------------------------------------------------------------------

          //Other Optional Methods to call the agent:


          //ChatMessage Method

          //ChatMessage message = new ChatMessage(ChatRole.User, userPrompt + " where possible formatted the response nicely in HTML");

          //AgentRunResponse response = await agent.RunAsync(message, thread);

          //Direct String Method

          //AgentRunResponse response = await agent.RunAsync(userPrompt + " where possible formatted the response nicely in HTML", thread);


          //--------------------------------------------------------------------------


          //Non-streaming response

          //responseText = response.Text;


          //Streaming response

          //A streaming response means the server sends back pieces of the result as soon as they are available,

          // instead of waiting for the whole answer to be ready. This is useful for things like chatbots,

          // where you want to show the answer as it's being generated.


          await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(userPrompt + " format every response in HTML"))

          {

          responseText += update.Text;

          StateHasChanged(); // Refresh UI with new content

          }

      • BlazorMSagentFrameworkToolsTravelAgentUpdate

        • Note the addition of very Detailed instructions in the CreateAIAgent statement that define the agent's personality, capabilities, and behavior These instructions shape how the agent responds and interacts with users. We also now fully implement a conversation thread to maintain context across multiple interactions.

        • // Reuse existing thread or create a new one if null

          // Threads enable the agent to remember previous exchanges and maintain conversational state

          // This is essential for multi-turn conversations and contextual understanding

          if (thread == null)

          {

          thread = agent.GetNewThread();

          }
          //Direct String Method

          AgentRunResponse response = await agent.RunAsync(userPrompt + " where possible formatted the response nicely in HTML", thread);


          //--------------------------------------------------------------------------


          //Non-streaming response

          responseText += response.Text;

          responseText += "<hr/>";

  • An Introduction to the Microsoft Agent Framework (Structured Output) Part 335:30

    In this Lecture we will

    • Learn how to Structure Output ... Response from AI is formatted using class properties

      • BlazorMSagentWebAppServerStructuredOutput  (hands on)

      • This application demonstrates how to interact with an AI model to retrieve structured and unstructured responses. By defining a specific format using the Movie class, we can structure the AI's response to return a list of movies with details like Title, Release Year, and Rating, which are displayed in a table. Alternatively, an unstructured response may return varying data formats, in one instance Title and Rating , and in another instance Title and Date. So if you want a consistent response a Structured technique needs to be implemented.

      • Coding Specifics

        • Since we are going to ask questions specific to movies we create a simple class called Movie.cs in a folder called Models
          public class Movie

          {

               public string Title { get; set; }

               public string ReleaseYear { get; set; }

               public string Rating { get; set; }

          }

        • Now in the Pages folder we create a razor component called AgentPage.razor

        • We will first add code to return an Unstructured response from the AI as a point of reference before adding code to return a Structured response.

          • First some initial usings
            @using OpenAI

            @using Microsoft.Agents.AI

          • Now to the Code Section some initial variable declarations
            private string userPrompt = string.Empty;

            private string? responseText;               // For unstructured response

            private List<Movie>? movies;                // For structured response

            private string buttonText = "Send to ChatGPT";

            const string apiKey = "yourKey";

            const string model = "gpt-4o-mini";

          • Here is our main Task which initially will only handle an Unstructured response and then the Structured response (note comments)
            private async Task SubmitPrompt()

            {

                buttonText = "Sending...";

               

                OpenAI.OpenAIClient client = new OpenAIClient(apiKey);


                //Unstructured Response

                //----------------------------------------------------------

                //AIAgent agent1 = client.GetChatClient(model).CreateAIAgent(instructions: "You are an expert in The Movie Datatbase Lists");

                //AgentRunResponse response1 = await agent1.RunAsync(userPrompt);

                //responseText = response1.Text;


                //Structured Response

                //----------------------------------------------------------

               ChatClientAgent agent2 = client.GetChatClient(model).CreateAIAgent(instructions: "You are an expert in The Movie Datatbase Lists");


            //Note the difference between AIAgent and ChatClientAgent:

            //--------------------------------------------------------

            //AIAgent is typically used for unstructured responses, where you just want free-form text from the AI (like a chat or general answer).

            //ChatClientAgent is a base class that provides more advanced features, including the ability to specify

            //and extract structured responses(like a list of Movie objects).

            //Define the structure of the response expected and pass it to the agent. Here we expect a list of Movie objects.

            //Extract the structured response with Result property instead of Text.

                AgentRunResponse<List<Movie>> response2 = await agent2.RunAsync<List<Movie>>(userPrompt);

            movies = response2.Result;


                buttonText = "Send to ChatGPT";

            }

            private void ClearAll()

            {

                userPrompt = string.Empty;

            responseText = null;

                movies = null;

            }

          • Now in the HTML section our coding for the Unstructured case is very straightforward
            <h3>Agent Page</h3>

            <div class="mb-3">

                <label for="userInput">Enter your Question</label>

                <input @bind="userPrompt" id="userInput" class="form-control" placeholder="Query The Movie Database" />

                <button @onclick="SubmitPrompt" class="btn btn-primary mt-3">@buttonText</button>

                <button @onclick="ClearAll" class="btn btn-secondary mt-3">Clear All</button>

            </div>


            @if (responseText != null)

            {

                <div class="alert alert-info">

                    <p>@((MarkupString)responseText)</p>

                </div>

            }

          • For the Structured case ... we will be returning  a LIST of Movie (ie movies)  so we can display these results in a Table format
            @if (movies != null && movies.Any())

            {

                <div class="alert alert-info">

                    <h4>Structured Response:</h4>

                    <table class="table table-bordered table-striped">

                        <thead>

                            <tr>

                                <th>Title</th>

                                <th>Release Year</th>

                                <th>Rating</th>

                            </tr>

                        </thead>

                        <tbody>

                            @foreach (var movie in movies)

                            {

                                <tr>

                                    <td>@movie.Title</td>

                                    <td>@movie.ReleaseYear</td>

                                    <td>@movie.Rating</td>

                                </tr>

                            }

                        </tbody>

                    </table>

                </div>

    • Learn how to Work with other inputs (images, pdf,text) ... BlazorHybridMAFimages (.NET MAUI Blazor Hybrid App) ... demo only

      • This application allows users to interact with an AI agent by providing inputs in the form of text, images, or PDF files. Users can choose between two options:

        • Hardcoded Options: Select from a predefined image , a PDF or enter your own Text query. We use a  dropdown list to specify the type of hardcoded input the user is making  (Text, Image, or PDF) to query the AI.  The Hard coded image/pdf  is stored in the wwwroot images folder and we also download and use  an image via a URI (Uniform Resource Identifier) that points to a Web Page

        • Custom Image Upload: We use the .NET MAUI  File Picker to upload your own image file. The dropdown list is not used in this case. We initially implemented the InputFile Component to do our file picking but experienced frequent issues so we switched to the more reliable .NET MAUI File Picker.

        • The AI processes the selected input and provides a response based on the chosen scenario.

        Note:This .NET MAUI Blazor Hybrid Application is Windows Desktop only

      • Coding Specifics

        • For this example we ended up using a Service/Class to implement a File Picker which we will use as our second technique to access images or pdf files.

          • In a Service folder we added a class called FileService.cs
            using Microsoft.Maui.Media;
                public  class FileService

                {

                    //Task that allows user to pick a photo from device using native file picker

                    //This is called (injected) in the AgentPage.razor component

                    public async Task<FileResult> PickAPhotoAsync()

                    {

            #if ANDROID || IOS || MACCATALYST || WINDOWS

                        if (MediaPicker.Default.IsCaptureSupported)

                        {

                            return await MediaPicker.Default.PickPhotoAsync();

                        }

                        else

                        {

                            throw new NotSupportedException("Photo access is not supported on this device.");

                        }

            #else

                        throw new NotSupportedException("Photo access is not supported on this device.");

            #endif

                    }

                }

          • ... then in MauiProgram.cs
            builder.Services.AddScoped<FileService>();

        • AgentPage.razor ... Key Elements

          • Using Declarations
            @using OpenAI

            @using Microsoft.Agents.AI

            @using Microsoft.Extensions.AI

            @using BlazorHybridMAFimages.Services

            @inject FileService fileService

          • Code Section

            • //Scenario selection for Hardcoded Options ... Where Scenario is an enum defined below

              Scenario scenario = Scenario.Text;

              // Enum to represent different scenarios for hardcoded options

              enum Scenario

              {

                  Text,

                  Pdf,

                  Image

              }

            • // The following variables are used to manage image data and display state:

              string dataUriImg = string.Empty;       // For Base64 image data URI

              string imageUri = string.Empty;         // For image URI


              bool isImageURI = false;                // Flag to indicate if image is from URI ... used in HTML section

              bool isImageBase64 = false;             // Flag to indicate if image is from Base64 ... used in HTML section

              string fileSelected = string.Empty;     // For displaying selected file info

            • // Method to submit the prompt to the AI agent and get a response ... Hardcoded Options

              private async Task SubmitPrompt()

              {

                  responseText = null;

              imageUri = string.Empty;

              dataUriImg = string.Empty;

                 

                  //if userPrompt is empty just return

                  if (string.IsNullOrWhiteSpace(userPrompt))

                  {

                      //responseText = "Please enter a prompt.";

                      await App.Current.Windows[0].Page.DisplayAlert("Warning", "Please enter a prompt", "OK");

                      return;

                  }


                  buttonText = "Sending...";


              //Create OpenAI Client and AIAgent

                  OpenAI.OpenAIClient client = new OpenAIClient(apiKey);

                  AIAgent agent = client.GetChatClient(model).CreateAIAgent();

                         

              //Handle different scenarios based on user selection

                  switch (scenario)

                  {

              //Text Scenario ... Standard technique with HTML formatting request

                      case Scenario.Text:

                          response = await agent.RunAsync(userPrompt + " format every response in HTML");

                          responseText = "Scenario Text " + "<hr/>" + response.Text;

                          break;

                      case Scenario.Pdf:

                          //Notes

                          //- This Scenario only work on OpenAI. Not AzureOpenAI!

                          //- PDFs can't be read via URI; Only 'local' data

                          //---------------------------------------------------------------------------------

                          string path = Path.Combine("wwwroot", "images", "lecture1.pdf");

                          //---------------------------------------------------------------------------------

                          //PDF as Base64

                          string base64Pdf = Convert.ToBase64String(File.ReadAllBytes(path));

                          string dataUri = "data:application/pdf;base64," + base64Pdf;

                          response = await agent.RunAsync(new ChatMessage(ChatRole.User,

                          [

                              new TextContent(userPrompt),

                              new DataContent(dataUri, "application/pdf")

                          ]));


                          responseText += "Scenario PDF as Base64 " + "<hr/>"  + response.Text + "<br/>"; 
                          break;        

                      case Scenario.Image:

                          //Image via URI

                          response = await agent.RunAsync(new ChatMessage(ChatRole.User,

                          [

                              new TextContent(userPrompt),

                              new UriContent("", "image/jpeg")

                          ]));


              //Set flag to show image from URI

                          imageUri = "";

                          isImageURI = true;

                         

                          responseText += "Scenario Image via URI" + "<hr/>"  +response.Text + "<br/>" + "<br/>";


                          //---------------------------------------------------------------------------------

                          //Local File

                          string pathImg = Path.Combine("wwwroot", "images", "beach.jpg");


                          //Image via Base64

                          string base64Img = Convert.ToBase64String(File.ReadAllBytes(pathImg));

                          dataUriImg = "data:image/jpeg;base64," + base64Img;

                          response = await agent.RunAsync(new ChatMessage(ChatRole.User,

                          [

                              new TextContent(userPrompt),

                              new DataContent(dataUriImg, "image/jpeg")

                          ]));


              //Set flag to show image from Base64

                          isImageBase64 = true;

                          responseText += "Scenario Image Local File via Base64" + "<hr/>"  + response.Text + "<br/>";

                          break;           

                  }


                  buttonText = "Send to ChatGPT ... Hardcoded";

              }

            • // Method to open the Media Picker and get a photo using the FileService

              // This method is an alternative to OnFileChanged and is more reliable in MAUI Blazor Hybrid

              private async Task GetPhoto()

              {

                  try

                  {

                      var photo = await fileService.PickAPhotoAsync();    // PickAPhotoAsync opens the Media Browser in Windows and allows user to load (pick) any photo from the file system

                      if (photo != null)

                      {                                               

                          dataUriImg = await GetPhotoDataUrlAsync(photo);   // Convert the photo to a data URL for display ... Base64-encoded image data URL

                          isImageBase64 = true;

                          StateHasChanged();

                      }

                  }

                  catch (Exception ex)

                  {

                      responseText = "Error loading Photo: " + "<br /><hr />" + ex.Message;

                  }   

              }


              // Helper method to convert the photo from the GetPhoto Task to a Base64 data URL

              private async Task<string> GetPhotoDataUrlAsync(FileResult photo)

              {

                  using var stream = await photo.OpenReadAsync();                                         // Open the photo stream for Reading

                  using var ms = new MemoryStream();                                                      // Create a Memory stream to hold the image data

                  await stream.CopyToAsync(ms);                                                           // Copy the photo stream to the Memory stream (to be displayed in the <img> tag)

                  var imageBytes = ms.ToArray();                                                          // Convert the memory stream to a byte array

                  return "data:" + photo.ContentType + ";base64," + Convert.ToBase64String(imageBytes);   // Create a data URL for the image where we convert the byte array to a Base64 string

              }

            • // Method to submit the user-selected image file (using GetPhoto) to the AI agent

              private async Task SubmitFilePrompt()

              {

                  responseText = null;


                  //if userPrompt is empty just return

                  if (string.IsNullOrWhiteSpace(userPrompt))

                  {

                      //responseText = "Please enter a prompt.";

                      await App.Current.Windows[0].Page.DisplayAlert("Warning", "Please enter a prompt", "OK");

                      return;

                  }


                  //if no file selected just return

                  if (string.IsNullOrWhiteSpace(dataUriImg))

                  {

                      //responseText = "Please select an image file.";

                      await App.Current.Windows[0].Page.DisplayAlert("Warning", "Please select an image file", "OK");

                      return;

                  }


                  buttonFileText = "Sending...";


                  OpenAI.OpenAIClient client = new OpenAIClient(apiKey);

                  AIAgent agent = client.GetChatClient(model).CreateAIAgent();


                  //Local File Image (using InputFile Component) via Base64


              //dataUriImg is set in OnFileChanged method or GetPhoto method

                  response = await agent.RunAsync(new ChatMessage(ChatRole.User,

                  [

                      new TextContent(userPrompt),

                      new DataContent(dataUriImg, "image/jpeg")

                  ]));


              //Set flag to show image from Base64

                  isImageBase64 = true;

                  responseText += "Scenario Image Local File via Base64" + "<hr/>"  + response.Text + "<br/>";


                  buttonFileText = "Send User Selected Local Image to ChatGPT";

              StateHasChanged();

              }

          • HTML section

            <div class="row">

                <div class="col-md-6 mb-3">

                    <label for="userInput">Enter a Query for the related Scenario Type or Your own Selected Image</label>

                    <input @bind="userPrompt" id="userInput" class="form-control" />

                </div>


                <div class="col-md-6 mb-3">

                    <label for="scenarioSelect">Select Scenario ... Only for Hardcoded Option</label>

                    <select @bind="scenario" id="scenarioSelect" class="form-select">

                        <option value="Text">Text</option>

                        <option value="Pdf">PDF</option>

                        <option value="Image">Image</option>

                    </select>

                </div>

            </div>



            <button @onclick="SubmitPrompt" class="btn btn-primary mt-2">@buttonText</button>

            <button @onclick="ClearAll" class="btn btn-secondary mt-2">Clear All</button>


            <hr/>


            <InputFile OnChange="OnFileChanged" accept="image/png,image/jpeg" class="form-control mt-2" style="width: 50%;" />

            <button class="btn btn-primary mt-2"  @onclick="GetPhoto">Load Image with Media Picker</button>

            <button @onclick="SubmitFilePrompt" class="btn btn-primary mt-2">@buttonFileText</button>


            @if (isImageURI)

            {

                <div class="mt-3">

                    <h5>Image from URI:</h5>

                    <img src="@imageUri" alt="Image from URI" style="max-width: 200px; max-height: 200px;" />

                </div>

            }


            @if (isImageBase64)

            {

                <div class="mt-3">

                    <h5>Image from Base64:</h5>

                    <img src="@dataUriImg" alt="Image from Base64" style="max-width: 200px; max-height: 200px;" />

                </div>

            }


            @if (responseText != null)

            {

                <div class="alert alert-info mt-3">

                    <p>@((MarkupString)responseText)</p>

                </div>

            }

    • Supplementary Demos

      • BlazorHybridMAFimagesWithThread

        • In this updated version we have included the implementation of a thread to maintain context when Loading Images with the Media Picker. Check out the SubmitFilePrompt method for coding details , in particular note AgentRunResponse response statement

      • BlazorHybridMAFimagesStruct

        • In this updated version We have introduced 3 agents with specific instructions to return either unstructured or structured responses when processing specific image input.

        • Response Handling: The AI agent's responses are displayed in HTML format for better readability. For image inputs, the application can extract structured data into predefined classes (FormResult and Table) if the image has the right format

        • The Table (image) structured results are displayed in a tabular format using QuickGrid.

      • BlazorHybridMAFimagesStructFinal

        • This update to the update features numerous additions and enhancements to review and extend our AI skills with a focus on Windows Specific implementations and a XAML page application that works also on Android mobile devices

        • Agent Page with Text, Image, and PDF Inputs ... Updated with Structured Output

          This page allows users to interact with an AI agent by providing inputs in the form of text, images, or PDF files. Users can choose between two options:

          • Hardcoded Options: Select from a predefined image, a PDF , or enter your own Text query. We use a dropdown list to specify the type of hardcoded input the user is making (Text, Image, or PDF) to query the AI. A Hard coded image and pdf are stored in the wwwroot images folder and we also download and use an image via a URI (Uniform Resource Identifier) that points to a Web Page

          • Custom Image Upload: Use the .NET MAUI File Picker to upload your own image file. The dropdown list is not used in this case.We initially implemented the InputFile Component to do our file picking but experienced frequent issues so we switched to the more reliable .NET MAUI File Picker.

        • Images Only ... with Structured Output + Speech

          This page allows users to interact with an AI agent by providing input in the form of images files. Custom Image Upload: Use the .NET MAUI File Picker to upload your own image file.

          • Note the implemenation of a Real Time Speech to Text component as an alternate way to enter a Query

        • Images + WebCam (Windows Only)

          This page allows users to interact with an AI agent by providing input in the form of images files. Custom Image Upload: Use the .NET MAUI File Picker to upload your own image file.

          This application highlights how to capture Images via a Webcam and perform Image Analysis. You will also be able to load and save images to and from the local file system (Windows) or Photo Gallery (Android).

        • Images + WebCam + QR Codes (Reading/Creating) ... Windows Only

          • *** NEWEST UPDATE !! *** This latest version demostrates how to CREATE and READ QR Codes + Live Cam Reading of QR codes.

            • We use a service/class called QrCodeService (Services folder) to generate QR codes from text input and display them as images.This service uses the QRCoder NuGet Package

            • We also implement the ability to read QR codes from images using JavaScript interop.See the script called QRscript and the reference to jsQR a JavaScript library used for decoding QR codes from image data

            • ... as usual make note of the declarations for QRCoder in MauiProgam.cs and QRscript and jsQR in the index.html file

            • The Live Cam Reading uses the qr-scanner javascript library that adds the ability to use the Webcam to instantly capture any QR code placed in front of it and read/decode it in real-time.

            • This js library uses the files qr-scanner.umd.min.js and qr-scanner-worker.min.js with the qrHelper.js kickstarting the process

          • Video AI Analysis Applications (Windows Only)

            • Windows Only. This page allows you to access a Webcam and Record , Save, and Load Videos and Take single frame Images from a live feed video and then Analyze these single frame images or a series of video frame images using AI.

            • We also add the ability to compare images for Similarity (a numerical representation of how alike two images are). Specifically we compare the first and last frames of any selected video using two techniques.

              • There are no video AI analysis buttons on screen (except for the basic "Analyze First Frame" button)

              • Once a user saves and then re-loads a video , 10 evenly spaced frames of the video appear and anyone of them can be clicked on for Analysis, additionally a new button appears labeled "Analyze All Captured Frames" which can be clicked.

              • *** UPDATE *** we have now also added a button to perform Motion Detection on the selected video frames (visible once video is selected and loaded on screen)

            • This implementation does not use a Service but rather directly connects to the webcam using the OpenCvSharp library (OpenCvSharp4.Windows NuGet Package) with all the code on this page.

          • Sound Recording , Playback , AI Transcribing and Translation ... and More ! (Windows Only)

            • This page allows you to Record Audio (using your device's microphone), Play Back Recordings,Save them as MP3 files, and Load existing audio files.

            • You can also select and play MP3 files from the AppDirectory audio folder.

            • It also implements some AI Integration by Converting Audio to Text (Transcribing), and Converting Foreign Audio to English Text (Translating) using the OpenAI's Whisper-1 model (See endpoints in ConvertAudioToText Task located on this page ).

            • There is also an option to Play back Text to Audio in a number of voices (See OpenAIServices ... ConvertTextToSpeechAsync Task ... text-to-speech api model called tts-1)

          • Generate Images

            • This XAML page prompts for an image suggestion and creates a matching image. We implement a Service called OpenAIServices which uses the image api model called dall-e-2 to generate images.

        • BlazorHybridMAFimgStrctFinalSpchTxt

          • Speech to Text using MAF and Whisper-1 Model

            This page provides a seamless Speech-to-Text experience using OpenAI's Whisper-1 model. Users can record their speech, which is transcribed and sent to an AI agent for intelligent query responses. Key features include:

            • Speech-to-text conversion powered by Whisper-1. WITHOUT the use of Javascript (ie speech-to-text.js) as was done in previous options in this application

            • Note for true Real-Time transcription (without using Javascript), you would need to use a different service like Azure Speech Services which designed for streaming audio and providing real-time transcription results.

            • In this application we capture all the audio before displaying the full transcription, when done the user clicks on a button to stop listening, and then this transcription (user query) is automatically sent to the AI for a response

            • In this application we reference the OpenAI.Audio namespace which is used to interact with the AudioClient class and its methods such as TranscribeAudioAsync. These are critical for the speech-to-text functionality implemented in the SpeechTextMAF and ContinueProcess methods

            • For the actual audio capture from the users microphone we reference the NAudio Nuget Package

            • ... As an added extra we have added a Read It Back (AI answer) and Language Convert (AI answer) functionality. Here we are borrowing from the AgentPageAudio.razor page methods. See the ReadItBack method (references OpenAIServices.ConvertTextToSpeechAsync) and the SpeechTextLangMAF method (uses a Language translation agent) for details.

  • An Introduction to the Microsoft Agent Framework (Multi-Agents) Part 435:41

    In this Lecture we will

    • Look at several Multi Agent Scenarios

      • Single agents are powerful, but real-world scenarios often require multiple specialized agents working together. Maybe your writing agent creates great content, but you also need an editor to polish it, or a fact-checker to verify details. Microsoft Agent Framework makes multi-agent orchestration as simple as connecting building blocks.

        • Each agent has its own

          • Name (identity)

          • Instructions (what it should do)

          • Model (the brain behind it)

      • BlazorMSagentWebAppServerMultiAgent  ... multi model - multi agent (hands on)

        • For this example we use 3 agents. The first agent is used to determine the intent using the cheaper gpt-4o-mini model and then if the intent is Movie, Music, or Other we will use gpt-4.1 .  We use three separate Agents based on the gpt-4.1 model that have specific instructions unique to Movies , Music or Other 

        • Coding Specifics

          • First thing we do is create a class called IntentResult in the Models folder , it will only have one property Intent which is itself of type enumeration Intent
            public class IntentResult

            {

                //The Description Attribute provides a human-readable description for the property.

                //It's often used by UI frameworks, design-time tools, or documentation generators to display helpful information about the property.

                [Description("What type of question is this ?")]

                public required Intent Intent { get; set; }

            }

            public enum Intent

            {

                MusicQuestion,

                MovieQuestion,

                Other

            }

          • AgentPage.razor

            • Our initial declarations  (usings) in the HTML section
              @using OpenAI

              @using Microsoft.Agents.AI

              @using OpenAI.Chat

            • Next we go into our Code section

              • Our initial declarations
                private string userPrompt = string.Empty;

                private string? responseText;               // For unstructured response

                private string buttonText = "Send to ChatGPT";

                //Our API key and model names

                const string apiKey = "yourKey";

                const string modelA = "gpt-4o-mini";

                const string modelB = "gpt-4.1";

              • private async Task SubmitPrompt()

                {

                //Check for empty prompt

                    if (string.IsNullOrWhiteSpace(userPrompt))

                    {

                        responseText = "<b>Please enter a valid question.</b>";

                        return;

                }       

                    buttonText = "Sending...";


                //Initialize OpenAI Client

                    OpenAI.OpenAIClient client = new OpenAIClient(apiKey);             


                //Multi Agent Example - Choose model based on user prompt

                    ChatClient chatClientMini = client.GetChatClient(modelA);

                    ChatClient chatClient = client.GetChatClient(modelB);


                //Determine the initial intentions based on user prompt ... here we return an IntentResult object (structured data)

                //Note we are using modelA ... gpt-4o-mini

                   

                    ChatClientAgent intentAgent = chatClientMini.CreateAIAgent(name: "IntentAgent", instructions: "Determine what type of question was asked. Never answer yourself");

                AgentRunResponse<IntentResult> initialResponse = await intentAgent.RunAsync<IntentResult>(userPrompt);

                IntentResult intentResult = initialResponse.Result;         

                    //Extract the structured IntentResult object by using the .Result property

                    //Based on the determined intent, choose the appropriate agent to answer the question

                    //Branch out based on Intent ... we are now using different agents for different intents with a different model (modelB) ... gpt-4.1

                    switch (intentResult.Intent)

                    {

                        case Intent.MusicQuestion:

                            responseText=  "Music Question"  + "<hr/>";

                            ChatClientAgent musicNerdAgent = chatClient.CreateAIAgent(name: "MusicNerd", instructions: "You are a Music Nerd answering questions ");

                            AgentRunResponse responseFromMusicNerd = await musicNerdAgent.RunAsync(userPrompt + " format every response in HTML");

                            responseText+= responseFromMusicNerd + "<br/>";

                            break;

                        case Intent.MovieQuestion:

                            responseText = "Movie Question"  + "<hr/>";

                            ChatClientAgent movieNerdAgent = chatClient.CreateAIAgent(name: "MovieNerd", instructions: "You are a Movie Nerd answering questions ");

                            AgentRunResponse responseFromMovieNerd = await movieNerdAgent.RunAsync(userPrompt +" format every response in HTML");

                            responseText+= responseFromMovieNerd + "<br/>";

                            break;

                        case Intent.Other:

                            responseText = "Other Question"  + "<hr/>";           

                            //Let Intent agent answer itself     

                            ChatClientAgent otherNerdAgent = chatClient.CreateAIAgent(name: "OtherNerd", instructions: "You are a general knowledge expert answering questions ");

                            AgentRunResponse responseFromOtherNerd = await otherNerdAgent.RunAsync(userPrompt + " format every response in HTML");

                responseText += responseFromOtherNerd + "<br/>";

                           

                            break;

                        default:

                            throw new ArgumentOutOfRangeException();

                }


                    buttonText = "Send to ChatGPT";

                }
                private void ClearAll()

                {

                    userPrompt = string.Empty;

                responseText = null;   

                }

              • HTML section

                <div class="mb-3">

                    <label for="userInput">Enter your Question... Movie or Music related or Other</label>

                    <input @bind="userPrompt" id="userInput" class="form-control"  placeholder="The GodFather"/>

                    <button @onclick="SubmitPrompt" class="btn btn-primary mt-3">@buttonText</button>

                    <button @onclick="ClearAll" class="btn btn-secondary mt-3">Clear All</button>

                </div>


                @if (responseText != null)

                {

                    <div class="alert alert-info">

                        <p>@((MarkupString)responseText)</p>

                    </div>

                }

      • BlazorMAFMultiAgentTools (demo)

        • One Agent is a Tool for another

          • This demo reviews and extends the concepts covered in Lecture 209 (Part 2) ... Implementing Custom Tools (Functions) ... augmented retrieval based on local fuction(s) BlazorMSagentFrameworkTools

          • If you recall ... Sometimes, AI models cannot answer certain questions directly—such as "What is the current time?"—because they lack real-time or external data access. This application demonstrated how to extend an AI agent with custom tools that the agent can call to fetch information like the current date, time, or timezone, enabling more useful and context-aware responses.

          • ... Now in this enhanced demo this application now includes two specialized agents: a StringAgent for string manipulation tasks and a NumberAgent for number-related operations. Each agent is equipped with its own set of tools. A Delegation Agent demonstrates how one agent can use other agents as tools, delegating string and number tasks without handling them directly. Additionally, a Jack of All Trades Agent is shown, which has direct access to both string and number tools, allowing it to perform all supported operations itself rather than delegating to specialized agents.

        • Coding Specifics

          • First make note of the NumberTools.cs and StringTools.cs classes in the Models folder. They contain static methods that can be referenced by our Agents
            public class NumberTools

            {

                 public static int AnswerToEverythingNumber()

                 {

                     return 42;

                 }   


                 public static int RandomNumber(int min=0,int max=100)

                 {

                     return Random.Shared.Next(min, max+1);

                 }

            }
            public class StringTools

            {

                public static string Uppercase(string input)

                {

                    return input.ToUpper();

                }


                public static string Lowercase(string input)

                {

                    return input.ToLower();

                }


                public static string Reverse(string input)

                {

                    char[] charArray = input.ToCharArray();

                    Array.Reverse(charArray);

                    return new string(charArray);

                }   


            }

          • Now in the ToolPage.razor  (note this application has the old example from Lecture 209 within it for comparison and point of reference

            • // Handle Tool Delegation Prompt Submission ... where one agent is a tool for another

              private async Task SubmitToolPrompt()

              {

              buttonText = "Sending...";

              OpenAI.OpenAIClient client = new OpenAIClient(apiKey);

              //Define two specialized agents with their own tools


              //String Manipulation Agent ... where StringTools are defined in Tools.cs (Models folder)

              AIAgent stringAgent = client.GetChatClient(model)

              .CreateAIAgent(

              name: "StringAgent",

              instructions: "You are string manipulator",

              tools:

              [

              AIFunctionFactory.Create(StringTools.Reverse),

              AIFunctionFactory.Create(StringTools.Uppercase),

              AIFunctionFactory.Create(StringTools.Lowercase)

              ]

              );


              //Number Expert Agent ... where NumberTools are defined in Tools.cs (Models folder)

              AIAgent numberAgent = client.GetChatClient(model)

              .CreateAIAgent(

              name: "NumberAgent",

              instructions: "You are number expert",

              tools:

              [

              AIFunctionFactory.Create(NumberTools.AnswerToEverythingNumber),

              AIFunctionFactory.Create(NumberTools.RandomNumber)

              ]

              );


              responseText = "DELEGATION AGENT<br/>";


              //Delegation Agent that uses the two specialized agents defined above as tools

              AIAgent delegationAgent = client.GetChatClient(model)

              .CreateAIAgent(

              name: "DelegateAgent",

              instructions: "Are a Delegator of String and Number Tasks. Never do such work yourself",

              tools:

              [

              //Use the specialized agents as tools ... where AsAIFunction wraps the agent as a tool

              stringAgent.AsAIFunction(new AIFunctionFactoryOptions

              {

              Name = "StringAgentAsTool"

              }),

              numberAgent.AsAIFunction(new AIFunctionFactoryOptions

              {

              Name = "NumberAgentAsTool"

              })

              ]

              );


              //Call the delegation agent to uppercase a string via the StringAgent

              AgentRunResponse responseFromDelegate = await delegationAgent.RunAsync("Uppercase 'Hello World'");

              responseText+= responseFromDelegate + "<br/>";


              responseText+= "JACK OF ALL TRADE AGENT" + "<br/>";


              //Jack of All Trades Agent that has both string and number tools directly instead of indirectly as we did in the Delegation Agent above

              AIAgent jackOfAllTradesAgent = client.GetChatClient(model)

              .CreateAIAgent(

              name: "JackOfAllTGradesAgent",

              instructions: "Are a Agent that can answer questions on strings and numbers",

              tools:

              [

              AIFunctionFactory.Create(StringTools.Reverse),

              AIFunctionFactory.Create(StringTools.Uppercase),

              AIFunctionFactory.Create(StringTools.Lowercase),

              AIFunctionFactory.Create(NumberTools.RandomNumber),

              AIFunctionFactory.Create(NumberTools.AnswerToEverythingNumber)

              ]

              );


              //AgentRunResponse responseFromJackOfAllTrade = await jackOfAllTradesAgent.RunAsync("RandomNumber");

              AgentRunResponse responseFromJackOfAllTrade = await jackOfAllTradesAgent.RunAsync("RandomNumber between 10 and 20");

              responseText+= responseFromJackOfAllTrade + "<br/>";


              buttonText = "Tool Delegation Example";

              }

    • Supplementary Demos

      • BlazorMAFMultiAgentToolsBookingsExample

        • This page demonstrates how to extend an AI agent with custom tools that the agent can call to fetch information like the NEXT available appointment for a Health Clinic. In this example we are giving our agent access to 4 tools:

          • GetNextAvailableAppointment - Returns the next available appointment date/time for the clinic

          • BookAppointment - Books an appointment for a specified date/time

          • CancelAppointment - Cancels an existing appointment

          • ListBookedAppointments - Lists all upcoming appointments

          We can interact with the agent using natural language. We’ll test if the ai agent can automatically map our prompted intent to the relevant function tool and performs the task desired.

      • BlazorMAFMultiAgentToolsBookingsExampleEnhanced

        • In this example we are giving our agent access to 5 tools:

          • ListAvailableDates - Returns all available appointment dates for the clinic

          • GetNextAvailableAppointment - Returns the next available appointment date/time for the clinic

          • BookAppointment - Books an appointment for a specified date/time

          • CancelAppointment - Cancels an existing appointment

          • ListBookedAppointments - Lists all upcoming appointments

          • Latest Update:

            • Conversation context is persisted by declaring the AgentThread outside the SubmitPrompt method, allowing the thread to maintain state across multiple user interactions.

            • This enables enhanced multi-turn conversations, where the assistant can remember previous exchanges and provide contextually relevant responses.

      • Also See Lecture 213 for an enhanced version using the OpenAI Response API BlazorMAFrespConversationTools

  • An Introduction to the Microsoft Agent Framework (Workflows) Part 526:51

    In this Lecture we will

    • Look at the concept of Agentic Workflows (a Beginners Implementation)

      • A workflow defines how agents connect and interact with each other. You can think of it as a flowchart where each agent passes its output to the next one in sequence or in parallel

      • You need to install the NuGet Package Microsoft.Agents.AI.Workflows to use Workflows besides having the usual NuGet Packages ... OpenAI and Microsoft.Agents.AI.OpenAI (Version 1.0 preview 251125.1)

      • Sequential (BlazorMAFWorkflowSequential)  ... hands on

        • This page demonstrates a sequential workflow using the Microsoft Agent Framework. Agents execute in order, passing results along the chain.

          The workflow consists of two agents: one for summarizing the input text and another for translating the summary into French. When you enter text and submit it, the workflow processes the input through both agents sequentially, displaying the results from each step.

        • Coding Specifics

          • HTML declarations
            @using OpenAI

            @using OpenAI.Chat

            @using Microsoft.Extensions.AI;

            @using ChatMessage = Microsoft.Extensions.AI.ChatMessage

            @using Microsoft.Agents.AI

            @*NuGet Package necessary for Workflows*@

            @using Microsoft.Agents.AI.Workflows

          • Code Section

            • Initial Declarations
              private string userPrompt = string.Empty;

              private string userLang = "French";

              private string? responseText;               // For unstructured response

              private string buttonText = "Begin AI Sequential Workflow";

              private bool isLoading = false;

              const string apiKey = "yourKey";

              const string model = "gpt-4o-mini";   

            • private async Task SubmitPrompt()

              {

                  // Check for empty prompt and display error message using responseText and return

                  if (string.IsNullOrWhiteSpace(userPrompt))

                  {

                      responseText = "<span class='text-danger'>Please enter text to process.</span>";

                      return;

                  }


                  buttonText = "Sending...";

                  isLoading = true;

                  responseText = null;


                  // Create the OpenAI client and chat client based on model

                  OpenAI.OpenAIClient client = new OpenAIClient(apiKey);       

                  ChatClient chatClient = client.GetChatClient(model);


              // Create Agents with specific instructions for each step

              // First Agent: Summarization

              // Second Agent: Translation to French/user specified language as indicated by userLang and user input

                  ChatClientAgent summaryAgent = chatClient.CreateAIAgent(name: "SummaryAgent", instructions: "Summarize the text you are given to max 50 words");

                  ChatClientAgent translationAgent = chatClient.CreateAIAgent(name: "TranslationAgent", instructions: "Take a summarized text and Translate it to " + userLang);


              // Build a Sequential Workflow with the two agents so that the output of the first is input to the second

                  Workflow workflow = AgentWorkflowBuilder.BuildSequential(summaryAgent, translationAgent);


              // Prepare the input message (userPrompt) so that it can be sent to the workflow

              // Where ChatMessage is from Microsoft.Extensions.AI and represents a message in the chat with various properties like Role and Text

                  var messages = new List<ChatMessage> { new(ChatRole.User, userPrompt) };


              // Execute the workflow and capture the output

              StreamingRun run = await InProcessExecution.StreamAsync(workflow, messages);   //here we pass our workflow and messages to the execution engine

              await run.TrySendMessageAsync(new TurnToken(emitEvents: true));                //here we actually start the workflow run


              // Process the output messages from the workflow

              // First we declare a list to hold the result

                 List<ChatMessage> result = new();

                 

              // Here we watch the stream of events from the workflow run and store the output when we get it into 'result'

                  await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))

                  {

              if (evt is WorkflowOutputEvent completed)               // Check for the output event ... which means the workflow has completed

                      {

              result = (List<ChatMessage>)completed.Data!;        // Cast the output data to the expected type in this case List<ChatMessage>

              break;                                              // completed.Data! means we are sure Data is not null

                      }

                  }


              // Now we can display the results from each agent in the workflow (where Role is not User)

              // AuthorName holds the name of the agent that produced the message

                  foreach (ChatMessage message in result.Where(x => x.Role != ChatRole.User))

                  {

                      responseText += message.AuthorName + "<hr/>" + message.Text + "<hr/>";

                  }


              buttonText = "Begin AI Sequential Workflow";

              isLoading = false;

              } 
              private void ClearAll()

              {

                   userPrompt = string.Empty;

                   responseText = null;

                   userLang = "French";

              }

            • HTML

              • <div class="mb-3">

                   

                    <label for="userInput">Enter your Text for Processing ... To be Summarized</label>

                    <textarea @bind="userPrompt" id="userInput" class="form-control" style="width:50vw;" rows="5"></textarea>

                   

                    <label class="mt-2">Enter Language you wish to Translate Text above <b>TO</b></label>

                    <input @bind="userLang" class="form-control" style="width:50vw;" />

                   

                    <button @onclick="SubmitPrompt" class="btn btn-primary mt-3">@buttonText</button>

                    <button @onclick="ClearAll" class="btn btn-secondary mt-3">Clear All</button>

                   

                    @if(isLoading)

                    {

                <img src="/images/running.gif" alt="Loading..." style="width:32px; height:32px; margin-left:10px;" />

                    }   

                </div>


                @if (responseText != null)

                {

                    <div class="alert alert-info">

                        <p>@((MarkupString)responseText)</p>

                    </div>

                }     

      • Concurrent (BlazorMAFWorkflowConcurrent)  ... demo only

        • This page demonstrates a Concurrent workflow using the Microsoft Agent Framework. Multiple agents work in parallel, addressing different aspects of a task simultaneously.

          The workflow consists of two agents: one for determining the Reading Level of the input text and another for determining spelling errors. When you enter text and submit it, the workflow processes the input through both agents concurrently.

        • Key Highlights (very similiar to Sequential Workflow)

          • In the HTML Section we use a textarea to hold a potential large story for analysis
            <div class="mb-3">   

                <label for="userInput">Enter your Text for Processing ... Document for Analysis</label>

                <textarea @bind="userPrompt" id="userInput" class="form-control" style="width:50vw;" rows="5"></textarea>       

                <button @onclick="SubmitPrompt" class="btn btn-primary mt-3">@buttonText</button>

                <button @onclick="ClearAll" class="btn btn-secondary mt-3">Clear All</button>

               

                @if(isLoading)

                {

            <img src="/images/running.gif" alt="Loading..." style="width:32px; height:32px; margin-left:10px;" />

                }

            </div>

          • Code Section

            • In the Task SubmitPrompt
              // Create Agents with specific instructions for each step

              // First Agent: Reading Level Analysis

              // Second Agent: Spelling Error Detection

                      ChatClientAgent ReadingLevelAgent = chatClient.CreateAIAgent(name: "ReadingLevelAgent", instructions: "You are a reading level expert who determines the reading level of a given document (use max 200 chars)");

                      ChatClientAgent SpellingErrorAgent = chatClient.CreateAIAgent(name: "SpellingErrorAgent", instructions: "You are a spelling expert if you spot any errors please report them" );


              // Build a Concurrent Workflow with the two agents one does not wait for the other

                      // Note: The [ReadingLevelAgent, SpellingErrorAgent] is a collection expression that creates a collection

                      // (like an array or list) of agents to pass to BuildConcurrent.

                      Workflow workflow = AgentWorkflowBuilder.BuildConcurrent([ReadingLevelAgent, SpellingErrorAgent]);

      • Supplementary Demos

        • BlazorMAFWorkflowSequentialUpdate

          • This updates adds a simple introductory page which  demonstrates a sequential workflow using the Microsoft Agent Framework.

            The workflow consists of three agents, that translate English text input (flow)  to French then Spanish and finally back to English

        • BlazorHybridMAFpdfSeqFlow

          • This page enables you to select any PDF file and extract its entire text content using AI. The extracted text is then processed through a two-step AI workflow:

            • Summarization: The first AI agent condenses the full PDF text into a concise summary (up to 150 words).

            • Translation: The summarized text is then translated into your chosen language by a second AI agent.

              This streamlined process helps you quickly understand and translate the core content of any PDF, all in one place. This is a redo and enhancement of the application created in Lecture 210 (working with other inputs)

            • Note: the updated FileService with the renamed Task PickImageOrPdfAsync which now uses FilePicker instead of MediaPicker(obsolete) and supports images and pdf selections

        • BlazorHybridMAFseqFlowFurniture/BlazorHybridMAFseqFlowFurnitureU

          • This application helps users analyze furniture in a room using AI. Start by selecting an image (preferably of a furnished room). Enter a prompt to analyze the furniture in the image. The initial AI response is then used to launch a sequential workflow involving three specialized agents:

            • SalesAgent – identifies and suggests furniture items from the image

            • PriceAgent – provides realistic price ranges and budget options

            • QuoteAgent – generates a structured quote document for the suggested furniture

            This workflow delivers a comprehensive analysis, pricing, and purchase quote for the furniture in your selected image.

          • The Updated version implements a check after the initial AI analysis of the room before it is sent off to the Sequential Workflow. We must make sure we actually have a room and not an image of something totally unrelated.

            • private async Task SubmitWorkflowPrompt()

              {

                  // Check for empty response from first step (image -> text)  display error message  and return

                  if (string.IsNullOrWhiteSpace(userWorkflowPrompt))

                  {

                      await App.Current.Windows[0].Page.DisplayAlert("Warning", "Please select a image file FIRST via the File Picker" + '\n' + "and process it via Send user selected image to ChatGPT button ", "OK");

                      return;

                  }


                  // Validate that the image contains furniture in a room context

                 if (!ContainsFurnitureContext(userWorkflowPrompt))

                  {

                      await App.Current.Windows[0].Page.DisplayAlert("Warning",

                          "The analyzed image does not appear to contain furniture or a room." + '\n' +

                          "Please select an image of a furnished room to proceed with the workflow.", "OK");

                      return;

                  }

            • // Helper method to validate if the image analysis contains furniture context

              private bool ContainsFurnitureContext(string analysisText)

              {

                  if (string.IsNullOrWhiteSpace(analysisText))

                      return false;


                  string lowerText = analysisText.ToLowerInvariant();


                  // Check for negative indicators (no furniture found)

                  string[] negativeIndicators = {"does not describe", "no furniture", "not a room", "cannot", "cannot identify","unable", "unable to detect", "no room", "not an interior", "outdoor scene", "nature", "animal", "insect", "butterfly" };

                  foreach (string indicator in negativeIndicators)

                  {

                      if (lowerText.Contains(indicator))

                          return false;

                  }


                  // Check for positive furniture-related keywords

                  string[] furnitureKeywords = { "sofa", "couch", "chair", "table", "desk", "bed", "cabinet", "shelf", "dresser", "living room", "bedroom", "dining room", "office", "interior" };

                  foreach (string keyword in furnitureKeywords)

                  {

                      if (lowerText.Contains(keyword))

                          return true;

                  }


                  return false;

              }

        • BlazorMAFWorkflowSequentialWithTools/BlazorMAFWorkflowSequentialWithToolsHIL

          • The first version  demonstrates a Sequential Workflow which includes a Tool calling agent which connects to and utilizes a Model Context Protocol (MCP) server, specifically GitHub's Copilot MCP endpoint.

          • The second updated version incorporates Human in the Loop capabilities, a fallback agent if the user prompt to the MCP servers does not return a response, and a Speech to Text component for voice input.

        • BlazorMAFWorkflowConcurrentUpdate

          • This update demonstrates a Concurrent workflow using the Microsoft Agent Framework. In concurrent orchestration, multiple agents work on the same task simultaneously and independently, providing diverse perspectives on the same input.Parallel processing reduces overall execution time, making the application faster. Ideal for tasks where agents do not need to build on each other's work in a specific sequence.
            The workflow consists of four agents:

            • Flight Agent: Searches various airlines for the best flight options and prices.

            • Hotel Agent: Looks for hotel accommodations within a specified budget and area.

            • Weather Agent: Retrieves historical weather data and forecasts for the time period.

            • Itinerary Agent: Provides the most popular activities and sites for the specified location.

        • BlazorMAFWorkflowConcurrentMore (two more practical applications)

          • In the first example, we create two agents:One to Plan a dinner party based on an input culture and the other to Research customs and menus of the given culture

          • In the second example, we create three agents to analyze customer feedback/reviews. One checks sentiment, the second one extracts key topics and the third flags urgent issues. This is a simple implementation which replicates the functionality of the Review Analysis you see now on many E-Commerice sites

        • BlazorMAFworkflowHandoff

          • This page demonstrates a multi-agent workflow using the Agent Workflow Framework (new NuGet Package) to automate Handoffs between specialized AI agents. When a user submits a question, an IntentAgent first classifies the query as movie, music, or other. Based on this classification, the workflow automatically routes the question to the appropriate expert agent (MovieNerd, MusicNerd, or OtherNerd) for a detailed response.

            This approach contrasts with the AgentPage component, where agent handoff was managed manually using a switch statement based on Intent (See Lecture 211) . Here, the workflow abstraction simplifies coordination between agents, making the process more maintainable and scalable.

        • BlazorMAFworkflowHandoffUpdate

          • This page demonstrates a multi-agent workflow using the Agent Workflow Framework (new NuGet Package) to automate Handoffs between specialized AI agents. In handoff orchestration, agents can transfer control to one another based on context, allowing for dynamic routing and specialized expertise handling.

            In this application we have a Triage agent which determines the context of a homework help question . We utilize three experts (agents) a History Tutor a Math Tutor and an Other Tutor.

        • BlazorMAFworkflowGroupChat

          • Group chat orchestration models a collaborative conversation among multiple agents, coordinated by a manager that determines speaker selection and conversation flow. This pattern is ideal for scenarios requiring iterative refinement, collaborative problem-solving, or multi-perspective analysis. In this application ,we demonstrate a marketing idea generation workflow where a "Writer" agent creates slogans and a "Reviewer" agent evaluates them.

            Group chat orchestration has distinct characteristics compared to other multi-agent patterns:

            • Centralized Coordination: Unlike handoff patterns where agents directly transfer control, group chat uses a manager to coordinate who speaks next

            • Iterative Refinement: Agents can review and build upon each other's responses in multiple rounds

            • Flexible Speaker Selection: The manager can use various strategies (round-robin, prompt-based, custom logic) to select speakers

            • Shared Context: All agents see the full conversation history, enabling collaborative refinement

            Group chat orchestration is ideal for:

            • Iterative Refinement: Multiple rounds of review and improvement

            • Collaborative Problem-Solving: Agents with complementary expertise working together

            • Content Creation: Writer-reviewer workflows for document creation

            • Multi-Perspective Analysis: Getting diverse viewpoints on the same input

            • Quality Assurance: Automated review and approval processes

            Consider alternatives when:

            • You need strict sequential processing (use Sequential orchestration)

            • Agents should work completely independently (use Concurrent orchestration)

            • Direct agent-to-agent handoffs are needed (use Handoff orchestration)

        • BlazorMAFworkflowGroupChatConcierge

          • In this Updated application ,we demonstrate a Tourist asking for best activities in the local city where the "Front Desk " agent offers recommendations and the "Concierge " agent evaluates them.

  • An Introduction to the Microsoft Agent Framework (Responses API ) Part 652:31

    In this Lecture we will

    • Learn that In March 2025, nearly two years after the launch of the Chat Completions API, OpenAI introduced the Response API, a unified interface that combines the best of both the Chat Completions and Assistant APIs.

    • Learn that the Microsoft Agent Framework supports creating agents that use the OpenAI API responses service.

    • The Response API (using OpenAIResponseClient) is a newer, higher-level abstraction for interacting with AI models, focusing on simplified conversation management and streaming support. In contrast, the ChatClient is a lower-level interface that requires more manual handling of conversation state and streaming.

      • Advantages of Response API

        • Manages conversation flow and state automatically.

        • Built-in support for streaming responses, enabling real-time UI updates.

        • Cleaner, more concise code with less boilerplate.

        • Allows for custom agent instructions and roles.

        • Access to tools (Web Search/File Search) not available to the ChatClient API

      • Disadvantages of Response API

        • May offer less granular control compared to ChatClient for advanced scenarios.

        • Potentially less mature or less documented than the established ChatClient.

        • Some advanced features may not be exposed yet.

        • Only available when using OpenAI API or Azure OpenAI

      • Advantages of ChatClient

        • Greater control over message formatting and conversation history.

        • Well-documented and widely used in existing projects and by most LLMs

        • Suitable for custom or complex chat flows.

      • Disadvantages of ChatClient

        • Requires manual management of conversation state and streaming.

        • More boilerplate code and potential for errors.

        • Less streamlined for rapid prototyping or simple use cases.

    • Simple Applications

      • BlazorMAFresponsesIntro  (Hands on) ... followed by quick demo of BlazorMAFresponsesIntroServer 

        • Basic redo of Lecture 208 intro example BlazorMSagentFrameworkIntro (Blazor WebAssembly Standalone App and Blazor Web App Interactive Render Mode Server)

        • First we need to install the OpenAI and Microsoft.Agents.AI.OpenAI NuGet Packages

        • Next we create a Razor Component page called AgentPage.razor and make a couple of declarations in the HTML section
          @page "/agentpage"

          @using OpenAI

          @using Microsoft.Agents.AI

        • Now we focus on the Code section

          • First our variable declarations
            private string userPrompt = string.Empty;

            private string? responseText ;

            private string buttonText = "Send to ChatGPT";

            const string apiKey = "yourKey";

            const string model = "gpt-4o-mini";

          • Next we include a simple method to Clear everything
            private void ClearAll()

            {

                userPrompt = string.Empty;

                responseText = null;

            }

          • Now the key method/task called private async Task SubmitPrompt()

            //No error checking error to keep things short and simple
            //BUT ... we will use it in all the future demos

            buttonText="Sending ...";
            responseText=null;

            // Initialize OpenAI Client

               OpenAI.OpenAIClient client = new OpenAIClient(apiKey);


            // Create the Response client as opposed to the Chat client

            //---------------------------------------------------------

                   
            // Suppress OPENAI001 by using pragma warning disable/restore

            //The OPENAI001 warning is a custom or SDK-specific compiler warning, not a standard C# or .NET warning.

            //It is defined by the OpenAI .NET SDK you are using.


            #pragma warning disable OPENAI001

            var responseClient = client.GetOpenAIResponseClient(model);
            //OpenAIResponseClient responseClient = client.GetOpenAIResponseClient(model);
            //if using the above line, ensure you have "using OpenAI.Response;" at the top

            #pragma warning restore OPENAI001

                   

            // Create the AI Agent with specific instructions

            AIAgent agent = responseClient.CreateAIAgent(name:"Test",instructions:"You are a general knowledge expert. Display your result in HTML");

                   

            //this is the older way using the Chat client

            //---------------------------------------------------------

            //AIAgent agent = client.GetChatClient(model).CreateAIAgent();

            //..... OR ... @using OpenAI.Chat

            //ChatClient chatClient = client.GetChatClient(model);

            //AIAgent agent = chatClient.CreateAIAgent();


            //Regular response - waits for full response

            AgentRunResponse response = await agent.RunAsync(userPrompt);

            responseText = response.Text;   
            buttonText = "Send to ChatGPT";


        • ... and finally we finish off the HTML section 
          <div class="mb-3">

              <label >Enter your Question</label>

              <input @bind="userPrompt"  class="form-control" />

              <button @onclick="SubmitPrompt" class="btn btn-primary mt-3">@buttonText</button>

              <button @onclick="ClearAll" class="btn btn-secondary mt-3">Clear All</button>

          </div>


          @if (responseText != null)

          {

              <div class="alert alert-info">

                  <p>@((MarkupString)responseText)</p>

              </div>

          }  

        • BlazorMAFresponsesIntroServer 

          • Check for empty prompt
            if (string.IsNullOrWhiteSpace(userPrompt))

            {

                responseText = "<p style='color:red;'>Please enter a valid question.</p>";

                return;

            }

          • Streaming response
            await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(userPrompt + " format every response in HTML"))

            {

                responseText += update.Text;

                StateHasChanged(); // Refresh UI with new content

            }

        • ... What about a Local LLM like Ollama

          • In the current Microsoft.Extensions.AI and OpenAI .NET SDK ecosystem, the GetOpenAIResponseClient extension method is designed specifically for OpenAI-compatible clients (OpenAIClient) , not for generic IChatClient implementations like those provided by OllamaSharp.

      • BlazorMAFresponsesWebSearch (Blazor WebAssembly Standalone App) ... (Demo only)

        • This page demonstrates how to access up-to-date information using the OpenAI Web Search Tool. By leveraging the HostedWebSearchTool from the Microsoft.Extensions.AI NuGet package, the AI agent can perform real-time web searches to answer questions about current events or topics not covered in its training data. This is especially useful for scenarios where the latest information is required, such as news, recent research, or dynamic content.

          Cost Considerations: Enabling the Web Search Tool may result in additional API calls, which can increase usage costs compared to standard model queries. Each web search performed by the agent incurs extra charges, so use this feature judiciously when up-to-date information is essential.

        • While the Chat Completions API also supports web search, it requires using specific models like gpt-4o-search-preview or gpt-4o-mini-search-preview. In contrast, the Response API offers greater flexibility, web search can be enabled across a wider range of models, making it easier to integrate into different applications without being locked into special model variants.

        • Important Notes:

          • The Microsoft.Extensions.AI NuGet package must be installed to enable web search capabilities.

          • Responses are formatted in HTML for better readability and presentation.

          • Streaming is used to provide incremental updates to the UI as the AI agent generates its response.

          •         // Initialize OpenAI Client

                    OpenAI.OpenAIClient client = new OpenAIClient(apiKey);


            // Create the RESPONSE client as opposed to the CHAT client

                   
            // Suppress OPENAI001 by using pragma warning disable/restore

            //The OPENAI001 warning is a custom or SDK-specific compiler warning, not a standard C# or .NET warning.

            //It is defined by the OpenAI .NET SDK you are using.


            #pragma warning disable OPENAI001

                    var responseClient = client.GetOpenAIResponseClient(model);
            #pragma warning restore OPENAI001


            // Create the AI Agent with specific instructions ... BEFORE using web search tool (comment temporarily)  ... test out to compare (Display all the NBA game scores from last night ... currrent date)

                    // AIAgent agent = responseClient.CreateAIAgent(name: "Test", instructions: "You are a general knowledge expert. Display your result in HTML");


            // Create/uncomment the AI Agent with Web Search Tool ... AFTER testing without tool

            // The HostedWebSearchTool allows the AI agent to perform web searches to gather up-to-date information.

            // This is particularly useful for answering questions about current events or topics not covered in the training data.  (Display all the NBA game scores from last night ... currrent date)

            // ... BUT it is more costly to use the tool as it involves additional API calls to perform the searches.

                   

                    AIAgent agent = responseClient.CreateAIAgent(

                        name: "Test", instructions: "You are a general knowledge expert. Display your result in HTML",

                        tools:[new HostedWebSearchTool()]

                    );       


                    //Streaming response

                    await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(userPrompt + " format every response in HTML"))

                    {

                        responseText += update.Text;

                        StateHasChanged(); // Refresh UI with new content

                    }

      • BlazorMAFresponsesFileSearchServer ... (Demo only)

        • Demonstrates the use of OpenAI's new File Search Tool for Retrieval-Augmented Generation (RAG) applications.

          • This tool allows the model to retrieve and reference information from files you've uploaded, making it great for use cases like answering questions from documentation, PDFs, knowledge bases, or research papers.

            The Chat Completions API doesn’t support this tool at all. This is one of the Response API’s more agentic capabilities, carried over and improved from the Assistant API.

        • Showcase the process of uploading a PDF file to OpenAI

          • The workflow is : file upload → vector store creation → file indexing → query processing (using the File Search Tool) → result display.

            • Uploaded  file  is  located in wwwroot/data folder

          • Check OpenAI API site ... go to  Dashboard to see your uploaded files (look for Storage section ... then Files/Vector Stores)

            • Explain how the uploaded file is indexed into a vector store to enable efficient semantic search.

            • Note: Need to comment the Vector Store and File deletions temporarily to demonstrate (see finally section of Code)

        • Coding Highlights

          • In the HTML section we have some familar declarations but some very specific to our File Search Tool Application
            @using OpenAI

            @using Microsoft.Agents.AI


            @*Needed to reference OpenAI FileSearch Tool*@

            @using Microsoft.Extensions.AI


            @using OpenAI.Files

            @using OpenAI.VectorStores

            @using System.ClientModel
            @*Use for ClientResult reference when accessing Files or Vector store from cloud service*@

          • In the Code Section (SubmitPrompt)

            • OpenAIFileClient fileClient = client.GetOpenAIFileClient();             //File client for uploading files

              VectorStoreClient vectorStoreClient = client.GetVectorStoreClient();    //Vector store client for creating vector stores

            • //Upload a file to OpenAI located in wwwroot/data folder ... will be stored in your OpenAI account

              //Check OpenAI API site Dashboard to see your uploaded files (look for Storage section)

              string filename="EmergencyKit.pdf";

              byte[] fileBytes = await File.ReadAllBytesAsync(Path.Combine("wwwroot", "data", filename));

              ClientResult<OpenAIFile> uploadedFile = await fileClient.UploadFileAsync(new BinaryData(fileBytes), filename, FileUploadPurpose.Assistants);
              //Get the uploaded file ID

              fileId = uploadedFile.Value.Id;

            • //Create a vector store based on the uploaded file (use the fileId) ...updated file will be indexed into the vector store

              //and named "MyVectorStore"

              ClientResult<VectorStore> vectorStore = await vectorStoreClient.CreateVectorStoreAsync(options: new VectorStoreCreationOptions

              {

                  Name = "MyVectorStore",

                  FileIds = { fileId }

              });

              //store the vector store ID

              vectorStoreId = vectorStore.Value.Id;

            • while (!isReady && attempts < maxAttempts)

              {

                  await Task.Delay(2000);

              ClientResult<VectorStore> storeStatus = await vectorStoreClient.GetVectorStoreAsync(vectorStoreId); //Get the vector store status

                 

              if (storeStatus.Value.FileCounts.Completed > 0 && storeStatus.Value.FileCounts.InProgress == 0)     //Check if indexing is complete

                  {

                      isReady = true;

                  }

                  else if (storeStatus.Value.FileCounts.Failed > 0)

                  {

                      responseText = "<p style='color:red;'>Vector store file indexing failed.</p>";

                      return;

                  }

                 

                  attempts++;

              responseText = "<p>Waiting for file indexing... (attempt " + attempts + "/" + maxAttempts + ")</p>";           //Update status message

                  StateHasChanged();

              }

              • Originally I did not provide any delay for the vector store to complete indexing the file before querying it and that caused errors.  So now we poll the vector store status until indexing is complete or timeout occurs

                I only discovered this when I was stepping through with the debugger and it worked fine because of the manual delays that occur when stepping through code.

            • //Create an AI Agent with the Hosted File Search Tool using the created vector store

              AIAgent agent = client

                  .GetOpenAIResponseClient(model)

                  .CreateAIAgent(

                      instructions: "You are a helpful assistant. Use the file search tool to answer questions based on the uploaded document.Only use tools, never your world knowledge. If you cannot find any relevant information, say 'I could not find any information on that topic.'",

                      tools:

                      [

                          new HostedFileSearchTool

                          {

                              Inputs = [new HostedVectorStoreContent(vectorStoreId)]

                          }

                      ]);


              AgentRunResponse response = await agent.RunAsync(userPrompt + "display the results nicely formated in HTML");


              responseText = response.Text;

              StateHasChanged();

            • finally

              {

                  if (vectorStoreId != null)

                  {

                      await vectorStoreClient.DeleteVectorStoreAsync(vectorStoreId);

                  }


                  if (fileId != null)

                  {

                      await fileClient.DeleteFileAsync(fileId);

                  }

                 

                  buttonText = "Send to ChatGPT";

              }

        • Note: Usage of file storage and vector store operations may incur additional costs beyond standard GPT model usage.

      • BlazorMAFrespConversations ... (Hands on simple implementation then Demo enhanced version)

        • This application demonstrates the use of OpenAI Responses ... Conversations

        • This mirrors the concept of Persistence that we covered in Lecture 209 (BlazorMAFPersistenceOldChatClientTech) where we used a ChatClient as opposed to the Response API.

          • Take a brief look at old code as a point of reference ... the Responses version will look similar (still uses Threads) but the AgentRunResponse has a new property called ResponseId which gives it enhanced capabilities to continue Conversations.

        • Coding Specifics

          • HTML Section

            • We will start a conversation with multiple prompts and then continue the conversation with a follow up prompt (use ConversationsHTMLsnippet)
              @page "/conversations"

              @using OpenAI

              @using OpenAI.Responses

              @using Microsoft.Agents.AI


              @*Needed to reference ChatOptions for the userPostFollowPrompt*@

              @using Microsoft.Extensions.AI


              <div class="mb-3">

                   <label for="userInput">Enter Query ... and follow up queries</label>

                   <input @bind="userPrompt1" id="userInput" placeholder="Who is Elon Musk" class="form-control" />

                   <input @bind="userPrompt2" id="userInput2" placeholder="What is his current wealth" class="form-control mt-2" />

                   <input @bind="userContinuedPrompt" id="userInput3" placeholder="Nationality " class="form-control mt-2" />

                   <button @onclick="SubmitPrompt" class="btn btn-primary mt-3">@buttonText</button>

                   <button @onclick="ClearAll" class="btn btn-secondary mt-3">Clear All</button>

              </div>

                 


              @if (!string.IsNullOrEmpty(responseText))

              {

                  <div class="alert alert-info">

                      <p>@((MarkupString)responseText)</p>

                  </div>

              }

          • Code Section

            • First we start we our variable declarations
              //To hold user prompts

              private string userPrompt1 = null;

              private string userPrompt2 = null;

              private string userContinuedPrompt = null;  // For continued prompt on left side


              //To hold the ConversationId for continuing conversations and follow-ups

              private string? responseId=null;


              private string? responseText;

              private string buttonText = "Send to ChatGPT";


              const string apiKey = "yourKey";

              const string model = "gpt-4o-mini";

            • Next we will create our simple ClearAll method
              private void ClearAll()

              {

                  userPrompt1 = string.Empty;

                  userPrompt2 = string.Empty;

                  userContinuedPrompt = string.Empty;   

                  responseText = null;

                  responseId = null;

              }

            • Now we focus on the  private async Task SubmitPrompt()

              • responseText = null; 

                responseId = null;

                buttonText = "Sending...";


                // Initialize OpenAI client

                OpenAI.OpenAIClient client = new OpenAIClient(apiKey);

              • //pragma stands for "pragmatic information".

                //It's a directive that provides instructions to the compiler

                //about how to handle specific code, rather than being part of the

                //actual program logic that executes at runtime.       

                //The OPENAI001 warning is a OpenAI SDK-specific compiler warning

                #pragma warning disable OPENAI001

              • // Create the Response client as opposed to the Chat client ... that has conversation support

                OpenAIResponseClient responseClient = client.GetOpenAIResponseClient(model);


                //Create an AI Agent with some basic instructions

                AIAgent agent = responseClient.CreateAIAgent(instructions: "You are a Nice AI ");


                //Create a new Agent Thread to maintain context across multiple prompts

                AgentThread thread = agent.GetNewThread();


                //Run the first two prompts in the same thread to maintain context

                AgentRunResponse response1 = await agent.RunAsync(userPrompt1 + "display the results nicely formated in HTML in 50 words or less", thread);

                responseText = response1.Text + "<hr/>";

                AgentRunResponse response2 = await agent.RunAsync(userPrompt2 + "display the results nicely formated in HTML in 50 words or less", thread);

                responseText += response2.Text + "<hr/>";


                //Imagine some time goes by and we want to continue the conversation ... here we use the ConversationId from the last response

                //... and then continue the conversation with a new prompt below which references prior context using the ConversationId stored in response2

                responseId = response2.ResponseId;                 

                //Here we use the standard AgentRunResponse but pass in ChatOptions with the ConversationId to continue the conversation

                AgentRunResponse response3 = await agent.RunAsync(userContinuedPrompt, options: new ChatClientAgentRunOptions

                {

                     ChatOptions = new ChatOptions

                     {

                         ConversationId = responseId

                     }

                });

                responseText += response3.Text;

                buttonText = "Send to ChatGPT";

              • #pragma warning restore OPENAI001

            • We still make use the AgentThread and AgentRunResponse like we did in a prior example BUT now we take adavantage of the ResponseId property to return to a conversation and continue it with a new prompt much later in time.

              • the OpenAI Responses API stores conversation data online by default for up to 30 days for application state and monitoring

        • Demo the enhanced version which includes a separate input area on the right and a new Task SubmitFollowupPrompt ... to really emphasize the use of the ResponseId to access a particular  conversation

    • Supplementary Demos

      • BlazorMAFresponseMCP

        • OpenAI Response API version of the MCP server  application from Lecture 209

      • BlazorMAFresponsesCodeInterpreter

        • This example demonstrates how to use the Code Interpreter Tool with the Response API and AI Agent framework to generate and display images based on user prompts. The Code Interpreter Tool allows the AI to execute code snippets, generate plots, and handle data analysis tasks.

        • Enter a question or request that involves data visualization or analysis, and the AI will respond with text and potentially generate an image to illustrate the results.

        • All the behind scenes stuff is handled by the HostedCodeInterpreterTool class which is part of the OpenAI.Containers namespace. It uses Python in the backend to execute code.

      • BlazorMAFresponsesBackground

        • This application demonstrates how to implement Background Responses for Queries that take a long time to get a full reply.

          How Background Response Works:

          • Note the declaration of an AgentThread. We create a new Agent Thread to maintain context for our background response as we poll for the final response

          • The ChatClientAgentRunOptions is configured with AllowBackgroundResponses = true, enabling the agent to return a continuation token for long-running queries.

          • Note: AgentRunResponse response = await agent.RunAsync(userPrompt,agentThread,options:options);
            The second parameter (agentThread) is used to maintain context and the third parameter (options) is to indicate a background response

          • The code uses async/await and a polling loop to periodically check for the completion of the response, updating the UI with each attempt.

          • The UI displays a loading indicator and status messages until the full response is received and shown to the user.

          • This approach is ideal for handling slow or complex AI queries in a user-friendly way within a Blazor application.

      • BlazorMAFrespConversationTools

        • This is an updated/enhanced version of the Multi Agent Tools Bookings Example from Lecture 211

          • In this example we are giving our agent access to 5 tools:

            • ListAvailableDates - Lists all available dates for booking

            • GetNextAvailableAppointment - Returns the next available appointment date/time for the clinic

            • BookAppointment - Books an appointment for a specified date/time

            • CancelAppointment - Cancels an existing appointment

            • ListBookedAppointments - Lists all upcoming appointments

        • Key points of this implementation:

          • Custom Tools are integrated via the BookingService class, which provides all static methods referenced in the agent's toolset.

          • Conversation context is persisted by declaring the AgentThread outside the SubmitPrompt method, allowing the thread to maintain state across multiple user interactions.

          • This enables enhanced multi-turn conversations, where the assistant can remember previous exchanges and provide contextually relevant responses.

          • The agent is configured to only answer booking-related questions and can access all available appointment dates at the clinic.

  • Implementing Human in the Loop AI Agents using the Microsoft Agent Framework41:01

    In the Lecture we will

    • Learn that when agents require any user input, for example to approve a function call, this is referred to as a Human-in-the-Loop pattern. An agent run that requires user input, will complete with a response that indicates what input is required from the user, instead of completing with a final answer. The caller of the agent is then responsible for getting the required input from the user, and passing it back to the agent as part of a new agent run.

    • Learn Why Human in the Loop (HITL) Matters

      • Agents can solve a variety of tasks and are great for creating automations. In certain use cases however you may prefer to still have human oversight. Example use cases may include:

        • Healthcare -> where clinical judgement must guide AI recommendations

        • Finance -> where compliance and risk management demand human intervention

        • Processing payments

        • Deleting production data

        • Handling sensitive data

        • Customer Service -> where empathy and context matter as much as speed

      • An AI agent misinterpreting its goal can have disastrous effects in situations like these. This is why it’s important to consider where human in the loop checkpoints should or can be inserted when designing your agentic AI system. Human in the loop checkpoints can occur in real-time whilst someone interacts with your agent. Alternatively, an agent may push a record onto a database table for a human to examine later. Human approval can then be granted or denied, the agents’ thread can be rehydrated from the file system or database, and the agent can resume the task with the appropriate approval status that was captured.

      • Your AI agent handles the tedious work of understanding requests and preparing actions, but humans retain control over key decisions. For regulated industries (finance, healthcare, legal), this pattern can be the difference between “we can’t use AI” and “we’re deploying AI safely in production”.

      • The takeaway: AI doesn't replace people , it empowers them. With frameworks like this, we're moving toward a future where humans and AI co-create value, each playing to their strengths

    • BlazorMAFMultiAgentToolsBookingsExampleHumanInLoopV1

      • This is a simple implementation of Human Intervention capability (using a manual/hardcoded techique). Think of this as a poor mans Human in the Loop Technique. This application serves as a precursor to Human in the Loop Approval via the ApprovalRequiredAIFunction which we cover in Version 2

      • This an updated version from the  Supplementary Demos of Lecture211 (BlazorMAFMultiAgentToolsBookingsExampleEnhanced) 

        Recall/Review ... Coding Overview:

        • The original application demonstrated how to extend an AI agent with 5 custom tools that the agent could call to fetch information like the NEXT available appointment for a Health Clinic. We can interact with the agent using natural language. We tested if the ai agent could automatically map our prompted intent to the relevant function tool and perform the task desired.

        • Conversation context was persisted by declaring the AgentThread outside the SubmitPrompt method, allowing the thread to maintain state across multiple user interactions.

          • This enabled enhanced multi-turn conversations, where the assistant could remember previous exchanges and provide contextually relevant responses.

      • In this first version of Human in the Loop approval we use simple conditionals within the Microsoft Agent Framework . In this Medical clinic scenario we are giving our agent access to 6 tools ... But we insist on Human confirmation before Finalizing the Appointment Booking

        • ListAvailableDates - Returns all available appointment dates for the clinic

        • GetNextAvailableAppointment - Returns the next available appointment date/time for the clinic

        • BookAppointment - Books an appointment for a specified date/time

        • CancelAppointment - Cancels an existing appointment

        • ListBookedAppointments - Lists all upcoming appointments

        • FindAppointmentsByName - Finds all appointments for a given patient name

      • Coding Highlights

        • In the Models folder

          • Appointments.cs class
            // Appointment class representing a booked appointment

            // with Date and PatientName properties

            // Used in BookingService to manage booked appointments

            public class Appointment

            {

                public DateTime Date { get; set; }

                public string PatientName { get; set; } = string.Empty;

            }

          • public class BookingService

            {

                // List of available dates the user can book (NEXT 30 days)

                // Does note include todays date as available for booking (assumed bookings must be made at least a day in advance)

                private static List<DateTime> _available = Enumerable.Range(1, 30)

                    .Select(i => DateTime.Now.AddDays(i).Date)

                    .ToList();


                // This List will hold the Booked appointments stored as objects of type Appointment class

                // containing Date and PatientName properties

                private static List<Appointment> _booked = new List<Appointment>(); //


                //The Description attribute provides a human-readable description of what each method does

                //This helps the AI agent understand the purpose of each method when it is invoked.

                //This is particularly useful in scenarios where the AI agent needs to choose which method to call based on user requests.

                //The Description can also be added to the parameters of each method to further clarify their purpose if desired.


                [Description("Determines the next available date for a booking")]

                public static DateTime GetNextAvailableDate(DateTime startDate)

                {

                    return _available.FirstOrDefault(d => d > startDate);

                }


                [Description("Allows the user to make a booking.")]

                public static string BookAppointment(DateTime date, string patientName)

                {

                    // Ensure we are only dealing with the date part (no time)

                    date = date.Date;


                    // Check if the date is available for booking ie in the _available list

                    // If it is, remove it from available and add to booked

                    if (_available.Contains(date))

                    {

                        _available.Remove(date);

                        _booked.Add(new Appointment { Date = date, PatientName = patientName });                // Add the new appointment to the booked list

                        return "Appointment booked for " + date.ToShortDateString() + " for " + patientName;    // which is an object with Date and PatientName properties

                    }

                    else

                    {

                        return "The selected date is not available";

                    }

                }


                [Description("Cancels a booking.")]

                public static string CancelAppointment(DateTime date, string patientName)

                {

                    //pull the appointment from the booked list based on date and patient name


                    var appointment = _booked.FirstOrDefault(a => a.Date == date && a.PatientName == patientName);

                    //Appointment appointment = _booked.FirstOrDefault(a => a.Date == date && a.PatientName == patientName);

                   

                    if (appointment != null)

                    {

                        _booked.Remove(appointment);            // Remove the appointment from the booked list which is an object of type Appointment

                        _available.Add(date);                   // Add the date back to the available list which is a DateTime objects


                        return "Appointment on " + date.ToShortDateString() + " for " + patientName + " cancelled";

                    }

                    else

                    {

                        return "The selected appointment is not booked";

                    }

                }


                [Description("Lists all booked appointments.")]

                public static string ListBookedAppointments()

                {

                    if (_booked.Count == 0)

                        return "No appointments booked.";


                    //This creates a new collection of strings, joining them together with HTML line breaks

                    //Each representing one appointment with its date and patient name.

                    return string.Join("<br/>", _booked.Select(a => "Date: " + a.Date.ToShortDateString() + ", Name: " + a.PatientName));

                }


                [Description("Lists all available dates for booking.")]

                public static List<DateTime> ListAvailableDates()

                {

                    return _available;

                }


               

                [Description("Finds all appointments for a given patient name.")]

                public static List<Appointment> FindAppointmentsByName(string patientName)

                {

                    return _booked.Where(a => a.PatientName.Equals(patientName, StringComparison.OrdinalIgnoreCase)).ToList();

                    // This returns a list of Appointment objects matching the patient name

                }

            }

        • In the BookingsPage.razor

          • HTML section
            @using Microsoft.Extensions.AI

            @using OpenAI

            @using Microsoft.Agents.AI

            @*User Input Section*@

            <div class="mb-3">

            <label for="userInput">Enter your Appointment Request </label>

            <input @bind="userPrompt" id="userInput" class="form-control" placeholder="Use Natural Language Prompts ... See what happens" style="max-width: 50vw;" />

            <button @onclick="() => SubmitTimePrompt()" class="btn btn-primary mt-3">@buttonText</button>

            <button @onclick="ClearAll" class="btn btn-secondary mt-3">Clear All</button>


            @if (isLoading)

            {

            <img src="/images/running.gif" alt="Loading..." style="width:32px; height:32px; margin-left:10px;" />

            }


            </div>


            @*Human-in-the-loop Confirmation Section*@

            @if (showConfirmation)

            {

                <div class="alert alert-warning" style="max-width: 600px; margin: 1rem auto;">

                    <strong>Confirm Booking:</strong>

                    <p>Do you want to book an appointment as requested?</p>

                    <input @bind="appointmentName" placeholder="Enter your name" class="form-control mb-2" style="max-width: 300px;" />

                    <button class="btn btn-success me-2" @onclick="ConfirmBooking" disabled="@(string.IsNullOrWhiteSpace(appointmentName))">Yes, Book</button>

                    <button class="btn btn-danger" @onclick="CancelBooking">No, Cancel</button>

                </div>

            }


            @*Chat History Section ... here we are display the most RECENT Requests and Responses FIRST

            ... in essence we are reversing the order of the chatHistory list for display purposes.

            *@

            @if (chatHistory.Any())

            {

            <div class="alert alert-info" style="background-color: #f8f9fa;">

            @foreach (var entry in chatHistory.AsEnumerable().Reverse())

            {

            <div style="background-color: #e3f2fd; border-radius: 6px; margin-bottom: 0.5rem; padding: 0.75rem;">

            <strong>@entry.Role:</strong>

            <span>@((MarkupString)entry.Text)</span>

            </div>

            }

            </div>

            }

          • Coding Section
            // Method to submit the user's prompt to the AI agent

            // This method is called when the user clicks the "Send to ChatGPT" button referencing -> SubmitTimePrompt() without parameters

            // When SubmitTimePrompt is called without a parameter value it  Defaults confirmed to false

            // Later when the user confirms the booking we call SubmitTimePrompt(true) with a the parameter value true to indicate confirmation

            private async Task SubmitTimePrompt(bool confirmed = false)

            {

            if (string.IsNullOrWhiteSpace(userPrompt))

            {

            return;

            }


            //This is our poor mans version of Human-in-the-loop confirmation
            // Only show confirmation if not already confirmed and the prompt contains the exact word "book" or "Book"

            // Use word boundaries to avoid matching "bookings" or similar by using regex

            if (!confirmed && System.Text.RegularExpressions.Regex.IsMatch(userPrompt, @"\b[Bb]ook\b"))

            {

            pendingPrompt = userPrompt;         // Store the initial user prompt (request) into pendingPrompt for later use

            showConfirmation = true;            // Show the confirmation dialog

            appointmentName = string.Empty;     // Clear any previous name input ... Patient will enter their name and press Yes Book , which will execute ConfirmBooking

            StateHasChanged();

            return;

            }


            buttonText = "Sending...";

            isLoading = true;


            //agent setup has access to 6 Booking related tools and instructions

            OpenAI.OpenAIClient client = new OpenAIClient(apiKey);

            AIAgent agent = client.GetChatClient(model)

            .CreateAIAgent(

            name: "BookingAgent",

            instructions: "You are an AI Bot that can call 6 Booking Tools related to booking appointments at a Medical Clinic."

            + "When displaying dates do not include the time" + "You can only answer questions related to bookings at the Clinic and have the ability to access all 30 available dates"

            + "If the user enters a possible appointment instead of asking for the next available date, it must include the month, day and year "

            + " and be in the range of acceptable appointment dates",

            tools:

            [

            AIFunctionFactory.Create(BookingService.ListAvailableDates),

            AIFunctionFactory.Create(BookingService.GetNextAvailableDate),

            AIFunctionFactory.Create(BookingService.CancelAppointment),

            AIFunctionFactory.Create(BookingService.BookAppointment),

            AIFunctionFactory.Create(BookingService.ListBookedAppointments),

            AIFunctionFactory.Create(BookingService.FindAppointmentsByName)

            ]

            );


            // Reuse existing thread or create a new one if null ... persistence across interactions

            if (thread == null)

            {

            thread = agent.GetNewThread();

            }


            // Add user message to chat history

            chatHistory.Add(("User", userPrompt));

            // Create user message with HTML formatting request

            ChatMessage message = new ChatMessage(ChatRole.User, userPrompt + " where possible formatted the response nicely in HTML");

            // Get the agent's response asynchronously ... note the use of the thread to maintain context

            AgentRunResponse response = await agent.RunAsync(message, thread);


            // Add assistant message to chat history

            chatHistory.Add(("Assistant", response.Text));


                buttonText = "Send to ChatGPT";

            isLoading = false;

            // Clear the user prompt for the next input

            appointmentName = string.Empty;

                userPrompt = string.Empty;

            }


            // Method to handle booking confirmation

            private async Task ConfirmBooking()

            {

            showConfirmation = false; 

                // Add the  Patient Name to the intial prompt (request for appointment)  so the agent can use it

                userPrompt = pendingPrompt + " for " + appointmentName;


                pendingPrompt = string.Empty;


            // Now submit the updated prompt with confirmation true to book the appointment using the method SubmitTimePrompt

                await SubmitTimePrompt(true);

            }


            private void CancelBooking()

            {

                showConfirmation = false;

                pendingPrompt = string.Empty;

                userPrompt = string.Empty;

                appointmentName = string.Empty;

            }

    • BlazorMAFMultiAgentToolsBookingsExampleHumanInLoopV2/updated

      • In this updated version we wrap the AIFunction instance of the BookAppointment function in an ApprovalRequiredAIFunction instance instead of using a hard-coded manual conditional statement to intercept Human intervention.

      • Coding Highlights

        • Note the Coding Section Declarations
          // Stores the function approval request content

          #pragma warning disable MEAI001

          FunctionApprovalRequestContent requestContent;

          #pragma warning restore MEAI001


          // Flag to indicate if we are awaiting user approval for a function call

          private bool requestFlag = false;

        • private async Task SubmitTimePrompt()

          #pragma warning disable MEAI001

          //When using functions, it's possible to indicate for each function, whether it requires human approval before being executed.

          // This is done by wrapping the AIFunction instance in an ApprovalRequiredAIFunction instance.
          AIFunction bookingFunction = AIFunctionFactory.Create(BookingService.BookAppointment);

          AIFunction approvalRequired = new ApprovalRequiredAIFunction(bookingFunction);

        • When declaring our agent we indicate all our tools including the approvalRequired tool which will require human approval before execution.
          AIAgent agent = client.GetChatClient(model)

          .CreateAIAgent(

          name: "BookingAgent",

          instructions: "You are an AI Bot that can call 6 Booking Tools related to booking appointments at a Medical Clinic."

          + "When displaying dates do not include the time" + "You can only answer questions related to bookings at the Clinic and have the ability to access all 30 available dates"

          + "If the user enters a possible appointment instead of asking for the next available date, it must include the month, day and year "

          + " and be in the range of acceptable appointment dates"

          + "Since all bookings require approval, if the user does not approve the booking , respond in a friendly manner and ask if there is any other date they may wish to book ",

          tools:

          [

          AIFunctionFactory.Create(BookingService.ListAvailableDates),

          AIFunctionFactory.Create(BookingService.GetNextAvailableDate),

          AIFunctionFactory.Create(BookingService.CancelAppointment),

          //AIFunctionFactory.Create(BookingService.BookAppointment),

          approvalRequired,

          AIFunctionFactory.Create(BookingService.ListBookedAppointments),

          AIFunctionFactory.Create(BookingService.FindAppointmentsByName)

          ]

          );

        • If we need approval to proceed then we create the approval message. First time through requestFlag is false so we go to the else block and let the agent process the user prompt as usual .If the agent determines that it needs to call a function that requires approval it will return a FunctionApprovalRequestContent (which we have stored in requestContent)
          if (requestFlag)

          {

          //Once the user has provided their input, you can reference the FunctionApprovalResponseContent instance (requestContent)  using the CreateResponse method

          // on the FunctionApprovalRequestContent (requestContent) . Pass true to approve the function call, or false to reject it.

          //The response content can then be passed to the agent in a new User ChatMessage,

          // along with the same thread object to get the result back from the agent.

          // Simple check for negative responses

          var negativeResponses = new[] { "no", "stop", "cancel", "reject", "deny" };

          bool isApproved = !negativeResponses.Any(x => userPrompt.Trim().ToLower().Contains(x));


          var approvalMessage = new ChatMessage(ChatRole.User, [requestContent.CreateResponse(isApproved)] );

          response = await agent.RunAsync(approvalMessage, thread);

          requestFlag = false;

          }

          else

          {

          // Create user message with HTML formatting request

          ChatMessage message = new ChatMessage(ChatRole.User, userPrompt + " where possible formatted the response nicely in HTML");


          // Get the agent's response asynchronously ... note the use of the thread to maintain context

          response = await agent.RunAsync(message, thread);

          }

        • Since we now have a function that requires approval, the agent might respond with a request for approval,  instead of executing the function directly and returning the result.  We must check the response content for any FunctionApprovalRequestContent instances,  which indicates that the agent requires user approval for a function.

          Extract function approval requests from the response ... if any are present we will handle them separately
          var functionApprovalRequests = response.Messages

          .SelectMany(x => x.Contents)

          .OfType<FunctionApprovalRequestContent>()

          .ToList();

        • Check if there are any function approval requests

          If there are any function approval requests, the detail of the function call including name and arguments can be found in the FunctionCall  property on the FunctionApprovalRequestContent instance.

          This can be shown to the user, so that they can decide whether to approve or reject the function call.
          if (functionApprovalRequests.Any())

          {

          // For simplicity, we will just take the first approval request

          //var requestContent = functionApprovalRequests.First();

          //FunctionApprovalRequestContent requestContent;


          requestContent = functionApprovalRequests.First();


          // Add assistant message to chat history indicating that approval is required for the function call

          // ... specifically the BookAppointment function in this example (FunctionCall.Name)

          chatHistory.Add(("Assistant", "We require approval to execute " + requestContent.FunctionCall.Name));


          // Set the request flag to true to indicate that we are awaiting user approval

          // which we will handle on the next SubmitTimePrompt() call with our if block (if requestFlag)

          requestFlag = true;


          // Update UI state reset loading indicators and button text

          buttonText = "Send to ChatGPT";

          isLoading = false;

          userPrompt = string.Empty;


          // Exit early to wait for user confirmation

          return;

          }

        • // Add assistant message to chat history

          chatHistory.Add(("Assistant", response.Text));


          buttonText = "Send to ChatGPT";

          isLoading = false;

          // Clear the user prompt for the next input

          userPrompt = string.Empty;


          #pragma warning restore MEAI001

    • Suggested Exercises

      • Take a Sequential Workflow application and interject a Human in the Loop approval using either manual conditional  technique or the more formal ApprovalRequiredAIFunction instance

        • BlazorMAFWorkflowSequential (Lecture 212) -> BlazorMAFWorkflowSequentialHumanInLoop

      • Pick a MAF application covered in one of  the recent Lectures 208-213 and implement Human In the Loop approval where appropriate 

    • Supplementary Demo

      • BlazorMAF-ToolsBookingsExampleHIL-SpeechText

        • This demo updates the HIL Booking application of this Lecture by implementing Real-Time Speech to Text capabilities.

        • The user can ask for a booking verbally . The speech will be converted into text, which can be edited if desired ,  and then sent off to ChatGPT

        • There are many options for speech-to-text:
          Cloud APIs (Azure Cognitive Services, Google, etc.). Native libraries wrapped with gRPC or REST. Browser Web Speech API.
          For a Blazor app that runs in the browser, the Web Speech API is the easiest place to start:
          No server round trips for audio. No external billing. Works directly in the user’s browser. The downside: not all browsers support it (mainly Chromium based ones do: Chrome, Edge, some versions of Opera). You will add a simple IsSupported check and show a friendly message when support is missing.

          The solution is built using:

          • A small JavaScript helper speech-to-text.js that wraps the Web Speech API for browser-based voice input.

          • Real-time transcription with interim and final results.

          • Error handling and browser support checks.

          • A reusable Blazor component (SpeechToText) to drive speech recognition and integrate with the form on the page

          • A simple binding pattern (bind-Value) to plug speech-to-text into any form field.

          User Benefits:
          This feature makes your app feel more modern and friendly, especially for long forms. Users can dictate feedback or other text, making data entry faster and more accessible.

          Technical Highlights:

          • Works entirely in the browser—no server round-trips for speech recognition.

          • Easy to reuse: drop the SpeechToText component into any form.

          • Real-time interim and final transcript display.

          • Status line: listening / idle / error message.

          • Clear integration pattern for parent-child communication (e.g., clearing form fields).

          • All the magic will happen in JavaScript, but Blazor will control it via JS interop, and you will get typed callbacks on the .NET side.


          Try it out: Enter appointment request. You can type or use your voice. Click Clear All to reset the form and start again!

  • Blazor and AI ... What We Know So Far ... Part 11:03:56

    In this Lecture we will

    • Review and Extend our Blazor - AI Integration skills with a particular focus on the Microsoft Agent Framework implementation. The .NET ecosystem finally has a unified, production-grade framework for building AI agents.

      • If you’ve been building .NET applications and watching the AI agent hype from the sidelines, wondering when it would become practical to integrate into your stack — that moment is now.

      • With .NET 10 (LTS), Microsoft shipped the Microsoft Agent Framework, a unified SDK that merges the best of Semantic Kernel and AutoGen into a single, enterprise-grade platform for building AI agents. It’s open source, MIT licensed, and designed for the same developers who build Web APIs, background services, and cloud-native apps every day.

      • What Is an AI Agent, Really?

        • Receives a goal (from a user, a scheduler, or another agent)

        • Reasons about it using an LLM

        • Takes actions by calling tools (your code, APIs, databases)

        • Evaluates results and decides what to do next

        • The key difference from a simple chatbot is autonomy. A chatbot responds. An agent acts. It can decide which tools to call, in what order, and whether the results satisfy the original goal — all without you writing explicit if/else logic for every scenario.

        • In the Microsoft Agent Framework, agents are built on Microsoft.Extensions.AI, which provides a unified abstraction (IChatClient) across OpenAI, Azure OpenAI, Ollama, and other providers. Your agent code doesn't change when you swap models.

    • Work through a comprehensive sample project BlzHybMAF. This is a .NET MAUI Blazor Hybrid Application which will serve to highlight a number of key concepts involved with AI Integration and  demonstrate a variety of interesting and practical applications . This will include

      • Windows Specific ... Multimedia and more Applications

        • MAF Intro with Local LM (OllamaMAF.razor)

          • This application implements a simple AI Conversation Agent that takes user input and sends it to the model running in Ollama and then returns the response back to the user.

          • Remember you need to have Ollama running locally and the model downloaded and available for this to work. In this example we are using the llama3.2:latest model from Ollama.

        • Simple Structured Response to Text based Query using Microsoft Agent Framework (SimpleStructured.razor)

          • This page demonstrates how to interact with an AI model to retrieve Structured and Unstructured responses. By defining a specific format using the Movie class, we can structure the AI's response to return a list of movies with details like Title, Release Year, and Rating, which are displayed in a table.

          • Alternatively, an unstructured response may return varying data formats, in one instance Title and Rating , and in another instance Title and Date .

          • So if you want a Consistent response a Structured technique needs to be implemented.

          • Key Coding Highlights

            • //Unstructured Response

              //----------------------------------------------------------

              //AIAgent agent1 = client.GetChatClient(model).CreateAIAgent(instructions: "You are an expert in The Movie Datatbase Lists");

              //AgentRunResponse response1 = await agent1.RunAsync(userPrompt);

              //responseText = response1.Text;

              //Structured Response

              //----------------------------------------------------------

              ChatClientAgent agent2 = client.GetChatClient(model).CreateAIAgent(instructions: "You are an expert in The Movie Datatbase Lists. Please use the User Score as the indicator for Rating ");


              //Note the difference between AIAgent and ChatClientAgent:

              //--------------------------------------------------------

              //AIAgent is typically used for unstructured responses, where you just want free-form text from the AI (like a chat or general answer).

              //ChatClientAgent is a base class that provides more advanced features, including the ability to specify

              //and extract structured responses(like a list of Movie objects).


              //Define the structure of the response expected and pass it to the agent. Here we expect a list of Movie objects.       

              AgentRunResponse<List<Movie>> response2 = await agent2.RunAsync<List<Movie>>(userPrompt);

                     

              //Extract the structured response with Result property instead of Text property and store it in the movies variable which is of type List<Movie>.

              //Using Result property allows us to directly get the list of Movie objects as defined by our structure,

              //rather than just a text response that we would need to parse manually.

              movies = response2.Result;

        • Agent Page with Text, Image, and PDF Inputs ... Updated with Structured Output (AgentPage.razor)

          • This page allows users to interact with an AI agent by providing inputs in the form of text, images, or PDF files. Users can choose between two options:

            • Hardcoded Options: Select from a predefined image, a PDF , or enter your own Text query. We use a dropdown list to specify the type of hardcoded input the user is making (Text, Image, or PDF) to query the AI. A Hard coded image and pdf are stored in the wwwroot images folder and we also download and use an image via a URI (Uniform Resource Identifier) that points to a Web Page

            • Note ... for Text queries we implemented a simple intent recognition step (Agent) where we determine the user's intent based on their query (using gpt-4o-mini)  and then route it to one of three specific agent (using gpt-4.1)  with instructions tailored to that intent. In this demo we have three intents (MusicQuestion, MovieQuestion, Other) and three related agents (MusicNerd, MovieNerd, OtherNerd) that will answer the user's query based on the determined intent. Think of this as a manual version of the Agent Workflow called Handoffs. See HandoffPage.razor for the more formal implementation of Agent Handoffs .

            • Custom Image Upload: Use the .NET MAUI File Picker to upload your own image file. The dropdown list is not used in this case.We initially implemented the InputFile Component to do our file picking but experienced frequent issues in this Blazor Hybrid App so we switched to the more reliable .NET MAUI File Picker (located in the Service folder in the PickAPhotoAsync method of the FileService class)

            Besides the regular images located in the wwwroot folder, check out 2 specific cases which are used in obtaining Structured results.

            • handform.png and simpleform are connected to the FormResult class

            • table.png is connected to the Table class ... and is actually a List of type Table and will be displayed using the QuickGrid component for better formatting and functionality (sorting, pagination, etc.)

            • Note:For the unstructured images (not charts or tables) we implement a thread to maintain context across multiple interactions with the agent. This allows the agent to "remember" previous exchanges and provide more coherent responses in a conversational format.
              //Standard unstructured response with new addtion of thread

              var response1 = await agent1.RunAsync(new ChatMessage(ChatRole.User,

              [

                  new TextContent(userPrompt),

              new DataContent(dataUriImg, "image/jpeg")               

              ]),thread);

          • Key Coding Highlights

            • case Scenario.Pdf:

                   //---------------------------------------------------------------------------------

                   string path = Path.Combine("wwwroot", "images", "lecture1.pdf");

                   //---------------------------------------------------------------------------------

                   //PDF as Base64

                   string base64Pdf = Convert.ToBase64String(File.ReadAllBytes(path));     //Read in all the bytes for the designated path containing the pdf and Convert to Base64

                   string dataUri = "data:application/pdf;base64," + base64Pdf;            //Create Data URI for PDF

                  response = await agent.RunAsync(new ChatMessage(ChatRole.User,          //Remember response is of object type AgentRunResponse

                   [

                       new TextContent(userPrompt),

                       new DataContent(dataUri, "application/pdf")

                   ]));


                   responseText += "Scenario PDF as Base64 " + "<hr/>"  + response.Text + "<br/>";

            • case Scenario.Image:

                   //Image via URI ... from Web Page

                   response = await agent.RunAsync(new ChatMessage(ChatRole.User,

                   [

                       new TextContent(userPrompt),

                       new UriContent("", "image/jpeg")

                   ]));

                   //Set flag to show image from URI on HTML section

                   imageUri = "";

                   isImageURI = true;


                   responseText += "Scenario Image via URI" + "<hr/>"  +response.Text + "<br/>" + "<br/>";

            • //Local File

              string pathImg = Path.Combine("wwwroot", "images", "beach.jpg");

              //Image via Base64

              string base64Img = Convert.ToBase64String(File.ReadAllBytes(pathImg));  //Read in all the bytes for the designated path containing the image and Convert to Base64

              dataUriImg = "data:image/jpeg;base64," + base64Img;                     //Create Data URI for Image

              response = await agent.RunAsync(new ChatMessage(ChatRole.User,

              [

                   new TextContent(userPrompt),

                   new DataContent(dataUriImg, "image/jpeg")                           //Use the Data URI for the image

              ]));

        • Images Only ... with Structured Output + Voice to Text (AgentPageImages.razor)

          • This page allows users to interact with an AI agent by providing input in the form of images files.Here we strictly use the .NET MAUI File Picker. We also leverage a Speech to Text component to allow users to speak their prompts instead of typing them. We also allow the user to press Enter to submit the prompt after speaking it instead of clicking the Send button. The AI agent will analyze the image and provide a response based on the user's prompt and the content of the image.

          • Besides the regular images located in the wwwroot folder, check out 3 specific cases which are used in obtaining Structured results.

            • handform.png and simpleform are connected to the FormResult class

            • table.png is connected to the Table class ... and is actually a List of type Table and will be displayed using the QuickGrid component for better formatting and functionality (sorting, pagination, etc.)

            • bill.png is connected to the Receipt (a list of LineItem's) and the LineItem class

          • Key Coding Highlights

            • @*Speech to Text Section*@

              <div class="mb-3">

                  <label class="form-label"><b>Say (Speak don't Type) </b>your Image Query ... Load image FIRST (Windows Only)</label>

                  <SpeechToText @ref="speechToTextRef" @bind-Value="Model.Feedback" OnClear="HandleClear" />   

              </div>

            • <div class="row">

                  <div class="col-md-12 mb-3">

                      <label for="userInput"><b>Final Query Request :</b> Can be Edited Manually after Speech to Text processing  ... Press Enter to Execute or Click Send Button below   </label>

                      <input @bind-value="Model.Feedback" @bind-value:event="oninput" id="userInput" class="form-control" placeholder="Identify items in the image" @onkeypress="HandleKeyPress" />

                  </div>   

              </div>

            • private async Task HandleKeyPress(KeyboardEventArgs e)

              {

                  if (e.Key is not "Enter") return;

                  await SubmitFilePrompt();

              }

            • public class LineItem

              {

                  public string? Name { get; set; }= string.Empty;

                  public string? Quantity { get; set; }

                  public string? UnitPrice { get; set; }

                  public string? TotalPrice { get; set; }

              }

            • public class Receipt

              {

                  public List<LineItem> Items { get; set; } = new List<LineItem>();

                  public string? Subtotal { get; set; }   

              }

            • //Structured response into Receipt class that is defined in the Models folder

              //So response4.Result will contain the structured data of the Receipt class properties

              //eg. reponse4.Result.Items, response4.Result.Subtotal

              AgentRunResponse<Receipt> response4 = await agent2.RunAsync<Receipt>(new ChatMessage(ChatRole.User,

              [

                  new TextContent(userPrompt),

                  new DataContent(dataUriImg, "image/jpeg")

              ]));

              //Store the structured response in the receiptResult object (instance of Receipt class)

              receiptResult = response4.Result;

              //Initialize the receiptResponseText variable to null before appending the receipt details

              receiptResponseText = null;

              //Check if we have valid data in the receiptResult and that it contains at least one item in the Items list

              if (receiptResult != null && receiptResult.Items.Count > 0)

              {

              //Initialize the receiptResponseText variable to an empty string before appending the receipt details

                  receiptResponseText = "<b>Receipt Results using Receipt class</b>" + "<br/>";

                         

              //Loop through each item in the receiptResult.Items list and append the details to the receiptResponseText variable

                  foreach (var item in receiptResult.Items)

                  {

                      receiptResponseText += "<br/><b>Item Name:</b> " + item.Name + " | <b>Quantity:</b> " + item.Quantity + " | <b>Unit Price:</b> " + item.UnitPrice + " | <b>Total Price:</b> " + item.TotalPrice;

                  }

              //After the loop we append the subtotal to the receiptResponseText variable

                  receiptResponseText += "<br/><hr/><b>Subtotal:</b> " + receiptResult.Subtotal;

              }

      • Images + WebCam (AgentPageCamera.razor)

        • This page allows users to interact with an AI agent by providing input in the form of images files.

          This updated application adds the ability to Capture Images via a Webcam and perform Image Analysis. You can also Load and Save images to and from the local file system (Windows) using the .NET MAUI File Picker.

        • Key Coding Highlights

          • private async Task TakePhoto()

          • public  class FileService

            {
            public async Task<FileResult> TakePhotoAsync()

            {
            #elif IOS || MACCATALYST || WINDOWS

                        if (!MediaPicker.Default.IsCaptureSupported)

                            throw new NotSupportedException("Camera capture is not supported on this device.");

                        var photo = await MediaPicker.Default.CapturePhotoAsync();                             
                        // This will prompt the user to take a photo using the camera

                        return photo;

               }

          • private async Task SavePhoto()

          • Implemetation of webCamState ... in MainLayout.razor

            • @inherits LayoutComponentBase

              @inject WebCamState webCamState

                          @if (OperatingSystem.IsWindows() && webCamState.IsCameraAvailable)

                          {

                              <span class="badge bg-warning text-dark ms-2">Webcam Available</span>

                          }

                          else if (OperatingSystem.IsWindows())

                          {

                              <span class="badge bg-warning text-dark ms-2">Webcam Not Available</span>

              }           

            • @code {

                  //This ensures that the page is re-rendered when the state of the webCamState service changes.

                  //We want this immediate update since the Menu and Top Bar from the MainLayout is always visible to the user.

                  protected override void OnInitialized()

                  {

                      webCamState.OnChange += StateHasChanged;

                  }

                  public void Dispose()

                  {

                      webCamState.OnChange -= StateHasChanged;

                  }

      • Images + WebCam + QR Codes (Reading/Creating)  (AgentPageQR.razor)

        • This application demostrates how to CREATE and READ QR Codes + Live Cam Reading of QR codes.

          • We use a service/class called QrCodeService (Services folder) to generate QR codes from text input and display them as images.This service uses the QRCoder NuGet Package

          • We also implement the ability to read QR codes from images using JavaScript interop. If the decoded QR code is a link we give the user the  ability to click the link and view the site in a Browser. See the script called QRscript and the reference to jsQR a JavaScript library used for decoding QR codes from image data

          • ... as usual make note of the declarations for QRCoder in MauiProgam.cs and QRscript and jsQR in the index.html file

          • The Live Cam Reading uses the qr-scanner javascript library that adds the ability to use the Webcam to instantly capture any QR code placed in front of it and read/decode it in real-time.

          • This js library uses the files qr-scanner.umd.min.js and qr-scanner-worker.min.js with the qrHelper.js kickstarting the process

        • Key Coding Highlights

          • Note the use of AutoComplete in the input of Text to QR option

            • public class SuggestionsApi

              {   

                  private const StringComparison _comparer = StringComparison.InvariantCultureIgnoreCase;


                  //DataSeed is a class that contains a list of names. It is used to provide the data for the suggestions.

                  //Here we make an instance of the DataSeed class to access the list of names. The MoreNames property of the DataSeed class contains a large list

                  private readonly DataSeed _data = new DataSeed();

                  public async Task<IList<string>> FindAsync(string phrase)

                  {

                      //fake delay

                      await Task.Delay(200);

                      //Change Names to MoreNames to access the large list of male names (approx 2700)

                      return _data

                          .MoreNames

                          .Where(x => x.StartsWith(phrase, _comparer))

                          .Take(7)

                          .ToList();

                  }

            • public class DataSeed

              {

                  //This large list is from

                  //If you want to connect to an api list of names, you can use this list

                  public readonly string[] MoreNames = new string[]

                  {

                      "Aaran", "Aaren", "Aarez", "Aarman", "Aaron", "Aaron-James", "Aarron", "Aaryan", "Aaryn", "Aayan", "Aazaan", "Abaan",

            • In AgentPageQR.razor

              • HTML section  ... @inject SuggestionsApi Sugs

                • @*

                      The @bind:after directive in Blazor is used to specify a callback that runs after a value is updated via data binding. It is available in .NET 8 and later.

                      How it works:

                      • When the bound value changes (e.g., user types in an input), Blazor updates the property.

                      • After the update, the method specified in @bind:after is called.

                  *@

                  <input id="fileNameInput2" class="form-control" style="max-width: 190px; height: 28px; font-size:0.70em; padding:2px 6px;"

                          @bind="QRtext" placeholder="Enter Text to QR ... Names Auto Complete"

                          @bind:event="oninput"

                          @bind:after="AutocompleteAsync"/>   

                • @*Auto Complete Implementation*@

                  @if (Suggestions.Any())

                  {

                      <h5 class="mt-2">Do you mean? ... </h5>

                      <ul>

                          @foreach (var suggestion in Suggestions)

                          {

                              <li @onclick="@(() => SelectSuggestion(suggestion))" style="cursor: pointer;">@suggestion</li>

                          }

                      </ul>

                  }        

              • Code Section

                • private IList<string> Suggestions { get; set; } = new List<string>();

                • private async Task AutocompleteAsync()

                  {

                      Suggestions = string.IsNullOrWhiteSpace(QRtext) ? new List<string>() : await Sugs.FindAsync(QRtext);

                      await InvokeAsync(StateHasChanged);

                  }

                  private void SelectSuggestion(string suggestion)

                  {

                      QRtext = suggestion;

                      Suggestions.Clear();

                  }

      • Video AI Analysis Applications (AgentPageVideo.razor)

        • This page allows you to access a Webcam and Record , Save, and Load Videos. You can take single frame image from the video and  Analyze it or even a series of video frames and Analyze them all collectively

        • We also add the ability to compare images for Similarity (a numerical representation of how alike two images are). Specifically we compare the first and last frames of any selected video using two techniques.

        • There are no video AI analysis buttons on screen (except for the basic "Analyze First Frame" button)

          • Once a user saves and then re-loads a video , 10 evenly spaced frames of the video appear and anyone of them can be clicked on for Analysis, additionally a new button appears labeled "Analyze All Captured Frames" which can be clicked.

        • *** UPDATE *** we have now also added a button to perform Motion Detection on the selected video frames (visible once video is selected and loaded on screen)

        • This implementation does not use a Service but rather directly connects to the webcam using the OpenCvSharp library (OpenCvSharp4.Windows NuGet Package) with all the code on this page.

        • We can apply this technology to real-world problems like insurance claim analysis. Automate report generation for vehicle damage videos, detailing the car model, year, damage specifics, and incident descriptions. This demonstrates AI’s transformative potential.

        • Key Coding Highlights

          • private void ReadVideoFile(string file)

          • Web Camera Implementation using OpenCvSharp

            private async Task StartCamera()

          • private async Task RecognizeFrameImages(List<string> frameImgs)

            {
            //Note: See the use of the spread operator (..) to pass the list of images as individual DataContent items in the content list for the ChatMessage

            //This is a new feature in C# 11 that allows us to easily pass a list of items as individual parameters to a method or constructor

            //Here we are passing the user prompt as a TextContent item and then using the spread operator to pass each image in the frameImgs list as a separate DataContent item

            //If you want a less slick approach look at the technique used in the MotionDetection method where we manually build

            // a List<AIContent> and add each item to the list including the images

            AgentRunResponse response1 = null; // Declare outside try

            try

            {

                response1 = await agent1.RunAsync(new ChatMessage(ChatRole.User,

                [

                    new TextContent(userPrompt),

                    ..frameImgs.Select(img => new DataContent(img, "image/jpeg"))

                ]));

            }

          • private async Task MotionDetection(List<string> frameImgs)

            {
            // Build the content list manually creating a List<AIContent> where AIContent comes Microsoft.Extensions.AI

            //This is an alternative to using the spread operator (..) technique shown in the RecognizeFrameImages method       

            var contentList = new List<AIContent>();

            contentList.Add(new TextContent(userPrompt));             

            //Remember here we are sending part of a LIST of images (frames) to the AI service not a single image

            //Add only every THIRD image to the content list to reduce the number of images sent to the AI service

            for (int i = 0; i < frameImgs.Count; i += 3)

            {

                contentList.Add(new DataContent(frameImgs[i], "image/jpeg"));

            }       

            // Pass the content list to the agent , our second response is structured into the SecurityResult class defined in the Models folder       

            AgentRunResponse response1 = null;

            try

            {

                response1 = await MoDetectAgent.RunAsync(new ChatMessage(ChatRole.User, contentList));

            }       

            AgentRunResponse<SecurityResult> response2 = null;       

            try

            {

                response2 = await MoDetectAgent.RunAsync<SecurityResult>(new ChatMessage(ChatRole.User, contentList));

            }

            //if AlertStatus is "High" and Confidence is 9 or greater, we will display a special warning message in the last section of the responseMessage

            if (response2.Result.AlertStatus == "High" && response2.Result.Confidence >= 9)

            {

                warningMessage = "<br/><b>Warning: High security risk detected </b><br/>";

            }

      • Sound Recording , Playback , AI Transcribing and Translation ... and More !  (AgentPageAudio.razor)

        • This page allows you to Record Audio (using your device's microphone), Play Back Recordings,Save them as MP3 files, and Load existing audio files. You can also select and play MP3 files from the AppDirectory audio folder.

          • Audio recording and playback is powered by the audioRecorder.js JavaScript module.

        • You can also select and play MP3 files from the AppDirectory audio folder.

          • We Load audio files via the device's media picker (File Picker).

          • MP3 files in File System AppDirectory /audio are listed for quick access.

        • We also implements some AI Integration by Converting Audio to Text (Transcribing), and Converting Foreign (Transcribed) Audio to English Text (Translating) using the OpenAI's Whisper-1 model (See endpoints in ConvertAudioToText Task located on this page ).

          • For our (Audio to Text and Text to Audio) AI Implementations we are using the following endpoints

        • There is also an option to Play back Text to Audio in a number of voices (See OpenAIServices ... ConvertTextToSpeechAsync Task ... text-to-speech api model called tts-1)

        • We also implement a simple AI Analyze Audio button (actually works with the Transcribe/Translated Audio to English Text) which attempts to return a structured reponse indicating Morning,Afternoon and Evening Tasks

          • Make note of how we pull out specific information according to some predefined properties we declared in a class called MyTasks. We are basically taking unstructured voice input and producing structured data

        • ... And finally the More!

          • We have added a Real Time Speech to Text component (show/hide) to demonstrate instant voice to text transcribing without AI help ... but instead using some Javascript

          • We have used this in other Page components previously ... but it's here also since we are focused in this page with Audio

          • In this particular implementation we use it to help us Translate ANY Transcribed text to ANY desired Language via AI. Once the Clicks on the Convert to Any Language we make the Speech to Text component appear with the Transcribed text already filled in. If you wish you can Voice input more to be added.

          • Then you select your desired Language to convert to and let the Agent take over

        • Key Coding Highlights

          • // This method is called when an MP3 file is selected from the File System App Directory

            // It reads the file from the app's audio directory, converts it to a base64 data URL, and sets it as the source for the audio player.

            // mUrl is set to the base64 data URL of the selected MP3 file, which is then used as the source (src) for the audio player.

              private async Task SelectSysMp3(string file) 

          • private async Task ConvertAudioToText(int type)

      • Speech to Text (Non-JS) (AgentSpeechText.razor)

        • This page provides a seamless Speech-to-Text experience using OpenAI's Whisper-1 model. Users can record their speech, which is transcribed and sent to an AI agent for intelligent query responses. Key features include:

          • Speech-to-text conversion powered by Whisper-1 WITHOUT the use of Javascript (ie speech-to-text.js) as was done in previous options in this application

          • Note for true Real-Time transcription (without using Javascript), you would need to use a different service like Azure Speech Services which designed for streaming audio and providing real-time transcription results.

          • In this application we capture all the audio before displaying the full transcription, when done the user clicks on a button to stop listening, and then this transcription (user query) is automatically sent to the AI for a response

          • Note: In the ContinueProcess method we implement a thread to maintain context across interactions with the agent, so that the agent can remember previous conversations and provide more coherent responses.

          • In this application we reference the OpenAI.Audio namespace which is used to interact with the AudioClient class and its methods such as TranscribeAudioAsync. These are critical for the speech-to-text functionality implemented in the SpeechTextMAF and ContinueProcess methods

          • For the actual audio capture from the users microphone we reference the NAudio Nuget Package

          • ... We have added a Read It Back (AI answer) and Language Convert (AI answer) functionality. Here we are borrowing from the AgentPageAudio.razor page methods. See the ReadItBack method (references OpenAIServices.ConvertTextToSpeechAsync) and the SpeechTextLangMAF method (uses a Language translation agent) for details.

          • Key Coding Highlights

            • focus on private async Task ContinueProcess()

      • AI Generated Content ... NBA Team Histories (AgentNBA.razor)

        • This application, designed to showcase the power of AI-generated content. This .NET App Generates Content for itself via AI. In this particular implementation we are generating detailed histories for NBA teams, providing unique and insightful content. The application uses a JSON file to store the initial NBA team data, structured according to the GameItem.cs model. We also use an SQLite database to store the same data, demonstrating dual data storage and concurrent updates.

        • AI-Powered Summaries: The application uses advanced AI to generate detailed histories for NBA teams, providing unique and insightful content.

        • JSON Data Storage: All initial NBA team data is stored in a JSON file, structured according to the GameItem.cs model. This ensures data consistency and easy updates.

        • Service Architecture:

          • Service folder contains the IOpenAIServiceNBA interface and its implementation OpenAIServiceNBA, which handles communication with the AI backend. We are using the Microsoft Agent Framework along with the OpenAI nuget package for this purpose. This service has a method which takes an item name (NBA Team Name) as input and generates a description by sending a prompt to the OpenAI API.

          • GameItemService : This service is responsible for managing the NBA team data, which is stored in a JSON file. It provides methods to retrieve all teams, get a specific team by ID, and update a team's description. The service loads the data from the JSON file into memory when it is instantiated and saves any changes back to the file when updates are made.

          • The Service folder also contains the INBAsqlService interface and its implementation NBAsqlService This service provides the actual logic for interacting with a SQLite database to manage NBA team data. It uses the SQLiteAsyncConnection class to perform asynchronous database operations such as creating tables, inserting, updating, deleting, and querying data.

        • Main Page: The GameItems.razor page displays all NBA teams and allows users to request an AI-generated history for any team.

        • Team History Modal: The generated history is shown in a non-routable component called ItemsDescriptionModal, which receives parameters to display the selected team's details.

        • Dual Data Storage: The application now uses both a SQLite database and a JSON file to store NBA team data. This ensures data persistence and flexibility across platforms.

        • Concurrent Updates: When a new team is added or updated, changes are reflected in both the SQLite database and the JSON file, keeping both sources in sync.

        • Data Consistency: Deleting or editing a team updates both storage systems, preventing mismatches and ensuring the UI always displays the latest information.

        • Platform Compatibility: SQLite provides robust local storage for .NET MAUI apps, while JSON offers easy integration and backup for web and desktop scenarios.

        • Seamless User Experience: All operations (add, edit, delete, generate history) are handled transparently, so users interact with a unified list regardless of the underlying storage.

        • Key Coding Highlights

          • Focus on GameItemService.cs

          • Focus on NBAsqlService.cs

      • Web Scraping AI ... Implementing HTML Agility Pack and MAF Page Capabilities: (AgentWebScraping.razor)

        • HTML Agility Pack Integration: Utilizes the HTML Agility Pack NuGet package for robust HTML parsing and web scraping functionality

        • HtmlScraperService Implementation: Leverages a custom service (HtmlScraperService) to efficiently load and parse HTML content from target URLs

          • public  class HtmlScraperService

            {
            ...

            //This method is an alternative implementation of LoadHtmlFromUrlAsync that uses HtmlWeb to load the HTML directly from the URL.

            //HtmlWeb is a class provided by the HtmlAgilityPack library that simplifies the process of loading HTML content from a URL.

            //It seems to have better success rate and avoids some site blocking issues that can arise when using HttpClient, as it mimics a web browser more closely.

            //Note here that we are using Task.Run to run the loading process on a separate thread,

            //which can help to avoid blocking the main thread of the application, especially if the loading process takes some time.

            //----------------------------------------------------------------------

            //We call this task from the AgentWebScraping.razor component in the ServiceScrapeAndProcess() method

            //var ScraperResponse = await htmlScraperService.LoadHtmlFromUrlAsync(targetUrl);

            public async Task<HtmlDocument> LoadHtmlFromUrlAsync(string url)

            {

                return await Task.Run(() =>

                {

                    var htmlWeb = new HtmlWeb();

                    return htmlWeb.Load(url);

                });

            }

        • Web Scraping: Scrapes a hardcoded URL  with the ability to modify the target address before scraping

          • protected async Task ServiceScrapeAndProcess()
            var ScraperResponse = await htmlScraperService.LoadHtmlFromUrlAsync(targetUrl);
            var rawHtml = ScraperResponse.ParsedText;

            var sanitizedHtml = SanitizeHtml(rawHtml);

            extractedData = sanitizedHtml;

        • Full Webpage AI Analysis: Analyzes the entire scraped webpage content using Microsoft Agent Framework and OpenAI's GPT-4o-mini model to provide insights about the site's purpose and content ... See ScrapeAndProcessMAF

        • Image Extraction & Analysis: Automatically extracts image URLs from the scraped content and provides AI-powered analysis of the first discovered image ... Task

        • Visual Feedback: Displays scraped data, extracted images, and AI-generated responses with HTML formatting support

        • Also note the use of Swipe Gestures which references the javascript files ... script.js and Hammer.min.js and the css file swipe.razor.css

          • protected override async Task OnAfterRenderAsync(bool firstRender)

        • Note the display of Token trackings (input and output)

          • responseMessage += "<br/><i>Input Tokens used: " + response1.Usage.InputTokenCount + "</i>";

            responseMessage += "<br/><i>Output Tokens used: " + response1.Usage.OutputTokenCount + "</i>";

      • Excel to Blazor with AI-Powered Analytics using MAF Page Capabilities: (AgentExcel.razor)

        • Transform your Excel data into interactive web tables with the power of AI-driven natural language queries including the option for Real Time Speech to Text querying.

        • This application seamlessly imports Excel (.xlsx) files into a Blazor WebAssembly application, displaying data in a responsive, virtualized table format with advanced AI filtering capabilities.

          • Note the use of the DocumentFormat.OpenXml NuGet Package

          • Note we use LINQ expression generation ... NOT an SQL Query since we are working with an Excel file NOT a DB

        • This updated version now allows a user to transfer the Excel file to an SQLite DB. Once transferred the user can now perform CRUD operations on the orginal data

        • Key Coding Highlights

          • Medalist.cs ... this class represents the Excel data (each property corresponds to a column in the file) + we have added an Id property for our SQLite implementation

          • All the Excel -> Blazor conversion is coded in the AgentExcel.razor page

          • The SQLite implementation involves and Interface and Class located in the Service folder and a razor component to handle updates

            • IExcelSqlService

            • ExcelSqlService (provides the actual logic for interacting with a SQLite database to manage the Olympic data results. It uses the SQLiteAsyncConnection class to perform asynchronous database operations such as creating tables, inserting, updating, deleting, and querying data.

            • AllMedalsState.cs  ... used to maintain state when a user moves from the main page (AgentExcel) to AddUpdateResults.razor and then returns 

      • Image Generation (AgentGenerateMAUI.xaml)

        • This is a XAML page as opposed to a razor component

        • The user enters an idea for an image to send to the AI

          • We call the  Task/Method GenerateImageAsync which is part of the OpenAIService.cs . This is a service that has been instantiated in the code behind file.

  • Blazor and AI ... What We Know So Far ... Part 21:29:44

    In this Lecture we will

    • Continue our Retrospective on AI implmentation within a Blazor Application

      • MAF Workflows ... Response API and more Applications

        • Simple Sequential Workflow (SimpleSeqFlow.razor)

          • This page demonstrates a Sequential Workflow using the Microsoft Agent Framework with Human-in-the-Loop Approval between steps.

          • Note: The NuGet Package Microsoft.Agents.AI.Workflows is installed but the Workflow class is not explicitly used in this example. We Manually handle the sequential process.

          • We use two agents: One for Summarizing the input text and Another for Translating the summary into a user-specified language. When you enter text and submit it, the workflow processes the input through both agents sequentially, with human approval required between steps.

          • Key Coding Highlights

            • private async Task ApproveSummary()

              {

                  // Step 2: Translation        

                  var client = new OpenAIClient(apiKey);

                  var chatClient = client.GetChatClient(model);

                     

              // Create a new AI Agent for translation with instructions that include the user-specified language

                  var translationAgent = chatClient.CreateAIAgent(name: "TranslationAgent", instructions: "Take the summarized text and Translate it to " + userLang);

                 

              var messages = new List<ChatMessage> { new(ChatRole.User, summary) }; 

              var translationResponse = await translationAgent.RunAsync(messages);           

                  translation = translationResponse.Text;

                  isLoading = false;

              }

            • @*First Step of Sequence ... Human In The Loop Approval*@

              @if (summary != null)

              {

                  <div class="alert alert-warning" style="width:50vw;">

                      <h5>Step 1: Review and Approve Summary</h5>

                      <textarea @bind="summary" class="form-control" rows="5"></textarea>

                      <div class="mt-2">

                          <button @onclick="ApproveSummary" class="btn btn-success me-2" disabled="@isLoading">Approve & Translate</button>

                          <button @onclick="RejectSummary" class="btn btn-danger" disabled="@isLoading">Not Approved</button>

                      </div>

                  </div>

              }

              @*Second step of Sequence ... if approved we display the Translated Text*@

              @if (translation != null)

              {

                  <div class="alert alert-info" style="width:50vw;">

                      <h5>Step 2: Translation Result</h5>

                      <p>@translation</p>

                  </div>

              }

        • PDF Sequential Workflow (PdfSeqFlow.razor)

          • This page enables you to select any PDF file and extract its entire text content using AI. The extracted text is then processed through a formal MAF  two-step  Sequential workflow: Note: The NuGet Package Microsoft.Agents.AI.Workflows is installed.

            1. Summarization: The first AI agent condenses the full PDF text into a concise summary (up to 150 words).

            2. Translation: The summarized text is then translated into your chosen language by a second AI agent.

          • This streamlined process helps you quickly understand and translate the core content of any PDF, all in one place. This is a redo and enhancement of the application created in Lecture 210 (working with other inputs)

          • Note: The FileService (Service folder) has been updated with the addition of Task PickImageOrPdfAsync which now uses FilePicker instead of MediaPicker(obsolete) and supports images and pdf selections

          • Key Coding Highlights

            • private async Task SubmitFilePrompt()

              { ...   

                  OpenAI.OpenAIClient client = new OpenAIClient(apiKey);

                  AIAgent agent = client.GetChatClient(model).CreateAIAgent(name: "SummaryAgent", instructions: "Extract the exact contents of the the pdf file text your are given. Format every response in HTML");

                  response = await agent.RunAsync(new ChatMessage(ChatRole.User,

                  [

                     new TextContent(userPrompt),

                     new DataContent(PDFdataUri, "application/pdf")

                  ]));


                  //Take the response from ChatGPT and use it as input to the Workflow in SubmitWorkflowPrompt below

                  //This will kickoff the sequential workflow where the first agent will summarize the extracted text and the second agent will translate it to the user requested language

                  userWorkflowPrompt = response.Text;


                  responseText += "PDF Local File  ... " + fileSelected + "<hr/>" + response.Text + "<br/>";  

              }

            • private async Task SubmitWorkflowPrompt()

              {

                  ...   

                   OpenAI.OpenAIClient client = new OpenAIClient(apiKey);

                   ChatClient chatClient = client.GetChatClient(model);

                 

                   ChatClientAgent summaryAgent = chatClient.CreateAIAgent(name: "SummaryAgent", instructions: "Summarize the text you are given to max 150 words");

                   ChatClientAgent translationAgent = chatClient.CreateAIAgent(name: "TranslationAgent", instructions: "Take a summarized text and Translate it to " + userLang);


                   // Build a Sequential Workflow with the two agents so that the output of the first is input to the second

                   Workflow workflow = AgentWorkflowBuilder.BuildSequential(summaryAgent, translationAgent);

                 

                   var messages = new List<ChatMessage> { new(ChatRole.User, userWorkflowPrompt) };


                   // Execute the workflow and capture the output

                  StreamingRun run = await InProcessExecution.StreamAsync(workflow, messages);   

                   await run.TrySendMessageAsync(new TurnToken(emitEvents: true));         

                 

                   List<ChatMessage> result = new();   


                   // Here we watch the stream of events from the workflow run and store the output when we get it into 'result'

                  await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))

                   {

                       if (evt is WorkflowOutputEvent completed)               // Check for the output event ... which means the workflow has completed

                       {

                           result = (List<ChatMessage>)completed.Data!;       

                           break;                                             

                       }

                   }


                   // Now we can display the results from each agent in the workflow (where Role is not User)

                   // AuthorName holds the name of the agent that produced the message ... SummaryAgent and then TranslationAgent

                   foreach (ChatMessage message in result.Where(x => x.Role != ChatRole.User))

                   {

                       responseFlowText += message.AuthorName + "<hr/>" + message.Text + "<hr/>";

                   }
              }

        • Custom Tool Calling : Booking Appointments Example ...  Manual  Human In The Loop Approval (BookingAgentTools.razor)

          • Sometimes, AI models cannot answer certain questions directly—such as "What is the current time?"—because they lack real-time or external data access. This page demonstrates how to extend an AI agent with custom tools that the agent can call to fetch information. In this application we extend an AI agents capabilities with 6 custom tools that the agent can call to fetch information like the NEXT available appointment for a Health Clinic.

          • Conversation context is persisted by declaring the AgentThread outside the SubmitTimePrompt method, allowing the thread to maintain state across multiple user interactions. This enables enhanced multi-turn conversations, where the assistant can remember previous exchanges and provide contextually relevant responses.

            • In this example we are giving our agent access to 6 tools (BookingService.cs in Models folder ... along with Appointment.cs)... But we insist on Human confirmation before Finalizing the Appointment Booking

              • ListAvailableDates - Returns all available appointment dates for the clinic

              • GetNextAvailableAppointment - Returns the next available appointment date/time for the clinic

              • BookAppointment - Books an appointment for a specified date/time

              • CancelAppointment - Cancels an existing appointment

              • ListBookedAppointments - Lists all upcoming appointments

              • FindAppointmentsByName - Finds all appointments for a given patient name

          • This is a simple implementation of Human Intervention capability (using a manual/hardcoded techique). Think of this as a poor mans Human in the Loop Technique. This application serves as a precursor to Human in the Loop Approval via the ApprovalRequiredAIFunction which we cover in Version 2

            • Consider a scenario where agents may have to interact with a real-world system such as Stripe for payments. In these types of use case, human oversight is required.In this updated application we’ll see how to implement human in the loop approval when creating agents using the Microsoft Agent Framework.

            • Why Human in the Loop Matters

              Agents can solve a variety of tasks and are great for creating automations. In certain use cases however you may prefer to still have human oversight. For example, use cases may include:

              • Processing payments

              • Deleting production data

              • Handling sensitive data

              An AI agent misinterpreting its goal can have disastrous effects in situations like these. This is why it’s important to consider where human in the loop checkpoints should or can be inserted when designing your agentic AI system.

          • Key Coding Highlights

            • public class Appointment

              {

                  public DateTime Date { get; set; }

                  public string PatientName { get; set; } = string.Empty;

              }

            • public class BookingService

              {

                  // a List of available dates the user can book (NEXT 30 days ... not current date)   

                  private static List<DateTime> _available = Enumerable.Range(1, 30)

                      .Select(i => DateTime.Now.AddDays(i).Date)

                      .ToList();


                  // This List will hold the Booked appointments stored as objects of type Appointment class

                  // containing Date and PatientName properties

                  private static List<Appointment> _booked = new List<Appointment>();

               

                  //The Tools ... used in the BookingAgentTools.razor component to interact with the booking system.

                  //These methods will be called by the MS Agent framework when the user interacts with the agent.

               

                  [Description("Determines the next available date for a booking")]

                  public static DateTime GetNextAvailableDate([Description("The date to start searching from")] DateTime startDate)

                  {       

                      return _available.FirstOrDefault(d => d > startDate);

                  }


                  [Description("Allows the user to make a booking.")]

                  public static string BookAppointment([Description("The date of the appointment")] DateTime date,string patientName)

                  {

                      // check if the date is available, ignore the time part

                      date = date.Date;


                      // Check if the date is available for booking ie in the _available list

                      // If it is, remove it from available and add to booked


                      if (_available.Contains(date))

                      {           

                          _available.Remove(date);           

                          _booked.Add(new Appointment { Date = date, PatientName = patientName });           

                          return "Appointment booked for " + date.ToShortDateString() + " for patient " + patientName;   

                      }

                      else

                      {

                          // return an error message

                          return "The selected date is not available";

                      }

                  }


                  [Description("Cancels a booking.")]

                  public static string CancelAppointment([Description("The date of the appointment to cancel")] DateTime date, string patientName)

                  {

                      //pull the appointment from the booked list based on date and patient name

                      var appointment = _booked.FirstOrDefault(a => a.Date == date && a.PatientName == patientName);
                      if (appointment != null)

                      {

                          _booked.Remove(appointment);           

                          _available.Add(date);                   


                          return "Appointment on " + date.ToShortDateString() + " for " + patientName + " cancelled";

                      }

                      else

                      {

                          return "The selected appointment is not booked";

                      }

                  } 

                  [Description("Lists all booked appointments.")]

                  public static string ListBookedAppointments()

                  {

                      if (_booked.Count == 0)

                          return "No appointments booked.";


                      //This creates a new collection of strings, joining them together with HTML line breaks

                      //Each representing one appointment with its date and patient name.

                      return string.Join("<br/>", _booked.Select(a => "Date: " + a.Date.ToShortDateString() + ", Name: " + a.PatientName));

                  }


                  [Description("Lists all available dates for booking.")]

                  public static List<DateTime> ListAvailableDates()

                  {

                      return _available;

                  }


                  [Description("Finds all appointments for a given patient name.")]

                  public static List<Appointment> FindAppointmentsByName(string patientName)

                  {

                      return _booked.Where(a => a.PatientName.Equals(patientName, StringComparison.OrdinalIgnoreCase)).ToList();       

                  }

              }

            • private async Task SubmitTimePrompt(bool confirmed = false)

              { ...
              // Only show confirmation if not already confirmed and the prompt contains the exact word "book" or "Book"

              // Use word boundaries to avoid matching "bookings" or similar by using regex

              if (!confirmed && System.Text.RegularExpressions.Regex.IsMatch(userPrompt, @"\b[Bb]ook\b"))

              {

              pendingPrompt = userPrompt;         // Store the user prompt (request) into pendingPrompt for later use ... it will be concatenated with the patient's name upon confirmation to create the final prompt for the agent

              showConfirmation = true;            // Show the confirmation dialog

              appointmentName = string.Empty;     

              StateHasChanged();

              return;

              }


              //agent setup has access to 6 Booking related tools ( static methods within BookingService.cs located in the Models folder) and a detailed set of instructions

              OpenAI.OpenAIClient client = new OpenAIClient(apiKey);

              AIAgent agent = client.GetChatClient(model)

              .CreateAIAgent(

              name: "BookingAgent",

              instructions: "You are an AI Bot that can call 6 Booking Tools related to booking appointments at a Medical Clinic."

              + "When displaying dates do not include the time" + "You can only answer questions related to bookings at the Clinic and have the ability to access all 30 available dates"

              + "If the user enters a possible appointment instead of asking for the next available date, it must include the month, day and year "

              + " and be in the range of acceptable appointment dates",

              tools:

              [

              AIFunctionFactory.Create(BookingService.ListAvailableDates),

              AIFunctionFactory.Create(BookingService.GetNextAvailableDate),

              AIFunctionFactory.Create(BookingService.CancelAppointment),

              AIFunctionFactory.Create(BookingService.BookAppointment),

              AIFunctionFactory.Create(BookingService.ListBookedAppointments),

              AIFunctionFactory.Create(BookingService.FindAppointmentsByName)

              ]

              );


              // Reuse existing thread or create a new one if null ... persistence across interactions

              if (thread == null)

              {

              thread = agent.GetNewThread();

              }

              chatHistory.Add(("User", userPrompt));

              ChatMessage message = new ChatMessage(ChatRole.User, userPrompt + " where possible formatted the response nicely in HTML");

              // Get the agent's response asynchronously ... note the use of the thread to maintain context

              AgentRunResponse response = await agent.RunAsync(message, thread);


              // Add assistant message to chat history

              chatHistory.Add(("Assistant", response.Text));

              }

            • @if (showConfirmation)

              {

              <div class="alert alert-warning" style="max-width: 600px; margin: 1rem auto;">

              <strong>Confirm Booking:</strong>

              <p>Do you want to book an appointment as requested?</p>

              <input @bind="appointmentName" placeholder="Enter your name ... then press tab to move focus to Yes Button" class="form-control mb-2" style="max-width: 400px;" />

              <button class="btn btn-success me-2" @onclick="ConfirmBooking" disabled="@(string.IsNullOrWhiteSpace(appointmentName))">Yes, Book</button>

              <button class="btn btn-danger" @onclick="CancelBooking">No, Cancel</button>

              </div>

              }

        • Sequential Workflow with Agents that reference Tools (MCP) and Human in the Loop (AgentSeqTools.razor)

          • This page demonstrates how to include a Tool calling agent in a Sequential Workflow, which connects to and utilizes a Model Context Protocol (MCP) server, specifically GitHub's Copilot MCP endpoint. To implement you must have the NuGet Package ModelContextProtocol installed.

            • In this version we first send the user prompt to the MCP Agent which has access to tools from the GitHub MCP server. If the MCP Agent cannot answer the prompt with the available tools it responds with "TOOL_NOT_FOUND" and we have a fallback general knowledge agent that can answer general questions without tools.

            • Once we get a response from either the MCP Agent or the fallback general agent we pause the workflow for human approval (Human in the Loop ...HITL) before continuing to the next steps of the workflow which are a Summary Agent and then a Newsletter Agent that takes the summary and creates a newsletter out of it.

            • Note: Even though we are using a formal sequential workflow we are only using one agent in the workflow at a time for each step.

          • About GitHub MCP Integration

            Connecting to the GitHub MCP (Model Context Protocol) server enables a Blazor app to interact with advanced AI services provided by GitHub Copilot. Here’s the significance:

            1. MCP Server:
              The MCP server acts as a gateway to AI models and tools (like Copilot) hosted by GitHub. By connecting, your app can send prompts and receive AI-generated responses.

            2. Available Tools:
              The MCP server exposes a set of "tools"—these are APIs or functions (such as code generation, summarization, or other AI-powered utilities) that the AI agent can use to enhance its responses.
              By listing and using these tools, your agent can perform more complex tasks, not just simple text completion.

            3. Why Use It?

              • Extensibility: You can leverage new tools as GitHub adds them, without changing your app’s core logic.

              • Security: Authentication (via tokens) ensures only authorized users can access these AI capabilities.

              • Advanced AI: You get access to the latest models and features from GitHub Copilot, beyond what’s available in basic OpenAI endpoints.

              • It enables AI applications to access real-time, real-world information and perform tasks, similar to how a web browser uses HTTP to connect to websites. MCP standardizes this connection, allowing AI agents to be more context-aware, perform actions, and overcome the limitations of their training data.

          • Key Coding Highlights

            • private async Task SubmitPrompt()

              {

                  userPrompt = Model.Feedback;

                  ....

              await using McpClient gitHubMcpClient = await McpClient.CreateAsync(new HttpClientTransport(new HttpClientTransportOptions

                  {

                      TransportMode = HttpTransportMode.StreamableHttp,

                      Endpoint = new Uri(""),

                      AdditionalHeaders = new Dictionary<string, string>

                      {

                          { "Authorization", "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" }

                      }

                  }));

                     

              // List available tools from GitHub MCP to use in the MCP Agent (mcpAgent) below ... this will allow the agent to call these tools when needed to answer the user prompt

                  toolsInGitHubMcp = await gitHubMcpClient.ListToolsAsync(); 

               

                  OpenAI.OpenAIClient client = new OpenAIClient(apiKey);

                  ChatClient chatClient = client.GetChatClient(model);       

                  // Create a fallback general knowledge agent ... just in case MCP agent cannot answer

                  AIAgent generalAgent = chatClient.CreateAIAgent(

                      instructions: "You are a helpful general knowledge assistant. Answer any question to the best of your ability.");


                  // Create MCP Agent with tools from GitHub MCP

                  AIAgent mcpAgent = chatClient.CreateAIAgent(

                      instructions: "You are a GitHub Expert. If you cannot answer using the available tools, respond with 'TOOL_NOT_FOUND'.",

                      tools: toolsInGitHubMcp.Cast<AITool>().ToList());

                 

                  Workflow workflow = AgentWorkflowBuilder.BuildSequential(mcpAgent);

               

                  var messages = new List<ChatMessage> { new(ChatRole.User, userPrompt + " format every response in HTML") };


                  StreamingRun run = await InProcessExecution.StreamAsync(workflow, messages);

                  await run.TrySendMessageAsync(new TurnToken(emitEvents: true));

                 

                  List<ChatMessage> result = new();     

               

                  await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))

                  {

                      if (evt is WorkflowOutputEvent completed)

                      {

                          result = (List<ChatMessage>)completed.Data!;

                          break;

                      }

                  }

                  // Find the mcpAgent's output (skip user)

                  var mcpMsg = result.LastOrDefault(x => x.Role != ChatRole.User && !string.IsNullOrWhiteSpace(x.Text));       

               

                  if (mcpMsg != null && mcpMsg.Text.Contains("TOOL_NOT_FOUND", StringComparison.OrdinalIgnoreCase))

                  {

                      // Fallback to general agent

                      Workflow generalWorkflow = AgentWorkflowBuilder.BuildSequential(generalAgent);

                      StreamingRun generalRun = await InProcessExecution.StreamAsync(generalWorkflow, messages);

                      await generalRun.TrySendMessageAsync(new TurnToken(emitEvents: true));     

                      result.Clear();           

                      await foreach (WorkflowEvent evt in generalRun.WatchStreamAsync().ConfigureAwait(false))

                      {

                          if (evt is WorkflowOutputEvent completed)

                          {

                              result = (List<ChatMessage>)completed.Data!;

                              break;

                          }

                      }           

                      // Update mcpMsg to the general agent's response

                      mcpMsg = result.LastOrDefault(x => x.Role != ChatRole.User && !string.IsNullOrWhiteSpace(x.Text));

                  }       

              //store raw text before being formatted output ... we will use this raw text to send to the Summary Agent

                  //in the next step of the workflow after human approval ...     

                  mcpAgentRawText = mcpMsg?.Text;       

                  mcpOutput = mcpMsg != null ? "<b>" + (mcpMsg.AuthorName ?? "MCP Agent") + "</b><hr/>" + mcpMsg.Text : "<span class='text-danger'>No MCP output.</span>";           

                  hitlStage = 1;      // Pause for human approval after mcpAgent

                  workflowStopped = false;       

              }

        • Demonstrate a Concurrent Workflow using the Microsoft Agent Framework. (FeedbackAgent.razor)

          • Concurrent Workflow allows us to build high-performance, parallel processing workflows that maximize throughput by executing multiple AI agents simultaneously while maintaining coordination and data consistency.

          • In this example, we create Three agents to analyze customer feedback/reviews. One checks Sentiment, the second one extracts Key Topics and the third flags Urgent Issues.

          • This is a simple implementation which replicates the functionality of the Review Analysis you see now on many E-Commerice sites

          • Key Coding Highlights

            • private async Task SubmitPrompt()

              {...
                  ChatClientAgent SentimentAgent = chatClient.CreateAIAgent(name: "Sentiment-Agent", instructions: "You are sentiment analysis expert. You determine the overall sentiment (positive/negative/neutral) of customer feedback");

                  ChatClientAgent TopicAgent = chatClient.CreateAIAgent(name: "Topic-Agent", instructions: "You are a topic extraction expert. You determine the main topics or themes from customer feedback " );

                  ChatClientAgent UrgencyAgent = chatClient.CreateAIAgent(name: "Urgency-Agent", instructions: "You are a urgency detection expert. You flag urgent or actionable items from customer feedback " );

                  Workflow workflow = AgentWorkflowBuilder.BuildConcurrent([SentimentAgent, TopicAgent, UrgencyAgent]);               

              // Prepare the input message (userPrompt) so that it can be sent to the workflow

                  // ... it must be in the form of a list of ChatMessage objects, even if we only have one message to send.

                  var messages = new List<ChatMessage> { new(ChatRole.User, userPrompt) };


                  // Execute the workflow and capture the output

                  StreamingRun run = await InProcessExecution.StreamAsync(workflow, messages);   

                  await run.TrySendMessageAsync(new TurnToken(emitEvents: true)); 


                  // First we declare a list to hold the result

                  List<ChatMessage> result = new();


                  // Here we watch the stream of events from the workflow run and store the output when we get it into 'result'

                  await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))

                  {

                      if (evt is WorkflowOutputEvent completed)         

                      {

              result = (List<ChatMessage>)completed.Data!; 

                              break;                                        l

                      }

                  }

               

                  foreach (ChatMessage message in result.Where(x => x.Role != ChatRole.User))

                  {

                      responseText += message.AuthorName + "<hr/>" + message.Text + "<hr/>";

                  }
              }

        • The Handoff Workflow (HandoffPage.razor)

          • This page demonstrates a multi-agent workflow using the Agent Workflow Framework to automate Handoffs between specialized AI agents. When a user submits a question, an IntentAgent (using gpt-4o-mini) first classifies the query as movie, music, or other. Based on this classification, the workflow automatically routes the question to the appropriate expert agent (MovieNerd, MusicNerd, or OtherNerd) for a detailed response using gpt-4.1

          • This approach contrasts with the AgentPage component, where agent handoff was managed manually using a switch statement based on Intent. Here, the workflow abstraction simplifies coordination between agents, making the process more maintainable and scalable.

          • Key Coding Highlights

            • private async Task SubmitPrompt()

              { ...   
                   //Get ChatClients for both models

                   ChatClient chatClientMini = client.GetChatClient(modelA);

                   ChatClient chatClient = client.GetChatClient(modelB);   

                  ChatClientAgent intentAgent = chatClientMini.CreateAIAgent(

              name: "IntentAgent",

              instructions: "Your only job is to classify the user's question as either 'movie', 'music', or 'other'. Do not answer the question. If 'movie', hand off to MovieNerd. If 'music', hand off to MusicNerd. If 'other', hand off to OtherNerd. Never answer the question yourself."

              );       

                   // Specialized Agents using more powerful model

                  ChatClientAgent movieNerd = chatClient.CreateAIAgent(name: "MovieNerd", instructions: "You are a Movie Nerd  answering questions. Display your answers formatted in HTML");

                   ChatClientAgent musicNerd = chatClient.CreateAIAgent(name: "MusicNerd", instructions: "You are a Music Nerd answering questions. Display your answers formatted in HTML");

                   ChatClientAgent otherNerd = chatClient.CreateAIAgent(name: "OtherNerd", instructions: "You are an expert at answering general questions not related to movies or music. Display your answers formatted in HTML.");   

                   List<ChatMessage> messages = new List<ChatMessage>();                

                   //Define the Workflow with Handoffs

                   Workflow workflow = AgentWorkflowBuilder.CreateHandoffBuilderWith(intentAgent)

                       .WithHandoffs(intentAgent, [movieNerd, musicNerd, otherNerd])             

                       .WithHandoffs([movieNerd, musicNerd, otherNerd], intentAgent)             

                       .Build();

                   messages.Add(new(ChatRole.User, userPrompt));                        

                     

              // Run the workflow using our method (RunWorkflowAsync) located below which streams output

                   messages.AddRange(await RunWorkflowAsync(workflow, messages));    

              }

            • private async Task<List<ChatMessage>> RunWorkflowAsync(Workflow workflow, List<ChatMessage> messages)

        • Group Chat Workflow ... Recipe Development ->Nutritional Review (GroupChatPage.razor)

          • Group chat orchestration models a collaborative conversation among multiple agents, coordinated by a manager that determines speaker selection and conversation flow. This pattern is ideal for scenarios requiring iterative refinement, collaborative problem-solving, or multi-perspective analysis.

          • Group chat orchestration has distinct characteristics compared to other multi-agent patterns:

            • Centralized Coordination: Unlike handoff patterns where agents directly transfer control, group chat uses a manager to coordinate who speaks next

            • Iterative Refinement: Agents can review and build upon each other's responses in multiple rounds

            • Flexible Speaker Selection: The manager can use various strategies (round-robin, prompt-based, custom logic) to select speakers

            • Shared Context: All agents see the full conversation history, enabling collaborative refinement

          • In this application we demonstrate a Recipe Development workflow where a "Chef" agent creates delicious recipes and a "Nutritionist" agent evaluates them for health and nutritional balance.

            • "Create a high-protein dinner recipe for athletes"

            • "Make a heart-healthy breakfast with low sodium"

            • "Design a kid-friendly lunch with hidden vegetables"

          • Key Coding Highlights

            • private async Task SubmitPrompt()

              { ...

              //Define instructions for our Chef Agent and Nutritionist Agent

                  const string CHEF_INSTRUCTIONS = @"""

              You are an experienced professional Chef with expertise in various cuisines. Your goal is to create delicious, flavorful recipes that satisfy the user's requirements. Provide ONE complete recipe per response including ingredients list and cooking instructions.    """;

                  const string  NUTRITIONIST_INSTRUCTIONS = @"""

              You are a certified Nutritionist with expertise in healthy eating and balanced diets.Your goal is to evaluate recipes for nutritional balance, health benefits, and dietary considerations. Analyze the recipe for macronutrients (protein, carbs, fats), vitamins, minerals, and overall health impact.    """;

                 ChatClientAgent chefAgent = chatClient.CreateAIAgent(name: "Chef", instructions: CHEF_INSTRUCTIONS);

              ChatClientAgent nutritionistAgent = chatClient.CreateAIAgent(name: "Nutritionist", instructions: NUTRITIONIST_INSTRUCTIONS);

                  List<ChatMessage> messages = new List<ChatMessage>();

              //Build our Group Chat Workflow with Round Robin Manager ... this will alternate between chefAgent and nutritionistAgent for each turn in the conversation

                  //Set MaximumIterationCount to limit the number of turns in the conversation   

                  Workflow workflow = AgentWorkflowBuilder.CreateGroupChatBuilderWith(agents =>

                  new RoundRobinGroupChatManager(agents)

                  {

                      MaximumIterationCount = 3  // Maximum number of turns

                  })

                  .AddParticipants(chefAgent, nutritionistAgent)

                  .Build(); 

                  messages.Add(new(ChatRole.User, userPrompt)); 

                  StreamingRun run = await InProcessExecution.StreamAsync(workflow, messages);

                  await run.TrySendMessageAsync(new TurnToken(emitEvents: true));   

                  string? lastAuthorName = null;

                  //Read the streaming events

                  await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))

                  {     


                      if (evt is AgentRunUpdateEvent update)

                      {

                          AgentRunResponse response = update.AsResponse();    

                          //Iterate through each message in the response

                          foreach (ChatMessage message in response.Messages)

                          {               

                              if (message.AuthorName != lastAuthorName)

                              {

                                  responseText += "<hr/>";

                                  responseText += "<b>" + "[" + message.AuthorName + "]" + ":</b> ";

                                  lastAuthorName = message.AuthorName;

                              }

                              responseText += message.Text;

                          }

                      }

                      else if (evt is WorkflowOutputEvent output)

                      {

                          //Extract the final conversation history from the output event as a list of ChatMessage objects

                          var conversationHistory = output.As<List<ChatMessage>>();

                          responseText += "<br/><hr/><b>Final Conversation History:</b><br/>";

                          lastAuthorName = null;


                          //Iterate through the entire conversation history to display all messages with proper formatting

                          foreach (var message in conversationHistory)

                          {

                              if (message.AuthorName != lastAuthorName)

                              {

                                  responseText += "<hr/>";

                                  responseText += "<b>" + "[" + message.AuthorName + "]" + ":</b> ";

                                  lastAuthorName = message.AuthorName;

                              }

                              responseText += message.Text + "<br/>";

                          }

                          break;

                      }

                  }   

              }

        • Using the Response API ... Conversations Demo (Conversations.razor)

          • This page demonstrates how to implement the ResponseId property unique to the Response API to maintain conversation context across multiple prompts and even return to a conversation much later in time to continue it with a new prompt.

            • Demonstrate the use of OpenAI Responses

            • This mirrors the concept of Persistence that we covered in Lecture 209 (BlazorMSagentFrameworkPersistence) where we used a ChatClient as opposed to the Response API.

            • We will start a conversation with multiple prompts and then continue the conversation with a follow up prompt

            • We still make use the AgentThread and AgentRunResponse like we did in a prior example BUT we now take adavantage of the ResponseId property to return to a conversation and continue it with a new prompt much later in time.

          • Key Coding Highlights

            • @using OpenAI.Responses   

            • private async Task SubmitPrompt()

                  {

                      if (string.IsNullOrWhiteSpace(userPrompt1) || string.IsNullOrWhiteSpace(userPrompt2))

                      {           

                          await App.Current.Windows[0].Page.DisplayAlert("Warning", "Please enter valid starting questions for first two prompts.", "OK");

                          return;

                      }      

              #pragma warning disable OPENAI001

                      // Create the Response client as opposed to the Chat client ... that has conversation support

                      OpenAIResponseClient responseClient = client.GetOpenAIResponseClient(model);

                   

                      AIAgent agent = responseClient.CreateAIAgent(instructions: "You are a Nice AI ");

                   

                      AgentThread thread = agent.GetNewThread();


                      //Run the first two prompts in the same thread to maintain context

                      AgentRunResponse response1 = await agent.RunAsync(userPrompt1 + "display the results nicely formated in HTML in 50 words or less", thread);

                      responseText = response1.Text + "<hr/>";

                      AgentRunResponse response2 = await agent.RunAsync(userPrompt2 + "display the results nicely formated in HTML in 50 words or less", thread);

                      responseText += response2.Text + "<hr/>";

                     

                      responseId = response2.ResponseId;


                      //make sure userContinuedPrompt is not empty otherwise we don't continue the conversation here

                      if (string.IsNullOrWhiteSpace(userContinuedPrompt))

                      {

                          buttonText = "Send to ChatGPT";

                          isLoadingOne = false;

                          return;

                      }

                     

                      //Here we use the standard AgentRunResponse but pass in ChatOptions with the ConversationId (responseId assigned to it)  to continue the conversation

                      AgentRunResponse response3 = await agent.RunAsync(userContinuedPrompt, options: new ChatClientAgentRunOptions

                      {

                          ChatOptions = new ChatOptions

                          {

                              ConversationId = responseId

                          }


                      });

                      responseText += response3.Text;


              #pragma warning restore OPENAI001

                  }

        • Reponse API ... Web Search Tool (ResponseWebSearch.razor)

          • This page demonstrates how to access up-to-date information using the OpenAI Web Search Tool. By leveraging the HostedWebSearchTool from the Microsoft.Extensions.AI NuGet package, the AI agent can perform real-time web searches to answer questions about current events or topics not covered in its training data. This is especially useful for scenarios where the latest information is required, such as news, recent research, or dynamic content.

          • While the Chat Completions API also supports web search, it requires using specific models like gpt-4o-search-preview or gpt-4o-mini-search-preview. In contrast, the Response API offers greater flexibility, web search can be enabled across a wider range of models, making it easier to integrate into different applications without being locked into special model variants.

          • Cost Considerations: Enabling the Web Search Tool may result in additional API calls, which can increase usage costs compared to standard model queries. Each web search performed by the agent incurs extra charges, so use this feature judiciously when up-to-date information is essential.

          • Important Notes:

            • The Microsoft.Extensions.AI NuGet package must be installed to enable web search capabilities.

            • Responses are formatted in HTML for better readability and presentation.

            • Streaming is used to provide incremental updates to the UI as the AI agent generates its response.

            • Test out the agent's performance and response quality WITHOUT the web search tool to understand the differences in results and costs.

              • Create the AI Agent with specific instructions ... BEFORE using web search tool(comment temporarily) ... test out to compare

              • (Display all the NBA game scores from last night ... currrent date)

              • AIAgent agent = responseClient.CreateAIAgent(name: "Test", instructions: "You are a general knowledge expert. Display your result in HTML");

            • Test out the agent's performance and response quality WITH the Web Search Tool to understand the differences in results and costs.

              • Create/uncomment the AI Agent with Web Search Tool ... AFTER testing without tool

              • The HostedWebSearchTool allows the AI agent to perform web searches to gather up-to-date information.

              • This is particularly useful for answering questions about current events or topics not covered in the training data.

              • (Display all the NBA game scores from last night ... currrent date)

              • ... BUT it is more costly to use the tool as it involves additional API calls to perform the searches.

          • Key Coding Highlights

            •     private async Task SubmitPrompt()

                  {... 

              AIAgent agent = responseClient.CreateAIAgent(

              name: "Test", instructions: "You are a general knowledge expert. Display your result in HTML", tools: [new HostedWebSearchTool()] );   
                                 
               
              if (!useStreaming)

              {

              AgentRunResponse response = await agent.RunAsync(userPrompt + " format every response in HTML");

              responseText = response.Text;


              // Get exact token usage from non-streaming response

              if (response.Usage != null)

              {

              var usage = response.Usage;

              usageText = "<strong>Token Usage:</strong> Input: " + usage.InputTokenCount + ", Output: " + usage.OutputTokenCount + ", Total: " + usage.TotalTokenCount;

              }

              }

                  }

        • Response API ... File Search Tool (FileSearch.razor)

          • This page demonstrates the use of OpenAI's new File Search Tool for Retrieval-Augmented Generation (RAG) applications. It is called HostedFileSearchTool

          • This tool allows the model to retrieve and reference information from files you've uploaded, making it great for use cases like answering questions from documentation, PDFs, knowledge bases, or research papers.

          • The Chat Completions API doesn’t support this tool at all. This is one of the Response API’s more agentic capabilities, carried over and improved from the Assistant API.

          • Note this application uploads a PDF file to OpenAI, storing it securely in your account.... go to Dashboard to see your uploaded files (look for Storage section ... then Files/Vector Stores)

          • Note how the uploaded file is indexed into a vector store to enable efficient semantic search.

          • The application allows users to submit queries about the PDF, which are answered using the File Search Tool and the indexed content.

          • The workflow: file upload → vector store creation → file indexing → query processing → result display.

          • Note: Usage of file storage and vector store operations may incur additional costs beyond standard GPT model usage.

          • Note: This application also implements a Background Response for queries that may take longer to process, which is common when using tools that require file indexing and search.

          • Note: The implementation of a pdf viewer ... user can click on the File List display to view any desired pdf

          • Key Coding Highlights

            • SubmitPrompt/SubmitFilePrompt (Hardcoded/FilePicker)         

        • Response API ... Code Interpreter Tool (CodeInterpreterPage.razor)

          • This example demonstrates how to use the Code Interpreter Tool with the Response API and AI Agent framework to generate and display images based on user prompts.

          • The Code Interpreter Tool allows the AI to execute code snippets, generate plots, and handle data analysis tasks.

          • Enter a question or request that involves data visualization or analysis, and the AI will respond with text and potentially generate an image to illustrate the results.All the behind scenes stuff is handled by the HostedCodeInterpreterTool class which is part of the OpenAI.Containers namespace. It uses Python in the backend to execute code.

          • Key Coding Highlights

            • Note the use of a Hashset to lookup file extensions

        • Custom Tool Calling : Booking Appointments Example  ... Human In The Loop Approval with Speech to Text (BookingAgentToolsHITL.razor)

          • This is the more formal Human In The Loop technique implemented using the MAF

          • In this updated version we wrap the AIFunction instance of the BookAppointment and CancelAppointment  function in an ApprovalRequiredAIFunction instance instead of using a hard-coded manual conditional statement to intercept Human intervention.

            This demo implements Real-Time Speech to Text capabilities. The user can ask for a booking verbally . The speech will be converted into text, which can be edited if desired , and then sent off to ChatGPT

        • Adding a Custom User Memory to your Agent in Microsoft Agent Framework  ***NEW *** (CustomMemoryPage.razor)

          • This page demonstrates how to add custom memory to the Microsoft Agent Framework. It enables AI agents to recall user preferences, past interactions, and domain-specific knowledge across sessions, moving beyond stateless, one-off conversations.

          • Benefits of Custom User Memory

            • Persistent Personalization: Allows the agent to remember user preferences, interests, and past interactions, enabling more personalized and relevant responses that evolve over time.

            • Contextual Awareness: Enhances the agent's ability to maintain context across multiple interactions, making conversations feel more natural and coherent.Eliminates the need for users to repeat information.

            • Improved User Experience: Users can build a relationship with the agent as it remembers important details about them, leading to increased engagement and satisfaction.

            • Improved Effeciency and Reduced Cost:Agents can act on prior instructions immediately, resulting in fewer tokens used and faster results.

            • Long Term Learning: Agents can store and retrieve facts, such as "favorite color is blue" or "always summarize in bullet points," improving their usefulness over time.

            Scenarios Where Custom User Memory Shines

            • Virtual Personal Assistants: Remembering user schedules, preferences, and past interactions to provide proactive assistance.

            • Customer Support Bots: Retaining information about previous issues or purchases to offer more efficient and personalized support.

            • Educational Tutors: Keeping track of a student's progress, strengths, and weaknesses to tailor lessons accordingly.

            • Healthcare Advisors: Storing patient information and medical history to provide informed advice and reminders.

            • Personalized Shopping AssistantAn AI remembers a user's budget, clothing size, and preferred brands from previous conversations to suggest new items.

            • Travel Planner AgentAn agent retains user preferences (e.g., "always prefers window seats," "only books Marriott hotels") for future trip planning.

          • Architecture Overview

            This implementation uses a dual-agent architecture to manage user memory:

            • Main Agent (agentWithCustomMemory): The primary conversational AI that interacts with the user and uses stored memories to provide personalized responses.

            • Memory Extractor Agent (memoryExtractorAgent): A specialized agent that analyzes user messages to identify and extract new facts worth remembering.

            CustomContextProvider Class

            The CustomContextProvider class manages the memory lifecycle with three key methods:

            1. LoadMemoriesAsync(): Loads previously stored user facts from a persistent file located in the system's temp directory. Each user has their own memory file named {userId}.txt.

            2. GetMemoryInstructions(): Formats stored facts into a system instruction that gets injected into the conversation context, allowing the main agent to "remember" information about the user.

            3. StoreMemoriesAsync(): After each interaction, this method invokes the memory extractor agent to analyze the user's message and identify new facts to store. The extractor is informed of existing facts to avoid duplication.

            Memory Lifecycle Flow

            1. Pre-Interaction: Existing memories are loaded from disk and injected as system instructions into the message list.

            2. User Input: The user's message is sent to the main agent along with the memory context.

            3. Agent Response: The main agent responds with awareness of stored facts about the user.

            4. Post-Interaction: The memory extractor agent analyzes the user's message to identify new facts, which are then appended to the memory file.

            Key Technical Decisions

            • File-Based Persistence: Memories are stored as plain text files, one per user, making them persistent across app sessions.

            • Manual Context Management: Rather than using the built-in AIContextProvider infrastructure (which may have API changes), this implementation manually manages context injection and memory extraction for better control and compatibility.

            • Deduplication: The Distinct() method ensures no duplicate facts are stored.

            • Two-Agent Pattern: Separating memory extraction into its own agent with specialized instructions improves accuracy in identifying relevant facts to remember.

            Try It Out

            Enter messages that contain personal information (e.g., "My name is Sarah and I love hiking" or "I'm a software developer from Seattle"). In subsequent messages, ask questions that relate to previously mentioned facts to see the memory system in action.


      Wrapping Up

      • .NET 10 and the Microsoft Agent Framework represent a genuine inflection point for .NET developers working with AI. For the first time, you can build production-grade AI agents using the same patterns, tools, and infrastructure you already know — dependency injection, middleware — without jumping to Python or learning an entirely new ecosystem.

      • The AI agent space is evolving rapidly, and .NET is no longer playing catch-up. If anything, the tight integration with Azure, the enterprise-grade tooling, and the familiar C# patterns make it one of the strongest platforms for building agents that real businesses can actually depend on.

    • Supplementary Demos  ...

  • The Microsoft Agent Framework 1.0 General Availability ... What you Need to Know41:59

    In this Lecture we will

    • Learn that Microsoft officially announced the general availability of the MAF 1.0 on Apr 3 2026

      • This production-ready release offers stable APIs, long-term support for both .NET and Python, and integrates AutoGen with Semantic Kernel for enterprise-grade multi-agent orchestration and cross-runtime interoperability

      • The framework serves as a core component of the Microsoft AI agent landscape, designed to unify the creation and management of AI agents with secure, enterprise-ready tooling

      • Version 1.0 is officially stable for production deployment, ending the preview period  (on which on my previous lectures were based on)  featuring numerous breaking changes.

    • We will highlight the key breaking changes and key updates within our updated core application MAFv1BzHyb

      • Home.razor

        • Note the link to the MAF Documentation Site

        • Breaking Changes

          • The .CreateAIAgent() that creates the ChatCompletionAgent is now called .AsAIAgent (see SimpleStructured.razor)

            • ChatClientAgent agent = client.GetChatClient("model").CreateAIAgent();
              ChatClientAgent agent = client.GetChatClient("model").AsAIAgent();

            • OpenAIClient client = new OpenAIClient(apiKey);
              ChatClient chatClient = client.GetChatClient(model);
              ChatClientAgent agent = chatClient.AsAIAgent(name: "myAgent", instructions: systemMessage);

          • The method agent .GetNewThread() is now CreateSessionAsync() and was initially updated to agent.GetNewThreadAsync() (see AgentPage.razor)

            • AgentThread thread = agent.GetNewThread();
              AgentThread thread = await agent.GetNewThreadAsync();

            • AgentSession? thread
              thread = await agent.CreateSessionAsync().ConfigureAwait(false);

          • The return object of the .RunAsync(...) has been renamed from AgentRunResponse to AgentResponse ... Standard and Streaming Responses

            • AgentRunResponse response = await agent.RunAsync(message);
              AgentResponse response = await agent.RunAsync(message);

            • Streaming Response  (see MAF Intro Google Gemini)
              await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(userPrompt))

          • OpenAI Responses API ... OpenAI.Responses (see Conversations.razor)

            • Old method prior to MAF 1.0
              OpenAIResponseClient responseClient = client.GetOpenAIResponseClient(model);

            • MAF 1.0
              ResponsesClient responseClient = client.GetResponsesClient();

          • Workflow Updates (see PdfSeqFlow.razor)

            • StreamingRun run = await InProcessExecution.StreamAsync(workflow, messages);
              StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, messages);

          • Note: OpenAI Nuget package used is version 2.9.1 NOT 2.10 (issues with Code Intrepreter Tool)

            • OpenAI (2.9.1)

            • Microsoft.Agents.AI.OpenAI(1.0.0)

            • Microsoft.Agents.AI.Workflows(1.0.0)

      • MAF Intro with OpenAI (SimpleStructured.razor) ... AsAIAgent/AgentResponse

        • SubmitPrompt

          OpenAIClient client = new OpenAIClient(apiKey);

          //Unstructured Response

          //----------------------------------------------------------

          //AIAgent agent1 = client.GetChatClient(model).AsAIAgent(instructions: "You are an expert in The Movie Datatbase Lists");

          //AgentResponse response1 = await agent1.RunAsync(userPrompt);

          //responseText = response1.Text;


          //Structured Response

          //----------------------------------------------------------

          ChatClientAgent agent2 = client.GetChatClient(model).AsAIAgent(instructions: "You are an expert in The Movie Datatbase Lists. Please use the User Score as the indicator for Rating ");


          //Note the difference between AIAgent and ChatClientAgent:

          //--------------------------------------------------------

          //AIAgent is typically used for UNSTRUCTURED responses, where you just want free-form text from the AI (like a chat or general answer).

          //ChatClientAgent is a base class that provides more advanced features, including the ability to specify

          //and extract STRUCTURED responses(like a list of Movie objects).

          try

          {

              // Define the structure of the response expected and pass it to the agent. Here we expect a list of Movie objects.       

              AgentResponse<List<Movie>> response2 = await agent2.RunAsync<List<Movie>>(userPrompt);
              movies = response2.Result;

          }

      • MAF Intro Google Gemini (GeminiMAF.razor) ...AgentResponseUpdate/RunStreamingAsync

        • SubmitPrompt

          IChatClient client = new GenerativeAIChatClient("yourKey", "gemini-2.5-flash-lite");
          ChatClientAgent agent = new(client, "Gemini Agent", "You are a helpful assistant that provides concise and accurate answers to user queries.");   
          responseText = string.Empty; // Clear any previous response

          await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(userPrompt)

          {

              responseText += update.Text;

              StateHasChanged(); // Refresh UI with new content as it streams in

          }     

      • Static Images+ (AgentPage.razor) ... AgentSession/CreateSessionAsync

        • private AgentSession? thread = null;

        • SubmitFilePrompt
          //First Agent with standard instructions for unstructured response

          AIAgent agent1 = chatClient.AsAIAgent(

          name: "ImageAgent",

          instructions: "You are a friendly Image recognition specialist. ");
          if (thread == null)

          {     

              thread = await agent1.CreateSessionAsync().ConfigureAwait(false);     

          }

          //Standard unstructured response with new addition of thread

          var response1 = await agent1.RunAsync(new ChatMessage(ChatRole.User,

          [

              new TextContent(userPrompt),

          new DataContent(dataUriImg, "image/jpeg")               

          ]),thread);

      • Seq Workflow (PdfSeqFlow) ... RunStreamingAsync         

        • SubmitWorkflowPrompt
          ChatClientAgent summaryAgent = chatClient.AsAIAgent(name: "SummaryAgent", instructions: "Summarize the text you are given to max 150 words");

          ChatClientAgent translationAgent = chatClient.AsAIAgent(name: "TranslationAgent", instructions: instructionsLang);


          // Build a Sequential Workflow with the two agents so that the output of the first is input to the second

          Workflow workflow = AgentWorkflowBuilder.BuildSequential(summaryAgent, translationAgent);

          var messages = new List<ChatMessage> { new(ChatRole.User, userWorkflowPrompt) };
          StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, messages);

        • Note the addition of a PDF viewer via the browser for any selected pdf file

          • private async Task ShowSavedPdf(string base64DataUri)

            {

                try

                {

                    // Check if a PDF has been loaded

                    if (string.IsNullOrWhiteSpace(base64DataUri))

                    {

                        await App.Current.Windows[0].Page.DisplayAlertAsync("Error", "No PDF loaded. Please select a PDF file first.", "OK");

                        return;

                    }


                    // Extract the base64 content from the data URI

                    // Format: "data:application/pdf;base64,<base64-content>"

            // So we want everything after the comma which is the actual base64 string

            // Then we convert that base64 string back to bytes so we can write it to a temporary file and open it

                    var base64Data = base64DataUri.Split(',')[1];

                    var pdfBytes = Convert.FromBase64String(base64Data);


                    // Create a temporary file in the cache directory

                    var cacheDir = FileSystem.CacheDirectory;

                    var cachedFilePath = Path.Combine(cacheDir, $"temp_pdf_{DateTime.Now:yyyyMMddHHmmss}.pdf");


            // Write the PDF bytes to the cache file saved with the name temp_pdf_timestamp.pdf

                    await File.WriteAllBytesAsync(cachedFilePath, pdfBytes);


                    // Open the PDF using the default system viewer/browser This works on Windows and mobile platforms

                    await Launcher.OpenAsync(new OpenFileRequest

                    {

                        File = new ReadOnlyFile(cachedFilePath)

                    });

                }

                catch (Exception ex)

                {

                    await App.Current.Windows[0].Page.DisplayAlertAsync("Error", $"Could not open PDF: {ex.Message}", "OK");

                }

            }

      • Conversations (Intro Responses API) ... ResponsesClient

        • SubmitPrompt
          // Initialize OpenAI client

          OpenAIClient client = new OpenAIClient(apiKey);
          #pragma warning disable OPENAI001

                  // Create the Response client as opposed to the Chat client ... that has conversation support       

          //Old method prior to MAF 1.0

                  //OpenAIResponseClient responseClient = client.GetOpenAIResponseClient(model);


                  //MAF 1.0

                  ResponsesClient responseClient = client.GetResponsesClient();


                  //Create an AI Agent with some basic instructions

                  AIAgent agent = responseClient.AsAIAgent(model:model,instructions: "You are a Nice AI ");             

                  AgentSession thread = await agent.CreateSessionAsync().ConfigureAwait(false);       

                  AgentResponse response1 = await agent.RunAsync(userPrompt1 + "display the results nicely formated in HTML in 50 words or less", thread);

                  responseText = response1.Text + "<hr/>";

      • Two Steps Forward One Step Back

        • Booking Agent Custom Tools (BookingAgentToolsHITL.razor)

          • Since the Gemini Implementation ( MAF Intro Google Gemini) the function FunctionApprovalRequestContent is no longer available (Gemini NuGet Package Conflict Issue)

          • ... so this version of HITL will NOT work.Please refer back to the Manual version listed in the left menu as "Booking Agent Cust Tools" (BookingAgentTools.razor) or the pre-Gemini Implementation version of this entire Project (BlzHybMAFudemy) found in the Resources for Lecture 215

    • Highlight other Updates/Changes to multi Application Project

      • Note the new Links to many of the NuGet and Javascript Libraries used through the Project. New Links have also been added for Supplementary Resources  students may find useful ... these can be accessed in each of the application opening screens

      • Microsoft Foundry Local (all new!)

        • This application implements a simple AI Conversation Agent that takes user input and sends it to the qwen3-0.6b model running locally via Microsoft Foundry Local and then returns the response back to the user.

          • Foundry Local is a Microsoft tool for running Large Language Models (LLMs) and AI inference  that run directly on the user's device (Windows, Mac, Linux, or Azure Local) rather than in the cloud. It is essentially Microsoft's own version of Ollama. User data never leaves the device, responses start immediately with zero network latency, and your app works offline. No per-token costs, no API keys, no backend infrastructure to maintain, and no Azure subscription required.

        • Key Features and Capabilities

          • On-Device AI: A catalog of high-quality models optimized for on-device use across a wide range of consumer hardware. The catalog covers chat completions (for example, GPT OSS, Qwen, DeepSeek, Mistral and Phi) and audio transcription (for example, Whisper).

          • Simplified Deployment: Handles model lifecycle management and automatically detects hardware (GPU/NPU/CPU) for acceleration without requiring complex setup.

          • Developer Friendly: Provides SDKs in Python, JavaScript, C#, and Rust. Supports OpenAI request and response formats including the OpenAI Responses API format. If your application already uses the OpenAI SDK, point it to a Foundry Local endpoint with minimal code changes.

          • Offline Functionality: Once models are downloaded, they can run in air-gapped environments, making it ideal for edge scenarios and secure environments.

        • Special Note:

          Microsoft.AI.Foundry.Local is a Windows-only NuGet package — it only works on net10.0-windows. But this .NET MAUI project also targets Android/iOS/Mac Catalyst, so both the package reference and the using failed on those platforms. Two fixes applied:

          1. .csproj — Made the PackageReference conditional on Windows:

          2. FoundryLocal.razor — Removed the top-level using Microsoft.AI.Foundry.Local (Razor's using directives can't be wrapped in #if preprocessor guards). Instead, use #if WINDOWS inside code { } blocks and reference Foundry.Local types with their fully-qualified names (e.g., Microsoft.AI.Foundry.Local.FoundryLocalManager).

      • Images+Speech (AgentPagesImages.razor)

        • New Structured Case

          • bill.png is connected to the Receipt class ... which contains a property called Items which is a LIST of type LineItem (containing properties Name, Quantity, Unit Price, Total Price)

          • and a property called Subtotal which is the final total amount shown on the receipt.

      • Song Rel DB (SQLite)  (SongPage.razor)

        • Filter by Artist ... Instant Results  ... in code section
          //Replaced with the new implementation of the Filter property below that automatically shows the SQL table when the user starts typing in the filter box

          //public string Filter { get; set; }                      // Used for filtering the songs by artist only ... Non AI technique for filtering

          // We want to implement a filter that automatically shows the SQL table when the user starts typing in the filter box

          // Here we are using a private backing field called _filter  (actually stores the value)  for the Filter property so that we can add custom logic

          //• When we use @bind="Filter" in our HTML markup, Blazor will call the "set" accessor of Filter whenever the input changes.

          //• The setter updates _filter and runs the extra logic to possibly show the SQL table.

          //• The getter returns the current value of _filter for display or logic.

          //The property Filter exposes this value to the rest of the codeand to data binding in the UI

          private string _filter;

          public string Filter

          {

          get => _filter;

          set

          {

          _filter = value;

          // Auto-show SQL table when filtering starts (only first time)

          if (!string.IsNullOrWhiteSpace(_filter) && !showSongInfoSql)

          {

          showSongInfoSql = true;

          }

          }

          }

      • Note the addition of a "Save and Exit Application" button in the AgentNBA.razor and SongPage.razor pages.

      • Note improved PDF Viewer in PdfSeqFlow.razor and FileSearch.razor

        • uses the WebSiteViewerCC XAML page and allows us to display the pdf in a modal page inside the app itself.

    • Final Thoughts

      • The “Learn to Code” Era is Dead. Long Live the Architect.

        • A coder is now more like an architect than a construction worker.

      • Coding After Coders: The End of Computer Programming as We Know It

        • If describing and talking are now much of the work of a software developer, the talk nonetheless remains pretty complex and highly technical. An amateur can’t do it. You can’t just tell an agent, Build me the code for a successful start-up. The agents work best when they’re being asked to perform one step at a time; ask for too much and they can lose the plot.

        • It’s a delusion to imagine that your A.I. agent will generate a whole project at once, in a “Big Bang” moment. Yes, you can get it to write 5,000 lines of code — but then, he says, “you test it and nothing works.” This, all the software developers say, is where their training and expertise are still needed: knowing how a big codebase ought to be structured, how to design the system so it’s reliable and how to figure out if the agent is sloppy.

    • Supplementary Demos

      • MAFv1BzHybLoc (Language Localization)

        • Localization in Blazor is the process of adapting an application to support different languages, regions, and cultures, using .NET Resource Files (.resx). Localization allows us to translate UI text and formats dates, numbers, and currencies according to user preferences

          This application implements a simple Localization Page utilizing the built in Resource Explorer and the ResX Manager
          Installing the ResX Manager

          • Install as an extension in Visual Studio (search for "ResX Manager" in the Extensions Marketplace ... Click Extensions-Manage Extensions)

          • Download from Visual Studio Marketplace and install manually

        • Note:

          • The current .NET 10 version of the Microsoft.Extensions.Localizations NuGet package is used to manage localization in this application.

          • We enable localization by adding the necessary service in the MauiProgram.cs file, which sets up the dependency injection for localization... builder.Services.AddLocalization()

          • The .resx files are located in the Resources/Languages folder and are named using the format MyStrings.[culture].resx (e.g., MyStrings.resx for English, MyStrings.fr-CA.resx for French Canadian, etc.).

          • The IStringLocalizer interface is injected into this component (LocalizationPage.razor) and Home.razor to access localized strings based on the current culture.

          • We use the Localizer object to retrieve localized strings in the UI, such as Localizer["Welcome"] and Localizer["Language"] on this page and the Home.razor page

      • MAFv1BzHybWebApi (Connecting to External API)

        • This application connects to an External API Service that tracks Global Exchange Rates. The app allows the user to enter the Country of their choice (via a Dropdown list) which then displays it's associated Currency Code (eg US is USD, Canada is CAD). The Currency Code can then be passed on to the API Service which will return the value of that currency against all the other world currencies.

        • This Update also now includes an AI powered Currency Conversion feature that allows the user to ask a natural language question about currency conversion and get an answer in real time.

          This application uses the ExchangeRate-API Service which has a free plan but the API call requires an API key.

        • Note:

          • Here are the details for working with the API (Requests and JSON response) Standard Requests (Documentation for the API Endpoint)

            • You should notice that the “conversion_rates” field in the JSON file that will be returned is not an array but rather a key,value pair. Are you thinking Dictionary? ... Check out the Rate.cs file in the Models folder to see how I created a class to handle this type of data structure.

          • I couldn’t find a free external site offering a Country Currency Codes API but I did find a site offering a comparable list in CSV format (Comma Separated Values).

            • Currency Codes and More

            • I then converted the CSV file to JSON . The site above has a built in converter that allows you to convert CSV to JSON and then I saved the JSON file (newcurrencies.json) in the wwwroot folder of this project and read it from there. (See OnInitializedAsync method)

            • Check out the NewCountry.cs file in the Models folder to see how I created a class to match this json file's format

        • AI Currency Conversion — Key Implementation Notes

          The second card on this page adds a natural language AI feature on top of the existing exchange rate lookup. Here is what was required to make it work:

          • CurrencyResponse.cs (Models folder) — A new model class with three string properties: Amount, BaseCurrency, and ExchangeCurrency. This defines the structured shape the AI must return, ensuring a consistent, parseable response every time.

          • @using directives — Three new namespaces were added at the top of this page: OpenAI, OpenAI.Chat, and Microsoft.Agents.AI, plus the ChatMessage alias — the same pattern used in SimpleStructured.razor.

          • ChatClientAgent + RunAsync<CurrencyResponse>() — Instead of a plain AIAgent (which returns free-form text), a ChatClientAgent is used so the structured generic overload RunAsync<T> can be called. This instructs the MAF to coerce the GPT-4o-mini response directly into a CurrencyResponse object — no manual JSON parsing needed.

          • System prompt (instructions) — The agent is given explicit instructions to always return ISO 4217 three-letter currency codes (e.g. CAD, USD, EUR) and nothing else, keeping the structured data clean and reliable.

          • ExchangeRate-API call — Once the AI returns the BaseCurrency, the existing HttpClient (already injected on this page) is reused to hit the same ExchangeRate-API endpoint. The ExchangeCurrency code is looked up inside the returned conversion_rates dictionary, and the math is applied: converted amount = entered amount × rate.

      • MAFv1BzHybCascade (Cascading Menus)

        • This application implements a simple 3-level cascading menu system.

          • Each dropdown list is populated based on the selection made in the previous dropdown.

          • We start off with Country -> City -> Tourist Sites. Each menu item is a link to a dropdown list that displays the relevant data.

          • All three components are stored in an SQLite database with full CRUD operations implemented for each.

          • Site descriptions can be created via AI in the TouristEdit.razor page.

        • Key Coding Implementation Notes

          • First we defined 3 classes — Country, City, and Tourist — to represent the data for each dropdown level.

          • Then we created SQL services for each class to handle database interactions, and added them to MauiProgram.cs . They are now ready to be injected into the appropriate razor pages.

            • ICountrySqlService/CountrySqlService

            • ICitySqlService/CitySqlService

            • ITouristSqlService/TouristSqlService

          • Next we created 3 razor pages to handle the CRUD operations for each class, and injected the appropriate SQL service on each page to get the data we need to display and manipulate.

            • CountryPage.razor/CountryEdit.razor

            • CityPage.razor/CityEdit.razor

            • TouristPage.razor/TouristEdit.razor

          • In the TouristEdit.razor page we have implemented AI Description Generation functionality using OpenAI.

            • We added a button (Generate Description) to trigger the description generation process.

            • This button calls a method in the OpenAIServiceNBA.cs file called GenerateTouristSiteDescriptionAsync which takes the name of the tourist site as input and returns a generated description including a Google Map URL for the location of the tourist site.

            • The generated description is stored in the database and displayed in the final results section of the CascadingPage.razor component.

          • Next we created the CascadingPage.razor component to implement the cascading dropdown functionality, and injected the 3 SQL services to get the data we need for each dropdown list.

          • The Key to the cascading functionality is the use of the bind-Value:after event on the InputSelect components for the country and city dropdown lists. This allows us to execute a method immediately after a selection is made and the value is updated, which is where we filter the data for the next dropdown list based on the selection made.

      • MAFv1BzHybSignalRwithSongUpdate

        • Here we have created a classic Real-Time chat demo (UPDATED to a Polling Application - Real Time Vote Counting) powered by ASP.NET Core SignalR, running entirely inside a .NET MAUI Blazor Hybrid app. Unlike a Blazor Web App — which has separate Server and Client projects — this single-project app hosts the SignalR hub in-process using a lightweight Kestrel server on localhost:5177, and this Blazor page connects to it as a standard SignalR client.

          • You can have Multiple running instances of the app and they will all share the same hub and communicate in real time (Check out the feature updates of a Vote Frequency Counter and the Late Joiner History Replay)

          • Note the addition of some AI Analysis of the Polling results using the Group Chat Workflow of the Microsoft Agent Framework.

        • How It Works — The Key Insight

          A MAUI Blazor Hybrid app has no separate server project, so the classic Hub (ChatHub) → Program.cs → Client split from a Blazor Web App does not apply directly. This solution uses three ideas working together:

          1. The MAUI app is the server — it spins up a tiny in-process Kestrel web host on localhost:5177 at startup (Windows only)... Click on "What is an in-Process Kestrel Host" button for more info.

          2. The Blazor page is the client — it connects to localhost:5177/chathub with a standard HubConnection, exactly like the Blazor Web App approach.

          3. Second / third instances of the app fail to bind port 5177 (already owned by the first), but still connect as clients to the first instance's hub — so all instances chat together seamlessly.

        • Why Two Separate NuGet / Framework Additions Were Needed

          The .NET MAUI Blazor Hybrid template does not include SignalR support out of the box. Two distinct additions were required, each serving a different role:

          1. Microsoft.AspNetCore.SignalR.Client (NuGet package) — needed by ChatPage.razor to connect to the hub as a client. This is the same package you would add to a Blazor WebAssembly client project. It provides HubConnectionBuilder and the HubConnection API used in the chat page.

          2. Microsoft.AspNetCore.App (framework reference, Windows-only) — needed by SignalRServerService.cs to host the hub server using WebApplication / Kestrel. This is the full ASP.NET Core server-side runtime that is not shipped with MAUI by default — which is exactly why the first build attempt failed with CS0246: The type or namespace name 'WebApplication' could not be found until this reference was added.

          In a classic Blazor Web App the Server project already has Microsoft.AspNetCore.App implicitly — it is an ASP.NET Core app — which is why you never had to think about it there. In a MAUI Hybrid app you are stepping outside MAUI's normal boundaries to host a web server, so it must be pulled in explicitly and guarded with #if WINDOWS so other platform targets are not affected.

        • Feature Update — Vote Frequency Counter

          Each call to PollSendMessage() on the hub now maintains a thread-safe running total for every language option. The logic uses a shared static Dictionary<string, int> (keyed by language name) protected by a lock object so that concurrent votes from multiple clients never corrupt the count.

          1. The hub calls _voteCounts.TryGetValue() to read the current count (defaulting to 0 if the language has not been seen before).

          2. It increments the value and writes it back to the dictionary — atomically, inside the lock block.

          3. The updated Frequency property is stamped onto the Chat object before it is broadcast, so every client receives the live cumulative total alongside the voter's name and language choice.

          Because _voteCounts is static, the totals survive for the entire lifetime of the hub process — all instances connected to the same hub share one authoritative count. If the host instance is closed, the in-memory state resets (as expected for this demo-level implementation).

        • Feature Update — Late-Joiner History Replay

          Before this update, any instance that connected after voting had already started would see an empty message list and stale (zero) totals. The hub now overrides OnConnectedAsync to solve this.

          Every accepted vote is appended to a shared static _history list (inside the same lock as the frequency dictionary, so the two are always consistent). When a new client connects, OnConnectedAsync:

          1. Takes a snapshot of _history inside the lock — preventing interference from votes arriving at the same moment.

          2. Sends every item in the snapshot to only the caller (Clients.Caller) — existing clients are not affected and will not see duplicate entries.

          3. Calls base.OnConnectedAsync() to complete the standard SignalR connection handshake.

          The client-side handler (PollReceiveMessage) is identical for both replayed history items and live votes — the late joiner's message list is populated exactly as if they had been present from the start.

        • Feature Update — Autocomplete on the Name Field

          As the user types into the Name field, a live suggestion list appears showing up to 7 names that start with the characters typed so far. Clicking any suggestion immediately fills the field and dismisses the list. The feature is wired together from three parts:

          1 — Data Source: DataSeed.cs (Models folder)

          The DataSeed class in the Models folder holds a large in-memory name array: MoreNames (male and female names, A–Z, ~2 700 entries). This is the raw data set that is searched on every keystroke.

          2 — Service: SuggestionsApi

          The SuggestionsApi service is injected at the top of this page with @inject SuggestionsApi Sugs. Its FindAsync(phrase) method filters the DataSeed name list using a case-insensitive StartsWith check and returns the first 7 matches as an IList<string>. A simulated Task.Delay(200) mimics a real async API call.

          3 — UI Wiring in ChatPage.razor (lines 648–678)

          The Name input is a plain HTML <input> element rather than the Blazor InputText component. This was a deliberate decision: @bind-Value:event="oninput" has no effect on a Blazor component because InputText abstracts away the underlying DOM event — the modifier is silently ignored. A native <input> element is required so that @bind:event="oninput" and @bind:after work as designed.

          <input type="text" class="form-control"

                 @bind="Chat.UserName"

                 @bind:event="oninput"

                 @bind:after="AutocompleteAsync" />

          • @bind:event="oninput" — updates Chat.UserName on every keystroke rather than waiting for the field to lose focus (onchange).

          • @bind:after="AutocompleteAsync" — fires AutocompleteAsync() (line 1028) after the binding has written the new value to the model, so the method always sees the latest typed text.

          AutocompleteAsync() calls Sugs.FindAsync(Chat.UserName) and stores the result in the Suggestions list. Blazor re-renders the suggestion <ul> automatically. Clicking a suggestion calls SelectSuggestion(), which writes the chosen name back to Chat.UserName and clears the list.

          ? Note — ValidationMessage still works even though InputText was replaced. <ValidationMessage For="() => Chat.UserName" /> keys off the model expression, not the input component type, so data-annotation validation continues to function normally.

        • This application also links back to the Song Rel DB (SQLite) SongPage.razor

          • Now this application also implements SignalR. To test out Launch a second instance of the same app — by running the .exe directly from the build output folder ... bin/Debug/net10windows/win-x64/BlazorHybridMAFimages (you can run multiple instances simultaneously).Make an edit/update to the Songs or Categories in one instance and watch the update appear instantly in the other instance

      • MAFv1BzHybSignalRKanban

        • Drag and Drop has become a popular interface solution in modern applications. It's common to find it in productivity tools like Trello and JIRA. These are more formally known as Project Management Tools or Project Trackers. They follow the Kanban , style of list making.

        • In this Application will build a Project Tracker. It has three zones, Requests, In Progress and Done. The application will allow you to enter new projects and give you the ability to drag then to the other zones as the projects progress to completion. The individual projects (tasks) will be date/time stamped and there will be trash zone to delete projects.

        • Extra Features

          • Database implementation so that the projects are saved and loaded from a local SQLite database... TrackerSqlService

          • SignalR integration to allow multiple users to see the same project board in real time. When one user makes a change, all other users see the update immediately without refreshing... ChatHub.cs (TrackerSendMessage)

          • AI Board Review — a Microsoft Agent Framework Group Chat Workflow analyses the live board state. Three specialist agents — BoardAnalyst, PriorityAdvisor, and ProjectCoach — collaborate to surface bottlenecks, flag stale tasks, and deliver actionable recommendations.

        • The core challenge was that the standard HTML5 native drag API (draggable, ondragstart, ondragover, ondrop) is not supported inside the WebView embedded in .NET MAUI on Android and iOS. The events simply never fire, so no cursor change, no ghost image, and no drop is detected. The solution was to replace the entire drag mechanism with Javascript coding so that it works on every platform (mouse, touch, stylus). For all the coding details click the Programming Details button below.

  • Authentication and Authorization in a .NET MAUI Blazor Hybrid Application53:44

    In this Lecture we will

    • Learn that unlike a Blazor Web App (which has an option in the project template to enable authentication), a Blazor Hybrid app has no server-side authentication middleware. Instead you must implement a custom AuthenticationStateProvider that feeds the current identity into Blazor's built-in authorization system (<AuthorizeView>, [Authorize], AuthenticationState).

    • As a point of comparison Review creating a simple Blazor Web App Authentication application

      • Authentication type ... Individual Accounts ... Interactive Render Mode Server

      • Prebuilt Navmenu options ... Auth Required/Register/Login

      • Note pre-made folders ... Data/Account (Pages/Shared/Passkey files) and more

    • Recall Biometric Authentication from Lecture 203 (not the focus of this lecture) ... see HyPh-2026NK

      • We used the Plugin.Maui.Biometric Nuget Package to add Biometric Authentication to the Blazor Hybrid App.

      • The Biometric Authentication was only for the Android Emulator

        • You needed to add the Biometric permission (USE_BIOMETRIC) in the AndroidManifest.xml file

        • Almost all of the coding was in the MainPage.xaml (BlazorWebView initially invisible) and MainPage.xaml.cs files (most of the coding was here where PIN verification was performed)

    • Learn that the Windows Identity provider was replaced with a fully self-contained local user login system.

      • We define a UserAccount.cs model that represents a user record with username, password hash, salt, and roles.

      • The UserService.cs class manages all user operations including credential validation, user creation, updates, and deletion.

        • passwords are hashed with SHA-256 + random salt, and roles control what each user can access inside the app.

      • Users are stored in a JSON file on the device (acceptable only for a demo — for anything serious, use a secure backend like SQLite),

        • _userFilePath = Users\chiar\AppData\Local\UserName\com.companyname.mauiapp1\Data\users.json

        • Note json file will look like this:

          [

            {

              "Username": "admin",

              "PasswordHash": "b3w5Z2c1rlKw/g8W0CVKvVf6DZn2bzxTXI8FcqfHxh4=",

              "Salt": "BU64swU7jdyb5cB5Qdg1Og==",

              "Roles": [

                "Admin",

                "Manager",

                "User"

              ]

              }

    • The Steps (MAFv1BzHybAuth)

      • Step 1 — Add the NuGet Package ... Microsoft.AspNetCore.Components.Authorization

        • This package supplies AuthenticationStateProvider, AuthenticationState, CascadingAuthenticationState, AuthorizeRouteView, AuthorizeView, and the [Authorize] attribute.

      • Step 2 — Auth/UserAccount.cs (Model)

        • A simple model that represents one user record stored to disk
          public class UserAccount

          {

              public string Username     { get; set; } = string.Empty;

              public string PasswordHash { get; set; } = string.Empty;

              public string Salt         { get; set; } = string.Empty;

              public List<string> Roles  { get; set; } = new();

          }

      • Step 3 — Auth/UserService.cs (Data + Hashing)

        • A singleton service that manages all users including validating credentials, adding new users, and updating or deleting existing users.

          • This service is referenced(injected) in the Login.razor component to validate user credentials and manage user accounts.
            var user = UserService.ValidateCredentials(_username, _password);

          • It is also injected in the Register.razor component to add new user accounts.
            var success = UserService.AddUser(_username, _password, _selectedRoles);

          • This service is called directly in the ManageUsers.razor component to update and delete user accounts.
            var success = UserService.UpdateUser(user.Username, user.Roles, newPassword);
            var success = UserService.DeleteUser(user.Username);

          Key Points:

          • Available Roles are defined as a readonly array: public static readonly string[] AvailableRoles = { "Admin", "Manager", "User" };

          • Policies (AdminOnly/ManagerOnly) are defined in MauiProgram.cs ... See Step 5
            Policies are a layer above roles — they group one or more roles under a single named rule

          • Users are persisted to users.json inside FileSystem.AppDataDirectory (sandboxed, per-device storage)
            _userFilePath = Users\chiar\AppData\Local\UserName\com.companyname.mauiapp1\Data\users.json

          • Each user gets a unique random 16-byte salt generated via RandomNumberGenerator.GetBytes().

          • The stored PassworHash is SHA256(password + salt) — raw passwords are never saved. SHA256 is a widely used cryptographic hash function that produces a 256-bit (32-byte) hash value, which is commonly used for securely storing passwords

            • At Registration (AddUser):
              "mypassword" + "xK9mZ2..." (random salt) → SHA256 → "abc123..." (stored PasswordHash)
              At Login (ValidateCredentials):
              "mypassword" + "xK9mZ2..." (same salt retrieved from file) → SHA256 → "abc123..."

            • This does NOT decrypt or re-form the original password — hashing is a ONE-WAY function. Instead, it relies on hashing being DETERMINISTIC: the same input always produces the same output.

          • A default admin / admin123 account is seeded automatically on first run.

          // Password hashing

          private static string HashPassword(string password, string salt)

          {

              var combined = Encoding.UTF8.GetBytes(password + salt);

              var hash = SHA256.HashData(combined);

              return Convert.ToBase64String(hash);

          }

          // Credential validation

          public UserAccount? ValidateCredentials(string username, string password)

          {

              var user = _users.FirstOrDefault(u =>

                  u.Username.Equals(username,

                      StringComparison.OrdinalIgnoreCase));

              if (user is null) return null;

              return HashPassword(password, user.Salt) == user.PasswordHash

                  ? user : null;

          }

      • Step 4 — Create Auth/LocalAuthStateProvider.cs (Active Provider)

        • Replaces WindowsAuthStateProvider (kept for historical reference only). Implements AuthenticationStateProvider with two methods:

          public void SignIn(UserAccount user)

          {

              var claims = new List<Claim>

              {

                  new(ClaimTypes.Name, user.Username)

              };

              foreach (var role in user.Roles)

                  claims.Add(new Claim(ClaimTypes.Role, role));


              var identity = new ClaimsIdentity(claims, "LocalAuth");

              _currentUser = new ClaimsPrincipal(identity);

              NotifyAuthenticationStateChanged(

                  GetAuthenticationStateAsync());
          }

          public void SignOut()

          {

              _currentUser = new ClaimsPrincipal(new ClaimsIdentity());

              NotifyAuthenticationStateChanged(

                  GetAuthenticationStateAsync());

          }

          Calling NotifyAuthenticationStateChanged() is critical — it tells Blazor to re-evaluate all <AuthorizeView> components and [Authorize] pages immediately.

      • Step 5 — Register Services in MauiProgram.cs

        • Three key registrations were added or changed:

          // 1. Authorization Policies

          builder.Services.AddAuthorizationCore(options =>

          {

              options.AddPolicy("AdminOnly",

                  p => p.RequireRole("Admin"));

              options.AddPolicy("ManagerOnly",

                  p => p.RequireRole("Admin", "Manager"));

          });


          // 2. UserService as singleton (one instance, shared everywhere)

          builder.Services.AddSingleton<UserService>();


          // 3. Register LocalAuthStateProvider under BOTH its own type

          //    AND the base AuthenticationStateProvider type so that

          //    components injecting either type get the same instance

          builder.Services.AddSingleton<LocalAuthStateProvider>();

          builder.Services.AddSingleton<AuthenticationStateProvider>(sp =>

              sp.GetRequiredService<LocalAuthStateProvider>());

        • The double registration pattern ensures that @inject LocalAuthStateProvider (needed to call SignIn/SignOut) and @inject AuthenticationStateProvider (used internally by Blazor's auth system) both resolve to the same singleton.

        • Note: Policies are a layer above Roles — they group one or more roles under a single named rule. So the policy "ManagerOnly" means: allow access if the user has the Admin OR Manager role. The roles (Admin, Manager, User) are still the same — the policy is just a convenient named wrapper over a role check that you can reuse across pages with [Authorize(Policy = "ManagerOnly")].

      • Step 6 — Update _Imports.razor

        • @using Microsoft.AspNetCore.Components.Authorization

          @using Microsoft.AspNetCore.Authorization

          @using MauiApp1.Auth

        • These global usings make <AuthorizeView>, AuthenticationState, [Authorize], UserService, and LocalAuthStateProvider available in every .razor file without per-file imports.

      • Step 7 — Update Routes.razor

        • <CascadingAuthenticationState>

              <Router AppAssembly="typeof(MauiProgram).Assembly"

                      NotFoundPage="typeof(Pages.NotFound)">

                  <Found Context="routeData">

                      <AuthorizeRouteView

                          RouteData="routeData"

                          DefaultLayout="typeof(Layout.MainLayout)">

                          <NotAuthorized>

                              <p>You are not authorized.</p>

                          </NotAuthorized>

                      </AuthorizeRouteView>

                      <FocusOnNavigate RouteData="routeData"

                                       Selector="h1" />

                  </Found>

              </Router>

          </CascadingAuthenticationState>

        • <CascadingAuthenticationState> — cascades the AuthenticationState down the component tree so all child components can access it.

        • <AuthorizeRouteView> — replaces <RouteView> and enforces [Authorize] attributes on pages.

      • Step 8a — Components/Pages/Login.razor

        • A login form at route /login. On submit it:

          • Calls UserService.ValidateCredentials() to check the hash.

          • Calls AuthProvider.SignIn(user) to update the auth state.

          • Uses NavigationManager.NavigateTo("/") to redirect home.

          • No [Authorize] attribute — this page must be publicly accessible.

      • Step 8b — Components/Pages/Register.razor

        • A registration form at route /register that allows new users to create an account. On submit it:

          • Calls UserService.AddUser() to hash the password and persist the new user... check if user name already taken

          • Calls UserService.ValidateCredentials() to verify the username and password during login.

          • Calls AuthProvider.SignIn(user) to immediately authenticate the newly registered user.

          • Uses NavigationManager.NavigateTo("/") to redirect home.

          • No [Authorize] attribute — this page must be publicly accessible. It is the entry point for first-time users and is especially important in a local/offline MAUI context where there is no external identity provider.

      • Step 9 — Components/Pages/ManageUsers.razor

        • Protected with @attribute [Authorize(Policy = "AdminOnly")]. Provides a full CRUD UI:

          • Add — username + password + role checkboxes.

          • Edit — change roles or reset password (leave blank to keep current).

          • Delete — removes user from the JSON store.

        • All changes are written back to users.json immediately via UserService.

      • Step 10 — Components/RedirectToLogin.razor

        • A tiny component that calls NavigationManager.NavigateTo("/login") from OnInitialized. It is placed in the <NotAuthorized> template of Routes.razor to auto-redirect unauthenticated visitors:

          <NotAuthorized>

              @if (!context.User.Identity?.IsAuthenticated ?? true)

              {

                  <RedirectToLogin />   

              }

              else

              {

                  <p>Access Denied</p> 

              }

          </NotAuthorized>

      • Step 11 — NavMenu.razor Updates

        • Every nav link is wrapped in an <AuthorizeView> block so the menu changes dynamically the moment the user signs in or out — no page reload required.

          What an unauthenticated user sees

          Until authenticated, the Counter and Weather pages are completely hidden from the navigation menu. The only options shown are:

          • Home — always visible (marked @attribute [AllowAnonymous]).

          • Sign In — navigates to /login.

          • Register — navigates to /register to create a new account.

          <AuthorizeView>

              <Authorized>

                  <!-- Counter and Weather links only rendered when signed in -->

                  <NavLink href="counter">Counter</NavLink>

                  <NavLink href="weather">Weather</NavLink>

              </Authorized>

              <NotAuthorized>

                  <!-- Only Sign In and Register are shown to guests -->

                  <NavLink href="/login">Sign In</NavLink>

                  <NavLink href="/register">Register</NavLink>

              </NotAuthorized>

          </AuthorizeView>

          What an authenticated user sees

          Once signed in, the menu changes to show:

          • Counter ... All authenticated users can see and access this page.

            @attribute [Authorize]

          • Weather — ... All authenticated users can see the link to the Weather page BUT actual page contents are only visible to users with the "Manager" role: ie Admin and Manager users can see Weather page, but regular User role cannot see it

            @attribute [Authorize(Policy = "ManagerOnly")]

          • Manage Users — only visible to users with the Admin role, because it is wrapped in <AuthorizeView Policy="AdminOnly">.
            ... and as added extra security measure the actual ManageUsers.razor page is also protected with

            @attribute [Authorize(Policy = "AdminOnly")]

          • Sign Out (@context.User.Identity?.Name) — displays the logged-in username

          <AuthorizeView Policy="AdminOnly">

              <NavLink href="manage-users">Manage Users</NavLink>

          </AuthorizeView>


          <AuthorizeView>

              <Authorized>

                  <button @onclick="SignOut">

                      Sign Out (@context.User.Identity?.Name)

                  </button>

              </Authorized>

          </AuthorizeView>

        • How Sign Out works

          The Sign Out button calls the SignOut method which in turn calls AuthProvider.SignOut() where AuthProvider is an instance of the the injected LocalAuthStateProvider, it replaces the current ClaimsPrincipal with an empty (unauthenticated) identity and fires NotifyAuthenticationStateChanged(). Blazor immediately re-evaluates all <AuthorizeView> blocks across the entire component tree, so the menu reverts to showing only Sign In and Register without any page reload.

    • Demonstrate Authentication using Auth0 (BzHybAuth0)

      • In this application we implement authentication and authorization in a .NET MAUI Blazor Hybrid app using Auth0.

      • What is Auth0?

        • Overview

          Auth0 is a cloud-based Identity and Access Management (IAM) platform that provides developers with the building blocks to safely and quickly add authentication and authorization to their applications. It eliminates the need to build and maintain complex security infrastructure from scratch.

        • ⚙️ Core Features

          • Authentication (Who you are)

            Handles user sign-ups, logins, and Single Sign-On (SSO). Supports standard credentials, social logins (e.g., Google, Apple), and passwordless/biometric options like passkeys.

            • Note: SSO and Social Login are a little different, although they both save you from creating new passwords.

              • Social Login: Allows you to use an existing account (like Google, Apple, or Facebook) to easily sign up for or log in to a specific third-party app or website.

              • SSO (Single Sign-On): Lets you log in once and automatically gain access to multiple interconnected applications without needing to re-enter your credentials for each one

          • Authorization (What you can do)

            Manages user permissions, ensuring users only access the data or services they are allowed to.

          • Universal Login

            Provides a pre-built, centralized, and customizable login page that adapts to web, mobile, and legacy applications.

          • Security & MFA

            Protects against credential stuffing and brute-force attacks by offering Multi-Factor Authentication (MFA) and anomaly detection.

          • Why Developers Use It

            Building a secure login system involves database management, encryption, token handling, and compliance. Auth0 abstracts this away via APIs and SDKs, reducing identity-related development and maintenance.

      • This app features a custom AuthenticationStateProvider called Auth0AuthenicationStateProvider (recall LocalAuthStateProvider from previous Blazor Hybrid Authentication project) that integrates Auth0's OIDC (OpenID Connect) client with Blazor's built-in authorization system.

        • Users can log in via Auth0's hosted login page, and their authentication state is reflected in the Blazor UI using AuthorizeView and route protection.

        • User claims (like name and profile picture) are displayed on the Home page after login.

        • We have previously used Auth0 in a basic Blazor Web App (Lectures 130-131)

      • Authentication with Auth0 Deeper Dive (link to Website Guide)

      • Coding Details

        • Auth0 Setup

          • Sign Up for Account Auth0

          • Register a Native application in the Auth0 dashboard (+ Create Application)

          • In the Applications Menu ... Set Allowed Callback URLs and Allowed Logout URLs to: myapp://callback/

          • Note your Domain and Client ID for use in MauiProgram.cs

          • Note in the Connections Menu which identity providers are enabled (by default Google). To add more, go to Authentication/Social and Create a new Connection

          • There are other Advanced Settings available in the Auth0 dashboard for fine-tuning your application's authentication and authorization behavior (additional costs may apply) ie Multi-Factor Authentication (under Security)

        • NuGet Packages added

          • Auth0.OidcClient.MAUI — Auth0 OIDC (OpenID Connect) client for MAUI

          • Microsoft.AspNetCore.Components.Authorization — Blazor authorization components

          • Duende.IdentityModel.OidcClient — Provides IBrowser, BrowserResult types (used in MauiWebViewBrowser.cs file to display the Auth0 Login Page with a custom WebView)

        • Project Structure (Some Key Files)

          • Components/Layout/MainLayout.razor — App shell with LoginButton displayed on top right corner of the page

          • Components/Pages/LoginButton.razor — Login/Logout UI component

          • Auth0AuthenticationStateProvider.cs — Custom Blazor authentication bridge ... Similar to LocalAuthenticationStateProvider used in companion Blazor Hybrid Demo

          • MauiProgram.cs — App startup, service registration

          • MauiWebViewBrowser.cs — Custom Auth0 IBrowser implementation ... It uses a Maui WebView to display the authentication page.

          • Auth0LoginPage.xaml/.cs — MAUI ContentPage hosting the Auth0 login WebView

          • Platforms/Windows/Package.appxmanifest — MSIX manifest with myapp protocol ... see more details below (Windows Packaging)

        • Blazor UI Protection

          • Routes.razor uses <AuthorizeRouteView> to protect all routed pages

          • All the Pages (except Home.razor) use @attribute [Authorize] to restrict access to authenticated users

          • The Weather.razor page is not visible in the NavMenu until the user is authenticated

          • Home.razor uses <AuthorizeView> to show personalized content while keeping buttons accessible to all users

          • LoginButton.razor uses <AuthorizeView> to toggle between Log in / Log out

        • Auth0AuthenticationStateProvider ... The heart of the Authentication System in this application

          • This class bridges MAUI authentication and Blazor's authorization system. It extends AuthenticationStateProvider and:

            • Calls auth0Client.LoginAsync() to authenticate via Auth0

            • Uses NotifyAuthenticationStateChanged() to update Blazor's UI reactively

            • Calls auth0Client.LogoutAsync() and clears the user on logout

        • User Profile  (Home.razor)

          • After login, user claims are available via AuthenticationStateProvider.GetAuthenticationStateAsync(). The name and picture claims are extracted and displayed on the Home page. We also capture the authentication provider (e.g., Google, Facebook) from the sub claim or the authentication type as a fallback, and display it on the Home page as well.

        • WebSiteViewerCC

          • A reusable MAUI ContentPage (WebSiteViewerCC.xaml) that opens any URL in an embedded WebView (.NET MAUI Control) — bypassing iframe restrictions common on websites. Used on the Home.razor page to link to external website resources related to Auth0 and OIDC.
            Called via:

            await App.Current.MainPage.Navigation.PushModalAsync(new Pages.WebSiteViewerCC(url));

        • Custom Browser - MauiWebViewBrowser

          • By default, Auth0's login opens a system browser popup. To avoid this, a custom IBrowser implementation (MauiWebViewBrowser) was created. It is similar to the WebSiteViewerCC implementation created above.

          • The main reason we use this class is so that we can use a Maui WebView to handle the authentication flow with Auth0

            • ... ie display the Auth0 login page within our app instead of opening an external browser.

            • ... this allows us to stay consistent with how we display things like links in the app, and to avoid opening an external browser which can be a jarring experience for users.

            • Implements Duende.IdentityModel.OidcClient.Browser.IBrowser

            • Opens Auth0LoginPage modally — a MAUI ContentPage with a WebView (.NET MAUI Control)

            • Intercepts the myapp://callback/ redirect in the Navigating event

            • Completes the OIDC flow by resolving a TaskCompletionSource<BrowserResult>

            • Registered via Browser = new MauiWebViewBrowser() in Auth0Client options

        • Windows Packaging ... Here's where is gets tricky specifically for the Windows platform

          • The WebAuthenticator API requires a packaged Windows app. The following changes were made:

            • Removed <WindowsPackageType>None</WindowsPackageType> from the .csproj

            • Added the myapp URI protocol to Platforms/Windows/Package.appxmanifest:

              <uap:Extension Category="windows.protocol">

                <uap:Protocol Name="myapp">

                  <uap:DisplayName>MauiAuth0App</uap:DisplayName>

                </uap:Protocol>

              </uap:Extension>

            • In the Platforms/Windows folder updated the App.xaml.cs file ... public App()
              if (Auth0.OidcClient.Platforms.Windows.Activator.Default.CheckRedirectionActivation()) return;

            • Added a launchSettings.json profile using "commandName": "MsixPackage"

            • In Visual Studio, enable Deploy in Configuration Manager for the Windows target

      • AI Implementation Prompt (Experimental)

        • A detailed sample AI prompt that could be used to create a new .NET MAUI Blazor Hybrid Application that implements Authentication using Auth0


  • An Introduction to AG-UI (Agent User Interaction Protocol) with MAF56:23

    In this Lecture we will

    • Learn that the AG-UI (Agent User Interaction  Protocol) is an open protocol that standardises how your  AI agent backend communicates with your user interface frontend via a stream of typed events (SSE Server Sent Events) that include events like thinking steps, tool calls, state changes and text output. It acts as the bridge connecting the smarts of MAF to the web or mobile app the user is looking at. Think of it as the dashboard display.

    • Why AG-UI?

      • AG-UI was created out of necessity, not theory. As AI agents move from background automation to in-app interaction, developers need a consistent, reliable way to bridge agent intelligence with real-time user experiences. AG-UI directly addresses this emerging need.

      • The Emerging Open Protocol Stack for AI Agents

        • Three complementary open protocols now cover every communication boundary in a modern agentic system:

        • MCP — Tools talk to Agents

          The Model Context Protocol standardises how external tools, APIs, and data sources expose themselves to an agent. The agent calls a tool; the tool returns structured results — no custom glue code per integration.

        • A2A — Agents talk to Agents

          The Agent-to-Agent Protocol lets one agent delegate tasks to another — spawning sub-agents, routing specialised work, and composing multi-agent pipelines — all over a standard HTTP contract.

        • AG-UI — Agents talk to Users

          AG-UI is the missing third layer — it standardises how an agent streams its live state (thinking steps, tool calls, text, state patches) to any UI in real time. Where MCP and A2A stay inside the backend, AG-UI faces the human.

      • AG-UI was built to solve these pain points:

        • Streaming Responses
          LLMs don’t respond in one go, they stream token by token. AG-UI’s message.delta events allow frontends to mirror this output in real time.

        • Tool & API Orchestration
          Interactive agents often pause execution to fetch data or await input. AG-UI models this natively through structured tool_call / tool_result events.

        • State Management at Scale
          Agents working with large objects (e.g., a full codebase or UI state) can’t re-send everything. AG-UI supports state.patch events to send just the changes.

        • Concurrency and Control
          Users switch tabs, cancel operations, or issue overlapping commands. AG-UI includes control primitives to manage these edge cases cleanly.

        • Security and Enterprise Readiness
          With built-in support for CORS (Cross-Origin Resource Sharing  a browser security mechanism that allows a web server to tell your browser which websites are permitted to access its data), auth headers, and auditing, AG-UI is ready for secure, production-grade deployments.

        • Framework-Agnostic Integration
          Every agent framework, from LangGraph to CrewAI to MAF , has its own interface. AG-UI creates a unified frontend contract, making agent UIs easier to build and maintain.

    • How AG-UI Works: Event Flow in Action

      • At its core, AG-UI defines a bidirectional event-driven pipeline between your frontend UI and the agent backend. This design enables seamless, low-latency interaction without the need for custom APIs or manual polling.

        Here’s a breakdown of the interaction cycle:

        Request Initiation
        The client initiates an interaction by sending a POST request to the agent’s endpoint. This request can include user input, context, and metadata required to start the agent run.

        Unified Event Stream Subscription
        After the request is made, the client establishes a persistent connection, typically via Server-Sent Events (SSE) or an alternative like WebSockets. This stream becomes the single channel through which all agent-side events are pushed.

        Event Format
        Every event emitted by the agent contains:

        • A standardized type (e.g., message.delta, tool_call, state.patch)

        • A compact, schema-aligned payload specific to the event type

        Real-Time Agent Emission
        As the agent processes input, it emits events in real time. These can include streamed messages, tool execution requests, state diffs, or completion signals, each pushed instantly to the frontend.

        Frontend Responsiveness
        The frontend reacts to these events as they arrive, updating the UI, visualizing partial output, or pausing for user confirmation, without waiting for the agent run to finish.

        Bi-Directional Context Updates
        The client (frontend) can also send structured updates, such as UI context, user actions, or cancel signals — back to the agent via input events. This enables live feedback loops and interactive control flows.

        This real-time, event-loop architecture is what allows AG-UI to support streaming UIs, agent pauses, tool calls, and fine-grained control, all through a single, extensible interface.

    • Shifting Focus: From Backend Automation to Interactive Agents

      • While these backend tasks have significantly optimized operational efficiency, the real next frontier is interactive agents, those that work in tandem with users. The challenge? Developing agents that don’t just complete tasks autonomously but engage in real-time interactions, responding to user input, updating the interface dynamically, and adapting to changing contexts.

        This interactive layer is where AG-UI steps in, providing the event-driven framework necessary for building agents that respond instantaneously and in a structured manner, supporting concurrent user queries, dynamic feedback, and real-time system updates.

      • Why Real-Time Collaboration Matters

        For many real-world use cases, especially in complex coding environments, agents are most effective when they can work alongside users. This collaborative approach brings several key benefits:

        • Transparency: Developers can track the agent’s actions and gain insights into how it’s arriving at results, enabling them to provide more informed feedback.

        • Co-Creation: Developers and AI agents can build together, refining code in real time. This approach is crucial for evolving code in a fast-paced development cycle.

        • Flexibility: With interactive agents, users can rapidly iterate, adjusting inputs as they go and seeing the effects immediately. This is a stark contrast to purely automated agents, where changes often require starting over or dealing with a less flexible workflow.

    • ... and finally after the theory we look at two simple applications related to Travel Planning

      • MAFv1ByHybAGUI (.NET MAUI Blazor Hybrid App)

        • How the demo works:

          1. Enter a travel destination and click Start Agent Run.

          2. A Researcher Agent analyses the destination. Watch the AG-UI Event Stream panel below to see each event appear in real time (RUN_STARTED, THINKING, TOOL_CALL, TEXT_CHUNK, RUN_FINISHED).

          3. Once the Researcher finishes you are asked to approve its output before the second agent runs — this is the Human-in-the-Loop (HITL) pattern.

          4. If you approve, a Planner Agent generates a 3-day itinerary, again streaming its events to the panel.

        • Solution Notes:

          • Why AG-UI + MAF

            • Problem 1 — The "Black Box" Effect

              • Without a proper UI, users just see a spinning loader while the AI thinks in the background. If it fails, you don't know why. There is no visibility into what the agent is actually doing.

              • AG-UI streams execution events in real time. It visually shows you what the AI is thinking (THINKING), which agent is active (RUN_STARTED), and which tools it is currently using (TOOL_CALL_START). The event stream panel on this page is that solution.

            • Problem 2 — Complex Multi-Step Workflows

              • When multiple agents hand off tasks to each other, a simple chat window completely breaks down. Which agent is talking? What stage are we at? Did the handoff succeed?

              • AG-UI allows frontends to display live status updates, tracking the exact flow as specialised agents (e.g., a Researcher Agent handing off to a Planner Agent) do their jobs. STATE_DELTA events carry structured state updates between steps.

            • Problem 3 — Safe Human-in-the-Loop Actions

              • Letting an AI agent take actions on your behalf — like booking a flight or issuing a refund — without human checks is dangerous. There is no standard way to pause and ask.

              • AG-UI allows agents to pause and emit HUMAN_INPUT_REQUIRED. The UI can pop up interactive elements (like an Approve / Reject panel) so the human remains safely in control. This demo shows exactly this pattern between the Researcher and Planner agents.

            • Problem 4 — Generative & Dynamic UI

              • Users are restricted to simple text bubbles. The agent can only describe things — it can't render an actionable component. Text output is a one-size-fits-all solution.

              • Rather than just talking about a flight, the agent can trigger the frontend to render an actual UI component — a date-picker, seat map, or colour-picker. This demo shows this with change_heading_color and change_background_color tools that mutate Blazor state directly from the agent.

          • Special Programming Note: In-Process vs Server Based Solution

            • If we were using a Server Based Application (See next demo below)  we would use the

              • Microsoft.Agent.AI.AGUI NuGet package to implement AG-UI in a Blazor Application. It is the primary Client-Side library for doing so.

                • It provides the AGUIChatClient which handles the heavy lifting of HTTP communication, SSE event parsing, and state management.

              • ... On the Server Side we would use Microsoft.Agents.AI.Hosting.AGUI.AspNetCore (Server-side): This is installed in your ASP.NET Core Web API. It provides the MapAGUIAgent() extension method to easily expose your AI agents as AG-UI-compliant endpoints

            • BUT, we are running a completely serverless, offline .NET MAUI Blazor Hybrid app where the AI agent runs entirely locally on the device (e.g., using a local model with Microsoft Agent Framework or Semantic Kernel), you do not need the Microsoft.Agents.AI.AGUI package at all.

            • Instead of using the AG-UI package, we leverage native .NET memory structures to get the exact same "Generative UI" and streaming text experience without needing a separate backend endpoint.

          • Coding Highlights

            • The agent lives inside the Blazor component (AgentAGUIIntro.razor). The OpenAI API is called directly. No HTTP server is involved. No separate service classes, no dependency-injected interfaces, no extra files. Everything — UI markup, state, agent creation, tool registration, and streaming logic — lives inside AgentAGUIIntro.razor. This makes the AG-UI patterns easy to follow without jumping between files.

            • Record: AgUiEvent

              Declared at the top of @code as a positional record: This is the single unit of data appended to LIST of agUiEvents each time an event fires:

              private record AgUiEvent(

              string EventType,

              string Content,

              string BadgeClass,

              DateTime Time);

              Each entry in the AG-UI Event Stream table is one AgUiEvent. The list agUiEvents is bound directly to the table rows in markup.

            • Method: EmitEvent()

              private async Task EmitEvent(

              string eventType, string content,

              string badgeClass, int delayMs = 350)

              {

              agUiEvents.Add(new AgUiEvent(...));

              StateHasChanged();

              await Task.Delay(delayMs);

              }

              This is the core streaming simulation helper. All AG-UI events flow through this one helper. The delay creates the visible "trickle" effect that mimics real SSE streaming.

            • The demo uses two specialised agents orchestrated in sequence: (Manually Orchestrated Sequential Workflow)

              • Researcher Agent — gathers key facts about the destination (climate, culture, safety, entry requirements).

              • Planner Agent — takes the approved research and produces a structured 3-day travel itinerary.

              In a real AG-UI deployment each agent would run in its own ASP.NET Core hosted service and the Blazor client would consume the SSE stream from /api/agent/run.

            • The Researcher Agent is equipped with two AIFunction tools registered via AIFunctionFactory.Create:

              • one tool sets the CSS colour of the page heading in real time ... References the method ChangeHeadingColor

              • the other tool sets the CSS background colour of the page card ... References the method ChangeBackgroundColor

              When the agent calls one of these tools, a real TOOL_CALL_START / TOOL_CALL_END event pair appears in the AG-UI Event Stream panel and the page re-renders immediately. Try asking: "Research Ireland and make the heading green".

            • Method: StartAgentRun()

              Orchestrates the Researcher Agent run:

              1. Emits simulated RUN_STARTED, THINKING, and TOOL_CALL_START events.

              2. Creates an OpenAIClient, gets a ChatClient, and registers uiTools via AIFunctionFactory.Create.

              3. Calls chatClient.AsAIAgent(name, instructions, tools) — the MAF extension method that wraps the OpenAI client as an AIAgent.

              4. Calls researchAgent.RunStreamingAsync(messages) and iterates the AgentResponseUpdate stream.

              5. Switches on AIContent subtypes (TextContent, FunctionCallContent, FunctionResultContent) to emit real AG-UI events.

              6. Calls updates.ToAgentResponse().Text to extract the final text after streaming ends.

              7. Sets awaitingApproval = true to trigger the HITL approval panel.

            • Method: ApproveAndPlan()

              Runs when the user clicks Approve & Run Planner Agent — the HITL handoff. Creates a second Planner Agent (no UI tools needed here — it just writes text) and calls plannerAgent.RunAsync() with the approved research as context. The result is word-by-word chunked via EmitEvent("TEXT_CHUNK") to simulate streaming, then stored in itinerary which the markup renders automatically.

            • Tool Methods: ChangeHeadingColor & ChangeBackgroundColor

              private async Task ChangeHeadingColor(string color)

              {

              headingColor = color;

              await InvokeAsync(StateHasChanged);

              }

              private async Task ChangeBackgroundColor(string color)

              {

              bgColor = color;

              await InvokeAsync(StateHasChanged);

              }

              • These are plain C# instance methods — not manually called from Blazor. They are wrapped by AIFunctionFactory.Create(...) and handed to the agent via the tools: parameter. When the LLM decides to call change_heading_color, MAF invokes the delegate automatically during RunStreamingAsync. InvokeAsync(StateHasChanged) is used instead of plain StateHasChanged() because the MAF streaming loop may run on a background thread — InvokeAsync safely marshals the re-render call back to the Blazor/MAUI UI thread.

      • BlazorWebAppAGUI  (Blazor Web App Render Mode WebAssembly - Server and Client)

        • In this version we implement  REAL streaming agents, Human-in-the-Loop approval, and live UI state updates.

        • Two modes

          • Mode 1
            Runs as an In-Process Agent Host  inside the Blazor Server page AgentAGUIIntro.razor (InteractiveServer component) . The server registers agents with AddAIAgent() and runs them in-process. The Blazor app communicates with the agent directly without any SSE endpoints. This implementation is almost identical to the .NET MAUI Blazor Hybrid approach.

            • Here the component Is the agent host — it constructs agents, attaches tools, and streams responses in-process.

            • It demonstrates how AG-UI events are produced: every RUN_STARTED, THINKING, TOOL_CALL, TEXT_CHUNK and RUN_FINISHED event originates here, inside the same Blazor circuit.

              • RUN_STARTED The agent run has begun.

              • THINKING The agent is reasoning internally.

              • TOOL_CALL_START The agent is calling a tool.

              • TOOL_CALL_END The tool call returned a result.

              • TEXT_CHUNK A streamed fragment of the agent's text response.

              • STATE_DELTA A portion of the agent's shared state was updated.

              • HUMAN_INPUT_REQUIRED The agent requires human input to proceed.

              • RUN_FINISHEDThe agent run completed successfully.

            • Coding Highlights (very similar to .NET MAUI Blazor Hybrid App)

              • Microsoft.Agents.AI but not Microsoft.Agents.AI.AGUI

              • Record: AgUiEvent ... every entry in the AG-UI event stream is one AgUiEvent.

              • Method: EmitEvent() ... the streaming engine (every AG-UI event flows through this helper)

              • Two function tools declared

                • var uiTools = new AIFunction[]

                  {

                  AIFunctionFactory.Create(ChangeHeadingColor,

                  name: "change_heading_color",

                  description: "Changes the colour of the page heading. Pass any valid CSS colour string, e.g. 'red', '#0d6efd', 'darkgreen'."),

                  AIFunctionFactory.Create(ChangeBackgroundColor,

                  name: "change_background_color",

                  description: "Changes the CSS background colour of the page card. Pass any valid CSS colour string, e.g. 'lightyellow', '#e8f5e9', 'aliceblue'.")

                  };

                • private async Task ChangeHeadingColor(string color)

                  {

                  headingColor = color;

                  await InvokeAsync(StateHasChanged);

                  }


                  private async Task ChangeBackgroundColor(string color)

                  {

                  bgColor = color;

                  await InvokeAsync(StateHasChanged);

                  }

              • This page creates its own in-process agent instances and calls RunStreamingAsync directly — no HTTP hop, no SSE endpoint involved (that would be Mode 2). The full streaming pipeline runs inside the same SignalR circuit as the UI.

              • Method: StartAgentRun()

                Orchestrates the Researcher Agent run, using everything defined above:

                1. Emits simulated RUN_STARTED, THINKING, and TOOL_CALL_START events.

                2. Creates an OpenAIClient, gets a ChatClient, and registers uiTools via AIFunctionFactory.Create.

                3. Calls chatClient.AsAIAgent(name, instructions, tools) — the MAF extension method that wraps the OpenAI client as an AIAgent.

                  1. var client = new OpenAIClient(apiKey)

                    .GetChatClient(model);

                    var agent = client.AsAIAgent(

                    name: "ResearcherAgent",

                    instructions: "...",

                    tools: uiTools);   // ← live UI tools

                4. Calls researchAgent.RunStreamingAsync(messages) and iterates the AgentResponseUpdate stream.

                  1. await foreach (var update in

                  agent.RunStreamingAsync(messages))

                5. Switches on AIContent subtypes (TextContent, FunctionCallContent, FunctionResultContent) to emit real AG-UI events.

                6. Calls updates.ToAgentResponse().Text to extract the final text after streaming ends.

                7. Sets awaitingApproval = true to trigger the HITL approval panel.

              • Method: ApproveAndPlan()

                Runs when the user clicks Approve & Run Planner Agent — the HITL handoff. Creates a second Planner Agent (no UI tools needed here — it just writes text) and calls plannerAgent.RunAsync() with the approved research as context. The result is word-by-word chunked via EmitEvent("TEXT_CHUNK") to simulate streaming, then stored in itinerary which the markup renders automatically.

          • Mode 2 :
            Runs on a Server (via Program.cs setup) , the Server Component Page (AgentAGUIClient.razor) is just a streaming consumer of it's events.
            This  is a completely REAL implementation of the AG-UI protocol  using the Microsoft Agent Framework (MAF) and AGUIChatClient. (Microsoft.Agents.AI.AGUI NuGet Package)

            • It acts as a Remote SSE Consumer (Server Sent Events). SSE  is a web technology and transport protocol that allows an AI agent's backend to continuously stream real-time data, text tokens, and status updates to your user interface over a single, persistent HTTP connection.

            • The .NET client creates an AGUIChatClient which includes a reference to a  plain HttpClient and points at one of the remote AG-UI SSE endpoints. There is no OpenAI key or In-Process agent construction here (we do a lot of the setup work in Program.cs) — the agents runs entirely on the server

            • Coding Highlights

              • Client Page ( AgentAGUIClient.razor)

                • Microsoft.Agents.AI.AGUI

                  • Provides AGUIChatClient for connecting to the external AG-UI SSE server (Program.cs) . Used by the client side of the protocol.

                • Microsoft.Extensions.AI

                  • Shared abstractions: ChatRole, ChatMessage, TextContent, FunctionCallContent

                • AGUIChatClient → GetStreamingResponseAsync()

                  • The three-line pattern

                    // 1. Get an HttpClient from the factory (registered in Program.cs)

                    var http = HttpClientFactory.CreateClient("AGUI");


                    // 2. Create an AGUIChatClient pointing at the live SSE endpoint

                    var agui = new AGUIChatClient(http, Nav.BaseUri + "api/agui/researcher");


                    // 3. Stream exactly like any other IChatClient

                    await foreach (var update in agui.GetStreamingResponseAsync(messages))

                    {

                    foreach (var content in update.Contents.OfType<TextContent>())

                    textSoFar += content.Text;

                    }

                • What is AGUIChatClient?

                  • AGUIChatClient (from Microsoft.Agents.AI.AGUI) is an IChatClient implementation that speaks the AG-UI wire format. Instead of calling an LLM API directly it:

                    1. Serialises your ChatMessage list into an AG-UI JSON body.

                    2. POSTs to the endpoint URL and keeps the SSE connection open.

                    3. Deserialises each incoming SSE event back into AgentResponseUpdate items.

                    4. Yields those updates through the IAsyncEnumerable just like a local streaming model.

                • Heading and background colours can be changed

                  • manually with the colour pickers in the toolbar.

                  • You can also Toggle " Agent chooses colours" to let the server-side agents drive them instead .

                    • Tools run on the server (Program.cs);

                    • UI colour changes arrive as FunctionCallContent events in the SSE stream. This page intercepts them in the stream loop and applies the CSS update — there are no local tool registrations.

              • Program.cs

                • Microsoft.Agents.AI.Hosting.AGUI.AspNetCore

                  • Server-side AG-UI support. Provides AddAGUI() and MapAGUI() to expose agents as SSE endpoints.

                • Microsoft.Agents.AI.OpenAI

                  • MAF agents backed by OpenAI. Provides AsAIAgent() on ChatClient.

                • The  AI API key lives on the server (actually stored in Server Project Program.cs), not in the AgentAGUIClient.razor page 

                • The four endpoints (two are used to modify the Heading and Background)  it calls are the real AG-UI endpoints registered in Program.cs with app.MapAGUI(...):


                  The real SSE endpoints are wired up in just a few lines:

                  // 1. Register AG-UI services

                  builder.Services.AddAGUI();


                  // 2. Register agents using a factory delegate

                  builder.Services.AddAIAgent("ResearcherAgent",

                  (_, _) => new OpenAIClient(apiKey)

                  .GetChatClient(model)

                  .AsAIAgent(name: "ResearcherAgent", instructions: "..."));


                  builder.Services.AddAIAgent("PlannerAgent",

                  (_, _) => new OpenAIClient(apiKey)

                  .GetChatClient(model)

                  .AsAIAgent(name: "PlannerAgent", instructions: "..."));


                  // 3. Map SSE endpoints (after var app = builder.Build())

                  app.MapAGUI("ResearcherAgent", "/api/agui/researcher");

                  app.MapAGUI("PlannerAgent",    "/api/agui/planner");

                • UI - tool variants (colour stub tools the LLM can call

                  • var colourTools = new AIFunction[]

                    {

                        AIFunctionFactory.Create(

                            ([System.ComponentModel.Description("Any valid CSS colour string, e.g. 'red', '#0d6efd', 'darkgreen'.")] string color)

                                => "applied",

                            name: "change_heading_color",

                            description: "Changes the colour of the page heading. Pass any valid CSS colour string."),

                        AIFunctionFactory.Create(

                            ([System.ComponentModel.Description("Any valid CSS colour string, e.g. 'lightyellow', '#e8f5e9', 'aliceblue'.")] string color)

                                => "applied",

                            name: "change_background_color",

                            description: "Changes the CSS background colour of the page card. Pass any valid CSS colour string."),

                    };

                  • builder.Services.AddAIAgent(

                        name: "ResearcherAgentUI",

                        createAgentDelegate: (_, _) =>

                            new OpenAIClient(ApiKey).GetChatClient(Model)

                                .AsAIAgent(

                                    name: "ResearcherAgentUI",

                                    instructions:

                                        "You are a travel research specialist. When given a destination, provide a " +

                                        "concise factual overview covering: best time to visit, typical climate, " +

                                        "must-see attractions (top 3), local cuisine highlights, visa/entry requirements " +

                                        "for most travellers, and one safety tip. Keep the response under 200 words " +

                                        "and use clear headings. " +

                                        "You have two UI tools: change_heading_color and change_background_color. " +

                                        "Proactively call them with a thematically fitting CSS colour for the destination " +

                                        "(e.g. green for Ireland, red for Japan, blue for Greece) before writing your research.",

                                    tools: colourTools));


                    builder.Services.AddAIAgent(

                        name: "PlannerAgentUI",

                        createAgentDelegate: (_, _) =>

                            new OpenAIClient(ApiKey).GetChatClient(Model)

                                .AsAIAgent(

                                    name: "PlannerAgentUI",

                                    instructions:

                                        "You are an expert travel itinerary planner. Using the research notes provided, " +

                                        "create a detailed and engaging 3-day travel itinerary. For each day list: morning " +

                                        "activity, afternoon activity, evening recommendation, and one dining suggestion. " +

                                        "Format clearly with Day 1 / Day 2 / Day 3 headings. " +

                                        "You have two UI tools: change_heading_color and change_background_color. " +

                                        "Call change_background_color with a warm, welcoming CSS colour before you start writing.",

                                    tools: colourTools));

                  • // ── UI-tool variants — include colour stub tools; client intercepts calls ─

                    app.MapAGUI("ResearcherAgentUI", "/api/agui/researcher-ui"); // This line maps the "ResearcherAgentUI" to the "/api/agui/researcher-ui" endpoint, allowing external clients to interact with this agent via AG-UI's SSE protocol and use the UI tools.

                    app.MapAGUI("PlannerAgentUI",    "/api/agui/planner-ui");    // This line maps the "PlannerAgentUI" to the "/api/agui/planner-ui" endpoint, enabling external clients to send requests and receive responses from this agent using AG-UI's SSE protocol and use the UI tools.

                  • called from Client like this

                    • var http = HttpClientFactory.CreateClient("AGUI");

                    • var researcherUrl = agentDrivesColours ? Nav.BaseUri + "api/agui/researcher-ui" : Nav.BaseUri + "api/agui/researcher";

                    • var agui = new AGUIChatClient(http, researcherUrl);

    • Blazor Web App vs Hybrid - Architecture Summary

      • Agent execution

        • MAUI : in-process (on-device)

        • Server-side :  inside the Blazor SignalR circuit

      • AG-UI SSE endpoints

        • Hybrid and Mode 1 : None — simulation only

        • Mode 2 : Real HTTP endpoints (see Program.cs)
          /api/agui/researcher
          /api/agui/planner

      • External clients

        • Hybrid and Mode 1 : Not possible

        • Mode 2: Any client via AGUIChatClient — React, CopilotKit, mobile apps, curl

      • UI colour tools

        • In-process (Hybrid and Mode 1)  : bound to component state

        • Mode 2 : same — instance-bound delegates cannot live in DI-registered agents

      • Agent DI registration

        • Hybrid and Mode 1 : None — created ad-hoc in component

        • Mode 2 :
          builder.Services.AddAIAgent(...)
          builder.Services.AddAGUI()

      • Streaming mechanism

        • Hybrid and Mode 1 : Real LLM streaming + simulated pre/post events via Task.Delay()

        • Mode 2 : Real LLM streaming + simulated pre/post events via Task.Delay() (identical loop)

      • AGUIChatClient

        • Hybrid and Mode 1 : Documented in help only — no real endpoint to connect to

        • Mode 2 : Fully available — connect to the live /api/agui/* endpoints

      • External URL handling

        • Hybrid : App.Current.MainPage.Navigation.PushModalAsync()

        • Blazor Web App : await JS.InvokeVoidAsync("open", url, "_blank")

  • Integrating a Blazor Web App with an n8n Workflow ... Part 135:01

    In this Lecture we will

    • Introduce the concept of an n8n Workflow

      • A powerful, open-source workflow automation tool. It allows you to connect different apps and services (like Gmail, Slack, and Google Sheets) so they can talk to each other. It automates tasks and creates complex workflows without writing code .

      • How it Works (Triggers and Nodes)

        • Nodes: Think of nodes like digital Lego blocks. Each node does one specific job, like sending an email or updating a database. There are hundreds of pre-built integrations (nodes) that allow you to seamlessly connect to your favourite tools.

        • Triggers: These are special nodes that start a workflow. For example, a trigger might say, "Start this workflow when a new email arrives."

        • Workflows: You visually link triggers and actions together on a drag-and-drop canvas to build automated tasks

      • Why You Should Care

        • It is free and private ( when using Self-Hostable option): Unlike cloud automation tools (like Zapier) that charge you based on how many tasks you run, n8n is free to download and run on your own computer or server. This keeps all your sensitive data private and completely eliminates monthly per-task fees.

        • No limits on complex logic: While some beginner tools stop working when things get complicated, n8n lets you easily do advanced things like adding If/Else conditions, loops, and custom coding.

        • It is built for the AI age: You can easily integrate AI models (like OpenAI) into your daily workflows. For example, n8n has built-in AI nodes that can read your incoming emails, decide how to respond, and draft the reply for you automatically.

        • Endless possibilities: If n8n doesn't have a direct connection to an app you use, it has a universal node that can connect to almost any service on the internet.

      • Real-World Examples

        • E-commerce: Automatically update inventory in your store and send a tracking link to a customer when they pay.

        • Customer Service: When a support ticket comes in, have an AI read the ticket, summarize the problem, and alert your team in Slack.

        • Marketing: When you post a new blog, have it automatically generate social media posts and publish them to X, LinkedIn, and Facebook.

    • Hosting Options

      • n8n Hosting Prices : Starter-Pro-Business-Enterprise

      • For a reasonably priced hosting option Hostinger ... which uses a Virtual Private Server (VPS) . You use Docker to install n8n safely.

      • For a truly Self Hosted Local n8n installation, watch the video walkthrough ... here are the key steps

        • Cloudflare (First we need to get a Domain Name) ... a web infrastructure and security company that provides content delivery network services, DDoS mitigation, Internet security, and distributed domain name server services. https://www.cloudflare.com/

          • Login to Cloudflare and create a free account if you don't have one.

          • Add a domain name to your Cloudflare account. You can either use an existing domain or register a new one through Cloudflare (small cost).... chiarelli.cc

        • Docker Desktop (prerequisite for running n8n locally... must always be running in the background) ... a platform for building, sharing, and running containerized applications. It provides an easy-to-use interface for managing Docker containers and images. https://www.docker.com/products/docker-desktop/

        • Cloudflared (allows Cloudflare to create a secure tunnel to your local development environment) ... a command-line tool that allows you to create secure tunnels to your local development environment, enabling you to expose your local web server to the internet without configuring firewall rules or port forwarding. https://github.com/cloudflare/cloudflared

        • CribOps (Installer for n8n and other tools) ... Deploy N8N locally with Docker or scale to production-ready AWS infrastructure with a single command. https://github.com/CloudBedrock/cribops-docs

          • ... From the terminal type : cribops-cli setup -h

          • Use the one-line setup with Cloudflare tunnel (no prompts)

          • You must use the domain you added to Cloudflare

          • Make sure to pick Deployment mode .... Local Docker setup

          • Finish we the command ... docker compose up -d

          • ***Note: The newly created n8n app domain: https://n8n.chiarelli.cc

        • Accessing n8n via the Cloudflare tunnel (first time) https://n8n.chiarelli.cc

          • Go through registration

          • Look for the verification email with License and complete the setup

          • Activate account with the provided license key

          • YOU ARE READY TO START USING n8n

        • Ngrok ... a tool that allows you to expose a local development server to the internet, providing a public URL that can be used for testing webhooks, APIs, and other services. https://ngrok.com/  ... used in the 2nd Lecture when connecting to n8n to Blazor Frontend

          • Download and install ngrok

          • From the Terminal Run the command: ngrok http https://localhost:7033

          • (from Properties/launchSettings.json, the Blazor app is running on port 7033)

          • Copy the generated public URL (e.g., https://abcd1234.ngrok.io)

          • Use this URL in your n8n HTTP request node + "/webhook/update" to receive callbacks from n8n to your Blazor app

    • Look at several simple applications

      • Basic Agent Demo ... Santa's Helper

        • chat message -> Santa Helper AI Agent

        • System Message

          • "You are a helpful assistant . Here is what I want my Santa Helper Agent to do: It will receive some details from a kid about their age and interests The agent should then search to find the top 3 relevant toys for that child that are the most popular at the moment. The kid will then give feedback about those toys , whether they are good or whether more should be suggested."     

      • Basic Agent Demo ... with Calculator and Search Tool

        • chat message -> AI Agent (simple memory,Calculator tool, Google search in SerpApi tool)

        • Here we use a tool our AI Agent can use to get real-time Google search results (must register at SerpApi and get API key)

        • Recall: Most LLMs are trained on slightly older inforamation

      • Simple Sequential Workflow

        • chat message -> Blog Writer -> Blog Formatter

        • Can also execute this Workflow using the chat URL defined in the chat Node ... open a browser and paste it in.

        • Note source for Blog Writer ... Prompt (User Message ) {{$json.chatInput}}

          • You are a helpful assistant.

            Create a raw, unformatted blog post post on the topic:

            "{{ $json.chat.Input }}". Do not add any headings or formatting.

            Just output a continunos stream of text. Make it around 500 words.

        • Note source for Blog Formatter {{$json.output}}  ... output from Blog Writer (Raw)
          You can also drag and drop output from the Blog Writer after initial execution

          • You are a helpful assistant

            Take the following text and format it into a properly structured blog post.

            Add a catchy title, relevant subheadings and bod key terms.

            The output must be ready for publication. Here is the text to format:

            {{ $json.output }}

      • Email for Locally Hosted n8n

        • chat message -> AI Agent (Send Email Tool)

        • Here we picked "Send Email Tool" NOT Gmail Tool. We need to do this because we are hosting n8n locally

        • First we need to create a Google APP password

        • go to security google com    .... app passwords

  • Integrating a Blazor Web App with an n8n Workflow ... Part 224:35

    In this Lecture we will

    • Look at a Blazor Web App that triggers several sample n8n workflows (which are running asynchronously in the background) ... SignalRn8n

      • Simple Implementation Demos ( URL Link access ... little coding MoreN8N.razor)

        • YouTube Scraper with JS

          • -> Webhook (URL) -> Search YouTube -> Filter Best Videos (Code Node JS) -> Summarize Filtered Info for AI (JS) -> AI Agent Analyze Videos -> Respond to Webhook >

          • The Webhook uses a GET because we simplying want to trigger (kickstart) an n8nworkflow. We can do this by pasting a URL link into a web browser (make sure to Publish Workflow first and then go into Parameters and note Production URL)

          • Search YouTube is an Apify node ... Run an Actor

            • We need to get an API Key  ... goto Apify site Console and then "Integrations" page and find manager Tokens

            • Now goto  Apify store and look for "Youtube Channel Scraper"

            • Setup all parameters and start once in Apify then choose Actor (tool) in Node setup

            • Back in Apify grab JSON data and add it to the Node setup

            • This node when executed will Pull the requested info (defined on Apify)

            • Make sure Operation is set to Run an Actor and Get Dataset

            • Filters it with Javascript

            • ... then sends it to AI Agent for Analysis

            • Final results are sent to the browser via a Response to Webhook node

              • Respond with "Text"

              • Response Body {{$json.output}}

              • Options Response Header

                • Name : Content-Type

                • Value: text/html

          • Filter Best Videos (Code Node JS)

            • // Only keep videos with 50,000 or more views

              //Code created by first downloading Worksheet code (JSON) to ChatGpt

              //and asking to write code snippet that performs requested filtering

              return $input.all().filter(item => {

                const views = Number(item.json.viewCount || 0);

                return views >= 50000;

              });

          • Summarize Filtered Info for AI (JS)

            • //Create one nice summary for AI Agent

              const allItems=$input.all()

              const formattedList=allItems.map(item=>{

                const title=item.json.title || "No title";

                const link = item.json.url || "No URL";

                const views = item.json.viewCount || "No Views";

                const description = item.json.description || "No description";

                return `- [${title}](${link}) - ${views} views\n ${description}`; 

              }).join("\n\n");

              return [

                {

                  json:{

                    summary:formattedList

                  }

                }

              ];

          • AI Agent Analyzes Videos

            • I would like a nice summary of the top 3 relevant youtube videos with the most curr{{ $json.summary }}ent info. Particularly focus on anything to do with the Toronto Raptors when appropriate.

        • DB Tool (Natural Language Querying) ... AirTable Implmentation (Music Library)

          • Chat Message (URL) -> AI Agent (Searches records in Airtable tool)

          • Get the Chat URL from the "Chat Message" node and you can execute the workflow from a browser ( make sure to Publish Workflow first and then go into Parameters and note Chat URL and Make Chat Publicly Available)

          • AirTable Tool Node Specifics

            • We must create a new Credential when you are connecting to a new database (Music Library)

            • Go to the airtable site ... sign up for free (stick with free plan)

            • Choose "Build an app on your own". Name the DB and then a Table within the DB

            • Next you need your Personal Access Token (see create/tokens)

              • Click Create Token

              • Choose DB

              • Set Scopes

                • data.records:read

                • data.records:write

                • schema.bases:read

            • In Node Parameters

              • Resource:Record

              • Operation:Search

              • Base: from Music Library

              • Table: Songs

  • Integrating a Blazor Web App with an n8n Workflow ... Part 328:42

    In this Lecture we will

    • Complete our look at a Blazor Web App that triggers several sample n8n workflows (which are running asynchronously in the background) ... SignalRn8n

      • Advanced Implemenation Demos (Direct connection to Blazor App)

        • Our Blazor app pushes a chat request to an n8n Webhook which kickstarts a workflow

          • An AI agent is leveraged from within the n8n workflow to create an intelligent response to a user's message.

        • Once the workflow task completes, an HTTP Request is used to return the result back to  the Blazor app, Signal-R is then used to push the data to the Blazor UI in real time.

        • Blazor Frontend Chat App with Email Option ... Webhook-HTTP Request (ChatForm.razor)

          • -> Webhook -> AI Agent (Model,Simple memory, Send Email Tool)

            • Note here that the Webhook uses a POST

            • POST webhooks receive data in the body: The sending application (Blazor) pushes a heavy payload (usually a JSON object, form data, or raw text) hidden inside the HTTP request body

            • You find the webhook path in your n8n workflow, in the Webhook node, under the "Webhook URL" section.

              It will look something like this:(This is the Published ... ie Production URL from n8n, not the local Test URL)

              private string n8nWebHookPath = "https://n8n.chiarelli.cc/webhook/9459ecc8-6889-4f8f-ae52-5ad897116bd8";   

          • <- HTTP Request

            • Note use of /webhook/update as the endpoint for the webhook in n8n (HTTP Request)

              This is the endpoint that n8n will use/call when an update occurs.

              You attach this endpoint to the n8n workflow HTTP Request node (POST method) to send updates to the SignalR clients.

              The complete URL for the webhook will be something like https://morbidly-congenial-obedience.ngrok-free.dev/webhook/update.

            • To get this URL  - you can use a service like ngrok to expose your local development server to the internet for testing purposes.

              • In cmd (terminal mode) you type ... ngrok http https://localhost:7033 (or whatever port your app is running on ... see Properties/launchSettings.json)to get a public URL for your local server.

              • So this command will receive the POST request from n8n

              • In the HTTP Request Node also update the Body Parameters ...

                • reply Name to "reply"

                • value: {{$json.output}}

              • In Program.cs

                • //extract the "reply" property from the JSON payload, and then send that message to all connected SignalR clients via the "ReceivedMessage" event.

                  app.MapPost("/webhook/update", async (IHubContext<SignalRn8nTest.SignalRHub.UpdateHub> hubContext,HttpContext ctx) =>

                  {

                      var body = await new StreamReader(ctx.Request.Body).ReadToEndAsync();   // Read the request body as a string


                      dynamic dynamicObject = JsonConvert.DeserializeObject(body);            // Deserialize the JSON payload into a dynamic object


                      string reply = dynamicObject["reply"];       // Extract the "reply" property from the dynamic object

                                                                                              //must match the property name in the JSON payload sent from n8n

                                                                                              //See n8n workflow HTTP Request Object (Body parameter) for the JSON structure   


                      await hubContext.Clients.All.SendAsync("ReceivedMessage", reply);       // Send the "reply" message to all connected SignalR clients via the "ReceivedMessage" event


                      return Results.Ok(new { Status = "Update pushed to clients" });

                  });


                  app.Run();


        • Sequential Workflow  ... Webhook-HTTP Request (SequentialWorkFlow.razor)

          • ->Webhook -> Blog Writer -> Formatter -> HTTP Request

    • Suggested Exercise(s)

      • Connect the the AirTable DB (Music Library ... Song Table) ... SignalRn8nAirTableCRUD

        • Perform Natural Language Queries

        • Full CRUD capabilities

  • Agentic RAG with Blazor and n8n43:29

    In this Lecture we will

    • Review/Recap from our previous lectures  that RAG ... Retrieval-Augmented Generation  is used to provide answers based on your custom data.

      • User asks a Question

      • Retreival ... The AI searches for relevant information from connected sources such as policy documents, product manuals or internal knowledge bases. Top relevant results are retrieved.

      • Augmentation ... The retrieved content is fed into the AI model as additional context. This step is what makes RAG special, the model doesn’t rely solely on what it “knows” from training, but gets enriched, real-time domain-specific information to base its answer on leading to more accurate , factual, and trustworthy outputs (no hallucinations)

      • Generation ... using this combination, its internal language capabilities plus the retrieved data, the AI produces a natural-language response. 

      • Imagine asking a chatbot, “What did the President say in his speech yesterday?” Most large language models, like GPT, are trained only up to a certain date and lack up-to-date knowledge. This is where RAG comes in. It could query an external database, updated daily with the latest news articles, to find relevant information. The database returns a match, which is then incorporated into the query before being passed to the generative AI model. By framing the query to include this new information, such as “Consider the text of the following news article in your response,” the AI can generate a response that accounts for the latest data. This method allows for the integration of external, possibly proprietary, data, effectively creating a sophisticated search engine.

    • Learn how to create a RAG application (simple traditional RAG  and advanced  Agentic RAG versions) by Integrating a Blazor Web App with an n8n Automation Workflow ... SignalRn8nRAG

      • Simple Implementation (RAGn8n.razor)

        • Blazor to n8n Workflow referencing URL addresses to upload pdf documents to a Vector Database  and retrieve info from a Vector Database

        • n8n RAG Ingestion Process

          • n8n Form (On form submission) -> Qdrant Vector Store - Default Data Loader - Embeddings OpenAI 

            • ... See Deeper Look button for all details

          • Key Elements

            • A Default Data Loader in n8n is a LangChain sub-node used to ingest binary  files or JSON data into vector stores or AI summarization chains. It reads incoming content, handles data formatting and splits text into chunks for AI models

            • Embeddings model in AI is a machine learning tool that converts complex data - such as words, sentences, images, or audio - into long lists of numbers called vectors. These numbers capture the actual meaning and context of the data so that computer systems can compare and understand relationships between different pieces of information

            • The Connection Workflow

              • The Loader Extracts: The Default Data Loader takes raw text, JSON files or files like PDFs and breaks them down into smaller text chunks

              • The Loader Passes Data: The loader then passes these raw text chunks directly to the Embeddings Model node.

              • The Model Converts: The Embeddings Model (such as OpenAI) translates those text chunks into mathematical vectors (long lists of numbers) that capture the semantic meaning of the words.

              • The Destination Stores: These vectors along with the original text, are finally saved into a Vector Store node like Qdrant or Pinecone.

        • n8n RAG Chatbot

          • On Chat Message -> AI Agent - OpenAI Chat Model - Simple Memory - Qdrant Vector Store -> Embeddings OpenAI 

            • ...  See Deeper Look button for all details

        • Blazor Web App Coding Highlights

          • We use two simple links

            • URL to PDF Upload Form

              • This displays an n8n Submission form in the browser which allows the user to chose a local pdf file which will be added to the Vector Database via the n8n Workflow (Ingestion process)

            • URL to RAG Chat

              • This displays a browser based chat where the user enters a simple request and the AI Agent response is displayed in a simple browser window.

          • On start up we display some predefined pdf's  (via  a call from within ...  protected override void OnInitialized) saved locally in the wwwroot folder  (data subfolder)

            • private List<string> savedPdfFiles = new();

            • private void LoadSavedPdfs()

              {

                  ClearAll();

                  try

                  {

                      var pdfsPath = Path.Combine("wwwroot", "data");        

                      if (Directory.Exists(pdfsPath))

                      {

                          var files = Directory.GetFiles(pdfsPath, "*.*")                           

                              .Where(f => f.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))

                              .ToArray();

                          savedPdfFiles = files.Select(f => Path.GetFileName(f)).ToList();         
              // Using GetFileName get only the file names (including extensions), not the full paths

                      }

                      else                                                                         

                      {

                          savedPdfFiles = new List<string>();

                      }

                  }

                  catch

                  {

                      savedPdfFiles = new List<string>();                                             

                  }

                  StateHasChanged();

              }

            • Here is the HTML snippet

              • <ul class="list-group" style="max-height: 250px; overflow-y: auto;">

                    @if (savedPdfFiles.Count == 0)

                    {

                        <li class="list-group-item">No saved pdf's found.</li>

                    }

                    else

                    {

                        @foreach (var file in savedPdfFiles)

                        {

                            <li class="list-group-item list-group-item-action"

                                style="cursor:pointer; font-size:1.0em; padding-top:4px; padding-bottom:4px; min-height:28px; height:28px; line-height:20px;"

                                @onclick="() => ShowSavedPdf(file)">

                                @file

                            </li>

                        }

                    }

                </ul>

            • Clicking on any of the listed pdf's in the list executes the ShowSavedPdf method which displays the pdf contents in the browser.

              • private void ShowSavedPdf(string fileName)

                {

                    try

                    {

                        // Construct the URL to the PDF file in the wwwroot/data folder

                        var pdfUrl = "/data/" + fileName;

                           

                        // In Blazor, PDFs in wwwroot/data can be accessed directly via their web path

                        // Navigate to the PDF, which will open it in the browser

                        //Navigation.NavigateTo(pdfUrl, forceLoad: true);

                           

                        // Alternative: Open in a new tab/window using JavaScript           

                        JSRuntime.InvokeVoidAsync("open", pdfUrl, "_blank");

                    }

                    catch (Exception ex)

                    {

                        // Handle error appropriately for Blazor

                        // You might want to show an error message to the user

                        Console.WriteLine($"Error opening PDF: {ex.Message}");

                    }

                }   

      • Advanced Implementation ... Agentic RAG  (MoreRAGn8n.razor)

        • Agentic RAG describes an AI agent-based implementation of RAG. Specifically, it incorporates AI agents into the RAG pipeline to orchestrate its components and perform additional actions (Send Email in our case) beyond simple information retrieval and generation to overcome the limitations of the non-agentic pipeline.

        • Blazor  to n8n Workflow using direct two way connection via SignalR

        • Blazor uploads pdf documents to the Workflow via a WebHook which then transfers them to a Vector Database

        • Blazor sends queries about the pdf document contents  to the Workflow AI Agent also via a Webhook with the answers returned through an HTTP Request node 

        • We have also incorporated an optional Send Email node

        • n8n RAG Ingestion Process

          • Webhook -> Qdrant Vector Store (add pdf doc to vector store) - Default Data Loader - Embeddings OpenAI  

            • ...  See Deeper Look button for all details

        • n8n RAG Chatbot

          • Webhook -> AI Agent -> HTTP Request

          • AI Agent - OpenAI Chat Model - Simple Memory - Send Email - Qdrant Vector Store -> Embeddings OpenAI 

            • ...  See Deeper Look button for all details

        • Blazor Web App Coding Highlights

          • In this second , more advanced implementation , we have two separate operations that need to be performed for a complete RAG implementation.

          • First we may need to choose a local pdf file (if no document exists yet to work with) to send to the Vector Database , connecting to the n8n workflow  via an  Webhook.

            • We again display a list of pdf's we have stored in the wwwroot folder and allow the user to view the contents in the browser.

            • Next we allow the user to choose a particular pdf file using the built in component InputFile
              @*This creates a more stylized version of the InputFile Component*@

              <div class="mb-0">

                   <label class="btn btn-info btn-lg" for="fileInput">

                       <i class="bi bi-file-pdf"></i> Choose local PDF File <br /> To Send to Vector Database <br /> Via n8n Webhook

                   </label>

                   <InputFile id="fileInput" OnChange="@UploadPdf" accept=".pdf" style="display: none;" />

              </div>

            • The UploadPdf Task does all of the necessary transfer to the n8n workflow Webhook.

              • Note we have a new address for the Webhook that flows into the Vector Database connection. ie n8nWebHookPathVectorDB

              • private async Task UploadPdf(InputFileChangeEventArgs e)

                {

                    // Get the uploaded file from the InputFileChangeEventArgs

                    // file is of type IBrowserFile, which represents the file selected by the user in the InputFile component.

                    // It provides properties and methods to access the file's content, name, size, and other metadata.

                    var file = e.File;


                    // Here we create a MultipartFormDataContent object to send the file and metadata to the n8n webhook.

                    // MultipartFormDataContent is used to send files and form data in a single HTTP request, which is suitable for file uploads.

                    using var content = new MultipartFormDataContent();


                    // Read the selected file which is of type IBrowserFile and create a StreamContent to send it in the HTTP request.

                    var stream = file.OpenReadStream(maxAllowedSize: 50 * 1024 * 1024); // 50 MB

                    var fileContent = new StreamContent(stream);


                    // Set the MIME type ie Content-Type header for the file being uploaded.

                    // In this case, we are uploading a PDF file, so we set it to "application/pdf".

                    fileContent.Headers.ContentType =

                        new System.Net.Http.Headers.MediaTypeHeaderValue("application/pdf");


                    // "data" is the form field name that n8n will receive it is referenced in the n8n workflow in the Webhook node as the binary data field name.

                    content.Add(fileContent, "data", file.Name);


                    // Optional metadata for the uploaded file, which can be used by the n8n workflow for processing or storage in the vector database.

                    content.Add(new StringContent("Charlie"), "user");

                    content.Add(new StringContent("Training PDF"), "description");


                    // Send the file to the n8n webhook for processing and storage in the vector database

                    // Where the content has the pdf and the metadata (user and description) as form data

                    // This will be taken by the Webhook node (as binary data) in n8n

                    // and passed to the next nodes in the workflow for processing and storage in the vector database (Qdrant)

                    // ie first to the Default Data Loader node and then to the Embeddings OpenAI node and then to the Qdrant Vector Store node in the n8n workflow

                    var response = await Http.PostAsync(n8nWebHookPathVectorDB, content);


                    response.EnsureSuccessStatusCode();


                    // Read the response content as a string (if needed for logging or debugging)

                    // In this case, we are not using the response content, but we read it to ensure the request was successful and to log any potential issues.

                    var result = await response.Content.ReadAsStringAsync();


                    Console.WriteLine(result);

                }

          • Once we have a document or two to work with we now can make a request/query  to the AI Agent again connecting to the n8n workflow via a second Webhook. This workflow will return an answer via an HTTP Request node.

            • This process is basically identical to Lecture 3

            • The Blazor web app starts the process by triggering an External n8n workflow (via a WebHook)

              • Blazor pushes a payload (JSON object holding the user prompt) inside an HTTP request body ... var resp = await Http.PostAsJsonAsync(n8nWebHookPath, payload);

            • The n8n workflow handles the AI logic creating an agent-driven response based on the Qdrant Vector Store1 node tool

            • Then there is a callback from n8n back to the Blazor app (via an HTTP Request node in the n8n Workflow)

              • webhook/update is added to the URL for the Webhook

              • This URL is exposed using "ngrok http https://localhost:7033"

              • In the Node Parameters we set ... reply Name to "reply" and value: {{$json.output}}

              • See Program.cs ... app.MapPost("/webhook/update" .... for the specific details

            • Once back in the Blazor App, Real-time UI updates are handled with SignalR

              • See Program.cs

              • See the folder SignalRHub

              • See the code section on this page (ChatForm.razor)

            • We use a Docker-based setup and ngrok tunneling support ... See "Background Info" button below for more info

  • Image Analysis with Blazor and n8n: Human in the Loop Receipt Approval46:03

    In this Lecture we will

    • We take our Blazor and n8n (the workflow automation tool for the AI age) integration to the next level by introducing AI-powered image analysis and applying it to a practical business scenario: Receipt Approval.

    • Start off by implementing some general image recognition skills via a simple n8n workflow

    • Build a more advanced n8n workflow that accepts a receipt image from a Blazor Web App, passes it to an AI Agent for analysis, extracts the receipt details and total amount, and then uses that information to determine whether the receipt can be automatically approved or requires further review.

      • For receipts that exceed the automatic approval threshold, we introduce a Human-in-the-Loop process. The workflow pauses while a manager reviews the receipt  (view email) and chooses to approve or reject it. The resulting decision is then returned to the Blazor application.

    • Receipt Analysis & Approval Workflow Application (SignalRn8nImgAnalysis)

      • Step 1 - Uploading the Receipt

        The process begins in the Blazor application. The user selects a receipt image from their computer and uploads it through the application.

        • The image is read by the Blazor application.

        • The image is converted into a Base64 string.

        • The image data , the user's message  , name  and sessionId  are sent to an n8n Webhook.

        • The Webhook becomes the starting point for the n8n workflow.

        • Coding Highlights

          • LoadSavePhotos ... ShowSavePhoto  (from HTML group list) ... dataUriImg

          • InputFile component ... UploadImage ... GetPhotoDataUrlAsync ... dataUriImg

          • ChatRequest class has been updated ... note SessionId used by n8n simple memory node
            private class ChatRequest

            {

                [Required(ErrorMessage = "Message is required.")]

                public string? Message { get; set; }

                public string? Name {get;set;}

                public string? Image { get; set; } 

                public string? SessionId{ get; set; }

            }

            private string sessionId = Guid.NewGuid().ToString(); // Unique session ID for the user session   

          • In HandleAuthValidSubmit method are payload is now updated
            var payload = new ChatRequest

            {

                Message = model.Message,

                Name=model.Name
                Image = dataUriImg,

                SessionId = sessionId

            };
            var resp = await Http.PostAsJsonAsync(n8nWebHookPath, payload);

      • Step 2 -  Receiving the Image in n8n

        • The n8n Webhook receives the request from Blazor (note: Authenication is set).

        • The workflow then prepares the incoming image so it can be passed to the AI Agent.

          • The Webhook receives, image, user name, request, and sessionId.

          • A Code node prepares the information required by the AI Agent.
            const dataUrl = $json.body.image;


            const base64 = dataUrl.split(',')[1];


            return {

              json: {

                message: $json.body.message,

                imageBase64: base64,

                sessionId: $json.body.sessionId

              }

            };

          • The image is converted into the appropriate binary format (Convert to File node).

            • Operation: Move Base64 String to File

            • Base64 Input Field: imageBase64

            • Put Output File in Field: data

          • The AI Agent is configured to automatically pass the binary image to the model.

            • Prompt (User Message)
              {{ $('Webhook').item.json.body.message }}

            • Automatically Passthrough

      • Step 3 -  AI Receipt Analysis

        • The image is then sent to the AI Agent. The AI Agent uses an AI vision-capable model to examine the receipt and extract the important information.

          • OpenAI Chat Model

          • Simple Memory (key)
            {{ $('Webhook').first().json.body.sessionId }}

          • AI Agent System Message

            • You are a helpful image-analysis assistant.

              Analyze every image that you receive and provide a useful description of what is visible.

              If the image is a purchase receipt, extract the receipt information carefully.

              For a purchase receipt:

              1. Identify the business name, date, items purchased, prices, subtotal, tax, and other available receipt information.

              2. Identify the FINAL TOTAL. Look specifically for labels such as "Total", "Grand Total", "Balance", or "Amount Due".

              3. Do NOT use the Subtotal as the FINAL TOTAL.

              4. Do NOT use an individual item price as the FINAL TOTAL.

              5. Include tax and other charges when determining the FINAL TOTAL.

              6. Do not guess a missing FINAL TOTAL.

              For a receipt, begin the response with these exact lines:

              IMAGE_TYPE: RECEIPT

              FINAL_TOTAL: [amount]

              DECISION: [ACCEPTED or UNDER_REVIEW]

              The decision rule is:

              * FINAL_TOTAL less than $50.00 = ACCEPTED

              * FINAL_TOTAL $50.00 or greater = UNDER_REVIEW

              Example:

              IMAGE_TYPE: RECEIPT

              FINAL_TOTAL: 31.12

              DECISION: ACCEPTED

              For a receipt with a total of $50.00 or more:

              IMAGE_TYPE: RECEIPT

              FINAL_TOTAL: 75.50

              DECISION: UNDER_REVIEW

              After these three lines, provide the complete receipt analysis formatted nicely in HTML.

              The FINAL TOTAL must be displayed in **Bold** in the HTML.

              If the image is NOT a purchase receipt, begin the response with exactly:

              IMAGE_TYPE: OTHER

              Do not provide a FINAL_TOTAL or an ACCEPTED/UNDER_REVIEW decision for a non-receipt image.

              After IMAGE_TYPE: OTHER, analyze and describe the image normally and format the response nicely in HTML.

              If you are unable to extract specific receipt information from the image, still describe the visual content nicely formatted in HTML. Do not invent missing information.

              IMPORTANT:

              Do NOT send emails.

              Do NOT attempt to send an email.

              The AI Agent's job is ONLY to analyze the image and produce the structured result and HTML response.

              Email sending will be handled separately by the n8n workflow.

              Always analyze the image, whether it is a receipt or another type of image.

        • The AI analyzes the receipt and determines information such as:

          • Store or vendor name

          • Date of purchase

          • Items purchased

          • Subtotal

          • Taxes

          • Total amount

        • The most important value for this workflow is the receipt total. The workflow uses that value to determine whether the receipt can be automatically approved or must be reviewed by a manager.

      • Step 4 -  Approval Decision

        • After the AI Agent analyzes the receipt, the workflow uses an IF node to determine what should happen next.

        • IF1 (is IMAGE_TYPE a RECEIPT or not)

          • {{ $json.output }} starts with IMAGE_TYPE:RECEIPT

          • If NOT true (false) -> HTTP Request -> Blazor  (just a regular image  that the AI will determine the contents of)

        • IF2 (if IF1 is true)

          • Receipt total less than $50: The receipt is automatically approved.

          • {{ Number($json.output.match(/FINAL_TOTAL:\s*([0-9.]+)/)[1]) }}
            is less than : 50

          • -> HTTP Request (Back to Blazor with approval ... no Human in the Loop approval required)

          • Automatic Approval

            • When the receipt is below the approval threshold, no manager intervention is required.

            • The workflow generates an Approved response.

            • An HTTP Request node sends the result back to the Blazor application.

              • Value
                <p><strong>Submitted By:</strong> {{ $('Webhook').item.json.body.name }}</p>

                <hr>

                {{ $json.output }}

            • Blazor receives the response and displays the result to the user.

            • The entire process can therefore be completed automatically without waiting for anyone to review the receipt.

        • IF2 (false)

          • Receipt total greater than $50: The receipt is placed Under Review.

      • Step 5 - Manager Review

        • When the receipt exceeds the automatic approval threshold, the workflow follows the Under Review path.

        • The receipt is identified as requiring manager approval.

        • An HTTP Request returns the initial Under Review status to Blazor.

        • An email is sent to the manager containing the receipt information.

        • The email contains options for the manager to approve or reject the receipt.

        • At this point, the n8n workflow uses a Wait node. The workflow pauses until the manager makes a decision.

      • Step 6 - Waiting for Manager Approval

        • First we go through a New HTTP Request which sends back to Blazor all the current details ... otherwise the Under Review process would not display anything on the Blazor UI until the Manager responded to the sent out email leaving the user in limbo

          • Value
            <p><strong>Submitted By:</strong> {{ $('Webhook').item.json.body.name }}</p>

            <hr>

            {{ $('AI Agent').item.json.output }}

        • Next we go to the Send an Email (not Send Email Tool that we have used previously ... note we have deactivated that node in the workflow)

          • Credentials same the same

          • Send

          • From/To  (user email)

          • Subject: Expense Report Requires Approval

          • Email Format : HTML

          • HTML
            <p><strong>Submitted By:</strong> {{ $('Webhook').item.json.body.name }}</p>

            <hr>

            {{ $('AI Agent').item.json.output }}

            <hr>

            <p>Please select an action:</p>

            <p>

            <a href="{{ $execution.resumeUrl }}&decision=approved">

            <strong>APPROVE EXPENSE</strong>

            </a>

            </p>

            <p>

            <a href="{{ $execution.resumeUrl }}&decision=rejected">

            <strong>REJECT EXPENSE</strong>

            </a>

            </p>

        • Here we use a Wait Node (waiting for Email response)

          • Resume: On Webhook Call

          • HTTP Method: GET

          • Respond: Immediately

      • Step 7 - Approve-Reject

        • When the manager makes a selection Approve/Reject , the waiting workflow resumes. The approval result is then processed by the next IF node (IF3)

          • {{ $json.query.decision }} is equal to approved (true branch)

          • The approval response is detected.

          • The workflow creates the final approval message using Edit Fields1

            • Mode: Manual Mapping

            • Fields to Set output
              {{ $('AI Agent').item.json.output + '\n\n<h2>MANAGER DECISION: APPROVED</h2>' }}

          • An HTTP Request sends the final approval result back to Blazor.

          • Blazor receives the final response and can update the user interface to show that the receipt has now been approved.

          • false branch

            • Edit Fields 2

            • final rejection message is generated.

            • Mode: Manual Mapping

            • Fields to Set output
              {{ $('AI Agent').item.json.output + '\n\n<h2>MANAGER DECISION: REJECTED</h2>' }}

            • An HTTP Request returns the rejection result to Blazor.

            • Blazor can then display the final Rejected status to the user.

      • Step 8 - Returning Results to Blazor

        • HTTP Request nodes are used at the appropriate points in the workflow to communicate the results back to the Blazor application.

        • This is important because there are effectively two stages of communication for receipts that require manager review:

          • Initial response: Blazor is informed that the receipt is Under Review.

            • New HTTP Request
              <p><strong>Submitted By:</strong> {{ $('Webhook').item.json.body.name }}</p>

              <hr>

              {{ $('AI Agent').item.json.output }}

          • Final response: After the manager makes a decision, Blazor receives the final Approved or Rejected result.

            • HTTP Request
              <p><strong>Submitted By:</strong> {{ $('Webhook').item.json.body.name }}</p>

              <hr>

              {{ $json.output }}

      • The Complete Workflow Overview

        • User selects a receipt/regular image in the Blazor application.

        • Blazor converts the image to Base64 data.

        • Blazor sends the image and message to the n8n Webhook.

        • n8n prepares the image for AI processing.

        • The AI Agent analyzes the receipt.

        • The receipt total is extracted.

        • An IF node determines whether it's a Receipt or not ... if not (ie its a regular image) straight through to the HTTP Request

        • If it is a Receipt A second  IF node determines whether approval is required.

        • Receipts below $50 are automatically approved straight through to HTTP Request

        • Receipts above $50 are placed Under Review.

        • The Under Review status is returned to Blazor.

        • A manager receives an approval email.

        • The n8n Wait node pauses the workflow.

        • The manager approves or rejects the receipt.

        • The workflow resumes and a 3rd IF node processes the decision.

        • If approved ... Edit Field1 creates an approved return message

        • If rejected ... Edit Field2 creates a rejected return message

        • The final result is returned to Blazor through an HTTP Request.

        • Blazor displays the final status to the user.

      • Key Technologies Used

        • Blazor Web App – User interface and receipt upload.

        • HTTP / Webhook – Communication between Blazor and n8n.

        • n8n – Workflow orchestration and business logic.

        • Code Node – Preparing the uploaded image data.

        • Convert to File nodes

        • AI Agent – Analyzing the receipt image and extracting information.

        • IF Nodes – Controlling the approval logic.

        • HTTP Request Nodes – Returning workflow results to Blazor.

        • Email base approval – Sending receipts requiring review to the manager.

        • Wait Node – Pausing the workflow until a manager responds.

        • Edit field nodes

    • Summary 

      • The most important concept demonstrated by this application is that Blazor does not have to perform all of the processing itself. The Blazor application acts as the user interface, while n8n manages the automation and decision-making process.

      • The AI Agent handles the difficult task of understanding the receipt, n8n controls the business workflow, and Blazor provides the user with the results. This creates a flexible architecture where additional business rules, approval levels, databases, notifications, or AI capabilities can be added to the workflow without significantly changing the Blazor application.

      • Blazor → n8n Webhook → Image Processing → AI Agent → Approval Decision → Automatic Approval OR Manager Review → Final Decision → HTTP Request → Blazor

  • Blazor + n8n: Building an AI-Powered Web Scraping & Data Analysis Workflow52:43

    In this Lecture we will

    • Build a Blazor Web App (SignalRn8nScraping) that triggers an n8n workflow to scrape real-world data (TechCruch AI Articles)

      • We will process and display the results sending the scraped data to Google Sheets, an Email Address and back to Blazor

      • We will also use an AI Agent to answer natural-language questions about the scraped content.

      • The Key Steps

        • Build the basic Blazor page.

        • Connect Blazor to the n8n webhook.

        • Scrape the website.

        • Return articles to Blazor with SignalR.

        • Store the data in Google Sheets and send off an Email

        • Add the AI Agent.

        • Pass the scraped articles and user query to AI.

        • Display the AI response.

        • Add Simple Memory and Session ID.

        • Add conversation history.

    • Version 1 Web Scraping -> Google Sheet

      • This application demonstrates how a Blazor web app can trigger n8n workflow that web scrapes a given site and sends the results off to a Google Sheet and an Email.  A "Respond to Webhook"  node informs Blazor the operation has ended, but we don't use SignalR

      • n8n Workflow

        • Webhook (GET) -> HTTP Request (GET)  -> Code in Javascript -> Append row in Google Sheet -> Respond to Webhook

        • Code in Javascript -> Create Email Content (JS Code node) -> Send an Email

      • Coding Highlights

        • // Method to start the web scraping process by invoking the n8n workflow via an HTTP GET request

          // We use Basic Authentication to secure the request, and we handle the response to update the UI accordingly.

          // We don't have a payload to send, so we just call the GET webhook and wait for the response.

          private async Task StartWebScraper()

          {

               // Set the scraping flag and update the status message

               IsScraping = true;

               ScrapingStatus = "? Workflow has begun...";


               // Update the UI to reflect the current state ie the workflow has started

               await InvokeAsync(StateHasChanged);


               // Use a try-catch block to handle any exceptions that may occur during the HTTP request

               try

               {

                   // Add Basic Authentication since the Webhook in the n8n workflow is protected by a username and password. The credentials are encoded in Base64 format.

                   var credentials = Convert.ToBase64String(

                       System.Text.Encoding.ASCII.GetBytes("c:1"));


                   // Set the Authorization header for the HTTP client to include the Basic Authentication credentials.

                   // Where "Basic" indicates the authentication scheme and the credentials are the Base64-encoded username and password.

                   // "Basic" means that the client sends the username and password in the HTTP request header, which is a simple authentication method.

                   Http.DefaultRequestHeaders.Authorization =

                       new System.Net.Http.Headers.AuthenticationHeaderValue(

                           "Basic", credentials);


                   // Call the n8n production GET webhook and wait for the response.

                   // The webhook response isnt returned until after the scraping and Google Sheets work as completed.

                   //----------------------------------------------------------------------------------------------------------

                   // Note: Path website-scraper was manually edited the the n8n Webhook node to make it more descriptive.

                   // The original path was a UUID which stands for a unique identifier for the workflow. The path was changed to "website-scraper" to make it more user-friendly and easier to remember.

                     

                  var resp = await Http.GetAsync(

                       "https://n8n.chiarelli.cc/webhook/website-scraper");

                     

                   // IsSuccessStatusCode is a property of the HttpResponseMessage class that indicates whether the HTTP response was successful (status code 2xx ie OK ) or not.

                   if (resp.IsSuccessStatusCode)

                   {

                       // "reply" holds the actual body/content of the response sent back by n8n via the "Respond to Webhook" node.

                       // The content is a string that contains the results of the scraping and Google Sheets operation.

                       // But we don't use it in this example used just for testing and debugging
                       //We just display a success message to the user.

                       var reply = await resp.Content.ReadAsStringAsync();


                       ScrapingStatus =

                           "✅ Scraping completed! Data has been added to the Google Sheet and an Email has been sent.";

                   }

                   else

                   {

                       var error = await resp.Content.ReadAsStringAsync();


                       ScrapingStatus =

                           $"❌ Workflow failed: {resp.StatusCode}";


                       Console.WriteLine(error);

                   }

               }

               catch (Exception ex)

               {

                   ScrapingStatus =

                       $"❌ Error starting workflow: {ex.Message}";

               }

               finally

               {

                   IsScraping = false;


                   await InvokeAsync(StateHasChanged);

               }

          }

    • Version 2 Web Scraping -> AI Agent -> Blazor

      • This application demonstrates how a Blazor web app uploads a payload (user query and sessionId) to an n8n workflow that initiates a web scrape and returns the results back to this Blazor app (using SignalR). The results are then deserialized according to a predefined class and be displayed in tabular form. We then use an AI Agent to answer natural language questions about the scraped content. The key here is that we are now using two unique Webhook endpoints and two unique SignalR events

      • n8n Workflow

        • Webhook (POST) -> HTTP Request (GET) -> Code in Javascript -> HTTP Request (POST) ->  Blazor

          • This does the Initial Scraping and returns the results back to Blazor for display (via SignalR) using the "update" webhook (see program.cs and HTTP Request node)

          • uses SignalR event "ReceivedMessage"

        • Code in Javascript ->Code in Javascript1 -> AI Agent (OpenAI Chat Model - Simple Memory) -> HTTP Request (AI Response ) -> Blazor

          • This returns the AI response to the supplied query from the payload that was sent to the Webhook again returned back to Blazor via SignalR using the "update-ai" webhook (see program.cs and HTTP Request - AI Response)

          • uses SignalR event "ReceivedAIResponse"

      • Coding Highlights

        • Program.cs
          //----------------------------------------------------------------------------------

          // SCRAPING RESPONSE WEBHOOK

          //----------------------------------------------------------------------------------

          //So this command will receive the POST request from n8n,

          //extract the "reply" property from the JSON payload, and then send that message to all connected SignalR clients via the "ReceivedMessage" event.

          app.MapPost("/webhook/update", async (IHubContext<SignalRn8nTest.SignalRHub.UpdateHub> hubContext,HttpContext ctx) =>

          {

              var body = await new StreamReader(ctx.Request.Body).ReadToEndAsync();   // Read the request body as a string


              dynamic dynamicObject = JsonConvert.DeserializeObject(body);            // Deserialize the JSON payload into a dynamic object


              string reply = dynamicObject["reply"];                                  // Extract the "reply" property from the dynamic object

                                                                                      //must match the property name in the JSON payload sent from n8n

                                                                                      //See n8n workflow HTTP Request Object (Body parameter) for the JSON structure   


              await hubContext.Clients.All.SendAsync("ReceivedMessage", reply);       // Send the "reply" message to all connected SignalR clients via the "ReceivedMessage" event


              return Results.Ok(new { Status = "Update pushed to clients" });

          });


          //----------------------------------------------------------------------------------

          // AI RESPONSE WEBHOOK

          //----------------------------------------------------------------------------------

          // This endpoint receives the AI Agent response from n8n and broadcasts it

          // separately from the individual scraped article messages.


          app.MapPost("/webhook/update-ai", async (

              IHubContext<SignalRn8nTest.SignalRHub.UpdateHub> hubContext,

              HttpContext ctx) =>

          {

              // Read the JSON body sent from the n8n AI HTTP Request node

              var body = await new StreamReader(ctx.Request.Body).ReadToEndAsync();


              // Deserialize the JSON payload

              dynamic dynamicObject = JsonConvert.DeserializeObject(body);


              // Extract the "reply" property

              string reply = dynamicObject["reply"];


              // Send the AI response using a separate SignalR event

              await hubContext.Clients.All.SendAsync("ReceivedAIResponse", reply);


              return Results.Ok(new

              {

                  Status = "AI response pushed to clients"

              });

          });

        • WebScrapingSignalR.razor

          • HTML section

            • Page specific Stylesheet has been moved to WebScrapingSignalR.razor.css

            • @*These are required in this application for JSON deserialization and using PostAsJsonAsync*@

              @using System.Text.Json;

              @using System.Net.Http.Json;

            • @*New QuickGrid component which provides a second way to display tabular data*@

              @using Microsoft.AspNetCore.Components.QuickGrid

            • @* Only display the conversation history when enabled *@

              @if (showAIResponse)

              {

                  @foreach (var conversation in AIConversation)

                  {

                      @* User Query *@

                      <div class="ai-user-query">

                          <strong>

                              <i class="bi bi-person-fill"></i>

                              Your Query:

                          </strong>


                          @conversation.Query

                      </div>


                      @* AI Response *@

                      <div class="ai-response-text">

                          @((MarkupString)conversation.Response)

                      </div>


                      @* Separator between conversations *@

                      @if (conversation != AIConversation.Last())

                      {

                          <hr class="ai-conversation-divider" />

                      }

                  }

              }

          • Code Section

            • //Scraping Specific Declarations

              //---------------------------------------------------------------------------   

              // This property holds the final AI response received from the n8n workflow after processing the scraped articles.

              private string? AIResponse;


              // Holds the complete conversation between the user and the AI Agent.

              // Here the list is a collection of tuples, where each tuple contains the user's query and the corresponding AI response.

              private List<(string Query, string Response)> AIConversation { get; set; } = new();


              //TechnCrunch Scrape

              private List<TechCrunchScrapeResponse> ScrapedArticles { get; set; } = new();


              // Unique identifier for this user's conversation/session.

              // This allows n8n Simple Memory to associate multiple queries with the same conversation.

              private string sessionId = Guid.NewGuid().ToString();

                 

              private string? LastQuery { get; set; }             // Store the last user query so it can be used to send to the n8n workflow for context in followup questions.

              private DateTime? LastScrapeCompleted;              // Store the timestamp of the last completed scrape, used to display when the last update occurred in the UI.             

                     

              //QuickGrid Specific Declarations

              IQueryable<TechCrunchScrapeResponse>? TechQ;   

              PaginationState pagination = new PaginationState { ItemsPerPage = 5 };

            • Class Declarations

              • // Declare a simple class to represent the chat request payload sent to the n8n webhook (not used in this Scraper version ... we use WebSrapeRequest instead)

                // BUT ... this class is still used for data binding and validation in the EditForm.

                // private ChatRequest model = new();

                // <EditForm Model="@model" OnValidSubmit="StartWebScraper">

                private class ChatRequest

                {

                    [Required(ErrorMessage = "Message is required.")]

                    public string? Message { get; set; }

                }

              • // Declare a class to represent the response from the TechCrunch scraping workflow. This class is used to DESERIALIZE the JSON response received from the SignalR hub.

                private class TechCrunchScrapeResponse

                {

                    public string? Title { get; set; }                  // Title of the article

                    public string? Url { get; set; }                    // URL of the article in the table

                    public string? Category { get; set; }

                    public List<string>? Authors { get; set; }

                    public string? PublishedAt { get; set; }

                    public string? PublishedRelative { get; set; }

                    public string? ImageUrl { get; set; }               //used by the <img>

                    public string? ImageAlt { get; set; }

                    public string? Duration { get; set; }

                }

              • // Declare a class to represent the request payload sent to the n8n webhook for web scraping.

                // This class is used to serialize the user's query into JSON format when making the POST request.

                public class WebScrapeRequest

                {

                    public string Query { get; set; } = string.Empty;

                    public string SessionId { get; set; } = string.Empty;

                }

            • // This method initializes the SignalR connection when the component is first rendered.

              protected override async Task OnInitializedAsync()

              {

                  // Initialize SignalR connection to the hub at the specified URL, with automatic reconnection enabled.

                  //------------------------------------------------------------------------------------------

                  // The hub URL is relative to the base URL of the application ... see the SignalRHub folder in the project for the server-side implementation.

                  hubConnection = new HubConnectionBuilder()

                      .WithUrl(Nav.ToAbsoluteUri("/updateHub"))

                      .WithAutomaticReconnect()

                      .Build();


                  // Register a handler for the "ReceivedMessage" and "ReceivedAIResponse" event from the SignalR hub.       

                  // Here we listen for TWO separate SignalR event messages sent from the server

                  // referenced by the "ReceivedMessage" or "ReceivedAIResponse" event and update the UI accordingly

                  //-----------------------------------------------------------------------------------------               


                  //Scraper Event

                  //Used to handle the received messages from the SignalR hub, which are expected to be JSON strings representing TechCrunchScrapeResponse objects.

                  //So we need to deserialize the JSON string into a TechCrunchScrapeResponse object and add it to the ScrapedArticles list, then update the UI accordingly.

                  hubConnection.On<string>("ReceivedMessage", (reply) =>

                  {

                      Console.WriteLine("SIGNALR REPLY:");

                      Console.WriteLine(reply);


                      ScrapeReply = reply;    // Store the raw reply for debugging purposes, but not used since we are deserializing the JSON into a TechCrunchScrapeResponse object.


                      try

                      {

                          //Note JsonSerializerOptions is used to make the property name matching case-insensitive, allowing for more flexible deserialization.

                          //Here we Deserialize the JSON string received from the SignalR hub into a TechCrunchScrapeResponse object, which represents a single scraped article.

                          var article = JsonSerializer.Deserialize<TechCrunchScrapeResponse>(

                              reply,

                              new JsonSerializerOptions

                              {

                                  PropertyNameCaseInsensitive = true

                              });

                             

                          //Note the use of InvokeAsync to ensure that the UI update occurs on the correct thread, as SignalR callbacks may be executed on a different thread than the UI thread.

                          if (article != null)

                          {

                              InvokeAsync(() =>

                              {

                                  ScrapedArticles.Add(article);                           // Add the newly received article to the list of scraped articles.

                                  TechQ = ScrapedArticles.AsQueryable();                  // Update the QuickGrid data source to reflect the new list of articles.

                                  ScrapingStatus = "Articles received. Processing...";    // Update the scraping status to inform the user that articles have been received and are being processed.

                                  StateHasChanged();

                              });

                          }

                      }

                      catch (JsonException ex)

                      {

                          Console.WriteLine($"JSON Error: {ex.Message}");

                      }

                  });


                  //AI Response Event

                  //Here we listen for the "ReceivedAIResponse" event from the SignalR hub, which is expected to be a string containing the AI's response.

                  //No need to deserialize since the AI response is a simple string. We normalize the HTML, add it to the conversation history, and update the UI accordingly.

                  hubConnection.On<string>("ReceivedAIResponse", (reply) =>

                  {

                      //Console.WriteLine("AI RESPONSE RECEIVED FROM SIGNALR:");

                      //Console.WriteLine(reply);


                      InvokeAsync(() =>

                      {

                          // Normalize the HTML returned by the AI Agent ... we do this because the AI Agent may return HTML with empty paragraphs,

                          // excessive line breaks, or other formatting issues that can cause large gaps in the chat bubbles and AI response sections of the UI.

                          var normalizedResponse = NormalizeHtml(reply);


                          // Add this query/response pair to the conversation history.If this is the first query, LastQuery will be null, so we use an empty string in that case.

                          // LastQuery is set in the StartWebScraper method when the user submits a new query, so it always contains the most recent user query when the AI response is received.

                          AIConversation.Add((

                              LastQuery ?? string.Empty,

                              normalizedResponse

                          ));


                          // Keep AIResponse pointing to the latest response... but is not used in this updated version since we are now storing the entire conversation in AIConversation.

                          AIResponse = normalizedResponse;


                          ScrapingStatus = "Scraping and AI analysis completed.";


                          // Store the timestamp of the last completed scrape.

                          LastScrapeCompleted = DateTime.Now;


                          // The entire workflow is now finished.This will turn off the loading animation.

                          isLoading = false;


                          StateHasChanged();

                      });

                  });


                  // Start the SignalR connection.

                  await hubConnection.StartAsync();

              }

            • // This is the updated method to start the web scraper workflow in n8n via an HTTP POST request with Basic Authentication.

              // The old method used a GET request, which is less flexible and doesn't allow for sending complex data like the user's query and session ID (payload).

              private async Task StartWebScraper()

              {

                  // Check if the message is longer than the allowed maximum.

                  if (model.Message?.Length > MaxLength)

                  {

                      StatusMessage =

                          "Query exceeds maximum length of " + MaxLength + " characters.";

                      return;

                  }


                  // Do not allow an empty scraping/AI query.

                  if (string.IsNullOrWhiteSpace(model.Message))

                  {

                      StatusMessage = "Please enter a query before starting the scraper.";

                      return;

                  }


                     

                  IsSending = true;                   // Set the sending state to true.

                  isLoading = true;                   // Set the loading state to true to show a loading indicator (animated gif) while the scraping and AI analysis are in progress.


                  // Clear any previous status messages.

                  StatusMessage = null;

                  ScrapingStatus = "";


                  // Clear the previous final webhook response... not implemented in this version of the app

                  WebhookReply = null;


                  // Clear previous AI response.... this was replaced with a more sophisticated approach to handle multiple AI responses if needed.

                  // AIResponse = null;


                  // IMPORTANT:

                  // For a new search, clear the previous scraped articles

                  // ... even though the user may want to see the previous articles, we clear them to avoid confusion and ensure that the new results are displayed cleanly.

                     

                  ScrapedArticles.Clear();    // Clear the list of scraped articles to prepare for new results.

                  TechQ = null;               // Clear the QuickGrid data source.


                  // Store the last user query for reference.

                  // Used in the AI response handler to associate the AI's response with the correct user query in the conversation history that is being stored in AIConversation.

                  // AIConversation.Add((LastQuery ?? string.Empty, normalizedResponse)

                  LastQuery = model.Message; 


                  // Create the payload (of type WebScrapeRequest) sent to the n8n POST webhook consisting of the user's query and the unique session ID for this conversation.

                  // Note model.Message is bound to the user's input in the EditForm...where model is an instance of the ChatRequest class .

                  var payload = new WebScrapeRequest

                  {

                      Query = model.Message,

                      SessionId = sessionId

                  };


                  try

                  {

                      // Add Basic Authentication.

                      var credentials = Convert.ToBase64String(

                          System.Text.Encoding.ASCII.GetBytes("c:1"));


                      Http.DefaultRequestHeaders.Authorization =

                          new System.Net.Http.Headers.AuthenticationHeaderValue(

                              "Basic",

                              credentials);


                      // Send the user's query and session ID to the n8n webhook.

                      var resp = await Http.PostAsJsonAsync(n8nWebHookPath, payload);


                      // Check whether the POST request was successful.

                      if (resp.IsSuccessStatusCode)

                      {

                          // Read the HTTP response from the Respond to Webhook node.

                          var reply = await resp.Content.ReadAsStringAsync();


                          if (string.IsNullOrWhiteSpace(reply))

                          {

                              reply = "(no response)";

                          }


                          // This is your final HTTP response after the workflow completes

                          // ... just for debugging purposes, not used in this version of the app since we are using SignalR to receive the scraped articles and AI responses.

                          WebhookReply = reply;


                          ScrapingStatus = "? Scraping workflow has started. Waiting for articles...";

                      }

                      else

                      {

                          // Read the error returned by the server.

                          var error = await resp.Content.ReadAsStringAsync();


                          StatusMessage =

                              $"Error: {resp.StatusCode} {error}";

                      }

                  }

                  catch (Exception ex)

                  {

                      StatusMessage =

                          $"Network or server error: {ex.Message}";

                  }

                  finally

                  {

                      // Clear the user's query box.

                      model.Message = string.Empty;


                      // Re-enable the button.

                      IsSending = false;


                      // Refresh the UI.

                      await InvokeAsync(StateHasChanged);

                  }

              }   

    • Supplementary Demos

      • SignalRn8nImgAnalysisWebcam

        • This is an updated version of the application created in Lecture 224 "Image Analysis with Blazor and n8n:  Human in the Loop Receipt Approval"

          • We created a  workflow that accepts a receipt image from a Blazor Web App, passes it to an AI Agent for analysis, extracts the receipt details and total amount, and then uses that information to determine whether the receipt can be automatically approved or requires further review.

          • For receipts that exceed the automatic approval threshold, we introduce a Human-in-the-Loop process. The workflow pauses while a manager reviews the receipt  (view email) and chooses to approve or reject it. The resulting decision is then returned to the Blazor application.

        • In this version we add the capability of the Blazor Web App to connect to a webcam to take a picture of the receipt.

          • We use two techniques to access the users camera . The first uses the OpenCvSharp library and the second technique uses Javascript.

          • The user then can take a snapshot of the receipt and it is automatically saved into the local wwwroot folder  and instantly available for upload to our n8n workflow for analysis

        • We also added a right click context menu capability , so a user can delete any image files from the onscreen list.

  • Blazor + n8n + Telegram: See, Speak, Reason and More ... Part 125:59

    In this Lecture we will

    • Start to build a Blazor Web App that connects to an  AI assistant in n8n that integrates with  Telegram - it sees images, understands voice, replies with audio, does language translations and uses tools to handle your email, calendar, and web search.

    • Telegram is a cloud-based instant messaging and social media app used for sending texts, sharing large files, and broadcasting information to massive online groups.

      • Core Uses

        • Direct Messaging: Send text, voice notes, photos, and videos to friends or family.

        • Large Group Chats: Host communities with up to 200,000 members for gaming, study groups, or social clubs.

        • Broadcast Channels: Follow public or private channels for one-way updates from news outlets, creators, or brands.

        • File Sharing: Send large files and uncompressed media up to gigabytes in size across multiple devices.

        • Voice and Video Calls: Make encrypted audio and video calls or hold public live streams. [1, 2, 3, 4, 5, 6]

        Key Features

        • Cloud Syncing: Access your chat history instantly across phones, tablets, and computers at the same time without needing your phone online.

        • Secret Chats: Use optional end-to-end encryption with self-destructing messages for private conversations.

        • Bots and Automation: Use automated bots for games, reminders, file conversions, or group moderation.

      • Setup

        • Download and install the mobile and then desktop app

        • Start chat with botfather https://telegram.me/BotFather  (/start)

        • Enter the /newbot command to create a new bot

        • Create a name (Telegram Bot) and a unique username ... must end with 'bot'  chiarelli_n8n_bot

        • New bot will be at https://t.me/chiarelli_n8n_bot

        • Write down the Access Token to access the HTTP API (in n8n it will be used in the Telegram Trigger - On message  node)


    • Step by Step n8n Implementation

      • Version 1

        • Set your Time Zone (go to Settings ... top right corner 3 dots)  ... useful for Calendar, Emails, Research and other upcoming features eg. America/Toronto

        • Telegram Text

          • Telegram Trigger (On message) -> AI Agent (OpenAI Chat Model - Simple Memory) -> Telegram Send a Text Message (remember to disable Append n8n Attribution)

        • Telegram Voice

          • Telegram Trigger -> Switch

          • Switch (Audio) -> Get Audio (Telegram Get File) -> OpenAI Transcribe Recording -> AI Agent

          • Switch (Text) -> Agent Input (Edit Fields Set) -> AI Agent

          • AI Agent (Model-Simple Memory) -> IF

          • IF (true) -> OpenAI Generate Audio -> Telegram Audio Response (Telegram Send an Audio File)

          • IF (false) -> Telegram Send a Text Message

    • Blazor Web App (SignalRn8nTelegram)

      • This first simple application starts by providing the user access to the Telegram AI Assistant  via the Desktop app ... pointing to  https://t.me/chiarelli_n8n_bot or optionally  via their Cell Phone by starting up the Telegram App and searching for https://t.me/chiarelli_n8n_bot (Telegram Bot)

        • The user selects the Telegram link to open the assistant and begin a conversation.

        • Telegram sends your message to the n8n workflow. The n8n AI Agent processes your request and sends the response back to Telegram.

        • There is no other interaction with the Blazor component ... we add SignalR in our second more advanced implmentation.

      • <a href="https://t.me/chiarelli_n8n_bot"

           target="_blank"

           class="btn btn-primary btn-lg">

            <i class="bi bi-telegram me-2"></i>

            Open Telegram AI Assistant

        </a>


        <button class="btn btn-secondary btn-lg me-2"

                @onclick="OpenTelegramModal">

            <i class="bi bi-phone me-2"></i>

            Cell Phone Option

        </button>

  • Blazor + n8n + Telegram: See, Speak, Reason and More ... Part 21:12:21

    In this Lecture we will

    • Continue our introduction to Blazor + n8n + Telegram integration

    • Step by Step n8n Implementation

      • Version 2

        • Telegram Vision

          • Telegram Trigger -> Switch

          • Switch (Image ... an array of images ) -> Download Image  (Telegram Get File) -> Code (MimeType to image/jpeg ) -> OpenAI Analyze Image -> Format Image Input (Edit Fields Set) -> Trigger Send a Text Message

        • Telegram Email

          • AI Agent -> Tools -> Read Gmail (Gmail Tool) - Send a Message in Gmail (Gmail Tool)

          • Note: You will need Gmail Credentials ie Client ID and Client Secret

            • Make note of OAuth Redirect URL : https://n8n.chiarelli.cc/rest/oauth2-credential/callback it will be used for Authorised redirect URIs

            • Make note of local host n8n address: https://n8n.chiarelli.cc for use in Authorised JavaScript origins

            • Go to Google Cloud console

            • API and Services

            • Credentials -> Create Credentials -> OAuth client ID -> Web Application

            • Select Gmail API and enable

        • Telegram Calendar

          • AI Agent -> Tools -> Get Calendar (Google Calendar Tool) - Create Event (Google Calendar Tool)

          • Make sure the AI Agent knows the current date and time otherwise it will use the last "Training Date" ie possibly 2023

            • CURRENT DATE AND TIME

              The current date and time is {{ $now }}.

        • Telegram Web Search

          • AI Agent -> Tools -> Google search in SerpApi

            • Need to get API key

          • Sample Test Query "Get the latest AI news and email the results to chiarelli.charlie@gmail.com"

        • Update : Blazor + n8n Connection Techniques

          • Blazor - n8n  Simple Connection Test

            • Webhook (POST) -> Respond to Webhook (respond with JSON)

            • All this does is make sure n8n is up and running otherwise our Telegrame integration wont work.

          • Blazor - n8n Advanced SignalR Connection

            • No Webhook into Workflow in this implementation , rather we use a link to the Telegram Bot (https://t.me.chiarelli_n8n_bot) which then activates the workflow with a Trigger On "Message"

            • AI Agent -> HTTP Request (POST)

        • Send Gmail HITL

          • System Message
            Send a Message in Gmail

            Use this tool to send emails. This tool requires the recipient email, the email subject and body text

            Before sending any email ask the user to accept or reject

        • Security - Block other users

          • Only allow access to a specific Telegram user and report access denied to others

          • Telegram Trigger -> IF

            • {{ $json.message.from.id }} # is equal to "user message id"
              9876543219

            • IF (true) -> Switch

            • IF (false) -> Send a text message

              • Chat ID {{ $('Telegram Trigger').item.json.message.chat.id }}

              • Text: Invalid User

          • Note also in this update the addition of "Send Text Version of Audio Response" node

            • AI Agent  -> IF (true) -> Send Text Version of Audio Response

    • Blazor Web App (SignalRn8nTelegram)

      • This application starts by providing access to the Telegram AI Assistant.

        • The user selects the Telegram link to open the assistant and begin a conversation.

        • Telegram sends your message to the n8n workflow. The n8n AI Agent processes your request and sends the response back to Telegram.

        • At the same time, n8n sends the question and AI response to this Blazor application through an HTTP Request  webhook and SignalR, allowing the conversation to be displayed here in real time.

          Blazor ↔ n8n ↔ Telegram

      • Programming Details

        • Program.cs
          //----------------------------------------------------------------------------------

          // TELEGRAM AI RESPONSE WEBHOOK

          //----------------------------------------------------------------------------------

          // This endpoint is specifically for the Telegram AI Assistant.

          // It receives both the user's Telegram query and the AI Agent's response

          // from n8n, then sends them to the Blazor clients through SignalR.

          //


          app.MapPost("/webhook/telegram-ai", async (

              IHubContext<SignalRn8nTest.SignalRHub.UpdateHub> hubContext,

              HttpContext ctx) =>

          {

              // Read the JSON body sent from the n8n HTTP Request node

              var body = await new StreamReader(ctx.Request.Body).ReadToEndAsync();


              // Deserialize the JSON payload

              dynamic dynamicObject = JsonConvert.DeserializeObject(body);


              // Extract the Telegram query and AI response

              string query = dynamicObject["query"];

              string reply = dynamicObject["reply"];


              // Send both values to the Blazor clients

              await hubContext.Clients.All.SendAsync(

                  "ReceivedTelegramAIResponse",

                  query,

                  reply);


              return Results.Ok(new

              {

                  Status = "Telegram AI response pushed to clients"

              });

          });

        • TelegramSee.razor

          • HTML

            • <button class="btn btn-success btn-lg me-2" style="font-size:0.75rem;" @onclick="TestN8nConnection"><i class="bi bi-diagram-3 me-2"></i>Test n8n Connection</button>

            • @if (!string.IsNullOrEmpty(N8nResponse))

              {

                  <div class="alert alert-success mt-3">

                      @N8nResponse

                  </div>

              }

            • <a href="https://t.me/chiarelli_n8n_bot"

                 target="_blank"

                 class="btn btn-primary btn-lg me-2">

                  <i class="bi bi-telegram me-2"></i>

                  Open Telegram AI Assistant

              </a>

            • <div class="card-body">

                  <p>

                      <strong>Question:</strong><br />

                      @TelegramQuery

                  </p>


                  <p class="mb-0">

                      <strong>AI Response:</strong><br />

                      @((MarkupString)FormatTelegramResponse(TelegramReply))

                  </p>

              </div>

        • Code

          • protected override async Task OnInitializedAsync()

            {       


                // Initialize SignalR connection to the hub at the specified URL, with automatic reconnection enabled.

                //--------------------------------------------------------------------------------------------------------

                // The hub URL is relative to the base URL of the application ... see the SignalRHub folder in the project for the server-side implementation.

                hubConnection = new HubConnectionBuilder()

                    .WithUrl(Nav.ToAbsoluteUri("/updateHub"))

                    .WithAutomaticReconnect()

                    .Build();


                // Register a handler for AI responses coming from the Telegram Assistant.

                // The n8n workflow will send both the original Telegram query and the AI response.

                hubConnection.On<string, string>("ReceivedTelegramAIResponse", (query, reply) =>

                {

                    // Store the query and AI response for display in the Blazor UI.

                    TelegramQuery = query;

                    TelegramReply = reply;


                    // Refresh the Blazor UI when the SignalR message is received.

                    InvokeAsync(StateHasChanged);

                });

                   


                // Start the SignalR connection.

                await hubConnection.StartAsync();

            }

          • private async Task TestN8nConnection()

            {

                var response = await Http.PostAsync("https://n8n.chiarelli.cc/webhook/blazor-test", null);


                if (response.IsSuccessStatusCode)

                {

                    //var json = await response.Content.ReadAsStringAsync();

                    //N8nResponse = json;


                    var json = await response.Content.ReadFromJsonAsync<N8nResponseModel>();

                    N8nResponse = json?.message ?? "No response received from n8n.";


                }

                else

                {

                    N8nResponse = $"n8n returned an error: {response.StatusCode}";

                }

            }   

            private class N8nResponseModel

            {

                public string? status { get; set; }

                public string? message { get; set; }

            }

          • Telegram Trigger ->  IF (true) ->  Switch (PDF) -> Download PDF -> Extract from File (Extract from PDF) -> Edit Fields(Set) -> HTTP Request

          • The built-in Extract From File node hit a PDF.js dependency/version issue, so I replaced it with a community PDF Parse node while keeping the rest of the workflow unchanged.

            • Go to Settings bottom left ... Community Nodes

            • Install: n8n-nodes-pdf-parse 

    • Suggested Exercises

      • Add the capability to Upload a PDF file to the n8n workflow.

        • The AI Agent should provide a brief summary of its contents.

        • If the pdf is a resume, the user has the option to add a query to the caption/prompt asking the AI  to critique the resume according to the related position the person is applying to.

        • The AI Agent should respond with an overall review and suggestions for improvements.

        • Solution:

          • Telegram Trigger ->  IF (true) ->  Switch (PDF) -> Download PDF -> Extract from File (Extract from PDF) -> Edit Fields(Set) -> HTTP Request

          • The built-in Extract From File node hit a PDF.js dependency/version issue, so I replaced it with a community PDF Parse node while keeping the rest of the workflow unchanged.

            • Go to Settings bottom left ... Community Nodes

            • Install: n8n-nodes-pdf-parse 

      • Add the capability to Create Images

        • Switch (Create) -> Generate Image -> Send a Photo Image -> HTTP Request2

      • Also note the following updates:

        • The image analysis now returns results back to Blazor via a new HTTP Request1
          Download Image -> Code in Javascript -> Analyze image -> Format Image Output -> HTTP Request1

        • The Voice (Audio)  results (transcribed) are send back to Blazor via a revamped original HTTP Request node
          {

            "query": {{ JSON.stringify($('Telegram Trigger').item.json.message.voice ? $('Transcribe a recording').item.json.text : ($('Telegram Trigger').item.json.message.text || $('Telegram Trigger').item.json.message.caption || 'Please provide a concise summary of this PDF.')) }},

            "reply": {{ JSON.stringify($json.output) }}

          }

        • In Telegram we updated the Telegram bot profile ... Open BotFather and enter /mybots

          • Select chiarelli_n8n_bot and choose Edit Bot

          • Edit About

            • AI Assistant powered by Blazor, n8n and OpenAI.

          • Edit Description

            • Send text, voice messages, images and PDFs for AI analysis. Create AI images, work with Gmail and Google Calendar, search the web and translate text.

              Type /help to see all available features and examples.

          • Back in n8n we updated Switch and added a new rule for /help ... just after Create Image and Before Text

            • Value 1:

              {{ $json.message.text }}


              Operation:

              equals


              Value 2:

              /help

          • Connected to the help branch we added a Telegram Send Text Message Node

            • Chat Id

              • {{ $('Telegram Trigger').item.json.message.chat.id }}

            • Text
              AI Assistant Help

              Text — Ask me a question

              Voice — Send a voice recording

              Images — Send an image for analysis

              PDFs — Send a PDF for analysis

              Create Image — Type "create image" + description

              Gmail — Read or send email (with approval)

              Calendar — Read or create events

              Web — Search for current information

              Translate — Start with "Translate"

              SignalR — Responses also update the Blazor app

    • Looking Back — and Looking Forward

      • As we bring these two lectures to a close, I think it is worth stepping back for a moment and looking at how our approach to building AI applications has evolved throughout this course. Much of our earlier work concentrated almost entirely on Blazor. We integrated AI models directly into Blazor applications, experimented with RAG, built agent-based applications, explored Microsoft Agent Framework, and even created Blazor Hybrid applications capable of running on mobile devices. All of that remains valuable. Blazor gives us something extremely important: complete control over our application. We decide what the interface looks like, how users interact with it, how our application is structured and exactly how the different pieces fit together.

      • More recently, however, we have begun moving beyond the idea that Blazor has to do everything itself. Our introduction of n8n was an important step in that direction. Instead of writing all of the integration and orchestration code ourselves, we could allow a Blazor application to initiate an n8n workflow and let that workflow communicate with AI models, databases, email systems, calendars, web services and other external resources. Blazor could remain the application interface while n8n became an automation and orchestration layer behind it.

      • In these latest lectures we have taken that idea another step further by introducing Telegram. Telegram gives us something that would require a considerable amount of work to reproduce ourselves: a mature mobile and desktop messaging interface that already understands text, voice messages, photographs and documents. By connecting a Telegram bot to our n8n workflow, we were able to turn Telegram into the front end for a surprisingly capable AI assistant. Our user can type a question, speak a question, submit an image for AI vision analysis or upload a PDF document for analysis. The AI Agent can then go beyond simply answering questions. Through tools connected within n8n, it can search the web, work with Gmail, access a calendar and perform other useful operations.

        That is an enormous amount of functionality without having to build an entire mobile application ourselves.

        This became particularly apparent when comparing this solution with some of our earlier Blazor Hybrid work. We certainly can build a mobile AI application with Blazor Hybrid, and doing so gives us far greater control over the interface and user experience. But supporting cameras, files, voice input, permissions, mobile platforms and the surrounding application infrastructure requires considerably more development work. Telegram already provides much of that infrastructure.

      • There is, of course, a trade-off. With a Blazor or Blazor Hybrid application, we own the user experience. We control every button, component, page, layout and interaction. With Telegram, we are borrowing somebody else's user experience. We gain tremendous convenience and cross-platform capability, but our application must operate within the interface and capabilities Telegram provides. Neither approach is inherently better.

        The interesting question for a modern software developer is increasingly:

        Which parts of this application should I actually build myself, and which parts can I integrate, orchestrate or reuse? That question represents an important change in the way we have approached development throughout this course.

      • Modern AI development is no longer limited to writing one large application containing everything. We now have several approaches available to us. We can build traditional code-first applications with Blazor and .NET. We can use visual workflow automation platforms such as n8n to orchestrate external services. We can use existing communication platforms such as Telegram as application channels. And we can use agent frameworks to create increasingly sophisticated AI systems capable of reasoning, using tools and coordinating workflows.

      • Our final implementation also demonstrates that these approaches do not have to exist in separate silos.

        Telegram provides our mobile interface. n8n orchestrates our workflow and AI tools. The AI Agent handles the intelligent interaction. And through an HTTP request and SignalR, the activity can be pushed back into our Blazor application in real time. So Blazor has not disappeared from the architecture. Its role has changed. Instead of asking Blazor to be the user interface, AI engine, workflow system, integration platform and mobile application all at once, we are allowing each technology to do what it does particularly well.

      • And what about Microsoft Agent Framework? I certainly would not leave it behind.

        Microsoft Agent Framework represents another important direction for us as .NET developers. It gives us a code-first environment for building agents, tools, memory, RAG, multi-agent systems and sophisticated workflows while remaining firmly within the C# and .NET ecosystem. That makes it especially attractive when we require greater control over our agent architecture, application logic, testing, deployment and long-term maintainability.

      • n8n approaches the problem from another direction. It allows us to visually assemble workflows and connect services extremely quickly. For applications involving Gmail, calendars, webhooks, databases, APIs, human approval and other external systems, that can dramatically reduce the amount of integration code we need to write.

      • So I don't see our recent work with n8n and Telegram as replacing Microsoft Agent Framework.

        I see it as expanding our toolbox. There will be applications where Blazor and Microsoft Agent Framework are the right combination. There will be applications where Blazor and n8n make more sense. There will be situations where Telegram provides an excellent ready-made mobile interface. And increasingly, there will be applications that combine several of these technologies. That is really the larger lesson I hope you take away from these lectures.

      • We started by asking:

        How can I add AI to my Blazor application?

        We are now beginning to ask a much broader question:

        How can I use Blazor, AI agents, automation workflows, messaging platforms and external services together to build applications that would be difficult—or unnecessarily complicated—to build with any one technology alone?

        I think that is an exciting progression, and it is also a very natural direction for us as .NET developers.

        Blazor remains our foundation, but it no longer needs to be our boundary.


Requirements

  • You should have a basic understanding of how to create simple web pages using HTML
  • Having some knowledge of CSS will help but it's not required
  • You should have a basic beginners understanding of C# (STRONGLY recommended)
  • You should have Beginners knowledge of ASP.NET and specifically the Blazor Framework
  • You should have access to the Visual Studio IDE for Windows ... Community Edition Version 2022/2026 (freely downloadable)

Description

Artificial Intelligence is transforming software development, and Blazor developers now have access to an incredible set of tools for building intelligent, responsive, and modern applications.

In this course, you'll learn how to integrate AI capabilities directly into your Blazor applications using practical, hands-on examples designed for developers who are new to AI as well as those looking to expand their existing skills.

We'll begin by exploring AI integration fundamentals, including working with the OpenAI API, running local Large Language Models (LLMs), and using AI to analyze web content and generate SQL queries. From there, we'll dive into advanced topics such as Retrieval Augmented Generation (RAG), AI-powered data access, and Blazor Hybrid applications enhanced with intelligent features.

You'll also gain valuable experience with the latest AI development tools and frameworks, including GitHub Copilot Agent Mode, the Microsoft Agent Framework, custom tools, MCP, structured outputs, multi-agent systems, workflows, human-in-the-loop agents, and the Responses API. Along the way, we'll examine emerging technologies such as AG-UI and discuss the evolving landscape of AI development within the .NET ecosystem.

By the end of this course, you'll have the knowledge and confidence to design, build, and deploy AI-powered Blazor applications using modern techniques, tools, and best practices available today.

Update Sept 2026

  • Integrating a Blazor Web App with n8n Automations implementing SignalR for Real Time two way communication

    • n8n is a powerful, low-code automation platform that allows you to connect over 400 apps and integrate AI agents without complex coding. You build workflows using visual triggers, data transformation nodes, and action nodes, and can choose to self-host on your own infrastructure or use their cloud service.

    • This is a highly relevant, bleeding-edge architecture. The combination of Blazor Web Apps for interactive, responsive front-ends and n8n for back-end node-based multi-agent orchestration is an incredibly smart engineering choice. The lack of noise in the ASP.NET community is not a sign of outdated technology; it is simply a reflection of a traditional community lag and standard ecosystem silos

    • Why This Stack is Ahead of the Curve

      • Separation of Concerns: Writing multi-step, multi-agent logic entirely in C# means writing heavy boilerplate, dealing with state persistence, and constantly rewriting code when tweaking LLM prompts or agent steps. Passing orchestration to n8n keeps Blazor lightweight.

      • Rapid Iteration: n8n acts as your visual, living backend. You can modify a prompt, change an agent's tool, or swap out an LLM provider (e.g., swapping OpenAI for Claude or Gemini) on the fly in n8n without a single recompilation or redeployment of your Blazor app.

      • The "Glue" Factor: n8n has hundreds of native integrations (Google Sheets, Slack, databases, CRM systems). Building those connections natively via C# NuGet packages takes weeks; in n8n, it takes seconds

    • Sampling of Topics covered

      • Simple queries with responses directed to user specified emails

      • Sequential workflows

      • Connecting to an AirTable database and using Natural Language to  perform queries and full CRUD functionality with voice input

      • Connecting to an n8n workflow which performs RAG data ingestion using a Qdrant Vector Store and Document analysis via an AI Agent .

      • Image Analysis with Blazor and n8n:  Human in the Loop Receipt Approval

      • Web Scraping where the Blazor web app uploads a payload (user query and sessionId) to an n8n workflow that initiates a web scrape and returns the results back to Blazor  (using SignalR). The results are then deserialized according to a predefined class and be displayed in tabular form. We then use an AI Agent to answer natural language questions about the scraped content. The key here is that we are now using two unique Webhook endpoints and two unique SignalR events

      • Take our Blazor and n8n integration in an exciting new direction by bringing Telegram into the picture (mobile or desktop) . We're going to build an AI-powered assistant that goes far beyond a traditional text-based chatbot. This assistant will be able to see images, understand spoken questions, respond with both text and audio, translate languages, search the web, work with email and calendars, and much more.

    • Through practical, real-world examples, you'll see how this integration can take a traditional Blazor application and turn it into a smart, connected, and automated business application—while keeping the complexity of workflow orchestration outside your core application.


Who this course is for:

  • Beginner ASP.NET developers who want to extend their skills related to implementing AI in Blazor specific applications
  • Experienced Developers who want to gain expertise in more advanced topics like Hybrid Applications (mobile/desktop/web) and AI implementations