├── .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
--------------------------------------------------------------------------------