Showing posts with label Easy. Show all posts
Showing posts with label Easy. Show all posts

Saturday, December 14, 2024

.NET Aspire and Semantic Kernel AI

 Let's learn how to use the .NET Aspire Azure OpenAI client. We will familiarize ourselves with the Aspire.Azure.AI.OpenAI library, which is used to register an OpenAIClient in the dependency injection (DI) container for consuming Azure OpenAI or OpenAI functionality. In addition, it enables corresponding logging and telemetry.

Companion Video: https://youtu.be/UuLnCRdYvEI
Final Solution Code: https://github.com/medhatelmasry/AspireAI_Final

Pre-requisites:

  • .NET 9.0
  • Visual Studio Code
  • .NET Aspire Workload
  • "C# Dev Kit" extension for VS Code

Getting Started

We will start by cloning a simple C# solution that contains two projects that use Semantic Kernel - namely a console project (ConsoleAI) and a razor-pages project (RazorPagesAI). Clone the project in a working directory on your computer by executing these commands in a terminal window:

git clone https://github.com/medhatelmasry/AspireAI.git
cd AspireAI

The cloned solution contains a console application (ConsoleAI) and a razor-pages application (RazorPagesAI). They both do pretty much do the same thing. The objective of today’s exercise is to:

  • use .NET Aspire so that both projects get started from one place 
  • pass environment variables to the console and razor-pages web apps from the .AppHost project that belongs to .NET Aspire

Open the solution in VS Code and update the values in the following appsettings.json files with your access parameters for Azure OpenAI and/or OpenAI:

ConsoleAI/appsettings.json
RazorPagesAI/appsettings.json

The most important settings are the connection strings. They are identical in both projects:

"ConnectionStrings": {
  "azureOpenAi": "Endpoint=Azure-OpenAI-Endpoint-Here;Key=Azure-OpenAI-Key-Here;",
  "openAi": "Key=OpenAI-Key-Here"
}

After you update your access parameters, try each application separately to see what it does:

Here is my experience using the console application (ConsoleAI) with AzureOrOpenAI set to “OpenAI”:

cd ConsoleAI
dotnet run


I then changed the AzureOrOpenAI setting to “Azure” and ran the console application (ConsoleAI) again:

Next, try the razor pages web application (RazorPagesAI) with AzureOrOpenAI set to “OpenAI”:

cd ../RazorPagesAI
dotnet watch


In the RazorPagesAI web app’s appsettings.json file, I changed AzureOrOpenAI to “Azure”, resulting in a similar experience.


In the root folder, add .NET Aspire to the solution:

cd ..
dotnet new aspire --force

Add the previous projects to the newly created .sln file with:

dotnet sln add ./AiLibrary/AiLibrary.csproj
dotnet sln add ./RazorPagesAI/RazorPagesAI.csproj
dotnet sln add ./ConsoleAI/ConsoleAI.csproj

Add the following .NET Aspire agent packages to the client ConsoleAI and RazorPagesAI projects with:

dotnet add ./ConsoleAI/ConsoleAI.csproj package Aspire.Azure.AI.OpenAI --prerelease
dotnet add ./RazorPagesAI/RazorPagesAI.csproj package Aspire.Azure.AI.OpenAI --prerelease

To add Azure hosting support to your IDistributedApplicationBuilder, install the 📦 Aspire.Hosting.Azure.CognitiveServices NuGet package in the .AppHost project:

dotnet add ./AspireAI.AppHost/AspireAI.AppHost.csproj package Aspire.Hosting.Azure.CognitiveServices

In VS Code, add the following references:

  1. Add a reference from the .AppHost project into ConsoleAI project.
  2. Add a reference from the .AppHost project into RazorPagesAI project.
  3. Add a reference from the ConsoleAI project into .ServiceDefaults project.
  4. Add a reference from the RazorPagesAI project into .ServiceDefaults project.

Copy the AI and ConnectionStrings blocks from either the console (ConsoleAI) or web app (RazorPagesAI)  appsettings.json file into the appsettings.json file of the .AppHost project. The appsettings.json file in the .AppHost project will look similar to this:

