├── .azdo └── pipelines │ └── azure-dev.yml ├── .devcontainer └── devcontainer.json ├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── labeler.yml └── workflows │ ├── azure-dev.yml │ └── labeler.yml ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── LICENSE.md ├── README.md ├── assets ├── ai-shopping-cart.png └── architecture-diagram.png ├── azure.yaml ├── infra ├── README.md ├── abbreviations.json ├── app │ ├── ai-shopping-cart-service.bicep │ └── frontend.bicep ├── core │ ├── database │ │ └── postgresql │ │ │ └── flexible-server.bicep │ ├── host │ │ ├── container-app-upsert.bicep │ │ ├── container-app.bicep │ │ ├── container-apps-environment.bicep │ │ ├── container-registry.bicep │ │ └── spring-apps-consumption.bicep │ ├── monitor │ │ ├── application-insights-dashboard.bicep │ │ ├── application-insights.bicep │ │ ├── log-analytics-workspace.bicep │ │ └── monitoring.bicep │ └── security │ │ └── registry-access.bicep ├── main.bicep └── main.parameters.json └── src ├── ai-shopping-cart-service ├── .gitignore ├── .mvn │ └── wrapper │ │ └── maven-wrapper.properties ├── README.md ├── client.http ├── mvnw ├── mvnw.cmd ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── microsoft │ │ └── azure │ │ └── samples │ │ └── aishoppingcartservice │ │ ├── AiShoppingCartServiceApplication.java │ │ ├── AiShoppingCartServiceConfiguration.java │ │ ├── cartitem │ │ ├── CartItem.java │ │ ├── CartItemController.java │ │ ├── CartItemRepository.java │ │ └── exception │ │ │ ├── CartItemNotFoundException.java │ │ │ ├── EmptyCartException.java │ │ │ └── RestResponseEntityExceptionHandler.java │ │ └── openai │ │ ├── ShoppingCartAiRecommendations.java │ │ ├── SystemMessageConstants.java │ │ └── UserMessageConstants.java │ └── resources │ ├── META-INF │ └── additional-spring-configuration-metadata.json │ └── application.properties └── frontend ├── .gitignore ├── Dockerfile ├── README.MD ├── entrypoint.js ├── entrypoint.sh ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── @types │ └── window.d.ts ├── App.tsx ├── components │ ├── AddItemForm.tsx │ ├── AiNutritionAnalysis.tsx │ ├── Error.tsx │ ├── Header.tsx │ ├── Item.tsx │ ├── ItemList.tsx │ ├── Loader.tsx │ ├── NutriscoreBar.tsx │ ├── NutriscoreBarItem.tsx │ ├── ShoppingCart.tsx │ ├── Top3Recipes.tsx │ └── index.ts ├── config │ └── index.ts ├── index.scss ├── index.tsx ├── models │ ├── CartItem.ts │ ├── Status.ts │ └── index.ts ├── react-app-env.d.ts ├── reportWebVitals.ts ├── setupTests.ts └── utils │ ├── idUtils.ts │ └── stringUtils.ts └── tsconfig.json /.azdo/pipelines/azure-dev.yml: -------------------------------------------------------------------------------- 1 | # Run when commits are pushed to mainline branch (main) 2 | # Set this to the mainline branch you are using 3 | trigger: 4 | - main 5 | 6 | # Azure Pipelines workflow to deploy to Azure using azd 7 | # To configure required secrets for connecting to Azure, simply run `azd pipeline config --provider azdo` 8 | 9 | pool: 10 | vmImage: ubuntu-latest 11 | 12 | # Use azd provided container image that has azd, infra, multi-language build tools pre-installed. 13 | container: mcr.microsoft.com/azure-dev-cli-apps:latest 14 | 15 | steps: 16 | - pwsh: | 17 | azd config set auth.useAzCliAuth "true" 18 | displayName: Configure AZD to Use AZ CLI Authentication. 19 | 20 | - task: AzureCLI@2 21 | displayName: Set Azure Spring Apps alpha feature on 22 | inputs: 23 | azureSubscription: azconnection 24 | scriptType: bash 25 | scriptLocation: inlineScript 26 | inlineScript: | 27 | azd config set alpha.springapp on 28 | 29 | - task: AzureCLI@2 30 | displayName: Provision Infrastructure 31 | inputs: 32 | azureSubscription: azconnection 33 | scriptType: bash 34 | scriptLocation: inlineScript 35 | inlineScript: | 36 | azd provision --no-prompt 37 | env: 38 | AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) 39 | AZURE_ENV_NAME: $(AZURE_ENV_NAME) 40 | AZURE_LOCATION: $(AZURE_LOCATION) 41 | azureOpenAiApiKey: $(AZURE_OPENAI_API_KEY) 42 | azureOpenAiEndpoint: $(AZURE_OPENAI_ENDPOINT) 43 | azureOpenAiDeploymentId: $(AZURE_OPENAI_DEPLOYMENT_ID) 44 | isAzureOpenAiGpt4Model: $(IS_AZURE_OPENAI_GPT4_MODEL) 45 | 46 | - task: AzureCLI@2 47 | displayName: Deploy Application 48 | inputs: 49 | azureSubscription: azconnection 50 | scriptType: bash 51 | scriptLocation: inlineScript 52 | inlineScript: | 53 | azd deploy --no-prompt 54 | env: 55 | AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) 56 | AZURE_ENV_NAME: $(AZURE_ENV_NAME) 57 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AI Shopping Cart Dev Container", 3 | "image": "mcr.microsoft.com/devcontainers/java:1-17-bullseye", 4 | "features": { 5 | // See https://containers.dev/features for list of features 6 | "ghcr.io/devcontainers/features/docker-in-docker:2": { 7 | }, 8 | "ghcr.io/azure/azure-dev/azd:latest": {}, 9 | "ghcr.io/devcontainers/features/java:1": { 10 | "version": "none", 11 | "installMaven": true, 12 | "mavenVersion": "3.8.8" 13 | }, 14 | "ghcr.io/devcontainers/features/node:1": { 15 | "version": "20.5.0" 16 | }, 17 | "ghcr.io/devcontainers-contrib/features/typescript:2": {} 18 | }, 19 | "customizations": { 20 | "vscode": { 21 | "extensions": [ 22 | "GitHub.vscode-github-actions", 23 | "ms-azuretools.azure-dev", 24 | "ms-azuretools.vscode-azurefunctions", 25 | "ms-azuretools.vscode-bicep", 26 | "ms-azuretools.vscode-docker", 27 | "vscjava.vscode-java-pack", 28 | "amodio.tsl-problem-matcher" 29 | ] 30 | } 31 | }, 32 | "forwardPorts": [ 33 | // Forward ports if needed for local development 34 | ], 35 | "postCreateCommand": "", 36 | "remoteUser": "vscode", 37 | "hostRequirements": { 38 | "memory": "8gb" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | > Please provide us with the following information: 5 | > --------------------------------------------------------------- 6 | 7 | ### This issue is for a: (mark with an `x`) 8 | ``` 9 | - [ ] bug report -> please search issues before submitting 10 | - [ ] feature request 11 | - [ ] documentation issue or request 12 | - [ ] regression (a behavior that used to work and stopped in a new release) 13 | ``` 14 | 15 | ### Minimal steps to reproduce 16 | > 17 | 18 | ### Any log messages given by the failure 19 | > 20 | 21 | ### Expected/desired behavior 22 | > 23 | 24 | ### Versions 25 | > 26 | 27 | ### Mention any other details that might be useful 28 | 29 | > --------------------------------------------------------------- 30 | > Thanks! We'll be in touch soon. 31 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | 3 | * ... 4 | 5 | ## Does this introduce a breaking change? 6 | 7 | ``` 8 | [ ] Yes 9 | [ ] No 10 | ``` 11 | 12 | ## Pull Request Type 13 | What kind of change does this Pull Request introduce? 14 | 15 | 16 | ``` 17 | [ ] Bugfix 18 | [ ] Feature 19 | [ ] Code style update (formatting, local variables) 20 | [ ] Refactoring (no functional changes, no api changes) 21 | [ ] Documentation content changes 22 | [ ] Other... Please describe: 23 | ``` 24 | 25 | ## How to Test 26 | * Get the code 27 | 28 | ``` 29 | git clone [repo-address] 30 | cd [repo-name] 31 | git checkout [branch-name] 32 | ``` 33 | 34 | * Run the front-end 35 | 36 | ``` 37 | cd src/frontend 38 | npm install 39 | npm start 40 | ``` 41 | 42 | * Run the AI Shopping Cart Service 43 | 44 | ``` 45 | cd ../ai-shopping-cart-service 46 | ./mvnw clean package 47 | java -jar target/ai-shopping-cart-service-.jar 48 | ``` 49 | 50 | * Test the code 51 | 52 | ``` 53 | ``` 54 | 55 | ## What to Check 56 | Verify that the following are valid 57 | * ... 58 | 59 | ## Other Information 60 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | # Add 'automation: azd' for changes in Azure Developer CLI YAML file 2 | 'automation: azd': 3 | - 'azure.yaml' 4 | 5 | # Add 'automation: CI/CD' for changes in GitHub actions (or other) and in Azure DevOps pipeline(s) 6 | 'automation: CI/CD': 7 | - '.github/*' 8 | - '.github/**/*' 9 | - '.azdo/*' 10 | - '.azdo/**/*' 11 | 12 | # Add 'automation: infra' for changes in infrastructure code (IaC) 13 | 'automation: infra': 14 | - 'infra/*' 15 | - 'infra/**/*' 16 | 17 | # Add 'documentation' label to any change in md files or in any assets in the /assets folder 18 | documentation: 19 | - '*.md' 20 | - '**/*.md' 21 | - 'assets/*' 22 | - 'assets/**/*' 23 | 24 | # Add 'src: ai-shopping-cart-service' for any change in /src/ai-shopping-cart-service 25 | 'src: ai-shopping-cart-service': 26 | - 'src/ai-shopping-cart-service/*' 27 | - 'src/ai-shopping-cart-service/**/*' 28 | 29 | # Add 'src: frontend' for any change in /src/frontend 30 | 'src: frontend': 31 | - 'src/frontend/*' 32 | - 'src/frontend/**/*' 33 | 34 | # Add 'tools: dev-container' for any change in /.devcontainer 35 | 'tools: dev-container': 36 | - '.devcontainer/*' 37 | - '.devcontainer/**/*' 38 | -------------------------------------------------------------------------------- /.github/workflows/azure-dev.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | push: 4 | branches: 5 | - main 6 | 7 | # GitHub Actions workflow to deploy to Azure using azd 8 | # To configure required secrets for connecting to Azure, simply run `azd pipeline config` 9 | 10 | # Set up permissions for deploying with secretless Azure federated credentials 11 | # https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure?tabs=azure-portal%2Clinux#set-up-azure-login-with-openid-connect-authentication 12 | permissions: 13 | id-token: write 14 | contents: read 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | env: 20 | AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }} 21 | AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }} 22 | AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} 23 | AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v3 27 | 28 | - uses: actions/setup-java@v3 29 | with: 30 | distribution: 'microsoft' 31 | java-version: '17' 32 | 33 | - name: Install azd 34 | uses: Azure/setup-azd@v0.1.0 35 | 36 | - name: Log in with Azure (Federated Credentials) 37 | if: ${{ env.AZURE_CLIENT_ID != '' }} 38 | run: | 39 | azd auth login ` 40 | --client-id "$Env:AZURE_CLIENT_ID" ` 41 | --federated-credential-provider "github" ` 42 | --tenant-id "$Env:AZURE_TENANT_ID" 43 | shell: pwsh 44 | 45 | - name: Log in with Azure (Client Credentials) 46 | if: ${{ env.AZURE_CREDENTIALS != '' }} 47 | run: | 48 | $info = $Env:AZURE_CREDENTIALS | ConvertFrom-Json -AsHashtable; 49 | Write-Host "::add-mask::$($info.clientSecret)" 50 | 51 | azd auth login ` 52 | --client-id "$($info.clientId)" ` 53 | --client-secret "$($info.clientSecret)" ` 54 | --tenant-id "$($info.tenantId)" 55 | shell: pwsh 56 | env: 57 | AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} 58 | 59 | - name: Set Azure Spring Apps alpha feature on 60 | run: azd config set alpha.springapp on 61 | 62 | - name: Provision Infrastructure 63 | run: azd provision --no-prompt 64 | env: 65 | AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} 66 | AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} 67 | AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} 68 | azureOpenAiApiKey: ${{ secrets.AZURE_OPENAI_API_KEY }} 69 | azureOpenAiEndpoint: ${{ vars.AZURE_OPENAI_ENDPOINT }} 70 | azureOpenAiDeploymentId: ${{ vars.AZURE_OPENAI_DEPLOYMENT_ID }} 71 | isAzureOpenAiGpt4Model: ${{ vars.IS_AZURE_OPENAI_GPT4_MODEL }} 72 | 73 | - name: Deploy Application 74 | run: azd deploy --no-prompt 75 | env: 76 | AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} 77 | AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} 78 | AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} 79 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | # Add a label to the PR when it is opened or updated 2 | 3 | name: Pull Request Labeler 4 | 5 | on: [pull_request] 6 | 7 | jobs: 8 | triage: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | pull-requests: write 13 | steps: 14 | - uses: actions/labeler@v4 15 | with: 16 | sync-labels: true 17 | configuration-path: .github/labeler.yml 18 | dot: true 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .azure 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node-terminal", 9 | "name": "Frontend", 10 | "command": "npm start", 11 | "request": "launch", 12 | "cwd": "${workspaceFolder}/src/frontend", 13 | "preLaunchTask": "Restore all dependecies" 14 | }, 15 | { 16 | "type": "java", 17 | "name": "AI Shopping Cart Service", 18 | "request": "launch", 19 | "mainClass": "com.microsoft.azure.samples.aishoppingcartservice.AiShoppingCartServiceApplication", 20 | "projectName": "ai-shopping-cart-service" 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2 3 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "shell", 6 | "label": "Restore all dependecies", 7 | "detail": "azd restore", 8 | "command": "azd restore", 9 | "problemMatcher": [], 10 | "presentation": { 11 | "reveal": "silent", 12 | "panel": "shared" 13 | } 14 | }, 15 | { 16 | "type": "npm", 17 | "label": "Run Frontend", 18 | "detail": "npm start", 19 | "script": "start", 20 | "path": "src/frontend", 21 | "problemMatcher": [], 22 | "presentation": { 23 | "reveal": "always", 24 | "panel": "dedicated" 25 | }, 26 | "dependsOn": [ 27 | "Restore all dependecies" 28 | ] 29 | }, 30 | { 31 | "type": "shell", 32 | "label": "Run AI Shopping Cart Service", 33 | "command": "./mvnw spring-boot:run", 34 | "options": { 35 | "cwd": "${workspaceFolder}/src/ai-shopping-cart-service" 36 | }, 37 | "problemMatcher": [], 38 | "presentation": { 39 | "reveal": "always", 40 | "panel": "dedicated" 41 | } 42 | }, 43 | { 44 | "label": "Run All", 45 | "dependsOn": [ 46 | "Run Frontend", 47 | "Run AI Shopping Cart Service" 48 | ], 49 | "problemMatcher": [] 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## AI Shopping Cart - App Template for Java, Azure OpenAI and Azure Spring Apps - Changelog 2 | 3 | 4 | # 1.1.0 (2023-08-28) 5 | 6 | ## What's Changed 7 | 8 | *Features* 9 | 10 | * Update the application to display the error message in the frontend by @pmalarme in https://github.com/Azure-Samples/app-templates-java-openai-springapps/pull/35 11 | 12 | *Bug Fixes* 13 | 14 | *Breaking Changes* 15 | 16 | **Full Changelog**: https://github.com/Azure-Samples/app-templates-java-openai-springapps/compare/v1.0.1...v1.1.0 17 | 18 | 19 | # 1.0.1 (2023-08-25) 20 | 21 | ## What's Changed 22 | 23 | *Features* 24 | 25 | *Bug Fixes* 26 | 27 | * Fix Changelog link for v1.0.0 commit list by @pmalarme in https://github.com/Azure-Samples/app-templates-java-openai-springapps/pull/28 28 | * Fix the name of the spring apps instance to be globally unique. by @pmalarme in https://github.com/Azure-Samples/app-templates-java-openai-springapps/pull/31 29 | 30 | *Breaking Changes* 31 | 32 | **Full Changelog**: https://github.com/Azure-Samples/app-templates-java-openai-springapps/compare/v1.0.0...v1.0.1 33 | 34 | 35 | # 1.0.0 (2023-08-04) 36 | 37 | ## What's Changed 38 | 39 | *Features* 40 | 41 | * Add the labeler workflow to automatically label the pull requests by @pmalarme in https://github.com/Azure-Samples/app-templates-java-openai-springapps/pull/3 42 | * Add the initial version of ai shopping cart by @pmalarme in https://github.com/Azure-Samples/app-templates-java-openai-springapps/pull/4 43 | * Add the documentation on how to deploy the template by @pmalarme in https://github.com/Azure-Samples/app-templates-java-openai-springapps/pull/9 44 | * Add the metadata for Microsoft Learn Sample app onboarding by @pmalarme in https://github.com/Azure-Samples/app-templates-java-openai-springapps/pull/11 45 | * Update README.md by @pmalarme in https://github.com/Azure-Samples/app-templates-java-openai-springapps/pull/12 46 | * Update README.md to fix typo in enabling alpha for spring apps by @pmalarme in https://github.com/Azure-Samples/app-templates-java-openai-springapps/pull/21 47 | * Make mvnw an executable by @pmalarme in https://github.com/Azure-Samples/app-templates-java-openai-springapps/pull/22 48 | * Add telemetry by @aarthiem in https://github.com/Azure-Samples/app-templates-java-openai-springapps/pull/19 49 | * Improve the documentation of Azure OpenAI by @pmalarme in https://github.com/Azure-Samples/app-templates-java-openai-springapps/pull/18 50 | * Update README.md to fix a typo in Telemetry section by @pmalarme in https://github.com/Azure-Samples/app-templates-java-openai-springapps/pull/23 51 | * Add support for GPT-3.5 Turbo by @pmalarme in https://github.com/Azure-Samples/app-templates-java-openai-springapps/pull/25 52 | * Add azure devops pipeline to deploy the sample app with azure developer cli by @pmalarme in https://github.com/Azure-Samples/app-templates-java-openai-springapps/pull/26 53 | * Update README.md to add Enterprise scenarios by @aarthiem in https://github.com/Azure-Samples/app-templates-java-openai-springapps/pull/27 54 | 55 | *Bug Fixes* 56 | 57 | * Fix the link for the blog post by @pmalarme in https://github.com/Azure-Samples/app-templates-java-openai-springapps/pull/10 58 | 59 | *Breaking Changes* 60 | 61 | ## New Contributors 62 | 63 | * @pmalarme made their first contribution in https://github.com/Azure-Samples/app-templates-java-openai-springapps/pull/3 64 | * @aarthiem made their first contribution in https://github.com/Azure-Samples/app-templates-java-openai-springapps/pull/19 65 | 66 | **Full Changelog**: https://github.com/Azure-Samples/app-templates-java-openai-springapps/commits/v1.0.0 67 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to AI Shopping Cart - App Template for Java, Azure OpenAI and Azure Spring Apps 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 5 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 6 | 7 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 8 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 9 | provided by the bot. You will only need to do this once across all repos using our CLA. 10 | 11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 14 | 15 | - [Code of Conduct](#coc) 16 | - [Issues and Bugs](#issue) 17 | - [Feature Requests](#feature) 18 | - [Submission Guidelines](#submit) 19 | 20 | ## Code of Conduct 21 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 22 | 23 | ## Found an Issue? 24 | If you find a bug in the source code or a mistake in the documentation, you can help us by 25 | [submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can 26 | [submit a Pull Request](#submit-pr) with a fix. 27 | 28 | ## Want a Feature? 29 | You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub 30 | Repository. If you would like to *implement* a new feature, please submit an issue with 31 | a proposal for your work first, to be sure that we can use it. 32 | 33 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 34 | 35 | ## Submission Guidelines 36 | 37 | ### Submitting an Issue 38 | Before you submit an issue, search the archive, maybe your question was already answered. 39 | 40 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 41 | Help us to maximize the effort we can spend fixing issues and adding new 42 | features, by not reporting duplicate issues. Providing the following information will increase the 43 | chances of your issue being dealt with quickly: 44 | 45 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 46 | * **Version** - what version is affected (e.g. 0.1.2) 47 | * **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you 48 | * **Browsers and Operating System** - is this a problem with all browsers? 49 | * **Reproduce the Error** - provide a live example or a unambiguous set of steps 50 | * **Related Issues** - has a similar issue been reported before? 51 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 52 | causing the problem (line of code or commit) 53 | 54 | You can file new issues by providing the above information at the corresponding repository's issues link: https://github.com/Azure-Samples/app-templates-java-openai-springapps/issues/new]. 55 | 56 | ### Submitting a Pull Request (PR) 57 | Before you submit your Pull Request (PR) consider the following guidelines: 58 | 59 | * Search the repository (https://github.com/Azure-Samples/app-templates-java-openai-springapps/pulls) for an open or closed PR 60 | that relates to your submission. You don't want to duplicate effort. 61 | 62 | * Make your changes in a new git fork: 63 | 64 | * Commit your changes using a descriptive commit message 65 | * Push your fork to GitHub: 66 | * In GitHub, create a pull request 67 | * If we suggest changes then: 68 | * Make the required updates. 69 | * Rebase your fork and force push to your GitHub repository (this will update your Pull Request): 70 | 71 | ```shell 72 | git rebase main -i 73 | git push -f 74 | ``` 75 | 76 | That's it! Thank you for your contribution! 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Azure Samples 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_type: sample 3 | languages: 4 | - azdeveloper 5 | - java 6 | - typescript 7 | - bicep 8 | - html 9 | products: 10 | - azure 11 | - azure-container-apps 12 | - azure-spring-apps 13 | - azure-container-registry 14 | - azure-monitor 15 | - ms-build-openjdk 16 | - ai-services 17 | - azure-openai 18 | - azure-database-postgresql 19 | urlFragment: app-templates-java-openai-springapps 20 | name: AI Shopping Cart - App Template for Java, Azure OpenAI and Azure Spring Apps 21 | description: AI Shopping Cart Sample Application with Azure OpenAI and Azure Spring Apps 22 | --- 23 | 24 | # AI Shopping Cart - App Template for Java, Azure OpenAI and Azure Spring Apps 25 | 26 | [![Open in GitHub Codespaces](https://img.shields.io/badge/Github_Codespaces-Open-black?style=for-the-badge&logo=github 27 | )](https://codespaces.new/Azure-Samples/app-templates-java-openai-springapps) 28 | [![Open in Remote - Dev Containers](https://img.shields.io/badge/Dev_Containers-Open-blue?style=for-the-badge&logo=visualstudiocode 29 | )](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/Azure-Samples/app-templates-java-openai-springapps) 30 | 31 | AI Shopping Cart is a sample application that supercharges your shopping experience with the power of AI. It leverages Azure OpenAI and Azure Spring Apps to build a recommendation engine that is not only scalable, resilient, and secure, but also personalized to your needs. Taking advantage of Azure OpenAI, the application performs nutrition analysis on the items in your cart and generates the top 3 recipes using those ingredients. With Azure Developer CLI (azd), you’re just a few commands away from having this fully functional sample application up and running in Azure. Let's get started! 32 | 33 | > This sample application take inspiration on this original work: https://github.com/lopezleandro03/ai-assisted-groceries-cart 34 | 35 | > Refer to the [App Templates](https://github.com/microsoft/App-Templates) repository Readme for more samples that are compatible with [`azd`](https://github.com/Azure/azure-dev/). 36 | 37 | ![AI Shopping Cart](./assets/ai-shopping-cart.png) 38 | 39 | ## Pre-requisites 40 | 41 | - [Install the Azure Developer CLI](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) 42 | - An Azure account with an active subscription. [Create one for free](https://azure.microsoft.com/free). 43 | - [OpenJDK 17](https://learn.microsoft.com/en-us/java/openjdk/install) 44 | - [Node.js 20.5.0+](https://nodejs.org/en/download/) 45 | - [Docker](https://docs.docker.com/get-docker/) 46 | - [Azure OpenAI with `gpt-4` or `gpt-35-turbo`](https://learn.microsoft.com/en-us/azure/ai-services/openai/overview#how-do-i-get-access-to-azure-openai) [\[Note\]](#azure-openai) 47 | - Review the [architecture diagram and the resources](#application-architecture) you'll deploy and the [Azure OpenAI](#azure-openai) section. 48 | 49 | ## Quickstart 50 | 51 | To learn how to get started with any template, follow [this quickstart](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/get-started?tabs=localinstall&pivots=programming-language-java). For this template `Azure-Samples/app-templates-java-openai-springapps`, you need to execute a few additional steps as described below. 52 | 53 | This quickstart will show you how to authenticate on Azure, enable Spring Apps [alpha feature](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/feature-versioning#alpha-features) for azd, initialize using a template, set the [environment variables](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/manage-environment-variables) for Azure OpenAI, provision the infrastructure, and deploy the code to Azure: 54 | 55 | ```bash 56 | # Log in to azd if you haven't already 57 | azd auth login 58 | 59 | # Enable Azure Spring Apps alpha feature for azd 60 | azd config set alpha.springapp on 61 | 62 | # First-time project setup. Initialize a project in the current directory using this template 63 | azd init --template Azure-Samples/app-templates-java-openai-springapps 64 | 65 | # Set the environment variables for Azure OpenAI 66 | azd env set azureOpenAiApiKey 67 | azd env set azureOpenAiEndpoint 68 | azd env set azureOpenAiDeploymentId 69 | 70 | # To use GPT-3.5 Turbo model set this environment variable to false 71 | azd env set isAzureOpenAiGpt4Model true 72 | 73 | # Provision and deploy to Azure 74 | azd up 75 | ``` 76 | 77 | > Notes 78 | > * Replace the placeholders with the values from your Azure OpenAI resource. 79 | > * If you are using `gpt-35-turbo` model, you need to set `isAzureOpenAiGpt4Model` to `false` before provisioning the resource and deploying the sample application to Azure: 80 | > ```bash 81 | > azd env set isAzureOpenAiGpt4Model false 82 | > ``` 83 | 84 | At the end of the deployment, you will see the URL of the front-end. Open the URL in a browser to see the application in action. 85 | 86 | ## Application Architecture 87 | 88 | This sample application uses the following Azure resources: 89 | 90 | - [Azure Container Apps (Environment)](https://learn.microsoft.com/en-us/azure/container-apps/) to host the frontend as a Container App and Azure Spring Apps [Standard comsumption and dedicated plan](https://learn.microsoft.com/en-us/azure/spring-apps/overview#standard-consumption-and-dedicated-plan) 91 | - [Azure Spring Apps](https://learn.microsoft.com/azure/spring-apps/) to host the AI Shopping Cart Service as a Spring App 92 | - [Azure Container Registry](https://learn.microsoft.com/azure/container-registry/) to host the Docker image for the frontend 93 | - [Azure Database for PostgreSQL (Flexible Server)](https://learn.microsoft.com/azure/postgresql/) to store the data for the AI Shopping Cart Service 94 | - [Azure Monitor](https://learn.microsoft.com/en-us/azure/azure-monitor/) for monitoring and logging 95 | - [Azure OpenAI](https://learn.microsoft.com/en-us/azure/ai-services/openai/) to perform nutrition analysis and generate top 3 recipes. It is not deployed with the sample app[\[Note\]](#azure-openai). 96 | 97 | Here's a high level architecture diagram that illustrates these components. Excepted Azure OpenAI, all the other resources are provisioned in a single [resource group](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/manage-resource-groups-portal) that is created when you create your resources using `azd up`. 98 | 99 | ![Architecture diagram](./assets/architecture-diagram.png) 100 | 101 | > This template provisions resources to an Azure subscription that you will select upon provisioning them. Please refer to the [Pricing calculator for Microsoft Azure](https://azure.microsoft.com/pricing/calculator/) and, if needed, update the included Azure resource definitions found in `infra/main.bicep` to suit your needs. 102 | 103 | ## Azure OpenAI 104 | 105 | This sample application uses Azure OpenAI. It is not part of the automated deployment process. You will need to create an Azure OpenAI resource and configure the application to use it. Please follow the instructions in the [Azure OpenAI documentation](https://learn.microsoft.com/en-us/azure/ai-services/openai/overview#how-do-i-get-access-to-azure-openai) to get access to Azure OpenAI. Do not forget to read the [overview of the Responsible AI practices for Azure OpenAI models](https://learn.microsoft.com/en-us/legal/cognitive-services/openai/overview?context=%2Fazure%2Fai-services%2Fopenai%2Fcontext%2Fcontext) before you start using Azure OpenAI and request access. 106 | 107 | The current version of the sample app requires a publicly accessible Azure OpenAI resource (i.e. Allow access from all networks). This sample is not intended to be used in production. To know more about networking and security for Azure OpenAI, please refer to the [Azure OpenAI documentation](#azure-spring-apps-consumption---networking-and-security). 108 | 109 | This sample app was developed to be used with `gpt-4` model. It also supports `gpt-35-turbo`. To use `gpt-35-turbo`, you need to set `isAzureOpenAiGpt4Model` to `false` (cf. [Quickstart](#quickstart)). By default, this parameter/environment variable is set to `true`. To complete the setup of the application, you need to set the following information from the Azure OpenAI resource: 110 | 111 | - `azureOpenAiApiKey` - Azure OpenAI API key 112 | - `azureOpenAiEndpoint` - Azure OpenAI endpoint 113 | - `azureOpenAiDeploymentId` - Azure OpenAI deployment ID of `gpt-4` or `gpt-3.5-turbo` model 114 | 115 | The API key and the endpoint can be found in the Azure Portal. You can follow these instructions: [Retrieve key and enpoint](https://learn.microsoft.com/en-us/azure/ai-services/openai/quickstart?tabs=command-line&pivots=programming-language-java#retrieve-key-and-endpoint). The deployment id corresponds to the `deployment name` in [this guide](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/create-resource?pivots=web-portal#deploy-a-model). 116 | 117 | [Prompt engineering](https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/prompt-engineering) is important to get the best results from Azure OpenAI. Text prompts are how users interact with GPT models. As with all generative large language model (LLM), GPT models try to produce the next series of words that are the most likely to follow the previous text. It is a bit like asking to the AI model: What is the first thing that comes to mind when I say ``? 118 | 119 | With the [Chat Completion API](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/chatgpt?pivots=programming-language-chat-completions), there are distinct sections of the prompt that are sent to the API associated with a specific role: system, user and assitant. The system message is included at the begining of the prompt and is used to provides the initial instructions to the model: description of the assitant, personality traits, instructions/rules it will follow, etc. 120 | 121 | `AI Shopping Cart Service` is using [Azure OpenAI client library for Java](https://learn.microsoft.com/en-us/java/api/overview/azure/ai-openai-readme). This libary is part of of [Azure SDK for Java](https://learn.microsoft.com/en-us/azure/developer/java/sdk/). It is implemented as a [chat completion](https://learn.microsoft.com/en-us/java/api/overview/azure/ai-openai-readme?view=azure-java-preview#chat-completions). In the service, we have 2 system messages in [SystemMessageConstants.java](src/ai-shopping-cart-service/src/main/java/com/microsoft/azure/samples/aishoppingcartservice/openai/SystemMessageConstants.java): one for AI Nutrition Analysis and one to generate top 3 recipes. The system message is followed by a user message: `The basket is: `. The assistant message is the response from the model. The service is using the [ShoppingCartAiRecommendations](src/ai-shopping-cart-service/src/main/java/com/microsoft/azure/samples/aishoppingcartservice/openai/ShoppingCartAiRecommendations.java) to interact with Azure OpenAI. In this class you will find the code that is responsible for generating the prompt and calling the Azure OpenAI API: `getChatCompletion`. To know more about temperature and topP used in this class, please refer to [the documentation](https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/advanced-prompt-engineering?pivots=programming-language-chat-completions#temperature-and-top_p-parameters). 122 | 123 | For `gpt-35-turbo` model, more context is added to the user message. This additional context is added at the end of the user message. It provides more information on the format of the JSON that OpenAI model needs to return and ask the model tor return only the JSON without additional text. This additional context is available in [UserMessageConstants.java](src/ai-shopping-cart-service/src/main/java/com/microsoft/azure/samples/aishoppingcartservice/openai/UserMessageConstants.java). 124 | 125 | - [Pre-requisites](#pre-requisites) :arrow_heading_up: 126 | - [Application Architecture](#application-architecture) :arrow_heading_up: 127 | 128 | ## Application Code 129 | 130 | This template is structured to follow the [Azure Developer CLI template convetions](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/make-azd-compatible?pivots=azd-create#azd-conventions). You can learn more about `azd` architecture in [the official documentation](https://learn.microsoft.com/azure/developer/azure-developer-cli/make-azd-compatible?pivots=azd-create#understand-the-azd-architecture). 131 | 132 | ## Next Steps 133 | 134 | At this point, you have a complete application deployed on Azure. 135 | 136 | ### Enterprise Scenarios 137 | 138 | For enterprise needs, looking for polyglot applications deployment, Tanzu components support and SLA assurance, we recommend to use [Azure Spring Apps Enterprise](https://learn.microsoft.com/en-us/azure/spring-apps/overview#enterprise-plan). Check the [Azure Spring Apps landing zone accelerator](https://github.com/Azure/azure-spring-apps-landing-zone-accelerator) that provides architectural guidance designed to streamline the production ready infrastructure provisioning and deployment of Spring Boot and Spring Cloud applications to Azure Spring Apps. As the workload owner, use [architectural guidance](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/scenarios/app-platform/spring-apps/landing-zone-accelerator) provided in landing zone accelerator to achieve your target technical state with confidence. 139 | 140 | ### Azure Developer CLI 141 | 142 | You have deployed the sample application using Azure Developer CLI, however there is much more that the Azure Developer CLI can do. These next steps will introduce you to additional commands that will make creating applications on Azure much easier. Using the Azure Developer CLI, you can setup your pipelines, monitor your application, test and debug locally. 143 | 144 | - [`azd down`](https://learn.microsoft.com/azure/developer/azure-developer-cli/reference#azd-down) - to delete all the Azure resources created with this template 145 | 146 | - [`azd pipeline config`](https://learn.microsoft.com/azure/developer/azure-developer-cli/configure-devops-pipeline?tabs=GitHub) - to configure a CI/CD pipeline (using GitHub Actions or Azure DevOps) to deploy your application whenever code is pushed to the main branch. 147 | - Several environment variables / secrets need to be set for Azure OpenAI resource: 148 | - `AZURE_OPENAI_API_KEY`: API key for Azure OpenAI resource 149 | - For GitHub workflows, you should use [GitHub Secrets](https://docs.github.com/en/actions/reference/encrypted-secrets) 150 | - For Azure DevOps pipelines, you check 'Keep this value secret' when creating the variable 151 | - `AZURE_OPENAI_ENDPOINT`: Endpoint for Azure OpenAI resource 152 | - `AZURE_OPENAI_DEPLOYMENT_ID`: Deployment ID/name for Azure OpenAI resource 153 | - `IS_AZURE_OPENAI_GPT4_MODEL`: Set to `true` if you are using GPT-4 model and to `false` if you are using GPT-3.5 Turbo model 154 | 155 | - [`azd monitor`](https://learn.microsoft.com/azure/developer/azure-developer-cli/monitor-your-app) - to monitor the application and quickly navigate to the various Application Insights dashboards (e.g. overview, live metrics, logs) 156 | 157 | - [Run and Debug Locally](https://learn.microsoft.com/azure/developer/azure-developer-cli/debug?pivots=ide-vs-code) - using Visual Studio Code and the Azure Developer CLI extension 158 | 159 | ### Additional `azd` commands 160 | 161 | The Azure Developer CLI includes many other commands to help with your Azure development experience. You can view these commands at the terminal by running `azd help`. You can also view the full list of commands on our [Azure Developer CLI command](https://aka.ms/azure-dev/ref) page. 162 | 163 | ## Resources 164 | 165 | These are additional resources that you can use to learn more about the sample application and its underlying technologies. 166 | 167 | - [Start from zero and scale to zero – Azure Spring Apps consumption plan](https://techcommunity.microsoft.com/t5/apps-on-azure-blog/start-from-zero-and-scale-to-zero-azure-spring-apps-consumption/ba-p/3774825) 168 | 169 | ### Azure Spring Apps Consumption - Networking and Security 170 | 171 | - [https://learn.microsoft.com/en-us/azure/ai-services/openai/encrypt-data-at-rest](https://learn.microsoft.com/en-us/azure/ai-services/cognitive-services-virtual-networks?context=%2Fazure%2Fai-services%2Fopenai%2Fcontext%2Fcontext&tabs=portal) 172 | - [https://learn.microsoft.com/en-us/azure/ai-services/openai/encrypt-data-at-rest](https://learn.microsoft.com/en-us/azure/ai-services/openai/encrypt-data-at-rest) 173 | - [How to configure Azure OpenAI Service with managed identities](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/managed-identity) 174 | 175 | ## Data Collection 176 | The software may collect information about you and your use of the software and send it to Microsoft. Microsoft may use this information to provide services and improve our products and services. You may turn off the telemetry as described in the repository. There are also some features in the software that may enable you and Microsoft to collect data from users of your applications. If you use these features, you must comply with applicable law, including providing appropriate notices to users of your applications together with a copy of Microsoft's privacy statement. Our privacy statement is located at https://go.microsoft.com/fwlink/?LinkId=521839. You can learn more about data collection and use in the help documentation and our privacy statement. Your use of the software operates as your consent to these practices. 177 | 178 | ## Telemetry Configuration 179 | Telemetry collection is on by default. 180 | 181 | To opt-out, set the variable enableTelemetry to false in `infra/main.parameters.json` or in bicep template `infra/main.bicep`. It can be set using the following command when the provisionning is done with Azure Developer CLI: 182 | 183 | ```bash 184 | azd env set enableTelemetry false 185 | ``` 186 | 187 | ## Trademarks 188 | 189 | This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft 190 | trademarks or logos is subject to and must follow 191 | [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). 192 | Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. 193 | Any use of third-party trademarks or logos are subject to those third-party's policies. 194 | -------------------------------------------------------------------------------- /assets/ai-shopping-cart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/app-templates-java-openai-springapps/d3061b97ba58d1e04aceaa0d6e86d2ef2bf448e5/assets/ai-shopping-cart.png -------------------------------------------------------------------------------- /assets/architecture-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/app-templates-java-openai-springapps/d3061b97ba58d1e04aceaa0d6e86d2ef2bf448e5/assets/architecture-diagram.png -------------------------------------------------------------------------------- /azure.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/alpha/azure.yaml.json 2 | 3 | name: shopping-cart 4 | metadata: 5 | template: app-templates-java-openai-springapps@0.0.1-beta 6 | services: 7 | frontend: 8 | language: js 9 | project: ./src/frontend 10 | host: containerapp 11 | ai-shopping-cart-service: 12 | language: java 13 | project: ./src/ai-shopping-cart-service 14 | host: springapp 15 | -------------------------------------------------------------------------------- /infra/README.md: -------------------------------------------------------------------------------- 1 | # Infrastructure for AI Shopping Cart Sample App 2 | 3 | This folder contains the infrastructure for the AI Shopping Cart sample apps. The infrastructure is supposed to be deployed using [Azure Developer CLI](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/overview) (azd). You can check the [AI Shopping Cart README](../README.md) for more information about the sample apps and to deploy it using `azd`. 4 | 5 | ## Deploy with Azure Developer CLI 6 | 7 | To deploy only the infrastructure, you can use the following command in the root folder of this repository: 8 | 9 | ```bash 10 | azd provision 11 | ``` 12 | 13 | Be sure to set all the parameters as described in the [AI Shopping Cart README](../README.md). 14 | 15 | ## Deploy with Azure CLI 16 | 17 | It can also be deployed manually using [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest) (az). The following instructions assume you have already installed Azure CLI. Follow these steps to deploy the infrastructure: 18 | 19 | 1. Login to Azure CLI using the following command: 20 | 21 | ```bash 22 | az login 23 | ``` 24 | 25 | 1. Set the subscription you want to use: 26 | 27 | ```bash 28 | az account set --subscription 29 | ``` 30 | 31 | 1. Set the required parameters in `main.parameters.json` file 32 | 33 | 1. Deploy the template: 34 | 35 | ```bash 36 | az deployment sub --template-file main.bicep --location --name --parameters ./main.parameters.json 37 | ``` -------------------------------------------------------------------------------- /infra/abbreviations.json: -------------------------------------------------------------------------------- 1 | { 2 | "analysisServicesServers": "as", 3 | "apiManagementService": "apim-", 4 | "appConfigurationConfigurationStores": "appcs-", 5 | "appManagedEnvironments": "cae-", 6 | "appContainerApps": "ca-", 7 | "authorizationPolicyDefinitions": "policy-", 8 | "automationAutomationAccounts": "aa-", 9 | "blueprintBlueprints": "bp-", 10 | "blueprintBlueprintsArtifacts": "bpa-", 11 | "cacheRedis": "redis-", 12 | "cdnProfiles": "cdnp-", 13 | "cdnProfilesEndpoints": "cdne-", 14 | "cognitiveServicesAccounts": "cog-", 15 | "cognitiveServicesFormRecognizer": "cog-fr-", 16 | "cognitiveServicesTextAnalytics": "cog-ta-", 17 | "computeAvailabilitySets": "avail-", 18 | "computeCloudServices": "cld-", 19 | "computeDiskEncryptionSets": "des", 20 | "computeDisks": "disk", 21 | "computeDisksOs": "osdisk", 22 | "computeGalleries": "gal", 23 | "computeSnapshots": "snap-", 24 | "computeVirtualMachines": "vm", 25 | "computeVirtualMachineScaleSets": "vmss-", 26 | "containerInstanceContainerGroups": "ci", 27 | "containerRegistryRegistries": "cr", 28 | "containerServiceManagedClusters": "aks-", 29 | "databricksWorkspaces": "dbw-", 30 | "dataFactoryFactories": "adf-", 31 | "dataLakeAnalyticsAccounts": "dla", 32 | "dataLakeStoreAccounts": "dls", 33 | "dataMigrationServices": "dms-", 34 | "dBforMySQLServers": "mysql-", 35 | "dBforPostgreSQLServers": "psql-", 36 | "devicesIotHubs": "iot-", 37 | "devicesProvisioningServices": "provs-", 38 | "devicesProvisioningServicesCertificates": "pcert-", 39 | "documentDBDatabaseAccounts": "cosmos-", 40 | "eventGridDomains": "evgd-", 41 | "eventGridDomainsTopics": "evgt-", 42 | "eventGridEventSubscriptions": "evgs-", 43 | "eventHubNamespaces": "evhns-", 44 | "eventHubNamespacesEventHubs": "evh-", 45 | "hdInsightClustersHadoop": "hadoop-", 46 | "hdInsightClustersHbase": "hbase-", 47 | "hdInsightClustersKafka": "kafka-", 48 | "hdInsightClustersMl": "mls-", 49 | "hdInsightClustersSpark": "spark-", 50 | "hdInsightClustersStorm": "storm-", 51 | "hybridComputeMachines": "arcs-", 52 | "insightsActionGroups": "ag-", 53 | "insightsComponents": "appi-", 54 | "keyVaultVaults": "kv-", 55 | "kubernetesConnectedClusters": "arck", 56 | "kustoClusters": "dec", 57 | "kustoClustersDatabases": "dedb", 58 | "logicIntegrationAccounts": "ia-", 59 | "logicWorkflows": "logic-", 60 | "machineLearningServicesWorkspaces": "mlw-", 61 | "managedIdentityUserAssignedIdentities": "id-", 62 | "managementManagementGroups": "mg-", 63 | "migrateAssessmentProjects": "migr-", 64 | "networkApplicationGateways": "agw-", 65 | "networkApplicationSecurityGroups": "asg-", 66 | "networkAzureFirewalls": "afw-", 67 | "networkBastionHosts": "bas-", 68 | "networkConnections": "con-", 69 | "networkDnsZones": "dnsz-", 70 | "networkExpressRouteCircuits": "erc-", 71 | "networkFirewallPolicies": "afwp-", 72 | "networkFirewallPoliciesWebApplication": "waf", 73 | "networkFirewallPoliciesRuleGroups": "wafrg", 74 | "networkFrontDoors": "fd-", 75 | "networkFrontdoorWebApplicationFirewallPolicies": "fdfp-", 76 | "networkLoadBalancersExternal": "lbe-", 77 | "networkLoadBalancersInternal": "lbi-", 78 | "networkLoadBalancersInboundNatRules": "rule-", 79 | "networkLocalNetworkGateways": "lgw-", 80 | "networkNatGateways": "ng-", 81 | "networkNetworkInterfaces": "nic-", 82 | "networkNetworkSecurityGroups": "nsg-", 83 | "networkNetworkSecurityGroupsSecurityRules": "nsgsr-", 84 | "networkNetworkWatchers": "nw-", 85 | "networkPrivateDnsZones": "pdnsz-", 86 | "networkPrivateLinkServices": "pl-", 87 | "networkPublicIPAddresses": "pip-", 88 | "networkPublicIPPrefixes": "ippre-", 89 | "networkRouteFilters": "rf-", 90 | "networkRouteTables": "rt-", 91 | "networkRouteTablesRoutes": "udr-", 92 | "networkTrafficManagerProfiles": "traf-", 93 | "networkVirtualNetworkGateways": "vgw-", 94 | "networkVirtualNetworks": "vnet-", 95 | "networkVirtualNetworksSubnets": "snet-", 96 | "networkVirtualNetworksVirtualNetworkPeerings": "peer-", 97 | "networkVirtualWans": "vwan-", 98 | "networkVpnGateways": "vpng-", 99 | "networkVpnGatewaysVpnConnections": "vcn-", 100 | "networkVpnGatewaysVpnSites": "vst-", 101 | "notificationHubsNamespaces": "ntfns-", 102 | "notificationHubsNamespacesNotificationHubs": "ntf-", 103 | "operationalInsightsWorkspaces": "log-", 104 | "portalDashboards": "dash-", 105 | "powerBIDedicatedCapacities": "pbi-", 106 | "purviewAccounts": "pview-", 107 | "recoveryServicesVaults": "rsv-", 108 | "resourcesResourceGroups": "rg-", 109 | "searchSearchServices": "srch-", 110 | "serviceBusNamespaces": "sb-", 111 | "serviceBusNamespacesQueues": "sbq-", 112 | "serviceBusNamespacesTopics": "sbt-", 113 | "serviceEndPointPolicies": "se-", 114 | "serviceFabricClusters": "sf-", 115 | "signalRServiceSignalR": "sigr", 116 | "springApps": "spring-", 117 | "sqlManagedInstances": "sqlmi-", 118 | "sqlServers": "sql-", 119 | "sqlServersDataWarehouse": "sqldw-", 120 | "sqlServersDatabases": "sqldb-", 121 | "sqlServersDatabasesStretch": "sqlstrdb-", 122 | "storageStorageAccounts": "st", 123 | "storageStorageAccountsVm": "stvm", 124 | "storSimpleManagers": "ssimp", 125 | "streamAnalyticsCluster": "asa-", 126 | "synapseWorkspaces": "syn", 127 | "synapseWorkspacesAnalyticsWorkspaces": "synw", 128 | "synapseWorkspacesSqlPoolsDedicated": "syndp", 129 | "synapseWorkspacesSqlPoolsSpark": "synsp", 130 | "timeSeriesInsightsEnvironments": "tsi-", 131 | "webServerFarms": "plan-", 132 | "webSitesAppService": "app-", 133 | "webSitesAppServiceEnvironment": "ase-", 134 | "webSitesFunctions": "func-", 135 | "webStaticSites": "stapp-" 136 | } -------------------------------------------------------------------------------- /infra/app/ai-shopping-cart-service.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'resourceGroup' 2 | 3 | /* -------------------------------------------------------------------------- */ 4 | /* PARAMETERS */ 5 | /* -------------------------------------------------------------------------- */ 6 | 7 | @description('Name of the Spring Apps instance to deploy.') 8 | param name string 9 | 10 | @description('Location in which the resources will be deployed. Default value is the resource group location.') 11 | param location string = resourceGroup().location 12 | 13 | @description('Tags that will be added to all the resources. For Azure Developer CLI, "azd-env-name" should be added to the tags.') 14 | param tags object = {} 15 | 16 | // param applicationInsightsName string 17 | 18 | @description('Name of the existing Container Apps Environment in which the Spring Apps instance is deployed.') 19 | param containerAppsEnvironmentName string 20 | 21 | @description('Name of the Spring App. This name is used to add "azd-service-name" to the tags for the Spring Apps Instance. This is required for Azure Developer CLI to know which service to deploy. Default value is "ai-shopping-cart-service". If you change this value, make sure to change the name of the service in "azure.yaml" file as well.') 22 | param appName string = 'ai-shopping-cart-service' 23 | 24 | @description('Relative path to the AI shopping cart service JAR.') 25 | param relativePath string 26 | 27 | /* ------------------------------- PostgreSQL ------------------------------- */ 28 | 29 | @description('Name of the existing Postgres Flexible Server. This is the relational database used by the Spring App to save the state of the shopping cart.') 30 | param postgresFlexibleServerName string 31 | 32 | @description('Name of the Postgres database. Several databases can be created in the same Postgres Flexible Server. We need to know the one that is created for this microservice.') 33 | param postgresDatabaseName string 34 | 35 | @secure() 36 | @description('Password of the Postgres Flexible Server administrator. This is the password that was set when the Postgres Flexible Server was created.') 37 | param postgresAdminPassword string 38 | 39 | /* --------------------------------- OpenAI --------------------------------- */ 40 | 41 | @description('Azure Open AI API key.') 42 | param azureOpenAiApiKey string 43 | 44 | @description('Azure Open AI endpoint.') 45 | param azureOpenAiEndpoint string 46 | 47 | @description('Azure Open AI deployment ID.') 48 | param azureOpenAiDeploymentId string 49 | 50 | @description('Set if the model deployed is Azure Open AI "gpt-4" (true) or "gpt-35-turbo" (false).') 51 | param isAzureOpenAiGpt4Model bool 52 | 53 | /* -------------------------------------------------------------------------- */ 54 | /* VARIABLES */ 55 | /* -------------------------------------------------------------------------- */ 56 | 57 | @description('Spring Data Source URL. It is composed of the Postgres Flexible Server FQDN and the database name.') 58 | var springDatasourceUrl = 'jdbc:postgresql://${postgresFlexibleServer.properties.fullyQualifiedDomainName}:5432/${postgresDatabaseName}?sslmode=require' 59 | 60 | /* -------------------------------------------------------------------------- */ 61 | /* RESOURCES */ 62 | /* -------------------------------------------------------------------------- */ 63 | 64 | resource postgresFlexibleServer 'Microsoft.DBforPostgreSQL/flexibleServers@2023-03-01-preview' existing = { 65 | name: postgresFlexibleServerName 66 | } 67 | 68 | module springApps '../core/host/spring-apps-consumption.bicep' = { 69 | name: name 70 | params: { 71 | name: name 72 | location: location 73 | tags: union(tags, { 'azd-service-name': appName }) 74 | // applicationInsightsName: applicationInsightsName 75 | containerAppsEnvironmentName: containerAppsEnvironmentName 76 | appName: appName 77 | relativePath: relativePath 78 | environmentVariables: { 79 | SPRING_DATASOURCE_URL: springDatasourceUrl 80 | SPRING_DATASOURCE_USERNAME: postgresFlexibleServer.properties.administratorLogin 81 | SPRING_DATASOURCE_PASSWORD: postgresAdminPassword 82 | AZURE_OPENAI_API_KEY: azureOpenAiApiKey 83 | AZURE_OPENAI_ENDPOINT: azureOpenAiEndpoint 84 | AZURE_OPENAI_DEPLOYMENT_ID: azureOpenAiDeploymentId 85 | AZURE_OPENAI_IS_GPT4: isAzureOpenAiGpt4Model ? 'true' : 'false' 86 | } 87 | } 88 | } 89 | 90 | /* -------------------------------------------------------------------------- */ 91 | /* OUTPUTS */ 92 | /* -------------------------------------------------------------------------- */ 93 | 94 | @description('Name of AI Shopping Cart Service Spring App.') 95 | output SERVICE_AI_SHOPPING_CART_NAME string = springApps.outputs.springAppName 96 | 97 | @description('URI of AI Shopping Cart Service Spring App.') 98 | output SERVICE_AI_SHOPPING_CART_URI string = springApps.outputs.uri 99 | -------------------------------------------------------------------------------- /infra/app/frontend.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'resourceGroup' 2 | 3 | /* -------------------------------------------------------------------------- */ 4 | /* PARAMETERS */ 5 | /* -------------------------------------------------------------------------- */ 6 | 7 | @description('Name of the container app.') 8 | param name string 9 | 10 | @description('Location in which the resources will be deployed. Default value is the resource group location.') 11 | param location string = resourceGroup().location 12 | 13 | @description('Tags that will be added to all the resources. For Azure Developer CLI, "azd-env-name" should be added to the tags.') 14 | param tags object = {} 15 | 16 | @description('Name of the service. This name is used to add "azd-service-name" tag to the tags for the container app. Default value is "frontend". If you change this value, make sure to change the name of the service in "azure.yaml" file as well.') 17 | param serviceName string = 'frontend' 18 | 19 | @description('Name of the identity that will be created and used by the container app to pull image from the container registry.') 20 | param identityName string 21 | 22 | @description('URI of the AI Shopping Cart service that is used to managed the cart items.') 23 | param aiShoppingCartServiceUri string 24 | 25 | @description('Name of the existing Application Insights instance that will be used by the container app.') 26 | param applicationInsightsName string 27 | 28 | @description('Name of the existing container apps environment.') 29 | param containerAppsEnvironmentName string 30 | 31 | @description('Name of the existing container registry that will be used by the container app.') 32 | param containerRegistryName string 33 | 34 | @description('Flag that indicates whether the container app already exists or not. This is used in container app upsert to set the image name to the value of the existing container apps image name.') 35 | param exists bool 36 | 37 | /* -------------------------------------------------------------------------- */ 38 | /* RESOURCES */ 39 | /* -------------------------------------------------------------------------- */ 40 | 41 | resource frontendIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { 42 | name: identityName 43 | location: location 44 | } 45 | 46 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = { 47 | name: applicationInsightsName 48 | } 49 | 50 | module app '../core/host/container-app-upsert.bicep' = { 51 | name: '${serviceName}-container-app' 52 | params: { 53 | name: name 54 | location: location 55 | tags: union(tags, { 'azd-service-name': serviceName }) 56 | identityType: 'UserAssigned' 57 | identityName: identityName 58 | exists: exists 59 | containerAppsEnvironmentName: containerAppsEnvironmentName 60 | containerRegistryName: containerRegistryName 61 | env: [ 62 | { 63 | name: 'REACT_APP_APPLICATIONINSIGHTS_CONNECTION_STRING' 64 | value: applicationInsights.properties.ConnectionString 65 | } 66 | { 67 | name: 'REACT_APP_API_URL' 68 | value: aiShoppingCartServiceUri 69 | } 70 | { 71 | name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' 72 | value: applicationInsights.properties.ConnectionString 73 | } 74 | ] 75 | targetPort: 80 76 | } 77 | } 78 | 79 | /* -------------------------------------------------------------------------- */ 80 | /* OUTPUTS */ 81 | /* -------------------------------------------------------------------------- */ 82 | 83 | @description('ID of the service principal that is used by the container app to pull image from the container registry.') 84 | output SERVICE_FRONTEND_IDENTITY_PRINCIPAL_ID string = frontendIdentity.properties.principalId 85 | 86 | @description('Name of the container app.') 87 | output SERVICE_FRONTEND_NAME string = app.outputs.name 88 | 89 | @description('URI of the container app.') 90 | output SERVICE_FRONTEND_URI string = app.outputs.uri 91 | 92 | @description('Name of the container apps image.') 93 | output SERVICE_FRONTEND_IMAGE_NAME string = app.outputs.imageName 94 | -------------------------------------------------------------------------------- /infra/core/database/postgresql/flexible-server.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'resourceGroup' 2 | 3 | param name string 4 | param location string = resourceGroup().location 5 | param tags object = {} 6 | 7 | param postgresVersion string 8 | param sku object 9 | param storage object 10 | param administratorLogin string 11 | @secure() 12 | param administratorLoginPassword string 13 | param databaseNames array = [] 14 | param allowAzureIPsFirewall bool = false 15 | param allowAllIPsFirewall bool = false 16 | param allowedSingleIPs array = [] 17 | 18 | resource postgresFlexibleServer 'Microsoft.DBforPostgreSQL/flexibleServers@2023-03-01-preview' = { 19 | location: location 20 | tags: tags 21 | name: name 22 | sku: sku 23 | properties: { 24 | version: postgresVersion 25 | administratorLogin: administratorLogin 26 | administratorLoginPassword: administratorLoginPassword 27 | storage: storage 28 | authConfig: { 29 | activeDirectoryAuth: 'Disabled' 30 | passwordAuth: 'Enabled' 31 | } 32 | highAvailability: { 33 | mode: 'Disabled' 34 | } 35 | } 36 | 37 | resource database 'databases' = [for name in databaseNames: { 38 | name: name 39 | properties: { 40 | charset: 'UTF8' 41 | collation: 'en_US.utf8' 42 | } 43 | }] 44 | 45 | resource firewall_all 'firewallRules' = if (allowAllIPsFirewall) { 46 | name: 'allow-all-IPs' 47 | properties: { 48 | startIpAddress: '0.0.0.0' 49 | endIpAddress: '255.255.255.255' 50 | } 51 | } 52 | 53 | resource firewall_azure 'firewallRules' = if (allowAzureIPsFirewall) { 54 | name: 'allow-all-azure-internal-IPs' 55 | properties: { 56 | startIpAddress: '0.0.0.0' 57 | endIpAddress: '0.0.0.0' 58 | } 59 | } 60 | 61 | resource firewall_single 'firewallRules' = [for ip in allowedSingleIPs: { 62 | name: 'allow-single-${replace(ip, '.', '')}' 63 | properties: { 64 | startIpAddress: ip 65 | endIpAddress: ip 66 | } 67 | }] 68 | 69 | } 70 | 71 | output name string = postgresFlexibleServer.name 72 | output POSTGRES_DOMAIN_NAME string = postgresFlexibleServer.properties.fullyQualifiedDomainName 73 | -------------------------------------------------------------------------------- /infra/core/host/container-app-upsert.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'resourceGroup' 2 | 3 | param name string 4 | param location string = resourceGroup().location 5 | param tags object = {} 6 | 7 | @description('The environment name for the container apps') 8 | param containerAppsEnvironmentName string 9 | 10 | @description('The number of CPU cores allocated to a single container instance, e.g., 0.5') 11 | param containerCpuCoreCount string = '0.5' 12 | 13 | @description('The maximum number of replicas to run. Must be at least 1.') 14 | @minValue(1) 15 | param containerMaxReplicas int = 10 16 | 17 | @description('The amount of memory allocated to a single container instance, e.g., 1Gi') 18 | param containerMemory string = '1.0Gi' 19 | 20 | @description('The minimum number of replicas to run. Must be at least 1.') 21 | @minValue(1) 22 | param containerMinReplicas int = 1 23 | 24 | @description('The name of the container') 25 | param containerName string = 'main' 26 | 27 | @description('The name of the container registry') 28 | param containerRegistryName string = '' 29 | 30 | @allowed([ 'http', 'grpc' ]) 31 | @description('The protocol used by Dapr to connect to the app, e.g., HTTP or gRPC') 32 | param daprAppProtocol string = 'http' 33 | 34 | @description('Enable or disable Dapr for the container app') 35 | param daprEnabled bool = false 36 | 37 | @description('The Dapr app ID') 38 | param daprAppId string = containerName 39 | 40 | @description('Specifies if the resource already exists') 41 | param exists bool = false 42 | 43 | @description('Specifies if Ingress is enabled for the container app') 44 | param ingressEnabled bool = true 45 | 46 | @description('The type of identity for the resource') 47 | @allowed([ 'None', 'SystemAssigned', 'UserAssigned' ]) 48 | param identityType string = 'None' 49 | 50 | @description('The name of the user-assigned identity') 51 | param identityName string = '' 52 | 53 | @description('The name of the container image') 54 | param imageName string = '' 55 | 56 | @description('The secrets required for the container') 57 | param secrets array = [] 58 | 59 | @description('The environment variables for the container') 60 | param env array = [] 61 | 62 | @description('Specifies if the resource ingress is exposed externally') 63 | param external bool = true 64 | 65 | @description('The service binds associated with the container') 66 | param serviceBinds array = [] 67 | 68 | @description('The target port for the container') 69 | param targetPort int = 80 70 | 71 | resource existingApp 'Microsoft.App/containerApps@2023-04-01-preview' existing = if (exists) { 72 | name: name 73 | } 74 | 75 | module app 'container-app.bicep' = { 76 | name: '${deployment().name}-update' 77 | params: { 78 | name: name 79 | location: location 80 | tags: tags 81 | identityType: identityType 82 | identityName: identityName 83 | ingressEnabled: ingressEnabled 84 | containerName: containerName 85 | containerAppsEnvironmentName: containerAppsEnvironmentName 86 | containerRegistryName: containerRegistryName 87 | containerCpuCoreCount: containerCpuCoreCount 88 | containerMemory: containerMemory 89 | containerMinReplicas: containerMinReplicas 90 | containerMaxReplicas: containerMaxReplicas 91 | daprEnabled: daprEnabled 92 | daprAppId: daprAppId 93 | daprAppProtocol: daprAppProtocol 94 | secrets: secrets 95 | external: external 96 | env: env 97 | imageName: !empty(imageName) ? imageName : exists ? existingApp.properties.template.containers[0].image : '' 98 | targetPort: targetPort 99 | serviceBinds: serviceBinds 100 | } 101 | } 102 | 103 | output defaultDomain string = app.outputs.defaultDomain 104 | output imageName string = app.outputs.imageName 105 | output name string = app.outputs.name 106 | output uri string = app.outputs.uri 107 | -------------------------------------------------------------------------------- /infra/core/host/container-app.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'resourceGroup' 2 | 3 | param name string 4 | param location string = resourceGroup().location 5 | param tags object = {} 6 | 7 | @description('Allowed origins') 8 | param allowedOrigins array = [] 9 | 10 | @description('Name of the environment for container apps') 11 | param containerAppsEnvironmentName string 12 | 13 | @description('CPU cores allocated to a single container instance, e.g., 0.5') 14 | param containerCpuCoreCount string = '0.5' 15 | 16 | @description('The maximum number of replicas to run. Must be at least 1.') 17 | @minValue(1) 18 | param containerMaxReplicas int = 10 19 | 20 | @description('Memory allocated to a single container instance, e.g., 1Gi') 21 | param containerMemory string = '1.0Gi' 22 | 23 | @description('The minimum number of replicas to run. Must be at least 1.') 24 | param containerMinReplicas int = 1 25 | 26 | @description('The name of the container') 27 | param containerName string = 'main' 28 | 29 | @description('The name of the container registry') 30 | param containerRegistryName string = '' 31 | 32 | @description('The protocol used by Dapr to connect to the app, e.g., http or grpc') 33 | @allowed([ 'http', 'grpc' ]) 34 | param daprAppProtocol string = 'http' 35 | 36 | @description('The Dapr app ID') 37 | param daprAppId string = containerName 38 | 39 | @description('Enable Dapr') 40 | param daprEnabled bool = false 41 | 42 | @description('The environment variables for the container') 43 | param env array = [] 44 | 45 | @description('Specifies if the resource ingress is exposed externally') 46 | param external bool = true 47 | 48 | @description('The name of the user-assigned identity') 49 | param identityName string = '' 50 | 51 | @description('The type of identity for the resource') 52 | @allowed([ 'None', 'SystemAssigned', 'UserAssigned' ]) 53 | param identityType string = 'None' 54 | 55 | @description('The name of the container image') 56 | param imageName string = '' 57 | 58 | @description('Specifies if Ingress is enabled for the container app') 59 | param ingressEnabled bool = true 60 | 61 | param revisionMode string = 'Single' 62 | 63 | @description('The secrets required for the container') 64 | param secrets array = [] 65 | 66 | @description('The service binds associated with the container') 67 | param serviceBinds array = [] 68 | 69 | @description('The name of the container apps add-on to use. e.g. redis') 70 | param serviceType string = '' 71 | 72 | @description('The target port for the container') 73 | param targetPort int = 80 74 | 75 | resource userIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = if (!empty(identityName)) { 76 | name: identityName 77 | } 78 | 79 | // Private registry support requires both an ACR name and a User Assigned managed identity 80 | var usePrivateRegistry = !empty(identityName) && !empty(containerRegistryName) 81 | 82 | // Automatically set to `UserAssigned` when an `identityName` has been set 83 | var normalizedIdentityType = !empty(identityName) ? 'UserAssigned' : identityType 84 | 85 | module containerRegistryAccess '../security/registry-access.bicep' = if (usePrivateRegistry) { 86 | name: '${deployment().name}-registry-access' 87 | params: { 88 | containerRegistryName: containerRegistryName 89 | principalId: usePrivateRegistry ? userIdentity.properties.principalId : '' 90 | } 91 | } 92 | 93 | resource app 'Microsoft.App/containerApps@2023-04-01-preview' = { 94 | name: name 95 | location: location 96 | tags: tags 97 | // It is critical that the identity is granted ACR pull access before the app is created 98 | // otherwise the container app will throw a provision error 99 | // This also forces us to use an user assigned managed identity since there would no way to 100 | // provide the system assigned identity with the ACR pull access before the app is created 101 | dependsOn: usePrivateRegistry ? [ containerRegistryAccess ] : [] 102 | identity: { 103 | type: normalizedIdentityType 104 | userAssignedIdentities: !empty(identityName) && normalizedIdentityType == 'UserAssigned' ? { '${userIdentity.id}': {} } : null 105 | } 106 | properties: { 107 | managedEnvironmentId: containerAppsEnvironment.id 108 | configuration: { 109 | activeRevisionsMode: revisionMode 110 | ingress: ingressEnabled ? { 111 | external: external 112 | targetPort: targetPort 113 | transport: 'auto' 114 | corsPolicy: { 115 | allowedOrigins: union([ 'https://portal.azure.com', 'https://ms.portal.azure.com' ], allowedOrigins) 116 | } 117 | } : null 118 | dapr: daprEnabled ? { 119 | enabled: true 120 | appId: daprAppId 121 | appProtocol: daprAppProtocol 122 | appPort: ingressEnabled ? targetPort : 0 123 | } : { enabled: false } 124 | secrets: secrets 125 | service: !empty(serviceType) ? { type: serviceType } : null 126 | registries: usePrivateRegistry ? [ 127 | { 128 | server: '${containerRegistryName}.azurecr.io' 129 | identity: userIdentity.id 130 | } 131 | ] : [] 132 | } 133 | template: { 134 | serviceBinds: !empty(serviceBinds) ? serviceBinds : null 135 | containers: [ 136 | { 137 | image: !empty(imageName) ? imageName : 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' 138 | name: containerName 139 | env: env 140 | resources: { 141 | cpu: json(containerCpuCoreCount) 142 | memory: containerMemory 143 | } 144 | } 145 | ] 146 | scale: { 147 | minReplicas: containerMinReplicas 148 | maxReplicas: containerMaxReplicas 149 | } 150 | } 151 | } 152 | } 153 | 154 | resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-04-01-preview' existing = { 155 | name: containerAppsEnvironmentName 156 | } 157 | 158 | output defaultDomain string = containerAppsEnvironment.properties.defaultDomain 159 | output identityPrincipalId string = normalizedIdentityType == 'None' ? '' : (empty(identityName) ? app.identity.principalId : userIdentity.properties.principalId) 160 | output imageName string = imageName 161 | output name string = app.name 162 | output serviceBind object = !empty(serviceType) ? { serviceId: app.id, name: name } : {} 163 | output uri string = ingressEnabled ? 'https://${app.properties.configuration.ingress.fqdn}' : '' 164 | -------------------------------------------------------------------------------- /infra/core/host/container-apps-environment.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'resourceGroup' 2 | 3 | /* -------------------------------------------------------------------------- */ 4 | /* PARAMETERS */ 5 | /* -------------------------------------------------------------------------- */ 6 | 7 | @description('Name of the container apps environment.') 8 | param name string 9 | 10 | @description('Location in which the container apps environment will be deployed. Default value is the resource group location.') 11 | param location string = resourceGroup().location 12 | 13 | @description('Tags to associate with the container apps environment. Defaults to an empty object ({}).') 14 | param tags object = {} 15 | 16 | @description('Name of the existing application insights instance to use for the container apps environment. If the name is not provided, Dapr telemetry will be disabled even if "enableDaprTelemetry" is set to true.') 17 | param applicationInsightsName string = '' 18 | 19 | @description('Set if Dapr telemetry is enabled.') 20 | param enableDaprTelemetry bool = false 21 | 22 | @description('Name of the existing log analytic workspace to use for the container apps environment.') 23 | param logAnalyticsWorkspaceName string 24 | 25 | /* -------------------------------------------------------------------------- */ 26 | /* RESOURCES */ 27 | /* -------------------------------------------------------------------------- */ 28 | 29 | resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' existing = { 30 | name: logAnalyticsWorkspaceName 31 | } 32 | 33 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (enableDaprTelemetry && !empty(applicationInsightsName)) { 34 | name: applicationInsightsName 35 | } 36 | 37 | resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-04-01-preview' = { 38 | name: name 39 | location: location 40 | tags: tags 41 | properties: { 42 | vnetConfiguration: { 43 | internal: false 44 | } 45 | zoneRedundant: false 46 | workloadProfiles: [ 47 | { 48 | workloadProfileType: 'Consumption' 49 | name: 'Consumption' 50 | } 51 | ] 52 | appLogsConfiguration: { 53 | destination: 'log-analytics' 54 | logAnalyticsConfiguration: { 55 | customerId: logAnalyticsWorkspace.properties.customerId 56 | sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey 57 | } 58 | } 59 | daprAIInstrumentationKey: enableDaprTelemetry && !empty(applicationInsightsName) ? applicationInsights.properties.InstrumentationKey : '' 60 | } 61 | } 62 | 63 | /* -------------------------------------------------------------------------- */ 64 | /* OUTPUTS */ 65 | /* -------------------------------------------------------------------------- */ 66 | 67 | @description('The name of the container apps environment.') 68 | output name string = containerAppsEnvironment.name 69 | -------------------------------------------------------------------------------- /infra/core/host/container-registry.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'resourceGroup' 2 | 3 | param name string 4 | param location string = resourceGroup().location 5 | param tags object = {} 6 | 7 | @description('Indicates whether admin user is enabled') 8 | param adminUserEnabled bool = false 9 | 10 | @description('Indicates whether anonymous pull is enabled') 11 | param anonymousPullEnabled bool = false 12 | 13 | @description('Indicates whether data endpoint is enabled') 14 | param dataEndpointEnabled bool = false 15 | 16 | @description('Encryption settings') 17 | param encryption object = { 18 | status: 'disabled' 19 | } 20 | 21 | @description('Options for bypassing network rules') 22 | param networkRuleBypassOptions string = 'AzureServices' 23 | 24 | @description('Public network access setting') 25 | param publicNetworkAccess string = 'Enabled' 26 | 27 | @description('SKU settings') 28 | param sku object = { 29 | name: 'Basic' 30 | } 31 | 32 | @description('Zone redundancy setting') 33 | param zoneRedundancy string = 'Disabled' 34 | 35 | @description('The log analytics workspace ID used for logging and monitoring') 36 | param workspaceId string = '' 37 | 38 | // 2022-02-01-preview needed for anonymousPullEnabled 39 | resource containerRegistry 'Microsoft.ContainerRegistry/registries@2022-02-01-preview' = { 40 | name: name 41 | location: location 42 | tags: tags 43 | sku: sku 44 | properties: { 45 | adminUserEnabled: adminUserEnabled 46 | anonymousPullEnabled: anonymousPullEnabled 47 | dataEndpointEnabled: dataEndpointEnabled 48 | encryption: encryption 49 | networkRuleBypassOptions: networkRuleBypassOptions 50 | publicNetworkAccess: publicNetworkAccess 51 | zoneRedundancy: zoneRedundancy 52 | } 53 | } 54 | 55 | // TODO: Update diagnostics to be its own module 56 | // Blocking issue: https://github.com/Azure/bicep/issues/622 57 | // Unable to pass in a `resource` scope or unable to use string interpolation in resource types 58 | resource diagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(workspaceId)) { 59 | name: 'registry-diagnostics' 60 | scope: containerRegistry 61 | properties: { 62 | workspaceId: workspaceId 63 | logs: [ 64 | { 65 | category: 'ContainerRegistryRepositoryEvents' 66 | enabled: true 67 | } 68 | { 69 | category: 'ContainerRegistryLoginEvents' 70 | enabled: true 71 | } 72 | ] 73 | metrics: [ 74 | { 75 | category: 'AllMetrics' 76 | enabled: true 77 | timeGrain: 'PT1M' 78 | } 79 | ] 80 | } 81 | } 82 | 83 | output loginServer string = containerRegistry.properties.loginServer 84 | output name string = containerRegistry.name 85 | -------------------------------------------------------------------------------- /infra/core/host/spring-apps-consumption.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'resourceGroup' 2 | 3 | /* -------------------------------------------------------------------------- */ 4 | /* PARAMETERS */ 5 | /* -------------------------------------------------------------------------- */ 6 | 7 | @description('Name of the Spring Apps instance to deploy.') 8 | param name string 9 | 10 | @description('Location in which the resources will be deployed. Default value is the resource group location.') 11 | param location string = resourceGroup().location 12 | 13 | @description('Tags to associate with the Spring Apps instance.') 14 | param tags object = {} 15 | 16 | // @description('Name of the existing Application Insights instance to use for this Spring Apps platform to deploy.') 17 | // param applicationInsightsName string 18 | 19 | @description('Name of the existing container apps environment to use for this Spring Apps platform to deploy.') 20 | param containerAppsEnvironmentName string 21 | 22 | @minLength(4) 23 | @maxLength(32) 24 | @description('Name of the Spring App to deploy.') 25 | param appName string 26 | 27 | @description('Relative path to the JAR file to deploy.') 28 | param relativePath string 29 | 30 | @description('Environment variables to set for the Spring App. Default value is an empty object.') 31 | param environmentVariables object = {} 32 | 33 | /* -------------------------------------------------------------------------- */ 34 | /* RESOURCES */ 35 | /* -------------------------------------------------------------------------- */ 36 | 37 | resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-04-01-preview' existing = { 38 | name: containerAppsEnvironmentName 39 | } 40 | 41 | // resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = { 42 | // name: applicationInsightsName 43 | // } 44 | 45 | resource springApps 'Microsoft.AppPlatform/Spring@2023-05-01-preview' = { 46 | name: name 47 | location: location 48 | tags: tags 49 | sku: { 50 | name: 'S0' 51 | tier: 'Standard' 52 | } 53 | properties: { 54 | managedEnvironmentId: containerAppsEnvironment.id 55 | zoneRedundant: false 56 | } 57 | } 58 | 59 | resource springApp 'Microsoft.AppPlatform/Spring/apps@2023-05-01-preview' = { 60 | name: appName 61 | location: location 62 | parent: springApps 63 | properties: { 64 | public: true 65 | workloadProfileName: 'Consumption' 66 | } 67 | } 68 | 69 | resource springAppDeployment 'Microsoft.AppPlatform/Spring/apps/deployments@2023-05-01-preview' = { 70 | name: 'default' 71 | parent: springApp 72 | properties: { 73 | source: { 74 | type: 'Jar' 75 | relativePath: relativePath 76 | runtimeVersion: 'Java_17' 77 | jvmOptions: '-Xms1024m -Xmx2048m' 78 | } 79 | deploymentSettings: { 80 | resourceRequests: { 81 | cpu: '1' 82 | memory: '2Gi' 83 | } 84 | scale: { 85 | minReplicas: 1 86 | maxReplicas: 1 87 | } 88 | environmentVariables: environmentVariables 89 | } 90 | } 91 | } 92 | 93 | // resource springAppsMonitoringSettings 'Microsoft.AppPlatform/Spring/monitoringSettings@2023-05-01-preview' = { 94 | // name: 'default' 95 | // parent: springApps 96 | // properties: { 97 | // appInsightsInstrumentationKey: applicationInsights.properties.InstrumentationKey 98 | // appInsightsSamplingRate: 88 99 | // } 100 | // } 101 | 102 | /* -------------------------------------------------------------------------- */ 103 | /* OUTPUTS */ 104 | /* -------------------------------------------------------------------------- */ 105 | 106 | @description('Name of the Spring Apps instance.') 107 | output springAppsInstanceName string = springApps.name 108 | 109 | @description('Name of the Spring App.') 110 | output springAppName string = springApp.name 111 | 112 | @description('URI of the Spring App.') 113 | output uri string = springApp.properties.url 114 | -------------------------------------------------------------------------------- /infra/core/monitor/application-insights.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'resourceGroup' 2 | 3 | param name string 4 | param dashboardName string 5 | param location string = resourceGroup().location 6 | param tags object = {} 7 | param includeDashboard bool = true 8 | param logAnalyticsWorkspaceId string 9 | 10 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { 11 | name: name 12 | location: location 13 | tags: tags 14 | kind: 'web' 15 | properties: { 16 | Application_Type: 'web' 17 | WorkspaceResourceId: logAnalyticsWorkspaceId 18 | } 19 | } 20 | 21 | module applicationInsightsDashboard 'application-insights-dashboard.bicep' = if (includeDashboard) { 22 | name: 'application-insights-dashboard' 23 | params: { 24 | name: dashboardName 25 | location: location 26 | applicationInsightsName: applicationInsights.name 27 | } 28 | } 29 | 30 | output connectionString string = applicationInsights.properties.ConnectionString 31 | output instrumentationKey string = applicationInsights.properties.InstrumentationKey 32 | output name string = applicationInsights.name 33 | -------------------------------------------------------------------------------- /infra/core/monitor/log-analytics-workspace.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'resourceGroup' 2 | 3 | param name string 4 | param location string = resourceGroup().location 5 | param tags object = {} 6 | 7 | resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = { 8 | name: name 9 | location: location 10 | tags: tags 11 | properties: any({ 12 | retentionInDays: 30 13 | features: { 14 | searchVersion: 1 15 | } 16 | sku: { 17 | name: 'PerGB2018' 18 | } 19 | }) 20 | } 21 | 22 | output id string = logAnalyticsWorkspace.id 23 | output name string = logAnalyticsWorkspace.name 24 | -------------------------------------------------------------------------------- /infra/core/monitor/monitoring.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'resourceGroup' 2 | 3 | param logAnalyticsWorkspaceName string 4 | param applicationInsightsName string 5 | param applicationInsightsDashboardName string 6 | param location string = resourceGroup().location 7 | param tags object = {} 8 | param includeDashboard bool = true 9 | 10 | // TODO update deployment names 11 | 12 | module logAnalyticsWorkspace 'log-analytics-workspace.bicep' = { 13 | name: 'logAnalyticsWorkspace' 14 | params: { 15 | name: logAnalyticsWorkspaceName 16 | location: location 17 | tags: tags 18 | } 19 | } 20 | 21 | module applicationInsights 'application-insights.bicep' = { 22 | name: 'applicationinsights' 23 | params: { 24 | name: applicationInsightsName 25 | location: location 26 | tags: tags 27 | dashboardName: applicationInsightsDashboardName 28 | includeDashboard: includeDashboard 29 | logAnalyticsWorkspaceId: logAnalyticsWorkspace.outputs.id 30 | } 31 | } 32 | 33 | output applicationInsightsConnectionString string = applicationInsights.outputs.connectionString 34 | output applicationInsightsInstrumentationKey string = applicationInsights.outputs.instrumentationKey 35 | output applicationInsightsName string = applicationInsights.outputs.name 36 | output logAnalyticsWorkspaceId string = logAnalyticsWorkspace.outputs.id 37 | output logAnalyticsWorkspaceName string = logAnalyticsWorkspace.outputs.name 38 | -------------------------------------------------------------------------------- /infra/core/security/registry-access.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'resourceGroup' 2 | 3 | param containerRegistryName string 4 | param principalId string 5 | 6 | var acrPullRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') 7 | 8 | resource acrPullAssignmentRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 9 | scope: containerRegistry // Use when specifying a scope that is different than the deployment scope 10 | name: guid(subscription().id, resourceGroup().id, principalId, acrPullRole) 11 | properties: { 12 | roleDefinitionId: acrPullRole 13 | principalType: 'ServicePrincipal' 14 | principalId: principalId 15 | } 16 | } 17 | 18 | resource containerRegistry 'Microsoft.ContainerRegistry/registries@2022-02-01-preview' existing = { 19 | name: containerRegistryName 20 | } 21 | -------------------------------------------------------------------------------- /infra/main.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'subscription' 2 | 3 | /* -------------------------------------------------------------------------- */ 4 | /* PARAMETERS */ 5 | /* -------------------------------------------------------------------------- */ 6 | 7 | @minLength(1) 8 | @maxLength(64) 9 | @description('Name of the the environment which is used to generate a name for all resources. Use only alphanumerics and hyphens. It cannot starts with a hyphen. For resource that requires globally unique name, a token is generated based on this name and the ide of the subscription.') 10 | param environmentName string 11 | 12 | @minLength(1) 13 | @description('Primary location for all resources') 14 | param location string 15 | 16 | /* ----------------------------- Resource Names ----------------------------- */ 17 | 18 | @maxLength(90) 19 | @description('Name of the resource group to deploy. If not specified, a name will be generated.') 20 | param resourceGroupName string = '' 21 | 22 | @maxLength(60) 23 | @description('Name of the container apps environment to deploy. If not specified, a name will be generated. The maximum length is 60 characters.') 24 | param containerAppsEnvironmentName string = '' 25 | 26 | @maxLength(50) 27 | @description('Name of the container registry to deploy. If not specified, a name will be generated. The name is global and must be unique within Azure. The maximum length is 50 characters.') 28 | param containerRegistryName string = '' 29 | 30 | @maxLength(63) 31 | @description('Name of the log analytics workspace to deploy. If not specified, a name will be generated. The maximum length is 63 characters.') 32 | param logAnalyticsWorkspaceName string = '' 33 | 34 | @maxLength(255) 35 | @description('Name of the application insights to deploy. If not specified, a name will be generated. The maximum length is 255 characters.') 36 | param applicationInsightsName string = '' 37 | 38 | @maxLength(160) 39 | @description('Name of the application insights dashboard to deploy. If not specified, a name will be generated. The maximum length is 160 characters.') 40 | param applicationInsightsDashboardName string = '' 41 | 42 | @maxLength(32) 43 | @description('Name of the spring apps instance to deploy the AI shopping cart service. If not specified, a name will be generated. The name is global and must be unique within Azure. The maximum length is 32 characters. It contains only lowercase letters, numbers and hyphens.') 44 | param springAppsInstanceName string = '' 45 | 46 | @maxLength(63) 47 | @description('Name of the PostgreSQL flexible server to deploy. If not specified, a name will be generated. The name is global and must be unique within Azure. The maximum length is 63 characters. It contains only lowercase letters, numbers and hyphens, and cannot start nor end with a hyphen.') 48 | param postgresFlexibleServerName string = '' 49 | 50 | /* ------------------------------- PostgreSQL ------------------------------- */ 51 | 52 | @description('Name of the PostgreSQL admin user.') 53 | param postgresAdminUsername string = 'shoppingcartadmin' 54 | 55 | @secure() 56 | @description('Password for the PostgreSQL admin user. If not specified, a password will be generated.') 57 | param postgresAdminPassword string = newGuid() 58 | 59 | /* -------------------------------- Front-end ------------------------------- */ 60 | 61 | @maxLength(32) 62 | @description('Name of the frontend container app to deploy. If not specified, a name will be generated. The maximum length is 32 characters.') 63 | param frontendContainerAppName string = '' 64 | 65 | @description('Set if the frontend container app already exists.') 66 | param frontendAppExists bool = false 67 | 68 | /* ------------------------ AI Shopping Cart Service ------------------------ */ 69 | 70 | @description('Relative path to the AI shopping cart service JAR.') 71 | param aiShoppingCartServiceRelativePath string 72 | 73 | @description('Azure Open AI API key. This is required. Azure OpenAI is not deployed with this template.') 74 | param azureOpenAiApiKey string 75 | 76 | @description('Azure Open AI endpoint. This is required. Azure OpenAI is not deployed with this template.') 77 | param azureOpenAiEndpoint string 78 | 79 | @description('Azure Open AI deployment id. This is required. Azure OpenAI is not deployed with this template.') 80 | param azureOpenAiDeploymentId string 81 | 82 | @description('Set if the model deployed is Azure Open AI "gpt-4" (true) or "gpt-35-turbo" (false). Default is "gpt-4".') 83 | param isAzureOpenAiGpt4Model bool = true 84 | 85 | /* -------------------------------- Telemetry ------------------------------- */ 86 | 87 | @description('Enable usage and telemetry feedback to Microsoft.') 88 | param enableTelemetry bool = true 89 | 90 | 91 | /* -------------------------------------------------------------------------- */ 92 | /* VARIABLES */ 93 | /* -------------------------------------------------------------------------- */ 94 | 95 | @description('Abbreviations prefix for resources.') 96 | var abbreviations = loadJsonContent('./abbreviations.json') 97 | 98 | @description('Unique token used for global resource names. Unique string returns a 13 characters long string.') 99 | // See: https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/bicep-functions-string#remarks-4 100 | var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) 101 | 102 | @description('Name of the environment with only alphanumeric characters. Used for resource names that require alphanumeric characters only.') 103 | var alphaNumericEnvironmentName = replace(replace(environmentName, '-', ''), ' ', '') 104 | 105 | // tags that should be applied to all resources. 106 | var tags = { 107 | // Tag all resources with the environment name. 108 | 'azd-env-name': environmentName 109 | } 110 | 111 | // Telemetry Deployment 112 | @description('Enable usage and telemetry feedback to Microsoft.') 113 | var telemetryId = '11d2e1bb-4e66-4a54-9d49-df3778d0e9a1-asaopenai-${location}' 114 | 115 | /* ----------------------------- Resource Names ----------------------------- */ 116 | 117 | var _resourceGroupName = !empty(resourceGroupName) ? resourceGroupName : take('${abbreviations.resourcesResourceGroups}${environmentName}', 90) 118 | var _containerAppsEnvironmentName = !empty(containerAppsEnvironmentName) ? containerAppsEnvironmentName : take('${abbreviations.appManagedEnvironments}${environmentName}', 60) 119 | var _logAnalyticsWorkspaceName = !empty(logAnalyticsWorkspaceName) ? logAnalyticsWorkspaceName : take('${abbreviations.operationalInsightsWorkspaces}${environmentName}', 63) 120 | var _applicationInsightsName = !empty(applicationInsightsName) ? applicationInsightsName : take('${abbreviations.insightsComponents}${environmentName}', 255) 121 | var _applicationInsightsDashboardName = !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : take('${abbreviations.portalDashboards}${environmentName}', 160) 122 | var _frontendContainerAppName = !empty(frontendContainerAppName) ? frontendContainerAppName : take('${abbreviations.appContainerApps}frontend-${environmentName}', 32) 123 | 124 | /* --------------------- Globally Unique Resource Names --------------------- */ 125 | 126 | // 'cr' is 2 characters long, 'resourceToken' is 13 characters long, so, as the maximum length is 50 characters, the environment name can be maximum 35 characters long. 127 | // The 'take(..., 50)' function is used to ensure the name is not longer than 50 characters, even if it is not necessary. 128 | // The name can contains only alpha numeric characters and no hyphens. This is why 'alphaNumericEnvironmentName' is used instead of 'environmentName'. 129 | var _containerRegistryName = !empty(containerRegistryName) ? containerRegistryName : take('${abbreviations.containerRegistryRegistries}${take(alphaNumericEnvironmentName, 35)}${resourceToken}', 50) 130 | 131 | // 'spring-' is 7 characters long, 'resourceToken' is 13 characters long, there is one hyphen, so, as the maximum length is 32 characters, the environment name can be maximum 11 characters long. 132 | // The 'take(..., 32)' function is used to ensure the name is not longer than 32 characters, even if it is not necessary. 133 | // The name needs to be lower case, so it is converted to lower case. 134 | var _springAppsInstanceName = !empty(springAppsInstanceName) ? springAppsInstanceName : take(toLower('${abbreviations.springApps}${take(environmentName, 11)}-${resourceToken}'), 32) 135 | 136 | // 'psql-' is 5 characters long, 'resourceToken' is 13 characters long, there is one hyphen, so, as the maximum length is 63 characters, the environment name can be maximum 44 characters long. 137 | // The 'take(..., 63)' function is used to ensure the name is not longer than 63 characters, even if it is not necessary. 138 | // The name needs to be lower case, so it is converted to lower case. 139 | var _postgresFlexibleServerName = !empty(postgresFlexibleServerName) ? postgresFlexibleServerName : take(toLower('${abbreviations.dBforPostgreSQLServers}${take(environmentName, 44)}-${resourceToken}'), 63) 140 | 141 | /* -------------------------------------------------------------------------- */ 142 | /* RESOURCES */ 143 | /* -------------------------------------------------------------------------- */ 144 | 145 | // Organize resources in a resource group 146 | resource resourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = { 147 | name: _resourceGroupName 148 | location: location 149 | tags: tags 150 | } 151 | 152 | module containerAppsEnvironment 'core/host/container-apps-environment.bicep' = { 153 | name: _containerAppsEnvironmentName 154 | scope: resourceGroup 155 | params: { 156 | name: _containerAppsEnvironmentName 157 | location: location 158 | tags: tags 159 | logAnalyticsWorkspaceName: monitoring.outputs.logAnalyticsWorkspaceName 160 | applicationInsightsName: monitoring.outputs.applicationInsightsName 161 | } 162 | } 163 | 164 | module containerRegistry 'core/host/container-registry.bicep' = { 165 | name: _containerRegistryName 166 | scope: resourceGroup 167 | params: { 168 | name: _containerRegistryName 169 | location: location 170 | tags: tags 171 | } 172 | } 173 | 174 | module monitoring 'core/monitor/monitoring.bicep' = { 175 | name: 'monitoring' 176 | scope: resourceGroup 177 | params: { 178 | location: location 179 | tags: tags 180 | logAnalyticsWorkspaceName: _logAnalyticsWorkspaceName 181 | applicationInsightsName: _applicationInsightsName 182 | applicationInsightsDashboardName: _applicationInsightsDashboardName 183 | } 184 | } 185 | 186 | module postgresFlexibleServer 'core/database/postgresql/flexible-server.bicep' = { 187 | name: _postgresFlexibleServerName 188 | scope: resourceGroup 189 | params: { 190 | name: _postgresFlexibleServerName 191 | location: location 192 | tags: tags 193 | postgresVersion: '15' 194 | sku: { 195 | name: 'Standard_B1ms' 196 | tier: 'Burstable' 197 | } 198 | storage: { 199 | storageSizeGB: 32 200 | autoGrow: 'Disabled' 201 | } 202 | administratorLogin: postgresAdminUsername 203 | administratorLoginPassword: postgresAdminPassword 204 | databaseNames: [ 205 | environmentName 206 | ] 207 | allowAllIPsFirewall: true 208 | allowAzureIPsFirewall: true 209 | } 210 | } 211 | 212 | module frontend './app/frontend.bicep' = { 213 | name: 'frontend' 214 | scope: resourceGroup 215 | params: { 216 | name: _frontendContainerAppName 217 | location: location 218 | tags: tags 219 | identityName: _frontendContainerAppName 220 | aiShoppingCartServiceUri: aiShoppingCartService.outputs.SERVICE_AI_SHOPPING_CART_URI 221 | applicationInsightsName: monitoring.outputs.applicationInsightsName 222 | containerAppsEnvironmentName: containerAppsEnvironment.outputs.name 223 | containerRegistryName: containerRegistry.outputs.name 224 | exists: frontendAppExists 225 | } 226 | } 227 | 228 | module aiShoppingCartService 'app/ai-shopping-cart-service.bicep' = { 229 | name: 'ai-shopping-cart-service' 230 | scope: resourceGroup 231 | params: { 232 | name: _springAppsInstanceName 233 | location: location 234 | tags: tags 235 | // applicationInsightsName: monitoring.outputs.applicationInsightsName 236 | containerAppsEnvironmentName: containerAppsEnvironment.outputs.name 237 | relativePath: aiShoppingCartServiceRelativePath 238 | postgresFlexibleServerName: postgresFlexibleServer.outputs.name 239 | postgresDatabaseName: environmentName 240 | postgresAdminPassword: postgresAdminPassword 241 | azureOpenAiApiKey: azureOpenAiApiKey 242 | azureOpenAiEndpoint: azureOpenAiEndpoint 243 | azureOpenAiDeploymentId: azureOpenAiDeploymentId 244 | isAzureOpenAiGpt4Model: isAzureOpenAiGpt4Model 245 | } 246 | } 247 | 248 | resource telemetrydeployment 'Microsoft.Resources/deployments@2021-04-01' = if (enableTelemetry) { 249 | name: telemetryId 250 | location: location 251 | properties: { 252 | mode: 'Incremental' 253 | template: { 254 | '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#' 255 | contentVersion: '1.0.0.0' 256 | resources: {} 257 | } 258 | } 259 | } 260 | 261 | /* -------------------------------------------------------------------------- */ 262 | /* OUTPUTS */ 263 | /* -------------------------------------------------------------------------- */ 264 | 265 | // Outputs are automatically saved in the local azd environment .env file. 266 | // To see these outputs, run `azd env get-values`, or `azd env get-values --output json` for json output. 267 | 268 | @description('The location of all resources.') 269 | output AZURE_LOCATION string = location 270 | 271 | @description('The id of the tenant.') 272 | output AZURE_TENANT_ID string = tenant().tenantId 273 | 274 | @description('The endpoint of the container registry.') 275 | output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.outputs.loginServer 276 | 277 | @description('The URI of the frontend.') 278 | output SERVICE_FRONTEND_URI string = frontend.outputs.SERVICE_FRONTEND_URI 279 | -------------------------------------------------------------------------------- /infra/main.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "environmentName": { 6 | "value": "${AZURE_ENV_NAME}" 7 | }, 8 | "location": { 9 | "value": "${AZURE_LOCATION}" 10 | }, 11 | "frontendAppExists": { 12 | "value": "${SERVICE_FRONTEND_RESOURCE_EXISTS=false}" 13 | }, 14 | "aiShoppingCartServiceRelativePath": { 15 | "value": "${SERVICE_AI_SHOPPING_CART_SERVICE_RELATIVE_PATH=}" 16 | }, 17 | "azureOpenAiApiKey": { 18 | "value": "${azureOpenAiApiKey}" 19 | }, 20 | "azureOpenAiEndpoint": { 21 | "value": "${azureOpenAiEndpoint}" 22 | }, 23 | "azureOpenAiDeploymentId": { 24 | "value": "${azureOpenAiDeploymentId}" 25 | }, 26 | "isAzureOpenAiGpt4Model": { 27 | "value": "${isAzureOpenAiGpt4Model=true}" 28 | }, 29 | "enableTelemetry": { 30 | "value": "${enableTelemetry=true}" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/ai-shopping-cart-service/.gitignore: -------------------------------------------------------------------------------- 1 | ### IntelliJ ### 2 | .idea 3 | *.iml 4 | *.ipr 5 | 6 | ### Java ### 7 | # Compiled class file 8 | *.class 9 | 10 | # Log file 11 | *.log 12 | 13 | # BlueJ files 14 | *.ctxt 15 | 16 | # Mobile Tools for Java (J2ME) 17 | .mtj.tmp/ 18 | 19 | # Package Files # 20 | *.jar 21 | *.war 22 | *.nar 23 | *.ear 24 | *.zip 25 | *.tar.gz 26 | *.rar 27 | 28 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 29 | hs_err_pid* 30 | replay_pid* 31 | 32 | ### Maven ### 33 | target/ 34 | pom.xml.tag 35 | pom.xml.releaseBackup 36 | pom.xml.versionsBackup 37 | pom.xml.next 38 | release.properties 39 | dependency-reduced-pom.xml 40 | buildNumber.properties 41 | .mvn/timing.properties 42 | # https://github.com/takari/maven-wrapper#usage-without-binary-jar 43 | .mvn/wrapper/maven-wrapper.jar 44 | 45 | # Eclipse m2e generated files 46 | # Eclipse Core 47 | .project 48 | # JDT-specific (Eclipse Java Development Tools) 49 | .classpath 50 | 51 | .azure -------------------------------------------------------------------------------- /src/ai-shopping-cart-service/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.8/apache-maven-3.8.8-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar 19 | -------------------------------------------------------------------------------- /src/ai-shopping-cart-service/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with AI Shopping Cart Service 2 | 3 | This is the AI Shopping Cart Service sample app project. It is a simple microservice that provides a REST API to manage a shopping cart. It is used in the [AI Shopping Cart Front-end](../frontend/README.md) sample app. 4 | 5 | **Note: This project is a sample app that is not suited to be used in production.** 6 | 7 | This project was bootstrapped with [Spring Initializr](https://start.spring.io/). Please find below information about this Spring Boot microservice and how to run it locally. To deploy it to [Azure Spring Apps](https://learn.microsoft.com/en-us/azure/spring-apps/), please use the [Azure Developer CLI](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/overview) as described in the [AI Shopping Cart README](../../README.md). 8 | 9 | ## Dependencies 10 | 11 | This microservice uses the following dependencies: 12 | 13 | - [Azure OpenAI](https://learn.microsoft.com/en-us/azure/ai-services/openai/): This is the OpenAI API that is used to generate the AI nutrition analysis of the shopping cart and the top 3 recipes based on the shopping cart content. It was designed to use [GPT-4 model](https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models#gpt-4) with 8k tokens. 14 | - [PostgreSQL](https://www.postgresql.org/): This is the database that is used to store the shopping cart state. [Azure Database for PostgreSQL Flexible Server](https://docs.microsoft.com/en-us/azure/postgresql/flexible-server/overview) is used to deploy AI Shopping Cart sample app. 15 | 16 | ## Pre-requisites 17 | 18 | ### Dvelopment Tools 19 | 20 | The following pre-requisites are required to run this sample app locally: 21 | 22 | - [Java 17](https://learn.microsoft.com/en-us/java/openjdk/install) 23 | 24 | For Maven, a wrapper is provided in the project. This project was developped and deployed with Maven 3.8.8. 25 | 26 | ### Required Properties 27 | 28 | There are 6 properties that must be set before running the app. These properties can be set in the [application.properties](src/main/resources/application.properties) file or as environment variables. The description of these properties can be found in the [additional spring configuration metadata file](src/main/resources/META-INF/additional-spring-configuration-metadata.json). The following table describes these properties: 29 | 30 | | Property | Environment Variable | Description | 31 | | --- | --- | --- | 32 | | `spring.datasource.url` | `SPRING_DATASOURCE_URL` | The JDBC URL of the PostgreSQL database. The URL format is: `jdbc:postgresql://[host]:[port]/[database]?sslmode=require` | 33 | | `spring.datasource.username` | `SPRING_DATASOURCE_USERNAME` | The username to connect to the PostgreSQL database. | 34 | | `spring.datasource.password` | `SPRING_DATASOURCE_PASSWORD` | The password to connect to the PostgreSQL database. | 35 | | `azure.openai.api-key` | `AZURE_OPENAI_API_KEY` | The API key to connect to the Azure OpenAI API. | 36 | | `azure.openai.endpoint` | `AZURE_OPENAI_ENDPOINT` | The endpoint to connect to the Azure OpenAI API. | 37 | | `azure.openai.deployment.id` | `AZURE_OPENAI_DEPLOYMENT_ID` | The deployment ID of the Azure OpenAI model to use for chat completion. | 38 | 39 | There are 2 additional properties that can be set to change the behavior of the app: 40 | 41 | | Property | Environment Variable | Description | 42 | | --- | --- | --- | 43 | | `azure.openai.temperature` | `AZURE_OPENAI_TEMPERATURE` | The temperature to use for chat completion. The default value is 0.7. | 44 | | `azure.openai.top.p` | `AZURE_OPENAI_TOP_P` | The top P value to use for chat completion. The default value is 1.0. | 45 | 46 | 47 | ## Running the app locally 48 | 49 | To run the app locally, you can use the following command: 50 | 51 | ```bash 52 | mvnw spring-boot:run 53 | ``` 54 | 55 | You can also build the app and run it with the following commands: 56 | 57 | ```bash 58 | mvnw clean package 59 | java -jar target/ai-shopping-cart-service-1.0.0-SNAPSHOT.jar 60 | ``` 61 | -------------------------------------------------------------------------------- /src/ai-shopping-cart-service/client.http: -------------------------------------------------------------------------------- 1 | # Variables 2 | @host = http://localhost:8080 3 | @name = Pear 4 | @category = Fruits 5 | @quantity = 10 6 | @id = 1 7 | 8 | ### Add a cart item 9 | 10 | POST {{host}}/api/cart-items 11 | Content-Type: application/json 12 | 13 | { 14 | "name": "{{name}}", 15 | "category": "{{category}}", 16 | "quantity": {{quantity}} 17 | } 18 | 19 | 20 | ### Get all cart items 21 | 22 | GET {{host}}/api/cart-items 23 | 24 | ### Remove all cart items 25 | 26 | DELETE {{host}}/api/cart-items 27 | 28 | ### Remove a cart item 29 | 30 | DELETE {{host}}/api/cart-items/{{id}} 31 | 32 | ### Get AI Nutrition Analysis 33 | 34 | GET {{host}}/api/cart-items/ai-nutrition-analysis 35 | 36 | ### Get Top 3 Recipes 37 | 38 | GET {{host}}/api/cart-items/top-3-recipes -------------------------------------------------------------------------------- /src/ai-shopping-cart-service/mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.2.0 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | # e.g. to debug Maven itself, use 32 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | # ---------------------------------------------------------------------------- 35 | 36 | if [ -z "$MAVEN_SKIP_RC" ] ; then 37 | 38 | if [ -f /usr/local/etc/mavenrc ] ; then 39 | . /usr/local/etc/mavenrc 40 | fi 41 | 42 | if [ -f /etc/mavenrc ] ; then 43 | . /etc/mavenrc 44 | fi 45 | 46 | if [ -f "$HOME/.mavenrc" ] ; then 47 | . "$HOME/.mavenrc" 48 | fi 49 | 50 | fi 51 | 52 | # OS specific support. $var _must_ be set to either true or false. 53 | cygwin=false; 54 | darwin=false; 55 | mingw=false 56 | case "$(uname)" in 57 | CYGWIN*) cygwin=true ;; 58 | MINGW*) mingw=true;; 59 | Darwin*) darwin=true 60 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 61 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 62 | if [ -z "$JAVA_HOME" ]; then 63 | if [ -x "/usr/libexec/java_home" ]; then 64 | JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME 65 | else 66 | JAVA_HOME="/Library/Java/Home"; export JAVA_HOME 67 | fi 68 | fi 69 | ;; 70 | esac 71 | 72 | if [ -z "$JAVA_HOME" ] ; then 73 | if [ -r /etc/gentoo-release ] ; then 74 | JAVA_HOME=$(java-config --jre-home) 75 | fi 76 | fi 77 | 78 | # For Cygwin, ensure paths are in UNIX format before anything is touched 79 | if $cygwin ; then 80 | [ -n "$JAVA_HOME" ] && 81 | JAVA_HOME=$(cygpath --unix "$JAVA_HOME") 82 | [ -n "$CLASSPATH" ] && 83 | CLASSPATH=$(cygpath --path --unix "$CLASSPATH") 84 | fi 85 | 86 | # For Mingw, ensure paths are in UNIX format before anything is touched 87 | if $mingw ; then 88 | [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && 89 | JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" 90 | fi 91 | 92 | if [ -z "$JAVA_HOME" ]; then 93 | javaExecutable="$(which javac)" 94 | if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then 95 | # readlink(1) is not available as standard on Solaris 10. 96 | readLink=$(which readlink) 97 | if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then 98 | if $darwin ; then 99 | javaHome="$(dirname "\"$javaExecutable\"")" 100 | javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" 101 | else 102 | javaExecutable="$(readlink -f "\"$javaExecutable\"")" 103 | fi 104 | javaHome="$(dirname "\"$javaExecutable\"")" 105 | javaHome=$(expr "$javaHome" : '\(.*\)/bin') 106 | JAVA_HOME="$javaHome" 107 | export JAVA_HOME 108 | fi 109 | fi 110 | fi 111 | 112 | if [ -z "$JAVACMD" ] ; then 113 | if [ -n "$JAVA_HOME" ] ; then 114 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 115 | # IBM's JDK on AIX uses strange locations for the executables 116 | JAVACMD="$JAVA_HOME/jre/sh/java" 117 | else 118 | JAVACMD="$JAVA_HOME/bin/java" 119 | fi 120 | else 121 | JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" 122 | fi 123 | fi 124 | 125 | if [ ! -x "$JAVACMD" ] ; then 126 | echo "Error: JAVA_HOME is not defined correctly." >&2 127 | echo " We cannot execute $JAVACMD" >&2 128 | exit 1 129 | fi 130 | 131 | if [ -z "$JAVA_HOME" ] ; then 132 | echo "Warning: JAVA_HOME environment variable is not set." 133 | fi 134 | 135 | # traverses directory structure from process work directory to filesystem root 136 | # first directory with .mvn subdirectory is considered project base directory 137 | find_maven_basedir() { 138 | if [ -z "$1" ] 139 | then 140 | echo "Path not specified to find_maven_basedir" 141 | return 1 142 | fi 143 | 144 | basedir="$1" 145 | wdir="$1" 146 | while [ "$wdir" != '/' ] ; do 147 | if [ -d "$wdir"/.mvn ] ; then 148 | basedir=$wdir 149 | break 150 | fi 151 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 152 | if [ -d "${wdir}" ]; then 153 | wdir=$(cd "$wdir/.." || exit 1; pwd) 154 | fi 155 | # end of workaround 156 | done 157 | printf '%s' "$(cd "$basedir" || exit 1; pwd)" 158 | } 159 | 160 | # concatenates all lines of a file 161 | concat_lines() { 162 | if [ -f "$1" ]; then 163 | # Remove \r in case we run on Windows within Git Bash 164 | # and check out the repository with auto CRLF management 165 | # enabled. Otherwise, we may read lines that are delimited with 166 | # \r\n and produce $'-Xarg\r' rather than -Xarg due to word 167 | # splitting rules. 168 | tr -s '\r\n' ' ' < "$1" 169 | fi 170 | } 171 | 172 | log() { 173 | if [ "$MVNW_VERBOSE" = true ]; then 174 | printf '%s\n' "$1" 175 | fi 176 | } 177 | 178 | BASE_DIR=$(find_maven_basedir "$(dirname "$0")") 179 | if [ -z "$BASE_DIR" ]; then 180 | exit 1; 181 | fi 182 | 183 | MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR 184 | log "$MAVEN_PROJECTBASEDIR" 185 | 186 | ########################################################################################## 187 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 188 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 189 | ########################################################################################## 190 | wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" 191 | if [ -r "$wrapperJarPath" ]; then 192 | log "Found $wrapperJarPath" 193 | else 194 | log "Couldn't find $wrapperJarPath, downloading it ..." 195 | 196 | if [ -n "$MVNW_REPOURL" ]; then 197 | wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 198 | else 199 | wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 200 | fi 201 | while IFS="=" read -r key value; do 202 | # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) 203 | safeValue=$(echo "$value" | tr -d '\r') 204 | case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; 205 | esac 206 | done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" 207 | log "Downloading from: $wrapperUrl" 208 | 209 | if $cygwin; then 210 | wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") 211 | fi 212 | 213 | if command -v wget > /dev/null; then 214 | log "Found wget ... using wget" 215 | [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" 216 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 217 | wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 218 | else 219 | wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 220 | fi 221 | elif command -v curl > /dev/null; then 222 | log "Found curl ... using curl" 223 | [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" 224 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 225 | curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" 226 | else 227 | curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" 228 | fi 229 | else 230 | log "Falling back to using Java to download" 231 | javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" 232 | javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" 233 | # For Cygwin, switch paths to Windows format before running javac 234 | if $cygwin; then 235 | javaSource=$(cygpath --path --windows "$javaSource") 236 | javaClass=$(cygpath --path --windows "$javaClass") 237 | fi 238 | if [ -e "$javaSource" ]; then 239 | if [ ! -e "$javaClass" ]; then 240 | log " - Compiling MavenWrapperDownloader.java ..." 241 | ("$JAVA_HOME/bin/javac" "$javaSource") 242 | fi 243 | if [ -e "$javaClass" ]; then 244 | log " - Running MavenWrapperDownloader.java ..." 245 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" 246 | fi 247 | fi 248 | fi 249 | fi 250 | ########################################################################################## 251 | # End of extension 252 | ########################################################################################## 253 | 254 | # If specified, validate the SHA-256 sum of the Maven wrapper jar file 255 | wrapperSha256Sum="" 256 | while IFS="=" read -r key value; do 257 | case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; 258 | esac 259 | done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" 260 | if [ -n "$wrapperSha256Sum" ]; then 261 | wrapperSha256Result=false 262 | if command -v sha256sum > /dev/null; then 263 | if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then 264 | wrapperSha256Result=true 265 | fi 266 | elif command -v shasum > /dev/null; then 267 | if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then 268 | wrapperSha256Result=true 269 | fi 270 | else 271 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." 272 | echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." 273 | exit 1 274 | fi 275 | if [ $wrapperSha256Result = false ]; then 276 | echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 277 | echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 278 | echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 279 | exit 1 280 | fi 281 | fi 282 | 283 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 284 | 285 | # For Cygwin, switch paths to Windows format before running java 286 | if $cygwin; then 287 | [ -n "$JAVA_HOME" ] && 288 | JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") 289 | [ -n "$CLASSPATH" ] && 290 | CLASSPATH=$(cygpath --path --windows "$CLASSPATH") 291 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 292 | MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") 293 | fi 294 | 295 | # Provide a "standardized" way to retrieve the CLI args that will 296 | # work with both Windows and non-Windows executions. 297 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" 298 | export MAVEN_CMD_LINE_ARGS 299 | 300 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 301 | 302 | # shellcheck disable=SC2086 # safe args 303 | exec "$JAVACMD" \ 304 | $MAVEN_OPTS \ 305 | $MAVEN_DEBUG_OPTS \ 306 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 307 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 308 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 309 | -------------------------------------------------------------------------------- /src/ai-shopping-cart-service/mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Apache Maven Wrapper startup batch script, version 3.2.0 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 28 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 29 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 30 | @REM e.g. to debug Maven itself, use 31 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 32 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 33 | @REM ---------------------------------------------------------------------------- 34 | 35 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 36 | @echo off 37 | @REM set title of command window 38 | title %0 39 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 40 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 41 | 42 | @REM set %HOME% to equivalent of $HOME 43 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 44 | 45 | @REM Execute a user defined script before this one 46 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 47 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 48 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 49 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 50 | :skipRcPre 51 | 52 | @setlocal 53 | 54 | set ERROR_CODE=0 55 | 56 | @REM To isolate internal variables from possible post scripts, we use another setlocal 57 | @setlocal 58 | 59 | @REM ==== START VALIDATION ==== 60 | if not "%JAVA_HOME%" == "" goto OkJHome 61 | 62 | echo. 63 | echo Error: JAVA_HOME not found in your environment. >&2 64 | echo Please set the JAVA_HOME variable in your environment to match the >&2 65 | echo location of your Java installation. >&2 66 | echo. 67 | goto error 68 | 69 | :OkJHome 70 | if exist "%JAVA_HOME%\bin\java.exe" goto init 71 | 72 | echo. 73 | echo Error: JAVA_HOME is set to an invalid directory. >&2 74 | echo JAVA_HOME = "%JAVA_HOME%" >&2 75 | echo Please set the JAVA_HOME variable in your environment to match the >&2 76 | echo location of your Java installation. >&2 77 | echo. 78 | goto error 79 | 80 | @REM ==== END VALIDATION ==== 81 | 82 | :init 83 | 84 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 85 | @REM Fallback to current working directory if not found. 86 | 87 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 88 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 89 | 90 | set EXEC_DIR=%CD% 91 | set WDIR=%EXEC_DIR% 92 | :findBaseDir 93 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 94 | cd .. 95 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 96 | set WDIR=%CD% 97 | goto findBaseDir 98 | 99 | :baseDirFound 100 | set MAVEN_PROJECTBASEDIR=%WDIR% 101 | cd "%EXEC_DIR%" 102 | goto endDetectBaseDir 103 | 104 | :baseDirNotFound 105 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 106 | cd "%EXEC_DIR%" 107 | 108 | :endDetectBaseDir 109 | 110 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 111 | 112 | @setlocal EnableExtensions EnableDelayedExpansion 113 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 114 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 115 | 116 | :endReadAdditionalConfig 117 | 118 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 123 | 124 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 125 | IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B 126 | ) 127 | 128 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 129 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 130 | if exist %WRAPPER_JAR% ( 131 | if "%MVNW_VERBOSE%" == "true" ( 132 | echo Found %WRAPPER_JAR% 133 | ) 134 | ) else ( 135 | if not "%MVNW_REPOURL%" == "" ( 136 | SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 137 | ) 138 | if "%MVNW_VERBOSE%" == "true" ( 139 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 140 | echo Downloading from: %WRAPPER_URL% 141 | ) 142 | 143 | powershell -Command "&{"^ 144 | "$webclient = new-object System.Net.WebClient;"^ 145 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 146 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 147 | "}"^ 148 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ 149 | "}" 150 | if "%MVNW_VERBOSE%" == "true" ( 151 | echo Finished downloading %WRAPPER_JAR% 152 | ) 153 | ) 154 | @REM End of extension 155 | 156 | @REM If specified, validate the SHA-256 sum of the Maven wrapper jar file 157 | SET WRAPPER_SHA_256_SUM="" 158 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 159 | IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B 160 | ) 161 | IF NOT %WRAPPER_SHA_256_SUM%=="" ( 162 | powershell -Command "&{"^ 163 | "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ 164 | "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ 165 | " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ 166 | " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ 167 | " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ 168 | " exit 1;"^ 169 | "}"^ 170 | "}" 171 | if ERRORLEVEL 1 goto error 172 | ) 173 | 174 | @REM Provide a "standardized" way to retrieve the CLI args that will 175 | @REM work with both Windows and non-Windows executions. 176 | set MAVEN_CMD_LINE_ARGS=%* 177 | 178 | %MAVEN_JAVA_EXE% ^ 179 | %JVM_CONFIG_MAVEN_PROPS% ^ 180 | %MAVEN_OPTS% ^ 181 | %MAVEN_DEBUG_OPTS% ^ 182 | -classpath %WRAPPER_JAR% ^ 183 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 184 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 185 | if ERRORLEVEL 1 goto error 186 | goto end 187 | 188 | :error 189 | set ERROR_CODE=1 190 | 191 | :end 192 | @endlocal & set ERROR_CODE=%ERROR_CODE% 193 | 194 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 195 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 196 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 197 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 198 | :skipRcPost 199 | 200 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 201 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 202 | 203 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 204 | 205 | cmd /C exit /B %ERROR_CODE% 206 | -------------------------------------------------------------------------------- /src/ai-shopping-cart-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 3.1.2 10 | 11 | 12 | 13 | com.microsoft.azure.samples 14 | ai-shopping-cart-service 15 | 1.0.0-SNAPSHOT 16 | ai-shopping-cart-service 17 | AI Shopping Cart Service 18 | 19 | 20 | 17 21 | 22 | 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-data-jpa 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-web 31 | 32 | 33 | org.postgresql 34 | postgresql 35 | runtime 36 | 37 | 38 | com.azure 39 | azure-ai-openai 40 | 1.0.0-beta.2 41 | 42 | 43 | org.springframework.boot 44 | spring-boot-starter-test 45 | test 46 | 47 | 48 | 49 | 50 | 51 | 52 | org.springframework.boot 53 | spring-boot-maven-plugin 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/ai-shopping-cart-service/src/main/java/com/microsoft/azure/samples/aishoppingcartservice/AiShoppingCartServiceApplication.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.azure.samples.aishoppingcartservice; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class AiShoppingCartServiceApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(AiShoppingCartServiceApplication.class, args); 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /src/ai-shopping-cart-service/src/main/java/com/microsoft/azure/samples/aishoppingcartservice/AiShoppingCartServiceConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.azure.samples.aishoppingcartservice; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | import com.microsoft.azure.samples.aishoppingcartservice.openai.ShoppingCartAiRecommendations; 8 | 9 | @Configuration 10 | public class AiShoppingCartServiceConfiguration { 11 | @Value("${azure.openai.apikey}") 12 | private String azureOpenAiApiKey; 13 | @Value("${azure.openai.endpoint}") 14 | private String azureOpenAiEndpoint; 15 | @Value("${azure.openai.deployment.id}") 16 | private String azureOpenAiModelDeploymentId; 17 | @Value("${azure.openai.temperature}") 18 | private double temperature; 19 | @Value("${azure.openai.top.p}") 20 | private double topP; 21 | @Value("${azure.openai.is.gpt4}") 22 | private boolean isGpt4; 23 | 24 | @Bean 25 | public ShoppingCartAiRecommendations shoppingCartRecommendations() { 26 | return new ShoppingCartAiRecommendations( 27 | this.azureOpenAiApiKey, 28 | this.azureOpenAiEndpoint, 29 | this.azureOpenAiModelDeploymentId, 30 | this.temperature, 31 | this.topP, 32 | this.isGpt4); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/ai-shopping-cart-service/src/main/java/com/microsoft/azure/samples/aishoppingcartservice/cartitem/CartItem.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.azure.samples.aishoppingcartservice.cartitem; 2 | 3 | import jakarta.persistence.Entity; 4 | import jakarta.persistence.GeneratedValue; 5 | import jakarta.persistence.GenerationType; 6 | import jakarta.persistence.Id; 7 | 8 | @Entity 9 | public class CartItem { 10 | @Id 11 | @GeneratedValue(strategy = GenerationType.AUTO) 12 | private long id; 13 | private String name; 14 | private String category; 15 | private int quantity; 16 | 17 | public CartItem() { 18 | } 19 | 20 | public CartItem(final String name, final String category, final int quantity) { 21 | this.name = name; 22 | this.category = category; 23 | this.quantity = quantity; 24 | } 25 | 26 | public long getId() { 27 | return this.id; 28 | } 29 | 30 | public String getName() { 31 | return this.name; 32 | } 33 | 34 | public String getCategory() { 35 | return this.category; 36 | } 37 | 38 | public int getQuantity() { 39 | return this.quantity; 40 | } 41 | 42 | public void setId(final long id) { 43 | this.id = id; 44 | } 45 | 46 | public void setName(final String name) { 47 | this.name = name; 48 | } 49 | 50 | public void setCategory(final String category) { 51 | this.category = category; 52 | } 53 | 54 | public void setQuantity(final int quantity) { 55 | this.quantity = quantity; 56 | } 57 | 58 | @Override 59 | public String toString() { 60 | return String.format( 61 | "CartItem[id=%d, name='%s', category='%s', quantity='%d']", 62 | this.id, 63 | this.name, 64 | this.category, 65 | this.quantity 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/ai-shopping-cart-service/src/main/java/com/microsoft/azure/samples/aishoppingcartservice/cartitem/CartItemController.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.azure.samples.aishoppingcartservice.cartitem; 2 | 3 | import java.util.List; 4 | 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.web.bind.annotation.CrossOrigin; 9 | import org.springframework.web.bind.annotation.DeleteMapping; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.PathVariable; 12 | import org.springframework.web.bind.annotation.PostMapping; 13 | import org.springframework.web.bind.annotation.PutMapping; 14 | import org.springframework.web.bind.annotation.RequestBody; 15 | import org.springframework.web.bind.annotation.RequestMapping; 16 | import org.springframework.web.bind.annotation.ResponseStatus; 17 | import org.springframework.web.bind.annotation.RestController; 18 | 19 | import com.microsoft.azure.samples.aishoppingcartservice.cartitem.exception.CartItemNotFoundException; 20 | import com.microsoft.azure.samples.aishoppingcartservice.cartitem.exception.EmptyCartException; 21 | import com.microsoft.azure.samples.aishoppingcartservice.openai.ShoppingCartAiRecommendations; 22 | 23 | @RestController 24 | @RequestMapping("/api/cart-items") 25 | public class CartItemController { 26 | private static final Logger log = LoggerFactory.getLogger(CartItemController.class); 27 | private static final String AI_NUTRITION_ANALYSIS_EXCEPTION_MESSAGE = "AI nutrition analysis cannot be performed on an empty cart."; 28 | private static final String TOP_3_RECIPES_EXEPTION_MESSAGE = "Top 3 recipes cannot be generated for an empty cart."; 29 | 30 | private final CartItemRepository cartItemRepository; 31 | private final ShoppingCartAiRecommendations shoppingCartAiRecommendations; 32 | 33 | public CartItemController(final CartItemRepository cartItemRepository, 34 | final ShoppingCartAiRecommendations shoppingCartAiRecommendations) { 35 | this.cartItemRepository = cartItemRepository; 36 | this.shoppingCartAiRecommendations = shoppingCartAiRecommendations; 37 | } 38 | 39 | @CrossOrigin 40 | @PostMapping 41 | @ResponseStatus(HttpStatus.CREATED) 42 | public CartItem addCartItem(@RequestBody final CartItem cartItem) { 43 | log.info("Creating cart item: {}", cartItem); 44 | return this.cartItemRepository.save(cartItem); 45 | } 46 | 47 | @CrossOrigin 48 | @GetMapping 49 | public Iterable getCartItems() { 50 | log.info("Getting all cart items"); 51 | return this.cartItemRepository.findAll(); 52 | } 53 | 54 | @CrossOrigin 55 | @PutMapping("/{id}") 56 | public CartItem updateCartItem(@PathVariable("id") final Long id, 57 | @RequestBody final CartItem cartItem) throws CartItemNotFoundException { 58 | log.info("Updating cart item: {}", cartItem); 59 | final CartItem existingCartItem = this.cartItemRepository.findById(id) 60 | .orElseThrow(() -> new CartItemNotFoundException(id)); 61 | existingCartItem.setName(cartItem.getName()); 62 | existingCartItem.setCategory(cartItem.getCategory()); 63 | existingCartItem.setQuantity(cartItem.getQuantity()); 64 | return this.cartItemRepository.save(existingCartItem); 65 | } 66 | 67 | @CrossOrigin 68 | @DeleteMapping("/{id}") 69 | public void removeCartItem(@PathVariable("id") final Long id) throws CartItemNotFoundException { 70 | log.info("Deleting cart item with id: {}", id); 71 | this.cartItemRepository.deleteById(id); 72 | } 73 | 74 | @CrossOrigin 75 | @DeleteMapping 76 | public void removeAllCartItems() { 77 | log.info("Deleting all cart items"); 78 | this.cartItemRepository.deleteAll(); 79 | } 80 | 81 | @CrossOrigin 82 | @GetMapping(value = "/ai-nutrition-analysis", produces = "application/json") 83 | public String getAiNutritionAnalysis() throws EmptyCartException { 84 | log.info("Getting AI nutrition analysis"); 85 | return this.shoppingCartAiRecommendations.getAINutritionAnalysis( 86 | getAllCartItems(AI_NUTRITION_ANALYSIS_EXCEPTION_MESSAGE)); 87 | } 88 | 89 | @CrossOrigin 90 | @GetMapping(value = "/top-3-recipes", produces = "application/json") 91 | public String getTop3Recipes() throws EmptyCartException { 92 | log.info("Getting top 3 recipes"); 93 | return this.shoppingCartAiRecommendations.getTop3Recipes( 94 | getAllCartItems(TOP_3_RECIPES_EXEPTION_MESSAGE)); 95 | } 96 | 97 | private List getAllCartItems(final String exceptionMessage) throws EmptyCartException { 98 | final List cartItems = this.cartItemRepository.findAll(); 99 | if (cartItems.isEmpty()) { 100 | throw new EmptyCartException(exceptionMessage); 101 | } 102 | return cartItems; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/ai-shopping-cart-service/src/main/java/com/microsoft/azure/samples/aishoppingcartservice/cartitem/CartItemRepository.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.azure.samples.aishoppingcartservice.cartitem; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | public interface CartItemRepository extends JpaRepository { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/ai-shopping-cart-service/src/main/java/com/microsoft/azure/samples/aishoppingcartservice/cartitem/exception/CartItemNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.azure.samples.aishoppingcartservice.cartitem.exception; 2 | 3 | public class CartItemNotFoundException extends Exception { 4 | public CartItemNotFoundException(final Long id) { 5 | super("Could not find cart item " + id); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/ai-shopping-cart-service/src/main/java/com/microsoft/azure/samples/aishoppingcartservice/cartitem/exception/EmptyCartException.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.azure.samples.aishoppingcartservice.cartitem.exception; 2 | 3 | public class EmptyCartException extends Exception { 4 | public EmptyCartException(final String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/ai-shopping-cart-service/src/main/java/com/microsoft/azure/samples/aishoppingcartservice/cartitem/exception/RestResponseEntityExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.azure.samples.aishoppingcartservice.cartitem.exception; 2 | 3 | import org.springframework.http.HttpHeaders; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.http.HttpStatusCode; 6 | import org.springframework.http.ProblemDetail; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.ControllerAdvice; 9 | import org.springframework.web.bind.annotation.ExceptionHandler; 10 | import org.springframework.web.context.request.WebRequest; 11 | import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; 12 | 13 | @ControllerAdvice 14 | public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler { 15 | @ExceptionHandler({EmptyCartException.class}) 16 | protected ResponseEntity handleEmptyCartException(final Exception exception, 17 | final WebRequest request) { 18 | return this.handleException(exception, request, HttpStatus.BAD_REQUEST); 19 | } 20 | 21 | @ExceptionHandler({CartItemNotFoundException.class}) 22 | protected ResponseEntity handleCartItemNotFoundException(final Exception exception, 23 | final WebRequest request) { 24 | return this.handleException(exception, request, HttpStatus.NOT_FOUND); 25 | } 26 | 27 | /** 28 | * Handle an exception with a given HTTP status and return a {@link ResponseEntity} that corresponds 29 | * to the error to return to the client. The response body follow the RFC 7807 standard. 30 | *

