diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 7e0c2d8..db78cbc 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -8,18 +8,17 @@ // Features to add to the dev container. More info: https://containers.dev/features. "features": { - "ghcr.io/devcontainers/features/azure-cli:1": { - "installBicep": true, - "version": "latest" - }, "ghcr.io/azure/azure-dev/azd:latest": { "version": "stable" }, - "ghcr.io/devcontainers/features/dotnet:2": { - } + "ghcr.io/devcontainers/features/dotnet:2": {} }, + "customizations": { - "vscode": { + "codespaces": { + "openFiles": ["docs/get-started-codespaces.md"] + }, + "vscode": { "extensions": [ "ms-dotnettools.csdevkit", "ms-dotnettools.vscode-dotnet-runtime", @@ -27,10 +26,38 @@ "ms-azuretools.azure-dev", "ms-azuretools.vscode-bicep", "ms-azuretools.vscode-docker", - "ms-vscode.js-debug" ] + "ms-vscode.js-debug"], + "settings": { + "remote.autoForwardPorts": false + } } }, + // Workarounds to run .NET Aspire in CodeSpaces until future support is added + "containerEnv": { + "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true", + "DOTNET_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS ": "true" + }, + + "forwardPorts": [7243, 17099], + + "portsAttributes": { + "17099": { + "label": "Dashboard HTTPS", + "protocol": "https" + }, + "7243": { + "label": "Web HTTPS", + "protocol": "https" + }, + "5153": { + "label": "Web HTTPS" + }, + "15282": { + "label": "Dashboard HTTP" + } + }, + "postCreateCommand": "dotnet dev-certs https --trust" // Enable HTTPS support // Use 'forwardPorts' to make a list of ports inside the container available locally. diff --git a/.vscode/launch.json b/.vscode/launch.json index 100b77b..d7efd80 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,10 +5,11 @@ "version": "0.2.0", "configurations": [ { - "name": "C#: AppHost Debug", + "name": "C#: AIChatApp.AppHost [https]", "type": "dotnet", "request": "launch", - "projectPath": "${workspaceFolder}/src/AIChatApp.AppHost/AIChatApp.AppHost.csproj" + "projectPath": "${workspaceFolder}/src/AIChatApp.AppHost/AIChatApp.AppHost.csproj", + "launchConfigurationId": "TargetFramework=;https" } ] diff --git a/README.md b/README.md index 40d04f9..f3a4fe8 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,17 @@ since the local app needs credentials for Azure OpenAI to work properly. You have a few options for getting started with this template. +### GitHub Codespaces + +You can run this template virtually by using GitHub Codespaces. The button will open a web-based VS Code instance in your browser: + +1. Open the template (this may take several minutes): + + [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/Azure-Samples/ai-chat-aspire-sk-csharp) + +2. Open a terminal window +3. Continue with the [deploying steps](#deploying) + ### Local Environment If you're not using one of the above options for opening the project, then you'll need to: @@ -48,7 +59,6 @@ If you're not using one of the above options for opening the project, then you'l 1. Make sure the following tools are installed: * [.NET 8](https://dotnet.microsoft.com/downloads/) - * With the [.NET Aspire workload installed](https://learn.microsoft.com/dotnet/aspire/fundamentals/setup-tooling?tabs=windows&pivots=visual-studio#install-net-aspire) using `dotnet workload install aspire` * [Git](https://git-scm.com/downloads) * [Azure Developer CLI (azd)](https://aka.ms/install-azd) * [VS Code](https://code.visualstudio.com/Download) or [Visual Studio](https://visualstudio.microsoft.com/downloads/) @@ -77,10 +87,6 @@ Visual Stuido Code Dev Containers can also be used locally, which will open the 4. Continue with the [deploying steps](#deploying) -### GitHub Codespaces - -**Coming soon** - GitHub Codespaces support works best with Aspire 9, which is coming soon. For now, you can load the project and build in CodeSpaces, but won't be able to run. - ## Deploying Once you've opened the project [locally](#local-environment) or in [Dev Containers](#vs-code-dev-containers), you can deploy it to Azure. @@ -103,13 +109,14 @@ From a Terminal window, open the folder with the clone of this repo and run the azd auth login ``` -2. Provision and deploy all the resources: +2. Provision and deploy dependencies for the project: ```shell - azd up + azd env new + azd provision ``` - It will prompt you to provide an `azd` environment name (like "chat-app"), select a subscription from your Azure account, and select a [location where OpenAI is available](https://azure.microsoft.com/explore/global-infrastructure/products-by-region/?products=cognitive-services®ions=all) (like "francecentral"). Then it will provision the resources in your account and deploy the latest code. If you get an error or timeout with deployment, changing the location can help, as there may be availability constraints for the OpenAI resource. + You'll need to replace with an environment name you want to use (like "chat-app"), which will be used as a prefix for resources in Azure. Select a subscription from your Azure account, and select a [location where OpenAI is available](https://azure.microsoft.com/explore/global-infrastructure/products-by-region/?products=cognitive-services®ions=all) (like "francecentral"). Then it will provision the resources in your account and deploy the latest code. If you get an error or timeout with deployment, changing the location can help, as there may be availability constraints for the OpenAI resource. 3. When `azd` has finished deploying, you'll see an endpoint URI in the command output. Visit that URI, and you should see the chat app! 🎉 @@ -121,16 +128,18 @@ From a Terminal window, open the folder with the clone of this repo and run the ## Run the application -Start the project: +Start the project by pressing the F5 key (or clicking the Run button in the Run & Debug sidebar). - **If using Visual Studio**, choose the `Debug > Start Debugging` menu. - **If using VS Code or GitHub CodeSpaces***, choose the `Run > Start Debugging` menu. - Finally, if using the command line, run the following from the project directory: +If using the command line, run the following from the `src` directory: ```shell dotnet run ``` +In the Debug Console (or Terminal window) that appears, you'll see status messages written as the .NET Aspire application starts up. When it's finished starting, look for the text that says something like `Login to the dashboard at https://localhost:17099/login?t=8e08b4369732034c8d67dc80f54fa1db`. Copy the text after "t=" - in this example you'd copy the text "8e08b4369732034c8d67dc80f54fa1db" this is a token you'll use to login to the .NET Aspire Dashboard. Then, click on the https://localhost:17099 URL, paste the token you just copied, and login. + +Finally, in the dashboard that appears you'll see the aichatapp-web resource listed. Click on the URL under the Endpoints column to launch the web application and try the chat experience. + ## Using an existing deployment In order to run this app, you need to have an Azure OpenAI account deployed (from the [deploying steps](#deploying)). After deployment, Azure OpenAI is configured for you using [User Secrets](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets). If you could not run the deployment steps here, or you want to use an existing Azure OpenAI resource and deployment, open a terminal from the root of this repo and run the following diff --git a/azure.yaml b/azure.yaml index d2af017..3587968 100644 --- a/azure.yaml +++ b/azure.yaml @@ -16,5 +16,5 @@ hooks: posix: shell: sh run: ./infra/post-script/store-env-variables.sh - interactive: false + interactive: true continueOnError: true diff --git a/docs/get-started-codespaces.md b/docs/get-started-codespaces.md new file mode 100644 index 0000000..93f35c6 --- /dev/null +++ b/docs/get-started-codespaces.md @@ -0,0 +1,63 @@ +# Provision Azure resources + +Once you've opened the project, you can deploy its dependencies to Azure. + +## Azure account setup + +1. Sign up for a [free Azure account](https://azure.microsoft.com/free/) and create an Azure Subscription. +2. Check that you have the necessary permissions: + + * Your Azure account must have `Microsoft.Authorization/roleAssignments/write` permissions, such as [Role Based Access Control Administrator](https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#role-based-access-control-administrator-preview), [User Access Administrator](https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#user-access-administrator), or [Owner](https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#owner). If you don't have subscription-level permissions, you must be granted [RBAC](https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#role-based-access-control-administrator-preview) for an existing resource group and [deploy to that existing group](/docs/deploy_existing.md#resource-group). + * Your Azure account also needs `Microsoft.Resources/deployments/write` permissions on the subscription level. + +## Deploying with azd + +From a Terminal window, open the folder with the clone of this repo and run the following commands. + +1. Login to Azure: + + ```shell + azd auth login + ``` + +2. Provision and deploy dependencies for the project: + + ```shell + azd env new + azd provision + ``` + + You'll need to replace with an environment name you want to use (like "chat-app"), which will be used as a prefix for resources in Azure. Select a subscription from your Azure account, and select a [location where OpenAI is available](https://azure.microsoft.com/explore/global-infrastructure/products-by-region/?products=cognitive-services®ions=all) (like "francecentral"). Then it will provision the resources in your account and deploy the latest code. If you get an error or timeout with deployment, changing the location can help, as there may be availability constraints for the OpenAI resource. + +3. When `azd` has finished deploying, you'll see an endpoint URI in the command output. Visit that URI, and you should see the chat app! 🎉 + +4. When you've made any changes to the app code, you can just run: + + ```shell + azd deploy + ``` + +## Run the application + +Start the project by pressing the F5 key (or clicking the Run button in the Run & Debug sidebar). + +If using the command line, run the following from the `src` directory: + + ```shell + dotnet run + ``` + +In the Debug Console (or Terminal window) that appears, you'll see status messages written as the .NET Aspire application starts up. When it's finished starting, look for the text that says something like `Login to the dashboard at https://localhost:17099/login?t=8e08b4369732034c8d67dc80f54fa1db`. Copy the text after "t=" - in this example you'd copy the text "8e08b4369732034c8d67dc80f54fa1db" this is a token you'll use to login to the .NET Aspire Dashboard. Then, click on the https://localhost:17099 URL, paste the token you just copied, and login. + +Finally, in the dashboard that appears you'll see the aichatapp-web resource listed. Click on the URL under the Endpoints column to launch the web application and try the chat experience. + +## Using an existing deployment + +In order to run this app, you need to have an Azure OpenAI account deployed (using the instructions above). After deployment, Azure OpenAI is configured for you using [User Secrets](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets). If you could not run the deployment steps here, or you want to use an existing Azure OpenAI resource and deployment, open a terminal from the root of this repo and run the following + +```bash +cd ./src/AIChatApp.AppHost +dotnet user-secrets set "ConnectionStrings:openai" "https://{account_name}.openai.azure.com/" +``` + +The value for the connection string can be found in the Keys & Endpoint section when examining your resource from the Azure portal. Alternatively, you can find the value in the Azure OpenAI Studio > Playground > Code View. An example endpoint is: https://docs-test-001.openai.azure.com/. \ No newline at end of file diff --git a/infra/main.bicep b/infra/main.bicep index 4e034df..6e45ec6 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -7,6 +7,17 @@ param environmentName string @minLength(1) @description('The location used for all deployed resources') +// Look for the desired model in availability table. Default model is gpt-4o-mini: +// https://learn.microsoft.com/azure/ai-services/openai/concepts/models#standard-deployment-model-availability +@allowed([ + 'eastus' + 'eastus2' + 'northcentralus' + 'southcentralus' + 'swedencentral' + 'westus' + 'westus3' +]) param location string @description('Id of the user or app to assign application roles') diff --git a/infra/main.parameters.json b/infra/main.parameters.json index 57d6343..80d9097 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -10,6 +10,9 @@ }, "location": { "value": "${AZURE_LOCATION=eastus}" + }, + "createRoleForUser": { + "value": "${CREATE_ROLE_FOR_USER=true}" } } } diff --git a/src/AIChatApp.AppHost/AIChatApp.AppHost.sln b/src/AIChatApp.AppHost/AIChatApp.AppHost.sln deleted file mode 100644 index ec69c74..0000000 --- a/src/AIChatApp.AppHost/AIChatApp.AppHost.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.5.002.0 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AIChatApp.AppHost", "AIChatApp.AppHost.csproj", "{4D6E0B13-F222-4E81-80FD-34524B2177B5}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {4D6E0B13-F222-4E81-80FD-34524B2177B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4D6E0B13-F222-4E81-80FD-34524B2177B5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4D6E0B13-F222-4E81-80FD-34524B2177B5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4D6E0B13-F222-4E81-80FD-34524B2177B5}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {2CCDBEAC-A29C-4A0B-A9AB-43434E8BA146} - EndGlobalSection -EndGlobal diff --git a/src/AIChatApp.AppHost/Program.cs b/src/AIChatApp.AppHost/Program.cs index 0210eb1..0ef2ff2 100644 --- a/src/AIChatApp.AppHost/Program.cs +++ b/src/AIChatApp.AppHost/Program.cs @@ -1,8 +1,13 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.Json; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; using Projects; +using System.Collections.Immutable; +using System.Security.Cryptography; -var builder = DistributedApplication.CreateBuilder(args); +var builder = DistributedApplication.CreateBuilder(args) + .WithCodespacesSupport(); var chatDeploymentName = "chat"; @@ -22,3 +27,67 @@ .WithExternalHttpEndpoints(); builder.Build().Run(); + +// WORKAROUND: Enables GitHub Codespaces when running in that environment. This will be fixed in a future .NET Aspire release. +public static class CodespaceExtensions +{ + public static IDistributedApplicationBuilder WithCodespacesSupport(this IDistributedApplicationBuilder builder) + { + if (!builder.Configuration.GetValue("CODESPACES")) + { + return builder; + } + + builder.Eventing.Subscribe((e, ct) => { + _ = Task.Run(() => UrlRewriterAsync(e.Services, ct)); + return Task.CompletedTask; + }); + + return builder; + } + + private static async Task UrlRewriterAsync(IServiceProvider services, CancellationToken cancellationToken) + { + var configuration = services.GetRequiredService(); + var gitHubCodespacesPortForwardingDomain = configuration.GetValue("GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN") ?? throw new DistributedApplicationException("Codespaces was detected but GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN environment missing."); + var codespaceName = configuration.GetValue("CODESPACE_NAME") ?? throw new DistributedApplicationException("Codespaces was detected but CODESPACE_NAME environment missing."); + + var rns = services.GetRequiredService(); + + var resourceEvents = rns.WatchAsync(cancellationToken); + + await foreach (var resourceEvent in resourceEvents) + { + Dictionary? remappedUrls = null; + + foreach (var originalUrlSnapshot in resourceEvent.Snapshot.Urls) + { + var uri = new Uri(originalUrlSnapshot.Url); + + if (!originalUrlSnapshot.IsInternal && (uri.Scheme == "http" || uri.Scheme == "https") && uri.Host == "localhost") + { + if (remappedUrls is null) + { + remappedUrls = new(); + } + + var newUrlSnapshot = originalUrlSnapshot with { + Url = $"{uri.Scheme}://{codespaceName}-{uri.Port}.{gitHubCodespacesPortForwardingDomain}{uri.AbsolutePath}" + }; + + remappedUrls.Add(originalUrlSnapshot, newUrlSnapshot); + } + } + + if (remappedUrls is not null) + { + var transformedUrls = from originalUrl in resourceEvent.Snapshot.Urls + select remappedUrls.TryGetValue(originalUrl, out var remappedUrl) ? remappedUrl : originalUrl; + + await rns.PublishUpdateAsync(resourceEvent.Resource, resourceEvent.ResourceId, s => s with { + Urls = transformedUrls.ToImmutableArray() + }); + } + } + } +} \ No newline at end of file diff --git a/src/AIChatApp.ServiceDefaults/AIChatApp.ServiceDefaults.csproj b/src/AIChatApp.ServiceDefaults/AIChatApp.ServiceDefaults.csproj index fffb423..36bd541 100644 --- a/src/AIChatApp.ServiceDefaults/AIChatApp.ServiceDefaults.csproj +++ b/src/AIChatApp.ServiceDefaults/AIChatApp.ServiceDefaults.csproj @@ -1,18 +1,22 @@ + net8.0 enable enable true + - - + + + + \ No newline at end of file diff --git a/src/AIChatApp.ServiceDefaults/Extensions.cs b/src/AIChatApp.ServiceDefaults/Extensions.cs index 13151bf..ce94dc2 100644 --- a/src/AIChatApp.ServiceDefaults/Extensions.cs +++ b/src/AIChatApp.ServiceDefaults/Extensions.cs @@ -3,7 +3,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.ServiceDiscovery; using OpenTelemetry; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; @@ -15,7 +14,7 @@ namespace Microsoft.Extensions.Hosting; // To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults public static class Extensions { - public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder + public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder) { builder.ConfigureOpenTelemetry(); @@ -32,16 +31,10 @@ public static TBuilder AddServiceDefaults(this TBuilder builder) where http.AddServiceDiscovery(); }); - // Uncomment the following to restrict the allowed schemes for service discovery. - // builder.Services.Configure(options => - // { - // options.AllowedSchemes = ["https"]; - // }); - return builder; } - public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder + public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder) { builder.Logging.AddOpenTelemetry(logging => { @@ -58,8 +51,7 @@ public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) w }) .WithTracing(tracing => { - tracing.AddSource(builder.Environment.ApplicationName) - .AddAspNetCoreInstrumentation() + tracing.AddAspNetCoreInstrumentation() // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) //.AddGrpcClientInstrumentation() .AddHttpClientInstrumentation(); @@ -70,7 +62,7 @@ public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) w return builder; } - private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder + private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder) { var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); @@ -89,7 +81,7 @@ private static TBuilder AddOpenTelemetryExporters(this TBuilder builde return builder; } - public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder + public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) { builder.Services.AddHealthChecks() // Add a default liveness check to ensure app is responsive diff --git a/src/AIChatApp.Web/AIChatApp.Web.csproj b/src/AIChatApp.Web/AIChatApp.Web.csproj index c5ff91f..e3ae016 100644 --- a/src/AIChatApp.Web/AIChatApp.Web.csproj +++ b/src/AIChatApp.Web/AIChatApp.Web.csproj @@ -4,7 +4,6 @@ enable enable AIChatApp - b0ee5b46-677b-4906-bcc3-e74dafe7a50f diff --git a/src/AIChatApp.Web/Properties/launchSettings.json b/src/AIChatApp.Web/Properties/launchSettings.json index 33ab031..37bebe7 100644 --- a/src/AIChatApp.Web/Properties/launchSettings.json +++ b/src/AIChatApp.Web/Properties/launchSettings.json @@ -1,13 +1,5 @@ { "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:21040", - "sslPort": 44341 - } - }, "profiles": { "http": { "commandName": "Project", @@ -29,13 +21,6 @@ "DOTNET_ENVIRONMENT": "Development", "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22128" } - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } } } } \ No newline at end of file