"AI": {
  "AzureOrOpenAI": "OpenAI",
  "OpenAiChatModel": "gpt-3.5-turbo",
  "AzureChatDeploymentName": "gpt-35-turbo"
},
"ConnectionStrings": {
  "azureOpenAi": "Endpoint=Azure-OpenAI-Endpoint-Here;Key=Azure-OpenAI-Key-Here;",
  "openAi": "Key=OpenAI-Key-Here"
}

Add the following code to the Program.cs file in the .AppHost project just before builder.Build().Run()

IResourceBuilder<IResourceWithConnectionString> openai;
var AzureOrOpenAI = builder.Configuration["AI:AzureOrOpenAI"] ?? "Azure"; ;
var chatDeploymentName = builder.Configuration["AI:AzureChatDeploymentName"];
var openAiChatModel = builder.Configuration["AI:OpenAiChatModel"];
 
// Register an Azure OpenAI resource. 
// The AddAzureAIOpenAI method reads connection information
// from the app host's configuration
if (AzureOrOpenAI.ToLower() == "azure") {
    openai = builder.ExecutionContext.IsPublishMode
        ? builder.AddAzureOpenAI("azureOpenAi")
        : builder.AddConnectionString("azureOpenAi");
} else {
    openai = builder.ExecutionContext.IsPublishMode
        ? builder.AddAzureOpenAI("openAi")
        : builder.AddConnectionString("openAi");
}
 
// Register the RazorPagesAI project and pass to it environment variables.
//  WithReference method passes connection info to client project
builder.AddProject<Projects.RazorPagesAI>("razor")
    .WithReference(openai)
    .WithEnvironment("AI__AzureChatDeploymentName", chatDeploymentName)
    .WithEnvironment("AI__AzureOrOpenAI", AzureOrOpenAI)
    .WithEnvironment("AI_OpenAiChatModel", openAiChatModel);
 
 // register the ConsoleAI project and pass to it environment variables
builder.AddProject<Projects.ConsoleAI>("console")
    .WithReference(openai)
    .WithEnvironment("AI__AzureChatDeploymentName", chatDeploymentName)
    .WithEnvironment("AI__AzureOrOpenAI", AzureOrOpenAI)
    .WithEnvironment("AI_OpenAiChatModel", openAiChatModel);

We need to add .NET Aspire agents in both our console and web apps. Let us start with the web app. Add this code to the Program.cs file in the RazorPagesAI project right before “var app = builder.Build()”: 

builder.AddServiceDefaults();

In the same Program.cs of the web app (RazorPagesAI), comment out the if (azureOrOpenAi.ToLower() == "openai") { …. } else { ….. } block and replace it with this code:

if (azureOrOpenAi.ToLower() == "openai") {
    builder.AddOpenAIClient("openAi");
    builder.Services.AddKernel()
        .AddOpenAIChatCompletion(openAiChatModel);
} else {
    builder.AddAzureOpenAIClient("azureOpenAi");
    builder.Services.AddKernel()
        .AddAzureOpenAIChatCompletion(azureChatDeploymentName);
}

In the above code, we call the extension method to register an OpenAIClient for use via the dependency injection container. The method takes a connection name parameter. Also, register Semantic Kernel with the DI. 

Also, in the Program.cs file in the ConsoleAI project, add this code right below the using statements:

var hostBuilder = Host.CreateApplicationBuilder();
hostBuilder.AddServiceDefaults();

In the same Program.cs of the console app (ConsoleAI), comment out the if (azureOrOpenAi.ToLower() == "azure") { …. } else { ….. } block and replace it with this code:

if (azureOrOpenAI.ToLower() == "azure") {
    var azureChatDeploymentName = config["AI:AzureChatDeploymentName"] ?? "gpt-35-turbo";
    hostBuilder.AddAzureOpenAIClient("azureOpenAi");
    hostBuilder.Services.AddKernel()
        .AddAzureOpenAIChatCompletion(azureChatDeploymentName);
} else {
    var openAiChatModel = config["AI:OpenAiChatModel"] ?? "gpt-3.5-turbo";
    hostBuilder.AddOpenAIClient("openAi");
    hostBuilder.Services.AddKernel()
        .AddOpenAIChatCompletion(openAiChatModel);
}
var app = hostBuilder.Build();