31 | *

32 | * The method creates a {@link ProblemDetail} instance from the given exception, status and request. 33 | * The default detail of the problem details is the exception's message. This problem detail 34 | * object is then used as the body when calling 35 | * {@link ResponseEntityExceptionHandler#handleExceptionInternal(Exception, Object, HttpHeaders, HttpStatusCode, WebRequest)}. 36 | * 37 | * @param exception The exception 38 | * @param request The request 39 | * @param status The status representing the problem 40 | * @return A {@link ResponseEntity} with the problem details as the body 41 | */ 42 | private ResponseEntity handleException(final Exception exception, 43 | final WebRequest request, 44 | final HttpStatus status) { 45 | final ProblemDetail body = this.createProblemDetail(exception, status, exception.getMessage(), 46 | null, null, request); 47 | return this.handleExceptionInternal(exception, body, new HttpHeaders(), status, request); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/ai-shopping-cart-service/src/main/java/com/microsoft/azure/samples/aishoppingcartservice/openai/ShoppingCartAiRecommendations.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.azure.samples.aishoppingcartservice.openai; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | import java.util.stream.Collectors; 6 | 7 | import com.azure.ai.openai.OpenAIClient; 8 | import com.azure.ai.openai.OpenAIClientBuilder; 9 | import com.azure.ai.openai.models.ChatChoice; 10 | import com.azure.ai.openai.models.ChatCompletions; 11 | import com.azure.ai.openai.models.ChatCompletionsOptions; 12 | import com.azure.ai.openai.models.ChatMessage; 13 | import com.azure.ai.openai.models.ChatRole; 14 | import com.azure.core.credential.AzureKeyCredential; 15 | 16 | import com.microsoft.azure.samples.aishoppingcartservice.cartitem.CartItem; 17 | 18 | public class ShoppingCartAiRecommendations { 19 | private static final String THE_BASKET_IS = "The basket is: "; 20 | 21 | private final OpenAIClient openAIClient; 22 | private final String azureOpenAiModelDeploymentId; 23 | private final double temperature; 24 | private final double topP; 25 | private final boolean isGpt4; 26 | 27 | public ShoppingCartAiRecommendations(final String azureOpenAiApiKey, 28 | final String azureOpenAiEndpoint, 29 | final String azureOpenAiModelDeploymentId, 30 | final double temperature, 31 | final double topP, 32 | final boolean isGpt4) { 33 | this.openAIClient = new OpenAIClientBuilder() 34 | .credential(new AzureKeyCredential(azureOpenAiApiKey)) 35 | .endpoint(azureOpenAiEndpoint) 36 | .buildClient(); 37 | this.azureOpenAiModelDeploymentId = azureOpenAiModelDeploymentId; 38 | this.temperature = temperature; 39 | this.topP = topP; 40 | this.isGpt4 = isGpt4; 41 | } 42 | 43 | public String getAINutritionAnalysis(final List cartItems) { 44 | // If the model used is GPT-3.5 Turbo instead of GPT-4, we need to add a postfix to the user message 45 | // to have a message that is more likely to be understood by the model. 46 | final String userMessagePostfix = this.isGpt4 ? "" : UserMessageConstants.GPT_3_5_AI_NUTRITION_ANALYSIS_POSTFIX; 47 | return getChatCompletion(SystemMessageConstants.AI_NUTRITION_ANALYSIS, cartItems, userMessagePostfix); 48 | } 49 | 50 | public String getTop3Recipes(final List cartItems) { 51 | // If the model used is GPT-3.5 Turbo instead of GPT-4, we need to add a postfix to the user message 52 | // to have a message that is more likely to be understood by the model. 53 | final String userMessagePostfix = this.isGpt4 ? "" : UserMessageConstants.GPT_3_5_RECIPES_POSTFIX; 54 | return getChatCompletion(SystemMessageConstants.RECIPES, cartItems, userMessagePostfix); 55 | } 56 | 57 | /** 58 | * Get a chat completion from the OpenAI API 59 | * 60 | * @param systemMessage The system message to be sent to the AI. It provides the context and instructions to the AI 61 | * to generate the chat completion. 62 | * @param cartItems The items in the shopping cart. 63 | * @param userMessagePostfix The postfix to be added to the user message to be sent to the AI. If the postfix is 64 | * blank, the user message is not modified. 65 | * @return The chat completion generated by the AI. 66 | */ 67 | private String getChatCompletion(final String systemMessage, 68 | final List cartItems, 69 | final String userMessagePostfix) { 70 | final List chatMessages = Arrays.asList( 71 | generateSystemChatMessage(systemMessage), 72 | generateUserChatMessageWithCartItems(cartItems, userMessagePostfix) 73 | ); 74 | final ChatCompletionsOptions chatCompletionsOptions = new ChatCompletionsOptions(chatMessages) 75 | .setTemperature(this.temperature) 76 | .setTopP(this.topP) 77 | .setN(1); // Number of chat completion choices to be generated 78 | final ChatCompletions chatCompletions = 79 | this.openAIClient.getChatCompletions(this.azureOpenAiModelDeploymentId, chatCompletionsOptions); 80 | return chatCompletions 81 | .getChoices() 82 | .stream() 83 | .map(ChatChoice::getMessage) 84 | .map(ChatMessage::getContent) 85 | .collect(Collectors.joining("\n")); 86 | } 87 | 88 | private ChatMessage generateSystemChatMessage(final String systemMessage) { 89 | final ChatMessage chatMessage = new ChatMessage(ChatRole.SYSTEM); 90 | chatMessage.setContent(systemMessage); 91 | return chatMessage; 92 | } 93 | 94 | /** 95 | * Generate a user chat message with the items in the shopping cart. 96 | *

