├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── images ├── architecture.png ├── chainlit-access-prompt-playground.png ├── chainlit-before-upload.png ├── chainlit-chain-of-thought.png ├── chainlit-dark-mode.png ├── chainlit-document-reply.png ├── chainlit-markdown-format-result.png ├── chainlit-processing-documents.png ├── chainlit-prompt-playground-question.png ├── chainlit-prompt-playground-reply.png ├── chainlit-prompt-playground-variable.png ├── chainlit-prompt-playground.png ├── chainlit-simple-chat.png ├── chainlit-source.png ├── chainlit-welcome-screen.png ├── containers.png ├── federatedidentitycredentials.png ├── log-stream.png ├── logs.png ├── openai.png └── resources.png ├── src ├── .chainlit │ ├── .langchain.db │ └── config.toml ├── .env ├── .vscode │ └── launch.json ├── 00-variables.sh ├── 01-build-docker-images.sh ├── 02-run-docker-container.sh ├── 03-push-docker-image.sh ├── Dockerfile ├── chainlit.md ├── chat.py ├── doc.py └── requirements.txt ├── terraform ├── apps │ ├── deploy.sh │ ├── main.tf │ ├── modules │ │ └── container_app │ │ │ ├── main.tf │ │ │ ├── outputs.tf │ │ │ └── variables.tf │ ├── outputs.tf │ ├── terraform.tfvars │ └── variables.tf └── infra │ ├── deploy.sh │ ├── main.tf │ ├── modules │ ├── container_app_environment │ │ ├── main.tf │ │ ├── outputs.tf │ │ └── variables.tf │ ├── container_registry │ │ ├── main.tf │ │ ├── outputs.tf │ │ └── variables.tf │ ├── log_analytics │ │ ├── main.tf │ │ ├── output.tf │ │ └── variables.tf │ ├── managed_identity │ │ ├── main.tf │ │ ├── output.tf │ │ └── variables.tf │ ├── openai │ │ ├── main.tf │ │ ├── output.tf │ │ └── variables.tf │ ├── private_dns_zone │ │ ├── main.tf │ │ ├── outputs.tf │ │ └── variables.tf │ ├── private_endpoint │ │ ├── main.tf │ │ ├── outputs.tf │ │ └── variables.tf │ └── virtual_network │ │ ├── main.tf │ │ ├── outputs.tf │ │ └── variables.tf │ ├── outputs.tf │ ├── terraform.tfvars │ └── variables.tf └── visio └── architecture.vsdx /.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 | - [x] 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 | ### OS and Version? 25 | > Windows 7, 8 or 10. Linux (which distribution). macOS (Yosemite? El Capitan? Sierra?) 26 | 27 | ### Versions 28 | > 29 | 30 | ### Mention any other details that might be useful 31 | Adding steps to help the flow of the deployment. 32 | > --------------------------------------------------------------- 33 | > Thanks! We'll be in touch soon. 34 | -------------------------------------------------------------------------------- /.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 | npm install 33 | ``` 34 | 35 | * Test the code 36 | 37 | ``` 38 | ``` 39 | 40 | ## What to Check 41 | Verify that the following are valid 42 | * ... 43 | 44 | ## Other Information 45 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [project-title] Changelog 2 | 3 | 4 | # x.y.z (yyyy-mm-dd) 5 | 6 | *Features* 7 | * ... 8 | 9 | *Bug Fixes* 10 | * ... 11 | 12 | *Breaking Changes* 13 | * ... 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to [project-title] 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/[organization-name]/[repository-name]/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/[organization-name]/[repository-name]/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 master -i 73 | git push -f 74 | ``` 75 | 76 | That's it! Thank you for your contribution! 77 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/container-apps-openai/45a9d8143aba1550a2e473cc911d65b576335966/images/architecture.png -------------------------------------------------------------------------------- /images/chainlit-access-prompt-playground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/container-apps-openai/45a9d8143aba1550a2e473cc911d65b576335966/images/chainlit-access-prompt-playground.png -------------------------------------------------------------------------------- /images/chainlit-before-upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/container-apps-openai/45a9d8143aba1550a2e473cc911d65b576335966/images/chainlit-before-upload.png -------------------------------------------------------------------------------- /images/chainlit-chain-of-thought.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/container-apps-openai/45a9d8143aba1550a2e473cc911d65b576335966/images/chainlit-chain-of-thought.png -------------------------------------------------------------------------------- /images/chainlit-dark-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/container-apps-openai/45a9d8143aba1550a2e473cc911d65b576335966/images/chainlit-dark-mode.png -------------------------------------------------------------------------------- /images/chainlit-document-reply.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/container-apps-openai/45a9d8143aba1550a2e473cc911d65b576335966/images/chainlit-document-reply.png -------------------------------------------------------------------------------- /images/chainlit-markdown-format-result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/container-apps-openai/45a9d8143aba1550a2e473cc911d65b576335966/images/chainlit-markdown-format-result.png -------------------------------------------------------------------------------- /images/chainlit-processing-documents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/container-apps-openai/45a9d8143aba1550a2e473cc911d65b576335966/images/chainlit-processing-documents.png -------------------------------------------------------------------------------- /images/chainlit-prompt-playground-question.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/container-apps-openai/45a9d8143aba1550a2e473cc911d65b576335966/images/chainlit-prompt-playground-question.png -------------------------------------------------------------------------------- /images/chainlit-prompt-playground-reply.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/container-apps-openai/45a9d8143aba1550a2e473cc911d65b576335966/images/chainlit-prompt-playground-reply.png -------------------------------------------------------------------------------- /images/chainlit-prompt-playground-variable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/container-apps-openai/45a9d8143aba1550a2e473cc911d65b576335966/images/chainlit-prompt-playground-variable.png -------------------------------------------------------------------------------- /images/chainlit-prompt-playground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/container-apps-openai/45a9d8143aba1550a2e473cc911d65b576335966/images/chainlit-prompt-playground.png -------------------------------------------------------------------------------- /images/chainlit-simple-chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/container-apps-openai/45a9d8143aba1550a2e473cc911d65b576335966/images/chainlit-simple-chat.png -------------------------------------------------------------------------------- /images/chainlit-source.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/container-apps-openai/45a9d8143aba1550a2e473cc911d65b576335966/images/chainlit-source.png -------------------------------------------------------------------------------- /images/chainlit-welcome-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/container-apps-openai/45a9d8143aba1550a2e473cc911d65b576335966/images/chainlit-welcome-screen.png -------------------------------------------------------------------------------- /images/containers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/container-apps-openai/45a9d8143aba1550a2e473cc911d65b576335966/images/containers.png -------------------------------------------------------------------------------- /images/federatedidentitycredentials.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/container-apps-openai/45a9d8143aba1550a2e473cc911d65b576335966/images/federatedidentitycredentials.png -------------------------------------------------------------------------------- /images/log-stream.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/container-apps-openai/45a9d8143aba1550a2e473cc911d65b576335966/images/log-stream.png -------------------------------------------------------------------------------- /images/logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/container-apps-openai/45a9d8143aba1550a2e473cc911d65b576335966/images/logs.png -------------------------------------------------------------------------------- /images/openai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/container-apps-openai/45a9d8143aba1550a2e473cc911d65b576335966/images/openai.png -------------------------------------------------------------------------------- /images/resources.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/container-apps-openai/45a9d8143aba1550a2e473cc911d65b576335966/images/resources.png -------------------------------------------------------------------------------- /src/.chainlit/.langchain.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/container-apps-openai/45a9d8143aba1550a2e473cc911d65b576335966/src/.chainlit/.langchain.db -------------------------------------------------------------------------------- /src/.chainlit/config.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | # If true (default), the app will be available to anonymous users. 3 | # If false, users will need to authenticate and be part of the project to use the app. 4 | public = true 5 | 6 | # The project ID (found on https://cloud.chainlit.io). 7 | # The project ID is required when public is set to false or when using the cloud database. 8 | #id = "" 9 | 10 | # Uncomment if you want to persist the chats. 11 | # local will create a database in your .chainlit directory (requires node.js installed). 12 | # cloud will use the Chainlit cloud database. 13 | # custom will load use your custom client. 14 | # database = "local" 15 | 16 | # Whether to enable telemetry (default: true). No personal data is collected. 17 | enable_telemetry = true 18 | 19 | # List of environment variables to be provided by each user to use the app. 20 | user_env = [] 21 | 22 | [UI] 23 | # Name of the app and chatbot. 24 | name = "Chatbot" 25 | 26 | # Description of the app and chatbot. This is used for HTML tags. 27 | # description = "" 28 | 29 | # The default value for the expand messages settings. 30 | default_expand_messages = false 31 | 32 | # Hide the chain of thought details from the user in the UI. 33 | hide_cot = false 34 | 35 | # Link to your github repo. This will add a github button in the UI's header. 36 | # github = "" 37 | 38 | [meta] 39 | generated_by = "0.5.2" 40 | -------------------------------------------------------------------------------- /src/.env: -------------------------------------------------------------------------------- 1 | AZURE_OPENAI_TYPE=azure_ad -------------------------------------------------------------------------------- /src/.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 | "name": "Python: Chainlit", 9 | "type": "python", 10 | "request": "launch", 11 | "module": "chainlit", 12 | "args": [ 13 | "run", 14 | "${file}", 15 | "-w" 16 | ], 17 | "console": "integratedTerminal", 18 | "justMyCode": false 19 | }, 20 | ] 21 | } -------------------------------------------------------------------------------- /src/00-variables.sh: -------------------------------------------------------------------------------- 1 | # Variables 2 | 3 | # Azure Container Registry 4 | prefix="Blue" 5 | acrName="${prefix}Registry" 6 | acrResourceGrougName="${prefix}RG" 7 | location="northeurope" 8 | 9 | # Python Files 10 | docAppFile="doc.py" 11 | chatAppFile="chat.py" 12 | 13 | # Docker Images 14 | docImageName="doc" 15 | chatImageName="chat" 16 | tag="v1" 17 | port="8000" 18 | 19 | # Arrays 20 | images=($docImageName $chatImageName) 21 | filenames=($docAppFile $chatAppFile) -------------------------------------------------------------------------------- /src/01-build-docker-images.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Variables 4 | source ./00-variables.sh 5 | 6 | # Use a for loop to build the docker images using the array index 7 | for index in ${!images[@]}; do 8 | # Build the docker image 9 | docker build -t ${images[$index]}:$tag -f Dockerfile --build-arg FILENAME=${filenames[$index]} --build-arg PORT=$port . 10 | done -------------------------------------------------------------------------------- /src/02-run-docker-container.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Variables 4 | source ./00-variables.sh 5 | 6 | # Print the menu 7 | echo "====================================" 8 | echo "Run Docker Container (1-3): " 9 | echo "====================================" 10 | options=( 11 | "Chat" 12 | "Doc" 13 | ) 14 | name="" 15 | # Select an option 16 | COLUMNS=0 17 | select option in "${options[@]}"; do 18 | case $option in 19 | "Chat") 20 | docker run -it \ 21 | --rm \ 22 | -p $port:$port \ 23 | -e AZURE_OPENAI_BASE=$AZURE_OPENAI_BASE \ 24 | -e AZURE_OPENAI_KEY=$AZURE_OPENAI_KEY \ 25 | -e AZURE_OPENAI_MODEL=$AZURE_OPENAI_MODEL \ 26 | -e AZURE_OPENAI_DEPLOYMENT=$AZURE_OPENAI_DEPLOYMENT \ 27 | -e AZURE_OPENAI_VERSION=$AZURE_OPENAI_VERSION \ 28 | -e AZURE_OPENAI_TYPE=$AZURE_OPENAI_TYPE \ 29 | -e TEMPERATURE=$TEMPERATURE \ 30 | --name $chatImageName \ 31 | $chatImageName:$tag 32 | break 33 | ;; 34 | "Doc") 35 | docker run -it \ 36 | --rm \ 37 | -p $port:$port \ 38 | -e AZURE_OPENAI_BASE=$AZURE_OPENAI_BASE \ 39 | -e AZURE_OPENAI_KEY=$AZURE_OPENAI_KEY \ 40 | -e AZURE_OPENAI_MODEL=$AZURE_OPENAI_MODEL \ 41 | -e AZURE_OPENAI_DEPLOYMENT=$AZURE_OPENAI_DEPLOYMENT \ 42 | -e AZURE_OPENAI_ADA_DEPLOYMENT=$AZURE_OPENAI_ADA_DEPLOYMENT \ 43 | -e AZURE_OPENAI_VERSION=$AZURE_OPENAI_VERSION \ 44 | -e AZURE_OPENAI_TYPE=$AZURE_OPENAI_TYPE \ 45 | -e TEMPERATURE=$TEMPERATURE \ 46 | --name $docImageName \ 47 | $docImageName:$tag 48 | break 49 | ;; 50 | "Quit") 51 | exit 52 | ;; 53 | *) echo "invalid option $REPLY" ;; 54 | esac 55 | done -------------------------------------------------------------------------------- /src/03-push-docker-image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Variables 4 | source ./00-variables.sh 5 | 6 | # Login to ACR 7 | echo "Logging in to [${acrName,,}] container registry..." 8 | az acr login --name ${acrName,,} 9 | 10 | # Retrieve ACR login server. Each container image needs to be tagged with the loginServer name of the registry. 11 | echo "Retrieving login server for the [${acrName,,}] container registry..." 12 | loginServer=$(az acr show --name ${acrName,,} --query loginServer --output tsv) 13 | 14 | # Use a for loop to tag and push the local docker images to the Azure Container Registry 15 | for index in ${!images[@]}; do 16 | # Tag the local sender image with the loginServer of ACR 17 | docker tag ${images[$index],,}:$tag $loginServer/${images[$index],,}:$tag 18 | 19 | # Push the container image to ACR 20 | docker push $loginServer/${images[$index],,}:$tag 21 | done -------------------------------------------------------------------------------- /src/Dockerfile: -------------------------------------------------------------------------------- 1 | # app/Dockerfile 2 | 3 | # # Stage 1 - Install build dependencies 4 | 5 | # A Dockerfile must start with a FROM instruction which sets the base image for the container. 6 | # The Python images come in many flavors, each designed for a specific use case. 7 | # The python:3.11-slim image is a good base image for most applications. 8 | # It is a minimal image built on top of Debian Linux and includes only the necessary packages to run Python. 9 | # The slim image is a good choice because it is small and contains only the packages needed to run Python. 10 | # For more information, see: 11 | # * https://hub.docker.com/_/python 12 | # * https://docs.streamlit.io/knowledge-base/tutorials/deploy/docker 13 | FROM python:3.11-slim AS builder 14 | 15 | # The WORKDIR instruction sets the working directory for any RUN, CMD, ENTRYPOINT, COPY and ADD instructions that follow it in the Dockerfile. 16 | # If the WORKDIR doesn’t exist, it will be created even if it’s not used in any subsequent Dockerfile instruction. 17 | # For more information, see: https://docs.docker.com/engine/reference/builder/#workdir 18 | WORKDIR /app 19 | 20 | # Set environment variables. 21 | # The ENV instruction sets the environment variable to the value . 22 | # This value will be in the environment of all “descendant” Dockerfile commands and can be replaced inline in many as well. 23 | # For more information, see: https://docs.docker.com/engine/reference/builder/#env 24 | ENV PYTHONDONTWRITEBYTECODE 1 25 | ENV PYTHONUNBUFFERED 1 26 | 27 | # Install git so that we can clone the app code from a remote repo using the RUN instruction. 28 | # The RUN comand has 2 forms: 29 | # * RUN (shell form, the command is run in a shell, which by default is /bin/sh -c on Linux or cmd /S /C on Windows) 30 | # * RUN ["executable", "param1", "param2"] (exec form) 31 | # The RUN instruction will execute any commands in a new layer on top of the current image and commit the results. 32 | # The resulting committed image will be used for the next step in the Dockerfile. 33 | # For more information, see: https://docs.docker.com/engine/reference/builder/#run 34 | RUN apt-get update && apt-get install -y \ 35 | build-essential \ 36 | curl \ 37 | software-properties-common \ 38 | git \ 39 | && rm -rf /var/lib/apt/lists/* 40 | 41 | # Create a virtualenv to keep dependencies together 42 | RUN python -m venv /opt/venv 43 | ENV PATH="/opt/venv/bin:$PATH" 44 | 45 | # Clone the requirements.txt which contains dependencies to WORKDIR 46 | # COPY has two forms: 47 | # * COPY (this copies the files from the local machine to the container's own filesystem) 48 | # * COPY ["",... ""] (this form is required for paths containing whitespace) 49 | # For more information, see: https://docs.docker.com/engine/reference/builder/#copy 50 | COPY requirements.txt . 51 | 52 | # Install the Python dependencies 53 | RUN pip install --no-cache-dir --no-deps -r requirements.txt 54 | 55 | # Stage 2 - Copy only necessary files to the runner stage 56 | 57 | # The FROM instruction initializes a new build stage for the application 58 | FROM python:3.11-slim 59 | 60 | # Define the filename to copy as an argument 61 | ARG FILENAME 62 | 63 | # Deefine the port to run the application on as an argument 64 | ARG PORT=8000 65 | 66 | # Set an environment variable 67 | ENV FILENAME=${FILENAME} 68 | 69 | # Sets the working directory to /app 70 | WORKDIR /app 71 | 72 | # Copy the virtual environment from the builder stage 73 | COPY --from=builder /opt/venv /opt/venv 74 | 75 | # Set environment variables 76 | ENV PATH="/opt/venv/bin:$PATH" 77 | 78 | # Clone the $FILENAME containing the application code 79 | COPY $FILENAME . 80 | 81 | # Copy the chainlit.md file to the working directory 82 | COPY chainlit.md . 83 | 84 | # Copy the .chainlit folder to the working directory 85 | COPY ./.chainlit ./.chainlit 86 | 87 | # The EXPOSE instruction informs Docker that the container listens on the specified network ports at runtime. 88 | # For more information, see: https://docs.docker.com/engine/reference/builder/#expose 89 | EXPOSE $PORT 90 | 91 | # The ENTRYPOINT instruction has two forms: 92 | # * ENTRYPOINT ["executable", "param1", "param2"] (exec form, preferred) 93 | # * ENTRYPOINT command param1 param2 (shell form) 94 | # The ENTRYPOINT instruction allows you to configure a container that will run as an executable. 95 | # For more information, see: https://docs.docker.com/engine/reference/builder/#entrypoint 96 | CMD chainlit run $FILENAME --port=$PORT -------------------------------------------------------------------------------- /src/chainlit.md: -------------------------------------------------------------------------------- 1 | # Funny Chat 🤖 2 | Hey there, curious minds and merry souls! 🌟 Looking for a burst of laughter and a dash of wit? You've landed in the right spot! Welcome to our funny chat, where the doors to imagination and humor are wide open! 🚪 3 | 4 | ## Useful Links 🔗 5 | This chat is built using [Chainlit](https://github.com/Chainlit/chainlit): 6 | 7 | - **Documentation:** Get started with [Chainlit Documentation](https://docs.chainlit.io) 📚 8 | - **Discord Community:** Join [Chainlit Discord](https://discord.gg/ZThrUxbAYw) to ask questions, share projects, and connect with other developers! 💬 -------------------------------------------------------------------------------- /src/chat.py: -------------------------------------------------------------------------------- 1 | # Import packages 2 | import os 3 | import sys 4 | from openai import AsyncAzureOpenAI 5 | import logging 6 | import chainlit as cl 7 | from azure.identity import DefaultAzureCredential, get_bearer_token_provider 8 | from dotenv import load_dotenv 9 | from dotenv import dotenv_values 10 | 11 | # Load environment variables from .env file 12 | if os.path.exists(".env"): 13 | load_dotenv(override=True) 14 | config = dotenv_values(".env") 15 | 16 | # Read environment variables 17 | temperature = float(os.environ.get("TEMPERATURE", 0.9)) 18 | api_base = os.getenv("AZURE_OPENAI_BASE") 19 | api_key = os.getenv("AZURE_OPENAI_KEY") 20 | api_type = os.environ.get("AZURE_OPENAI_TYPE", "azure") 21 | api_version = os.environ.get("AZURE_OPENAI_VERSION", "2023-12-01-preview") 22 | engine = os.getenv("AZURE_OPENAI_DEPLOYMENT") 23 | model = os.getenv("AZURE_OPENAI_MODEL") 24 | system_content = os.getenv( 25 | "AZURE_OPENAI_SYSTEM_MESSAGE", "You are a helpful assistant." 26 | ) 27 | max_retries = int(os.getenv("MAX_RETRIES", 5)) 28 | timeout = int(os.getenv("TIMEOUT", 30)) 29 | debug = os.getenv("DEBUG", "False").lower() in ("true", "1", "t") 30 | 31 | # Create Token Provider 32 | token_provider = get_bearer_token_provider( 33 | DefaultAzureCredential(), "https://cognitiveservices.azure.com/.default" 34 | ) 35 | 36 | # Configure OpenAI 37 | if api_type == "azure": 38 | openai = AsyncAzureOpenAI( 39 | api_version=api_version, 40 | api_key=api_key, 41 | azure_endpoint=api_base, 42 | max_retries=max_retries, 43 | timeout=timeout, 44 | ) 45 | else: 46 | openai = AsyncAzureOpenAI( 47 | api_version=api_version, 48 | azure_endpoint=api_base, 49 | azure_ad_token_provider=token_provider, 50 | max_retries=max_retries, 51 | timeout=timeout, 52 | ) 53 | 54 | # Configure a logger 55 | logging.basicConfig( 56 | stream=sys.stdout, 57 | format="[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s", 58 | level=logging.INFO, 59 | ) 60 | logger = logging.getLogger(__name__) 61 | 62 | 63 | @cl.on_chat_start 64 | async def start_chat(): 65 | await cl.Avatar( 66 | name="Chatbot", url="https://cdn-icons-png.flaticon.com/512/8649/8649595.png" 67 | ).send() 68 | await cl.Avatar( 69 | name="Error", url="https://cdn-icons-png.flaticon.com/512/8649/8649595.png" 70 | ).send() 71 | await cl.Avatar( 72 | name="You", 73 | url="https://media.architecturaldigest.com/photos/5f241de2c850b2a36b415024/master/w_1600%2Cc_limit/Luke-logo.png", 74 | ).send() 75 | cl.user_session.set( 76 | "message_history", 77 | [{"role": "system", "content": system_content}], 78 | ) 79 | 80 | 81 | @cl.on_message 82 | async def on_message(message: cl.Message): 83 | message_history = cl.user_session.get("message_history") 84 | message_history.append({"role": "user", "content": message.content}) 85 | logger.info("Question: [%s]", message.content) 86 | 87 | # Create the Chainlit response message 88 | msg = cl.Message(content="") 89 | 90 | async for stream_resp in await openai.chat.completions.create( 91 | model=model, 92 | messages=message_history, 93 | temperature=temperature, 94 | stream=True, 95 | ): 96 | if stream_resp and len(stream_resp.choices) > 0: 97 | token = stream_resp.choices[0].delta.content or "" 98 | await msg.stream_token(token) 99 | 100 | if debug: 101 | logger.info("Answer: [%s]", msg.content) 102 | 103 | message_history.append({"role": "assistant", "content": msg.content}) 104 | await msg.send() 105 | -------------------------------------------------------------------------------- /src/doc.py: -------------------------------------------------------------------------------- 1 | # Import packages 2 | import os 3 | import io 4 | import sys 5 | import logging 6 | import chainlit as cl 7 | from chainlit.playground.config import AzureChatOpenAI 8 | from pypdf import PdfReader 9 | from docx import Document 10 | from azure.identity import DefaultAzureCredential, get_bearer_token_provider 11 | from dotenv import load_dotenv 12 | from dotenv import dotenv_values 13 | from langchain.embeddings import AzureOpenAIEmbeddings 14 | from langchain.text_splitter import RecursiveCharacterTextSplitter 15 | from langchain.vectorstores.chroma import Chroma 16 | from langchain.chains import RetrievalQAWithSourcesChain 17 | from langchain.chat_models import AzureChatOpenAI 18 | from langchain.prompts.chat import ( 19 | ChatPromptTemplate, 20 | SystemMessagePromptTemplate, 21 | HumanMessagePromptTemplate, 22 | ) 23 | 24 | # Load environment variables from .env file 25 | if os.path.exists(".env"): 26 | load_dotenv(override=True) 27 | config = dotenv_values(".env") 28 | 29 | # Read environment variables 30 | temperature = float(os.environ.get("TEMPERATURE", 0.9)) 31 | api_base = os.getenv("AZURE_OPENAI_BASE") 32 | api_key = os.getenv("AZURE_OPENAI_KEY") 33 | api_type = os.environ.get("AZURE_OPENAI_TYPE", "azure") 34 | api_version = os.environ.get("AZURE_OPENAI_VERSION", "2023-12-01-preview") 35 | chat_completion_deployment = os.getenv("AZURE_OPENAI_DEPLOYMENT") 36 | embeddings_deployment = os.getenv("AZURE_OPENAI_ADA_DEPLOYMENT") 37 | model = os.getenv("AZURE_OPENAI_MODEL") 38 | max_size_mb = int(os.getenv("CHAINLIT_MAX_SIZE_MB", 100)) 39 | max_files = int(os.getenv("CHAINLIT_MAX_FILES", 10)) 40 | max_files = int(os.getenv("CHAINLIT_MAX_FILES", 10)) 41 | text_splitter_chunk_size = int(os.getenv("TEXT_SPLITTER_CHUNK_SIZE", 1000)) 42 | text_splitter_chunk_overlap = int(os.getenv("TEXT_SPLITTER_CHUNK_OVERLAP", 10)) 43 | embeddings_chunk_size = int(os.getenv("EMBEDDINGS_CHUNK_SIZE", 16)) 44 | max_retries = int(os.getenv("MAX_RETRIES", 5)) 45 | retry_min_seconds = int(os.getenv("RETRY_MIN_SECONDS", 1)) 46 | retry_max_seconds = int(os.getenv("RETRY_MAX_SECONDS", 5)) 47 | timeout = int(os.getenv("TIMEOUT", 30)) 48 | debug = os.getenv("DEBUG", "False").lower() in ("true", "1", "t") 49 | 50 | # Configure system prompt 51 | system_template = """Use the following pieces of context to answer the users question. 52 | If you don't know the answer, just say that you don't know, don't try to make up an answer. 53 | ALWAYS return a "SOURCES" part in your answer. 54 | The "SOURCES" part should be a reference to the source of the document from which you got your answer. 55 | 56 | Example of your response should be: 57 | 58 | ``` 59 | The answer is foo 60 | SOURCES: xyz 61 | ``` 62 | 63 | Begin! 64 | ---------------- 65 | {summaries}""" 66 | messages = [ 67 | SystemMessagePromptTemplate.from_template(system_template), 68 | HumanMessagePromptTemplate.from_template("{question}"), 69 | ] 70 | prompt = ChatPromptTemplate.from_messages(messages) 71 | chain_type_kwargs = {"prompt": prompt} 72 | 73 | # Configure a logger 74 | logging.basicConfig( 75 | stream=sys.stdout, 76 | format="[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s", 77 | level=logging.INFO, 78 | ) 79 | logger = logging.getLogger(__name__) 80 | 81 | # Create Token Provider 82 | if api_type == "azure_ad": 83 | token_provider = get_bearer_token_provider( 84 | DefaultAzureCredential(), "https://cognitiveservices.azure.com/.default" 85 | ) 86 | 87 | # Setting the environment variables for the playground 88 | if api_type == "azure": 89 | os.environ["AZURE_OPENAI_API_KEY"] = api_key 90 | os.environ["AZURE_OPENAI_API_VERSION"] = api_version 91 | os.environ["AZURE_OPENAI_ENDPOINT"] = api_base 92 | os.environ["AZURE_OPENAI_DEPLOYMENT_NAME"] = chat_completion_deployment 93 | 94 | 95 | @cl.on_chat_start 96 | async def start(): 97 | await cl.Avatar( 98 | name="Chatbot", url="https://cdn-icons-png.flaticon.com/512/8649/8649595.png" 99 | ).send() 100 | await cl.Avatar( 101 | name="Error", url="https://cdn-icons-png.flaticon.com/512/8649/8649595.png" 102 | ).send() 103 | await cl.Avatar( 104 | name="You", 105 | url="https://media.architecturaldigest.com/photos/5f241de2c850b2a36b415024/master/w_1600%2Cc_limit/Luke-logo.png", 106 | ).send() 107 | 108 | # Initialize the file list to None 109 | files = None 110 | 111 | # Wait for the user to upload a file 112 | while files == None: 113 | files = await cl.AskFileMessage( 114 | content=f"Please upload up to {max_files} `.pdf` or `.docx` files to begin.", 115 | accept=[ 116 | "application/pdf", 117 | "application/vnd.openxmlformats-officedocument.wordprocessingml.document", 118 | ], 119 | max_size_mb=max_size_mb, 120 | max_files=max_files, 121 | timeout=86400, 122 | raise_on_timeout=False, 123 | ).send() 124 | 125 | # Create a message to inform the user that the files are being processed 126 | content = "" 127 | if len(files) == 1: 128 | content = f"Processing `{files[0].name}`..." 129 | else: 130 | files_names = [f"`{f.name}`" for f in files] 131 | content = f"Processing {', '.join(files_names)}..." 132 | logger.info(content) 133 | msg = cl.Message(content=content, author="Chatbot") 134 | await msg.send() 135 | 136 | # Create a list to store the texts of each file 137 | all_texts = [] 138 | 139 | # Process each file uplodaded by the user 140 | for file in files: 141 | # Read file contents 142 | with open(file.path, "rb") as uploaded_file: 143 | file_contents = uploaded_file.read() 144 | 145 | logger.info("[%d] bytes were read from %s", 146 | len(file_contents), file.path) 147 | 148 | # Create an in-memory buffer from the file content 149 | bytes = io.BytesIO(file_contents) 150 | 151 | # Get file extension 152 | extension = file.name.split(".")[-1] 153 | 154 | # Initialize the text variable 155 | text = "" 156 | 157 | # Read the file 158 | if extension == "pdf": 159 | reader = PdfReader(bytes) 160 | for i in range(len(reader.pages)): 161 | text += reader.pages[i].extract_text() 162 | if debug: 163 | logger.info("[%s] read from %s", text, file.path) 164 | elif extension == "docx": 165 | doc = Document(bytes) 166 | paragraph_list = [] 167 | for paragraph in doc.paragraphs: 168 | paragraph_list.append(paragraph.text) 169 | if debug: 170 | logger.info("[%s] read from %s", paragraph.text, file.path) 171 | text = "\n".join(paragraph_list) 172 | 173 | # Split the text into chunks 174 | text_splitter = RecursiveCharacterTextSplitter( 175 | chunk_size=text_splitter_chunk_size, 176 | chunk_overlap=text_splitter_chunk_overlap, 177 | ) 178 | texts = text_splitter.split_text(text) 179 | 180 | # Add the chunks and metadata to the list 181 | all_texts.extend(texts) 182 | 183 | # Create a metadata for each chunk 184 | metadatas = [{"source": f"{i}-pl"} for i in range(len(all_texts))] 185 | 186 | # Create a Chroma vector store 187 | if api_type == "azure": 188 | embeddings = AzureOpenAIEmbeddings( 189 | openai_api_version=api_version, 190 | openai_api_type=api_type, 191 | openai_api_key=api_key, 192 | azure_endpoint=api_base, 193 | azure_deployment=embeddings_deployment, 194 | max_retries=max_retries, 195 | retry_min_seconds=retry_min_seconds, 196 | retry_max_seconds=retry_max_seconds, 197 | chunk_size=embeddings_chunk_size, 198 | timeout=timeout, 199 | ) 200 | else: 201 | embeddings = AzureOpenAIEmbeddings( 202 | openai_api_version=api_version, 203 | openai_api_type=api_type, 204 | azure_endpoint=api_base, 205 | azure_ad_token_provider=token_provider, 206 | azure_deployment=embeddings_deployment, 207 | max_retries=max_retries, 208 | retry_min_seconds=retry_min_seconds, 209 | retry_max_seconds=retry_max_seconds, 210 | chunk_size=embeddings_chunk_size, 211 | timeout=timeout, 212 | ) 213 | 214 | # Create a Chroma vector store 215 | db = await cl.make_async(Chroma.from_texts)( 216 | all_texts, embeddings, metadatas=metadatas 217 | ) 218 | 219 | # Create an AzureChatOpenAI llm 220 | if api_type == "azure": 221 | llm = AzureChatOpenAI( 222 | openai_api_type=api_type, 223 | openai_api_version=api_version, 224 | openai_api_key=api_key, 225 | azure_endpoint=api_base, 226 | temperature=temperature, 227 | azure_deployment=chat_completion_deployment, 228 | streaming=True, 229 | max_retries=max_retries, 230 | timeout=timeout, 231 | ) 232 | else: 233 | llm = AzureChatOpenAI( 234 | openai_api_type=api_type, 235 | openai_api_version=api_version, 236 | azure_endpoint=api_base, 237 | api_key=api_key, 238 | temperature=temperature, 239 | azure_deployment=chat_completion_deployment, 240 | azure_ad_token_provider=token_provider, 241 | streaming=True, 242 | max_retries=max_retries, 243 | timeout=timeout, 244 | ) 245 | 246 | # Create a chain that uses the Chroma vector store 247 | chain = RetrievalQAWithSourcesChain.from_chain_type( 248 | llm=llm, 249 | chain_type="stuff", 250 | retriever=db.as_retriever(), 251 | return_source_documents=True, 252 | chain_type_kwargs=chain_type_kwargs, 253 | ) 254 | 255 | # Save the metadata and texts in the user session 256 | cl.user_session.set("metadatas", metadatas) 257 | cl.user_session.set("texts", all_texts) 258 | 259 | # Create a message to inform the user that the files are ready for queries 260 | content = "" 261 | if len(files) == 1: 262 | content = f"`{files[0].name}` processed. You can now ask questions!" 263 | logger.info(content) 264 | else: 265 | files_names = [f"`{f.name}`" for f in files] 266 | content = f"{', '.join(files_names)} processed. You can now ask questions." 267 | logger.info(content) 268 | msg.content = content 269 | msg.author = "Chatbot" 270 | await msg.update() 271 | 272 | # Store the chain in the user session 273 | cl.user_session.set("chain", chain) 274 | 275 | 276 | @cl.on_message 277 | async def main(message: cl.Message): 278 | # Retrieve the chain from the user session 279 | chain = cl.user_session.get("chain") 280 | 281 | # Create a callback handler 282 | cb = cl.AsyncLangchainCallbackHandler() 283 | 284 | # Get the response from the chain 285 | response = await chain.acall(message.content, callbacks=[cb]) 286 | logger.info("Question: [%s]", message.content) 287 | 288 | # Get the answer and sources from the response 289 | answer = response["answer"] 290 | sources = response["sources"].strip() 291 | source_elements = [] 292 | 293 | if debug: 294 | logger.info("Answer: [%s]", answer) 295 | 296 | # Get the metadata and texts from the user session 297 | metadatas = cl.user_session.get("metadatas") 298 | all_sources = [m["source"] for m in metadatas] 299 | texts = cl.user_session.get("texts") 300 | 301 | if sources: 302 | found_sources = [] 303 | 304 | # Add the sources to the message 305 | for source in sources.split(","): 306 | source_name = source.strip().replace(".", "") 307 | # Get the index of the source 308 | try: 309 | index = all_sources.index(source_name) 310 | except ValueError: 311 | continue 312 | text = texts[index] 313 | found_sources.append(source_name) 314 | # Create the text element referenced in the message 315 | source_elements.append(cl.Text(content=text, name=source_name)) 316 | 317 | if found_sources: 318 | answer += f"\nSources: {', '.join(found_sources)}" 319 | else: 320 | answer += "\nNo sources found" 321 | 322 | await cl.Message(content=answer, elements=source_elements).send() 323 | 324 | # Setting the AZURE_OPENAI_API_KEY environment variable for the playground 325 | if api_type == "azure_ad": 326 | os.environ["AZURE_OPENAI_API_KEY"] = token_provider() 327 | -------------------------------------------------------------------------------- /src/requirements.txt: -------------------------------------------------------------------------------- 1 | aiofiles==23.2.1 2 | aiohttp==3.9.1 3 | aiosignal==1.3.1 4 | annotated-types==0.6.0 5 | anyio==3.7.1 6 | asgiref==3.7.2 7 | async-timeout==4.0.3 8 | asyncer==0.0.2 9 | attrs==23.1.0 10 | auth0-python==3.24.1 11 | azure-core==1.29.6 12 | azure-identity==1.15.0 13 | backoff==2.2.1 14 | bcrypt==4.1.2 15 | bidict==0.22.1 16 | cachetools==5.3.2 17 | certifi==2023.11.17 18 | cffi==1.16.0 19 | chainlit==1.0.200 20 | charset-normalizer==3.3.2 21 | chroma-hnswlib==0.7.3 22 | chromadb==0.4.20 23 | click==8.1.7 24 | coloredlogs==15.0.1 25 | cryptography==41.0.7 26 | dataclasses-json==0.5.14 27 | Deprecated==1.2.14 28 | distro==1.8.0 29 | exceptiongroup==1.2.0 30 | fastapi==0.100.1 31 | fastapi-socketio==0.0.10 32 | filelock==3.13.1 33 | filetype==1.2.0 34 | flatbuffers==23.5.26 35 | frozenlist==1.4.1 36 | fsspec==2023.12.2 37 | google-auth==2.25.2 38 | googleapis-common-protos==1.62.0 39 | greenlet==3.0.2 40 | grpcio==1.60.0 41 | h11==0.14.0 42 | httpcore==0.17.3 43 | httptools==0.6.1 44 | httpx==0.24.1 45 | huggingface-hub==0.19.4 46 | humanfriendly==10.0 47 | idna==3.6 48 | importlib-metadata==6.11.0 49 | importlib-resources==6.1.1 50 | jsonpatch==1.33 51 | jsonpointer==2.4 52 | kubernetes==28.1.0 53 | langchain==0.0.351 54 | langchain-community==0.0.4 55 | langchain-core==0.1.1 56 | langsmith==0.0.70 57 | Lazify==0.4.0 58 | literalai==0.0.103 59 | lxml==4.9.3 60 | marshmallow==3.20.1 61 | mmh3==4.0.1 62 | monotonic==1.6 63 | mpmath==1.3.0 64 | msal==1.26.0 65 | msal-extensions==1.1.0 66 | multidict==6.0.4 67 | mypy-extensions==1.0.0 68 | nest-asyncio==1.5.8 69 | numpy==1.26.2 70 | oauthlib==3.2.2 71 | onnxruntime==1.16.3 72 | openai==1.4.0 73 | opentelemetry-api==1.22.0 74 | opentelemetry-exporter-otlp==1.22.0 75 | opentelemetry-exporter-otlp-proto-common==1.22.0 76 | opentelemetry-exporter-otlp-proto-grpc==1.22.0 77 | opentelemetry-exporter-otlp-proto-http==1.22.0 78 | opentelemetry-instrumentation==0.43b0 79 | opentelemetry-instrumentation-asgi==0.42b0 80 | opentelemetry-instrumentation-fastapi==0.42b0 81 | opentelemetry-proto==1.22.0 82 | opentelemetry-sdk==1.22.0 83 | opentelemetry-semantic-conventions==0.43b0 84 | opentelemetry-util-http==0.42b0 85 | overrides==7.4.0 86 | packaging==23.2 87 | Pillow==10.1.0 88 | portalocker==2.8.2 89 | posthog==3.1.0 90 | protobuf==4.25.1 91 | pulsar-client==3.3.0 92 | pyasn1==0.5.1 93 | pyasn1-modules==0.3.0 94 | pycparser==2.21 95 | pydantic==2.5.2 96 | pydantic_core==2.14.5 97 | PyJWT==2.8.0 98 | pyOpenSSL==23.3.0 99 | pypdf==3.17.3 100 | PyPika==0.48.9 101 | python-dateutil==2.8.2 102 | python-docx==1.1.0 103 | python-dotenv==1.0.0 104 | python-engineio==4.8.0 105 | python-graphql-client==0.4.3 106 | python-multipart==0.0.6 107 | python-socketio==5.10.0 108 | PyYAML==6.0.1 109 | regex==2023.10.3 110 | requests==2.31.0 111 | requests-oauthlib==1.3.1 112 | rsa==4.9 113 | simple-websocket==1.0.0 114 | six==1.16.0 115 | sniffio==1.3.0 116 | SQLAlchemy==2.0.23 117 | starlette==0.27.0 118 | sympy==1.12 119 | syncer==2.0.3 120 | tenacity==8.2.3 121 | tiktoken==0.5.2 122 | tokenizers==0.15.0 123 | tomli==2.0.1 124 | tqdm==4.66.1 125 | typer==0.9.0 126 | typing-inspect==0.9.0 127 | typing_extensions==4.9.0 128 | uptrace==1.22.0 129 | urllib3==1.26.18 130 | uvicorn==0.25.0 131 | uvloop==0.19.0 132 | watchfiles==0.20.0 133 | websocket-client==1.7.0 134 | websockets==12.0 135 | wrapt==1.16.0 136 | wsproto==1.2.0 137 | yarl==1.9.4 138 | zipp==3.17.0 139 | -------------------------------------------------------------------------------- /terraform/apps/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Terraform Init 4 | terraform init 5 | 6 | # Terraform validate 7 | terraform validate -compact-warnings 8 | 9 | # Terraform plan 10 | terraform plan -compact-warnings 11 | 12 | # Terraform apply 13 | terraform apply -compact-warnings -------------------------------------------------------------------------------- /terraform/apps/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.3" 3 | required_providers { 4 | azurerm = { 5 | source = "hashicorp/azurerm" 6 | version = "~> 3.65" 7 | } 8 | } 9 | } 10 | 11 | provider "azurerm" { 12 | features {} 13 | } 14 | 15 | data "azurerm_client_config" "current" { 16 | } 17 | 18 | module "container_apps" { 19 | source = "./modules/container_app" 20 | resource_group_name = var.resource_group_name 21 | container_app_environment_name = var.container_app_environment_name 22 | container_registry_name = var.container_registry_name 23 | workload_managed_identity_name = var.workload_managed_identity_name 24 | container_apps = var.container_apps 25 | container_app_secrets = var.container_app_secrets 26 | tags = var.tags 27 | } -------------------------------------------------------------------------------- /terraform/apps/modules/container_app/main.tf: -------------------------------------------------------------------------------- 1 | data "azurerm_container_app_environment" "container_app_environment" { 2 | name = var.container_app_environment_name 3 | resource_group_name = var.resource_group_name 4 | } 5 | 6 | data "azurerm_container_registry" "container_registry" { 7 | name = var.container_registry_name 8 | resource_group_name = var.resource_group_name 9 | } 10 | 11 | data "azurerm_user_assigned_identity" "workload_user_assigned_identity" { 12 | name = var.workload_managed_identity_name 13 | resource_group_name = var.resource_group_name 14 | } 15 | 16 | locals { 17 | identity = { 18 | type = "UserAssigned" 19 | identity_ids = [data.azurerm_user_assigned_identity.workload_user_assigned_identity.id] 20 | } 21 | identity_env = { 22 | name = "AZURE_CLIENT_ID" 23 | secret_name = null 24 | value = data.azurerm_user_assigned_identity.workload_user_assigned_identity.client_id 25 | } 26 | registry = { 27 | server = data.azurerm_container_registry.container_registry.login_server 28 | identity = data.azurerm_user_assigned_identity.workload_user_assigned_identity.id 29 | } 30 | } 31 | 32 | resource "azurerm_container_app" "container_app" { 33 | for_each = {for app in var.container_apps: app.name => app} 34 | 35 | container_app_environment_id = data.azurerm_container_app_environment.container_app_environment.id 36 | name = each.key 37 | resource_group_name = var.resource_group_name 38 | revision_mode = each.value.revision_mode 39 | tags = each.value.tags 40 | 41 | template { 42 | max_replicas = each.value.template.max_replicas 43 | min_replicas = each.value.template.min_replicas 44 | revision_suffix = each.value.template.revision_suffix 45 | 46 | dynamic "container" { 47 | for_each = each.value.template.containers 48 | 49 | content { 50 | cpu = container.value.cpu 51 | image = "${data.azurerm_container_registry.container_registry.login_server}/${container.value.image}" 52 | memory = container.value.memory 53 | name = container.value.name 54 | args = container.value.args 55 | command = container.value.command 56 | 57 | dynamic "env" { 58 | for_each = container.value.env == null ? [local.identity_env] : concat(container.value.env, [local.identity_env]) 59 | 60 | content { 61 | name = env.value.name 62 | secret_name = env.value.secret_name 63 | value = env.value.value 64 | } 65 | } 66 | 67 | dynamic "liveness_probe" { 68 | for_each = container.value.liveness_probe == null ? [] : [container.value.liveness_probe] 69 | 70 | content { 71 | port = liveness_probe.value.port 72 | transport = liveness_probe.value.transport 73 | failure_count_threshold = liveness_probe.value.failure_count_threshold 74 | host = liveness_probe.value.host 75 | initial_delay = liveness_probe.value.initial_delay 76 | interval_seconds = liveness_probe.value.interval_seconds 77 | path = liveness_probe.value.path 78 | timeout = liveness_probe.value.timeout 79 | 80 | dynamic "header" { 81 | for_each = liveness_probe.value.header == null ? [] : [liveness_probe.value.header] 82 | 83 | content { 84 | name = header.value.name 85 | value = header.value.value 86 | } 87 | } 88 | } 89 | } 90 | 91 | dynamic "readiness_probe" { 92 | for_each = container.value.readiness_probe == null ? [] : [container.value.readiness_probe] 93 | 94 | content { 95 | port = readiness_probe.value.port 96 | transport = readiness_probe.value.transport 97 | failure_count_threshold = readiness_probe.value.failure_count_threshold 98 | host = readiness_probe.value.host 99 | interval_seconds = readiness_probe.value.interval_seconds 100 | path = readiness_probe.value.path 101 | success_count_threshold = readiness_probe.value.success_count_threshold 102 | timeout = readiness_probe.value.timeout 103 | 104 | dynamic "header" { 105 | for_each = readiness_probe.value.header == null ? [] : [readiness_probe.value.header] 106 | 107 | content { 108 | name = header.value.name 109 | value = header.value.value 110 | } 111 | } 112 | } 113 | } 114 | 115 | dynamic "startup_probe" { 116 | for_each = container.value.startup_probe == null ? [] : [container.value.startup_probe] 117 | 118 | content { 119 | port = startup_probe.value.port 120 | transport = startup_probe.value.transport 121 | failure_count_threshold = startup_probe.value.failure_count_threshold 122 | host = startup_probe.value.host 123 | interval_seconds = startup_probe.value.interval_seconds 124 | path = startup_probe.value.path 125 | timeout = startup_probe.value.timeout 126 | 127 | dynamic "header" { 128 | for_each = startup_probe.value.header == null ? [] : [startup_probe.value.header] 129 | 130 | content { 131 | name = header.value.name 132 | value = header.value.name 133 | } 134 | } 135 | } 136 | } 137 | 138 | dynamic "volume_mounts" { 139 | for_each = container.value.volume_mounts == null ? [] : [container.value.volume_mounts] 140 | 141 | content { 142 | name = volume_mounts.value.name 143 | path = volume_mounts.value.path 144 | } 145 | } 146 | } 147 | } 148 | 149 | dynamic "volume" { 150 | for_each = each.value.template.volume == null ? [] : each.value.template.volume 151 | 152 | content { 153 | name = volume.value.name 154 | storage_name = volume.value.storage_name 155 | storage_type = volume.value.storage_type 156 | } 157 | } 158 | } 159 | 160 | dynamic "dapr" { 161 | for_each = each.value.dapr == null ? [] : [each.value.dapr] 162 | 163 | content { 164 | app_id = dapr.value.app_id 165 | app_port = dapr.value.app_port 166 | app_protocol = dapr.value.app_protocol 167 | } 168 | } 169 | 170 | dynamic "identity" { 171 | for_each = each.value.identity == null ? [local.identity] : [each.value.identity] 172 | 173 | content { 174 | type = identity.value.type 175 | identity_ids = identity.value.identity_ids 176 | } 177 | } 178 | 179 | dynamic "ingress" { 180 | for_each = each.value.ingress == null ? [] : [each.value.ingress] 181 | 182 | content { 183 | target_port = ingress.value.target_port 184 | allow_insecure_connections = ingress.value.allow_insecure_connections 185 | external_enabled = ingress.value.external_enabled 186 | transport = ingress.value.transport 187 | 188 | dynamic "traffic_weight" { 189 | for_each = ingress.value.traffic_weight == null ? [] : [ingress.value.traffic_weight] 190 | 191 | content { 192 | percentage = traffic_weight.value.percentage 193 | label = traffic_weight.value.label 194 | latest_revision = traffic_weight.value.latest_revision 195 | revision_suffix = traffic_weight.value.revision_suffix 196 | } 197 | } 198 | } 199 | } 200 | 201 | dynamic "registry" { 202 | for_each = each.value.registry == null ? [local.registry] : concat(each.value.registry, [local.registry]) 203 | 204 | content { 205 | server = registry.value.server 206 | identity = registry.value.identity 207 | } 208 | } 209 | 210 | dynamic "secret" { 211 | for_each = nonsensitive(toset([for pair in lookup(var.container_app_secrets, each.key, []) : pair.name])) 212 | 213 | content { 214 | name = secret.key 215 | value = local.container_app_secrets[each.key][secret.key] 216 | } 217 | } 218 | } -------------------------------------------------------------------------------- /terraform/apps/modules/container_app/outputs.tf: -------------------------------------------------------------------------------- 1 | output "container_app_fqdn" { 2 | description = "The FQDN of the Latest Revision of the Container App." 3 | value = { for name, container_app in azurerm_container_app.container_app : name => "https://${container_app.latest_revision_fqdn}" } 4 | } -------------------------------------------------------------------------------- /terraform/apps/modules/container_app/variables.tf: -------------------------------------------------------------------------------- 1 | 2 | variable "container_app_environment_name" { 3 | description = "(Required) Specifies the name of the managed environment." 4 | type = string 5 | } 6 | 7 | variable "container_registry_name" { 8 | description = "Specifies the name of the container registry" 9 | type = string 10 | default = "Registry" 11 | } 12 | 13 | variable "workload_managed_identity_name" { 14 | description = "(Required) Specifies the name of the workload user-defined managed identity" 15 | type = string 16 | default = "WorkloadIdentity" 17 | } 18 | 19 | variable "resource_group_name" { 20 | description = "(Required) Specifies the resource group name" 21 | type = string 22 | } 23 | 24 | variable "tags" { 25 | description = "(Optional) Specifies the tags of the log analytics workspace" 26 | type = map(any) 27 | default = {} 28 | } 29 | 30 | variable "container_apps" { 31 | type = list(object({ 32 | name = string 33 | tags = optional(map(string)) 34 | revision_mode = string 35 | 36 | template = object({ 37 | containers = set(object({ 38 | name = string 39 | image = string 40 | args = optional(list(string)) 41 | command = optional(list(string)) 42 | cpu = string 43 | memory = string 44 | env = optional(list(object({ 45 | name = string 46 | secret_name = optional(string) 47 | value = optional(string) 48 | }))) 49 | liveness_probe = optional(object({ 50 | failure_count_threshold = optional(number) 51 | header = optional(object({ 52 | name = string 53 | value = string 54 | })) 55 | host = optional(string) 56 | initial_delay = optional(number, 1) 57 | interval_seconds = optional(number, 10) 58 | path = optional(string) 59 | port = number 60 | timeout = optional(number, 1) 61 | transport = string 62 | })) 63 | readiness_probe = optional(object({ 64 | failure_count_threshold = optional(number) 65 | header = optional(object({ 66 | name = string 67 | value = string 68 | })) 69 | host = optional(string) 70 | interval_seconds = optional(number, 10) 71 | path = optional(string) 72 | port = number 73 | success_count_threshold = optional(number, 3) 74 | timeout = optional(number) 75 | transport = string 76 | })) 77 | startup_probe = optional(object({ 78 | failure_count_threshold = optional(number) 79 | header = optional(object({ 80 | name = string 81 | value = string 82 | })) 83 | host = optional(string) 84 | interval_seconds = optional(number, 10) 85 | path = optional(string) 86 | port = number 87 | timeout = optional(number) 88 | transport = string 89 | })) 90 | volume_mounts = optional(object({ 91 | name = string 92 | path = string 93 | })) 94 | })) 95 | max_replicas = optional(number) 96 | min_replicas = optional(number) 97 | revision_suffix = optional(string) 98 | 99 | volume = optional(set(object({ 100 | name = string 101 | storage_name = optional(string) 102 | storage_type = optional(string) 103 | }))) 104 | }) 105 | 106 | ingress = optional(object({ 107 | allow_insecure_connections = optional(bool, false) 108 | external_enabled = optional(bool, false) 109 | target_port = number 110 | transport = optional(string) 111 | traffic_weight = object({ 112 | label = optional(string) 113 | latest_revision = optional(string) 114 | revision_suffix = optional(string) 115 | percentage = number 116 | }) 117 | })) 118 | 119 | identity = optional(object({ 120 | type = string 121 | identity_ids = optional(list(string)) 122 | })) 123 | 124 | dapr = optional(object({ 125 | app_id = string 126 | app_port = number 127 | app_protocol = optional(string) 128 | })) 129 | 130 | registry = optional(list(object({ 131 | server = string 132 | username = optional(string) 133 | password_secret_name = optional(string) 134 | identity = optional(string) 135 | }))) 136 | })) 137 | description = "The container apps to deploy." 138 | nullable = false 139 | 140 | validation { 141 | condition = length(var.container_apps) >= 1 142 | error_message = "At least one container should be provided." 143 | } 144 | } 145 | 146 | variable "container_app_secrets" { 147 | type = map(list(object({ 148 | name = string 149 | value = string 150 | }))) 151 | default = {} 152 | description = "(Optional) The secrets of the container apps. The key of the map should be aligned with the corresponding container app." 153 | nullable = false 154 | sensitive = true 155 | } -------------------------------------------------------------------------------- /terraform/apps/outputs.tf: -------------------------------------------------------------------------------- 1 | output "container_app_fqdn" { 2 | description = "The FQDN of the Latest Revision of the Container Apps." 3 | value = module.container_apps.container_app_fqdn 4 | } -------------------------------------------------------------------------------- /terraform/apps/terraform.tfvars: -------------------------------------------------------------------------------- 1 | resource_group_name = "BlueRG" 2 | container_app_environment_name = "BlueEnvironment" 3 | container_registry_name = "BlueRegistry" 4 | workload_managed_identity_name = "BlueWorkloadIdentity" 5 | container_apps = [ 6 | { 7 | name = "chatapp" 8 | revision_mode = "Single" 9 | ingress = { 10 | allow_insecure_connections = true 11 | external_enabled = true 12 | target_port = 8000 13 | transport = "http" 14 | traffic_weight = { 15 | label = "default" 16 | latest_revision = true 17 | revision_suffix = "default" 18 | percentage = 100 19 | } 20 | } 21 | template = { 22 | containers = [ 23 | { 24 | name = "chat" 25 | image = "chat:v1" 26 | cpu = 0.5 27 | memory = "1Gi" 28 | env = [ 29 | { 30 | name = "TEMPERATURE" 31 | value = 0.9 32 | }, 33 | { 34 | name = "AZURE_OPENAI_BASE" 35 | value = "https://blueopenai.openai.azure.com/" 36 | }, 37 | { 38 | name = "AZURE_OPENAI_KEY" 39 | value = "" 40 | }, 41 | { 42 | name = "AZURE_OPENAI_TYPE" 43 | value = "azure_ad" 44 | }, 45 | { 46 | name = "AZURE_OPENAI_VERSION" 47 | value = "2023-06-01-preview" 48 | }, 49 | { 50 | name = "AZURE_OPENAI_DEPLOYMENT" 51 | value = "gpt-35-turbo-16k" 52 | }, 53 | { 54 | name = "AZURE_OPENAI_MODEL" 55 | value = "gpt-35-turbo-16k" 56 | }, 57 | { 58 | name = "AZURE_OPENAI_SYSTEM_MESSAGE" 59 | value = "You are a helpful assistant." 60 | }, 61 | { 62 | name = "MAX_RETRIES" 63 | value = 5 64 | }, 65 | { 66 | name = "BACKOFF_IN_SECONDS" 67 | value = "1" 68 | }, 69 | { 70 | name = "TOKEN_REFRESH_INTERVAL" 71 | value = 2700 72 | } 73 | ] 74 | liveness_probe = { 75 | failure_count_threshold = 3 76 | initial_delay = 30 77 | interval_seconds = 60 78 | path = "/" 79 | port = 8000 80 | timeout = 30 81 | transport = "HTTP" 82 | } 83 | readiness_probe = { 84 | failure_count_threshold = 3 85 | interval_seconds = 60 86 | path = "/" 87 | port = 8000 88 | success_count_threshold = 3 89 | timeout = 30 90 | transport = "HTTP" 91 | } 92 | startup_probe = { 93 | failure_count_threshold = 3 94 | interval_seconds = 60 95 | path = "/" 96 | port = 8000 97 | timeout = 30 98 | transport = "HTTP" 99 | } 100 | } 101 | ] 102 | min_replicas = 1 103 | max_replicas = 3 104 | } 105 | }, 106 | { 107 | name = "docapp" 108 | revision_mode = "Single" 109 | ingress = { 110 | allow_insecure_connections = true 111 | external_enabled = true 112 | target_port = 8000 113 | transport = "http" 114 | traffic_weight = { 115 | label = "default" 116 | latest_revision = true 117 | revision_suffix = "default" 118 | percentage = 100 119 | } 120 | } 121 | template = { 122 | containers = [ 123 | { 124 | name = "doc" 125 | image = "doc:v1" 126 | cpu = 0.5 127 | memory = "1Gi" 128 | env = [ 129 | { 130 | name = "TEMPERATURE" 131 | value = 0.9 132 | }, 133 | { 134 | name = "AZURE_OPENAI_BASE" 135 | value = "https://blueopenai.openai.azure.com/" 136 | }, 137 | { 138 | name = "AZURE_OPENAI_KEY" 139 | value = "" 140 | }, 141 | { 142 | name = "AZURE_OPENAI_TYPE" 143 | value = "azure_ad" 144 | }, 145 | { 146 | name = "AZURE_OPENAI_VERSION" 147 | value = "2023-06-01-preview" 148 | }, 149 | { 150 | name = "AZURE_OPENAI_DEPLOYMENT" 151 | value = "gpt-35-turbo-16k" 152 | }, 153 | { 154 | name = "AZURE_OPENAI_MODEL" 155 | value = "gpt-35-turbo-16k" 156 | }, 157 | { 158 | name = "AZURE_OPENAI_ADA_DEPLOYMENT" 159 | value = "text-embedding-ada-002" 160 | }, 161 | { 162 | name = "AZURE_OPENAI_SYSTEM_MESSAGE" 163 | value = "You are a helpful assistant." 164 | }, 165 | { 166 | name = "MAX_RETRIES" 167 | value = 5 168 | }, 169 | { 170 | name = "CHAINLIT_MAX_FILES" 171 | value = 10 172 | }, 173 | { 174 | name = "TEXT_SPLITTER_CHUNK_SIZE" 175 | value = 1000 176 | }, 177 | { 178 | name = "TEXT_SPLITTER_CHUNK_OVERLAP" 179 | value = 10 180 | }, 181 | { 182 | name = "EMBEDDINGS_CHUNK_SIZE" 183 | value = 16 184 | }, 185 | { 186 | name = "BACKOFF_IN_SECONDS" 187 | value = "1" 188 | }, 189 | { 190 | name = "CHAINLIT_MAX_SIZE_MB" 191 | value = 100 192 | }, 193 | { 194 | name = "TOKEN_REFRESH_INTERVAL" 195 | value = 2700 196 | } 197 | ] 198 | liveness_probe = { 199 | failure_count_threshold = 3 200 | initial_delay = 30 201 | interval_seconds = 60 202 | path = "/" 203 | port = 8000 204 | timeout = 30 205 | transport = "HTTP" 206 | } 207 | readiness_probe = { 208 | failure_count_threshold = 3 209 | interval_seconds = 60 210 | path = "/" 211 | port = 8000 212 | success_count_threshold = 3 213 | timeout = 30 214 | transport = "HTTP" 215 | } 216 | startup_probe = { 217 | failure_count_threshold = 3 218 | interval_seconds = 60 219 | path = "/" 220 | port = 8000 221 | timeout = 30 222 | transport = "HTTP" 223 | } 224 | } 225 | ] 226 | min_replicas = 1 227 | max_replicas = 3 228 | } 229 | }] -------------------------------------------------------------------------------- /terraform/apps/variables.tf: -------------------------------------------------------------------------------- 1 | variable "resource_group_name" { 2 | description = "Name of the resource group in which the resources will be created" 3 | default = "RG" 4 | } 5 | 6 | variable "tags" { 7 | description = "(Optional) Specifies tags for all the resources" 8 | default = { 9 | createdWith = "Terraform", 10 | openAi = "true", 11 | containerApps = "true" 12 | } 13 | } 14 | 15 | variable "container_app_environment_name" { 16 | description = "(Required) Specifies the name of the Azure Container Apps Environment." 17 | type = string 18 | default = "Environment" 19 | } 20 | 21 | variable "container_registry_name" { 22 | description = "Specifies the name of the container registry" 23 | type = string 24 | default = "Registry" 25 | } 26 | 27 | variable "workload_managed_identity_name" { 28 | description = "(Required) Specifies the name of the workload user-defined managed identity" 29 | type = string 30 | default = "WorkloadIdentity" 31 | } 32 | 33 | variable "container_apps" { 34 | type = list(object({ 35 | name = string 36 | tags = optional(map(string)) 37 | revision_mode = string 38 | 39 | template = object({ 40 | containers = set(object({ 41 | name = string 42 | image = string 43 | args = optional(list(string)) 44 | command = optional(list(string)) 45 | cpu = string 46 | memory = string 47 | env = optional(list(object({ 48 | name = string 49 | secret_name = optional(string) 50 | value = optional(string) 51 | }))) 52 | liveness_probe = optional(object({ 53 | failure_count_threshold = optional(number) 54 | header = optional(object({ 55 | name = string 56 | value = string 57 | })) 58 | host = optional(string) 59 | initial_delay = optional(number, 1) 60 | interval_seconds = optional(number, 10) 61 | path = optional(string) 62 | port = number 63 | timeout = optional(number, 1) 64 | transport = string 65 | })) 66 | readiness_probe = optional(object({ 67 | failure_count_threshold = optional(number) 68 | header = optional(object({ 69 | name = string 70 | value = string 71 | })) 72 | host = optional(string) 73 | interval_seconds = optional(number, 10) 74 | path = optional(string) 75 | port = number 76 | success_count_threshold = optional(number, 3) 77 | timeout = optional(number) 78 | transport = string 79 | })) 80 | startup_probe = optional(object({ 81 | failure_count_threshold = optional(number) 82 | header = optional(object({ 83 | name = string 84 | value = string 85 | })) 86 | host = optional(string) 87 | interval_seconds = optional(number, 10) 88 | path = optional(string) 89 | port = number 90 | timeout = optional(number) 91 | transport = string 92 | })) 93 | volume_mounts = optional(object({ 94 | name = string 95 | path = string 96 | })) 97 | })) 98 | max_replicas = optional(number) 99 | min_replicas = optional(number) 100 | revision_suffix = optional(string) 101 | 102 | volume = optional(set(object({ 103 | name = string 104 | storage_name = optional(string) 105 | storage_type = optional(string) 106 | }))) 107 | }) 108 | 109 | ingress = optional(object({ 110 | allow_insecure_connections = optional(bool, false) 111 | external_enabled = optional(bool, false) 112 | target_port = number 113 | transport = optional(string) 114 | traffic_weight = object({ 115 | label = optional(string) 116 | latest_revision = optional(string) 117 | revision_suffix = optional(string) 118 | percentage = number 119 | }) 120 | })) 121 | 122 | identity = optional(object({ 123 | type = string 124 | identity_ids = optional(list(string)) 125 | })) 126 | 127 | dapr = optional(object({ 128 | app_id = string 129 | app_port = number 130 | app_protocol = optional(string) 131 | })) 132 | 133 | registry = optional(list(object({ 134 | server = string 135 | username = optional(string) 136 | password_secret_name = optional(string) 137 | identity = optional(string) 138 | }))) 139 | })) 140 | description = "The container apps to deploy." 141 | nullable = false 142 | 143 | validation { 144 | condition = length(var.container_apps) >= 1 145 | error_message = "At least one container should be provided." 146 | } 147 | } 148 | 149 | variable "container_app_secrets" { 150 | type = map(list(object({ 151 | name = string 152 | value = string 153 | }))) 154 | default = {} 155 | description = "(Optional) The secrets of the container apps. The key of the map should be aligned with the corresponding container app." 156 | nullable = false 157 | sensitive = true 158 | } -------------------------------------------------------------------------------- /terraform/infra/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Terraform Init 4 | terraform init 5 | 6 | # Terraform validate 7 | terraform validate -compact-warnings 8 | 9 | # Terraform plan 10 | terraform plan -compact-warnings 11 | 12 | # Terraform apply 13 | terraform apply -compact-warnings -------------------------------------------------------------------------------- /terraform/infra/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.3" 3 | required_providers { 4 | azurerm = { 5 | source = "hashicorp/azurerm" 6 | version = "~> 3.65" 7 | } 8 | } 9 | } 10 | 11 | provider "azurerm" { 12 | features {} 13 | } 14 | 15 | data "azurerm_client_config" "current" { 16 | } 17 | 18 | resource "random_string" "prefix" { 19 | length = 6 20 | special = false 21 | upper = false 22 | numeric = false 23 | } 24 | 25 | resource "azurerm_resource_group" "rg" { 26 | name = var.name_prefix == null ? "${random_string.prefix.result}${var.resource_group_name}" : "${var.name_prefix}${var.resource_group_name}" 27 | location = var.location 28 | tags = var.tags 29 | } 30 | 31 | module "log_analytics_workspace" { 32 | source = "./modules/log_analytics" 33 | name = var.name_prefix == null ? "${random_string.prefix.result}${var.log_analytics_workspace_name}" : "${var.name_prefix}${var.log_analytics_workspace_name}" 34 | location = var.location 35 | resource_group_name = azurerm_resource_group.rg.name 36 | tags = var.tags 37 | } 38 | 39 | module "virtual_network" { 40 | source = "./modules/virtual_network" 41 | resource_group_name = azurerm_resource_group.rg.name 42 | vnet_name = var.name_prefix == null ? "${random_string.prefix.result}${var.vnet_name}" : "${var.name_prefix}${var.vnet_name}" 43 | location = var.location 44 | address_space = var.vnet_address_space 45 | tags = var.tags 46 | log_analytics_workspace_id = module.log_analytics_workspace.id 47 | log_analytics_retention_days = var.log_analytics_retention_days 48 | 49 | subnets = [ 50 | { 51 | name : var.aca_subnet_name 52 | address_prefixes : var.aca_subnet_address_prefix 53 | private_endpoint_network_policies_enabled : true 54 | private_link_service_network_policies_enabled : false 55 | }, 56 | { 57 | name : var.private_endpoint_subnet_name 58 | address_prefixes : var.private_endpoint_subnet_address_prefix 59 | private_endpoint_network_policies_enabled : true 60 | private_link_service_network_policies_enabled : false 61 | } 62 | ] 63 | } 64 | 65 | module "acr_private_dns_zone" { 66 | source = "./modules/private_dns_zone" 67 | name = "privatelink.azurecr.io" 68 | resource_group_name = azurerm_resource_group.rg.name 69 | tags = var.tags 70 | virtual_networks_to_link = { 71 | (module.virtual_network.name) = { 72 | subscription_id = data.azurerm_client_config.current.subscription_id 73 | resource_group_name = azurerm_resource_group.rg.name 74 | } 75 | } 76 | } 77 | 78 | module "openai_private_dns_zone" { 79 | source = "./modules/private_dns_zone" 80 | name = "privatelink.openai.azure.com" 81 | resource_group_name = azurerm_resource_group.rg.name 82 | tags = var.tags 83 | virtual_networks_to_link = { 84 | (module.virtual_network.name) = { 85 | subscription_id = data.azurerm_client_config.current.subscription_id 86 | resource_group_name = azurerm_resource_group.rg.name 87 | } 88 | } 89 | } 90 | 91 | module "openai_private_endpoint" { 92 | source = "./modules/private_endpoint" 93 | name = "${module.openai.name}PrivateEndpoint" 94 | location = var.location 95 | resource_group_name = azurerm_resource_group.rg.name 96 | subnet_id = module.virtual_network.subnet_ids[var.private_endpoint_subnet_name] 97 | tags = var.tags 98 | private_connection_resource_id = module.openai.id 99 | is_manual_connection = false 100 | subresource_name = "account" 101 | private_dns_zone_group_name = "OpenAiPrivateDnsZoneGroup" 102 | private_dns_zone_group_ids = [module.openai_private_dns_zone.id] 103 | } 104 | 105 | module "acr_private_endpoint" { 106 | source = "./modules/private_endpoint" 107 | name = "${module.container_registry.name}PrivateEndpoint" 108 | location = var.location 109 | resource_group_name = azurerm_resource_group.rg.name 110 | subnet_id = module.virtual_network.subnet_ids[var.private_endpoint_subnet_name] 111 | tags = var.tags 112 | private_connection_resource_id = module.container_registry.id 113 | is_manual_connection = false 114 | subresource_name = "registry" 115 | private_dns_zone_group_name = "AcrPrivateDnsZoneGroup" 116 | private_dns_zone_group_ids = [module.acr_private_dns_zone.id] 117 | } 118 | 119 | module "openai" { 120 | source = "./modules/openai" 121 | name = var.name_prefix == null ? "${random_string.prefix.result}${var.openai_name}" : "${var.name_prefix}${var.openai_name}" 122 | location = var.location 123 | resource_group_name = azurerm_resource_group.rg.name 124 | sku_name = var.openai_sku_name 125 | tags = var.tags 126 | deployments = var.openai_deployments 127 | custom_subdomain_name = var.openai_custom_subdomain_name == "" || var.openai_custom_subdomain_name == null ? var.name_prefix == null ? lower("${random_string.prefix.result}${var.openai_name}") : lower("${var.name_prefix}${var.openai_name}") : lower(var.openai_custom_subdomain_name) 128 | public_network_access_enabled = var.openai_public_network_access_enabled 129 | log_analytics_workspace_id = module.log_analytics_workspace.id 130 | log_analytics_retention_days = var.log_analytics_retention_days 131 | } 132 | 133 | module "container_registry" { 134 | source = "./modules/container_registry" 135 | name = var.name_prefix == null ? "${random_string.prefix.result}${var.acr_name}" : "${var.name_prefix}${var.acr_name}" 136 | resource_group_name = azurerm_resource_group.rg.name 137 | location = var.location 138 | sku = var.acr_sku 139 | admin_enabled = var.acr_admin_enabled 140 | georeplication_locations = var.acr_georeplication_locations 141 | log_analytics_workspace_id = module.log_analytics_workspace.id 142 | log_analytics_retention_days = var.log_analytics_retention_days 143 | tags = var.tags 144 | 145 | } 146 | 147 | module "workload_managed_identity" { 148 | source = "./modules/managed_identity" 149 | name = var.name_prefix == null ? "${random_string.prefix.result}${var.workload_managed_identity_name}" : "${var.name_prefix}${var.workload_managed_identity_name}" 150 | resource_group_name = azurerm_resource_group.rg.name 151 | location = var.location 152 | openai_id = module.openai.id 153 | acr_id = module.container_registry.id 154 | tags = var.tags 155 | } 156 | 157 | module "container_app_environment" { 158 | source = "./modules/container_app_environment" 159 | name = var.name_prefix == null ? "${random_string.prefix.result}${var.container_app_environment_name}" : "${var.name_prefix}${var.container_app_environment_name}" 160 | location = var.location 161 | resource_group_name = azurerm_resource_group.rg.name 162 | tags = var.tags 163 | infrastructure_subnet_id = module.virtual_network.subnet_ids[var.aca_subnet_name] 164 | internal_load_balancer_enabled = var.internal_load_balancer_enabled 165 | log_analytics_workspace_id = module.log_analytics_workspace.id 166 | } -------------------------------------------------------------------------------- /terraform/infra/modules/container_app_environment/main.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_container_app_environment" "container_app_environment" { 2 | name = var.name 3 | location = var.location 4 | resource_group_name = var.resource_group_name 5 | log_analytics_workspace_id = var.log_analytics_workspace_id 6 | infrastructure_subnet_id = var.infrastructure_subnet_id 7 | internal_load_balancer_enabled = var.internal_load_balancer_enabled 8 | tags = var.tags 9 | 10 | lifecycle { 11 | ignore_changes = [ 12 | tags 13 | ] 14 | } 15 | } -------------------------------------------------------------------------------- /terraform/infra/modules/container_app_environment/outputs.tf: -------------------------------------------------------------------------------- 1 | output "name" { 2 | value = azurerm_container_app_environment.container_app_environment.name 3 | description = "Specifies the name of the Azure Container Apps Environment." 4 | } 5 | 6 | output "id" { 7 | value = azurerm_container_app_environment.container_app_environment.id 8 | description = "Specifies the resource id of the Azure Container Apps Environment." 9 | } -------------------------------------------------------------------------------- /terraform/infra/modules/container_app_environment/variables.tf: -------------------------------------------------------------------------------- 1 | 2 | variable "name" { 3 | description = "(Required) Specifies the name of the managed environment." 4 | type = string 5 | } 6 | 7 | variable "resource_group_name" { 8 | description = "(Required) Specifies the resource group name" 9 | type = string 10 | } 11 | 12 | variable "tags" { 13 | description = "(Optional) Specifies the tags of the log analytics workspace" 14 | type = map(any) 15 | default = {} 16 | } 17 | 18 | variable "location" { 19 | description = "(Required) Specifies the supported Azure location where the resource exists. Changing this forces a new resource to be created." 20 | type = string 21 | } 22 | 23 | variable "infrastructure_subnet_id" { 24 | description = "(Optional) Specifies resource id of the subnet hosting the Azure Container Apps environment." 25 | type = string 26 | } 27 | 28 | variable "internal_load_balancer_enabled" { 29 | description = "(Optional) Should the Container Environment operate in Internal Load Balancing Mode? Defaults to false. Changing this forces a new resource to be created." 30 | type = bool 31 | default = false 32 | } 33 | 34 | variable "log_analytics_workspace_id" { 35 | description = "(Optional) The resource id of the Log Analytics Workspace used by the Container Apps Managed Environment." 36 | type = string 37 | } -------------------------------------------------------------------------------- /terraform/infra/modules/container_registry/main.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_container_registry" "acr" { 2 | name = var.name 3 | resource_group_name = var.resource_group_name 4 | location = var.location 5 | sku = var.sku 6 | admin_enabled = var.admin_enabled 7 | tags = var.tags 8 | 9 | identity { 10 | type = "UserAssigned" 11 | identity_ids = [ 12 | azurerm_user_assigned_identity.acr_identity.id 13 | ] 14 | } 15 | 16 | dynamic "georeplications" { 17 | for_each = var.georeplication_locations 18 | 19 | content { 20 | location = georeplications.value 21 | tags = var.tags 22 | } 23 | } 24 | 25 | lifecycle { 26 | ignore_changes = [ 27 | tags 28 | ] 29 | } 30 | } 31 | 32 | resource "azurerm_user_assigned_identity" "acr_identity" { 33 | resource_group_name = var.resource_group_name 34 | location = var.location 35 | tags = var.tags 36 | 37 | name = "${var.name}Identity" 38 | 39 | lifecycle { 40 | ignore_changes = [ 41 | tags 42 | ] 43 | } 44 | } 45 | 46 | resource "azurerm_monitor_diagnostic_setting" "settings" { 47 | name = "DiagnosticsSettings" 48 | target_resource_id = azurerm_container_registry.acr.id 49 | log_analytics_workspace_id = var.log_analytics_workspace_id 50 | 51 | enabled_log { 52 | category = "ContainerRegistryRepositoryEvents" 53 | 54 | retention_policy { 55 | enabled = true 56 | days = var.log_analytics_retention_days 57 | } 58 | } 59 | 60 | enabled_log { 61 | category = "ContainerRegistryLoginEvents" 62 | 63 | retention_policy { 64 | enabled = true 65 | days = var.log_analytics_retention_days 66 | } 67 | } 68 | 69 | metric { 70 | category = "AllMetrics" 71 | 72 | retention_policy { 73 | enabled = true 74 | days = var.log_analytics_retention_days 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /terraform/infra/modules/container_registry/outputs.tf: -------------------------------------------------------------------------------- 1 | output "name" { 2 | description = "Specifies the name of the container registry." 3 | value = azurerm_container_registry.acr.name 4 | } 5 | 6 | output "id" { 7 | description = "Specifies the resource id of the container registry." 8 | value = azurerm_container_registry.acr.id 9 | } 10 | 11 | output "resource_group_name" { 12 | description = "Specifies the name of the resource group." 13 | value = var.resource_group_name 14 | } 15 | 16 | output "login_server" { 17 | description = "Specifies the login server of the container registry." 18 | value = azurerm_container_registry.acr.login_server 19 | } 20 | 21 | output "login_server_url" { 22 | description = "Specifies the login server url of the container registry." 23 | value = "https://${azurerm_container_registry.acr.login_server}" 24 | } 25 | 26 | output "admin_username" { 27 | description = "Specifies the admin username of the container registry." 28 | value = azurerm_container_registry.acr.admin_username 29 | } 30 | -------------------------------------------------------------------------------- /terraform/infra/modules/container_registry/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | description = "(Required) Specifies the name of the Container Registry. Changing this forces a new resource to be created." 3 | type = string 4 | } 5 | 6 | variable "resource_group_name" { 7 | description = "(Required) The name of the resource group in which to create the Container Registry. Changing this forces a new resource to be created." 8 | type = string 9 | } 10 | 11 | variable "location" { 12 | description = "(Required) Specifies the supported Azure location where the resource exists. Changing this forces a new resource to be created." 13 | type = string 14 | } 15 | 16 | variable "admin_enabled" { 17 | description = "(Optional) Specifies whether the admin user is enabled. Defaults to false." 18 | type = string 19 | default = false 20 | } 21 | 22 | variable "sku" { 23 | description = "(Optional) The SKU name of the container registry. Possible values are Basic, Standard and Premium. Defaults to Basic" 24 | type = string 25 | default = "Basic" 26 | 27 | validation { 28 | condition = contains(["Basic", "Standard", "Premium"], var.sku) 29 | error_message = "The container registry sku is invalid." 30 | } 31 | } 32 | 33 | variable "tags" { 34 | description = "(Optional) A mapping of tags to assign to the resource." 35 | type = map(any) 36 | default = {} 37 | } 38 | 39 | variable "georeplication_locations" { 40 | description = "(Optional) A list of Azure locations where the container registry should be geo-replicated." 41 | type = list(string) 42 | default = [] 43 | } 44 | 45 | variable "log_analytics_workspace_id" { 46 | description = "Specifies the log analytics workspace id" 47 | type = string 48 | } 49 | 50 | variable "log_analytics_retention_days" { 51 | description = "Specifies the number of days of the retention policy" 52 | type = number 53 | default = 7 54 | } -------------------------------------------------------------------------------- /terraform/infra/modules/log_analytics/main.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_log_analytics_workspace" "log_analytics_workspace" { 2 | name = var.name 3 | location = var.location 4 | resource_group_name = var.resource_group_name 5 | sku = var.sku 6 | tags = var.tags 7 | retention_in_days = var.retention_in_days != "" ? var.retention_in_days : null 8 | 9 | lifecycle { 10 | ignore_changes = [ 11 | tags 12 | ] 13 | } 14 | } 15 | 16 | resource "azurerm_log_analytics_solution" "la_solution" { 17 | for_each = var.solution_plan_map 18 | 19 | solution_name = each.key 20 | location = var.location 21 | resource_group_name = var.resource_group_name 22 | workspace_resource_id = azurerm_log_analytics_workspace.log_analytics_workspace.id 23 | workspace_name = azurerm_log_analytics_workspace.log_analytics_workspace.name 24 | 25 | plan { 26 | product = each.value.product 27 | publisher = each.value.publisher 28 | } 29 | 30 | lifecycle { 31 | ignore_changes = [ 32 | tags 33 | ] 34 | } 35 | } -------------------------------------------------------------------------------- /terraform/infra/modules/log_analytics/output.tf: -------------------------------------------------------------------------------- 1 | output "id" { 2 | value = azurerm_log_analytics_workspace.log_analytics_workspace.id 3 | description = "Specifies the resource id of the log analytics workspace" 4 | } 5 | 6 | output "location" { 7 | value = azurerm_log_analytics_workspace.log_analytics_workspace.location 8 | description = "Specifies the location of the log analytics workspace" 9 | } 10 | 11 | output "name" { 12 | value = azurerm_log_analytics_workspace.log_analytics_workspace.name 13 | description = "Specifies the name of the log analytics workspace" 14 | } 15 | 16 | output "resource_group_name" { 17 | value = azurerm_log_analytics_workspace.log_analytics_workspace.resource_group_name 18 | description = "Specifies the name of the resource group that contains the log analytics workspace" 19 | } 20 | 21 | output "workspace_id" { 22 | value = azurerm_log_analytics_workspace.log_analytics_workspace.workspace_id 23 | description = "Specifies the workspace id of the log analytics workspace" 24 | } 25 | 26 | output "primary_shared_key" { 27 | value = azurerm_log_analytics_workspace.log_analytics_workspace.primary_shared_key 28 | description = "Specifies the workspace key of the log analytics workspace" 29 | sensitive = true 30 | } 31 | -------------------------------------------------------------------------------- /terraform/infra/modules/log_analytics/variables.tf: -------------------------------------------------------------------------------- 1 | variable "resource_group_name" { 2 | description = "(Required) Specifies the resource group name" 3 | type = string 4 | } 5 | 6 | variable "location" { 7 | description = "(Required) Specifies the location of the log analytics workspace" 8 | type = string 9 | } 10 | 11 | variable "name" { 12 | description = "(Required) Specifies the name of the log analytics workspace" 13 | type = string 14 | } 15 | 16 | variable "sku" { 17 | description = "(Optional) Specifies the sku of the log analytics workspace" 18 | type = string 19 | default = "PerGB2018" 20 | 21 | validation { 22 | condition = contains(["Free", "Standalone", "PerNode", "PerGB2018"], var.sku) 23 | error_message = "The log analytics sku is incorrect." 24 | } 25 | } 26 | 27 | variable "solution_plan_map" { 28 | description = "(Optional) Specifies the map structure containing the list of solutions to be enabled." 29 | type = map(any) 30 | default = {} 31 | } 32 | 33 | variable "tags" { 34 | description = "(Optional) Specifies the tags of the log analytics workspace" 35 | type = map(any) 36 | default = {} 37 | } 38 | 39 | variable "retention_in_days" { 40 | description = " (Optional) Specifies the workspace data retention in days. Possible values are either 7 (Free Tier only) or range between 30 and 730." 41 | type = number 42 | default = 30 43 | } -------------------------------------------------------------------------------- /terraform/infra/modules/managed_identity/main.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_user_assigned_identity" "workload_user_assigned_identity" { 2 | name = var.name 3 | resource_group_name = var.resource_group_name 4 | location = var.location 5 | tags = var.tags 6 | 7 | lifecycle { 8 | ignore_changes = [ 9 | tags 10 | ] 11 | } 12 | } 13 | 14 | resource "azurerm_role_assignment" "cognitive_services_user_assignment" { 15 | scope = var.openai_id 16 | role_definition_name = "Cognitive Services User" 17 | principal_id = azurerm_user_assigned_identity.workload_user_assigned_identity.principal_id 18 | skip_service_principal_aad_check = true 19 | } 20 | 21 | resource "azurerm_role_assignment" "acr_pull_assignment" { 22 | scope = var.acr_id 23 | role_definition_name = "AcrPull" 24 | principal_id = azurerm_user_assigned_identity.workload_user_assigned_identity.principal_id 25 | skip_service_principal_aad_check = true 26 | } -------------------------------------------------------------------------------- /terraform/infra/modules/managed_identity/output.tf: -------------------------------------------------------------------------------- 1 | output "id" { 2 | value = azurerm_user_assigned_identity.workload_user_assigned_identity.id 3 | description = "Specifies the resource id of the workload user-defined managed identity" 4 | } 5 | 6 | output "location" { 7 | value = azurerm_user_assigned_identity.workload_user_assigned_identity.location 8 | description = "Specifies the location of the workload user-defined managed identity" 9 | } 10 | 11 | output "name" { 12 | value = azurerm_user_assigned_identity.workload_user_assigned_identity.name 13 | description = "Specifies the name of the workload user-defined managed identity" 14 | } 15 | -------------------------------------------------------------------------------- /terraform/infra/modules/managed_identity/variables.tf: -------------------------------------------------------------------------------- 1 | variable "resource_group_name" { 2 | description = "(Required) Specifies the resource group name" 3 | type = string 4 | } 5 | 6 | variable "location" { 7 | description = "(Required) Specifies the location of the log analytics workspace" 8 | type = string 9 | } 10 | 11 | variable "name" { 12 | description = "(Required) Specifies the name of the log analytics workspace" 13 | type = string 14 | } 15 | 16 | variable "openai_id" { 17 | description = "(Required) Specifies resource id of the Azure OpenAI Service resource" 18 | type = string 19 | } 20 | 21 | variable "acr_id" { 22 | description = "(Required) Specifies resource id of the Azure Container Registry resource" 23 | type = string 24 | } 25 | 26 | variable "tags" { 27 | description = "(Optional) Specifies the tags of the log analytics workspace" 28 | type = map(any) 29 | default = {} 30 | } -------------------------------------------------------------------------------- /terraform/infra/modules/openai/main.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_cognitive_account" "openai" { 2 | name = var.name 3 | location = var.location 4 | resource_group_name = var.resource_group_name 5 | kind = "OpenAI" 6 | custom_subdomain_name = var.custom_subdomain_name 7 | sku_name = var.sku_name 8 | public_network_access_enabled = var.public_network_access_enabled 9 | tags = var.tags 10 | 11 | identity { 12 | type = "SystemAssigned" 13 | } 14 | 15 | lifecycle { 16 | ignore_changes = [ 17 | tags 18 | ] 19 | } 20 | } 21 | 22 | resource "azurerm_cognitive_deployment" "deployment" { 23 | for_each = {for deployment in var.deployments: deployment.name => deployment} 24 | 25 | name = each.key 26 | cognitive_account_id = azurerm_cognitive_account.openai.id 27 | 28 | model { 29 | format = "OpenAI" 30 | name = each.value.model.name 31 | version = each.value.model.version 32 | } 33 | 34 | scale { 35 | type = "Standard" 36 | } 37 | } 38 | 39 | resource "azurerm_monitor_diagnostic_setting" "settings" { 40 | name = "DiagnosticsSettings" 41 | target_resource_id = azurerm_cognitive_account.openai.id 42 | log_analytics_workspace_id = var.log_analytics_workspace_id 43 | 44 | enabled_log { 45 | category = "Audit" 46 | 47 | retention_policy { 48 | enabled = true 49 | days = var.log_analytics_retention_days 50 | } 51 | } 52 | 53 | enabled_log { 54 | category = "RequestResponse" 55 | 56 | retention_policy { 57 | enabled = true 58 | days = var.log_analytics_retention_days 59 | } 60 | } 61 | 62 | enabled_log { 63 | category = "Trace" 64 | 65 | retention_policy { 66 | enabled = true 67 | days = var.log_analytics_retention_days 68 | } 69 | } 70 | 71 | metric { 72 | category = "AllMetrics" 73 | 74 | retention_policy { 75 | enabled = true 76 | days = var.log_analytics_retention_days 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /terraform/infra/modules/openai/output.tf: -------------------------------------------------------------------------------- 1 | output "id" { 2 | value = azurerm_cognitive_account.openai.id 3 | description = "Specifies the resource id of the log analytics workspace" 4 | } 5 | 6 | output "location" { 7 | value = azurerm_cognitive_account.openai.location 8 | description = "Specifies the location of the log analytics workspace" 9 | } 10 | 11 | output "name" { 12 | value = azurerm_cognitive_account.openai.name 13 | description = "Specifies the name of the log analytics workspace" 14 | } 15 | 16 | output "resource_group_name" { 17 | value = azurerm_cognitive_account.openai.resource_group_name 18 | description = "Specifies the name of the resource group that contains the log analytics workspace" 19 | } 20 | 21 | output "endpoint" { 22 | value = azurerm_cognitive_account.openai.endpoint 23 | description = "Specifies the endpoint of the Azure OpenAI Service." 24 | } 25 | 26 | output "primary_access_key" { 27 | value = azurerm_cognitive_account.openai.primary_access_key 28 | sensitive = true 29 | description = "Specifies the primary access key of the Azure OpenAI Service." 30 | } 31 | 32 | output "secondary_access_key" { 33 | value = azurerm_cognitive_account.openai.secondary_access_key 34 | sensitive = true 35 | description = "Specifies the secondary access key of the Azure OpenAI Service." 36 | } -------------------------------------------------------------------------------- /terraform/infra/modules/openai/variables.tf: -------------------------------------------------------------------------------- 1 | variable "resource_group_name" { 2 | description = "(Required) Specifies the resource group name" 3 | type = string 4 | } 5 | 6 | variable "location" { 7 | description = "(Required) Specifies the location of the Azure OpenAI Service" 8 | type = string 9 | } 10 | 11 | variable "name" { 12 | description = "(Required) Specifies the name of the Azure OpenAI Service" 13 | type = string 14 | } 15 | 16 | variable "sku_name" { 17 | description = "(Optional) Specifies the sku name for the Azure OpenAI Service" 18 | type = string 19 | default = "S0" 20 | } 21 | 22 | variable "tags" { 23 | description = "(Optional) Specifies the tags of the Azure OpenAI Service" 24 | type = map(any) 25 | default = {} 26 | } 27 | 28 | variable "custom_subdomain_name" { 29 | description = "(Optional) Specifies the custom subdomain name of the Azure OpenAI Service" 30 | type = string 31 | } 32 | 33 | variable "public_network_access_enabled" { 34 | description = "(Optional) Specifies whether public network access is allowed for the Azure OpenAI Service" 35 | type = bool 36 | default = true 37 | } 38 | 39 | variable "deployments" { 40 | description = "(Optional) Specifies the deployments of the Azure OpenAI Service" 41 | type = list(object({ 42 | name = string 43 | model = object({ 44 | name = string 45 | version = string 46 | }) 47 | rai_policy_name = string 48 | })) 49 | default = [ 50 | { 51 | name = "gpt-35-turbo" 52 | model = { 53 | name = "gpt-35-turbo" 54 | version = "0301" 55 | } 56 | rai_policy_name = "" 57 | } 58 | ] 59 | } 60 | 61 | variable "log_analytics_workspace_id" { 62 | description = "Specifies the log analytics workspace id" 63 | type = string 64 | } 65 | 66 | variable "log_analytics_retention_days" { 67 | description = "Specifies the number of days of the retention policy" 68 | type = number 69 | default = 7 70 | } -------------------------------------------------------------------------------- /terraform/infra/modules/private_dns_zone/main.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_private_dns_zone" "private_dns_zone" { 2 | name = var.name 3 | resource_group_name = var.resource_group_name 4 | tags = var.tags 5 | 6 | lifecycle { 7 | ignore_changes = [ 8 | tags 9 | ] 10 | } 11 | } 12 | 13 | resource "azurerm_private_dns_zone_virtual_network_link" "link" { 14 | for_each = var.virtual_networks_to_link 15 | 16 | name = "link_to_${lower(basename(each.key))}" 17 | resource_group_name = var.resource_group_name 18 | private_dns_zone_name = azurerm_private_dns_zone.private_dns_zone.name 19 | virtual_network_id = "/subscriptions/${each.value.subscription_id}/resourceGroups/${each.value.resource_group_name}/providers/Microsoft.Network/virtualNetworks/${each.key}" 20 | 21 | lifecycle { 22 | ignore_changes = [ 23 | tags 24 | ] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /terraform/infra/modules/private_dns_zone/outputs.tf: -------------------------------------------------------------------------------- 1 | output "id" { 2 | description = "Specifies the resource id of the private dns zone" 3 | value = azurerm_private_dns_zone.private_dns_zone.id 4 | } -------------------------------------------------------------------------------- /terraform/infra/modules/private_dns_zone/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | description = "(Required) Specifies the name of the private dns zone" 3 | type = string 4 | } 5 | 6 | variable "resource_group_name" { 7 | description = "(Required) Specifies the resource group name of the private dns zone" 8 | type = string 9 | } 10 | 11 | variable "tags" { 12 | description = "(Optional) Specifies the tags of the private dns zone" 13 | default = {} 14 | } 15 | 16 | variable "virtual_networks_to_link" { 17 | description = "(Optional) Specifies the subscription id, resource group name, and name of the virtual networks to which create a virtual network link" 18 | type = map(any) 19 | default = {} 20 | } -------------------------------------------------------------------------------- /terraform/infra/modules/private_endpoint/main.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_private_endpoint" "private_endpoint" { 2 | name = var.name 3 | location = var.location 4 | resource_group_name = var.resource_group_name 5 | subnet_id = var.subnet_id 6 | tags = var.tags 7 | 8 | private_service_connection { 9 | name = "${var.name}Connection" 10 | private_connection_resource_id = var.private_connection_resource_id 11 | is_manual_connection = var.is_manual_connection 12 | subresource_names = try([var.subresource_name], null) 13 | request_message = try(var.request_message, null) 14 | } 15 | 16 | private_dns_zone_group { 17 | name = var.private_dns_zone_group_name 18 | private_dns_zone_ids = var.private_dns_zone_group_ids 19 | } 20 | 21 | lifecycle { 22 | ignore_changes = [ 23 | tags 24 | ] 25 | } 26 | } -------------------------------------------------------------------------------- /terraform/infra/modules/private_endpoint/outputs.tf: -------------------------------------------------------------------------------- 1 | output "id" { 2 | description = "Specifies the resource id of the private endpoint." 3 | value = azurerm_private_endpoint.private_endpoint.id 4 | } 5 | 6 | output "private_dns_zone_group" { 7 | description = "Specifies the private dns zone group of the private endpoint." 8 | value = azurerm_private_endpoint.private_endpoint.private_dns_zone_group 9 | } 10 | 11 | output "private_dns_zone_configs" { 12 | description = "Specifies the private dns zone(s) configuration" 13 | value = azurerm_private_endpoint.private_endpoint.private_dns_zone_configs 14 | } -------------------------------------------------------------------------------- /terraform/infra/modules/private_endpoint/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | description = "(Required) Specifies the name of the private endpoint. Changing this forces a new resource to be created." 3 | type = string 4 | } 5 | 6 | variable "resource_group_name" { 7 | description = "(Required) The name of the resource group. Changing this forces a new resource to be created." 8 | type = string 9 | } 10 | 11 | variable "private_connection_resource_id" { 12 | description = "(Required) Specifies the resource id of the private link service" 13 | type = string 14 | } 15 | 16 | variable "location" { 17 | description = "(Required) Specifies the supported Azure location where the resource exists. Changing this forces a new resource to be created." 18 | type = string 19 | } 20 | 21 | variable "subnet_id" { 22 | description = "(Required) Specifies the resource id of the subnet" 23 | type = string 24 | } 25 | 26 | variable "is_manual_connection" { 27 | description = "(Optional) Specifies whether the private endpoint connection requires manual approval from the remote resource owner." 28 | type = string 29 | default = false 30 | } 31 | 32 | variable "subresource_name" { 33 | description = "(Optional) Specifies a subresource name which the Private Endpoint is able to connect to." 34 | type = string 35 | default = null 36 | } 37 | 38 | variable "request_message" { 39 | description = "(Optional) Specifies a message passed to the owner of the remote resource when the private endpoint attempts to establish the connection to the remote resource." 40 | type = string 41 | default = null 42 | } 43 | 44 | variable "private_dns_zone_group_name" { 45 | description = "(Required) Specifies the Name of the Private DNS Zone Group. Changing this forces a new private_dns_zone_group resource to be created." 46 | type = string 47 | } 48 | 49 | variable "private_dns_zone_group_ids" { 50 | description = "(Required) Specifies the list of Private DNS Zones to include within the private_dns_zone_group." 51 | type = list(string) 52 | } 53 | 54 | variable "tags" { 55 | description = "(Optional) Specifies the tags of the network security group" 56 | default = {} 57 | } 58 | 59 | variable "private_dns" { 60 | default = {} 61 | } -------------------------------------------------------------------------------- /terraform/infra/modules/virtual_network/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.3" 3 | required_providers { 4 | azurerm = { 5 | source = "hashicorp/azurerm" 6 | version = "~> 3.65" 7 | } 8 | } 9 | } 10 | 11 | resource "azurerm_virtual_network" "vnet" { 12 | name = var.vnet_name 13 | address_space = var.address_space 14 | location = var.location 15 | resource_group_name = var.resource_group_name 16 | tags = var.tags 17 | 18 | lifecycle { 19 | ignore_changes = [ 20 | tags 21 | ] 22 | } 23 | } 24 | 25 | resource "azurerm_subnet" "subnet" { 26 | for_each = { for subnet in var.subnets : subnet.name => subnet } 27 | 28 | name = each.key 29 | resource_group_name = var.resource_group_name 30 | virtual_network_name = azurerm_virtual_network.vnet.name 31 | address_prefixes = each.value.address_prefixes 32 | private_endpoint_network_policies_enabled = each.value.private_endpoint_network_policies_enabled 33 | private_link_service_network_policies_enabled = each.value.private_link_service_network_policies_enabled 34 | } 35 | 36 | resource "azurerm_monitor_diagnostic_setting" "settings" { 37 | name = "DiagnosticsSettings" 38 | target_resource_id = azurerm_virtual_network.vnet.id 39 | log_analytics_workspace_id = var.log_analytics_workspace_id 40 | 41 | enabled_log { 42 | category = "VMProtectionAlerts" 43 | 44 | retention_policy { 45 | enabled = true 46 | days = var.log_analytics_retention_days 47 | } 48 | } 49 | 50 | metric { 51 | category = "AllMetrics" 52 | 53 | retention_policy { 54 | enabled = true 55 | days = var.log_analytics_retention_days 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /terraform/infra/modules/virtual_network/outputs.tf: -------------------------------------------------------------------------------- 1 | output name { 2 | description = "Specifies the name of the virtual network" 3 | value = azurerm_virtual_network.vnet.name 4 | } 5 | 6 | output vnet_id { 7 | description = "Specifies the resource id of the virtual network" 8 | value = azurerm_virtual_network.vnet.id 9 | } 10 | 11 | output subnet_ids { 12 | description = "Contains a list of the the resource id of the subnets" 13 | value = { for subnet in azurerm_subnet.subnet : subnet.name => subnet.id } 14 | } -------------------------------------------------------------------------------- /terraform/infra/modules/virtual_network/variables.tf: -------------------------------------------------------------------------------- 1 | variable "resource_group_name" { 2 | description = "Resource Group name" 3 | type = string 4 | } 5 | 6 | variable "location" { 7 | description = "Location in which to deploy the network" 8 | type = string 9 | } 10 | 11 | variable "vnet_name" { 12 | description = "VNET name" 13 | type = string 14 | } 15 | 16 | variable "address_space" { 17 | description = "VNET address space" 18 | type = list(string) 19 | } 20 | 21 | variable "subnets" { 22 | description = "Subnets configuration" 23 | type = list(object({ 24 | name = string 25 | address_prefixes = list(string) 26 | private_endpoint_network_policies_enabled = bool 27 | private_link_service_network_policies_enabled = bool 28 | })) 29 | } 30 | 31 | variable "tags" { 32 | description = "(Optional) Specifies the tags of the storage account" 33 | default = {} 34 | } 35 | 36 | variable "log_analytics_workspace_id" { 37 | description = "Specifies the log analytics workspace id" 38 | type = string 39 | } 40 | 41 | variable "log_analytics_retention_days" { 42 | description = "Specifies the number of days of the retention policy" 43 | type = number 44 | default = 7 45 | } -------------------------------------------------------------------------------- /terraform/infra/outputs.tf: -------------------------------------------------------------------------------- 1 | output "log_analytics_name" { 2 | value = module.log_analytics_workspace.name 3 | } 4 | 5 | output "log_analytics_workspace_id" { 6 | value = module.log_analytics_workspace.workspace_id 7 | } 8 | -------------------------------------------------------------------------------- /terraform/infra/terraform.tfvars: -------------------------------------------------------------------------------- 1 | name_prefix = "Blue" 2 | location = "EastUS" -------------------------------------------------------------------------------- /terraform/infra/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name_prefix" { 2 | description = "(Optional) A prefix for the name of all the resource groups and resources." 3 | type = string 4 | nullable = true 5 | } 6 | 7 | variable "location" { 8 | description = "(Required) Specifies the supported Azure location where the resource exists. Changing this forces a new resource to be created." 9 | type = string 10 | default = "WestEurope" 11 | } 12 | 13 | variable "resource_group_name" { 14 | description = "Name of the resource group in which the resources will be created" 15 | default = "RG" 16 | } 17 | 18 | variable "tags" { 19 | description = "(Optional) Specifies tags for all the resources" 20 | default = { 21 | createdWith = "Terraform", 22 | openAi = "true", 23 | containerApps = "true" 24 | } 25 | } 26 | 27 | variable "log_analytics_workspace_name" { 28 | description = "Specifies the name of the log analytics workspace" 29 | default = "Workspace" 30 | type = string 31 | } 32 | 33 | variable "log_analytics_retention_days" { 34 | description = "Specifies the number of days of the retention policy for the log analytics workspace." 35 | type = number 36 | default = 30 37 | } 38 | 39 | variable "vnet_name" { 40 | description = "Specifies the name of the virtual network" 41 | default = "VNet" 42 | type = string 43 | } 44 | 45 | variable "vnet_address_space" { 46 | description = "Specifies the address prefix of the virtual network" 47 | default = ["10.0.0.0/16"] 48 | type = list(string) 49 | } 50 | 51 | variable "aca_subnet_name" { 52 | description = "Specifies the name of the subnet" 53 | default = "ContainerApps" 54 | type = string 55 | } 56 | 57 | variable "aca_subnet_address_prefix" { 58 | description = "Specifies the address prefix of the Azure Container Apps environment subnet" 59 | default = ["10.0.0.0/20"] 60 | type = list(string) 61 | } 62 | 63 | variable "private_endpoint_subnet_name" { 64 | description = "Specifies the name of the subnet" 65 | default = "PrivateEndpoints" 66 | type = string 67 | } 68 | 69 | variable "private_endpoint_subnet_address_prefix" { 70 | description = "Specifies the address prefix of the private endpoints subnet" 71 | default = ["10.0.16.0/24"] 72 | type = list(string) 73 | } 74 | 75 | variable "container_app_environment_name" { 76 | description = "(Required) Specifies the name of the Azure Container Apps Environment." 77 | type = string 78 | default = "Environment" 79 | } 80 | 81 | variable "internal_load_balancer_enabled" { 82 | description = "(Optional) specifies whether the Azure Container Apps Environment operate in Internal Load Balancing Mode? Defaults to false. Changing this forces a new resource to be created." 83 | type = bool 84 | default = false 85 | } 86 | 87 | variable "acr_name" { 88 | description = "Specifies the name of the container registry" 89 | type = string 90 | default = "Registry" 91 | } 92 | 93 | variable "acr_sku" { 94 | description = "Specifies the name of the container registry" 95 | type = string 96 | default = "Premium" 97 | 98 | validation { 99 | condition = contains(["Basic", "Standard", "Premium"], var.acr_sku) 100 | error_message = "The container registry sku is invalid." 101 | } 102 | } 103 | 104 | variable "acr_admin_enabled" { 105 | description = "Specifies whether admin is enabled for the container registry" 106 | type = bool 107 | default = true 108 | } 109 | 110 | variable "acr_georeplication_locations" { 111 | description = "(Optional) A list of Azure locations where the container registry should be geo-replicated." 112 | type = list(string) 113 | default = [] 114 | } 115 | 116 | variable "openai_name" { 117 | description = "(Required) Specifies the name of the Azure OpenAI Service" 118 | type = string 119 | default = "OpenAI" 120 | } 121 | 122 | variable "openai_sku_name" { 123 | description = "(Optional) Specifies the sku name for the Azure OpenAI Service" 124 | type = string 125 | default = "S0" 126 | } 127 | 128 | variable "openai_custom_subdomain_name" { 129 | description = "(Optional) Specifies the custom subdomain name of the Azure OpenAI Service" 130 | type = string 131 | nullable = true 132 | default = "" 133 | } 134 | 135 | variable "openai_public_network_access_enabled" { 136 | description = "(Optional) Specifies whether public network access is allowed for the Azure OpenAI Service" 137 | type = bool 138 | default = true 139 | } 140 | 141 | variable "openai_deployments" { 142 | description = "(Optional) Specifies the deployments of the Azure OpenAI Service" 143 | type = list(object({ 144 | name = string 145 | model = object({ 146 | name = string 147 | version = string 148 | }) 149 | rai_policy_name = string 150 | })) 151 | default = [ 152 | { 153 | name = "gpt-35-turbo-16k" 154 | model = { 155 | name = "gpt-35-turbo-16k" 156 | version = "0613" 157 | } 158 | rai_policy_name = "" 159 | }, 160 | { 161 | name = "text-embedding-ada-002" 162 | model = { 163 | name = "text-embedding-ada-002" 164 | version = "2" 165 | } 166 | rai_policy_name = "" 167 | } 168 | ] 169 | } 170 | 171 | variable "workload_managed_identity_name" { 172 | description = "(Required) Specifies the name of the workload user-defined managed identity" 173 | type = string 174 | default = "WorkloadIdentity" 175 | } -------------------------------------------------------------------------------- /visio/architecture.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/container-apps-openai/45a9d8143aba1550a2e473cc911d65b576335966/visio/architecture.vsdx --------------------------------------------------------------------------------