diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..5dc46e6 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +* text=auto eol=lf +*.{cmd,[cC][mM][dD]} text eol=crlf +*.{bat,[bB][aA][tT]} text eol=crlf \ No newline at end of file diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml new file mode 100644 index 0000000..0c5baf5 --- /dev/null +++ b/.github/workflows/azure-dev.yml @@ -0,0 +1,63 @@ +on: + workflow_dispatch: + push: + branches: + - main + +permissions: + id-token: write + contents: read + actions: read + security-events: write + +jobs: + deploy: + runs-on: ubuntu-latest + env: + AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }} + AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }} + AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} + AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} + AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install azd + uses: Azure/setup-azd@v1.0.0 + + #- name: Run Microsoft Security DevOps Analysis + # uses: microsoft/security-devops-action@v1 + # id: msdo + # with: + # tools: templateanalyzer + + #- name: Upload results to Security tab + # uses: github/codeql-action/upload-sarif@v2 + # with: + # sarif_file: ${{ steps.msdo.outputs.sarifFile }} + + - name: Log in with Azure (Federated Credentials) + if: ${{ env.AZURE_CLIENT_ID != '' }} + run: | + azd auth login ` + --client-id "$Env:AZURE_CLIENT_ID" ` + --federated-credential-provider "github" ` + --tenant-id "$Env:AZURE_TENANT_ID" + shell: pwsh + + - name: Provision Infrastructure + run: azd provision --no-prompt + env: + AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} + AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} + AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} + CREATE_ROLE_FOR_USER: false + + - name: Deploy Application + run: azd deploy --no-prompt + env: + AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} + AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} + AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} diff --git a/.gitignore b/.gitignore index 8a30d25..1b1ef42 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,10 @@ +# Azure Developer CLI generated folder +# including environment variables +.azure + +# User-specific configuration +appSettings.local.json + ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## @@ -396,3 +403,6 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml + +# AZD +.[aA]zure/ \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..100b77b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "C#: AppHost Debug", + "type": "dotnet", + "request": "launch", + "projectPath": "${workspaceFolder}/src/AIChatApp.AppHost/AIChatApp.AppHost.csproj" + } + + ] +} \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..6257f2e --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,9 @@ +# Microsoft Open Source Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). + +Resources: + +- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) +- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..b3c89ef --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,41 @@ + + +## Security + +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin). + +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report). + +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp). + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd). + + diff --git a/azure.yaml b/azure.yaml new file mode 100644 index 0000000..44c16cb --- /dev/null +++ b/azure.yaml @@ -0,0 +1,8 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: ai-chat-aspire-sk-csharp +services: + app: + language: dotnet + project: ./src/AIChatApp.AppHost/AIChatApp.AppHost.csproj + host: containerapp diff --git a/infra/main.bicep b/infra/main.bicep new file mode 100644 index 0000000..31d38b2 --- /dev/null +++ b/infra/main.bicep @@ -0,0 +1,52 @@ +targetScope = 'subscription' + +@minLength(1) +@maxLength(64) +@description('Name of the environment that can be used as part of naming resource convention, the name of the resource group for your application will use this name, prefixed with rg-') +param environmentName string + +@minLength(1) +@description('The location used for all deployed resources') +param location string + +@description('Id of the user or app to assign application roles') +param principalId string = '' + +var tags = { + 'azd-env-name': environmentName +} + +resource rg 'Microsoft.Resources/resourceGroups@2022-09-01' = { + name: 'rg-${environmentName}' + location: location + tags: tags +} + +module resources 'resources.bicep' = { + scope: rg + name: 'resources' + params: { + location: location + tags: tags + principalId: principalId + } +} + +module openai 'openai/openai.module.bicep' = { + name: 'openai' + scope: rg + params: { + location: location + principalId: resources.outputs.MANAGED_IDENTITY_PRINCIPAL_ID + principalType: 'ServicePrincipal' + } +} +output MANAGED_IDENTITY_CLIENT_ID string = resources.outputs.MANAGED_IDENTITY_CLIENT_ID +output MANAGED_IDENTITY_NAME string = resources.outputs.MANAGED_IDENTITY_NAME +output AZURE_LOG_ANALYTICS_WORKSPACE_NAME string = resources.outputs.AZURE_LOG_ANALYTICS_WORKSPACE_NAME +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = resources.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT +output AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID string = resources.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID +output AZURE_CONTAINER_APPS_ENVIRONMENT_NAME string = resources.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_NAME +output AZURE_CONTAINER_APPS_ENVIRONMENT_ID string = resources.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID +output AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN string = resources.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN +output OPENAI_CONNECTIONSTRING string = openai.outputs.connectionString diff --git a/infra/main.parameters.json b/infra/main.parameters.json new file mode 100644 index 0000000..db7cedc --- /dev/null +++ b/infra/main.parameters.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "principalId": { + "value": "${AZURE_PRINCIPAL_ID}" + }, + "environmentName": { + "value": "${AZURE_ENV_NAME}" + }, + "location": { + "value": "${AZURE_LOCATION}" + } + } + } + \ No newline at end of file diff --git a/infra/openai/openai.module.bicep b/infra/openai/openai.module.bicep new file mode 100644 index 0000000..6cde0a3 --- /dev/null +++ b/infra/openai/openai.module.bicep @@ -0,0 +1,52 @@ +targetScope = 'resourceGroup' + +@description('') +param location string = resourceGroup().location + +@description('') +param principalId string + +@description('') +param principalType string + +resource cognitiveServicesAccount_wXAGTFUId 'Microsoft.CognitiveServices/accounts@2023-05-01' = { + name: toLower(take('openai${uniqueString(resourceGroup().id)}', 24)) + location: location + kind: 'OpenAI' + sku: { + name: 'S0' + } + properties: { + customSubDomainName: toLower(take(concat('openai', uniqueString(resourceGroup().id)), 24)) + publicNetworkAccess: 'Enabled' + disableLocalAuth: true + } +} + +resource roleAssignment_Hsk8rxWY8 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: cognitiveServicesAccount_wXAGTFUId + name: guid(cognitiveServicesAccount_wXAGTFUId.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a001fd3d-188f-4b5d-821b-7da978bf7442')) + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a001fd3d-188f-4b5d-821b-7da978bf7442') + principalId: principalId + principalType: principalType + } +} + +resource cognitiveServicesAccountDeployment_6E9woetGC 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = { + parent: cognitiveServicesAccount_wXAGTFUId + name: 'chat' + sku: { + name: 'GlobalStandard' + capacity: 10 + } + properties: { + model: { + format: 'OpenAI' + name: 'gpt-4o' + version: '2024-05-13' + } + } +} + +output connectionString string = 'Endpoint=${cognitiveServicesAccount_wXAGTFUId.properties.endpoint}' diff --git a/infra/resources.bicep b/infra/resources.bicep new file mode 100644 index 0000000..c3a95bd --- /dev/null +++ b/infra/resources.bicep @@ -0,0 +1,93 @@ +@description('The location used for all deployed resources') +param location string = resourceGroup().location +@description('Id of the user or app to assign application roles') +param principalId string = '' + + +@description('Tags that will be applied to all resources') +param tags object = {} + +var resourceToken = uniqueString(resourceGroup().id) + +resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: 'mi-${resourceToken}' + location: location + tags: tags +} + +resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-07-01' = { + name: replace('acr-${resourceToken}', '-', '') + location: location + sku: { + name: 'Basic' + } + tags: tags +} + +resource caeMiRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(containerRegistry.id, managedIdentity.id, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')) + scope: containerRegistry + properties: { + principalId: managedIdentity.properties.principalId + principalType: 'ServicePrincipal' + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') + } +} + +resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { + name: 'law-${resourceToken}' + location: location + properties: { + sku: { + name: 'PerGB2018' + } + } + tags: tags +} + +resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2024-02-02-preview' = { + name: 'cae-${resourceToken}' + location: location + properties: { + workloadProfiles: [{ + workloadProfileType: 'Consumption' + name: 'consumption' + }] + appLogsConfiguration: { + destination: 'log-analytics' + logAnalyticsConfiguration: { + customerId: logAnalyticsWorkspace.properties.customerId + sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey + } + } + } + tags: tags + + resource aspireDashboard 'dotNetComponents' = { + name: 'aspire-dashboard' + properties: { + componentType: 'AspireDashboard' + } + } + +} + +resource explicitContributorUserRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(containerAppEnvironment.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')) + scope: containerAppEnvironment + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') + } +} + +output MANAGED_IDENTITY_CLIENT_ID string = managedIdentity.properties.clientId +output MANAGED_IDENTITY_NAME string = managedIdentity.name +output MANAGED_IDENTITY_PRINCIPAL_ID string = managedIdentity.properties.principalId +output AZURE_LOG_ANALYTICS_WORKSPACE_NAME string = logAnalyticsWorkspace.name +output AZURE_LOG_ANALYTICS_WORKSPACE_ID string = logAnalyticsWorkspace.id +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.properties.loginServer +output AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID string = managedIdentity.id +output AZURE_CONTAINER_APPS_ENVIRONMENT_NAME string = containerAppEnvironment.name +output AZURE_CONTAINER_APPS_ENVIRONMENT_ID string = containerAppEnvironment.id +output AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN string = containerAppEnvironment.properties.defaultDomain diff --git a/src/AIChatApp.AppHost/AIChatApp.AppHost.csproj b/src/AIChatApp.AppHost/AIChatApp.AppHost.csproj new file mode 100644 index 0000000..2aea3c1 --- /dev/null +++ b/src/AIChatApp.AppHost/AIChatApp.AppHost.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + enable + enable + true + bfbf6497-561a-489f-b904-9e0f105be895 + + + + + + + + + + + + diff --git a/src/AIChatApp.AppHost/AIChatApp.AppHost.sln b/src/AIChatApp.AppHost/AIChatApp.AppHost.sln new file mode 100644 index 0000000..ec69c74 --- /dev/null +++ b/src/AIChatApp.AppHost/AIChatApp.AppHost.sln @@ -0,0 +1,25 @@ + +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 new file mode 100644 index 0000000..59d7fba --- /dev/null +++ b/src/AIChatApp.AppHost/Program.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Json; +using Projects; + +var builder = DistributedApplication.CreateBuilder(args); + +var chatDeploymentName = "chat"; +var connectionString = builder.Configuration.GetConnectionString("openai"); + +var openai = String.IsNullOrEmpty(connectionString) + ? builder.AddAzureOpenAI("openai") + .AddDeployment(new AzureOpenAIDeployment(chatDeploymentName, "gpt-4o", "2024-05-13", "GlobalStandard", 10)) + : builder.AddConnectionString("openai"); + +builder.AddProject("aichatapp-web") + .WithReference(openai) + .WithEnvironment("AI_ChatDeploymentName", chatDeploymentName); + +builder.Build().Run(); diff --git a/src/AIChatApp.AppHost/Properties/launchSettings.json b/src/AIChatApp.AppHost/Properties/launchSettings.json new file mode 100644 index 0000000..9887719 --- /dev/null +++ b/src/AIChatApp.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17099;http://localhost:15282", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21288", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22078" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15282", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19128", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20091" + } + } + } +} diff --git a/src/AIChatApp.AppHost/appsettings.Development.json b/src/AIChatApp.AppHost/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/AIChatApp.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/AIChatApp.AppHost/appsettings.json b/src/AIChatApp.AppHost/appsettings.json new file mode 100644 index 0000000..31c092a --- /dev/null +++ b/src/AIChatApp.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/src/AIChatApp.ServiceDefaults/AIChatApp.ServiceDefaults.csproj b/src/AIChatApp.ServiceDefaults/AIChatApp.ServiceDefaults.csproj new file mode 100644 index 0000000..ae38594 --- /dev/null +++ b/src/AIChatApp.ServiceDefaults/AIChatApp.ServiceDefaults.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + + diff --git a/src/AIChatApp.ServiceDefaults/Extensions.cs b/src/AIChatApp.ServiceDefaults/Extensions.cs new file mode 100644 index 0000000..ce94dc2 --- /dev/null +++ b/src/AIChatApp.ServiceDefaults/Extensions.cs @@ -0,0 +1,111 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder) + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + return builder; + } + + public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder) + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddAspNetCoreInstrumentation() + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder) + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/src/AIChatApp.Web/AIChatApp.Web.csproj b/src/AIChatApp.Web/AIChatApp.Web.csproj new file mode 100644 index 0000000..a2928ec --- /dev/null +++ b/src/AIChatApp.Web/AIChatApp.Web.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + enable + AIChatApp + b0ee5b46-677b-4906-bcc3-e74dafe7a50f + + + + + + + + + + + + + diff --git a/src/AIChatApp.Web/Components/App.razor b/src/AIChatApp.Web/Components/App.razor new file mode 100644 index 0000000..2782ec8 --- /dev/null +++ b/src/AIChatApp.Web/Components/App.razor @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + +@code { + IComponentRenderMode? PageRenderMode { get; } = new InteractiveServerRenderMode(false); +} \ No newline at end of file diff --git a/src/AIChatApp.Web/Components/Chat/Chat.razor b/src/AIChatApp.Web/Components/Chat/Chat.razor new file mode 100644 index 0000000..23ed529 --- /dev/null +++ b/src/AIChatApp.Web/Components/Chat/Chat.razor @@ -0,0 +1,22 @@ +@page "/chat" +@inject IConfiguration Configuration +@inject IJSRuntime JS +@using AIChatApp.Model + +Chat + +
+
+
+ @foreach (var message in messages) + { + + } +
+
+ +
+