Replace “var kernel = builder.Build();” with this code:

var kernel = app.Services.GetRequiredService<Kernel>();
app.Start();

You can now test that the .NET Aspire orchestration of both the Console and Web apps. Stop all applications, then, in a terminal window,  go to the .AppHost project and run the following command:

dotnet watch

You will see the .NET Aspire dashboard:


Click on Views under the Logs column. You will see this output indicating that the console application ran successfully:


Click on the link for the web app under the Endpoints column. It opens the razor pages web app in another tab in your browser. Test it out and verify that it works as expected.

Stop the .AppHost application, then comment out the AI and ConneectionStrings blocks in the appsettings.json files in both the console and web apps. If you run the .AppHost project again, you will discover that it works equally well because the environment variables are being passed from the .AppHost project into the console and web apps respectively.

One last refinement we can do to the console application is do away with the ConfigurationBuilder because we can get a configuration object from the ApplicationBuilder. Therefore, comment out the following code in the console application:

var config = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
    .Build();

Replace the above code with the following:

var config = hostBuilder.Configuration;

You can delete the following package from the ConsoleAI.csproj file:

<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />

Everything works just as it did before.


Friday, December 17, 2021

Explore .NET MAUI Blazor Apps with .NET 6.0 & Visual Studio 2022 Version 17.1.0 Preview 1.1

In a previous article, I wrote about .NET MAUI Apps. In this article, I will discuss the Blazor version of .NET MAUI Apps. This is known as .NET MAUI Blazor Apps.

MAUI is not yet officially released. The current bits offer a glimpse into what the final product will look like. 

This is the environment that I am using:

  • Windows 11 Version 21H2
  • Visual Studio 2022 Version 17.1.0 Preview 1.1
  • .NET 6.0.101
Source code for this application can be found at: https://github.com/medhatelmasry/FirstMauiBlazorApp

Setup

You will find installation instructions for .NET MAUI at: https://docs.microsoft.com/en-us/dotnet/maui/get-started/installation

The only workload I installed in Visual Studio 2022 (Preview) is "Mobile development with .NET", as shown below:


It is also worth noting that I do not have any other Android development application (like Android Studio) installed on my computer. The above Visual Studio 2022 workload also installed an Android emulator.

Application

Let's get started exploring what apps we can develop with .NET MAUI. Start Visual Studio 2022 (Preview) and select "Create a new project":


Enter "maui" in the filter field. You will discover that there are three MAUI-related projects that you can create - namely: 
  1. .NET MAUI App
  2. .NET MAUI Blazor App
  3. .NET MAUI Class Library
In this article, I explore the second in the above list - .NET MAUI Blazor App. Select this project type then click Next:




I named my application FirstMauiBlazorApp:




Let's firstly run our app on Windows. From the drop-down-list at the top, make sure you have chosen "Windows Machine".


Click on "Windows Machine" to run the application. Soon after, you should experience the following application running on your desktop:


Stop the application by either closing it or clicking on the red square button on the top of Visual Studio 2022. You will find that the application is installed in your "Apps and Features" on windows. You can, of course, uninstall it if you so desire.

Adding our own page

Add a "Razor Component..." to the Pages folder of your application.


I named my file Toons.razor.

We will modify Toons.razor so that it reads an online API that contains some cartoon characters. If you point your browser to https://apipool.azurewebsites.net/api/toons it will show the following data:

[{"id":1,"lastName":"Flintstone","firstName":"Fred","occupation":"Mining Manager","gender":"M","pictureUrl":"https://api4all.azurewebsites.net/images/flintstone/fred.png","votes":0},{"id":2,"lastName":"Rubble","firstName":"Barney","occupation":"Mining Assistant","gender":"M","pictureUrl":"https://api4all.azurewebsites.net/images/flintstone/barney.png","votes":0},{"id":3,"lastName":"Rubble","firstName":"Betty","occupation":"Nurse","gender":"F","pictureUrl":"https://api4all.azurewebsites.net/images/flintstone/betty.png","votes":0},{"id":4,"lastName":"Flintstone","firstName":"Wilma","occupation":"Teacher","gender":"F","pictureUrl":"https://api4all.azurewebsites.net/images/flintstone/wilma.png","votes":0},{"id":5,"lastName":"Rubble","firstName":"Bambam","occupation":"Baby","gender":"M","pictureUrl":"https://api4all.azurewebsites.net/images/flintstone/bambam.png","votes":0},{"id":6,"lastName":"Flintstone","firstName":"Pebbles","occupation":"Baby","gender":"M","pictureUrl":"https://api4all.azurewebsites.net/images/flintstone/pebbles.png","votes":0},{"id":7,"lastName":"Flintstone","firstName":"Dino","occupation":"Pet","gender":"F","pictureUrl":"https://api4all.azurewebsites.net/images/flintstone/dino.png","votes":0},{"id":8,"lastName":"Mouse","firstName":"Micky","occupation":"Hunter","gender":"M","pictureUrl":"https://api4all.azurewebsites.net/images/disney/MickyMouse.png","votes":0},{"id":9,"lastName":"Duck","firstName":"Donald","occupation":"Sailor","gender":"M","pictureUrl":"https://api4all.azurewebsites.net/images/disney/DonaldDuck.png","votes":0}]

Each JSON object contains the following properties:

id (int)
lastName (string)
firstName (string)
occupation (string)
gender (string)
pictureUrl (string)
votes (int)

Replace the content of Toons.razor with the following code:

@page "/toons"

@using System.Text.Json
@using System.Text.Json.Serialization

<h1>Toon Characters</h1>

@if (toonList == null) {
  <p><em>Loading...</em></p>
} else {
  <table class="table">
    <tbody>
      @foreach (var item in toonList) {
        <tr>
          <td>@item.FullName</td>
          <td><img src="@item.PictureUrl" style="height: 40px" alt="@item.FirstName @item.LastName"> </td>
        </tr>
      }
    </tbody>
  </table>
}

