diff --git a/.github/workflows/api.yml b/.github/workflows/api.yml new file mode 100644 index 000000000..9f8b80efb --- /dev/null +++ b/.github/workflows/api.yml @@ -0,0 +1,81 @@ +name: API Deployment + +env: + registryName: op3eqv3gu7pvecosureg.azurecr.io + repositoryName: techexcel/csapi + dockerFolderPath: ./src/ContosoSuitesWebAPI + tag: ${{github.run_number}} + +on: + push: + branches: [ main ] + paths: src/ContosoSuitesWebAPI/** + pull_request: + branches: [ main ] + paths: src/ContosoSuitesWebAPI/** + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0 + + - name: Restore dependencies + run: dotnet restore ./src/ContosoSuitesWebAPI/ContosoSuitesWebAPI.csproj + - name: Build + run: dotnet build --no-restore ./src/ContosoSuitesWebAPI/ContosoSuitesWebAPI.csproj + + dockerBuildPush: + runs-on: ubuntu-latest + needs: build + + steps: + - uses: actions/checkout@v4 + + - name: Docker Login + # You may pin to the exact commit or the version. + # uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c + uses: docker/login-action@v3 + with: + # Server address of Docker registry. If not set then will default to Docker Hub + registry: ${{ secrets.ACR_LOGIN_SERVER }} + # Username used to log against the Docker registry + username: ${{ secrets.ACR_USERNAME }} + # Password or personal access token used to log against the Docker registry + password: ${{ secrets.ACR_PASSWORD }} + # Log out from the Docker registry at the end of a job + logout: true + + - name: Docker Build + run: docker build -t $registryName/$repositoryName:$tag --build-arg build_version=$tag $dockerFolderPath + + - name: Docker Push + run: docker push $registryName/$repositoryName:$tag + + deploy-to-prod: + + runs-on: ubuntu-latest + needs: dockerBuildPush + environment: + name: prod + url: https://op3eqv3gu7pve-api.azurewebsites.net/ + + steps: + - uses: actions/checkout@v4 + + - name: 'Login via Azure CLI' + uses: azure/login@v2.1.1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - uses: azure/webapps-deploy@v2 + with: + app-name: 'op3eqv3gu7pve-api' + images: op3eqv3gu7pvecosureg.azurecr.io/techexcel/csapi:${{github.run_number}} diff --git a/.github/workflows/dashboard.yml b/.github/workflows/dashboard.yml new file mode 100644 index 000000000..7bdc6cf5d --- /dev/null +++ b/.github/workflows/dashboard.yml @@ -0,0 +1,70 @@ +name: Dashboard Deployment + +env: + registryName: op3eqv3gu7pvecosureg.azurecr.io + repositoryName: techexcel/csdash + dockerFolderPath: ./src/ContosoSuitesDashboard + tag: ${{github.run_number}} + +on: + push: + branches: [ main ] + paths: src/ContosoSuitesDashboard/** + pull_request: + branches: [ main ] + paths: src/ContosoSuitesDashboard/** + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: +jobs: + dockerBuildPush: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Docker Login + # You may pin to the exact commit or the version. + # uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c + uses: docker/login-action@v3 + with: + # Server address of Docker registry. If not set then will default to Docker Hub + registry: ${{ secrets.ACR_LOGIN_SERVER }} + # Username used to log against the Docker registry + username: ${{ secrets.ACR_USERNAME }} + # Password or personal access token used to log against the Docker registry + password: ${{ secrets.ACR_PASSWORD }} + # Log out from the Docker registry at the end of a job + logout: true + + - name: Create Secrets File + run: echo "$STREAMLIT_SECRETS" > ./src/ContosoSuitesDashboard/.streamlit/secrets.toml + shell: bash + env: + STREAMLIT_SECRETS: ${{ secrets.STREAMLIT_SECRETS }} + + - name: Docker Build + run: docker build -t $registryName/$repositoryName:$tag --build-arg build_version=$tag $dockerFolderPath + + - name: Docker Push + run: docker push $registryName/$repositoryName:$tag + + deploy-to-prod: + + runs-on: ubuntu-latest + needs: dockerBuildPush + environment: + name: prod + url: https://op3eqv3gu7pve-dash.azurewebsites.net/ + + steps: + - uses: actions/checkout@v4 + + - name: 'Login via Azure CLI' + uses: azure/login@v2.1.1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - uses: azure/webapps-deploy@v2 + with: + app-name: 'op3eqv3gu7pve-dash' + images: op3eqv3gu7pvecosureg.azurecr.io/techexcel/csdash:${{github.run_number}} diff --git a/.github/workflows/jekyll-gh-pages.yml b/.github/workflows/jekyll-gh-pages.yml deleted file mode 100644 index 431fe11f2..000000000 --- a/.github/workflows/jekyll-gh-pages.yml +++ /dev/null @@ -1,57 +0,0 @@ -# Sample workflow for building and deploying a Jekyll site to GitHub Pages -name: Deploy Jekyll with GitHub Pages dependencies preinstalled - -on: - # Runs on pushes targeting the default branch - push: - branches: ["main"] - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write - -# Allow only one concurrent deployment to GitHub Pages -concurrency: - group: "pages" - cancel-in-progress: true - -jobs: - # Build job - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.1' # Not needed with a .ruby-version file - bundler-cache: true # runs 'bundle install' and caches installed gems automatically - cache-version: 0 # Increment this number if you need to re-download cached gems - - name: Setup Pages - id: pages - uses: actions/configure-pages@v4 - - name: Build with Jekyll - # Outputs to the './_site' directory by default - run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}" - env: - JEKYLL_ENV: production - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - - # Deployment job - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - needs: build - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/.vscode/launch.json b/.vscode/launch.json index 6f90d62bc..d526e723c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,11 +1,11 @@ { - "version": "0.2.0", - "configurations": [ - { - "name": "Attach to .NET Functions", - "type": "coreclr", - "request": "attach", - "processId": "${command:azureFunctions.pickProcess}" - } - ] - } \ No newline at end of file + "version": "0.2.0", + "configurations": [ + { + "name": "Attach to .NET Functions", + "type": "coreclr", + "request": "attach", + "processId": "${command:azureFunctions.pickProcess}" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index b6f83d84d..fca79ee8d 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,81 +1,81 @@ { - "version": "2.0.0", - "tasks": [ - { - "label": "clean (functions)", - "command": "dotnet", - "args": [ - "clean", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "type": "process", - "problemMatcher": "$msCompile", - "options": { - "cwd": "${workspaceFolder}/src\\ContosoSuitesVectorizationFunction" - } - }, - { - "label": "build (functions)", - "command": "dotnet", - "args": [ - "build", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "type": "process", - "dependsOn": "clean (functions)", - "group": { - "kind": "build", - "isDefault": true - }, - "problemMatcher": "$msCompile", - "options": { - "cwd": "${workspaceFolder}/src\\ContosoSuitesVectorizationFunction" - } - }, - { - "label": "clean release (functions)", - "command": "dotnet", - "args": [ - "clean", - "--configuration", - "Release", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "type": "process", - "problemMatcher": "$msCompile", - "options": { - "cwd": "${workspaceFolder}/src\\ContosoSuitesVectorizationFunction" - } - }, - { - "label": "publish (functions)", - "command": "dotnet", - "args": [ - "publish", - "--configuration", - "Release", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "type": "process", - "dependsOn": "clean release (functions)", - "problemMatcher": "$msCompile", - "options": { - "cwd": "${workspaceFolder}/src\\ContosoSuitesVectorizationFunction" - } - }, - { - "type": "func", - "dependsOn": "build (functions)", - "options": { - "cwd": "${workspaceFolder}/src/ContosoSuitesVectorizationFunction/bin/Debug/net8.0" - }, - "command": "host start", - "isBackground": true, - "problemMatcher": "$func-dotnet-watch" - } - ] - } \ No newline at end of file + "version": "2.0.0", + "tasks": [ + { + "label": "clean (functions)", + "command": "dotnet", + "args": [ + "clean", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}/src\\ContosoSuitesVectorizationFunction" + } + }, + { + "label": "build (functions)", + "command": "dotnet", + "args": [ + "build", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "dependsOn": "clean (functions)", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}/src\\ContosoSuitesVectorizationFunction" + } + }, + { + "label": "clean release (functions)", + "command": "dotnet", + "args": [ + "clean", + "--configuration", + "Release", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}/src\\ContosoSuitesVectorizationFunction" + } + }, + { + "label": "publish (functions)", + "command": "dotnet", + "args": [ + "publish", + "--configuration", + "Release", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "dependsOn": "clean release (functions)", + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}/src\\ContosoSuitesVectorizationFunction" + } + }, + { + "type": "func", + "dependsOn": "build (functions)", + "options": { + "cwd": "${workspaceFolder}/src/ContosoSuitesVectorizationFunction/bin/Debug/net8.0" + }, + "command": "host start", + "isBackground": true, + "problemMatcher": "$func-dotnet-watch" + } + ] +} \ No newline at end of file diff --git a/src/ContosoSuitesDashboard/pages/1_Chat_with_Data.py b/src/ContosoSuitesDashboard/pages/1_Chat_with_Data.py index 98f65a586..d1b3abd40 100644 --- a/src/ContosoSuitesDashboard/pages/1_Chat_with_Data.py +++ b/src/ContosoSuitesDashboard/pages/1_Chat_with_Data.py @@ -11,6 +11,10 @@ def create_chat_completion(messages): aoai_key = st.secrets["aoai"]["key"] aoai_deployment_name = st.secrets["aoai"]["deployment_name"] + search_endpoint = st.secrets["search"]["endpoint"] + search_key = st.secrets["search"]["key"] + search_index_name = st.secrets["search"]["index_name"] + client = openai.AzureOpenAI( api_key=aoai_key, api_version="2024-06-01", @@ -23,9 +27,25 @@ def create_chat_completion(messages): {"role": m["role"], "content": m["content"]} for m in messages ], - stream=True + stream=True, + extra_body={ + "data_sources": [ + { + "type": "azure_search", + "parameters": { + "endpoint": search_endpoint, + "index_name": search_index_name, + "authentication": { + "type": "api_key", + "key": search_key + } + } + } + ] + } ) + def handle_chat_prompt(prompt): """Echo the user's prompt to the chat window. Then, send the user's prompt to Azure OpenAI and display the response.""" diff --git a/src/ContosoSuitesVectorizationFunction/CosmosChangeFeedVectorization.cs b/src/ContosoSuitesVectorizationFunction/CosmosChangeFeedVectorization.cs index 0eed4f385..1b9efbe32 100644 --- a/src/ContosoSuitesVectorizationFunction/CosmosChangeFeedVectorization.cs +++ b/src/ContosoSuitesVectorizationFunction/CosmosChangeFeedVectorization.cs @@ -55,7 +55,7 @@ public object Run([CosmosDBTrigger( { // Combine the hotel and details fields into a single string for embedding. var request_text = $"Hotel: {request.Hotel}\n Request Details: {request.Details}"; - // Generate a vector for the maintenance request. + // Generate a vector for the maintenance request var embedding = _embeddingClient.GenerateEmbedding(request_text); var requestVector = embedding.Value.Vector; diff --git a/src/ContosoSuitesWebAPI/ContosoSuitesWebAPI.csproj b/src/ContosoSuitesWebAPI/ContosoSuitesWebAPI.csproj index 93e25c9b2..7d45eac99 100644 --- a/src/ContosoSuitesWebAPI/ContosoSuitesWebAPI.csproj +++ b/src/ContosoSuitesWebAPI/ContosoSuitesWebAPI.csproj @@ -1,18 +1,20 @@ - - - - net8.0 - enable - enable - - - - - - - - - - - - + + + + net8.0 + enable + enable + 9001f04f-8067-44dc-adfa-3eb2b45b179f + + + + + + + + + + + + + diff --git a/src/ContosoSuitesWebAPI/Program.cs b/src/ContosoSuitesWebAPI/Program.cs index 983cd70bb..9040b4c3d 100644 --- a/src/ContosoSuitesWebAPI/Program.cs +++ b/src/ContosoSuitesWebAPI/Program.cs @@ -8,9 +8,17 @@ using Azure.AI.OpenAI; using Azure; using Microsoft.AspNetCore.Mvc; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Microsoft.SemanticKernel.ChatCompletion; var builder = WebApplication.CreateBuilder(args); +var config = new ConfigurationBuilder() + .AddUserSecrets() + .AddEnvironmentVariables() + .Build(); + // Add services to the container. // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); @@ -28,6 +36,18 @@ return client; }); +builder.Services.AddSingleton((_) => +{ + IKernelBuilder kernelBuilder = Kernel.CreateBuilder(); + kernelBuilder.AddAzureOpenAIChatCompletion( + deploymentName: builder.Configuration["AzureOpenAI:DeploymentName"]!, + endpoint: builder.Configuration["AzureOpenAI:Endpoint"]!, + apiKey: builder.Configuration["AzureOpenAI:ApiKey"]! + ); + kernelBuilder.Plugins.AddFromType(); + return kernelBuilder.Build(); +}); + builder.Services.AddSingleton((_) => { var endpoint = new Uri(builder.Configuration["AzureOpenAI:Endpoint"]!); @@ -57,34 +77,27 @@ app.MapGet("/Hotels", async () => { - throw new NotImplementedException(); + var hotels = await app.Services.GetRequiredService().GetHotels(); + return hotels; }) .WithName("GetHotels") .WithOpenApi(); - + app.MapGet("/Hotels/{hotelId}/Bookings/", async (int hotelId) => { - throw new NotImplementedException(); + var bookings = await app.Services.GetRequiredService().GetBookingsForHotel(hotelId); + return bookings; }) .WithName("GetBookingsForHotel") .WithOpenApi(); - + app.MapGet("/Hotels/{hotelId}/Bookings/{min_date}", async (int hotelId, DateTime min_date) => { - throw new NotImplementedException(); + var bookings = await app.Services.GetRequiredService().GetBookingsByHotelAndMinimumDate(hotelId, min_date); + return bookings; }) .WithName("GetRecentBookingsForHotel") .WithOpenApi(); - -app.MapPost("/Chat", async Task (HttpRequest request) => -{ - var message = await Task.FromResult(request.Form["message"]); - - return "This endpoint is not yet available."; -}) - .WithName("Chat") - .WithOpenApi(); - app.MapGet("/Vectorize", async (string text, [FromServices] IVectorizationService vectorizationService) => { var embeddings = await vectorizationService.GetEmbeddings(text); @@ -109,4 +122,19 @@ .WithName("Copilot") .WithOpenApi(); +app.MapPost("/Chat", async Task (HttpRequest request) => +{ + var message = await Task.FromResult(request.Form["message"]); + var kernel = app.Services.GetRequiredService(); + var chatCompletionService = kernel.GetRequiredService(); + var executionSettings = new OpenAIPromptExecutionSettings + { + ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions + }; + var response = await chatCompletionService.GetChatMessageContentAsync(message.ToString(), executionSettings, kernel); + return response?.Content!; +}) + .WithName("Chat") + .WithOpenApi(); + app.Run(); diff --git a/src/ContosoSuitesWebAPI/Services/DatabaseService.cs b/src/ContosoSuitesWebAPI/Services/DatabaseService.cs index 154bdd6c3..ba5881dcb 100644 --- a/src/ContosoSuitesWebAPI/Services/DatabaseService.cs +++ b/src/ContosoSuitesWebAPI/Services/DatabaseService.cs @@ -1,11 +1,16 @@ using System.Runtime.CompilerServices; using Microsoft.Data.SqlClient; using ContosoSuitesWebAPI.Entities; +using Microsoft.SemanticKernel; +using System.ComponentModel; namespace ContosoSuitesWebAPI.Services; public class DatabaseService : IDatabaseService { + + [KernelFunction] + [Description("Get all hotels.")] public async Task> GetHotels() { var sql = "SELECT HotelID, HotelName, City, Country FROM dbo.Hotel"; @@ -31,6 +36,98 @@ public async Task> GetHotels() return hotels; } + [KernelFunction] + [Description("Get bookings missing hotel rooms.")] + public async Task> GetBookingsMissingHotelRooms() + { + var sql = """ + SELECT + b.BookingID, + b.CustomerID, + b.HotelID, + b.StayBeginDate, + b.StayEndDate, + b.NumberOfGuests + FROM dbo.Booking b + WHERE NOT EXISTS + ( + SELECT 1 + FROM dbo.BookingHotelRoom h + WHERE + b.BookingID = h.BookingID + ); + """; + using var conn = new SqlConnection( + connectionString: Environment.GetEnvironmentVariable("SQLCONNSTR_ContosoSuites")! + ); + conn.Open(); + using var cmd = new SqlCommand(sql, conn); + using var reader = await cmd.ExecuteReaderAsync(); + var bookings = new List(); + while (await reader.ReadAsync()) + { + bookings.Add(new Booking + { + BookingID = reader.GetInt32(0), + CustomerID = reader.GetInt32(1), + HotelID = reader.GetInt32(2), + StayBeginDate = reader.GetDateTime(3), + StayEndDate = reader.GetDateTime(4), + NumberOfGuests = reader.GetInt32(5) + }); + } + conn.Close(); + + return bookings; + } + + [KernelFunction] + [Description("Get bookings with multiple hotel rooms.")] + public async Task> GetBookingsWithMultipleHotelRooms() + { + var sql = """ + SELECT + b.BookingID, + b.CustomerID, + b.HotelID, + b.StayBeginDate, + b.StayEndDate, + b.NumberOfGuests + FROM dbo.Booking b + WHERE + ( + SELECT COUNT(1) + FROM dbo.BookingHotelRoom h + WHERE + b.BookingID = h.BookingID + ) > 1; + """; + using var conn = new SqlConnection( + connectionString: Environment.GetEnvironmentVariable("SQLCONNSTR_ContosoSuites")! + ); + conn.Open(); + using var cmd = new SqlCommand(sql, conn); + using var reader = await cmd.ExecuteReaderAsync(); + var bookings = new List(); + while (await reader.ReadAsync()) + { + bookings.Add(new Booking + { + BookingID = reader.GetInt32(0), + CustomerID = reader.GetInt32(1), + HotelID = reader.GetInt32(2), + StayBeginDate = reader.GetDateTime(3), + StayEndDate = reader.GetDateTime(4), + NumberOfGuests = reader.GetInt32(5) + }); + } + conn.Close(); + + return bookings; + } + + [KernelFunction] + [Description("Get bookings for hotels.")] public async Task> GetBookingsForHotel(int hotelId) { var sql = "SELECT BookingID, CustomerID, HotelID, StayBeginDate, StayEndDate, NumberOfGuests FROM dbo.Booking WHERE HotelID = @HotelID"; @@ -59,6 +156,8 @@ public async Task> GetBookingsForHotel(int hotelId) return bookings; } + [KernelFunction] + [Description("Get bookings by hotel and minimum date.")] public async Task> GetBookingsByHotelAndMinimumDate(int hotelId, DateTime dt) { var sql = "SELECT BookingID, CustomerID, HotelID, StayBeginDate, StayEndDate, NumberOfGuests FROM dbo.Booking WHERE HotelID = @HotelID AND StayBeginDate >= @StayBeginDate"; diff --git a/src/ContosoSuitesWebAPI/Services/IDatabaseService.cs b/src/ContosoSuitesWebAPI/Services/IDatabaseService.cs index 8237bb43c..5775e9466 100644 --- a/src/ContosoSuitesWebAPI/Services/IDatabaseService.cs +++ b/src/ContosoSuitesWebAPI/Services/IDatabaseService.cs @@ -7,4 +7,8 @@ public interface IDatabaseService Task> GetHotels(); Task> GetBookingsForHotel(int hotelId); Task> GetBookingsByHotelAndMinimumDate(int hotelId, DateTime dt); + + Task> GetBookingsMissingHotelRooms(); +Task> GetBookingsWithMultipleHotelRooms(); + } \ No newline at end of file diff --git a/src/InfrastructureAsCode/DeployAzureResources.bicep b/src/InfrastructureAsCode/DeployAzureResources.bicep index c5a4c82d1..c956ad10e 100644 --- a/src/InfrastructureAsCode/DeployAzureResources.bicep +++ b/src/InfrastructureAsCode/DeployAzureResources.bicep @@ -2,7 +2,7 @@ param location string = resourceGroup().location @description('Password for the SQL Server admin user. PLEASE CHANGE THIS BEFORE DEPLOYMENT!') -param sqlAdminPassword string = 'g@G9@2nD7C1BP%uh' +param sqlAdminPassword string = '1$Password' @description('Model deployments for OpenAI') param deployments array = [