97 | * If the prefix is blank, the user message will be "The basket is: ", where is a comma 98 | * separated list of the items in the shopping cart. 99 | *

100 | * If the prefix is not blank, the user message will be "The basket is: . ", where 101 | * is a comma separated list of the items in the shopping cart and is the prefix. 102 | * 103 | * @param cartItems The items in the shopping cart. 104 | * @param userMessagePostfix The postfix to be added to the user message to be sent to the AI. If the postfix is 105 | * blank, the user message is not modified. 106 | * @return The user chat message with the items in the shopping cart. 107 | */ 108 | private ChatMessage generateUserChatMessageWithCartItems(final List cartItems, 109 | final String userMessagePostfix) { 110 | final ChatMessage chatMessage = new ChatMessage(ChatRole.USER); 111 | String messageContent = THE_BASKET_IS + getCartItemsAsStringWithCommaSeparatedValues(cartItems); 112 | if (userMessagePostfix != null && !userMessagePostfix.isEmpty()) { 113 | messageContent += ". " + userMessagePostfix; 114 | } 115 | chatMessage.setContent(messageContent); 116 | return chatMessage; 117 | } 118 | 119 | private String getCartItemsAsStringWithCommaSeparatedValues(final List cartItems) { 120 | return cartItems 121 | .stream() 122 | .map(CartItem::getName) 123 | .map(String::toLowerCase) 124 | .map(String::trim) 125 | .collect(Collectors.joining(", ")); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/ai-shopping-cart-service/src/main/java/com/microsoft/azure/samples/aishoppingcartservice/openai/SystemMessageConstants.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.azure.samples.aishoppingcartservice.openai; 2 | 3 | public final class SystemMessageConstants { 4 | public static final String AI_NUTRITION_ANALYSIS = "You will be acting as an expert in nutrition and healthy eating habits. The user will provide a basket containing food products. You need to answer how nutritive is it and how you can make it better. Add an overall nutriscore that is the mean nutriscore of each product in the basket. The nutriscore is a score from A to E, A being the best and E the worst. The nutriscore of a product is calculated based on the amount of energy, saturated fat, sugar, sodium, protein, fiber, fruits, vegetables and nuts in the product. The nutriscore of a product is calculated based on the amount of energy, saturated fat, sugar, sodium, protein, fiber, fruits, vegetables and nuts in the product. The nutriscore of a product is calculated based on the amount of energy, saturated fat, sugar, sodium, protein, fiber, fruits, vegetables and nuts in the product. Return only the overall nutriscore, a brief explanation how nutritive it is and how it can be improved as a JSON with the following format: { nutriscore: \"\", explanation: \"\", recommendation: \"\"}."; 5 | 6 | public static final String RECIPES = "You will be acting as a Top Chef. The user will provide a basket containing food products. You need to answer what are the best recipes that can be made with the products in the basket. Return the top 3 recipes with the ingredients and the instructions. Give the overall nutriscore of each recipes. The nutriscore is a score from A to E, A being the best and E the worst. The nutriscore of a product is calculated based on the amount of energy, saturated fat, sugar, sodium, protein, fiber, fruits, vegetables and nuts in the product. The nutriscore of a product is calculated based on the amount of energy, saturated fat, sugar, sodium, protein, fiber, fruits, vegetables and nuts in the product. The nutriscore of a product is calculated based on the amount of energy, saturated fat, sugar, sodium, protein, fiber, fruits, vegetables and nuts in the product. Return only the top 3 recipes and the overall nutriscore as a JSON with the following format: { recipes: [ { name: \"\", ingredients: [], instructions: [], nutriscore: \"\", }]}."; 7 | 8 | private SystemMessageConstants() { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/ai-shopping-cart-service/src/main/java/com/microsoft/azure/samples/aishoppingcartservice/openai/UserMessageConstants.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.azure.samples.aishoppingcartservice.openai; 2 | 3 | public final class UserMessageConstants { 4 | /** 5 | * These sentences are added at the end of the user message for the AI Nutrition Analysis to make sure that 6 | * GPT-3.5 Turbo model returns only a JSON with the right format and no other text. This is not use for GPT-4 model. 7 | */ 8 | public static final String GPT_3_5_AI_NUTRITION_ANALYSIS_POSTFIX = "Return only a JSON with the following format: { nutriscore: \"\", explanation: \"\", recommendation: \"\"}. Do not add any other text."; 9 | 10 | /** 11 | * These sentences are added at the end of the user message for top 3 recipes to make sure that GPT-3.5 Turbo model 12 | * returns only a JSON with the right format and no other text. It also ensures that it returns recipes even if it 13 | * 'considers' that the basket (i.e. shopping cart) is too small. This is not use for GPT-4 model. 14 | */ 15 | public static final String GPT_3_5_RECIPES_POSTFIX = "Even if the basket is too small, propose 3 recipes. Return only a JSON with the following format: { recipes: [ { name: \"\", ingredients: [], instructions: [], nutriscore: \"\", }]}. Do not add any other text."; 16 | 17 | private UserMessageConstants() { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/ai-shopping-cart-service/src/main/resources/META-INF/additional-spring-configuration-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": [ 3 | { 4 | "name": "azure.openai.apikey", 5 | "type": "java.lang.String", 6 | "description": "Azure OpenAI Key. It can be override by the environment variable AZURE_OPENAI_API_KEY" 7 | }, 8 | { 9 | "name": "azure.openai.endpoint", 10 | "type": "java.lang.String", 11 | "description": "Azure OpenAI Endpoint. It can be override by the environment variable AZURE_OPENAI_ENDPOINT" 12 | }, 13 | { 14 | "name": "azure.openai.deployment.id", 15 | "type": "java.lang.String", 16 | "description": "Azure OpenAI Model Deployment ID. It can be override by the environment variable AZURE_OPENAI_DEPLOYMENT_ID" 17 | }, 18 | { 19 | "name": "azure.openai.temperature", 20 | "type": "java.lang.Double", 21 | "description": "Azure OpenAI Temperature. It can be override by the environment variable AZURE_OPENAI_TEMPERATURE", 22 | "defaultValue": "0.7" 23 | }, 24 | { 25 | "name": "azure.openai.top.p", 26 | "type": "java.lang.Double", 27 | "description": "Azure OpenAI Top P. It can be override by the environment variable AZURE_OPENAI_TOP_P", 28 | "defaultValue": "1.0" 29 | }, 30 | { 31 | "name": "azure.openai.is.gpt4", 32 | "type": "java.lang.Boolean", 33 | "description": "Set if Azure OpenAI model deployed is GPT-4 (true) or GPT-3.5 Turbo (false). It can be override by the environment variable AZURE_OPENAI_IS_GPT4", 34 | "defaultValue": "true" 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /src/ai-shopping-cart-service/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | logging.level.org.hibernate.SQL=INFO 2 | 3 | server.error.include-message = always 4 | 5 | spring.datasource.url= 6 | spring.datasource.username= 7 | spring.datasource.password= 8 | 9 | spring.jpa.show-sql=false 10 | spring.jpa.hibernate.ddl-auto=update 11 | 12 | azure.openai.apikey=${AZURE_OPENAI_API_KEY} 13 | azure.openai.endpoint=${AZURE_OPENAI_ENDPOINT} 14 | azure.openai.deployment.id=${AZURE_OPENAI_DEPLOYMENT_ID} 15 | azure.openai.temperature=${AZURE_OPENAI_TEMPERATURE:0.7d} 16 | azure.openai.top.p=${AZURE_OPENAI_TOP_P:1.0d} 17 | azure.openai.is.gpt4=${AZURE_OPENAI_IS_GPT4:true} 18 | -------------------------------------------------------------------------------- /src/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | env-config.js 26 | -------------------------------------------------------------------------------- /src/frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine as build 2 | WORKDIR /app 3 | COPY . . 4 | # Fix shell script line endings 5 | RUN apk update && apk add --no-cache dos2unix \ 6 | && dos2unix /app/entrypoint.sh && chmod +x /app/entrypoint.sh \ 7 | && apk del dos2unix 8 | RUN npm ci 9 | RUN npm run build 10 | 11 | FROM nginx:alpine 12 | WORKDIR /usr/share/nginx/html 13 | COPY --from=build /app/entrypoint.sh /bin 14 | COPY --from=build /app/build . 15 | EXPOSE 80 16 | CMD ["/bin/sh", "-c", "/bin/entrypoint.sh -o /usr/share/nginx/html/env-config.js && nginx -g \"daemon off;\""] -------------------------------------------------------------------------------- /src/frontend/README.MD: -------------------------------------------------------------------------------- 1 | # Getting Started with AI Shopping Cart Front-end 2 | 3 | This is the front-end of the AI Shopping Cart sample app project. 4 | 5 | **Note: This project is a sample app that is not suited to be used in production.** 6 | 7 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). The first section of this README contains information about the Create React App scripts that are available for development and testing. 8 | 9 | A Dockerfile is provided to build a Docker image for the app. This image is used in the sample to deploy the frontend in [Azure Container Apps](https://docs.microsoft.com/en-us/azure/container-apps/). The second section of this README contains information about the Dockerfile and how to build the Docker image. 10 | 11 | ## Available Scripts for Development and Testing 12 | 13 | In the project directory, you can run: 14 | 15 | ### `npm start` 16 | 17 | Runs the app in the development mode.\ 18 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 19 | 20 | The page will reload if you make edits.\ 21 | You will also see any lint errors in the console. 22 | 23 | ### `npm test` 24 | 25 | Launches the test runner in the interactive watch mode.\ 26 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 27 | 28 | ### `npm run build` 29 | 30 | Builds the app for production to the `build` folder.\ 31 | It correctly bundles React in production mode and optimizes the build for the best performance. 32 | 33 | The build is minified and the filenames include the hashes.\ 34 | Your app is ready to be deployed! 35 | 36 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 37 | 38 | ### `npm run eject` 39 | 40 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 41 | 42 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 43 | 44 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 45 | 46 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 47 | 48 | ### Learn More 49 | 50 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 51 | 52 | To learn React, check out the [React documentation](https://reactjs.org/). 53 | 54 | ## Build the Docker Image 55 | 56 | Using [Azure Developer CLI](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/overview) (azd), you can deploy the full sample without the need of building the Docker image yourself. However, if you want to build the Docker image yourself and deploy it locally or to an Azure Container Registry, you can use the following steps: 57 | 58 | 1. Open a terminal and navigate to the frontend directory 59 | 60 | ```bash 61 | cd src/frontend 62 | ``` 63 | 64 | 1. Build the Docker image 65 | 66 | ```bash 67 | docker build -t : . 68 | ``` 69 | 70 | Replace `` with the name of the image and `` with the tag of the image. 71 | 72 | 73 | Now you can ever run the image locally or push it to an Azure Container Registry. 74 | 75 | ### Run the Docker Image Locally 76 | 77 | To run the Docker image locally, use the following command: 78 | 79 | ```bash 80 | docker run -p 3000:3000 : 81 | ``` 82 | 83 | Replace `` with the name of the image and `` with the tag of the image. 84 | 85 | ### Push the Docker Image to an Azure Container Registry 86 | 87 | To push the Docker image to an Azure Container Registry, use the following steps: 88 | 89 | 1. Lg in to a registry 90 | 91 | ```bash 92 | az login 93 | az acr login --name 94 | ``` 95 | 96 | Replace `` with the name of the Azure Container Registry. 97 | 98 | 1. Tag the image 99 | 100 | ```bash 101 | docker tag : .azurecr.io/: 102 | ``` 103 | 104 | Replace `` with the name of the image and `` with the tag of the image. 105 | 106 | 1. Push the image to the registry 107 | 108 | ```bash 109 | docker push .azurecr.io/: 110 | ``` 111 | 112 | Replace `` with the name of the image and `` with the tag of the image. 113 | -------------------------------------------------------------------------------- /src/frontend/entrypoint.js: -------------------------------------------------------------------------------- 1 | "use-strict;" 2 | 3 | const dotenv = require("dotenv"); 4 | const fs = require("fs"); 5 | const os = require("os"); 6 | 7 | let envFilePath = ".env" 8 | let configRoot = "ENV_CONFIG" 9 | let outputFile = "./public/env-config.js" 10 | 11 | for (let i = 2; i < process.argv.length; i++) { 12 | switch (process.argv[i]) { 13 | case "-e": 14 | envFilePath = process.argv[++i] 15 | break; 16 | case "-o": 17 | outputFile = process.argv[++i] 18 | break; 19 | case "-c": 20 | configRoot = process.argv[++i] 21 | break; 22 | default: 23 | throw Error(`unknown option ${process.argv[i]}`) 24 | } 25 | } 26 | 27 | if (fs.existsSync(envFilePath)) { 28 | console.log(`Loading environment file from '${envFilePath}'`) 29 | 30 | dotenv.config({ 31 | path: envFilePath 32 | }) 33 | } 34 | 35 | console.log(`Generating JS configuration output to: ${outputFile}`) 36 | 37 | fs.writeFileSync(outputFile, `window.${configRoot} = {${os.EOL}${ 38 | Object.keys(process.env).filter(x => x.startsWith("REACT_APP_")).map(key => { 39 | console.log(`- Found '${key}'`); 40 | return `${key}: '${process.env[key]}',${os.EOL}`; 41 | }).join("") 42 | }${os.EOL}}`); 43 | -------------------------------------------------------------------------------- /src/frontend/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | ENV_FILE_PATH=".env" 6 | CONFIG_ROOT="ENV_CONFIG" 7 | OUTPUT_FILE="./public/env-config.js" 8 | 9 | generateOutput() { 10 | echo "Generating JS configuration output to: $OUTPUT_FILE" 11 | echo "window.$CONFIG_ROOT = {" >"$OUTPUT_FILE" 12 | for line in $1; do 13 | if beginswith REACT_APP_ "$line"; then 14 | key=${line%%=*} 15 | value=${line#*=} 16 | printf " - Found '%s'" "$key" 17 | printf "\t%s: '%s',\n" "$key" "$value" >>"$OUTPUT_FILE" 18 | fi 19 | done 20 | echo "}" >>"$OUTPUT_FILE" 21 | } 22 | 23 | beginswith() { case $2 in "$1"*) true;; *) false;; esac; } 24 | 25 | usage() { 26 | printf 27 | printf "Arguments:" 28 | printf "\t-e\t Sets the .env file to use (default: .env)" 29 | printf "\t-o\t Sets the output filename (default: ./public/env-config.js)" 30 | printf "\t-c\t Sets the JS configuration key (default: ENV_CONFIG)" 31 | printf 32 | printf "Example:" 33 | printf "\tbash entrypoint.sh -e .env -o env-config.js" 34 | } 35 | 36 | while getopts "e:o:c:" opt; do 37 | case $opt in 38 | e) ENV_FILE_PATH=$OPTARG ;; 39 | o) OUTPUT_FILE=$OPTARG ;; 40 | c) CONFIG_ROOT=$OPTARG ;; 41 | :) 42 | echo "Error: -${OPTARG} requires a value" 43 | exit 1 44 | ;; 45 | *) 46 | usage 47 | exit 1 48 | ;; 49 | esac 50 | done 51 | 52 | # Load .env file if supplied 53 | ENV_FILE="" 54 | if [ -f "$ENV_FILE_PATH" ]; then 55 | echo "Loading environment file from '$ENV_FILE_PATH'" 56 | ENV_FILE="$(cat "$ENV_FILE_PATH")" 57 | fi 58 | 59 | # Load system environment variables 60 | ENV_VARS=$(printenv) 61 | 62 | # Merge .env file with env variables 63 | ALL_VARS="$ENV_FILE\n$ENV_VARS" 64 | generateOutput "$ALL_VARS" 65 | -------------------------------------------------------------------------------- /src/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ai-shopping-cart-frontend", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.5", 7 | "@testing-library/react": "^13.4.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "@types/jest": "^27.5.2", 10 | "@types/node": "^16.18.38", 11 | "@types/react": "^18.2.14", 12 | "@types/react-dom": "^18.2.6", 13 | "bootstrap": "^5.3.0", 14 | "bootstrap-icons": "^1.10.5", 15 | "react": "^18.2.0", 16 | "react-bootstrap": "^2.8.0", 17 | "react-dom": "^18.2.0", 18 | "react-scripts": "5.0.1", 19 | "sass": "^1.63.6", 20 | "typescript": "^4.9.5", 21 | "web-vitals": "^2.1.4" 22 | }, 23 | "scripts": { 24 | "envconfig": "node entrypoint.js -e .env -o ./public/env-config.js", 25 | "prestart": "npm run envconfig", 26 | "start": "react-scripts start", 27 | "build": "react-scripts build", 28 | "test": "react-scripts test", 29 | "eject": "react-scripts eject" 30 | }, 31 | "eslintConfig": { 32 | "extends": [ 33 | "react-app", 34 | "react-app/jest" 35 | ] 36 | }, 37 | "browserslist": { 38 | "production": [ 39 | ">0.2%", 40 | "not dead", 41 | "not op_mini all" 42 | ], 43 | "development": [ 44 | "last 1 chrome version", 45 | "last 1 firefox version", 46 | "last 1 safari version" 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/app-templates-java-openai-springapps/d3061b97ba58d1e04aceaa0d6e86d2ef2bf448e5/src/frontend/public/favicon.ico -------------------------------------------------------------------------------- /src/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 19 | Shopping Cart 20 | 21 | 22 | 23 |

24 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/app-templates-java-openai-springapps/d3061b97ba58d1e04aceaa0d6e86d2ef2bf448e5/src/frontend/public/logo192.png -------------------------------------------------------------------------------- /src/frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/app-templates-java-openai-springapps/d3061b97ba58d1e04aceaa0d6e86d2ef2bf448e5/src/frontend/public/logo512.png -------------------------------------------------------------------------------- /src/frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Shopping Cart", 3 | "name": "AI Shopping Cart", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/frontend/src/@types/window.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | declare global { 4 | interface Window { 5 | ENV_CONFIG: { 6 | REACT_APP_API_URL: string; 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /src/frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Header, ShoppingCart } from './components'; 2 | import { Container } from 'react-bootstrap'; 3 | 4 | function App() { 5 | return ( 6 | 7 |
8 | 9 | 10 | ); 11 | } 12 | 13 | export default App; 14 | -------------------------------------------------------------------------------- /src/frontend/src/components/AddItemForm.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Form, FormControl, InputGroup } from 'react-bootstrap'; 2 | 3 | import { CartItem } from '../models'; 4 | import { capitalizeFirstLetter } from '../utils/stringUtils'; 5 | 6 | interface AddItemFormProps { 7 | addItemCallback: (item: CartItem) => void; 8 | } 9 | 10 | export const AddItemForm = ({ addItemCallback }: AddItemFormProps) => { 11 | const handleOnSubmit = (event: React.FormEvent) => { 12 | event.preventDefault(); 13 | const form = event.target as HTMLFormElement; 14 | const nameOfItemToAdd = form.elements.namedItem('nameOfItemToAdd') as HTMLInputElement; 15 | const quantityOfItemToAdd = form.elements.namedItem('quantityOfItemToAdd') as HTMLInputElement; 16 | const item: CartItem = { 17 | name: capitalizeFirstLetter(nameOfItemToAdd.value), 18 | category: 'unknown', 19 | quantity: quantityOfItemToAdd.valueAsNumber 20 | }; 21 | addItemCallback(item); 22 | nameOfItemToAdd.value = ''; 23 | quantityOfItemToAdd.value = '1'; 24 | }; 25 | 26 | return ( 27 |
28 | 29 | 30 | x 31 | 32 | 33 | 34 |
35 | ); 36 | } -------------------------------------------------------------------------------- /src/frontend/src/components/AiNutritionAnalysis.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Status } from '../models'; 4 | import { Error, Loader, NutriscoreBar } from '../components'; 5 | import { Card } from 'react-bootstrap'; 6 | import { API_URL } from '../config'; 7 | 8 | interface AiNutritionAnalysisProps { 9 | } 10 | 11 | interface AiNutritionAnalysisState { 12 | nutriscore: string; 13 | explanation: string; 14 | recommendation: string; 15 | status: Status; 16 | errorMessage?: string; 17 | } 18 | 19 | export class AiNutritionAnalysis extends React.Component { 20 | state: AiNutritionAnalysisState = { 21 | nutriscore: '', 22 | explanation: '', 23 | recommendation: '', 24 | status: Status.LOADING, 25 | }; 26 | 27 | componentDidMount(): void { 28 | let responseOk: boolean = true; 29 | fetch(`${API_URL}/api/cart-items/ai-nutrition-analysis`) 30 | .then((response) => { 31 | responseOk = response.ok; 32 | return response.json() 33 | }) 34 | .then((data) => { 35 | if (!responseOk) { 36 | this.setState({ 37 | nutriscore: '', 38 | explanation: '', 39 | recommendation: '', 40 | status: Status.ERROR, 41 | errorMessage: data.message, 42 | }); 43 | } else { 44 | this.setState({ 45 | nutriscore: data.nutriscore, 46 | explanation: data.explanation, 47 | recommendation: data.recommendation, 48 | status: Status.LOADED 49 | }); 50 | } 51 | }) 52 | .catch((error) => this.setState({ 53 | nutriscore: '', 54 | explanation: '', 55 | recommendation: '', 56 | status: Status.ERROR, 57 | errorMessage: error.message, 58 | })); 59 | } 60 | 61 | getHtmlForRecommendation(): JSX.Element { 62 | return ( 63 | <> 64 | 65 | {this.state.explanation} 66 | {this.state.recommendation} 67 | 68 | ); 69 | } 70 | 71 | render() { 72 | const { status } = this.state; 73 | return ( 74 | 75 | 76 | AI Nutrition Analysis 77 | {status === Status.LOADING && } 78 | {status === Status.ERROR && } 79 | {status === Status.LOADED && this.getHtmlForRecommendation()} 80 | 81 | 82 | ); 83 | } 84 | } -------------------------------------------------------------------------------- /src/frontend/src/components/Error.tsx: -------------------------------------------------------------------------------- 1 | import { Alert } from 'react-bootstrap'; 2 | 3 | import { generateId } from '../utils/idUtils'; 4 | 5 | interface ErrorProps { 6 | errorMessage: string | any; 7 | } 8 | 9 | export const Error: React.FC = ({ errorMessage }: ErrorProps) => { 10 | return ( 11 | 12 | 13 |
{errorMessage}
14 |
15 | ); 16 | } -------------------------------------------------------------------------------- /src/frontend/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | interface HeaderProps { 2 | title: string; 3 | } 4 | 5 | export const Header: React.FC = ({ title }: HeaderProps) => { 6 | return ( 7 |

{title}

8 | ); 9 | } -------------------------------------------------------------------------------- /src/frontend/src/components/Item.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Badge, Button, Form, FormControl, InputGroup, ListGroup } from 'react-bootstrap'; 3 | 4 | import { CartItem } from '../models'; 5 | import { capitalizeFirstLetter } from '../utils/stringUtils'; 6 | 7 | interface ItemProps { 8 | item: CartItem; 9 | updateItemCallback: (item: CartItem) => void; 10 | deleteItemCallback: (item: CartItem) => void; 11 | } 12 | 13 | interface ItemState { 14 | isMouseOver: boolean; 15 | } 16 | 17 | export class Item extends React.Component { 18 | state: ItemState = { 19 | isMouseOver: false 20 | }; 21 | 22 | handleOnMouseEnter(event: React.MouseEvent) { 23 | event.preventDefault(); 24 | event.currentTarget.style.backgroundColor = '#e9ecef'; 25 | this.setState({ isMouseOver: true }); 26 | } 27 | 28 | handeOnMouseLeave(event: React.MouseEvent) { 29 | event.preventDefault(); 30 | event.currentTarget.style.backgroundColor = 'white'; 31 | this.setState({ isMouseOver: false }); 32 | } 33 | 34 | handleOnClickOnTrash(event: React.MouseEvent) { 35 | event.preventDefault(); 36 | this.props.deleteItemCallback(this.props.item); 37 | } 38 | 39 | handleOnSubmit(event: React.FormEvent) { 40 | event.preventDefault(); 41 | this.setState({ isMouseOver: false }); 42 | const form = event.target as HTMLFormElement; 43 | const nameOfItemToAdd = form.elements.namedItem('nameOfItemToAdd') as HTMLInputElement; 44 | const quantityOfItemToAdd = form.elements.namedItem('quantityOfItemToAdd') as HTMLInputElement; 45 | const item: CartItem = { 46 | id: this.props.item.id, 47 | name: capitalizeFirstLetter(nameOfItemToAdd.value), 48 | category: this.props.item.category, 49 | quantity: quantityOfItemToAdd.valueAsNumber 50 | }; 51 | this.props.updateItemCallback(item); 52 | } 53 | 54 | render() { 55 | const { item } = this.props; 56 | const { isMouseOver } = this.state; 57 | return ( 58 | 59 | {!isMouseOver && capitalizeFirstLetter(item.name)} 60 | 61 | 74 | 75 | ); 76 | } 77 | } -------------------------------------------------------------------------------- /src/frontend/src/components/ItemList.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | import { ListGroup } from 'react-bootstrap'; 3 | 4 | import { CartItem, Status } from '../models'; 5 | import { AddItemForm, Error, Item, Loader } from '../components'; 6 | import { API_URL } from '../config'; 7 | 8 | interface ItemListProps { 9 | /** 10 | * Callback function to be called when the cart changes to inform the parent component. 11 | * 12 | * @param isCartEmpty - true if the cart is empty, false otherwise 13 | */ 14 | handleCartChangeCallback: (isCartEmpty: boolean) => void; 15 | } 16 | 17 | interface ItemListState { 18 | items: Array; 19 | status: Status; 20 | errorMessage?: string; 21 | } 22 | 23 | export class ItemList extends React.Component { 24 | state: ItemListState = { 25 | items: [] as Array, 26 | status: Status.LOADING, 27 | }; 28 | 29 | componentDidMount(): void { 30 | fetch(`${API_URL}/api/cart-items`) 31 | .then((response) => response.json()) 32 | .then((items) => { 33 | this.setState({ 34 | items: items, 35 | status: Status.LOADED 36 | }); 37 | this.props.handleCartChangeCallback(items.length === 0); 38 | }) 39 | .catch((error) => this.setState({ 40 | items: [], 41 | status: Status.ERROR, 42 | errorMessage: error.message, 43 | })); 44 | } 45 | 46 | public addItemToShoppingCart(item: CartItem): void { 47 | fetch(`${API_URL}/api/cart-items`, { 48 | method: 'POST', 49 | headers: { 50 | 'Content-Type': 'application/json', 51 | }, 52 | body: JSON.stringify(item), 53 | }) 54 | .then((response) => response.json()) 55 | .then((item) => { 56 | this.setState({ 57 | items: [ 58 | ...this.state.items, 59 | item 60 | ], 61 | status: Status.LOADED 62 | }); 63 | this.props.handleCartChangeCallback(false); 64 | }) 65 | .catch((error) => this.setState({ 66 | items: [], 67 | status: Status.ERROR, 68 | errorMessage: error.message, 69 | })); 70 | } 71 | 72 | public updateItemInShoppingCart(item: CartItem): void { 73 | fetch(`${API_URL}/api/cart-items/${item.id}`, { 74 | method: 'PUT', 75 | headers: { 76 | 'Content-Type': 'application/json', 77 | }, 78 | body: JSON.stringify(item), 79 | }) 80 | .then((response) => response.json()) 81 | .then((item) => { 82 | let items = this.state.items.map((i) => i.id === item.id ? item : i); 83 | this.setState({ 84 | items: items, 85 | status: Status.LOADED 86 | }); 87 | this.props.handleCartChangeCallback(false); 88 | }) 89 | .catch((error) => this.setState({ 90 | items: [], 91 | status: Status.ERROR, 92 | errorMessage: error.message, 93 | })); 94 | } 95 | 96 | public removeItemFromShoppingCart(item: CartItem): void { 97 | fetch(`${API_URL}/api/cart-items/${item.id}`, { 98 | method: 'DELETE' 99 | }) 100 | .then((response) => response.text()) 101 | .then((text) => { 102 | let items = this.state.items.filter((i) => i.id !== item.id); 103 | this.setState({ 104 | items: items, 105 | status: Status.LOADED 106 | }); 107 | this.props.handleCartChangeCallback(items.length === 0); 108 | }) 109 | .catch((error) => this.setState({ 110 | items: [], 111 | status: Status.ERROR, 112 | errorMessage: error.message, 113 | })); 114 | } 115 | 116 | public clearShoppingCart(): void { 117 | fetch(`${API_URL}/api/cart-items`, { 118 | method: 'DELETE', 119 | }) 120 | .then((response) => response.text()) 121 | .then((text) => { 122 | this.setState({ 123 | items: [], 124 | status: Status.LOADED 125 | }); 126 | this.props.handleCartChangeCallback(true); 127 | }) 128 | .catch((error) => this.setState({ 129 | items: [], 130 | status: Status.ERROR, 131 | errorMessage: error.message, 132 | })); 133 | } 134 | 135 | render() { 136 | const { items, status } = this.state; 137 | return ( 138 | 139 | {status === Status.LOADING && } 140 | {status === Status.ERROR && } 141 | {status === Status.LOADED && 142 | 143 | {items.length === 0 && Shopping cart is empty.} 144 | {items.map((item: CartItem): ReactElement => ( 145 | 150 | ))} 151 | 152 | 153 | 154 | 155 | } 156 | 157 | ); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/frontend/src/components/Loader.tsx: -------------------------------------------------------------------------------- 1 | import { Spinner } from 'react-bootstrap'; 2 | 3 | export const Loader: React.FC = () => { 4 | return ( 5 |
6 | 7 | Loading... 8 | 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/frontend/src/components/NutriscoreBar.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonGroup } from 'react-bootstrap'; 2 | 3 | import { NutriscoreBarItem } from '../components'; 4 | 5 | interface NutriscoreBarProps { 6 | nutriscore: string; 7 | } 8 | 9 | export const NutriscoreBar: React.FC = ({ nutriscore }: NutriscoreBarProps) => { 10 | return ( 11 | 12 | 16 | 20 | 24 | 28 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/frontend/src/components/NutriscoreBarItem.tsx: -------------------------------------------------------------------------------- 1 | import { ToggleButton } from "react-bootstrap"; 2 | 3 | import { generateId } from "../utils/idUtils"; 4 | 5 | interface NutriscoreBarItemProps { 6 | nutriscoreItemLabel: string; 7 | selectedNutriscore: string; 8 | variant: string 9 | } 10 | 11 | const getNutriscoreColor = (nutriscoreItemLabel: string): string => { 12 | if (nutriscoreItemLabel.toUpperCase() === 'A') { 13 | return '#00803d'; 14 | } 15 | switch (nutriscoreItemLabel.toUpperCase()) { 16 | case 'A': return '#00803d'; 17 | case 'B': return '#86bc25'; 18 | case 'C': return '#ffcc00'; 19 | case 'D': return '#ef7d00'; 20 | case 'E': return '#e63312'; 21 | default: return '#000000'; 22 | } 23 | } 24 | 25 | export const NutriscoreBarItem: React.FC = ({ nutriscoreItemLabel, selectedNutriscore, variant }: NutriscoreBarItemProps) => { 26 | const isSelected: boolean = nutriscoreItemLabel.toUpperCase() === selectedNutriscore.toUpperCase(); 27 | const id: string = generateId(`nutriscore${nutriscoreItemLabel}`); 28 | const color: string = getNutriscoreColor(nutriscoreItemLabel); 29 | const style = isSelected ? 30 | { color: '#fff', backgroundColor: color, borderColor: color } : 31 | { color: color, borderColor: color }; 32 | return ( 33 | 42 | {nutriscoreItemLabel} 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/frontend/src/components/ShoppingCart.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, ButtonGroup, ButtonToolbar, Container } from 'react-bootstrap'; 3 | import { ItemList, AiNutritionAnalysis, Top3Recipes } from '../components'; 4 | 5 | interface ShoppingCartProps { 6 | }; 7 | 8 | enum DisplaySmartRecommendation { 9 | NONE, 10 | AI_NUTRITION_ANALYSIS, 11 | RECIPES 12 | } 13 | 14 | interface ShoppingCartState { 15 | displaySmartRecommendation: DisplaySmartRecommendation; 16 | disableToolbar: boolean; 17 | }; 18 | 19 | export class ShoppingCart extends React.Component { 20 | state: ShoppingCartState = { 21 | displaySmartRecommendation: DisplaySmartRecommendation.NONE as DisplaySmartRecommendation, 22 | disableToolbar: true 23 | }; 24 | 25 | private itemList: ItemList | null = null; 26 | 27 | public handleCartChange = (isCartEmpty: boolean): void => { 28 | if (isCartEmpty) { 29 | this.setState({ 30 | ...this.state, 31 | displaySmartRecommendation: DisplaySmartRecommendation.NONE, 32 | disableToolbar: true 33 | }); 34 | } else { 35 | this.setState({ 36 | ...this.state, 37 | displaySmartRecommendation: DisplaySmartRecommendation.NONE, 38 | disableToolbar: false 39 | }); 40 | } 41 | } 42 | 43 | displayNutriscore = (event: React.MouseEvent): void => { 44 | this.setState({ 45 | ...this.state, 46 | displaySmartRecommendation: DisplaySmartRecommendation.AI_NUTRITION_ANALYSIS 47 | }); 48 | } 49 | 50 | displayRecipes = (event: React.MouseEvent): void => { 51 | this.setState({ 52 | ...this.state, 53 | displaySmartRecommendation: DisplaySmartRecommendation.RECIPES 54 | }); 55 | } 56 | 57 | clearShoppingCart = (event: React.MouseEvent): void => { 58 | if (this.itemList) { 59 | this.itemList.clearShoppingCart(); 60 | } 61 | } 62 | 63 | render() { 64 | const { displaySmartRecommendation, disableToolbar } = this.state; 65 | return ( 66 | <> 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | this.itemList = instance} handleCartChangeCallback={this.handleCartChange} /> 76 | 77 | 78 | {displaySmartRecommendation === DisplaySmartRecommendation.AI_NUTRITION_ANALYSIS && } 79 | {displaySmartRecommendation === DisplaySmartRecommendation.RECIPES && } 80 | 81 | 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/frontend/src/components/Top3Recipes.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Status } from '../models'; 4 | import { Error, Loader, NutriscoreBar } from '.'; 5 | import { Card } from 'react-bootstrap'; 6 | import { API_URL } from '../config'; 7 | 8 | interface RecipesProps { 9 | } 10 | 11 | interface Recipe { 12 | name: string; 13 | ingredients: Array; 14 | instructions: Array; 15 | nutriscore: string; 16 | } 17 | 18 | interface RecipesState { 19 | recipes: Array; 20 | status: Status; 21 | errorMessage?: string; 22 | } 23 | 24 | export class Top3Recipes extends React.Component { 25 | state: RecipesState = { 26 | recipes: [] as Array, 27 | status: Status.LOADING, 28 | }; 29 | 30 | componentDidMount(): void { 31 | let responseOk: boolean = true; 32 | fetch(`${API_URL}/api/cart-items/top-3-recipes`) 33 | .then((response) => { 34 | responseOk = response.ok; 35 | return response.json() 36 | }) 37 | .then((data) => { 38 | if (!responseOk) { 39 | this.setState({ 40 | recipes: [] as Array, 41 | status: Status.ERROR, 42 | errorMessage: data.message 43 | }); 44 | } else { 45 | this.setState({ 46 | recipes: data.recipes, 47 | status: Status.LOADED 48 | }); 49 | } 50 | }) 51 | .catch((error) => this.setState({ 52 | recipes: [] as Array, 53 | status: Status.ERROR, 54 | errorMessage: error.message, 55 | })); 56 | } 57 | 58 | getRecipeHtml(recipe: Recipe): JSX.Element { 59 | return ( 60 | 61 | 62 | {recipe.name} 63 | 64 |
Ingredients
65 |
    {recipe.ingredients.map((ingredient) =>
  • {ingredient}
  • )}
66 |
Instructions
67 | {recipe.instructions.map((instruction) => {instruction})} 68 |
69 |
70 | ); 71 | } 72 | 73 | getHtmlForRecipes(): Array { 74 | if (this.state === undefined || this.state.recipes === undefined || this.state.recipes.length === 0) { 75 | return [No recipes found]; 76 | } 77 | return this.state.recipes.map((recipe) => this.getRecipeHtml(recipe)); 78 | } 79 | 80 | render() { 81 | const { status } = this.state; 82 | return ( 83 | 84 | 85 | Top 3 Recipes 86 | {status === Status.LOADING && } 87 | {status === Status.ERROR && } 88 | {status === Status.LOADED && this.getHtmlForRecipes()} 89 | 90 | 91 | ); 92 | } 93 | } -------------------------------------------------------------------------------- /src/frontend/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AiNutritionAnalysis'; 2 | export * from './AddItemForm'; 3 | export * from './Error'; 4 | export * from './Header'; 5 | export * from './Item'; 6 | export * from './ItemList'; 7 | export * from './Loader'; 8 | export * from './NutriscoreBar'; 9 | export * from './NutriscoreBarItem'; 10 | export * from './ShoppingCart'; 11 | export * from './Top3Recipes'; -------------------------------------------------------------------------------- /src/frontend/src/config/index.ts: -------------------------------------------------------------------------------- 1 | export const API_URL: string = window.ENV_CONFIG.REACT_APP_API_URL as string || 'http://localhost:8080'; -------------------------------------------------------------------------------- /src/frontend/src/index.scss: -------------------------------------------------------------------------------- 1 | // Import bootstrap css 2 | @import '~bootstrap/scss/bootstrap'; 3 | @import '~bootstrap-icons/font/bootstrap-icons'; -------------------------------------------------------------------------------- /src/frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.scss'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot( 8 | document.getElementById('root') as HTMLElement 9 | ); 10 | root.render( 11 | 12 | 13 | 14 | ); 15 | 16 | if (process.env.NODE_ENV !== 'production') { 17 | reportWebVitals(console.log); 18 | } 19 | -------------------------------------------------------------------------------- /src/frontend/src/models/CartItem.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * CartItem interface 3 | * 4 | * @description CartItem interface defines the structure of the cart item 5 | * @export 6 | * @interface CartItem 7 | * @property {number} id - unique id for the item 8 | * @property {string} name - name of the item 9 | * @property {string} category - category of the item 10 | * @property {number} quantity - quantity of the item 11 | */ 12 | export interface CartItem { 13 | id?: number; 14 | name: string; 15 | category: string; 16 | quantity: number; 17 | } -------------------------------------------------------------------------------- /src/frontend/src/models/Status.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Status of the request 3 | * 4 | * @enum {number} 5 | * @export Status 6 | * @property {number} LOADING - loading state 7 | * @property {number} LOADED - loaded state 8 | * @property {number} ERROR - error state 9 | */ 10 | export enum Status { 11 | LOADING, 12 | LOADED, 13 | ERROR 14 | } -------------------------------------------------------------------------------- /src/frontend/src/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CartItem'; 2 | export * from './Status'; -------------------------------------------------------------------------------- /src/frontend/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/frontend/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/frontend/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/frontend/src/utils/idUtils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generates a random id 3 | * 4 | * @param prefix Prefix to add to the id 5 | * @returns A random id 6 | * @example 7 | * generateId('prefix-') // prefix-3j4h5j6 8 | * generateId() // 3j4h5j6 9 | */ 10 | export const generateId = (prefix: string = ''): string => { 11 | const id = Math.random().toString(36).substring(2, 9);; 12 | return `${prefix}${id}`; 13 | } -------------------------------------------------------------------------------- /src/frontend/src/utils/stringUtils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Capitalize the first letter of a string and lowercase the rest 3 | * 4 | * @param str string to capitalize 5 | * @returns string with the first letter capitalized and the rest lowercase 6 | * @example 7 | * capitalizeFirstLetter('hello') // Hello 8 | * capitalizeFirstLetter('HELLO') // Hello 9 | */ 10 | export const capitalizeFirstLetter = (str: string) => { 11 | return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); 12 | } -------------------------------------------------------------------------------- /src/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ], 26 | "exclude": [ 27 | "node_modules" 28 | ] 29 | } 30 | --------------------------------------------------------------------------------