@code {
  private Toon[] toonList;

  protected override async Task OnInitializedAsync() {
      HttpClient client = new HttpClient();
      var stream = client.GetStreamAsync("https://apipool.azurewebsites.net/api/toons");
      toonList = await JsonSerializer.DeserializeAsync<Toon[]>(await stream);
  }

  public class Toon {
    [JsonPropertyName("id")]
    public int Id { get; set; }

    [JsonPropertyName("lastName")]
    public string LastName { get; set; }

    [JsonPropertyName("firstName")]
    public string FirstName { get; set; }

    [JsonPropertyName("occupation")]
    public string Occupation { get; set; }

    [JsonPropertyName("gender")]
    public string Gender { get; set; }

    [JsonPropertyName("pictureUrl")]
    public string PictureUrl { get; set; }

    [JsonPropertyName("votes")]
    public int Votes { get; set; }

    public string FullName {
      get {
        return string.Format("{0} {1}", this.FirstName, this.LastName);
      }
    }
  } 

Finally, edit the home page, Index.razor, so that it displays cartoon characters. This is done by updating Index.razor so that it looks like this:

@page "/"

<Toons />

Run your application and you will see the following output:


Let us see what this app looks like in an android emulator. To setup an emulator, choose Tools >> Android >> Android Device Manager...


You can configure an Android device of your choice. In my case, even though I configured both Pixel 4 & Pixel 5, I found Pixel 4 to be more cooperative.


You can start the emulator of your choice from within the Android Device Manager.

Choose the android emulator of your choice in the run drop-down-list at the top of Visual Studio 2022:


Run your app in the Android emulator. This is what it should look like:

If you have built some application using Blazor, here is an opportunity for you to migrate some of that functionality into the mobile world.

Thursday, December 16, 2021

Explore .NET MAUI Apps with .NET 6.0 & Visual Studio 2022 Version 17.1.0 Preview 1.1

The birth of .NET MAUI among the family of .NET products is very exciting. Although we did have Xamarin to develop cross-platform mobile applications in the past, .NET MAUI is a little different because it uses a unified version .NET 6.0 for mobile devices and desktop computers.

MAUI is not yet officially released. The current bits offer a glimpse into what the final product will look like. In this article, I will introduce you to .NET MAUI Apps. 

Meantime, this is the environment that I am using:

  • Windows 11 Version 21H2
  • Visual Studio 2022 Version 17.1.0 Preview 1.1
  • .NET 6.0.101
Source code for this application can be found at: https://github.com/medhatelmasry/FirstMauiApp

Setup

You will find installation instructions for .NET MAUI at: https://docs.microsoft.com/en-us/dotnet/maui/get-started/installation

The only workload I installed in Visual Studio 2022 (Preview) is "Mobile development with .NET", as shown below:


It is also worth noting that I do not have any other Android development application (like Android Studio) installed on my computer. The above Visual Studio 2022 workload also installed an Android emulator.

Application

Let's get started exploring what apps we can develop with .NET MAUI. Start Visual Studio 2022 (Preview) and select "Create a new project":


Enter "maui" in the filter field. You will discover that there are three MAUI-related projects that you can create - namely: 
  1. .NET MAUI App
  2. .NET MAUI Blazor App
  3. .NET MAUI Class Library
In this tutorial, I will explore the first in the above list - .NET MAUI App. Select this project type then click Next:


I named my application FirstMauiApp:


Firstly, let's run our app on Windows. From the drop-down-list at the top of Visual Studio 2022, make sure you have chosen "Windows Machine".


Click on "Windows Machine" to run the application. Soon after, you should experience the following application running on your desktop:


Stop the application by either closing it or clicking on the red square button on the top of Visual Studio 2022. You will find that the application gets installed in your "Apps and Features" on windows. You can, of course, uninstall it if you so desire.

Adding our own page

Add “.NET MAUI ContentPage (Preview)” named ToonPage.xaml:


Edit App.xaml.cs so that it starts ToonPage instead of MainPage.

MainPage = new ToonPage();

Run the application. It should look like this:

Stop the application.

We will modify ToonPage so that it reads an online API that contains some cartoon characters. If you point your browser to https://apipool.azurewebsites.net/api/toons it will show the following data:

[{"id":1,"lastName":"Flintstone","firstName":"Fred","occupation":"Mining Manager","gender":"M","pictureUrl":"https://api4all.azurewebsites.net/images/flintstone/fred.png","votes":0},{"id":2,"lastName":"Rubble","firstName":"Barney","occupation":"Mining Assistant","gender":"M","pictureUrl":"https://api4all.azurewebsites.net/images/flintstone/barney.png","votes":0},{"id":3,"lastName":"Rubble","firstName":"Betty","occupation":"Nurse","gender":"F","pictureUrl":"https://api4all.azurewebsites.net/images/flintstone/betty.png","votes":0},{"id":4,"lastName":"Flintstone","firstName":"Wilma","occupation":"Teacher","gender":"F","pictureUrl":"https://api4all.azurewebsites.net/images/flintstone/wilma.png","votes":0},{"id":5,"lastName":"Rubble","firstName":"Bambam","occupation":"Baby","gender":"M","pictureUrl":"https://api4all.azurewebsites.net/images/flintstone/bambam.png","votes":0},{"id":6,"lastName":"Flintstone","firstName":"Pebbles","occupation":"Baby","gender":"M","pictureUrl":"https://api4all.azurewebsites.net/images/flintstone/pebbles.png","votes":0},{"id":7,"lastName":"Flintstone","firstName":"Dino","occupation":"Pet","gender":"F","pictureUrl":"https://api4all.azurewebsites.net/images/flintstone/dino.png","votes":0},{"id":8,"lastName":"Mouse","firstName":"Micky","occupation":"Hunter","gender":"M","pictureUrl":"https://api4all.azurewebsites.net/images/disney/MickyMouse.png","votes":0},{"id":9,"lastName":"Duck","firstName":"Donald","occupation":"Sailor","gender":"M","pictureUrl":"https://api4all.azurewebsites.net/images/disney/DonaldDuck.png","votes":0}]

Each JSON object contains the following properties:

id (int)
lastName (string)
firstName (string)
occupation (string)
gender (string)
pictureUrl (string)
votes (int)

To represent the above data, we will add a class named Toon with the following content:

public class Toon {
  [JsonPropertyName("id")]
  public int Id { get; set; }

  [JsonPropertyName("lastName")]
  public string LastName { get; set; }

  [JsonPropertyName("firstName")]
  public string FirstName { get; set; }

  [JsonPropertyName("occupation")]
  public string Occupation { get; set; }

  [JsonPropertyName("gender")]
  public string Gender { get; set; }

  [JsonPropertyName("pictureUrl")]
  public string PictureUrl { get; set; }

  [JsonPropertyName("votes")]
  public int Votes { get; set; }

  public string FullName {
    get {
        return string.Format("{0} {1}", this.FirstName, this.LastName);
    }
  }

  public override string ToString() {
    return string.Format($"{Id}\t{FullName}\t{Occupation}\t{Gender}\t{PictureUrl}\t{Votes}");
  }
}

You will need to resolve the appropriate namespace for JsonPropertyName. This will add the following to your using statements:

using System.Text.Json.Serialization;

We will need to update the UI file named ToonPage.xaml, so that we  can display the contents of the Toon[] array. In ToonPage.xaml, replace the existing <Label .... /> control with:

<CollectionView x:Name="cvToons" ItemsLayout="VerticalGrid, 2">
  <CollectionView.ItemTemplate>
    <DataTemplate>
      <Grid Padding="10" RowDefinitions="60" ColumnDefinitions="70,*">
        <Image Grid.RowSpan="2" 
        Source="{Binding PictureUrl}" 
        Aspect="AspectFit"
        HeightRequest="60" 
        WidthRequest="60">
          <Image.Clip>
            <RectangleGeometry Rect="0,0,160,160"/>
          </Image.Clip>
        </Image>

        <Label Grid.Column="1" 
        Text="{Binding FullName}" 
        FontAttributes="Bold"
        TextColor="Black"
        VerticalOptions="Start"
        LineBreakMode="TailTruncation" />

        <Label Grid.Column="1" 
        Text="{Binding Occupation}"
        LineBreakMode="TailTruncation"
        FontAttributes="Italic" 
        TextColor="Black"
        VerticalOptions="End" />

      </Grid>
    </DataTemplate>
  </CollectionView.ItemTemplate>
</CollectionView>

Next, add the following method to your ToonPage.xaml.cs file, which reads the content of the API, hydrates it into an array of Toon objects Toon[], then binds the data to the CollectionView control with ID cvToons:

public async void GetToonsAsync() {
  HttpClient client = new HttpClient();
  var stream = client.GetStreamAsync("https://apipool.azurewebsites.net/api/toons");
  var data = await JsonSerializer.DeserializeAsync<Toon[]>(await stream);
  Dispatcher.Dispatch(() => cvToons.ItemsSource = data);
}

You will need to resolve the namespace for the JsonSerializer class, which is:

using System.Text.Json;

Finally, append a call to the above method in the constructor by adding this statement just below InitializeComponent():

GetToonsAsync();

Run your application and you will see the following output:


Let us see what this app looks like in an android emulator. To setup an emulator, choose Tools >> Android >> Android Device Manager...


You can configure an Android device of your choice. In my case, even though I configured both Pixel 4 & Pixel 5, I found Pixel 4 to be more cooperative.


You can start the emulator of your choice from within the "Android Device Manager".

Choose the android emulator of your choice in the run drop-down-list at the top of Visual Studio 2022:


Run your app in the Android emulator. This is what it should look like:

I hope you found this useful. I am sure this product will evolve and, perhaps, change by the time it is finally released.