diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b213342df..9a66d9c07 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -29,7 +29,7 @@ "TeamsDevApp.ms-teams-vscode-extension" ], "settings": { - "azureFunctions.projectSubpath": "code/batch", + "azureFunctions.projectSubpath": "code/backend/batch", "python.defaultInterpreterPath": "/usr/local/bin/python", "python.pythonPath": "/usr/local/bin/python" } diff --git a/.devcontainer/postCreate.sh b/.devcontainer/postCreate.sh index 7ddfb824f..ce4d6f4df 100755 --- a/.devcontainer/postCreate.sh +++ b/.devcontainer/postCreate.sh @@ -2,7 +2,7 @@ pip install --upgrade pip -pip install -r code/requirements.txt -r code/app/requirements.txt -r extensions/backend/requirements.txt +pip install -r code/requirements.txt -r code/backend/requirements.txt -r code/backend/batch/requirements.txt pip install -r code/dev-requirements.txt diff --git a/.env.sample b/.env.sample index 8cd7407af..5446cecca 100644 --- a/.env.sample +++ b/.env.sample @@ -49,4 +49,9 @@ ORCHESTRATION_STRATEGY=openai_functions #Speech-to-text feature AZURE_SPEECH_SERVICE_KEY= AZURE_SPEECH_SERVICE_REGION= -AZURE_AUTH_TYPE=rbac \ No newline at end of file +# Auth type environment variables. +# When AZURE_AUTH_TYPE=rbac, please make sure variable USE_KEY_VAULT=false +# When USE_KEY_VAULT=true, please make sure to set AZURE_KEY_VAULT_ENDPOINT +AZURE_AUTH_TYPE=keys +USE_KEY_VAULT=true +AZURE_KEY_VAULT_ENDPOINT= \ No newline at end of file diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index 7c0f6583f..7fbbbf26c 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -20,6 +20,6 @@ jobs: architecture: x64 - name: Install dependencies run: | - pip install -r code/requirements.txt -r code/dev-requirements.txt -r code/app/requirements.txt + pip install -r code/requirements.txt -r code/dev-requirements.txt -r code/backend/requirements.txt - name: Run Python tests - run: python -m pytest --rootdir=code -m "not azure" + run: cd code/; python -m pytest -m "not azure" diff --git a/.gitignore b/.gitignore index dc34e653b..5d3fe048e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore # generated frontend files -code/app/static/ +code/static/ # User-specific files *.rsuser diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 24a6a6cae..923d19164 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 23.12.1 + rev: 24.2.0 hooks: - id: black language_version: python3 diff --git a/.vscode/launch.json b/.vscode/launch.json index f6b73fe0b..d2c7e358c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -17,7 +17,7 @@ "run", "Admin.py" ], - "cwd": "${workspaceFolder}/code/admin", + "cwd": "${workspaceFolder}/code/backend", "preLaunchTask": "pip install (code)", }, { @@ -31,14 +31,14 @@ "--debug", "run" ], - "cwd": "${workspaceFolder}/code/app", + "cwd": "${workspaceFolder}/code", "preLaunchTask": "pip install (code)" }, { "name": "Launch Frontend (UI)", "type": "node", "request": "launch", - "cwd": "${workspaceFolder}/code/app/frontend", + "cwd": "${workspaceFolder}/code/frontend", "preLaunchTask": "npm install (code)", "runtimeExecutable": "npm", "runtimeArgs": [ diff --git a/.vscode/settings.json b/.vscode/settings.json index 86c787578..a33850f3e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { - "azureFunctions.deploySubpath": "code\\batch", + "azureFunctions.deploySubpath": "code\\backend\\batch", "azureFunctions.scmDoBuildDuringDeployment": true, "azureFunctions.pythonVenv": ".venv", "azureFunctions.projectLanguage": "Python", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 8ddda72a3..202ebec0c 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -9,7 +9,7 @@ "isBackground": true, "dependsOn": "pip install (functions)", "options": { - "cwd": "${workspaceFolder}/backend" + "cwd": "${workspaceFolder}/code/backend/batch" } }, { @@ -26,7 +26,7 @@ }, "problemMatcher": [], "options": { - "cwd": "${workspaceFolder}/backend" + "cwd": "${workspaceFolder}/code/backend" } }, { @@ -44,7 +44,7 @@ "command": "npm install", "problemMatcher": [], "options": { - "cwd": "${workspaceFolder}/code/app/frontend" + "cwd": "${workspaceFolder}/code/frontend" } } ] diff --git a/Makefile b/Makefile index 22015f8b5..a9e6d314b 100644 --- a/Makefile +++ b/Makefile @@ -16,12 +16,20 @@ endif help: ## ๐Ÿ’ฌ This help message :) @grep -E '[a-zA-Z_-]+:.*?## .*$$' $(firstword $(MAKEFILE_LIST)) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-23s\033[0m %s\n", $$1, $$2}' -ci: unittest build-frontend ## ๐Ÿš€ Continuous Integration (called by Github Actions) +ci: lint unittest build-frontend ## ๐Ÿš€ Continuous Integration (called by Github Actions) + +lint: ## ๐Ÿงน Lint the code + @echo -e "\e[34m$@\e[0m" || true + @flake8 code unittest: ## ๐Ÿงช Run the unit tests @echo -e "\e[34m$@\e[0m" || true - @python -m pytest -m "not azure" + @cd code/ && python -m pytest -m "not azure" build-frontend: ## ๐Ÿ—๏ธ Build the Frontend webapp @echo -e "\e[34m$@\e[0m" || true - @cd code/app/frontend && npm install && npm run build + @cd code/frontend && npm install && npm run build + +azd-login: ## ๐Ÿ”‘ Login to Azure with azd and a SPN + @echo -e "\e[34m$@\e[0m" || true + @azd auth login --client-id ${AZURE_CLIENT_ID} --client-secret ${AZURE_CLIENT_SECRET} --tenant-id ${AZURE_TENANT_ID} diff --git a/README.md b/README.md index 165a121e3..5ea9ef9c2 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,46 @@ az deployment group create --resource-group $RESOURCE_GROUP_NAME --template-file ![A screenshot of the chat app.](./media/web-unstructureddata.png) +### Running the sample using the Azure Developer CLI (azd) + +The Azure Developer CLI (`azd`) is a developer-centric command-line interface (CLI) tool for creating Azure applications. + +You need to install it before running and deploying with the Azure Developer CLI. (If you use the devcontainer, everything is already installed) + +### Windows + +```powershell +powershell -ex AllSigned -c "Invoke-RestMethod 'https://aka.ms/install-azd.ps1' | Invoke-Expression" +``` + +### Linux/MacOS + +``` +curl -fsSL https://aka.ms/install-azd.sh | bash +``` + +After logging in with the following command, you will be able to use the `azd` cli to quickly provision and deploy the application. + +``` +azd auth login +``` + +Then, execute the `azd init` command to initialize the environment (You do not need to run this command if you already have the code or have opened this in a Codespace or DevContainer). +``` +azd init -t chat-with-your-data-solution-accelerator +``` +Enter an environment name. + +**Notes:** the default auth type uses keys, if you want to switch to rbac, please run `azd env set AUTH_TYPE rbac`. + +Then, run `azd up` to provision all the resources to Azure and deploy the code to those resources. +``` +azd up +``` + +Select your desired `subscription` and `location`. Wait a moment for the resource deployment to complete, click the website endpoint and you will see the web app page. + +You can also run the sample directly locally (See below). ### [Local deployment instructions](./docs/LOCAL_DEPLOYMENT.md) To customize the accelerator or run it locally, first, copy the .env.sample file to your development environment's .env file, and edit it according to environment variable values table. Learn more about deploying locally [here](./docs/LOCAL_DEPLOYMENT.md). diff --git a/azure.yaml b/azure.yaml new file mode 100644 index 000000000..269e67c98 --- /dev/null +++ b/azure.yaml @@ -0,0 +1,33 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: chat-with-your-data-solution-accelerator +metadata: + template: chat-with-your-data-solution-accelerator@0.0.1-beta + +services: + web: + project: ./code + language: py + host: appservice + hooks: + prepackage: + windows: + shell: pwsh + run: cd ./frontend;npm install;npm run build; + interactive: true + continueOnError: false + posix: + shell: sh + run: cd ./frontend;npm install;npm run build; + interactive: true + continueOnError: false + + adminweb: + project: ./code/backend + language: py + host: appservice + + function: + project: ./code/backend/batch + language: py + host: function diff --git a/code/app/app.py b/code/app.py similarity index 77% rename from code/app/app.py rename to code/app.py index bfdf0da66..94e273a1b 100644 --- a/code/app/app.py +++ b/code/app.py @@ -2,11 +2,13 @@ import os import logging import requests -import openai +from openai import AzureOpenAI import mimetypes from flask import Flask, Response, request, jsonify from dotenv import load_dotenv +from azure.identity import DefaultAzureCredential, get_bearer_token_provider import sys +from backend.batch.utilities.helpers.EnvHelper import EnvHelper # Fixing MIME types for static files under Windows mimetypes.add_type("application/javascript", ".js") @@ -27,22 +29,27 @@ def static_file(path): return app.send_static_file(path) +env_helper: EnvHelper = EnvHelper() +AZURE_AUTH_TYPE = env_helper.AZURE_AUTH_TYPE +AZURE_SEARCH_KEY = env_helper.AZURE_SEARCH_KEY +AZURE_OPENAI_KEY = env_helper.AZURE_OPENAI_KEY +AZURE_SPEECH_KEY = env_helper.AZURE_SPEECH_KEY + + @app.route("/api/config", methods=["GET"]) def get_config(): # Retrieve the environment variables or other configuration data - azure_speech_key = os.getenv("AZURE_SPEECH_SERVICE_KEY") azure_speech_region = os.getenv("AZURE_SPEECH_SERVICE_REGION") # Return the configuration data as JSON return jsonify( - {"azureSpeechKey": azure_speech_key, "azureSpeechRegion": azure_speech_region} + {"azureSpeechKey": AZURE_SPEECH_KEY, "azureSpeechRegion": azure_speech_region} ) # ACS Integration Settings AZURE_SEARCH_SERVICE = os.environ.get("AZURE_SEARCH_SERVICE") AZURE_SEARCH_INDEX = os.environ.get("AZURE_SEARCH_INDEX") -AZURE_SEARCH_KEY = os.environ.get("AZURE_SEARCH_KEY") AZURE_SEARCH_USE_SEMANTIC_SEARCH = os.environ.get( "AZURE_SEARCH_USE_SEMANTIC_SEARCH", "False" ) @@ -59,7 +66,6 @@ def get_config(): # AOAI Integration Settings AZURE_OPENAI_RESOURCE = os.environ.get("AZURE_OPENAI_RESOURCE") AZURE_OPENAI_MODEL = os.environ.get("AZURE_OPENAI_MODEL") -AZURE_OPENAI_KEY = os.environ.get("AZURE_OPENAI_KEY") AZURE_OPENAI_TEMPERATURE = os.environ.get("AZURE_OPENAI_TEMPERATURE", 0) AZURE_OPENAI_TOP_P = os.environ.get("AZURE_OPENAI_TOP_P", 1.0) AZURE_OPENAI_MAX_TOKENS = os.environ.get("AZURE_OPENAI_MAX_TOKENS", 1000) @@ -75,7 +81,9 @@ def get_config(): AZURE_OPENAI_MODEL_NAME = os.environ.get( "AZURE_OPENAI_MODEL_NAME", "gpt-35-turbo" ) # Name of the model, e.g. 'gpt-35-turbo' or 'gpt-4' -AZURE_AUTH_TYPE = os.environ.get("AZURE_AUTH_TYPE", "keys") +AZURE_TOKEN_PROVIDER = get_bearer_token_provider( + DefaultAzureCredential(), "https://cognitiveservices.azure.com/.default" +) SHOULD_STREAM = True if AZURE_OPENAI_STREAM.lower() == "true" else False @@ -100,9 +108,11 @@ def prepare_body_headers_with_data(request): "temperature": AZURE_OPENAI_TEMPERATURE, "max_tokens": AZURE_OPENAI_MAX_TOKENS, "top_p": AZURE_OPENAI_TOP_P, - "stop": AZURE_OPENAI_STOP_SEQUENCE.split("|") - if AZURE_OPENAI_STOP_SEQUENCE - else None, + "stop": ( + AZURE_OPENAI_STOP_SEQUENCE.split("|") + if AZURE_OPENAI_STOP_SEQUENCE + else None + ), "stream": SHOULD_STREAM, "dataSources": [ { @@ -112,30 +122,42 @@ def prepare_body_headers_with_data(request): "key": AZURE_SEARCH_KEY, "indexName": AZURE_SEARCH_INDEX, "fieldsMapping": { - "contentField": AZURE_SEARCH_CONTENT_COLUMNS.split("|") - if AZURE_SEARCH_CONTENT_COLUMNS - else [], - "titleField": AZURE_SEARCH_TITLE_COLUMN - if AZURE_SEARCH_TITLE_COLUMN - else None, - "urlField": AZURE_SEARCH_URL_COLUMN - if AZURE_SEARCH_URL_COLUMN - else None, - "filepathField": AZURE_SEARCH_FILENAME_COLUMN - if AZURE_SEARCH_FILENAME_COLUMN - else None, + "contentField": ( + AZURE_SEARCH_CONTENT_COLUMNS.split("|") + if AZURE_SEARCH_CONTENT_COLUMNS + else [] + ), + "titleField": ( + AZURE_SEARCH_TITLE_COLUMN + if AZURE_SEARCH_TITLE_COLUMN + else None + ), + "urlField": ( + AZURE_SEARCH_URL_COLUMN if AZURE_SEARCH_URL_COLUMN else None + ), + "filepathField": ( + AZURE_SEARCH_FILENAME_COLUMN + if AZURE_SEARCH_FILENAME_COLUMN + else None + ), }, - "inScope": True - if AZURE_SEARCH_ENABLE_IN_DOMAIN.lower() == "true" - else False, + "inScope": ( + True + if AZURE_SEARCH_ENABLE_IN_DOMAIN.lower() == "true" + else False + ), "topNDocuments": AZURE_SEARCH_TOP_K, - "queryType": "semantic" - if AZURE_SEARCH_USE_SEMANTIC_SEARCH.lower() == "true" - else "simple", - "semanticConfiguration": AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG - if AZURE_SEARCH_USE_SEMANTIC_SEARCH.lower() == "true" - and AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG - else "", + "queryType": ( + "semantic" + if AZURE_SEARCH_USE_SEMANTIC_SEARCH.lower() == "true" + else "simple" + ), + "semanticConfiguration": ( + AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG + if AZURE_SEARCH_USE_SEMANTIC_SEARCH.lower() == "true" + and AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG + else "" + ), "roleInformation": AZURE_OPENAI_SYSTEM_MESSAGE, }, } @@ -241,10 +263,19 @@ def stream_without_data(response): def conversation_without_data(request): - openai.api_type = "azure" - openai.api_base = f"https://{AZURE_OPENAI_RESOURCE}.openai.azure.com/" - openai.api_version = "2023-12-01-preview" - openai.api_key = AZURE_OPENAI_KEY + azure_endpoint = f"https://{AZURE_OPENAI_RESOURCE}.openai.azure.com/" + if AZURE_AUTH_TYPE == "rbac": + openai_client = AzureOpenAI( + azure_endpoint=azure_endpoint, + api_version=AZURE_OPENAI_API_VERSION, + azure_ad_token_provider=AZURE_TOKEN_PROVIDER, + ) + else: + openai_client = AzureOpenAI( + azure_endpoint=azure_endpoint, + api_version=AZURE_OPENAI_API_VERSION, + api_key=AZURE_OPENAI_KEY, + ) request_messages = request.json["messages"] messages = [{"role": "system", "content": AZURE_OPENAI_SYSTEM_MESSAGE}] @@ -252,15 +283,18 @@ def conversation_without_data(request): for message in request_messages: messages.append({"role": message["role"], "content": message["content"]}) - response = openai.ChatCompletion.create( - engine=AZURE_OPENAI_MODEL, + # Azure Open AI takes the deployment name as the model name, "AZURE_OPENAI_MODEL" means deployment name. + response = openai_client.chat.completions.create( + model=AZURE_OPENAI_MODEL, messages=messages, temperature=float(AZURE_OPENAI_TEMPERATURE), max_tokens=int(AZURE_OPENAI_MAX_TOKENS), top_p=float(AZURE_OPENAI_TOP_P), - stop=AZURE_OPENAI_STOP_SEQUENCE.split("|") - if AZURE_OPENAI_STOP_SEQUENCE - else None, + stop=( + AZURE_OPENAI_STOP_SEQUENCE.split("|") + if AZURE_OPENAI_STOP_SEQUENCE + else None + ), stream=SHOULD_STREAM, ) @@ -314,13 +348,13 @@ def conversation_azure_byod(): def get_message_orchestrator(): - from utilities.helpers.OrchestratorHelper import Orchestrator + from backend.batch.utilities.helpers.OrchestratorHelper import Orchestrator return Orchestrator() def get_orchestrator_config(): - from utilities.helpers.ConfigHelper import ConfigHelper + from backend.batch.utilities.helpers.ConfigHelper import ConfigHelper return ConfigHelper.get_active_config_or_default().orchestrator diff --git a/code/admin/Admin.py b/code/backend/Admin.py similarity index 100% rename from code/admin/Admin.py rename to code/backend/Admin.py diff --git a/code/batch/.funcignore b/code/backend/batch/.funcignore similarity index 100% rename from code/batch/.funcignore rename to code/backend/batch/.funcignore diff --git a/code/batch/.gitignore b/code/backend/batch/.gitignore similarity index 100% rename from code/batch/.gitignore rename to code/backend/batch/.gitignore diff --git a/code/batch/.vscode/extensions.json b/code/backend/batch/.vscode/extensions.json similarity index 100% rename from code/batch/.vscode/extensions.json rename to code/backend/batch/.vscode/extensions.json diff --git a/code/batch/AddURLEmbeddings.py b/code/backend/batch/AddURLEmbeddings.py similarity index 100% rename from code/batch/AddURLEmbeddings.py rename to code/backend/batch/AddURLEmbeddings.py diff --git a/code/batch/BatchPushResults.py b/code/backend/batch/BatchPushResults.py similarity index 100% rename from code/batch/BatchPushResults.py rename to code/backend/batch/BatchPushResults.py diff --git a/code/batch/BatchStartProcessing.py b/code/backend/batch/BatchStartProcessing.py similarity index 100% rename from code/batch/BatchStartProcessing.py rename to code/backend/batch/BatchStartProcessing.py diff --git a/code/batch/GetConversationResponse.py b/code/backend/batch/GetConversationResponse.py similarity index 99% rename from code/batch/GetConversationResponse.py rename to code/backend/batch/GetConversationResponse.py index c17d68808..2bf5719e2 100644 --- a/code/batch/GetConversationResponse.py +++ b/code/backend/batch/GetConversationResponse.py @@ -9,11 +9,11 @@ bp_get_conversation_response = func.Blueprint() + @bp_get_conversation_response.route(route="GetConversationResponse") def get_conversation_response(req: func.HttpRequest) -> func.HttpResponse: logging.info("Python HTTP trigger function processed a request.") - message_orchestrator = Orchestrator() try: @@ -55,4 +55,4 @@ def get_conversation_response(req: func.HttpRequest) -> func.HttpResponse: except Exception as e: logging.exception("Exception in /api/GetConversationResponse") - return func.HttpResponse(json.dumps({"error": str(e)}), status_code=500) \ No newline at end of file + return func.HttpResponse(json.dumps({"error": str(e)}), status_code=500) diff --git a/code/batch/function_app.py b/code/backend/batch/function_app.py similarity index 100% rename from code/batch/function_app.py rename to code/backend/batch/function_app.py diff --git a/code/batch/host.json b/code/backend/batch/host.json similarity index 100% rename from code/batch/host.json rename to code/backend/batch/host.json diff --git a/code/batch/local.settings.json.sample b/code/backend/batch/local.settings.json.sample similarity index 100% rename from code/batch/local.settings.json.sample rename to code/backend/batch/local.settings.json.sample diff --git a/code/app/requirements.txt b/code/backend/batch/requirements.txt similarity index 73% rename from code/app/requirements.txt rename to code/backend/batch/requirements.txt index 81d063248..320a47e88 100644 --- a/code/app/requirements.txt +++ b/code/backend/batch/requirements.txt @@ -1,18 +1,24 @@ -azure-identity==1.15.0 -Flask==3.0.2 -openai==0.27.8 -azure-storage-blob==12.19.0 +azure-functions==1.18.0 +streamlit==1.30.0 +openai==1.6.1 +scipy==1.12.0 +transformers==4.37.2 python-dotenv==1.0.1 -langchain==0.1.4 azure-ai-formrecognizer==3.3.2 +azure-storage-blob==12.19.0 +azure-identity==1.15.0 +azure-ai-contentsafety==1.0.0 requests==2.31.0 tiktoken==0.6.0 azure-storage-queue==12.9.0 +langchain==0.1.4 +langchain-community==0.0.19 beautifulsoup4==4.12.3 fake-useragent==1.4.0 chardet==5.2.0 --extra-index-url https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/ azure-search-documents==11.4.0b8 opencensus-ext-azure==1.1.13 -azure-ai-contentsafety==1.0.0 +pandas==2.2.0 python-docx==1.1.0 +azure-keyvault-secrets==4.4.* diff --git a/code/__init__.py b/code/backend/batch/utilities/__init__.py similarity index 100% rename from code/__init__.py rename to code/backend/batch/utilities/__init__.py diff --git a/code/utilities/common/Answer.py b/code/backend/batch/utilities/common/Answer.py similarity index 100% rename from code/utilities/common/Answer.py rename to code/backend/batch/utilities/common/Answer.py diff --git a/code/utilities/common/SourceDocument.py b/code/backend/batch/utilities/common/SourceDocument.py similarity index 100% rename from code/utilities/common/SourceDocument.py rename to code/backend/batch/utilities/common/SourceDocument.py diff --git a/code/utilities/__init__.py b/code/backend/batch/utilities/common/__init__.py similarity index 100% rename from code/utilities/__init__.py rename to code/backend/batch/utilities/common/__init__.py diff --git a/code/utilities/document_chunking/DocumentChunkingBase.py b/code/backend/batch/utilities/document_chunking/DocumentChunkingBase.py similarity index 100% rename from code/utilities/document_chunking/DocumentChunkingBase.py rename to code/backend/batch/utilities/document_chunking/DocumentChunkingBase.py diff --git a/code/utilities/document_chunking/FixedSizeOverlap.py b/code/backend/batch/utilities/document_chunking/FixedSizeOverlap.py similarity index 100% rename from code/utilities/document_chunking/FixedSizeOverlap.py rename to code/backend/batch/utilities/document_chunking/FixedSizeOverlap.py diff --git a/code/utilities/document_chunking/Layout.py b/code/backend/batch/utilities/document_chunking/Layout.py similarity index 100% rename from code/utilities/document_chunking/Layout.py rename to code/backend/batch/utilities/document_chunking/Layout.py diff --git a/code/utilities/document_chunking/Page.py b/code/backend/batch/utilities/document_chunking/Page.py similarity index 100% rename from code/utilities/document_chunking/Page.py rename to code/backend/batch/utilities/document_chunking/Page.py diff --git a/code/utilities/document_chunking/Paragraph.py b/code/backend/batch/utilities/document_chunking/Paragraph.py similarity index 100% rename from code/utilities/document_chunking/Paragraph.py rename to code/backend/batch/utilities/document_chunking/Paragraph.py diff --git a/code/utilities/document_chunking/Strategies.py b/code/backend/batch/utilities/document_chunking/Strategies.py similarity index 100% rename from code/utilities/document_chunking/Strategies.py rename to code/backend/batch/utilities/document_chunking/Strategies.py diff --git a/code/utilities/document_chunking/__init__.py b/code/backend/batch/utilities/document_chunking/__init__.py similarity index 100% rename from code/utilities/document_chunking/__init__.py rename to code/backend/batch/utilities/document_chunking/__init__.py diff --git a/code/utilities/document_loading/DocumentLoadingBase.py b/code/backend/batch/utilities/document_loading/DocumentLoadingBase.py similarity index 100% rename from code/utilities/document_loading/DocumentLoadingBase.py rename to code/backend/batch/utilities/document_loading/DocumentLoadingBase.py diff --git a/code/utilities/document_loading/Layout.py b/code/backend/batch/utilities/document_loading/Layout.py similarity index 100% rename from code/utilities/document_loading/Layout.py rename to code/backend/batch/utilities/document_loading/Layout.py diff --git a/code/utilities/document_loading/Read.py b/code/backend/batch/utilities/document_loading/Read.py similarity index 100% rename from code/utilities/document_loading/Read.py rename to code/backend/batch/utilities/document_loading/Read.py diff --git a/code/utilities/document_loading/Strategies.py b/code/backend/batch/utilities/document_loading/Strategies.py similarity index 100% rename from code/utilities/document_loading/Strategies.py rename to code/backend/batch/utilities/document_loading/Strategies.py diff --git a/code/utilities/document_loading/Web.py b/code/backend/batch/utilities/document_loading/Web.py similarity index 100% rename from code/utilities/document_loading/Web.py rename to code/backend/batch/utilities/document_loading/Web.py diff --git a/code/utilities/document_loading/WordDocument.py b/code/backend/batch/utilities/document_loading/WordDocument.py similarity index 100% rename from code/utilities/document_loading/WordDocument.py rename to code/backend/batch/utilities/document_loading/WordDocument.py diff --git a/code/utilities/document_loading/__init__.py b/code/backend/batch/utilities/document_loading/__init__.py similarity index 100% rename from code/utilities/document_loading/__init__.py rename to code/backend/batch/utilities/document_loading/__init__.py diff --git a/code/utilities/helpers/AzureBlobStorageHelper.py b/code/backend/batch/utilities/helpers/AzureBlobStorageHelper.py similarity index 55% rename from code/utilities/helpers/AzureBlobStorageHelper.py rename to code/backend/batch/utilities/helpers/AzureBlobStorageHelper.py index d19e83d85..0196f4414 100644 --- a/code/utilities/helpers/AzureBlobStorageHelper.py +++ b/code/backend/batch/utilities/helpers/AzureBlobStorageHelper.py @@ -5,8 +5,10 @@ generate_blob_sas, generate_container_sas, ContentSettings, + UserDelegationKey, ) from .EnvHelper import EnvHelper +from azure.identity import DefaultAzureCredential class AzureBlobStorageClient: @@ -18,19 +20,55 @@ def __init__( ): env_helper: EnvHelper = EnvHelper() - self.account_name = ( - account_name if account_name else env_helper.AZURE_BLOB_ACCOUNT_NAME - ) - self.account_key = ( - account_key if account_key else env_helper.AZURE_BLOB_ACCOUNT_KEY - ) - self.connect_str = f"DefaultEndpointsProtocol=https;AccountName={self.account_name};AccountKey={self.account_key};EndpointSuffix=core.windows.net" - self.container_name: str = ( - container_name if container_name else env_helper.AZURE_BLOB_CONTAINER_NAME - ) - self.blob_service_client: BlobServiceClient = ( - BlobServiceClient.from_connection_string(self.connect_str) + self.auth_type = env_helper.AZURE_AUTH_TYPE + if self.auth_type == "rbac": + self.account_name = ( + account_name if account_name else env_helper.AZURE_BLOB_ACCOUNT_NAME + ) + self.container_name: str = ( + container_name + if container_name + else env_helper.AZURE_BLOB_CONTAINER_NAME + ) + credential = DefaultAzureCredential() + account_url = f"https://{self.account_name}.blob.core.windows.net/" + self.blob_service_client = BlobServiceClient( + account_url=account_url, credential=credential + ) + self.user_delegation_key = self.request_user_delegation_key( + blob_service_client=self.blob_service_client + ) + self.account_key = None + else: + self.account_name = ( + account_name if account_name else env_helper.AZURE_BLOB_ACCOUNT_NAME + ) + self.account_key = ( + account_key if account_key else env_helper.AZURE_BLOB_ACCOUNT_KEY + ) + self.user_delegation_key = None + self.connect_str = f"DefaultEndpointsProtocol=https;AccountName={self.account_name};AccountKey={self.account_key};EndpointSuffix=core.windows.net" + self.container_name: str = ( + container_name + if container_name + else env_helper.AZURE_BLOB_CONTAINER_NAME + ) + self.blob_service_client: BlobServiceClient = ( + BlobServiceClient.from_connection_string(self.connect_str) + ) + + def request_user_delegation_key( + self, blob_service_client: BlobServiceClient + ) -> UserDelegationKey: + # Get a user delegation key that's valid for 1 day + delegation_key_start_time = datetime.utcnow() + delegation_key_expiry_time = delegation_key_start_time + timedelta(days=1) + + user_delegation_key = blob_service_client.get_user_delegation_key( + key_start_time=delegation_key_start_time, + key_expiry_time=delegation_key_expiry_time, ) + return user_delegation_key def upload_file(self, bytes_data, file_name, content_type="application/pdf"): # Create a blob client using the local file name as the name for the blob @@ -43,7 +81,7 @@ def upload_file(self, bytes_data, file_name, content_type="application/pdf"): overwrite=True, content_settings=ContentSettings(content_type=content_type), ) - # Generate a SAS URL to the blob and return it + # Generate a SAS URL to the blob and return it, if auth_type is rbac, account_key is None, if not, user_delegation_key is None. return ( blob_client.url + "?" @@ -51,6 +89,7 @@ def upload_file(self, bytes_data, file_name, content_type="application/pdf"): self.account_name, self.container_name, file_name, + user_delegation_key=self.user_delegation_key, account_key=self.account_key, permission="r", expiry=datetime.utcnow() + timedelta(hours=3), @@ -88,6 +127,7 @@ def get_all_files(self): sas = generate_container_sas( self.account_name, self.container_name, + user_delegation_key=self.user_delegation_key, account_key=self.account_key, permission="r", expiry=datetime.utcnow() + timedelta(hours=3), @@ -99,28 +139,29 @@ def get_all_files(self): files.append( { "filename": blob.name, - "converted": blob.metadata.get("converted", "false") == "true" - if blob.metadata - else False, - "embeddings_added": blob.metadata.get( - "embeddings_added", "false" - ) - == "true" - if blob.metadata - else False, + "converted": ( + blob.metadata.get("converted", "false") == "true" + if blob.metadata + else False + ), + "embeddings_added": ( + blob.metadata.get("embeddings_added", "false") == "true" + if blob.metadata + else False + ), "fullpath": f"https://{self.account_name}.blob.core.windows.net/{self.container_name}/{blob.name}?{sas}", - "converted_filename": blob.metadata.get( - "converted_filename", "" - ) - if blob.metadata - else "", + "converted_filename": ( + blob.metadata.get("converted_filename", "") + if blob.metadata + else "" + ), "converted_path": "", } ) else: - converted_files[ - blob.name - ] = f"https://{self.account_name}.blob.core.windows.net/{self.container_name}/{blob.name}?{sas}" + converted_files[blob.name] = ( + f"https://{self.account_name}.blob.core.windows.net/{self.container_name}/{blob.name}?{sas}" + ) for file in files: converted_filename = file.pop("converted_filename", "") @@ -131,9 +172,14 @@ def get_all_files(self): return files def upsert_blob_metadata(self, file_name, metadata): - blob_client = BlobServiceClient.from_connection_string( - self.connect_str - ).get_blob_client(container=self.container_name, blob=file_name) + if self.auth_type == "rbac": + blob_client = self.blob_service_client.get_blob_client( + container=self.container_name, blob=file_name + ) + else: + blob_client = BlobServiceClient.from_connection_string( + self.connect_str + ).get_blob_client(container=self.container_name, blob=file_name) # Read metadata from the blob blob_metadata = blob_client.get_blob_properties().metadata # Update metadata @@ -146,6 +192,7 @@ def get_container_sas(self): return "?" + generate_container_sas( account_name=self.account_name, container_name=self.container_name, + user_delegation_key=self.user_delegation_key, account_key=self.account_key, permission="r", expiry=datetime.utcnow() + timedelta(hours=1), @@ -160,6 +207,7 @@ def get_blob_sas(self, file_name): account_name=self.account_name, container_name=self.container_name, blob_name=file_name, + user_delegation_key=self.user_delegation_key, account_key=self.account_key, permission="r", expiry=datetime.utcnow() + timedelta(hours=1), diff --git a/code/utilities/helpers/AzureFormRecognizerHelper.py b/code/backend/batch/utilities/helpers/AzureFormRecognizerHelper.py similarity index 85% rename from code/utilities/helpers/AzureFormRecognizerHelper.py rename to code/backend/batch/utilities/helpers/AzureFormRecognizerHelper.py index 9fa8376ba..4cc01cf4f 100644 --- a/code/utilities/helpers/AzureFormRecognizerHelper.py +++ b/code/backend/batch/utilities/helpers/AzureFormRecognizerHelper.py @@ -1,5 +1,6 @@ from azure.core.credentials import AzureKeyCredential from azure.ai.formrecognizer import DocumentAnalysisClient +from azure.identity import DefaultAzureCredential import html import traceback from .EnvHelper import EnvHelper @@ -12,15 +13,24 @@ def __init__(self) -> None: self.AZURE_FORM_RECOGNIZER_ENDPOINT: str = ( env_helper.AZURE_FORM_RECOGNIZER_ENDPOINT ) - self.AZURE_FORM_RECOGNIZER_KEY: str = env_helper.AZURE_FORM_RECOGNIZER_KEY - - self.document_analysis_client = DocumentAnalysisClient( - endpoint=self.AZURE_FORM_RECOGNIZER_ENDPOINT, - credential=AzureKeyCredential(self.AZURE_FORM_RECOGNIZER_KEY), - headers={ - "x-ms-useragent": "chat-with-your-data-solution-accelerator/1.0.0" - }, - ) + if env_helper.AZURE_AUTH_TYPE == "rbac": + self.document_analysis_client = DocumentAnalysisClient( + endpoint=self.AZURE_FORM_RECOGNIZER_ENDPOINT, + credential=DefaultAzureCredential(), + headers={ + "x-ms-useragent": "chat-with-your-data-solution-accelerator/1.0.0" + }, + ) + else: + self.AZURE_FORM_RECOGNIZER_KEY: str = env_helper.AZURE_FORM_RECOGNIZER_KEY + + self.document_analysis_client = DocumentAnalysisClient( + endpoint=self.AZURE_FORM_RECOGNIZER_ENDPOINT, + credential=AzureKeyCredential(self.AZURE_FORM_RECOGNIZER_KEY), + headers={ + "x-ms-useragent": "chat-with-your-data-solution-accelerator/1.0.0" + }, + ) form_recognizer_role_to_html = { "title": "h1", diff --git a/code/utilities/helpers/AzureSearchHelper.py b/code/backend/batch/utilities/helpers/AzureSearchHelper.py similarity index 93% rename from code/utilities/helpers/AzureSearchHelper.py rename to code/backend/batch/utilities/helpers/AzureSearchHelper.py index 20dd6aa96..e2ef3f932 100644 --- a/code/utilities/helpers/AzureSearchHelper.py +++ b/code/backend/batch/utilities/helpers/AzureSearchHelper.py @@ -65,9 +65,11 @@ def get_vector_store(self): return AzureSearch( azure_search_endpoint=env_helper.AZURE_SEARCH_SERVICE, - azure_search_key=env_helper.AZURE_SEARCH_KEY - if env_helper.AZURE_AUTH_TYPE == "keys" - else None, + azure_search_key=( + env_helper.AZURE_SEARCH_KEY + if env_helper.AZURE_AUTH_TYPE == "keys" + else None + ), index_name=env_helper.AZURE_SEARCH_INDEX, embedding_function=llm_helper.get_embedding_model().embed_query, fields=fields, @@ -139,9 +141,11 @@ def get_conversation_logger(self): return AzureSearch( azure_search_endpoint=env_helper.AZURE_SEARCH_SERVICE, - azure_search_key=env_helper.AZURE_SEARCH_KEY - if env_helper.AZURE_AUTH_TYPE == "keys" - else None, + azure_search_key=( + env_helper.AZURE_SEARCH_KEY + if env_helper.AZURE_AUTH_TYPE == "keys" + else None + ), index_name=env_helper.AZURE_SEARCH_CONVERSATIONS_LOG_INDEX, embedding_function=llm_helper.get_embedding_model().embed_query, fields=fields, diff --git a/code/utilities/helpers/ConfigHelper.py b/code/backend/batch/utilities/helpers/ConfigHelper.py similarity index 100% rename from code/utilities/helpers/ConfigHelper.py rename to code/backend/batch/utilities/helpers/ConfigHelper.py diff --git a/code/utilities/helpers/DocumentChunkingHelper.py b/code/backend/batch/utilities/helpers/DocumentChunkingHelper.py similarity index 100% rename from code/utilities/helpers/DocumentChunkingHelper.py rename to code/backend/batch/utilities/helpers/DocumentChunkingHelper.py diff --git a/code/utilities/helpers/DocumentLoadingHelper.py b/code/backend/batch/utilities/helpers/DocumentLoadingHelper.py similarity index 100% rename from code/utilities/helpers/DocumentLoadingHelper.py rename to code/backend/batch/utilities/helpers/DocumentLoadingHelper.py diff --git a/code/utilities/helpers/DocumentProcessorHelper.py b/code/backend/batch/utilities/helpers/DocumentProcessorHelper.py similarity index 100% rename from code/utilities/helpers/DocumentProcessorHelper.py rename to code/backend/batch/utilities/helpers/DocumentProcessorHelper.py diff --git a/code/utilities/helpers/EnvHelper.py b/code/backend/batch/utilities/helpers/EnvHelper.py similarity index 63% rename from code/utilities/helpers/EnvHelper.py rename to code/backend/batch/utilities/helpers/EnvHelper.py index 0d0111fa6..5f7e93f33 100644 --- a/code/utilities/helpers/EnvHelper.py +++ b/code/backend/batch/utilities/helpers/EnvHelper.py @@ -1,7 +1,8 @@ import os import logging from dotenv import load_dotenv -from azure.identity import DefaultAzureCredential +from azure.identity import DefaultAzureCredential, get_bearer_token_provider +from azure.keyvault.secrets import SecretClient logger = logging.getLogger(__name__) @@ -9,10 +10,10 @@ class EnvHelper: def __init__(self, **kwargs) -> None: load_dotenv() + # Azure Search self.AZURE_SEARCH_SERVICE = os.getenv("AZURE_SEARCH_SERVICE", "") self.AZURE_SEARCH_INDEX = os.getenv("AZURE_SEARCH_INDEX", "") - self.AZURE_SEARCH_KEY = os.getenv("AZURE_SEARCH_KEY", "") self.AZURE_SEARCH_USE_SEMANTIC_SEARCH = os.getenv( "AZURE_SEARCH_USE_SEMANTIC_SEARCH", "" ) @@ -46,11 +47,10 @@ def __init__(self, **kwargs) -> None: self.AZURE_SEARCH_CONVERSATIONS_LOG_INDEX = os.getenv( "AZURE_SEARCH_CONVERSATIONS_LOG_INDEX", "conversations" ) - self.AZURE_AUTH_TYPE = os.environ.get("AZURE_AUTH_TYPE", "keys") + self.AZURE_AUTH_TYPE = os.getenv("AZURE_AUTH_TYPE", "keys") # Azure OpenAI self.AZURE_OPENAI_RESOURCE = os.getenv("AZURE_OPENAI_RESOURCE", "") self.AZURE_OPENAI_MODEL = os.getenv("AZURE_OPENAI_MODEL", "") - self.AZURE_OPENAI_KEY = os.getenv("AZURE_OPENAI_KEY", "") self.AZURE_OPENAI_MODEL_NAME = os.getenv("AZURE_OPENAI_MODEL_NAME", "") self.AZURE_OPENAI_TEMPERATURE = os.getenv("AZURE_OPENAI_TEMPERATURE", "") self.AZURE_OPENAI_TOP_P = os.getenv("AZURE_OPENAI_TOP_P", "") @@ -62,26 +62,48 @@ def __init__(self, **kwargs) -> None: self.AZURE_OPENAI_EMBEDDING_MODEL = os.getenv( "AZURE_OPENAI_EMBEDDING_MODEL", "" ) + self.AZURE_TOKEN_PROVIDER = get_bearer_token_provider( + DefaultAzureCredential(), "https://cognitiveservices.azure.com/.default" + ) + # Initialize Azure keys based on authentication type and environment settings. + # When AZURE_AUTH_TYPE is "rbac", azure keys are None or an empty string. + # When USE_KEY_VAULT environment variable is set, keys are securely fetched from Azure Key Vault using DefaultAzureCredential. + # Otherwise, keys are obtained from environment variables. + if self.AZURE_AUTH_TYPE == "rbac": + self.AZURE_SEARCH_KEY = None + self.AZURE_OPENAI_KEY = "" + self.AZURE_SPEECH_KEY = None + elif os.environ.get("USE_KEY_VAULT"): + credential = DefaultAzureCredential() + self.secret_client = SecretClient( + os.environ.get("AZURE_KEY_VAULT_ENDPOINT"), credential + ) + self.AZURE_SEARCH_KEY = self.secret_client.get_secret( + os.environ.get("AZURE_SEARCH_KEY") + ).value + self.AZURE_OPENAI_KEY = self.secret_client.get_secret( + os.environ.get("AZURE_OPENAI_KEY", "") + ).value + self.AZURE_SPEECH_KEY = self.secret_client.get_secret( + os.environ.get("AZURE_SPEECH_SERVICE_KEY") + ).value + else: + self.AZURE_SEARCH_KEY = os.environ.get("AZURE_SEARCH_KEY") + self.AZURE_OPENAI_KEY = os.environ.get("AZURE_OPENAI_KEY", "") + self.AZURE_SPEECH_KEY = os.environ.get("AZURE_SPEECH_SERVICE_KEY") # Set env for OpenAI SDK self.OPENAI_API_BASE = ( f"https://{os.getenv('AZURE_OPENAI_RESOURCE')}.openai.azure.com/" ) self.OPENAI_API_TYPE = "azure" if self.AZURE_AUTH_TYPE == "keys" else "azure_ad" - if self.AZURE_AUTH_TYPE == "keys": - self.OPENAI_API_KEY = self.AZURE_OPENAI_KEY - else: - self.OPENAI_API_KEY = ( - DefaultAzureCredential(exclude_shared_token_cache_credential=True) - .get_token("https://cognitiveservices.azure.com/.default") - .token - ) + self.OPENAI_API_KEY = self.AZURE_OPENAI_KEY self.OPENAI_API_VERSION = self.AZURE_OPENAI_API_VERSION os.environ["OPENAI_API_TYPE"] = self.OPENAI_API_TYPE - os.environ[ - "OPENAI_API_BASE" - ] = f"https://{os.getenv('AZURE_OPENAI_RESOURCE')}.openai.azure.com/" + os.environ["OPENAI_API_BASE"] = ( + f"https://{os.getenv('AZURE_OPENAI_RESOURCE')}.openai.azure.com/" + ) os.environ["OPENAI_API_KEY"] = self.OPENAI_API_KEY - os.environ["OPENAI_API_VERSION"] = self.AZURE_OPENAI_API_VERSION + os.environ["OPENAI_API_VERSION"] = self.OPENAI_API_VERSION # Azure Functions - Batch processing self.BACKEND_URL = os.getenv("BACKEND_URL", "") self.AzureWebJobsStorage = os.getenv("AzureWebJobsStorage", "") @@ -90,13 +112,23 @@ def __init__(self, **kwargs) -> None: ) # Azure Blob Storage self.AZURE_BLOB_ACCOUNT_NAME = os.getenv("AZURE_BLOB_ACCOUNT_NAME", "") - self.AZURE_BLOB_ACCOUNT_KEY = os.getenv("AZURE_BLOB_ACCOUNT_KEY", "") + self.AZURE_BLOB_ACCOUNT_KEY = ( + self.secret_client.get_secret(os.getenv("AZURE_BLOB_ACCOUNT_KEY", "")).value + if os.getenv("USE_KEY_VAULT", "") + else os.getenv("AZURE_BLOB_ACCOUNT_KEY", "") + ) self.AZURE_BLOB_CONTAINER_NAME = os.getenv("AZURE_BLOB_CONTAINER_NAME", "") # Azure Form Recognizer self.AZURE_FORM_RECOGNIZER_ENDPOINT = os.getenv( "AZURE_FORM_RECOGNIZER_ENDPOINT", "" ) - self.AZURE_FORM_RECOGNIZER_KEY = os.getenv("AZURE_FORM_RECOGNIZER_KEY", "") + self.AZURE_FORM_RECOGNIZER_KEY = ( + self.secret_client.get_secret( + os.getenv("AZURE_FORM_RECOGNIZER_KEY", "") + ).value + if os.getenv("USE_KEY_VAULT", "") + else os.getenv("AZURE_FORM_RECOGNIZER_KEY", "") + ) # Azure App Insights self.APPINSIGHTS_CONNECTION_STRING = os.getenv( "APPINSIGHTS_CONNECTION_STRING", "" @@ -110,7 +142,13 @@ def __init__(self, **kwargs) -> None: and "api.cognitive.microsoft.com" not in self.AZURE_CONTENT_SAFETY_ENDPOINT ): self.AZURE_CONTENT_SAFETY_ENDPOINT = self.AZURE_FORM_RECOGNIZER_ENDPOINT - self.AZURE_CONTENT_SAFETY_KEY = os.getenv("AZURE_CONTENT_SAFETY_KEY", "") + self.AZURE_CONTENT_SAFETY_KEY = ( + self.secret_client.get_secret( + os.getenv("AZURE_CONTENT_SAFETY_KEY", "") + ).value + if os.getenv("USE_KEY_VAULT", "") + else os.getenv("AZURE_CONTENT_SAFETY_KEY", "") + ) # Orchestration Settings self.ORCHESTRATION_STRATEGY = os.getenv( "ORCHESTRATION_STRATEGY", "openai_function" diff --git a/code/backend/batch/utilities/helpers/LLMHelper.py b/code/backend/batch/utilities/helpers/LLMHelper.py new file mode 100644 index 000000000..0e42517d1 --- /dev/null +++ b/code/backend/batch/utilities/helpers/LLMHelper.py @@ -0,0 +1,101 @@ +from openai import AzureOpenAI +from typing import List +from langchain.chat_models import AzureChatOpenAI +from langchain.embeddings import AzureOpenAIEmbeddings +from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler +from .EnvHelper import EnvHelper + + +class LLMHelper: + def __init__(self): + env_helper: EnvHelper = EnvHelper() + self.auth_type = env_helper.AZURE_AUTH_TYPE + self.token_provider = env_helper.AZURE_TOKEN_PROVIDER + + if self.auth_type == "rbac": + self.openai_client = AzureOpenAI( + azure_endpoint=env_helper.OPENAI_API_BASE, + api_version=env_helper.AZURE_OPENAI_API_VERSION, + azure_ad_token_provider=self.token_provider, + ) + else: + self.openai_client = AzureOpenAI( + azure_endpoint=env_helper.OPENAI_API_BASE, + api_version=env_helper.AZURE_OPENAI_API_VERSION, + api_key=env_helper.OPENAI_API_KEY, + ) + + self.llm_model = env_helper.AZURE_OPENAI_MODEL + self.llm_max_tokens = ( + env_helper.AZURE_OPENAI_MAX_TOKENS + if env_helper.AZURE_OPENAI_MAX_TOKENS != "" + else None + ) + self.embedding_model = env_helper.AZURE_OPENAI_EMBEDDING_MODEL + + def get_llm(self): + if self.auth_type == "rbac": + return AzureChatOpenAI( + deployment_name=self.llm_model, + temperature=0, + max_tokens=self.llm_max_tokens, + openai_api_version=self.openai_client._api_version, + azure_ad_token_provider=self.token_provider, + ) + else: + return AzureChatOpenAI( + deployment_name=self.llm_model, + temperature=0, + max_tokens=self.llm_max_tokens, + openai_api_version=self.openai_client._api_version, + ) + + # TODO: This needs to have a custom callback to stream back to the UI + def get_streaming_llm(self): + if self.auth_type == "rbac": + return AzureChatOpenAI( + streaming=True, + callbacks=[StreamingStdOutCallbackHandler], + deployment_name=self.llm_model, + temperature=0, + max_tokens=self.llm_max_tokens, + openai_api_version=self.openai_client._api_version, + azure_ad_token_provider=self.token_provider, + ) + else: + return AzureChatOpenAI( + streaming=True, + callbacks=[StreamingStdOutCallbackHandler], + deployment_name=self.llm_model, + temperature=0, + max_tokens=self.llm_max_tokens, + openai_api_version=self.openai_client._api_version, + ) + + def get_embedding_model(self): + if self.auth_type == "rbac": + return AzureOpenAIEmbeddings( + azure_deployment=self.embedding_model, + chunk_size=1, + azure_ad_token_provider=self.token_provider, + ) + else: + return AzureOpenAIEmbeddings( + azure_deployment=self.embedding_model, chunk_size=1 + ) + + def get_chat_completion_with_functions( + self, messages: List[dict], functions: List[dict], function_call: str = "auto" + ): + return self.openai_client.chat.completions.create( + model=self.llm_model, + messages=messages, + functions=functions, + function_call=function_call, + ) + + def get_chat_completion(self, messages: List[dict]): + return self.openai_client.chat.completions.create( + model=self.llm_model, + messages=messages, + ) diff --git a/code/utilities/helpers/OrchestratorHelper.py b/code/backend/batch/utilities/helpers/OrchestratorHelper.py similarity index 100% rename from code/utilities/helpers/OrchestratorHelper.py rename to code/backend/batch/utilities/helpers/OrchestratorHelper.py diff --git a/code/utilities/common/__init__.py b/code/backend/batch/utilities/helpers/__init__.py similarity index 100% rename from code/utilities/common/__init__.py rename to code/backend/batch/utilities/helpers/__init__.py diff --git a/code/utilities/loggers/ConversationLogger.py b/code/backend/batch/utilities/loggers/ConversationLogger.py similarity index 100% rename from code/utilities/loggers/ConversationLogger.py rename to code/backend/batch/utilities/loggers/ConversationLogger.py diff --git a/code/utilities/loggers/TokenLogger.py b/code/backend/batch/utilities/loggers/TokenLogger.py similarity index 100% rename from code/utilities/loggers/TokenLogger.py rename to code/backend/batch/utilities/loggers/TokenLogger.py diff --git a/code/utilities/orchestrator/LangChainAgent.py b/code/backend/batch/utilities/orchestrator/LangChainAgent.py similarity index 100% rename from code/utilities/orchestrator/LangChainAgent.py rename to code/backend/batch/utilities/orchestrator/LangChainAgent.py diff --git a/code/utilities/orchestrator/OpenAIFunctions.py b/code/backend/batch/utilities/orchestrator/OpenAIFunctions.py similarity index 88% rename from code/utilities/orchestrator/OpenAIFunctions.py rename to code/backend/batch/utilities/orchestrator/OpenAIFunctions.py index ab7c8b8f4..9d35cad26 100644 --- a/code/utilities/orchestrator/OpenAIFunctions.py +++ b/code/backend/batch/utilities/orchestrator/OpenAIFunctions.py @@ -88,16 +88,16 @@ def orchestrate( messages, self.functions, function_call="auto" ) self.log_tokens( - prompt_tokens=result["usage"]["prompt_tokens"], - completion_tokens=result["usage"]["completion_tokens"], + prompt_tokens=result.usage.prompt_tokens, + completion_tokens=result.usage.completion_tokens, ) # TODO: call content safety if needed - if result["choices"][0]["finish_reason"] == "function_call": - if result["choices"][0]["message"].function_call.name == "search_documents": + if result.choices[0].finish_reason == "function_call": + if result.choices[0].message.function_call.name == "search_documents": question = json.loads( - result["choices"][0]["message"]["function_call"]["arguments"] + result.choices[0].message.function_call.arguments )["question"] # run answering chain answering_tool = QuestionAnswerTool() @@ -116,14 +116,12 @@ def orchestrate( prompt_tokens=answer.prompt_tokens, completion_tokens=answer.completion_tokens, ) - elif ( - result["choices"][0]["message"].function_call.name == "text_processing" - ): - text = json.loads( - result["choices"][0]["message"]["function_call"]["arguments"] - )["text"] + elif result.choices[0].message.function_call.name == "text_processing": + text = json.loads(result.choices[0].message.function_call.arguments)[ + "text" + ] operation = json.loads( - result["choices"][0]["message"]["function_call"]["arguments"] + result.choices[0].message.function_call.arguments )["operation"] text_processing_tool = TextProcessingTool() answer = text_processing_tool.answer_question( @@ -134,7 +132,7 @@ def orchestrate( completion_tokens=answer.completion_tokens, ) else: - text = result["choices"][0]["message"]["content"] + text = result.choices[0].message.content answer = Answer(question=user_message, answer=text) # Call Content Safety tool diff --git a/code/utilities/orchestrator/OrchestratorBase.py b/code/backend/batch/utilities/orchestrator/OrchestratorBase.py similarity index 100% rename from code/utilities/orchestrator/OrchestratorBase.py rename to code/backend/batch/utilities/orchestrator/OrchestratorBase.py diff --git a/code/utilities/orchestrator/Strategies.py b/code/backend/batch/utilities/orchestrator/Strategies.py similarity index 100% rename from code/utilities/orchestrator/Strategies.py rename to code/backend/batch/utilities/orchestrator/Strategies.py diff --git a/code/utilities/orchestrator/__init__.py b/code/backend/batch/utilities/orchestrator/__init__.py similarity index 100% rename from code/utilities/orchestrator/__init__.py rename to code/backend/batch/utilities/orchestrator/__init__.py diff --git a/code/utilities/parser/OutputParserTool.py b/code/backend/batch/utilities/parser/OutputParserTool.py similarity index 100% rename from code/utilities/parser/OutputParserTool.py rename to code/backend/batch/utilities/parser/OutputParserTool.py diff --git a/code/utilities/parser/ParserBase.py b/code/backend/batch/utilities/parser/ParserBase.py similarity index 100% rename from code/utilities/parser/ParserBase.py rename to code/backend/batch/utilities/parser/ParserBase.py diff --git a/code/utilities/parser/__init__.py b/code/backend/batch/utilities/parser/__init__.py similarity index 100% rename from code/utilities/parser/__init__.py rename to code/backend/batch/utilities/parser/__init__.py diff --git a/code/utilities/tools/AnswerProcessingBase.py b/code/backend/batch/utilities/tools/AnswerProcessingBase.py similarity index 100% rename from code/utilities/tools/AnswerProcessingBase.py rename to code/backend/batch/utilities/tools/AnswerProcessingBase.py diff --git a/code/utilities/tools/AnsweringToolBase.py b/code/backend/batch/utilities/tools/AnsweringToolBase.py similarity index 100% rename from code/utilities/tools/AnsweringToolBase.py rename to code/backend/batch/utilities/tools/AnsweringToolBase.py diff --git a/code/utilities/tools/ContentSafetyChecker.py b/code/backend/batch/utilities/tools/ContentSafetyChecker.py similarity index 67% rename from code/utilities/tools/ContentSafetyChecker.py rename to code/backend/batch/utilities/tools/ContentSafetyChecker.py index 2c6fa518a..b2ef13564 100644 --- a/code/utilities/tools/ContentSafetyChecker.py +++ b/code/backend/batch/utilities/tools/ContentSafetyChecker.py @@ -1,7 +1,8 @@ from azure.ai.contentsafety import ContentSafetyClient from azure.core.credentials import AzureKeyCredential +from azure.identity import DefaultAzureCredential from azure.core.exceptions import HttpResponseError -from azure.ai.contentsafety.models import AnalyzeTextOptions, TextCategory +from azure.ai.contentsafety.models import AnalyzeTextOptions from ..helpers.EnvHelper import EnvHelper from .AnswerProcessingBase import AnswerProcessingBase from ..common.Answer import Answer @@ -10,10 +11,17 @@ class ContentSafetyChecker(AnswerProcessingBase): def __init__(self): env_helper = EnvHelper() - self.content_safety_client = ContentSafetyClient( - env_helper.AZURE_CONTENT_SAFETY_ENDPOINT, - AzureKeyCredential(env_helper.AZURE_CONTENT_SAFETY_KEY), - ) + + if env_helper.AZURE_AUTH_TYPE == "rbac": + self.content_safety_client = ContentSafetyClient( + env_helper.AZURE_CONTENT_SAFETY_ENDPOINT, + DefaultAzureCredential(), + ) + else: + self.content_safety_client = ContentSafetyClient( + env_helper.AZURE_CONTENT_SAFETY_ENDPOINT, + AzureKeyCredential(env_helper.AZURE_CONTENT_SAFETY_KEY), + ) def process_answer(self, answer: Answer, **kwargs: dict) -> Answer: response_template = kwargs["response_template"] @@ -48,18 +56,11 @@ def _filter_text_and_replace(self, text, response_template): raise filtered_text = text - - hate_result = next(item for item in response.categories_analysis if item.category == TextCategory.HATE) - self_harm_result = next(item for item in response.categories_analysis if item.category == TextCategory.SELF_HARM) - sexual_result = next(item for item in response.categories_analysis if item.category == TextCategory.SEXUAL) - violence_result = next(item for item in response.categories_analysis if item.category == TextCategory.VIOLENCE) - - if ( - hate_result.severity > 0 - or self_harm_result.severity > 0 - or sexual_result.severity > 0 - or violence_result.severity > 0 - ): - filtered_text = response_template + + # if response.hate_result.severity > 0 or response.self_harm_result.severity > 0 or response.sexual_result.severity > 0 or response.violence_result.severity > 0: + # filtered_text = response_template + for result in response.categories_analysis: + if result.severity > 0: + filtered_text = response_template return filtered_text diff --git a/code/utilities/tools/PostPromptTool.py b/code/backend/batch/utilities/tools/PostPromptTool.py similarity index 100% rename from code/utilities/tools/PostPromptTool.py rename to code/backend/batch/utilities/tools/PostPromptTool.py diff --git a/code/utilities/tools/QuestionAnswerTool.py b/code/backend/batch/utilities/tools/QuestionAnswerTool.py similarity index 100% rename from code/utilities/tools/QuestionAnswerTool.py rename to code/backend/batch/utilities/tools/QuestionAnswerTool.py diff --git a/code/utilities/tools/TextProcessingTool.py b/code/backend/batch/utilities/tools/TextProcessingTool.py similarity index 83% rename from code/utilities/tools/TextProcessingTool.py rename to code/backend/batch/utilities/tools/TextProcessingTool.py index 2da77ca2b..3cc4506b5 100644 --- a/code/utilities/tools/TextProcessingTool.py +++ b/code/backend/batch/utilities/tools/TextProcessingTool.py @@ -27,9 +27,9 @@ def answer_question(self, question: str, chat_history: List[dict], **kwargs: dic answer = Answer( question=question, - answer=result["choices"][0]["message"]["content"], + answer=result.choices[0].message.content, source_documents=[], - prompt_tokens=result["usage"]["prompt_tokens"], - completion_tokens=result["usage"]["completion_tokens"], + prompt_tokens=result.usage.prompt_tokens, + completion_tokens=result.usage.completion_tokens, ) return answer diff --git a/code/utilities/helpers/__init__.py b/code/backend/batch/utilities/tools/__init__.py similarity index 100% rename from code/utilities/helpers/__init__.py rename to code/backend/batch/utilities/tools/__init__.py diff --git a/code/admin/images/favicon.ico b/code/backend/images/favicon.ico similarity index 100% rename from code/admin/images/favicon.ico rename to code/backend/images/favicon.ico diff --git a/code/admin/images/logo.png b/code/backend/images/logo.png similarity index 100% rename from code/admin/images/logo.png rename to code/backend/images/logo.png diff --git a/code/admin/pages/01_Ingest_Data.py b/code/backend/pages/01_Ingest_Data.py similarity index 57% rename from code/admin/pages/01_Ingest_Data.py rename to code/backend/pages/01_Ingest_Data.py index 9b25b2a80..8d611abfe 100644 --- a/code/admin/pages/01_Ingest_Data.py +++ b/code/backend/pages/01_Ingest_Data.py @@ -7,10 +7,17 @@ from datetime import datetime, timedelta import logging import requests -from azure.storage.blob import BlobServiceClient, generate_blob_sas, ContentSettings +from azure.identity import DefaultAzureCredential +from azure.keyvault.secrets import SecretClient +from azure.storage.blob import ( + BlobServiceClient, + generate_blob_sas, + ContentSettings, + UserDelegationKey, +) import urllib.parse import sys -from utilities.helpers.ConfigHelper import ConfigHelper +from batch.utilities.helpers.ConfigHelper import ConfigHelper from dotenv import load_dotenv sys.path.append(os.path.join(os.path.dirname(__file__), "..")) @@ -36,6 +43,21 @@ st.markdown(mod_page_style, unsafe_allow_html=True) +def request_user_delegation_key( + blob_service_client: BlobServiceClient, +) -> UserDelegationKey: + # Get a user delegation key that's valid for 1 day + delegation_key_start_time = datetime.utcnow() + delegation_key_expiry_time = delegation_key_start_time + timedelta(days=1) + + user_delegation_key = blob_service_client.get_user_delegation_key( + key_start_time=delegation_key_start_time, + key_expiry_time=delegation_key_expiry_time, + ) + + return user_delegation_key + + def remote_convert_files_and_add_embeddings(process_all=False): backend_url = urllib.parse.urljoin( os.getenv("BACKEND_URL", "http://localhost:7071"), "/api/BatchStartProcessing" @@ -88,39 +110,79 @@ def upload_file(bytes_data: bytes, file_name: str, content_type: Optional[str] = ) content_type = content_type if content_type is not None else "text/plain" account_name = os.getenv("AZURE_BLOB_ACCOUNT_NAME") - account_key = os.getenv("AZURE_BLOB_ACCOUNT_KEY") - container_name = os.getenv("AZURE_BLOB_CONTAINER_NAME") - if account_name is None or account_key is None or container_name is None: - raise ValueError( - "Please provide values for AZURE_BLOB_ACCOUNT_NAME, AZURE_BLOB_ACCOUNT_KEY and AZURE_BLOB_CONTAINER_NAME" + if os.environ.get("AUTH_TYPE") == "rbac": + credential = DefaultAzureCredential() + account_url = f"https://{account_name}.blob.core.windows.net/" + blob_service_client = BlobServiceClient( + account_url=account_url, credential=credential ) - connect_str = f"DefaultEndpointsProtocol=https;AccountName={account_name};AccountKey={account_key};EndpointSuffix=core.windows.net" - blob_service_client: BlobServiceClient = BlobServiceClient.from_connection_string( - connect_str - ) - # Create a blob client using the local file name as the name for the blob - blob_client = blob_service_client.get_blob_client( - container=container_name, blob=file_name - ) - # Upload the created file - blob_client.upload_blob( - bytes_data, - overwrite=True, - content_settings=ContentSettings(content_type=content_type + charset), - ) - # Generate a SAS URL to the blob and return it - st.session_state["file_url"] = ( - blob_client.url - + "?" - + generate_blob_sas( - account_name, - container_name, - file_name, - account_key=account_key, - permission="r", - expiry=datetime.utcnow() + timedelta(hours=3), + user_delegation_key = request_user_delegation_key( + blob_service_client=blob_service_client + ) + container_name = os.getenv("AZURE_BLOB_CONTAINER_NAME") + blob_client = blob_service_client.get_blob_client( + container=container_name, blob=file_name + ) + blob_client.upload_blob( + bytes_data, + overwrite=True, + content_settings=ContentSettings(content_type=content_type + charset), + ) + st.session_state["file_url"] = ( + blob_client.url + + "?" + + generate_blob_sas( + account_name, + container_name, + file_name, + user_delegation_key=user_delegation_key, + permission="r", + expiry=datetime.utcnow() + timedelta(hours=3), + ) + ) + else: + if os.environ.get("USE_KEY_VAULT"): + credential = DefaultAzureCredential() + secret_client = SecretClient( + os.environ.get("AZURE_KEY_VAULT_ENDPOINT"), credential + ) + account_key = ( + secret_client.get_secret(os.getenv("AZURE_BLOB_ACCOUNT_KEY")).value + if os.getenv("USE_KEY_VAULT") + else os.getenv("AZURE_BLOB_ACCOUNT_KEY") + ) + container_name = os.getenv("AZURE_BLOB_CONTAINER_NAME") + if account_name is None or account_key is None or container_name is None: + raise ValueError( + "Please provide values for AZURE_BLOB_ACCOUNT_NAME, AZURE_BLOB_ACCOUNT_KEY and AZURE_BLOB_CONTAINER_NAME" + ) + connect_str = f"DefaultEndpointsProtocol=https;AccountName={account_name};AccountKey={account_key};EndpointSuffix=core.windows.net" + blob_service_client: BlobServiceClient = ( + BlobServiceClient.from_connection_string(connect_str) + ) + # Create a blob client using the local file name as the name for the blob + blob_client = blob_service_client.get_blob_client( + container=container_name, blob=file_name + ) + # Upload the created file + blob_client.upload_blob( + bytes_data, + overwrite=True, + content_settings=ContentSettings(content_type=content_type + charset), + ) + # Generate a SAS URL to the blob and return it + st.session_state["file_url"] = ( + blob_client.url + + "?" + + generate_blob_sas( + account_name, + container_name, + file_name, + account_key=account_key, + permission="r", + expiry=datetime.utcnow() + timedelta(hours=3), + ) ) - ) try: diff --git a/code/admin/pages/02_Explore_Data.py b/code/backend/pages/02_Explore_Data.py similarity index 96% rename from code/admin/pages/02_Explore_Data.py rename to code/backend/pages/02_Explore_Data.py index 9231371dc..b4d89a4bf 100644 --- a/code/admin/pages/02_Explore_Data.py +++ b/code/backend/pages/02_Explore_Data.py @@ -5,7 +5,7 @@ import logging import pandas as pd import sys -from utilities.helpers.AzureSearchHelper import AzureSearchHelper +from batch.utilities.helpers.AzureSearchHelper import AzureSearchHelper from dotenv import load_dotenv sys.path.append(os.path.join(os.path.dirname(__file__), "..")) diff --git a/code/admin/pages/03_Delete_Data.py b/code/backend/pages/03_Delete_Data.py similarity index 97% rename from code/admin/pages/03_Delete_Data.py rename to code/backend/pages/03_Delete_Data.py index 993804a62..911185cf6 100644 --- a/code/admin/pages/03_Delete_Data.py +++ b/code/backend/pages/03_Delete_Data.py @@ -3,8 +3,7 @@ import traceback import logging import sys - -from utilities.helpers.AzureSearchHelper import AzureSearchHelper +from batch.utilities.helpers.AzureSearchHelper import AzureSearchHelper from dotenv import load_dotenv sys.path.append(os.path.join(os.path.dirname(__file__), "..")) diff --git a/code/admin/pages/04_Configuration.py b/code/backend/pages/04_Configuration.py similarity index 96% rename from code/admin/pages/04_Configuration.py rename to code/backend/pages/04_Configuration.py index 09e604d94..ed0a255bc 100644 --- a/code/admin/pages/04_Configuration.py +++ b/code/backend/pages/04_Configuration.py @@ -4,8 +4,7 @@ import logging from dotenv import load_dotenv import sys - -from utilities.helpers.ConfigHelper import ConfigHelper +from batch.utilities.helpers.ConfigHelper import ConfigHelper sys.path.append(os.path.join(os.path.dirname(__file__), "..")) @@ -40,13 +39,13 @@ if "post_answering_prompt" not in st.session_state: st.session_state["post_answering_prompt"] = config.prompts.post_answering_prompt if "enable_post_answering_prompt" not in st.session_state: - st.session_state[ - "enable_post_answering_prompt" - ] = config.prompts.enable_post_answering_prompt + st.session_state["enable_post_answering_prompt"] = ( + config.prompts.enable_post_answering_prompt + ) if "post_answering_filter_message" not in st.session_state: - st.session_state[ - "post_answering_filter_message" - ] = config.messages.post_answering_filter + st.session_state["post_answering_filter_message"] = ( + config.messages.post_answering_filter + ) if "enable_content_safety" not in st.session_state: st.session_state["enable_content_safety"] = config.prompts.enable_content_safety diff --git a/code/backend/requirements.txt b/code/backend/requirements.txt new file mode 100644 index 000000000..320a47e88 --- /dev/null +++ b/code/backend/requirements.txt @@ -0,0 +1,24 @@ +azure-functions==1.18.0 +streamlit==1.30.0 +openai==1.6.1 +scipy==1.12.0 +transformers==4.37.2 +python-dotenv==1.0.1 +azure-ai-formrecognizer==3.3.2 +azure-storage-blob==12.19.0 +azure-identity==1.15.0 +azure-ai-contentsafety==1.0.0 +requests==2.31.0 +tiktoken==0.6.0 +azure-storage-queue==12.9.0 +langchain==0.1.4 +langchain-community==0.0.19 +beautifulsoup4==4.12.3 +fake-useragent==1.4.0 +chardet==5.2.0 +--extra-index-url https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/ +azure-search-documents==11.4.0b8 +opencensus-ext-azure==1.1.13 +pandas==2.2.0 +python-docx==1.1.0 +azure-keyvault-secrets==4.4.* diff --git a/code/app/frontend/index.html b/code/frontend/index.html similarity index 100% rename from code/app/frontend/index.html rename to code/frontend/index.html diff --git a/code/app/frontend/package-lock.json b/code/frontend/package-lock.json similarity index 99% rename from code/app/frontend/package-lock.json rename to code/frontend/package-lock.json index b515da0a2..072f039c6 100644 --- a/code/app/frontend/package-lock.json +++ b/code/frontend/package-lock.json @@ -11,6 +11,7 @@ "@babel/traverse": "^7.23.2", "@fluentui/react": "^8.105.3", "@fluentui/react-icons": "^2.0.195", + "@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/react-fontawesome": "github:fortawesome/react-fontawesome", "lodash": "^4.17.21", @@ -955,18 +956,26 @@ } }, "node_modules/@fortawesome/fontawesome-svg-core": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.0.tgz", - "integrity": "sha512-5DrR+oxQr+ruRQ3CEVV8DSCT/q8Atm56+FzAs0P6eW/epW47OmecSpSwc/YTlJ3u5BfPKUBSGyPR2qjZ+5eIgA==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz", + "integrity": "sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==", "hasInstallScript": true, - "peer": true, "dependencies": { - "@fortawesome/fontawesome-common-types": "6.5.0" + "@fortawesome/fontawesome-common-types": "6.5.1" }, "engines": { "node": ">=6" } }, + "node_modules/@fortawesome/fontawesome-svg-core/node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz", + "integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==", + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, "node_modules/@fortawesome/free-solid-svg-icons": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.0.tgz", @@ -1150,12 +1159,14 @@ "node_modules/@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "dev": true }, "node_modules/@types/react": { "version": "18.2.11", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.11.tgz", "integrity": "sha512-+hsJr9hmwyDecSMQAmX7drgbDpyE+EgSF6t7+5QEBAn1tQK7kl1vWZ4iRf6SjQ8lk7dyEULxUmZOIpN0W5baZA==", + "dev": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -1166,6 +1177,7 @@ "version": "18.2.4", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.4.tgz", "integrity": "sha512-G2mHoTMTL4yoydITgOGwWdWMVd8sNgyEP85xVmMKAPUBwQWm9wBPQUmvbeF4V3WBY1P7mmL4BkjQ0SqUpf1snw==", + "dev": true, "dependencies": { "@types/react": "*" } @@ -1173,7 +1185,8 @@ "node_modules/@types/scheduler": { "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", + "dev": true }, "node_modules/@types/unist": { "version": "2.0.6", diff --git a/code/app/frontend/package.json b/code/frontend/package.json similarity index 95% rename from code/app/frontend/package.json rename to code/frontend/package.json index 492092f24..40da94c61 100644 --- a/code/app/frontend/package.json +++ b/code/frontend/package.json @@ -12,6 +12,7 @@ "@babel/traverse": "^7.23.2", "@fluentui/react": "^8.105.3", "@fluentui/react-icons": "^2.0.195", + "@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/react-fontawesome": "github:fortawesome/react-fontawesome", "lodash": "^4.17.21", diff --git a/code/app/frontend/public/favicon.ico b/code/frontend/public/favicon.ico similarity index 100% rename from code/app/frontend/public/favicon.ico rename to code/frontend/public/favicon.ico diff --git a/code/app/frontend/src/api/api.ts b/code/frontend/src/api/api.ts similarity index 100% rename from code/app/frontend/src/api/api.ts rename to code/frontend/src/api/api.ts diff --git a/code/app/frontend/src/api/index.ts b/code/frontend/src/api/index.ts similarity index 100% rename from code/app/frontend/src/api/index.ts rename to code/frontend/src/api/index.ts diff --git a/code/app/frontend/src/api/models.ts b/code/frontend/src/api/models.ts similarity index 100% rename from code/app/frontend/src/api/models.ts rename to code/frontend/src/api/models.ts diff --git a/code/app/frontend/src/assets/Azure.svg b/code/frontend/src/assets/Azure.svg similarity index 100% rename from code/app/frontend/src/assets/Azure.svg rename to code/frontend/src/assets/Azure.svg diff --git a/code/app/frontend/src/assets/Send.svg b/code/frontend/src/assets/Send.svg similarity index 100% rename from code/app/frontend/src/assets/Send.svg rename to code/frontend/src/assets/Send.svg diff --git a/code/app/frontend/src/assets/mic-outline.svg b/code/frontend/src/assets/mic-outline.svg similarity index 100% rename from code/app/frontend/src/assets/mic-outline.svg rename to code/frontend/src/assets/mic-outline.svg diff --git a/code/app/frontend/src/components/Answer/Answer.module.css b/code/frontend/src/components/Answer/Answer.module.css similarity index 100% rename from code/app/frontend/src/components/Answer/Answer.module.css rename to code/frontend/src/components/Answer/Answer.module.css diff --git a/code/app/frontend/src/components/Answer/Answer.tsx b/code/frontend/src/components/Answer/Answer.tsx similarity index 100% rename from code/app/frontend/src/components/Answer/Answer.tsx rename to code/frontend/src/components/Answer/Answer.tsx diff --git a/code/app/frontend/src/components/Answer/AnswerParser.tsx b/code/frontend/src/components/Answer/AnswerParser.tsx similarity index 100% rename from code/app/frontend/src/components/Answer/AnswerParser.tsx rename to code/frontend/src/components/Answer/AnswerParser.tsx diff --git a/code/app/frontend/src/components/Answer/index.ts b/code/frontend/src/components/Answer/index.ts similarity index 100% rename from code/app/frontend/src/components/Answer/index.ts rename to code/frontend/src/components/Answer/index.ts diff --git a/code/app/frontend/src/components/QuestionInput/QuestionInput.module.css b/code/frontend/src/components/QuestionInput/QuestionInput.module.css similarity index 100% rename from code/app/frontend/src/components/QuestionInput/QuestionInput.module.css rename to code/frontend/src/components/QuestionInput/QuestionInput.module.css diff --git a/code/app/frontend/src/components/QuestionInput/QuestionInput.tsx b/code/frontend/src/components/QuestionInput/QuestionInput.tsx similarity index 100% rename from code/app/frontend/src/components/QuestionInput/QuestionInput.tsx rename to code/frontend/src/components/QuestionInput/QuestionInput.tsx diff --git a/code/app/frontend/src/components/QuestionInput/index.ts b/code/frontend/src/components/QuestionInput/index.ts similarity index 100% rename from code/app/frontend/src/components/QuestionInput/index.ts rename to code/frontend/src/components/QuestionInput/index.ts diff --git a/code/app/frontend/src/index.css b/code/frontend/src/index.css similarity index 100% rename from code/app/frontend/src/index.css rename to code/frontend/src/index.css diff --git a/code/app/frontend/src/index.tsx b/code/frontend/src/index.tsx similarity index 100% rename from code/app/frontend/src/index.tsx rename to code/frontend/src/index.tsx diff --git a/code/app/frontend/src/pages/NoPage.tsx b/code/frontend/src/pages/NoPage.tsx similarity index 100% rename from code/app/frontend/src/pages/NoPage.tsx rename to code/frontend/src/pages/NoPage.tsx diff --git a/code/app/frontend/src/pages/chat/Chat.module.css b/code/frontend/src/pages/chat/Chat.module.css similarity index 100% rename from code/app/frontend/src/pages/chat/Chat.module.css rename to code/frontend/src/pages/chat/Chat.module.css diff --git a/code/app/frontend/src/pages/chat/Chat.tsx b/code/frontend/src/pages/chat/Chat.tsx similarity index 100% rename from code/app/frontend/src/pages/chat/Chat.tsx rename to code/frontend/src/pages/chat/Chat.tsx diff --git a/code/app/frontend/src/pages/layout/Layout.module.css b/code/frontend/src/pages/layout/Layout.module.css similarity index 100% rename from code/app/frontend/src/pages/layout/Layout.module.css rename to code/frontend/src/pages/layout/Layout.module.css diff --git a/code/app/frontend/src/pages/layout/Layout.tsx b/code/frontend/src/pages/layout/Layout.tsx similarity index 100% rename from code/app/frontend/src/pages/layout/Layout.tsx rename to code/frontend/src/pages/layout/Layout.tsx diff --git a/code/app/frontend/src/vite-env.d.ts b/code/frontend/src/vite-env.d.ts similarity index 100% rename from code/app/frontend/src/vite-env.d.ts rename to code/frontend/src/vite-env.d.ts diff --git a/code/app/frontend/tsconfig.json b/code/frontend/tsconfig.json similarity index 100% rename from code/app/frontend/tsconfig.json rename to code/frontend/tsconfig.json diff --git a/code/app/frontend/tsconfig.node.json b/code/frontend/tsconfig.node.json similarity index 100% rename from code/app/frontend/tsconfig.node.json rename to code/frontend/tsconfig.node.json diff --git a/code/app/frontend/vite.config.ts b/code/frontend/vite.config.ts similarity index 100% rename from code/app/frontend/vite.config.ts rename to code/frontend/vite.config.ts diff --git a/code/requirements.txt b/code/requirements.txt index 03703c69f..4de2def73 100644 --- a/code/requirements.txt +++ b/code/requirements.txt @@ -1,22 +1,27 @@ azure-functions==1.18.0 streamlit==1.30.0 -openai==0.27.8 scipy==1.12.0 transformers==4.37.2 python-dotenv==1.0.1 azure-ai-formrecognizer==3.3.2 azure-storage-blob==12.19.0 azure-identity==1.15.0 -azure-ai-contentsafety==1.0.0 +Flask==3.0.2 +openai==1.6.1 +azure-storage-blob==12.19.0 +python-dotenv==1.0.1 +langchain==0.1.4 +langchain-community==0.0.19 +azure-ai-formrecognizer==3.3.2 requests==2.31.0 tiktoken==0.6.0 azure-storage-queue==12.9.0 -langchain==0.1.4 beautifulsoup4==4.12.3 fake-useragent==1.4.0 chardet==5.2.0 --extra-index-url https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/ azure-search-documents==11.4.0b8 opencensus-ext-azure==1.1.13 -pandas==2.2.0 +azure-ai-contentsafety==1.0.0 python-docx==1.1.0 +azure-keyvault-secrets==4.4.* \ No newline at end of file diff --git a/tests/conftest.py b/code/tests/conftest.py similarity index 100% rename from tests/conftest.py rename to code/tests/conftest.py diff --git a/tests/test_AzureBlobStorage.py b/code/tests/test_AzureBlobStorage.py similarity index 85% rename from tests/test_AzureBlobStorage.py rename to code/tests/test_AzureBlobStorage.py index 2743bb932..307c42972 100644 --- a/tests/test_AzureBlobStorage.py +++ b/code/tests/test_AzureBlobStorage.py @@ -1,5 +1,7 @@ import pytest -from code.utilities.helpers.AzureBlobStorageHelper import AzureBlobStorageClient +from backend.batch.utilities.helpers.AzureBlobStorageHelper import ( + AzureBlobStorageClient, +) @pytest.fixture diff --git a/tests/test_ContentSafetyChecker.py b/code/tests/test_ContentSafetyChecker.py similarity index 87% rename from tests/test_ContentSafetyChecker.py rename to code/tests/test_ContentSafetyChecker.py index 5bd68864a..7aa4de603 100644 --- a/tests/test_ContentSafetyChecker.py +++ b/code/tests/test_ContentSafetyChecker.py @@ -1,5 +1,5 @@ import pytest -from code.utilities.tools.ContentSafetyChecker import ContentSafetyChecker +from backend.batch.utilities.tools.ContentSafetyChecker import ContentSafetyChecker @pytest.mark.azure("This test requires Azure Content Safety configured") diff --git a/tests/test_DocumentChunking.py b/code/tests/test_DocumentChunking.py similarity index 96% rename from tests/test_DocumentChunking.py rename to code/tests/test_DocumentChunking.py index a48a79e8b..3fac7803b 100644 --- a/tests/test_DocumentChunking.py +++ b/code/tests/test_DocumentChunking.py @@ -1,6 +1,6 @@ import pytest -from code.utilities.common.SourceDocument import SourceDocument -from code.utilities.helpers.DocumentChunkingHelper import ( +from backend.batch.utilities.common.SourceDocument import SourceDocument +from backend.batch.utilities.helpers.DocumentChunkingHelper import ( DocumentChunking, ChunkingSettings, ChunkingStrategy, diff --git a/tests/test_DocumentLoading.py b/code/tests/test_DocumentLoading.py similarity index 96% rename from tests/test_DocumentLoading.py rename to code/tests/test_DocumentLoading.py index 1c7a15004..8fce68b2a 100644 --- a/tests/test_DocumentLoading.py +++ b/code/tests/test_DocumentLoading.py @@ -1,5 +1,5 @@ import pytest -from code.utilities.helpers.DocumentLoadingHelper import ( +from backend.batch.utilities.helpers.DocumentLoadingHelper import ( DocumentLoading, LoadingSettings, ) diff --git a/tests/test_DocumentProcessor.py b/code/tests/test_DocumentProcessor.py similarity index 92% rename from tests/test_DocumentProcessor.py rename to code/tests/test_DocumentProcessor.py index 0393dc4a8..fcee6e30d 100644 --- a/tests/test_DocumentProcessor.py +++ b/code/tests/test_DocumentProcessor.py @@ -1,6 +1,8 @@ import pytest -from code.utilities.helpers.DocumentProcessorHelper import DocumentProcessor -from code.utilities.helpers.ConfigHelper import ConfigHelper +from backend.batch.utilities.helpers.DocumentProcessorHelper import ( + DocumentProcessor, +) +from backend.batch.utilities.helpers.ConfigHelper import ConfigHelper document_url = ( "https://csciblob.blob.core.windows.net/rag-sol-acc/cognitive-services.pdf" diff --git a/tests/test_Orchestrator.py b/code/tests/test_Orchestrator.py similarity index 94% rename from tests/test_Orchestrator.py rename to code/tests/test_Orchestrator.py index b548ca3ad..e3b94b062 100644 --- a/tests/test_Orchestrator.py +++ b/code/tests/test_Orchestrator.py @@ -1,5 +1,5 @@ import pytest -from code.utilities.helpers.OrchestratorHelper import ( +from backend.batch.utilities.helpers.OrchestratorHelper import ( Orchestrator, OrchestrationSettings, ) diff --git a/tests/test_OutputParserTool.py b/code/tests/test_OutputParserTool.py similarity index 97% rename from tests/test_OutputParserTool.py rename to code/tests/test_OutputParserTool.py index 6b2997556..7f826d61b 100644 --- a/tests/test_OutputParserTool.py +++ b/code/tests/test_OutputParserTool.py @@ -2,8 +2,8 @@ from typing import List -from code.utilities.parser.OutputParserTool import OutputParserTool -from code.utilities.common.SourceDocument import SourceDocument +from backend.batch.utilities.parser.OutputParserTool import OutputParserTool +from backend.batch.utilities.common.SourceDocument import SourceDocument def test_returns_parsed_messages(): diff --git a/tests/test_app.py b/code/tests/test_app.py similarity index 93% rename from tests/test_app.py rename to code/tests/test_app.py index 6466f5698..3ad13d890 100644 --- a/tests/test_app.py +++ b/code/tests/test_app.py @@ -3,7 +3,7 @@ from unittest.mock import Mock from unittest.mock import patch -from code.app.app import app +from app import app class TestConfig: @@ -35,8 +35,8 @@ def setup_method(self): ], } - @patch("code.app.app.get_message_orchestrator") - @patch("code.app.app.get_orchestrator_config") + @patch("app.get_message_orchestrator") + @patch("app.get_orchestrator_config") def test_converstation_custom_returns_correct_response( self, get_orchestrator_config_mock, get_message_orchestrator_mock ): @@ -66,8 +66,8 @@ def test_converstation_custom_returns_correct_response( "object": "response.object", } - @patch("code.app.app.get_message_orchestrator") - @patch("code.app.app.get_orchestrator_config") + @patch("app.get_message_orchestrator") + @patch("app.get_orchestrator_config") def test_converstation_custom_calls_message_orchestrator_correctly( self, get_orchestrator_config_mock, get_message_orchestrator_mock ): @@ -95,7 +95,7 @@ def test_converstation_custom_calls_message_orchestrator_correctly( orchestrator=self.orchestrator_config, ) - @patch("code.app.app.get_orchestrator_config") + @patch("app.get_orchestrator_config") def test_converstation_custom_returns_error_resonse_on_exception( self, get_orchestrator_config_mock ): @@ -115,8 +115,8 @@ def test_converstation_custom_returns_error_resonse_on_exception( "error": "Exception in /api/conversation/custom. See log for more details." } - @patch("code.app.app.get_message_orchestrator") - @patch("code.app.app.get_orchestrator_config") + @patch("app.get_message_orchestrator") + @patch("app.get_orchestrator_config") def test_converstation_custom_allows_multiple_messages_from_user( self, get_orchestrator_config_mock, get_message_orchestrator_mock ): diff --git a/code/utilities/helpers/LLMHelper.py b/code/utilities/helpers/LLMHelper.py deleted file mode 100644 index 241088c16..000000000 --- a/code/utilities/helpers/LLMHelper.py +++ /dev/null @@ -1,63 +0,0 @@ -import openai -from typing import List -from langchain.chat_models import AzureChatOpenAI -from langchain.embeddings import OpenAIEmbeddings -from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler -from .EnvHelper import EnvHelper - - -class LLMHelper: - def __init__(self): - env_helper: EnvHelper = EnvHelper() - - # Configure OpenAI API - openai.api_type = env_helper.OPENAI_API_TYPE - openai.api_version = env_helper.AZURE_OPENAI_API_VERSION - openai.api_base = env_helper.OPENAI_API_BASE - openai.api_key = env_helper.OPENAI_API_KEY - - self.llm_model = env_helper.AZURE_OPENAI_MODEL - self.llm_max_tokens = ( - env_helper.AZURE_OPENAI_MAX_TOKENS - if env_helper.AZURE_OPENAI_MAX_TOKENS != "" - else None - ) - self.embedding_model = env_helper.AZURE_OPENAI_EMBEDDING_MODEL - - def get_llm(self): - return AzureChatOpenAI( - deployment_name=self.llm_model, - temperature=0, - max_tokens=self.llm_max_tokens, - openai_api_version=openai.api_version, - ) - - # TODO: This needs to have a custom callback to stream back to the UI - def get_streaming_llm(self): - return AzureChatOpenAI( - streaming=True, - callbacks=[StreamingStdOutCallbackHandler], - deployment_name=self.llm_model, - temperature=0, - max_tokens=self.llm_max_tokens, - openai_api_version=openai.api_version, - ) - - def get_embedding_model(self): - return OpenAIEmbeddings(deployment=self.embedding_model, chunk_size=1) - - def get_chat_completion_with_functions( - self, messages: List[dict], functions: List[dict], function_call: str = "auto" - ): - return openai.ChatCompletion.create( - deployment_id=self.llm_model, - messages=messages, - functions=functions, - function_call=function_call, - ) - - def get_chat_completion(self, messages: List[dict]): - return openai.ChatCompletion.create( - deployment_id=self.llm_model, - messages=messages, - ) diff --git a/code/utilities/tools/__init__.py b/code/utilities/tools/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/docker/AdminWebApp.Dockerfile b/docker/AdminWebApp.Dockerfile index 03487937a..093aa3a3f 100644 --- a/docker/AdminWebApp.Dockerfile +++ b/docker/AdminWebApp.Dockerfile @@ -1,10 +1,10 @@ FROM python:3.11.7-bookworm RUN apt-get update && apt-get install python3-tk tk-dev -y -COPY ./code/requirements.txt /usr/local/src/myscripts/requirements.txt +COPY ./code/backend/requirements.txt /usr/local/src/myscripts/requirements.txt WORKDIR /usr/local/src/myscripts RUN pip install -r requirements.txt -COPY ./code/admin /usr/local/src/myscripts/admin -COPY ./code/utilities /usr/local/src/myscripts/utilities +COPY ./code/backend /usr/local/src/myscripts/admin +COPY ./code/backend/batch/utilities /usr/local/src/myscripts/utilities WORKDIR /usr/local/src/myscripts/admin ENV PYTHONPATH "${PYTHONPATH}:/usr/local/src/myscripts" EXPOSE 80 diff --git a/docker/AdminWebApp.dockerignore b/docker/AdminWebApp.dockerignore index 9745033f2..640e80c66 100644 --- a/docker/AdminWebApp.dockerignore +++ b/docker/AdminWebApp.dockerignore @@ -1,2 +1,2 @@ -code/app -code/batch \ No newline at end of file +code/ +code/backend/batch \ No newline at end of file diff --git a/docker/Backend.Dockerfile b/docker/Backend.Dockerfile index d06a4dec1..d1041ab5b 100644 --- a/docker/Backend.Dockerfile +++ b/docker/Backend.Dockerfile @@ -6,8 +6,8 @@ ENV AzureWebJobsScriptRoot=/home/site/wwwroot \ AzureFunctionsJobHost__Logging__Console__IsEnabled=true \ AzureWebJobsFeatureFlags=EnableWorkerIndexing -COPY ./code/requirements.txt / +COPY ./code/backend/requirements.txt / RUN pip install -r /requirements.txt -COPY ./code/utilities /home/site/wwwroot/utilities -COPY ./code/batch /home/site/wwwroot \ No newline at end of file +COPY ./code/backend/batch/utilities /home/site/wwwroot/utilities +COPY ./code/backend/batch /home/site/wwwroot \ No newline at end of file diff --git a/docker/Backend.Dockerfile.dockerignore b/docker/Backend.Dockerfile.dockerignore index 0c4c0938c..aa66d6eb8 100644 --- a/docker/Backend.Dockerfile.dockerignore +++ b/docker/Backend.Dockerfile.dockerignore @@ -1,2 +1,2 @@ -code/admin -code/app \ No newline at end of file +code/backend +code/ \ No newline at end of file diff --git a/docker/WebApp.Dockerfile b/docker/WebApp.Dockerfile index 3e33fca77..b2d316d1e 100644 --- a/docker/WebApp.Dockerfile +++ b/docker/WebApp.Dockerfile @@ -2,7 +2,7 @@ FROM node:20-alpine AS frontend RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app WORKDIR /home/node/app -COPY ./code/app/frontend/package*.json ./ +COPY ./code/frontend/package*.json ./ USER node RUN npm ci COPY --chown=node:node ./code/app/frontend ./frontend @@ -19,12 +19,12 @@ RUN apk add --no-cache --virtual .build-deps \ libpq \ && pip install --no-cache-dir uwsgi -COPY ./code/app/requirements.txt /usr/src/app/ +COPY ./code/requirements.txt /usr/src/app/ RUN pip install --no-cache-dir -r /usr/src/app/requirements.txt \ && rm -rf /root/.cache -COPY ./code/app/app.py /usr/src/app/ -COPY ./code/utilities /usr/src/app/utilities +COPY ./code/app.py /usr/src/app/ +COPY ./code/backend/batch/utilities /usr/src/app/utilities COPY --from=frontend /home/node/app/static /usr/src/app/static/ WORKDIR /usr/src/app EXPOSE 80 diff --git a/docs/LOCAL_DEPLOYMENT.md b/docs/LOCAL_DEPLOYMENT.md index 93b3807fb..1bcd7781a 100644 --- a/docs/LOCAL_DEPLOYMENT.md +++ b/docs/LOCAL_DEPLOYMENT.md @@ -91,14 +91,13 @@ This step is included if you cannot use the Launch configuration in VSCode. Open ```shell cd code python -m pip install -r requirements.txt -cd app python -m flask --app ./app.py --debug run ``` #### Starting the Typescript React app in dev mode (optional) This step is included if you cannot use the Launch configuration in VSCode. Open a new separate terminal and enter the following commands: ```shell -cd code\app\frontend +cd code\frontend npm install npm run dev ``` @@ -138,7 +137,7 @@ If you want to develop and run the batch processing functions container locally, First, install [Azure Functions Core Tools](https://learn.microsoft.com/en-us/azure/azure-functions/functions-run-local?tabs=windows%2Cportal%2Cv2%2Cbash&pivots=programming-language-python). ```shell -cd code\batch +cd code\backend\batch func start ``` diff --git a/extensions/backend/http_cwyod/__init__.py b/extensions/backend/http_cwyod/__init__.py index 083599461..b29172f06 100644 --- a/extensions/backend/http_cwyod/__init__.py +++ b/extensions/backend/http_cwyod/__init__.py @@ -11,7 +11,7 @@ def main(req: func.HttpRequest) -> func.HttpResponse: logging.info("Python HTTP trigger function processed a request.") - from utilities.helpers.OrchestratorHelper import Orchestrator + from code.backend.batch.utilities.helpers.OrchestratorHelper import Orchestrator message_orchestrator = Orchestrator() @@ -33,7 +33,7 @@ def main(req: func.HttpRequest) -> func.HttpResponse: user_assistant_messages[i + 1]["content"], ) ) - from utilities.helpers.ConfigHelper import ConfigHelper + from code.backend.batch.utilities.helpers.ConfigHelper import ConfigHelper messages = message_orchestrator.handle_message( user_message=user_message, diff --git a/extensions/backend/requirements.txt b/extensions/backend/requirements.txt index 4cf8ef7fb..0453cae22 100644 --- a/extensions/backend/requirements.txt +++ b/extensions/backend/requirements.txt @@ -4,7 +4,7 @@ azure-functions streamlit==1.30.0 -openai==0.27.8 +openai==1.6.1 scipy==1.12.0 transformers==4.37.2 python-dotenv==1.0.1 diff --git a/infra/app/adminweb.bicep b/infra/app/adminweb.bicep new file mode 100644 index 000000000..a962a0d24 --- /dev/null +++ b/infra/app/adminweb.bicep @@ -0,0 +1,58 @@ +param name string +param location string = resourceGroup().location +param tags object = {} +param storageAccountName string = '' +param formRecognizerName string = '' +param contentSafetyName string = '' +param allowedOrigins array = [] +param appServicePlanId string +param appCommandLine string = 'python -m streamlit run Admin.py --server.port 8000 --server.address 0.0.0.0 --server.enableXsrfProtection false' +param applicationInsightsName string = '' +param keyVaultName string = '' +param azureOpenAIName string = '' +param azureAISearchName string = '' +param speechServiceName string = '' +@secure() +param appSettings object = {} +param serviceName string = 'adminweb' +param useKeyVault bool +param openAIKeyName string = '' +param storageAccountKeyName string = '' +param formRecognizerKeyName string = '' +param searchKeyName string = '' +param contentSafetyKeyName string = '' +param speechKeyName string = '' +param keyVaultEndpoint string = '' +param authType string + +module adminweb '../core/host/appservice.bicep' = { + name: '${name}-app-module' + params: { + name: name + location: location + tags: union(tags, { 'azd-service-name': serviceName }) + allowedOrigins: allowedOrigins + appCommandLine: appCommandLine + applicationInsightsName: applicationInsightsName + appServicePlanId: appServicePlanId + appSettings: union(appSettings, { + AZURE_AUTH_TYPE: authType + USE_KEY_VAULT: useKeyVault ? useKeyVault : '' + AZURE_KEY_VAULT_ENDPOINT: useKeyVault ? keyVaultEndpoint : '' + AZURE_OPENAI_KEY: useKeyVault ? openAIKeyName : listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', azureOpenAIName), '2023-05-01').key1 + AZURE_SEARCH_KEY: useKeyVault ? searchKeyName : listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', azureAISearchName), '2021-04-01-preview').primaryKey + AZURE_BLOB_ACCOUNT_KEY: useKeyVault ? storageAccountKeyName : listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', storageAccountName), '2021-09-01').keys[0].value + AZURE_FORM_RECOGNIZER_KEY: useKeyVault ? formRecognizerKeyName : listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', formRecognizerName), '2023-05-01').key1 + AZURE_CONTENT_SAFETY_KEY: useKeyVault ? contentSafetyKeyName : listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', contentSafetyName), '2023-05-01').key1 + AZURE_SPEECH_SERVICE_KEY: useKeyVault ? speechKeyName: listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', speechServiceName), '2023-05-01').key1 + }) + keyVaultName: keyVaultName + runtimeName: 'python' + runtimeVersion: '3.11' + scmDoBuildDuringDeployment: true + } +} + +output WEBSITE_ADMIN_IDENTITY_PRINCIPAL_ID string = adminweb.outputs.identityPrincipalId +output WEBSITE_ADMIN_NAME string = adminweb.outputs.name +output WEBSITE_ADMIN_URI string = adminweb.outputs.uri diff --git a/infra/app/eventgrid.bicep b/infra/app/eventgrid.bicep new file mode 100644 index 000000000..78a091a84 --- /dev/null +++ b/infra/app/eventgrid.bicep @@ -0,0 +1,43 @@ +param name string +param location string +param storageAccountId string +param queueName string +param blobContainerName string + +resource eventGridSystemTopic 'Microsoft.EventGrid/systemTopics@2021-12-01' = { + name: name + location: location + properties: { + source: storageAccountId + topicType: 'Microsoft.Storage.StorageAccounts' + } +} + +resource eventGridSystemTopicNameBlobEvents 'Microsoft.EventGrid/systemTopics/eventSubscriptions@2021-12-01' = { + parent: eventGridSystemTopic + name: 'BlobEvents' + properties: { + destination: { + endpointType: 'StorageQueue' + properties: { + queueMessageTimeToLiveInSeconds: -1 + queueName: queueName + resourceId: storageAccountId + } + } + filter: { + includedEventTypes: [ + 'Microsoft.Storage.BlobCreated' + 'Microsoft.Storage.BlobDeleted' + ] + enableAdvancedFilteringOnArrays: true + subjectBeginsWith: '/blobServices/default/containers/${blobContainerName}/blobs/' + } + labels: [] + eventDeliverySchema: 'EventGridSchema' + retryPolicy: { + maxDeliveryAttempts: 30 + eventTimeToLiveInMinutes: 1440 + } + } +} diff --git a/infra/app/function.bicep b/infra/app/function.bicep new file mode 100644 index 000000000..e4c4352df --- /dev/null +++ b/infra/app/function.bicep @@ -0,0 +1,81 @@ +param name string +param location string = '' +param appServicePlanId string +param storageAccountName string = '' +param tags object = {} +@secure() +param appSettings object = {} +param serviceName string = 'function' +param runtimeName string = 'python' +param runtimeVersion string = '3.11' +@secure() +param clientKey string +param keyVaultName string = '' +param azureOpenAIName string = '' +param azureAISearchName string = '' +param formRecognizerName string = '' +param contentSafetyName string = '' +param speechServiceName string = '' +param useKeyVault bool +param openAIKeyName string = '' +param storageAccountKeyName string = '' +param formRecognizerKeyName string = '' +param searchKeyName string = '' +param contentSafetyKeyName string = '' +param speechKeyName string = '' +param keyVaultEndpoint string = '' +param authType string + +module function '../core/host/functions.bicep' = { + name: '${name}-app-module' + params: { + name: name + location: location + tags: union(tags, { 'azd-service-name': serviceName }) + appServicePlanId: appServicePlanId + storageAccountName: storageAccountName + keyVaultName: keyVaultName + runtimeName: runtimeName + runtimeVersion: runtimeVersion + appSettings: union(appSettings, { + AZURE_AUTH_TYPE: authType + USE_KEY_VAULT: useKeyVault ? useKeyVault : '' + AZURE_KEY_VAULT_ENDPOINT: useKeyVault ? keyVaultEndpoint : '' + AZURE_OPENAI_KEY: useKeyVault ? openAIKeyName : listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', azureOpenAIName), '2023-05-01').key1 + AZURE_SEARCH_KEY: useKeyVault ? searchKeyName : listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', azureAISearchName), '2021-04-01-preview').primaryKey + AZURE_BLOB_ACCOUNT_KEY: useKeyVault ? storageAccountKeyName : listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', storageAccountName), '2021-09-01').keys[0].value + AZURE_FORM_RECOGNIZER_KEY: useKeyVault ? formRecognizerKeyName : listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', formRecognizerName), '2023-05-01').key1 + AZURE_CONTENT_SAFETY_KEY: useKeyVault ? contentSafetyKeyName : listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', contentSafetyName), '2023-05-01').key1 + AZURE_SPEECH_SERVICE_KEY: useKeyVault ? speechKeyName: listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', speechServiceName), '2023-05-01').key1 + }) + } +} + +resource functionNameDefaultClientKey 'Microsoft.Web/sites/host/functionKeys@2018-11-01' = { + name: '${name}/default/clientKey' + properties: { + name: 'ClientKey' + value: clientKey + } + dependsOn: [ + function + waitFunctionDeploymentSection + ] +} + +resource waitFunctionDeploymentSection 'Microsoft.Resources/deploymentScripts@2020-10-01' = { + kind: 'AzurePowerShell' + name: 'WaitFunctionDeploymentSection' + location: location + properties: { + azPowerShellVersion: '3.0' + scriptContent: 'start-sleep -Seconds 300' + cleanupPreference: 'Always' + retentionInterval: 'PT1H' + } + dependsOn: [ + function + ] +} + +output FUNCTION_IDENTITY_PRINCIPAL_ID string = function.outputs.identityPrincipalId diff --git a/infra/app/storekeys.bicep b/infra/app/storekeys.bicep new file mode 100644 index 000000000..f149417ed --- /dev/null +++ b/infra/app/storekeys.bicep @@ -0,0 +1,74 @@ +param keyVaultName string = '' +param storageAccountName string = '' +param azureOpenAIName string = '' +param azureAISearchName string = '' +param rgName string = '' +param formRecognizerName string = '' +param contentSafetyName string = '' +param speechServiceName string = '' +param storageAccountKeyName string = 'AZURE-STORAGE-ACCOUNT-KEY' +param openAIKeyName string = 'AZURE-OPEN-AI-KEY' +param searchKeyName string = 'AZURE-SEARCH-KEY' +param formRecognizerKeyName string = 'AZURE-FORM-RECOGNIZER-KEY' +param contentSafetyKeyName string = 'AZURE-CONTENT-SAFETY-KEY' +param speechKeyName string = 'AZURE-SPEECH-KEY' + + +resource storageAccountKeySecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { + parent: keyVault + name: storageAccountKeyName + properties: { + value: listKeys(resourceId(subscription().subscriptionId, rgName, 'Microsoft.Storage/storageAccounts', storageAccountName), '2021-09-01').keys[0].value + } +} + +resource openAIKeySecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { + parent: keyVault + name: openAIKeyName + properties: { + value: listKeys(resourceId(subscription().subscriptionId, rgName, 'Microsoft.CognitiveServices/accounts', azureOpenAIName), '2023-05-01').key1 + } +} + +resource searchKeySecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { + parent: keyVault + name: searchKeyName + properties: { + value: listAdminKeys(resourceId(subscription().subscriptionId, rgName, 'Microsoft.Search/searchServices', azureAISearchName), '2021-04-01-preview').primaryKey + } +} + +resource formRecognizerKeySecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { + parent: keyVault + name: formRecognizerKeyName + properties: { + value: listKeys(resourceId(subscription().subscriptionId, rgName, 'Microsoft.CognitiveServices/accounts', formRecognizerName), '2023-05-01').key1 + } +} + +resource contentSafetyKeySecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { + parent: keyVault + name: contentSafetyKeyName + properties: { + value: listKeys(resourceId(subscription().subscriptionId, rgName, 'Microsoft.CognitiveServices/accounts', contentSafetyName), '2023-05-01').key1 + } +} + +resource speechKeySecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { + parent: keyVault + name: speechKeyName + properties: { + value: listKeys(resourceId(subscription().subscriptionId, rgName, 'Microsoft.CognitiveServices/accounts', speechServiceName), '2023-05-01').key1 + } +} + +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { + name: keyVaultName +} + +output CONTENT_SAFETY_KEY_NAME string = contentSafetyKeySecret.name +output FORM_RECOGNIZER_KEY_NAME string = formRecognizerKeySecret.name +output SEARCH_KEY_NAME string = searchKeySecret.name +output OPENAI_KEY_NAME string = openAIKeySecret.name +output STORAGE_ACCOUNT_KEY_NAME string = storageAccountKeySecret.name +output SPEECH_KEY_NAME string = speechKeySecret.name diff --git a/infra/app/web.bicep b/infra/app/web.bicep new file mode 100644 index 000000000..c4f7a1560 --- /dev/null +++ b/infra/app/web.bicep @@ -0,0 +1,58 @@ +param name string +param location string = resourceGroup().location +param tags object = {} +param allowedOrigins array = [] +param appCommandLine string = '' +param appServicePlanId string +param applicationInsightsName string = '' +param keyVaultName string = '' +param azureOpenAIName string = '' +param azureAISearchName string = '' +param storageAccountName string = '' +param formRecognizerName string = '' +param contentSafetyName string = '' +param speechServiceName string = '' +@secure() +param appSettings object = {} +param serviceName string = 'web' +param useKeyVault bool +param openAIKeyName string = '' +param storageAccountKeyName string = '' +param formRecognizerKeyName string = '' +param searchKeyName string = '' +param contentSafetyKeyName string = '' +param speechKeyName string = '' +param keyVaultEndpoint string = '' +param authType string + +module web '../core/host/appservice.bicep' = { + name: '${name}-app-module' + params: { + name: name + location: location + tags: union(tags, { 'azd-service-name': serviceName }) + allowedOrigins: allowedOrigins + appCommandLine: appCommandLine + applicationInsightsName: applicationInsightsName + appServicePlanId: appServicePlanId + appSettings: union(appSettings, { + AZURE_AUTH_TYPE: authType + USE_KEY_VAULT: useKeyVault ? useKeyVault : '' + AZURE_KEY_VAULT_ENDPOINT: useKeyVault ? keyVaultEndpoint : '' + AZURE_OPENAI_KEY: useKeyVault ? openAIKeyName : listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', azureOpenAIName), '2023-05-01').key1 + AZURE_SEARCH_KEY: useKeyVault ? searchKeyName : listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', azureAISearchName), '2021-04-01-preview').primaryKey + AZURE_BLOB_ACCOUNT_KEY: useKeyVault ? storageAccountKeyName : listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', storageAccountName), '2021-09-01').keys[0].value + AZURE_FORM_RECOGNIZER_KEY: useKeyVault ? formRecognizerKeyName : listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', formRecognizerName), '2023-05-01').key1 + AZURE_CONTENT_SAFETY_KEY: useKeyVault ? contentSafetyKeyName : listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', contentSafetyName), '2023-05-01').key1 + AZURE_SPEECH_SERVICE_KEY: useKeyVault ? speechKeyName: listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', speechServiceName), '2023-05-01').key1 + }) + keyVaultName: keyVaultName + runtimeName: 'python' + runtimeVersion: '3.11' + scmDoBuildDuringDeployment: true + } +} + +output FRONTEND_API_IDENTITY_PRINCIPAL_ID string = web.outputs.identityPrincipalId +output FRONTEND_API_NAME string = web.outputs.name +output FRONTEND_API_URI string = web.outputs.uri diff --git a/infra/core/ai/cognitiveservices.bicep b/infra/core/ai/cognitiveservices.bicep new file mode 100644 index 000000000..1bf5666b9 --- /dev/null +++ b/infra/core/ai/cognitiveservices.bicep @@ -0,0 +1,53 @@ +metadata description = 'Creates an Azure Cognitive Services instance.' +param name string +param location string = resourceGroup().location +param tags object = {} +@description('The custom subdomain name used to access the API. Defaults to the value of the name parameter.') +param customSubDomainName string = name +param deployments array = [] +param kind string = 'OpenAI' + +@allowed([ 'Enabled', 'Disabled' ]) +param publicNetworkAccess string = 'Enabled' +param sku object = { + name: 'S0' +} + +param allowedIpRules array = [] +param networkAcls object = empty(allowedIpRules) ? { + defaultAction: 'Allow' +} : { + ipRules: allowedIpRules + defaultAction: 'Deny' +} + +resource account 'Microsoft.CognitiveServices/accounts@2023-05-01' = { + name: name + location: location + tags: tags + kind: kind + properties: { + customSubDomainName: customSubDomainName + publicNetworkAccess: publicNetworkAccess + networkAcls: networkAcls + } + sku: sku +} + +@batchSize(1) +resource deployment 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = [for deployment in deployments: { + parent: account + name: deployment.name + properties: { + model: deployment.model + raiPolicyName: contains(deployment, 'raiPolicyName') ? deployment.raiPolicyName : null + } + sku: contains(deployment, 'sku') ? deployment.sku : { + name: 'Standard' + capacity: 20 + } +}] + +output endpoint string = account.properties.endpoint +output id string = account.id +output name string = account.name diff --git a/infra/core/host/appservice-appsettings.bicep b/infra/core/host/appservice-appsettings.bicep new file mode 100644 index 000000000..f4b22f816 --- /dev/null +++ b/infra/core/host/appservice-appsettings.bicep @@ -0,0 +1,17 @@ +metadata description = 'Updates app settings for an Azure App Service.' +@description('The name of the app service resource within the current resource group scope') +param name string + +@description('The app settings to be applied to the app service') +@secure() +param appSettings object + +resource appService 'Microsoft.Web/sites@2022-03-01' existing = { + name: name +} + +resource settings 'Microsoft.Web/sites/config@2022-03-01' = { + name: 'appsettings' + parent: appService + properties: appSettings +} diff --git a/infra/core/host/appservice.bicep b/infra/core/host/appservice.bicep new file mode 100644 index 000000000..bef4d2ba4 --- /dev/null +++ b/infra/core/host/appservice.bicep @@ -0,0 +1,123 @@ +metadata description = 'Creates an Azure App Service in an existing Azure App Service plan.' +param name string +param location string = resourceGroup().location +param tags object = {} + +// Reference Properties +param applicationInsightsName string = '' +param appServicePlanId string +param keyVaultName string = '' +param managedIdentity bool = !empty(keyVaultName) + +// Runtime Properties +@allowed([ + 'dotnet', 'dotnetcore', 'dotnet-isolated', 'node', 'python', 'java', 'powershell', 'custom' +]) +param runtimeName string +param runtimeNameAndVersion string = '${runtimeName}|${runtimeVersion}' +param runtimeVersion string + +// Microsoft.Web/sites Properties +param kind string = 'app,linux' + +// Microsoft.Web/sites/config +param allowedOrigins array = [] +param alwaysOn bool = true +param appCommandLine string = '' +@secure() +param appSettings object = {} +param clientAffinityEnabled bool = false +param enableOryxBuild bool = contains(kind, 'linux') +param functionAppScaleLimit int = -1 +param linuxFxVersion string = runtimeNameAndVersion +param minimumElasticInstanceCount int = -1 +param numberOfWorkers int = -1 +param scmDoBuildDuringDeployment bool = false +param use32BitWorkerProcess bool = false +param ftpsState string = 'FtpsOnly' +param healthCheckPath string = '' + +resource appService 'Microsoft.Web/sites@2022-03-01' = { + name: name + location: location + tags: tags + kind: kind + properties: { + serverFarmId: appServicePlanId + siteConfig: { + linuxFxVersion: linuxFxVersion + alwaysOn: alwaysOn + ftpsState: ftpsState + minTlsVersion: '1.2' + appCommandLine: appCommandLine + numberOfWorkers: numberOfWorkers != -1 ? numberOfWorkers : null + minimumElasticInstanceCount: minimumElasticInstanceCount != -1 ? minimumElasticInstanceCount : null + use32BitWorkerProcess: use32BitWorkerProcess + functionAppScaleLimit: functionAppScaleLimit != -1 ? functionAppScaleLimit : null + healthCheckPath: healthCheckPath + cors: { + allowedOrigins: union([ 'https://portal.azure.com', 'https://ms.portal.azure.com' ], allowedOrigins) + } + } + clientAffinityEnabled: clientAffinityEnabled + httpsOnly: true + } + + identity: { type: managedIdentity ? 'SystemAssigned' : 'None' } + + resource basicPublishingCredentialsPoliciesFtp 'basicPublishingCredentialsPolicies' = { + name: 'ftp' + properties: { + allow: false + } + } + + resource basicPublishingCredentialsPoliciesScm 'basicPublishingCredentialsPolicies' = { + name: 'scm' + properties: { + allow: false + } + } +} + +// Updates to the single Microsoft.sites/web/config resources that need to be performed sequentially +// sites/web/config 'appsettings' +module configAppSettings 'appservice-appsettings.bicep' = { + name: '${name}-appSettings' + params: { + name: appService.name + appSettings: union(appSettings, + { + SCM_DO_BUILD_DURING_DEPLOYMENT: string(scmDoBuildDuringDeployment) + ENABLE_ORYX_BUILD: string(enableOryxBuild) + }, + runtimeName == 'python' && appCommandLine == '' ? { PYTHON_ENABLE_GUNICORN_MULTIWORKERS: 'true'} : {}, + !empty(applicationInsightsName) ? { APPLICATIONINSIGHTS_CONNECTION_STRING: applicationInsights.properties.ConnectionString } : {}, + !empty(keyVaultName) ? { AZURE_KEY_VAULT_ENDPOINT: keyVault.properties.vaultUri } : {}) + } +} + +// sites/web/config 'logs' +resource configLogs 'Microsoft.Web/sites/config@2022-03-01' = { + name: 'logs' + parent: appService + properties: { + applicationLogs: { fileSystem: { level: 'Verbose' } } + detailedErrorMessages: { enabled: true } + failedRequestsTracing: { enabled: true } + httpLogs: { fileSystem: { enabled: true, retentionInDays: 1, retentionInMb: 35 } } + } + dependsOn: [configAppSettings] +} + +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = if (!(empty(keyVaultName))) { + name: keyVaultName +} + +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (!empty(applicationInsightsName)) { + name: applicationInsightsName +} + +output identityPrincipalId string = managedIdentity ? appService.identity.principalId : '' +output name string = appService.name +output uri string = 'https://${appService.properties.defaultHostName}' diff --git a/infra/core/host/appserviceplan.bicep b/infra/core/host/appserviceplan.bicep new file mode 100644 index 000000000..2e37e041f --- /dev/null +++ b/infra/core/host/appserviceplan.bicep @@ -0,0 +1,22 @@ +metadata description = 'Creates an Azure App Service plan.' +param name string +param location string = resourceGroup().location +param tags object = {} + +param kind string = '' +param reserved bool = true +param sku object + +resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = { + name: name + location: location + tags: tags + sku: sku + kind: kind + properties: { + reserved: reserved + } +} + +output id string = appServicePlan.id +output name string = appServicePlan.name diff --git a/infra/core/host/functions.bicep b/infra/core/host/functions.bicep new file mode 100644 index 000000000..7070a2c66 --- /dev/null +++ b/infra/core/host/functions.bicep @@ -0,0 +1,86 @@ +metadata description = 'Creates an Azure Function in an existing Azure App Service plan.' +param name string +param location string = resourceGroup().location +param tags object = {} + +// Reference Properties +param applicationInsightsName string = '' +param appServicePlanId string +param keyVaultName string = '' +param managedIdentity bool = !empty(keyVaultName) +param storageAccountName string + +// Runtime Properties +@allowed([ + 'dotnet', 'dotnetcore', 'dotnet-isolated', 'node', 'python', 'java', 'powershell', 'custom' +]) +param runtimeName string +param runtimeNameAndVersion string = '${runtimeName}|${runtimeVersion}' +param runtimeVersion string + +// Function Settings +@allowed([ + '~4', '~3', '~2', '~1' +]) +param extensionVersion string = '~4' + +// Microsoft.Web/sites Properties +param kind string = 'functionapp,linux' + +// Microsoft.Web/sites/config +param allowedOrigins array = [] +param alwaysOn bool = true +param appCommandLine string = '' +@secure() +param appSettings object = {} +param clientAffinityEnabled bool = false +param enableOryxBuild bool = contains(kind, 'linux') +param functionAppScaleLimit int = -1 +param linuxFxVersion string = runtimeNameAndVersion +param minimumElasticInstanceCount int = -1 +param numberOfWorkers int = -1 +param scmDoBuildDuringDeployment bool = true +param use32BitWorkerProcess bool = false +param healthCheckPath string = '' + +module functions 'appservice.bicep' = { + name: '${name}-functions' + params: { + name: name + location: location + tags: tags + allowedOrigins: allowedOrigins + alwaysOn: alwaysOn + appCommandLine: appCommandLine + applicationInsightsName: applicationInsightsName + appServicePlanId: appServicePlanId + appSettings: union(appSettings, { + AzureWebJobsStorage: 'DefaultEndpointsProtocol=https;AccountName=${storage.name};AccountKey=${storage.listKeys().keys[0].value};EndpointSuffix=${environment().suffixes.storage}' + FUNCTIONS_EXTENSION_VERSION: extensionVersion + FUNCTIONS_WORKER_RUNTIME: runtimeName + }) + clientAffinityEnabled: clientAffinityEnabled + enableOryxBuild: enableOryxBuild + functionAppScaleLimit: functionAppScaleLimit + healthCheckPath: healthCheckPath + keyVaultName: keyVaultName + kind: kind + linuxFxVersion: linuxFxVersion + managedIdentity: managedIdentity + minimumElasticInstanceCount: minimumElasticInstanceCount + numberOfWorkers: numberOfWorkers + runtimeName: runtimeName + runtimeVersion: runtimeVersion + runtimeNameAndVersion: runtimeNameAndVersion + scmDoBuildDuringDeployment: scmDoBuildDuringDeployment + use32BitWorkerProcess: use32BitWorkerProcess + } +} + +resource storage 'Microsoft.Storage/storageAccounts@2021-09-01' existing = { + name: storageAccountName +} + +output identityPrincipalId string = managedIdentity ? functions.outputs.identityPrincipalId : '' +output name string = functions.outputs.name +output uri string = functions.outputs.uri diff --git a/infra/core/monitor/applicationinsights-dashboard.bicep b/infra/core/monitor/applicationinsights-dashboard.bicep new file mode 100644 index 000000000..d082e668e --- /dev/null +++ b/infra/core/monitor/applicationinsights-dashboard.bicep @@ -0,0 +1,1236 @@ +metadata description = 'Creates a dashboard for an Application Insights instance.' +param name string +param applicationInsightsName string +param location string = resourceGroup().location +param tags object = {} + +// 2020-09-01-preview because that is the latest valid version +resource applicationInsightsDashboard 'Microsoft.Portal/dashboards@2020-09-01-preview' = { + name: name + location: location + tags: tags + properties: { + lenses: [ + { + order: 0 + parts: [ + { + position: { + x: 0 + y: 0 + colSpan: 2 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'id' + value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + { + name: 'Version' + value: '1.0' + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/AspNetOverviewPinnedPart' + asset: { + idInputName: 'id' + type: 'ApplicationInsights' + } + defaultMenuItemId: 'overview' + } + } + { + position: { + x: 2 + y: 0 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ComponentId' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'Version' + value: '1.0' + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/ProactiveDetectionAsyncPart' + asset: { + idInputName: 'ComponentId' + type: 'ApplicationInsights' + } + defaultMenuItemId: 'ProactiveDetection' + } + } + { + position: { + x: 3 + y: 0 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ComponentId' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'ResourceId' + value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/QuickPulseButtonSmallPart' + asset: { + idInputName: 'ComponentId' + type: 'ApplicationInsights' + } + } + } + { + position: { + x: 4 + y: 0 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ComponentId' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'TimeContext' + value: { + durationMs: 86400000 + endTime: null + createdTime: '2018-05-04T01:20:33.345Z' + isInitialTime: true + grain: 1 + useDashboardTimeRange: false + } + } + { + name: 'Version' + value: '1.0' + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/AvailabilityNavButtonPart' + asset: { + idInputName: 'ComponentId' + type: 'ApplicationInsights' + } + } + } + { + position: { + x: 5 + y: 0 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ComponentId' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'TimeContext' + value: { + durationMs: 86400000 + endTime: null + createdTime: '2018-05-08T18:47:35.237Z' + isInitialTime: true + grain: 1 + useDashboardTimeRange: false + } + } + { + name: 'ConfigurationId' + value: '78ce933e-e864-4b05-a27b-71fd55a6afad' + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/AppMapButtonPart' + asset: { + idInputName: 'ComponentId' + type: 'ApplicationInsights' + } + } + } + { + position: { + x: 0 + y: 1 + colSpan: 3 + rowSpan: 1 + } + metadata: { + inputs: [] + type: 'Extension/HubsExtension/PartType/MarkdownPart' + settings: { + content: { + settings: { + content: '# Usage' + title: '' + subtitle: '' + } + } + } + } + } + { + position: { + x: 3 + y: 1 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ComponentId' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'TimeContext' + value: { + durationMs: 86400000 + endTime: null + createdTime: '2018-05-04T01:22:35.782Z' + isInitialTime: true + grain: 1 + useDashboardTimeRange: false + } + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/UsageUsersOverviewPart' + asset: { + idInputName: 'ComponentId' + type: 'ApplicationInsights' + } + } + } + { + position: { + x: 4 + y: 1 + colSpan: 3 + rowSpan: 1 + } + metadata: { + inputs: [] + type: 'Extension/HubsExtension/PartType/MarkdownPart' + settings: { + content: { + settings: { + content: '# Reliability' + title: '' + subtitle: '' + } + } + } + } + } + { + position: { + x: 7 + y: 1 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ResourceId' + value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + { + name: 'DataModel' + value: { + version: '1.0.0' + timeContext: { + durationMs: 86400000 + createdTime: '2018-05-04T23:42:40.072Z' + isInitialTime: false + grain: 1 + useDashboardTimeRange: false + } + } + isOptional: true + } + { + name: 'ConfigurationId' + value: '8a02f7bf-ac0f-40e1-afe9-f0e72cfee77f' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/CuratedBladeFailuresPinnedPart' + isAdapter: true + asset: { + idInputName: 'ResourceId' + type: 'ApplicationInsights' + } + defaultMenuItemId: 'failures' + } + } + { + position: { + x: 8 + y: 1 + colSpan: 3 + rowSpan: 1 + } + metadata: { + inputs: [] + type: 'Extension/HubsExtension/PartType/MarkdownPart' + settings: { + content: { + settings: { + content: '# Responsiveness\r\n' + title: '' + subtitle: '' + } + } + } + } + } + { + position: { + x: 11 + y: 1 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ResourceId' + value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + { + name: 'DataModel' + value: { + version: '1.0.0' + timeContext: { + durationMs: 86400000 + createdTime: '2018-05-04T23:43:37.804Z' + isInitialTime: false + grain: 1 + useDashboardTimeRange: false + } + } + isOptional: true + } + { + name: 'ConfigurationId' + value: '2a8ede4f-2bee-4b9c-aed9-2db0e8a01865' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/CuratedBladePerformancePinnedPart' + isAdapter: true + asset: { + idInputName: 'ResourceId' + type: 'ApplicationInsights' + } + defaultMenuItemId: 'performance' + } + } + { + position: { + x: 12 + y: 1 + colSpan: 3 + rowSpan: 1 + } + metadata: { + inputs: [] + type: 'Extension/HubsExtension/PartType/MarkdownPart' + settings: { + content: { + settings: { + content: '# Browser' + title: '' + subtitle: '' + } + } + } + } + } + { + position: { + x: 15 + y: 1 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ComponentId' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'MetricsExplorerJsonDefinitionId' + value: 'BrowserPerformanceTimelineMetrics' + } + { + name: 'TimeContext' + value: { + durationMs: 86400000 + createdTime: '2018-05-08T12:16:27.534Z' + isInitialTime: false + grain: 1 + useDashboardTimeRange: false + } + } + { + name: 'CurrentFilter' + value: { + eventTypes: [ + 4 + 1 + 3 + 5 + 2 + 6 + 13 + ] + typeFacets: {} + isPermissive: false + } + } + { + name: 'id' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'Version' + value: '1.0' + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/MetricsExplorerBladePinnedPart' + asset: { + idInputName: 'ComponentId' + type: 'ApplicationInsights' + } + defaultMenuItemId: 'browser' + } + } + { + position: { + x: 0 + y: 2 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'sessions/count' + aggregationType: 5 + namespace: 'microsoft.insights/components/kusto' + metricVisualization: { + displayName: 'Sessions' + color: '#47BDF5' + } + } + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'users/count' + aggregationType: 5 + namespace: 'microsoft.insights/components/kusto' + metricVisualization: { + displayName: 'Users' + color: '#7E58FF' + } + } + ] + title: 'Unique sessions and users' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + openBladeOnClick: { + openBlade: true + destinationBlade: { + extensionName: 'HubsExtension' + bladeName: 'ResourceMenuBlade' + parameters: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + menuid: 'segmentationUsers' + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 4 + y: 2 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'requests/failed' + aggregationType: 7 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Failed requests' + color: '#EC008C' + } + } + ] + title: 'Failed requests' + visualization: { + chartType: 3 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + openBladeOnClick: { + openBlade: true + destinationBlade: { + extensionName: 'HubsExtension' + bladeName: 'ResourceMenuBlade' + parameters: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + menuid: 'failures' + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 8 + y: 2 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'requests/duration' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Server response time' + color: '#00BCF2' + } + } + ] + title: 'Server response time' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + openBladeOnClick: { + openBlade: true + destinationBlade: { + extensionName: 'HubsExtension' + bladeName: 'ResourceMenuBlade' + parameters: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + menuid: 'performance' + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 12 + y: 2 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'browserTimings/networkDuration' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Page load network connect time' + color: '#7E58FF' + } + } + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'browserTimings/processingDuration' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Client processing time' + color: '#44F1C8' + } + } + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'browserTimings/sendDuration' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Send request time' + color: '#EB9371' + } + } + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'browserTimings/receiveDuration' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Receiving response time' + color: '#0672F1' + } + } + ] + title: 'Average page load time breakdown' + visualization: { + chartType: 3 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 0 + y: 5 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'availabilityResults/availabilityPercentage' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Availability' + color: '#47BDF5' + } + } + ] + title: 'Average availability' + visualization: { + chartType: 3 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + openBladeOnClick: { + openBlade: true + destinationBlade: { + extensionName: 'HubsExtension' + bladeName: 'ResourceMenuBlade' + parameters: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + menuid: 'availability' + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 4 + y: 5 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'exceptions/server' + aggregationType: 7 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Server exceptions' + color: '#47BDF5' + } + } + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'dependencies/failed' + aggregationType: 7 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Dependency failures' + color: '#7E58FF' + } + } + ] + title: 'Server exceptions and Dependency failures' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 8 + y: 5 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'performanceCounters/processorCpuPercentage' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Processor time' + color: '#47BDF5' + } + } + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'performanceCounters/processCpuPercentage' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Process CPU' + color: '#7E58FF' + } + } + ] + title: 'Average processor and process CPU utilization' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 12 + y: 5 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'exceptions/browser' + aggregationType: 7 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Browser exceptions' + color: '#47BDF5' + } + } + ] + title: 'Browser exceptions' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 0 + y: 8 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'availabilityResults/count' + aggregationType: 7 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Availability test results count' + color: '#47BDF5' + } + } + ] + title: 'Availability test results count' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 4 + y: 8 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'performanceCounters/processIOBytesPerSecond' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Process IO rate' + color: '#47BDF5' + } + } + ] + title: 'Average process I/O rate' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 8 + y: 8 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'performanceCounters/memoryAvailableBytes' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Available memory' + color: '#47BDF5' + } + } + ] + title: 'Average available memory' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + ] + } + ] + } +} + +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = { + name: applicationInsightsName +} diff --git a/infra/core/monitor/applicationinsights.bicep b/infra/core/monitor/applicationinsights.bicep new file mode 100644 index 000000000..4b4d01e34 --- /dev/null +++ b/infra/core/monitor/applicationinsights.bicep @@ -0,0 +1,30 @@ +metadata description = 'Creates an Application Insights instance based on an existing Log Analytics workspace.' +param name string +param dashboardName string = '' +param location string = resourceGroup().location +param tags object = {} +param logAnalyticsWorkspaceId string + +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { + name: name + location: location + tags: tags + kind: 'web' + properties: { + Application_Type: 'web' + WorkspaceResourceId: logAnalyticsWorkspaceId + } +} + +module applicationInsightsDashboard 'applicationinsights-dashboard.bicep' = if (!empty(dashboardName)) { + name: 'application-insights-dashboard' + params: { + name: dashboardName + location: location + applicationInsightsName: applicationInsights.name + } +} + +output connectionString string = applicationInsights.properties.ConnectionString +output instrumentationKey string = applicationInsights.properties.InstrumentationKey +output name string = applicationInsights.name diff --git a/infra/core/monitor/loganalytics.bicep b/infra/core/monitor/loganalytics.bicep new file mode 100644 index 000000000..33f9dc294 --- /dev/null +++ b/infra/core/monitor/loganalytics.bicep @@ -0,0 +1,22 @@ +metadata description = 'Creates a Log Analytics workspace.' +param name string +param location string = resourceGroup().location +param tags object = {} + +resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = { + name: name + location: location + tags: tags + properties: any({ + retentionInDays: 30 + features: { + searchVersion: 1 + } + sku: { + name: 'PerGB2018' + } + }) +} + +output id string = logAnalytics.id +output name string = logAnalytics.name diff --git a/infra/core/monitor/monitoring.bicep b/infra/core/monitor/monitoring.bicep new file mode 100644 index 000000000..6bb05b0bd --- /dev/null +++ b/infra/core/monitor/monitoring.bicep @@ -0,0 +1,32 @@ +metadata description = 'Creates an Application Insights instance and a Log Analytics workspace.' +param logAnalyticsName string +param applicationInsightsName string +param applicationInsightsDashboardName string = '' +param location string = resourceGroup().location +param tags object = {} + +module logAnalytics 'loganalytics.bicep' = { + name: 'loganalytics' + params: { + name: logAnalyticsName + location: location + tags: tags + } +} + +module applicationInsights 'applicationinsights.bicep' = { + name: 'applicationinsights' + params: { + name: applicationInsightsName + location: location + tags: tags + dashboardName: applicationInsightsDashboardName + logAnalyticsWorkspaceId: logAnalytics.outputs.id + } +} + +output applicationInsightsConnectionString string = applicationInsights.outputs.connectionString +output applicationInsightsInstrumentationKey string = applicationInsights.outputs.instrumentationKey +output applicationInsightsName string = applicationInsights.outputs.name +output logAnalyticsWorkspaceId string = logAnalytics.outputs.id +output logAnalyticsWorkspaceName string = logAnalytics.outputs.name diff --git a/infra/core/search/search-services.bicep b/infra/core/search/search-services.bicep new file mode 100644 index 000000000..e7747e515 --- /dev/null +++ b/infra/core/search/search-services.bicep @@ -0,0 +1,63 @@ +metadata description = 'Creates an Azure AI Search instance.' +param name string +param location string = resourceGroup().location +param tags object = {} + +param sku object = { + name: 'standard' +} + +param authOptions object = {} +param disableLocalAuth bool = false +param disabledDataExfiltrationOptions array = [] +param encryptionWithCmk object = { + enforcement: 'Unspecified' +} +@allowed([ + 'default' + 'highDensity' +]) +param hostingMode string = 'default' +param networkRuleSet object = { + bypass: 'None' + ipRules: [] +} +param partitionCount int = 1 +@allowed([ + 'enabled' + 'disabled' +]) +param publicNetworkAccess string = 'enabled' +param replicaCount int = 1 +@allowed([ + 'disabled' + 'free' + 'standard' +]) +param semanticSearch string = 'disabled' + +resource search 'Microsoft.Search/searchServices@2021-04-01-preview' = { + name: name + location: location + tags: tags + identity: { + type: 'SystemAssigned' + } + properties: { + authOptions: authOptions + disableLocalAuth: disableLocalAuth + disabledDataExfiltrationOptions: disabledDataExfiltrationOptions + encryptionWithCmk: encryptionWithCmk + hostingMode: hostingMode + networkRuleSet: networkRuleSet + partitionCount: partitionCount + publicNetworkAccess: publicNetworkAccess + replicaCount: replicaCount + semanticSearch: semanticSearch + } + sku: sku +} + +output id string = search.id +output endpoint string = 'https://${name}.search.windows.net/' +output name string = search.name diff --git a/infra/core/security/keyvault-access.bicep b/infra/core/security/keyvault-access.bicep new file mode 100644 index 000000000..316775f21 --- /dev/null +++ b/infra/core/security/keyvault-access.bicep @@ -0,0 +1,22 @@ +metadata description = 'Assigns an Azure Key Vault access policy.' +param name string = 'add' + +param keyVaultName string +param permissions object = { secrets: [ 'get', 'list' ] } +param principalId string + +resource keyVaultAccessPolicies 'Microsoft.KeyVault/vaults/accessPolicies@2022-07-01' = { + parent: keyVault + name: name + properties: { + accessPolicies: [ { + objectId: principalId + tenantId: subscription().tenantId + permissions: permissions + } ] + } +} + +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { + name: keyVaultName +} diff --git a/infra/core/security/keyvault-secret.bicep b/infra/core/security/keyvault-secret.bicep new file mode 100644 index 000000000..7441b2961 --- /dev/null +++ b/infra/core/security/keyvault-secret.bicep @@ -0,0 +1,31 @@ +metadata description = 'Creates or updates a secret in an Azure Key Vault.' +param name string +param tags object = {} +param keyVaultName string +param contentType string = 'string' +@description('The value of the secret. Provide only derived values like blob storage access, but do not hard code any secrets in your templates') +@secure() +param secretValue string + +param enabled bool = true +param exp int = 0 +param nbf int = 0 + +resource keyVaultSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { + name: name + tags: tags + parent: keyVault + properties: { + attributes: { + enabled: enabled + exp: exp + nbf: nbf + } + contentType: contentType + value: secretValue + } +} + +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { + name: keyVaultName +} diff --git a/infra/core/security/keyvault.bicep b/infra/core/security/keyvault.bicep new file mode 100644 index 000000000..314a1db61 --- /dev/null +++ b/infra/core/security/keyvault.bicep @@ -0,0 +1,26 @@ +metadata description = 'Creates an Azure Key Vault.' +param name string +param location string = resourceGroup().location +param tags object = {} + +param principalId string = '' + +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = { + name: name + location: location + tags: tags + properties: { + tenantId: subscription().tenantId + sku: { family: 'A', name: 'standard' } + accessPolicies: !empty(principalId) ? [ + { + objectId: principalId + permissions: { secrets: [ 'get', 'list' ] } + tenantId: subscription().tenantId + } + ] : [] + } +} + +output endpoint string = keyVault.properties.vaultUri +output name string = keyVault.name diff --git a/infra/core/security/registry-access.bicep b/infra/core/security/registry-access.bicep new file mode 100644 index 000000000..5335efabc --- /dev/null +++ b/infra/core/security/registry-access.bicep @@ -0,0 +1,19 @@ +metadata description = 'Assigns ACR Pull permissions to access an Azure Container Registry.' +param containerRegistryName string +param principalId string + +var acrPullRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') + +resource aksAcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: containerRegistry // Use when specifying a scope that is different than the deployment scope + name: guid(subscription().id, resourceGroup().id, principalId, acrPullRole) + properties: { + roleDefinitionId: acrPullRole + principalType: 'ServicePrincipal' + principalId: principalId + } +} + +resource containerRegistry 'Microsoft.ContainerRegistry/registries@2022-02-01-preview' existing = { + name: containerRegistryName +} diff --git a/infra/core/security/role.bicep b/infra/core/security/role.bicep new file mode 100644 index 000000000..0b30cfd34 --- /dev/null +++ b/infra/core/security/role.bicep @@ -0,0 +1,21 @@ +metadata description = 'Creates a role assignment for a service principal.' +param principalId string + +@allowed([ + 'Device' + 'ForeignGroup' + 'Group' + 'ServicePrincipal' + 'User' +]) +param principalType string = 'ServicePrincipal' +param roleDefinitionId string + +resource role 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(subscription().id, resourceGroup().id, principalId, roleDefinitionId) + properties: { + principalId: principalId + principalType: principalType + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionId) + } +} diff --git a/infra/core/storage/storage-account.bicep b/infra/core/storage/storage-account.bicep new file mode 100644 index 000000000..dd398144a --- /dev/null +++ b/infra/core/storage/storage-account.bicep @@ -0,0 +1,81 @@ +metadata description = 'Creates an Azure storage account.' +param name string +param location string = resourceGroup().location +param tags object = {} + +@allowed([ + 'Cool' + 'Hot' + 'Premium' ]) +param accessTier string = 'Hot' +param allowBlobPublicAccess bool = true +param allowCrossTenantReplication bool = true +param allowSharedKeyAccess bool = true +param containers array = [] +param defaultToOAuthAuthentication bool = false +param deleteRetentionPolicy object = {} +@allowed([ 'AzureDnsZone', 'Standard' ]) +param dnsEndpointType string = 'Standard' +param kind string = 'StorageV2' +param minimumTlsVersion string = 'TLS1_2' +param queues array = [] +param supportsHttpsTrafficOnly bool = true +param networkAcls object = { + bypass: 'AzureServices' + defaultAction: 'Allow' +} +@allowed([ 'Enabled', 'Disabled' ]) +param publicNetworkAccess string = 'Enabled' +param sku object = { name: 'Standard_LRS' } + +resource storage 'Microsoft.Storage/storageAccounts@2022-05-01' = { + name: name + location: location + tags: tags + kind: kind + sku: sku + properties: { + accessTier: accessTier + allowBlobPublicAccess: allowBlobPublicAccess + allowCrossTenantReplication: allowCrossTenantReplication + allowSharedKeyAccess: allowSharedKeyAccess + defaultToOAuthAuthentication: defaultToOAuthAuthentication + dnsEndpointType: dnsEndpointType + minimumTlsVersion: minimumTlsVersion + networkAcls: networkAcls + publicNetworkAccess: publicNetworkAccess + supportsHttpsTrafficOnly: supportsHttpsTrafficOnly + } + + resource blobServices 'blobServices' = if (!empty(containers)) { + name: 'default' + properties: { + deleteRetentionPolicy: deleteRetentionPolicy + } + resource container 'containers' = [for container in containers: { + name: container.name + properties: { + publicAccess: contains(container, 'publicAccess') ? container.publicAccess : 'None' + } + }] + } + + resource queueServices 'queueServices' = if (!empty(queues)) { + name: 'default' + properties: { + cors: { + corsRules: [] + } + } + resource queue 'queues' = [for queue in queues: { + name: queue.name + properties: { + metadata: {} + } + }] + } +} + +output name string = storage.name +output id string = storage.id +output primaryEndpoints object = storage.properties.primaryEndpoints diff --git a/infra/main.bicep b/infra/main.bicep new file mode 100644 index 000000000..8b936432d --- /dev/null +++ b/infra/main.bicep @@ -0,0 +1,751 @@ +targetScope = 'subscription' + +@minLength(1) +@maxLength(64) +@description('Name of the the environment which is used to generate a short unique hash used in all resources.') +param environmentName string + +param resourceToken string = toLower(uniqueString(subscription().id, environmentName, location)) + +@description('Location for all resources.') +param location string + +@description('Name of App Service plan') +param hostingPlanName string = '${environmentName}-hosting-plan-${resourceToken}' + +@description('The pricing tier for the App Service plan') +@allowed([ + 'F1' + 'D1' + 'B1' + 'B2' + 'B3' + 'S1' + 'S2' + 'S3' + 'P1' + 'P2' + 'P3' + 'P4' +]) +param hostingPlanSku string = 'B3' + +@description('Name of Web App') +param websiteName string = '${environmentName}-website-${resourceToken}' + +@description('Name of Application Insights') +param applicationInsightsName string = '${environmentName}-appinsights-${resourceToken}' + +@description('Use semantic search') +param azureSearchUseSemanticSearch string = 'false' + +@description('Semantic search config') +param azureSearchSemanticSearchConfig string = 'default' + +@description('Is the index prechunked') +param azureSearchIndexIsPrechunked string = 'false' + +@description('Top K results') +param azureSearchTopK string = '5' + +@description('Enable in domain') +param azureSearchEnableInDomain string = 'false' + +@description('Content columns') +param azureSearchContentColumns string = 'content' + +@description('Filename column') +param azureSearchFilenameColumn string = 'filename' + +@description('Title column') +param azureSearchTitleColumn string = 'title' + +@description('Url column') +param azureSearchUrlColumn string = 'url' + +@description('Name of Azure OpenAI Resource') +param azureOpenAIResourceName string = '${environmentName}-openai-${resourceToken}' + +@description('Name of Azure OpenAI Resource SKU') +param azureOpenAISkuName string = 'S0' + +@description('Azure OpenAI Model Deployment Name') +param azureOpenAIModel string = 'gpt-35-turbo' + +@description('Azure OpenAI Model Name') +param azureOpenAIModelName string = 'gpt-35-turbo' + +param azureOpenAIModelVersion string = '0613' + +@description('Orchestration strategy: openai_function or langchain str. If you use a old version of turbo (0301), plese select langchain') +@allowed([ + 'openai_function' + 'langchain' +]) +param orchestrationStrategy string = 'langchain' + +@description('Azure OpenAI Temperature') +param azureOpenAITemperature string = '0' + +@description('Azure OpenAI Top P') +param azureOpenAITopP string = '1' + +@description('Azure OpenAI Max Tokens') +param azureOpenAIMaxTokens string = '1000' + +@description('Azure OpenAI Stop Sequence') +param azureOpenAIStopSequence string = '\n' + +@description('Azure OpenAI System Message') +param azureOpenAISystemMessage string = 'You are an AI assistant that helps people find information.' + +@description('Azure OpenAI Api Version') +param azureOpenAIApiVersion string = '2023-07-01-preview' + +@description('Whether or not to stream responses from Azure OpenAI') +param azureOpenAIStream string = 'true' + +@description('Azure OpenAI Embedding Model Deployment Name') +param azureOpenAIEmbeddingModel string = 'text-embedding-ada-002' + +@description('Azure OpenAI Embedding Model Name') +param azureOpenAIEmbeddingModelName string = 'text-embedding-ada-002' + +@description('Azure AI Search Resource') +param azureAISearchName string = '${environmentName}-search-${resourceToken}' + +@description('The SKU of the search service you want to create. E.g. free or standard') +@allowed([ + 'free' + 'basic' + 'standard' + 'standard2' + 'standard3' +]) +param azureSearchSku string = 'standard' + +@description('Azure AI Search Index') +param azureSearchIndex string = '${environmentName}-index-${resourceToken}' + +@description('Azure AI Search Conversation Log Index') +param azureSearchConversationLogIndex string = 'conversations' + +@description('Name of Storage Account') +param storageAccountName string = 'str${resourceToken}' + +@description('Name of Function App for Batch document processing') +param functionName string = '${environmentName}-backend-${resourceToken}' + +@description('Azure Form Recognizer Name') +param formRecognizerName string = '${environmentName}-formrecog-${resourceToken}' + +@description('Azure Content Safety Name') +param contentSafetyName string = '${environmentName}-contentsafety-${resourceToken}' + +@description('Azure Speech Service Name') +param speechServiceName string = '${environmentName}-speechservice-${resourceToken}' + +param newGuidString string = newGuid() +param searchTag string = 'chatwithyourdata-sa' +param useKeyVault bool + +@description('Id of the user or app to assign application roles') +param principalId string = '' + +@allowed([ + 'rbac' + 'keys' +]) +param authType string + +var blobContainerName = 'documents' +var queueName = 'doc-processing' +var clientKey = '${uniqueString(guid(subscription().id, deployment().name))}${newGuidString}' +var eventGridSystemTopicName = 'doc-processing' +var tags = { 'azd-env-name': environmentName } +var rgName = 'rg-${environmentName}' +var keyVaultName = 'kv-${resourceToken}' + +// Organize resources in a resource group +resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: rgName + location: location + tags: tags +} + +// Store secrets in a keyvault +module keyvault './core/security/keyvault.bicep' = if (useKeyVault || authType == 'rbac') { + name: 'keyvault' + scope: rg + params: { + name: keyVaultName + location: location + tags: tags + principalId: principalId + } +} + +module webaccess './core/security/keyvault-access.bicep' = if (useKeyVault) { + name: 'web-keyvault-access' + scope: rg + params: { + keyVaultName: keyVaultName + principalId: web.outputs.FRONTEND_API_IDENTITY_PRINCIPAL_ID + } +} + +module adminwebaccess './core/security/keyvault-access.bicep' = if (useKeyVault) { + name: 'adminweb-keyvault-access' + scope: rg + params: { + keyVaultName: keyVaultName + principalId: adminweb.outputs.WEBSITE_ADMIN_IDENTITY_PRINCIPAL_ID + } +} + +module functionaccess './core/security/keyvault-access.bicep' = if (useKeyVault) { + name: 'function-keyvault-access' + scope: rg + params: { + keyVaultName: keyVaultName + principalId: function.outputs.FUNCTION_IDENTITY_PRINCIPAL_ID + } +} + +module openai 'core/ai/cognitiveservices.bicep' = { + name: azureOpenAIResourceName + scope: rg + params: { + name: azureOpenAIResourceName + location: location + tags: tags + sku: { + name: azureOpenAISkuName + } + deployments: [ + { + name: azureOpenAIModel + model: { + format: 'OpenAI' + name: azureOpenAIModelName + version: azureOpenAIModelVersion + } + sku: { + name: 'Standard' + capacity: 30 + } + } + { + name: azureOpenAIEmbeddingModel + model: { + format: 'OpenAI' + name: azureOpenAIEmbeddingModelName + version: '2' + } + capacity: 30 + } + ] + } +} + +module speechService 'core/ai/cognitiveservices.bicep' = { + scope: rg + name: speechServiceName + params:{ + name: speechServiceName + location: location + sku: { + name: 'S0' + } + kind: 'SpeechServices' + } +} + +module storekeys './app/storekeys.bicep' = if (useKeyVault) { + name: 'storekeys' + scope: rg + params: { + keyVaultName: keyVaultName + azureOpenAIName: openai.outputs.name + azureAISearchName: search.outputs.name + storageAccountName: storage.outputs.name + formRecognizerName: formrecognizer.outputs.name + contentSafetyName: contentsafety.outputs.name + speechServiceName: speechServiceName + rgName: rgName + } +} + +module search './core/search/search-services.bicep' = { + name: azureAISearchName + scope: rg + params:{ + name: azureAISearchName + location: location + tags: { + deployment : searchTag + } + sku: { + name: azureSearchSku + } + authOptions: { + aadOrApiKey: { + aadAuthFailureMode: 'http403' + } + } + } +} + +module hostingplan './core/host/appserviceplan.bicep' = { + name: hostingPlanName + scope: rg + params: { + name: hostingPlanName + location: location + sku: { + name: hostingPlanSku + } + reserved: true + } +} + +module web './app/web.bicep' = { + name: websiteName + scope: rg + params: { + name: websiteName + location: location + tags: { 'azd-service-name': 'web' } + appServicePlanId: hostingplan.outputs.name + applicationInsightsName: monitoring.outputs.applicationInsightsName + azureOpenAIName: openai.outputs.name + azureAISearchName: search.outputs.name + storageAccountName: storage.outputs.name + formRecognizerName: formrecognizer.outputs.name + contentSafetyName: contentsafety.outputs.name + speechServiceName: speechService.outputs.name + openAIKeyName: useKeyVault ? storekeys.outputs.OPENAI_KEY_NAME : '' + storageAccountKeyName: useKeyVault ? storekeys.outputs.STORAGE_ACCOUNT_KEY_NAME : '' + formRecognizerKeyName: useKeyVault ? storekeys.outputs.FORM_RECOGNIZER_KEY_NAME : '' + searchKeyName: useKeyVault ? storekeys.outputs.SEARCH_KEY_NAME : '' + contentSafetyKeyName: useKeyVault ? storekeys.outputs.CONTENT_SAFETY_KEY_NAME : '' + speechKeyName: useKeyVault ? storekeys.outputs.SPEECH_KEY_NAME: '' + useKeyVault: useKeyVault + keyVaultName: useKeyVault || authType == 'rbac' ? keyvault.outputs.name : '' + keyVaultEndpoint: useKeyVault ? keyvault.outputs.endpoint : '' + authType: authType + appSettings: { + AZURE_SEARCH_USE_SEMANTIC_SEARCH: azureSearchUseSemanticSearch + AZURE_SEARCH_SERVICE: 'https://${azureAISearchName}.search.windows.net' + AZURE_SEARCH_INDEX: azureSearchIndex + AZURE_SEARCH_CONVERSATIONS_LOG_INDEX: azureSearchConversationLogIndex + AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG: azureSearchSemanticSearchConfig + AZURE_SEARCH_INDEX_IS_PRECHUNKED: azureSearchIndexIsPrechunked + AZURE_SEARCH_TOP_K: azureSearchTopK + AZURE_SEARCH_ENABLE_IN_DOMAIN: azureSearchEnableInDomain + AZURE_SEARCH_CONTENT_COLUMNS: azureSearchContentColumns + AZURE_SEARCH_FILENAME_COLUMN: azureSearchFilenameColumn + AZURE_SEARCH_TITLE_COLUMN: azureSearchTitleColumn + AZURE_SEARCH_URL_COLUMN: azureSearchUrlColumn + AZURE_OPENAI_RESOURCE: azureOpenAIResourceName + AZURE_OPENAI_MODEL: azureOpenAIModel + AZURE_OPENAI_MODEL_NAME: azureOpenAIModelName + AZURE_OPENAI_TEMPERATURE: azureOpenAITemperature + AZURE_OPENAI_TOP_P: azureOpenAITopP + AZURE_OPENAI_MAX_TOKENS: azureOpenAIMaxTokens + AZURE_OPENAI_STOP_SEQUENCE: azureOpenAIStopSequence + AZURE_OPENAI_SYSTEM_MESSAGE: azureOpenAISystemMessage + AZURE_OPENAI_API_VERSION: azureOpenAIApiVersion + AZURE_OPENAI_STREAM: azureOpenAIStream + AZURE_OPENAI_EMBEDDING_MODEL: azureOpenAIEmbeddingModel + AZURE_FORM_RECOGNIZER_ENDPOINT: 'https://${location}.api.cognitive.microsoft.com/' + AZURE_BLOB_ACCOUNT_NAME: storageAccountName + AZURE_BLOB_CONTAINER_NAME: blobContainerName + ORCHESTRATION_STRATEGY: orchestrationStrategy + AZURE_CONTENT_SAFETY_ENDPOINT: 'https://${location}.api.cognitive.microsoft.com/' + APPINSIGHTS_CONNECTION_STRING: monitoring.outputs.applicationInsightsConnectionString + AZURE_SPEECH_SERVICE_NAME: speechServiceName + AZURE_SPEECH_SERVICE_REGION: location + } + } +} + +module adminweb './app/adminweb.bicep' = { + name: '${websiteName}-admin' + scope: rg + params: { + name: '${websiteName}-admin' + location: location + tags: { 'azd-service-name': 'adminweb' } + appServicePlanId: hostingplan.outputs.name + applicationInsightsName: monitoring.outputs.applicationInsightsName + azureOpenAIName: openai.outputs.name + azureAISearchName: search.outputs.name + storageAccountName: storage.outputs.name + formRecognizerName: formrecognizer.outputs.name + contentSafetyName: contentsafety.outputs.name + speechServiceName: speechService.outputs.name + openAIKeyName: useKeyVault ? storekeys.outputs.OPENAI_KEY_NAME : '' + storageAccountKeyName: useKeyVault ? storekeys.outputs.STORAGE_ACCOUNT_KEY_NAME : '' + formRecognizerKeyName: useKeyVault ? storekeys.outputs.FORM_RECOGNIZER_KEY_NAME : '' + searchKeyName: useKeyVault ? storekeys.outputs.SEARCH_KEY_NAME : '' + contentSafetyKeyName: useKeyVault ? storekeys.outputs.CONTENT_SAFETY_KEY_NAME : '' + speechKeyName: useKeyVault ? storekeys.outputs.SPEECH_KEY_NAME: '' + useKeyVault: useKeyVault + keyVaultName: useKeyVault || authType == 'rbac' ? keyvault.outputs.name : '' + keyVaultEndpoint: useKeyVault ? keyvault.outputs.endpoint : '' + authType: authType + appSettings: { + AZURE_SEARCH_SERVICE: 'https://${azureAISearchName}.search.windows.net' + AZURE_SEARCH_INDEX: azureSearchIndex + AZURE_SEARCH_USE_SEMANTIC_SEARCH: azureSearchUseSemanticSearch + AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG: azureSearchSemanticSearchConfig + AZURE_SEARCH_INDEX_IS_PRECHUNKED: azureSearchIndexIsPrechunked + AZURE_SEARCH_TOP_K: azureSearchTopK + AZURE_SEARCH_ENABLE_IN_DOMAIN: azureSearchEnableInDomain + AZURE_SEARCH_CONTENT_COLUMNS: azureSearchContentColumns + AZURE_SEARCH_FILENAME_COLUMN: azureSearchFilenameColumn + AZURE_SEARCH_TITLE_COLUMN: azureSearchTitleColumn + AZURE_SEARCH_URL_COLUMN: azureSearchUrlColumn + AZURE_OPENAI_RESOURCE: azureOpenAIResourceName + AZURE_OPENAI_MODEL: azureOpenAIModel + AZURE_OPENAI_MODEL_NAME: azureOpenAIModelName + AZURE_OPENAI_TEMPERATURE: azureOpenAITemperature + AZURE_OPENAI_TOP_P: azureOpenAITopP + AZURE_OPENAI_MAX_TOKENS: azureOpenAIMaxTokens + AZURE_OPENAI_STOP_SEQUENCE: azureOpenAIStopSequence + AZURE_OPENAI_SYSTEM_MESSAGE: azureOpenAISystemMessage + AZURE_OPENAI_API_VERSION: azureOpenAIApiVersion + AZURE_OPENAI_STREAM: azureOpenAIStream + AZURE_OPENAI_EMBEDDING_MODEL: azureOpenAIEmbeddingModel + AZURE_FORM_RECOGNIZER_ENDPOINT: 'https://${location}.api.cognitive.microsoft.com/' + AZURE_BLOB_ACCOUNT_NAME: storageAccountName + AZURE_BLOB_CONTAINER_NAME: blobContainerName + DOCUMENT_PROCESSING_QUEUE_NAME: queueName + BACKEND_URL: 'https://${functionName}.azurewebsites.net' + FUNCTION_KEY: clientKey + ORCHESTRATION_STRATEGY: orchestrationStrategy + AZURE_CONTENT_SAFETY_ENDPOINT: 'https://${location}.api.cognitive.microsoft.com/' + APPINSIGHTS_INSTRUMENTATIONKEY: monitoring.outputs.applicationInsightsInstrumentationKey + } + } +} + +module monitoring './core/monitor/monitoring.bicep' = { + name: 'monitoring' + scope: rg + params: { + applicationInsightsName: applicationInsightsName + location: location + tags: { + 'hidden-link:${resourceId('Microsoft.Web/sites', applicationInsightsName)}': 'Resource' + } + logAnalyticsName: '${environmentName}-logAnalytics-${resourceToken}' + applicationInsightsDashboardName: 'dash-${applicationInsightsName}' + } +} + +module function './app/function.bicep' = { + name: functionName + scope: rg + params:{ + name: functionName + location: location + tags: { 'azd-service-name': 'function' } + appServicePlanId: hostingplan.outputs.name + azureOpenAIName: openai.outputs.name + azureAISearchName: search.outputs.name + storageAccountName: storage.outputs.name + formRecognizerName: formrecognizer.outputs.name + contentSafetyName: contentsafety.outputs.name + speechServiceName: speechService.outputs.name + clientKey: clientKey + openAIKeyName: useKeyVault ? storekeys.outputs.OPENAI_KEY_NAME : '' + storageAccountKeyName: useKeyVault ? storekeys.outputs.STORAGE_ACCOUNT_KEY_NAME : '' + formRecognizerKeyName: useKeyVault ? storekeys.outputs.FORM_RECOGNIZER_KEY_NAME : '' + searchKeyName: useKeyVault ? storekeys.outputs.SEARCH_KEY_NAME : '' + contentSafetyKeyName: useKeyVault ? storekeys.outputs.CONTENT_SAFETY_KEY_NAME : '' + speechKeyName: useKeyVault ? storekeys.outputs.SPEECH_KEY_NAME: '' + useKeyVault: useKeyVault + keyVaultName: useKeyVault || authType == 'rbac' ? keyvault.outputs.name : '' + keyVaultEndpoint: useKeyVault ? keyvault.outputs.endpoint : '' + authType: authType + appSettings: { + FUNCTIONS_EXTENSION_VERSION: '~4' + WEBSITES_ENABLE_APP_SERVICE_STORAGE: 'false' + AZURE_OPENAI_MODEL: azureOpenAIModel + AZURE_OPENAI_EMBEDDING_MODEL: azureOpenAIEmbeddingModel + AZURE_OPENAI_RESOURCE: azureOpenAIResourceName + AZURE_BLOB_ACCOUNT_NAME: storageAccountName + AZURE_BLOB_CONTAINER_NAME: blobContainerName + AZURE_FORM_RECOGNIZER_ENDPOINT: 'https://${location}.api.cognitive.microsoft.com/' + AZURE_SEARCH_SERVICE: 'https://${azureAISearchName}.search.windows.net' + DOCUMENT_PROCESSING_QUEUE_NAME: queueName + AZURE_OPENAI_API_VERSION: azureOpenAIApiVersion + AZURE_SEARCH_INDEX: azureSearchIndex + ORCHESTRATION_STRATEGY: orchestrationStrategy + AZURE_CONTENT_SAFETY_ENDPOINT: 'https://${location}.api.cognitive.microsoft.com/' + APPINSIGHTS_INSTRUMENTATIONKEY: monitoring.outputs.applicationInsightsInstrumentationKey + } + } +} + +module formrecognizer 'core/ai/cognitiveservices.bicep' = { + name: formRecognizerName + scope: rg + params: { + name: formRecognizerName + location: location + tags: tags + kind: 'FormRecognizer' + } +} + +module contentsafety 'core/ai/cognitiveservices.bicep' = { + name: contentSafetyName + scope: rg + params: { + name: contentSafetyName + location: location + tags: tags + kind: 'ContentSafety' + } +} + +module eventgrid 'app/eventgrid.bicep' = { + name: eventGridSystemTopicName + scope: rg + params: { + name: eventGridSystemTopicName + location: location + storageAccountId: storage.outputs.id + queueName: queueName + blobContainerName: blobContainerName + } +} + +module storage 'core/storage/storage-account.bicep' = { + name: storageAccountName + scope: rg + params: { + name: storageAccountName + location: location + sku:{ + name: 'Standard_GRS' + } + containers:[ + { + name: blobContainerName + publicAccess: 'None' + } + { + name: 'config' + publicAccess: 'None' + } + ] + queues: [ + { + name: 'doc-processing' + } + { + name: 'doc-processing-poison' + } + ] + } +} + +// USER ROLES +module storageRoleUser 'core/security/role.bicep' = if (authType == 'rbac') { + scope: rg + name: 'storage-role-user' + params: { + principalId: principalId + roleDefinitionId: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' + principalType: 'User' + } +} + +// USER ROLES +module openaiRoleUser 'core/security/role.bicep' = if (authType == 'rbac') { + scope: rg + name: 'openai-role-user' + params: { + principalId: principalId + roleDefinitionId: 'a97b65f3-24c7-4388-baec-2e87135dc908' + principalType: 'User' + } +} + +// USER ROLES +module openaiRoleUserContributor 'core/security/role.bicep' = if (authType == 'rbac') { + scope: rg + name: 'openai-role-user-contributor' + params: { + principalId: principalId + roleDefinitionId: 'b24988ac-6180-42a0-ab88-20f7382dd24c' + principalType: 'User' + } +} + +// USER ROLES +module searchRoleUser 'core/security/role.bicep' = if (authType == 'rbac') { + scope: rg + name: 'search-role-user' + params: { + principalId: principalId + roleDefinitionId: '8ebe5a00-799e-43f5-93ac-243d3dce84a7' + principalType: 'User' + } +} + +// SYSTEM IDENTITIES +module storageRoleBackend 'core/security/role.bicep' = if (authType == 'rbac'){ + scope: rg + name: 'storage-role-backend' + params: { + principalId: adminweb.outputs.WEBSITE_ADMIN_IDENTITY_PRINCIPAL_ID + roleDefinitionId: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' + principalType: 'ServicePrincipal' + } +} + +// SYSTEM IDENTITIES +module openAIRoleBackend 'core/security/role.bicep' = if (authType == 'rbac'){ + scope: rg + name: 'openai-role-backend' + params: { + principalId: adminweb.outputs.WEBSITE_ADMIN_IDENTITY_PRINCIPAL_ID + roleDefinitionId: 'a97b65f3-24c7-4388-baec-2e87135dc908' + principalType: 'ServicePrincipal' + } +} + +// SYSTEM IDENTITIES +module openAIRoleWeb 'core/security/role.bicep' = if (authType == 'rbac'){ + scope: rg + name: 'openai-role-web' + params: { + principalId: web.outputs.FRONTEND_API_IDENTITY_PRINCIPAL_ID + roleDefinitionId: 'a97b65f3-24c7-4388-baec-2e87135dc908' + principalType: 'ServicePrincipal' + } +} + +// SYSTEM IDENTITIES +module openAIRoleFunction 'core/security/role.bicep' = if (authType == 'rbac'){ + scope: rg + name: 'openai-role-function' + params: { + principalId: function.outputs.FUNCTION_IDENTITY_PRINCIPAL_ID + roleDefinitionId: 'a97b65f3-24c7-4388-baec-2e87135dc908' + principalType: 'ServicePrincipal' + } +} + +// SYSTEM IDENTITIES +module openAIRoleBackendContributor 'core/security/role.bicep' = if (authType == 'rbac'){ + scope: rg + name: 'openai-role-backend-contributor' + params: { + principalId: adminweb.outputs.WEBSITE_ADMIN_IDENTITY_PRINCIPAL_ID + roleDefinitionId: 'b24988ac-6180-42a0-ab88-20f7382dd24c' + principalType: 'ServicePrincipal' + } +} + +// SYSTEM IDENTITIES +module openAIRoleWebContributor 'core/security/role.bicep' = if (authType == 'rbac'){ + scope: rg + name: 'openai-role-web-contributor' + params: { + principalId: web.outputs.FRONTEND_API_IDENTITY_PRINCIPAL_ID + roleDefinitionId: 'b24988ac-6180-42a0-ab88-20f7382dd24c' + principalType: 'ServicePrincipal' + } +} + +// SYSTEM IDENTITIES +module openAIRoleFunctionContributor 'core/security/role.bicep' = if (authType == 'rbac'){ + scope: rg + name: 'openai-role-function-contributor' + params: { + principalId: function.outputs.FUNCTION_IDENTITY_PRINCIPAL_ID + roleDefinitionId: 'b24988ac-6180-42a0-ab88-20f7382dd24c' + principalType: 'ServicePrincipal' + } +} + +// SYSTEM IDENTITIES +module searchRoleBackend 'core/security/role.bicep' = if (authType == 'rbac'){ + scope: rg + name: 'search-role-backend' + params: { + principalId: adminweb.outputs.WEBSITE_ADMIN_IDENTITY_PRINCIPAL_ID + roleDefinitionId: '8ebe5a00-799e-43f5-93ac-243d3dce84a7' + principalType: 'ServicePrincipal' + } +} + +// SYSTEM IDENTITIES +module searchRoleWeb 'core/security/role.bicep' = if (authType == 'rbac'){ + scope: rg + name: 'search-role-web' + params: { + principalId: web.outputs.FRONTEND_API_IDENTITY_PRINCIPAL_ID + roleDefinitionId: '8ebe5a00-799e-43f5-93ac-243d3dce84a7' + principalType: 'ServicePrincipal' + } +} + +// SYSTEM IDENTITIES +module searchRoleFunction 'core/security/role.bicep' = if (authType == 'rbac'){ + scope: rg + name: 'search-role-function' + params: { + principalId: function.outputs.FUNCTION_IDENTITY_PRINCIPAL_ID + roleDefinitionId: '8ebe5a00-799e-43f5-93ac-243d3dce84a7' + principalType: 'ServicePrincipal' + } +} + +output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString +output APPINSIGHTS_INSTRUMENTATIONKEY string = monitoring.outputs.applicationInsightsInstrumentationKey +output AZURE_KEY_VAULT_ENDPOINT string = useKeyVault ? keyvault.outputs.endpoint : '' +output AZURE_KEY_VAULT_NAME string = useKeyVault || authType == 'rbac' ? keyvault.outputs.name : '' +output AZURE_LOCATION string = location +output AZURE_TENANT_ID string = tenant().tenantId +output AZURE_CONTENT_SAFETY_ENDPOINT string = contentsafety.outputs.endpoint +output AZURE_SEARCH_SERVICE string = search.outputs.endpoint +output AZURE_FORM_RECOGNIZER_ENDPOINT string = formrecognizer.outputs.endpoint +output AZURE_SEARCH_USE_SEMANTIC_SEARCH string = azureSearchUseSemanticSearch +output AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG string = azureSearchSemanticSearchConfig +output AZURE_SEARCH_INDEX_IS_PRECHUNKED string = azureSearchIndexIsPrechunked +output AZURE_SEARCH_TOP_K string = azureSearchTopK +output AZURE_SEARCH_ENABLE_IN_DOMAIN string = azureSearchEnableInDomain +output AZURE_SEARCH_CONTENT_COLUMNS string = azureSearchContentColumns +output AZURE_SEARCH_FILENAME_COLUMN string = azureSearchFilenameColumn +output AZURE_SEARCH_TITLE_COLUMN string = azureSearchTitleColumn +output AZURE_SEARCH_URL_COLUMN string = azureSearchUrlColumn +output AZURE_OPENAI_MODEL_NAME string = azureOpenAIModelName +output AZURE_OPENAI_STREAM string = azureOpenAIStream +output AZURE_OPENAI_SYSTEM_MESSAGE string = azureOpenAISystemMessage +output AZURE_OPENAI_STOP_SEQUENCE string = azureOpenAIStopSequence +output AZURE_OPENAI_MAX_TOKENS string = azureOpenAIMaxTokens +output AZURE_OPENAI_TOP_P string = azureOpenAITopP +output AZURE_OPENAI_TEMPERATURE string = azureOpenAITemperature +output AZURE_SEARCH_INDEX string = azureSearchIndex +output AZURE_OPENAI_API_VERSION string = azureOpenAIApiVersion +output DOCUMENT_PROCESSING_QUEUE_NAME string = queueName +output AZURE_BLOB_CONTAINER_NAME string = blobContainerName +output AZURE_BLOB_ACCOUNT_NAME string = storageAccountName +output AZURE_OPENAI_RESOURCE string = azureOpenAIResourceName +output AZURE_OPENAI_EMBEDDING_MODEL string = azureOpenAIEmbeddingModel +output AZURE_OPENAI_MODEL string = azureOpenAIModel +output USE_KEY_VAULT bool = useKeyVault +output AZURE_OPENAI_KEY_NAME string = useKeyVault ? storekeys.outputs.OPENAI_KEY_NAME : '' +output AZURE_BLOB_ACCOUNT_KEY_NAME string = useKeyVault ? storekeys.outputs.STORAGE_ACCOUNT_KEY_NAME : '' +output AZURE_FORM_RECOGNIZER_KEY_NAME string = useKeyVault ? storekeys.outputs.FORM_RECOGNIZER_KEY_NAME : '' +output AZURE_SEARCH_KEY_NAME string = useKeyVault ? storekeys.outputs.SEARCH_KEY_NAME : '' +output AZURE_CONTENT_SAFETY_KEY_NAME string = useKeyVault ? storekeys.outputs.CONTENT_SAFETY_KEY_NAME : '' +output AZURE_SPEECH_SERVICE_REGION string = location +output AZURE_SPEECH_SERVICE_KEY_NAME string = useKeyVault ? storekeys.outputs.SPEECH_KEY_NAME : '' diff --git a/infra/main.bicepparam b/infra/main.bicepparam new file mode 100644 index 000000000..ffade4e20 --- /dev/null +++ b/infra/main.bicepparam @@ -0,0 +1,12 @@ +using './main.bicep' + +param environmentName = readEnvironmentVariable('AZURE_ENV_NAME', 'env_name') + +param location = readEnvironmentVariable('AZURE_LOCATION', 'location') + +param principalId = readEnvironmentVariable('AZURE_PRINCIPAL_ID', 'principal_id') + +// Please make sure to set this value to false when using rbac with AZURE_AUTH_TYPE +param useKeyVault = bool(readEnvironmentVariable('USE_KEY_VAULT', 'true')) + +param authType = readEnvironmentVariable('AZURE_AUTH_TYPE', 'keys')