diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 7167e0a..78bcf95 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,7 +2,7 @@ "image": "mcr.microsoft.com/devcontainers/base:noble", "features": { "ghcr.io/ljtill/features/task:latest": { - "version": "3.40.1" + "version": "3.41.0" } }, "customizations": { diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 95c1c89..0472402 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,9 +31,9 @@ jobs: id: infra-auth run: az login --identity - - name: Apply Infrastructure - id: infra-apply - run: task infra-apply + - name: Deploy Infrastructure + id: infra-deploy + run: task infra-deploy - name: Destroy Infrastructure id: infra-destroy @@ -56,15 +56,9 @@ jobs: with: repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Build API Service - id: app-build-api - run: task app-build-api - env: - CI: false - - - name: Build UI Service - id: app-build-ui - run: task app-build-ui + - name: Build Application + id: app-build + run: task app-build env: CI: false @@ -83,18 +77,22 @@ jobs: with: repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Deploy Flows - id: flow-deploy - run: task flow-deploy + - name: Build Flows + id: flow-build + run: task flow-build - name: Deploy Agent Flows - id: flow-deploy-agent - run: task flow-deploy-agent + id: flow-create-agent + run: task flow-create-agent - name: Deploy Evaluation Flows - id: flow-deploy-eval - run: task flow-deploy-eval + id: flow-create-eval + run: task flow-create-eval + + - name: Deploy Review Flows + id: flow-create-review + run: task flow-create-review - name: Deploy ML Endpoints - id: flow-deploy-ml - run: task flow-deploy- + id: flow-deploy + run: task flow-deploy diff --git a/.task/app.yml b/.task/app.yml index 9f90df0..e76f004 100644 --- a/.task/app.yml +++ b/.task/app.yml @@ -1,4 +1,4 @@ -version: "3.40.1" +version: "3.41.0" tasks: app-build: @@ -8,35 +8,20 @@ tasks: - task: deps-setup aliases: - ab + dir: app cmds: - - task: app-build-api - - task: app-build-ui - - app-build-api: - desc: Build API service. - silent: true - deps: - - task: deps-setup - aliases: - - aba - dir: app/api - cmds: - - echo -e "\033[0;32mBuilding API...\033[0m" - - python3 -m venv .venv - - .venv/bin/pip3 install -r requirements.txt + - | + echo -e "\033[0;32mBuilding API...\033[0m" + pushd api/ + python3 -m venv .venv + .venv/bin/pip3 install -r requirements.txt + popd - app-build-ui: - desc: Build UI service. - silent: true - deps: - - task: deps-setup - aliases: - - abu - dir: app/ui - cmds: - - echo -e "\033[0;32mBuilding UI...\033[0m" - - npm install --no-fund --no-audit - - npm run build + echo -e "\033[0;32mBuilding UI...\033[0m" + pushd ui/ + npm install --no-fund --no-audit + npm run build + popd app-clean: desc: Remove build artifacts. @@ -45,34 +30,19 @@ tasks: - task: deps-setup aliases: - ac + dir: app cmds: - - task: app-clean-api - - task: app-clean-ui - - app-clean-api: - desc: Remove API build artifacts. - silent: true - deps: - - task: deps-setup - aliases: - - aca - dir: app/api - cmds: - - echo -e "\033[0;32mRemoving API artifacts...\033[0m" - - rm -rf .venv - - rm -rf www + - | + echo -e "\033[0;32mRemoving API artifacts...\033[0m" + pushd app/api + rm -rf .venv + rm -rf www + popd - app-clean-ui: - desc: Remove UI build artifacts. - silent: true - deps: - - task: deps-setup - aliases: - - acu - dir: app/ui - cmds: - - echo -e "\033[0;32mRemoving UI artifacts...\033[0m" - - rm -rf node_modules + echo -e "\033[0;32mRemoving UI artifacts...\033[0m" + pushd app/ui + rm -rf node_modules + popd app-deploy: desc: Deploy application to App Service. @@ -107,14 +77,26 @@ tasks: --src-path "$ZIP_PATH" \ --type zip + app-logs: + desc: Show application logs. + silent: true + aliases: + - al + dir: infra + cmds: + - | + echo -e "\033[0;32mShowing logs...\033[0m" + az webapp log tail -g $(terraform output -raw resource_group) -n $(terraform output -raw webapp_name) + api-run: desc: Launch local API instance. silent: true - dir: app/api deps: - task: deps-setup aliases: - ar + dir: app/api cmds: - - echo -e "\033[0;32mLaunching API...\033[0m" - - .venv/bin/python3 -m uvicorn main:app --reload + - | + echo -e "\033[0;32mLaunching API...\033[0m" + .venv/bin/python3 -m uvicorn main:app --reload diff --git a/.task/flow.yml b/.task/flow.yml index 2197398..99f57ab 100644 --- a/.task/flow.yml +++ b/.task/flow.yml @@ -1,8 +1,8 @@ -version: "3.40.1" +version: "3.41.0" tasks: - flow-deploy: - desc: Deploy flows to AI Foundry. + flow-create-review: + desc: Upload and create flows in AI Foundry. silent: true deps: - task: deps-setup @@ -10,19 +10,20 @@ tasks: - fd dir: flows cmds: - - cmd: | + - | NAME_ARGUMENT={{if .FLOW_NAME}}"--set display_name="{{.FLOW_NAME}}{{else}}{{end}} - pushd ../infra > /dev/null - eval "$(terraform output -json | jq -r 'to_entries | .[] | "export ADR_" + (.key | ascii_upcase) + "=\"" + .value.value + "\"" ')" - popd > /dev/null + pushd ../infra + RESOURCE_GROUP=$(terraform output -raw resource_group) + APP_NAME=$(terraform output -raw webapp_name) + popd pfazure flow create \ --flow ai_doc_review \ - --workspace-name "$ADR_AI_HUB_PROJECT_NAME" \ - --resource-group "$ADR_RESOURCE_GROUP" \ + --workspace-name "$AI_HUB_PROJECT_NAME" \ + --resource-group "$RESOURCE_GROUP" \ $NAME_ARGUMENT - flow-deploy-agent: + flow-create-agent: desc: Deploy agent flows to AI Foundry. silent: true deps: @@ -31,19 +32,20 @@ tasks: - fda dir: flows cmds: - - cmd: | + - | NAME_ARGUMENT={{if .FLOW_NAME}}"--set display_name="{{.FLOW_NAME}}{{else}}{{end}} - pushd ../infra > /dev/null - eval "$(terraform output -json | jq -r 'to_entries | .[] | "export ADR_" + (.key | ascii_upcase) + "=\"" + .value.value + "\"" ')" - popd > /dev/null + pushd ../infra + RESOURCE_GROUP=$(terraform output -raw resource_group) + AI_HUB_PROJECT_NAME=$(terraform output -raw ai_hub_project_name) + popd pfazure flow create \ --flow ai_doc_review/agent_template \ - --workspace-name "$ADR_AI_HUB_PROJECT_NAME" \ - --resource-group "$ADR_RESOURCE_GROUP" \ + --workspace-name "$AI_HUB_PROJECT_NAME" \ + --resource-group "$RESOURCE_GROUP" \ $NAME_ARGUMENT - flow-deploy-eval: + flow-create-eval: desc: Deploy evaluation flows to AI Foundry. silent: true deps: @@ -52,64 +54,103 @@ tasks: - fde dir: flows cmds: - - cmd: | + - | NAME_ARGUMENT={{if .FLOW_NAME}}"--set display_name="{{.FLOW_NAME}}{{else}}{{end}} - pushd ../infra > /dev/null - eval "$(terraform output -json | jq -r 'to_entries | .[] | "export ADR_" + (.key | ascii_upcase) + "=\"" + .value.value + "\"" ')" - popd > /dev/null + pushd ../infra + RESOURCE_GROUP=$(terraform output -raw resource_group) + AI_HUB_PROJECT_NAME=$(terraform output -raw ai_hub_project_name) + popd pfazure flow create \ --flow ai_doc_review_eval \ - --workspace-name "$ADR_AI_HUB_PROJECT_NAME" \ - --resource-group "$ADR_RESOURCE_GROUP" \ + --workspace-name "$AI_HUB_PROJECT_NAME" \ + --resource-group "$RESOURCE_GROUP" \ --set type=evaluation $NAME_ARGUMENT - flow-deploy-endpoint: - desc: Deploy to ML endpoints. + flow-build: + desc: Build flow artifacts. + silent: true + deps: + - task: deps-setup + aliases: + - fb + dir: flows + cmds: + - | + echo -e "\033[0;32mBuilding flow artifacts...\033[0m" + python3 -m venv .venv + .venv/bin/pip3 install -r requirements.txt + .venv/bin/pip3 install -r ./ai_doc_review/requirements.txt + .venv/bin/pip3 install -r ./ai_doc_review_eval/requirements.txt + .venv/bin/pip3 install keyrings.alt + + flow-clean: + desc: Remove flow artifacts. + silent: true + deps: + - task: deps-setup + aliases: + - fb + dir: flows + cmds: + - | + echo -e "\033[0;32mRemoving flow artifacts...\033[0m" + rm -rf .venv + rm -rf ai_doc_review/common + + flow-deploy: + desc: Deploy flow endpoint to App Service. silent: true deps: - task: deps-setup aliases: - fde - dir: flows/ai_doc_review + dir: flows cmds: - cmd: | - pushd ../../infra > /dev/null - eval "$(terraform output -json | jq -r 'to_entries | .[] | "export ADR_" + (.key | ascii_upcase) + "=\"" + .value.value + "\"" ')" - popd > /dev/null + echo -e "\033[0;32mUploading flow...\033[0m" + TEMP_DIR=$(mktemp -d) + trap "rm -rf $TEMP_DIR" EXIT - cp -r ../../common/ common/ - cp ./deployment.yaml ./sub_deployment.yaml - export MODEL_VERSION=$(az ml model create --workspace-name "$ADR_AI_HUB_PROJECT_NAME" --resource-group "$ADR_RESOURCE_GROUP" --file model.yaml --query version -o tsv) + cp -a ai_doc_review "$TEMP_DIR/" + rm -rf "$TEMP_DIR/ai_doc_review/common" + cp -a ../common "$TEMP_DIR/ai_doc_review/common" + pushd $TEMP_DIR/ai_doc_review + zip -q -r --symlinks "$TEMP_DIR/flow.zip" * + popd - sed -i 's|VAR_MODEL_VERSION|'"$MODEL_VERSION"'|g' sub_deployment.yaml - sed -i 's|VAR_SUBSCRIPTION_ID|'"$ADR_SUBSCRIPTION_ID"'|g' sub_deployment.yaml - sed -i 's|VAR_RESOURCE_GROUP|'"$ADR_RESOURCE_GROUP"'|g' sub_deployment.yaml - sed -i 's|VAR_AI_HUB_PROJECT_NAME|'"$ADR_AI_HUB_PROJECT_NAME"'|g' sub_deployment.yaml - sed -i 's|VAR_ENDPOINT_NAME|'"$ADR_AML_ENDPOINT_NAME"'|g' sub_deployment.yaml - sed -i 's|VAR_AZURE_OPENAI_ENDPOINT|'"$ADR_AZURE_OPENAI_ENDPOINT"'|g' sub_deployment.yaml - sed -i 's|VAR_IDENTITY_CLIENT_ID|'"$ADR_IDENTITY_CLIENT_ID"'|g' sub_deployment.yaml - sed -i 's|VAR_DOCUMENT_INTELLIGENCE_ENDPOINT|'"$ADR_DOCUMENT_INTELLIGENCE_ENDPOINT"'|g' sub_deployment.yaml - sed -i 's|VAR_STORAGE_URL_PREFIX|'"$ADR_STORAGE_URL_PREFIX"'|g' sub_deployment.yaml + pushd ../infra + RESOURCE_GROUP=$(terraform output -raw resource_group) + FLOW_NAME=$(terraform output -raw flowapp_name) + popd - az ml online-deployment show \ - --name ai-doc-review-deployment \ - --endpoint-name "$ADR_AML_ENDPOINT_NAME" \ - --workspace-name "$ADR_AI_HUB_PROJECT_NAME" \ - --resource-group "$ADR_RESOURCE_GROUP" \ - && az ml online-deployment update \ - --file sub_deployment.yaml \ - --workspace-name "$ADR_AI_HUB_PROJECT_NAME" \ - --resource-group "$ADR_RESOURCE_GROUP" \ - || az ml online-deployment create \ - --file sub_deployment.yaml \ - --workspace-name "$ADR_AI_HUB_PROJECT_NAME" \ - --resource-group "$ADR_RESOURCE_GROUP" \ - --all-traffic + ZIP_PATH="$TEMP_DIR/flow.zip" + az webapp deploy \ + -g $RESOURCE_GROUP \ + -n $FLOW_NAME \ + --src-path "$ZIP_PATH" \ + --type zip - az ml online-endpoint update \ - --name $ADR_AML_ENDPOINT_NAME \ - --traffic "ai-doc-review-deployment=100" \ - --workspace-name "$ADR_AI_HUB_PROJECT_NAME" \ - --resource-group "$ADR_RESOURCE_GROUP" + flow-run: + desc: Launch local flow instance. + silent: true + deps: + - task: deps-setup + aliases: + - fr + dir: flows/ai_doc_review + cmds: + - | + echo -e "\033[0;32mLaunching flow...\033[0m" + pf flow serve --source ./ --port 8080 --host localhost + + flow-logs: + desc: Show logs for ML endpoint. + silent: true + aliases: + - fle + dir: infra + cmds: + - | + az webapp log tail --name $(terraform output -raw flowapp_name) --resource-group $(terraform output -raw resource_group) diff --git a/.task/infra.yml b/.task/infra.yml index a643e42..e6f49a2 100644 --- a/.task/infra.yml +++ b/.task/infra.yml @@ -1,4 +1,4 @@ -version: "3.40.1" +version: "3.41.0" tasks: infra-init: @@ -10,8 +10,9 @@ tasks: - ii dir: infra cmds: - - echo -e "\033[0;32mInitialising Terraform...\033[0m" - - terraform init + - | + echo -e "\033[0;32mInitialising Terraform...\033[0m" + terraform init infra-plan: desc: Plan infrastructure changes. @@ -22,9 +23,9 @@ tasks: - ip dir: infra cmds: - - echo -e "\033[0;32mPlanning changes...\033[0m" - - terraform plan - -var-file=environments/local.tfvars + - | + echo -e "\033[0;32mPlanning changes...\033[0m" + terraform plan -var-file=environments/local.tfvars infra-deploy: desc: Deploy infrastructure. @@ -35,10 +36,9 @@ tasks: - ia dir: infra cmds: - - echo -e "\033[0;32mApplying changes...\033[0m" - - terraform apply - -var-file=environments/local.tfvars - -auto-approve + - | + echo -e "\033[0;32mApplying changes...\033[0m" + terraform apply -var-file=environments/local.tfvars -auto-approve infra-destroy: desc: Destroy infrastructure. @@ -49,44 +49,21 @@ tasks: - id dir: infra cmds: - - echo -e "\033[0;32mDestroying resources...\033[0m" - - terraform destroy - -var-file=environments/local.tfvars - -auto-approve + - | + echo -e "\033[0;32mDestroying resources...\033[0m" + terraform destroy -var-file=environments/local.tfvars -auto-approve - infra-state-refresh: - desc: Refresh the state of all infrastructure. + infra-clean: + desc: Remove infrastructure artifacts. silent: true deps: - task: deps-setup aliases: - - isf + - ic dir: infra cmds: - - echo -e "\033[0;32mRefreshing state...\033[0m" - - terraform refresh - -var-file=environments/local.tfvars - - infra-state-reset: - desc: Reset the state of all infrastructure. - silent: true - deps: - - task: deps-setup - aliases: - - isr - dir: infra - cmds: - - echo -e "\033[0;32mResetting state...\033[0m" - - terraform state rm $(terraform state list) || true && rm -f terraform.tfstate* - - infra-state-list: - desc: List all infrastructure state. - silent: true - deps: - - task: deps-setup - aliases: - - isl - dir: infra - cmds: - - echo -e "\033[0;32mListing state...\033[0m" - - terraform state list + - | + echo -e "\033[0;32mRemoving infrastructure artifacts...\033[0m" + rm -rf .terraform + rm -f terraform.tfstate + rm -f terraform.tfstate.backup diff --git a/Taskfile.yml b/Taskfile.yml index 1a81444..f2cd7db 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -1,4 +1,4 @@ -version: "3.40.1" +version: "3.41.0" dotenv: [".env", "flows/.env", "{{.HOME}}/.env"] @@ -19,6 +19,8 @@ tasks: deps-check: desc: Check toolchain dependencies. silent: true + aliases: + - dc cmds: - echo -e "\033[0;32mChecking dependencies...\033[0m" - | @@ -41,16 +43,6 @@ tasks: echo "Error: node is not installed. Please install node to continue." exit 1 fi - - | - if ! command -v mkcert > /dev/null; then - echo "Error: mkcert is not installed. Please install mkcert to continue." - exit 1 - fi - - | - if ! command -v pfazure > /dev/null; then - echo "Error: pfazure is not installed. Please install pfazure to continue." - exit 1 - fi - | if ! command -v git > /dev/null; then echo "Error: git is not installed. Please install git to continue." @@ -60,6 +52,8 @@ tasks: deps-install: desc: Install toolchain dependencies. silent: true + aliases: + - di cmds: - | if ! command -v az > /dev/null; then @@ -125,7 +119,7 @@ tasks: sudo curl -fsSL https://raw.githubusercontent.com/tj/n/master/bin/n -o /opt/n sudo bash /opt/n install 22 > /dev/null else - NODE_VERSION=$(node -v | sed 's/v//;s/\..*//') + NODE_VERSION=$(node -v | sed "s/v//;s/\..*//") if [ "$NODE_VERSION" -ne 22 ]; then echo -e "\033[0;33mUpdating node...\033[0m" cd "$(mktemp -d)" || exit 1 @@ -137,35 +131,17 @@ tasks: fi fi fi - - | - if ! command -v mkcert > /dev/null; then - echo -e "\033[0;32mInstalling mkcert...\033[0m" - cd "$(mktemp -d)" || exit 1 - - curl -sLf -o ./mkcert https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-linux-amd64 - chmod +x mkcert - sudo mv mkcert /usr/local/bin/mkcert - fi - - | - if ! command -v pf > /dev/null; then - echo -e "\033[0;32mInstalling promptflow...\033[0m" - cd "$(mktemp -d)" || exit 1 - - sudo mkdir -p /opt/python3/venvs/promptflow - sudo python3 -m venv /opt/python3/venvs/promptflow - sudo /opt/python3/venvs/promptflow/bin/pip3 install promptflow promptflow-tools promptflow-azure --quiet - sudo ln -s /opt/python3/venvs/promptflow/bin/pf /usr/local/bin/pf - sudo ln -s /opt/python3/venvs/promptflow/bin/pfazure /usr/local/bin/pfazure - fi deps-setup: desc: Setup toolchain dependencies. silent: true deps: - deps-check + aliases: + - ds cmds: - - echo -e "\033[0;32mSetting up environment...\033[0m" - | + echo -e "\033[0;32mSetting up environment...\033[0m" if [ ! -z "$SKIP_DEPS_SETUP" ]; then echo "SKIP_DEPS_SETUP is set. Skipping environment setup." exit 0 @@ -178,6 +154,7 @@ tasks: feature_state=$(az feature show --name AllowNSPInPublicPreview --namespace Microsoft.Network --query 'properties.state' -o tsv) if [ "$feature_state" == "NotRegistered" ]; then - echo "Registering feature 'AllowNSPInPublicPreview'..." + echo "Registering feature ' + AllowNSPInPublicPreview'..." az feature register --name AllowNSPInPublicPreview --namespace Microsoft.Network --only-show-errors fi diff --git a/app/api/.env.tpl b/app/api/.env.tpl index 64218b0..86e9392 100644 --- a/app/api/.env.tpl +++ b/app/api/.env.tpl @@ -2,18 +2,20 @@ AAD_CLIENT_ID="${AAD_CLIENT_ID}" AAD_TENANT_ID="${AAD_TENANT_ID}" AAD_USER_IMPERSONATION_SCOPE_ID="${AAD_USER_IMPERSONATION_SCOPE_ID}" +AZURE_CLIENT_ID="${AZURE_CLIENT_ID}" # Cosmos DB configuration COSMOS_URL="${COSMOS_URL}" DATABASE_NAME="${DATABASE_NAME}" -# Azure ML configuration +# Flow configuration SUBSCRIPTION_ID="${SUBSCRIPTION_ID}" RESOURCE_GROUP="${RESOURCE_GROUP}" AI_HUB_PROJECT_NAME="${AI_HUB_PROJECT_NAME}" AI_HUB_REGION="${AI_HUB_REGION}" -AML_ENDPOINT_NAME="${AML_ENDPOINT_NAME}" -AML_STREAMING_BATCH_SIZE=10 +FLOW_STREAMING_BATCH_SIZE=100 +FLOW_ENDPOINT_NAME="${FLOW_ENDPOINT_NAME}" +FLOW_APP_NAME="${FLOW_APP_NAME}" # App logging APPINSIGHTS_INSTRUMENTATION_KEY="${APPINSIGHTS_INSTRUMENTATION_KEY}" @@ -26,4 +28,4 @@ LOG_LEVEL="INFO" DEBUG=True # To Mount the UI at the root path -SERVE_STATIC=False # Set to True or False \ No newline at end of file +SERVE_STATIC=False # Set to True or False diff --git a/app/api/config/config.py b/app/api/config/config.py index 62e19fc..ae82c75 100644 --- a/app/api/config/config.py +++ b/app/api/config/config.py @@ -8,6 +8,7 @@ class Settings(BaseSettings): aad_client_id: str = "" aad_tenant_id: str = "" aad_user_impersonation_scope_id: str = "" + azure_client_id: str = "" serve_static: bool = True cosmos_url: str = "" cosmos_key: str = "" @@ -20,8 +21,9 @@ class Settings(BaseSettings): resource_group: str = "" ai_hub_project_name: str = "" ai_hub_region: str = "" - aml_endpoint_name: str = "" - aml_streaming_batch_size: int = 10 + flow_endpoint_name: str = "" + flow_app_name: str = "" + flow_streaming_batch_size: int = 100 appinsights_instrumentation_key: str = "" log_level: str = "INFO" model_config = SettingsConfigDict(env_file=".env") diff --git a/app/api/database/config.py b/app/api/database/config.py index 636c9ef..ca80f12 100644 --- a/app/api/database/config.py +++ b/app/api/database/config.py @@ -11,7 +11,8 @@ def __init__(self, container_name) -> None: self.container_name = container_name # Initialize the Cosmos client - self.client = CosmosClient(self.cosmos_url, DefaultAzureCredential()) + default_credential = DefaultAzureCredential() + self.client = CosmosClient(self.cosmos_url, default_credential) def get_client(self) -> CosmosClient: """Return the initialized Cosmos client.""" @@ -20,7 +21,7 @@ def get_client(self) -> CosmosClient: def get_database_name(self) -> str: """Return the database name.""" return self.database_name - + def get_container_name(self) -> str: """Return the container name.""" return self.container_name diff --git a/app/api/dependencies.py b/app/api/dependencies.py index 34ce69b..9838fe3 100644 --- a/app/api/dependencies.py +++ b/app/api/dependencies.py @@ -1,8 +1,8 @@ +import os from services.aml_client import AMLClient from database.issues_repository import IssuesRepository from services.issues_service import IssuesService -from azure.identity import DefaultAzureCredential -from azure.ai.ml import MLClient +from azure.identity import DefaultAzureCredential, ClientAssertionCredential, AzureCliCredential from config.config import settings @@ -10,11 +10,13 @@ def get_issues_service() -> IssuesService: return IssuesService(IssuesRepository(), get_aml_client()) def get_aml_client(): - credential = DefaultAzureCredential() - mlclient = MLClient( - credential, - settings.subscription_id, - settings.resource_group, - settings.ai_hub_project_name - ) - return AMLClient(mlclient) + if "WEBSITE_INSTANCE_ID" in os.environ: + credential = ClientAssertionCredential( + settings.aad_tenant_id, + os.environ.get('FLOW_CLIENT_ID'), + lambda: DefaultAzureCredential().get_token("api://AzureADTokenExchange/.default").token + ) + else: + credential = AzureCliCredential() + + return AMLClient(credential) diff --git a/app/api/services/aml_client.py b/app/api/services/aml_client.py index 15a6643..468d386 100644 --- a/app/api/services/aml_client.py +++ b/app/api/services/aml_client.py @@ -1,6 +1,4 @@ import json -import os -import ssl from typing import Any, AsyncGenerator import requests from http import HTTPStatus @@ -13,37 +11,36 @@ logging = get_logger(__name__) class AMLClient: - def __init__(self, ml_client_instance): - self.aml_client = ml_client_instance - + def __init__(self, credential): + self.credential = credential async def call_aml_endpoint(self, endpoint_name: str, pdf_name: str) -> AsyncGenerator[Any, Any]: """ - Calls the Azure ML endpoint with the name and data. + Calls the flow endpoint with the name and data. Args: - name (str): The name of the Azure ML endpoint. + name (str): The name of the flow endpoint. data (str): The body of the request. """ - if not os.environ.get('PYTHONHTTPSVERIFY', '') and getattr(ssl, '_create_unverified_context', None): - ssl._create_default_https_context = ssl._create_unverified_context # Get the scoring URI and API key - scoring_uri = f"https://{endpoint_name}.{settings.ai_hub_region}.inference.ml.azure.com/score" - keys = self.aml_client.online_endpoints.get_keys(name=endpoint_name) + scoring_uri = f"https://{endpoint_name}.azurewebsites.net/score" + + # Get access token from local endpoint + keys = self.credential.get_token(f"api://{settings.flow_app_name}/.default") - if not hasattr(keys, 'access_token'): - raise Exception(f"Unable to retrieve token for the Azure ML endpoint: {endpoint_name}. It may not have Entra Auth enabled.") + if not hasattr(keys, 'token'): + raise Exception(f"Unable to retrieve token for the flow endpoint: {endpoint_name}. It may not have Entra Auth enabled.") - headers = {'Content-Type': 'application/json', 'Authorization': f'Bearer {keys.access_token}', 'Accept': 'text/event-stream'} + headers = {'Content-Type': 'application/json', 'Authorization': f'Bearer {keys.token}', 'Accept': 'text/event-stream'} data = { "pdf_name": pdf_name, "stream": True, - "pagination": settings.aml_streaming_batch_size + "pagination": settings.flow_streaming_batch_size } try: - logging.info("Sending POST request to the Azure ML endpoint...") + logging.info("Sending POST request to the flow endpoint...") response = requests.post(scoring_uri, json=data, headers=headers, stream=True) response.raise_for_status() @@ -53,23 +50,23 @@ async def call_aml_endpoint(self, endpoint_name: str, pdf_name: str) -> AsyncGen client = SSEClient(response) for event in client.events(): - logging.debug(f"Received event: {event.data}") + logging.info(f"Received event: {event.data}") event_data = json.loads(event.data) if "flow_output_streaming" in event_data: yield event_data["flow_output_streaming"] elif "flow_output" in event_data: logging.debug("Ignoring non-streaming response event.") else: - raise RequestException("Unexpected event payload from Azure ML endpoint. Missing 'flow_output_streaming' property.") + raise RequestException("Unexpected event payload from flow endpoint. Missing 'flow_output_streaming' property.") else: - raise RequestException("Unexpected non-streaming response received from Azure ML endpoint.") + raise RequestException("Unexpected non-streaming response received from flow endpoint.") except HTTPError as http_err: logging.error(f"HTTP error occurred: {http_err}") raise HTTPException( status_code=response.status_code, - detail=f"Error from Azure ML: {http_err.response.text}" + detail=f"Error from flow: {http_err.response.text}" ) except RequestException as req_err: logging.error(f"Request error occurred: {req_err}") diff --git a/app/api/services/issues_service.py b/app/api/services/issues_service.py index ebdaea9..b8ca36f 100644 --- a/app/api/services/issues_service.py +++ b/app/api/services/issues_service.py @@ -52,7 +52,7 @@ async def initiate_review(self, pdf_name: str, user: User, time_stamp: datetime) logging.info(f"Initiating review for document {pdf_name}") # Initiate review to get a stream of issues - stream_data = self.aml_client.call_aml_endpoint(settings.aml_endpoint_name, pdf_name) + stream_data = self.aml_client.call_aml_endpoint(settings.flow_endpoint_name, pdf_name) async for chunk in stream_data: flow_output = FlowOutputChunk.model_validate_json(chunk) issues = [ @@ -85,7 +85,7 @@ async def accept_issue( issue_id: The ID of the issue. doc_id: The ID of the document. user: The user object. - modified_fields: optional - fields modified by user. + modified_fields: optional - fields modified by user. """ try: issue = await self.issues_repository.get_issue(doc_id, issue_id) diff --git a/docs/getting_started.md b/docs/getting_started.md index b29f6b3..c34a32f 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -23,7 +23,6 @@ task -a - Infrastructure: - `infra-deploy`, `infra-destroy`, `infra-plan`: Automate provisioning, updating, and destroying of Azure resources using Terraform. - - `infra-state-*`: Manage and inspect the state of deployed infrastructure. - Application: @@ -39,6 +38,9 @@ task -a - `api-run`: Launch the API locally for testing and development. - `deps-*`: Ensure dependencies are installed and toolchains are set up correctly. + - `app-logs`: Starts logging session from App Service for the API. + - `flow-logs`: Starts streaming logging session from App Service for the flow endpoint. + ## Deploying the solution The first step is to deploy the necessary Azure resources. This is managed through Terraform scripts and automated via Taskfile tasks. @@ -69,7 +71,9 @@ task infra-init cp infra/environments/local.tfvars.sample infra/environments/local.tfvars ``` -> NOTE: the `ml_engineers` variable determines the users/groups to provide access to edit PromptFlow flows, use Foundry AI compute, and access other resources required to author AI agents. We recommend creating an Entra ID Group for this, and adding its Object ID to the `ml_engineers` array. This way, you won't need to redeploy to add or remove users from the group to provide permissions - you can do so directly in Entra. +> NOTE: The `ml_engineers` variable determines the users/groups to provide access to edit PromptFlow flows, use Foundry AI compute, and access other resources required to author AI agents. We recommend creating an Entra ID Group for this, and adding its Object ID to the `ml_engineers` array. This way, you won't need to redeploy to add or remove users from the group to provide permissions - you can do so directly in Entra. + +> NOTE: The `subscription_id` must correspond to the active subscription selected in the Azure CLI. To verify the current subscription, run `az account show`. If you need to switch to a different subscription, use `az account set -s {subscription-id}`. 2. Edit `local.tfvars` with environment-specific configurations such as region, resource names, and other parameters. @@ -90,7 +94,7 @@ task app-build task app-deploy ``` -> Note: the deployment may take a few minutes to complete. You might see a 504 error (GatewayTimeout), but the deployment will still be running in the background. If so, follow the link in the error output to view the deployment logs. +> NOTE: The deployment may take a few minutes to complete. You might see a 504 error (GatewayTimeout), but the deployment will still be running in the background. If so, follow the link in the error output to view the deployment logs. ### Deploy PromptFlow endpoint @@ -129,30 +133,23 @@ You can click each to view more details and accept or dismiss them, and optional 1. Create a virtualenv for the API if it doesn't already exist and install the required packages: ```bash - cd app/api - python3 -m venv .venv - source .venv/bin/activate - pip install -r requirements.txt + task app-build ``` 2. Select the new venv Python interpreter in Visual Studio Code `Ctrl+Shift+P` > `Python: Select Interpreter` > `app/api/.venv/bin/python3` -3. Install the required packages for the web UI: +3. Ensure you are logged in to Azure in the VS Code terminal so the API can retrieve required credentials ```bash - cd app/ui - npm install + az login --scope "{app-id-uri}/user_impersonation" ``` -4. Ensure you are logged in to Azure in the VS Code terminal so the API can retrieve required credentials - - ```bash - az login - ``` +> NOTE: Note: You can locate the `app-id-uri` by navigating to the Entra ID App Registration and viewing the `adr-flow-adr-{name}` application. +> Alternatively, you can retrieve it by running the following command: `cd infra && terraform output flow_app_id_uri` -5. Select the compound debug profile `App (UI & API)` in Visual Studio Code and press Debug +4. Select the compound debug profile `App (UI & API)` in Visual Studio Code and press Debug #### Debugging Tips diff --git a/flows/ai_doc_review/deployment.yaml b/flows/ai_doc_review/deployment.yaml deleted file mode 100644 index 87f24aa..0000000 --- a/flows/ai_doc_review/deployment.yaml +++ /dev/null @@ -1,36 +0,0 @@ -$schema: https://azuremlschemas.azureedge.net/latest/managedOnlineDeployment.schema.json -name: ai-doc-review-deployment -endpoint_name: VAR_ENDPOINT_NAME -model: azureml:ai-doc-review-model:VAR_MODEL_VERSION -environment: - build: - path: . - dockerfile_path: Dockerfile - # inference config is used to build a serving container for online deployments - inference_config: - liveness_route: - path: /health - port: 8080 - readiness_route: - path: /health - port: 8080 - scoring_route: - path: /score - port: 8080 -instance_type: Standard_DS3_v2 -instance_count: 1 -request_settings: - # 5 min timeout - request_timeout_ms: 300000 - max_concurrent_requests_per_instance: 10 -environment_variables: - # for pulling connections from workspace - PRT_CONFIG_OVERRIDE: deployment.subscription_id=VAR_SUBSCRIPTION_ID,deployment.resource_group=VAR_RESOURCE_GROUP,deployment.workspace_name=VAR_AI_HUB_PROJECT_NAME,deployment.endpoint_name=VAR_ENDPOINT_NAME,deployment.deployment_name=ai-doc-review-deployment - AZURE_CLIENT_ID: VAR_IDENTITY_CLIENT_ID - # (Optional) When there are multiple fields in the response, using this env variable will filter the fields to expose in the response. - # For example, if there are 2 flow outputs: "answer", "context", and I only want to have "answer" in the endpoint response, I can set this env variable to '["answer"]'. - # If you don't set this environment, by default all flow outputs will be included in the endpoint response. - PROMPTFLOW_RESPONSE_INCLUDED_FIELDS: '["flow_output_streaming"]' - DOCUMENT_INTELLIGENCE_ENDPOINT: VAR_DOCUMENT_INTELLIGENCE_ENDPOINT - STORAGE_URL_PREFIX: VAR_STORAGE_URL_PREFIX - AZURE_OPENAI_ENDPOINT: VAR_AZURE_OPENAI_ENDPOINT diff --git a/flows/ai_doc_review/flow.dag.yaml b/flows/ai_doc_review/flow.dag.yaml index b0008b5..fbb041b 100644 --- a/flows/ai_doc_review/flow.dag.yaml +++ b/flows/ai_doc_review/flow.dag.yaml @@ -7,7 +7,7 @@ environment_variables: environment: python_requirements_txt: requirements.txt additional_includes: -- ../../common +- ./common inputs: pdf_name: type: string diff --git a/flows/ai_doc_review/requirements.txt b/flows/ai_doc_review/requirements.txt index 3132e0c..9f565bb 100644 --- a/flows/ai_doc_review/requirements.txt +++ b/flows/ai_doc_review/requirements.txt @@ -2,10 +2,11 @@ azure-ai-formrecognizer==3.3.3 asttokens==2.4.1 json5==0.9.5 openai==1.43.0 -promptflow_typed_llm==0.0.8 shapely==2.0.6 pymupdf==1.24.11 promptflow==1.17.1 promptflow[azure]==1.17.1 promptflow-tools==1.4.0 +promptflow_typed_llm==0.0.8 httpx==0.27.2 +uvicorn[standard]==0.32.0 diff --git a/flows/ai_doc_review_eval/requirements.txt b/flows/ai_doc_review_eval/requirements.txt index fe2dc8a..9d67dd5 100755 --- a/flows/ai_doc_review_eval/requirements.txt +++ b/flows/ai_doc_review_eval/requirements.txt @@ -1,3 +1,3 @@ -fuzzywuzzy -pandas -scikit-learn \ No newline at end of file +fuzzywuzzy==0.18.0 +pandas==2.2.3 +scikit-learn==1.6.1 diff --git a/infra/entra.tf b/infra/entra.tf index c99fd4e..28fc7cc 100644 --- a/infra/entra.tf +++ b/infra/entra.tf @@ -11,6 +11,8 @@ resource "azuread_service_principal" "storage" { resource "random_uuid" "api_app_api_scope_id" {} +resource "random_uuid" "flow_app_flow_scope_id" {} + resource "azuread_application" "api_app" { display_name = local.resource_name.aad_api_app owners = [data.azurerm_client_config.current.object_id] @@ -52,12 +54,11 @@ resource "azuread_application" "api_app" { redirect_uris = [ "http://localhost:8000/", "http://localhost:8000/oauth2-redirect", - "https://${local.resource_name.web_app}.azurewebsites.net/api", - "https://${local.resource_name.web_app}.azurewebsites.net/api/oauth2-redirect", + "https://${local.resource_name.web_api_app}.azurewebsites.net/api", + "https://${local.resource_name.web_api_app}.azurewebsites.net/api/oauth2-redirect", ] } } - resource "azuread_application" "client_app" { display_name = local.resource_name.aad_client_app owners = [data.azurerm_client_config.current.object_id] @@ -74,7 +75,7 @@ resource "azuread_application" "client_app" { single_page_application { redirect_uris = [ "http://localhost:5173/", - "https://${local.resource_name.web_app}.azurewebsites.net/" + "https://${local.resource_name.web_api_app}.azurewebsites.net/" ] } @@ -86,6 +87,53 @@ resource "azuread_application" "client_app" { ] } } +resource "azuread_application" "flow_app" { + display_name = local.resource_name.aad_flow_app + owners = [data.azurerm_client_config.current.object_id] + identifier_uris = ["api://adr-flow-${var.name}-${var.environment}"] + sign_in_audience = "AzureADMyOrg" + + required_resource_access { + resource_app_id = data.azuread_application_published_app_ids.well_known.result.MicrosoftGraph + + resource_access { + id = azuread_service_principal.msgraph.oauth2_permission_scope_ids["User.Read"] + type = "Scope" + } + } + + api { + oauth2_permission_scope { + admin_consent_description = "Allow the application to access data on behalf of the signed-in user." + admin_consent_display_name = "Access data" + enabled = true + id = random_uuid.flow_app_flow_scope_id.result + type = "User" + user_consent_description = "Allow the application to access data on your behalf." + user_consent_display_name = "Access data" + value = "user_impersonation" + } + requested_access_token_version = 2 + } + + # https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/application_api_access + # prevent constant changes + lifecycle { + ignore_changes = [ + required_resource_access + ] + } +} + +resource "azuread_application_pre_authorized" "flow_app" { + application_id = "/applications/${azuread_application.flow_app.object_id}" + authorized_client_id = "04b07795-8ddb-461a-bbee-02f9e1bf7b46" // Azure CLI + permission_ids = [azuread_application.flow_app.oauth2_permission_scope_ids["user_impersonation"]] +} + +resource "azuread_application_password" "flow_app" { + application_id = azuread_application.flow_app.id +} # Connection from Client -> API App resource "azuread_application_api_access" "api_connection" { @@ -123,7 +171,20 @@ resource "azuread_application_api_access" "storage_user_impersonation" { resource "azuread_service_principal" "api_app" { client_id = azuread_application.api_app.client_id } - resource "azuread_service_principal" "client_app" { client_id = azuread_application.client_app.client_id } +resource "azuread_service_principal" "flow_app" { + client_id = azuread_application.flow_app.client_id +} + +resource "azuread_application_federated_identity_credential" "flow_app" { + application_id = "/applications/${azuread_application.flow_app.object_id}" + + display_name = local.resource_name.umi_api + description = "Trust the workloads UAMI to impersonate the App" + + audiences = ["api://AzureADTokenExchange"] + issuer = "https://login.microsoftonline.com/${data.azurerm_client_config.current.tenant_id}/v2.0" + subject = azurerm_user_assigned_identity.api.principal_id +} diff --git a/infra/env_files.tf b/infra/env_files.tf index 163afdf..9599ef5 100644 --- a/infra/env_files.tf +++ b/infra/env_files.tf @@ -51,13 +51,16 @@ data "template_file" "api_env" { AAD_CLIENT_ID = azuread_application.api_app.client_id AAD_TENANT_ID = data.azurerm_client_config.current.tenant_id AAD_USER_IMPERSONATION_SCOPE_ID = "${tolist(azuread_application.api_app.identifier_uris)[0]}/user_impersonation" + AZURE_CLIENT_ID = azurerm_user_assigned_identity.api.client_id COSMOS_URL = azurerm_cosmosdb_account.main.endpoint DATABASE_NAME = azurerm_cosmosdb_sql_database.state.name SUBSCRIPTION_ID = data.azurerm_subscription.primary.subscription_id RESOURCE_GROUP = azurerm_resource_group.main.name AI_HUB_PROJECT_NAME = azapi_resource.ai_project.name AI_HUB_REGION = azapi_resource.ai_hub.location - AML_ENDPOINT_NAME = azapi_resource.ai_online_endpoint.name + FLOW_CLIENT_ID = azuread_application.flow_app.client_id + FLOW_ENDPOINT_NAME = azurerm_linux_web_app.flow.name + FLOW_APP_NAME = azuread_application.flow_app.display_name APPINSIGHTS_INSTRUMENTATION_KEY = azurerm_application_insights.main.instrumentation_key } depends_on = [ @@ -67,7 +70,6 @@ data "template_file" "api_env" { azurerm_resource_group.main, azapi_resource.ai_project, azapi_resource.ai_hub, - azapi_resource.ai_online_endpoint, azurerm_application_insights.main ] } diff --git a/infra/identities.tf b/infra/identities.tf index 879a1a1..afa8492 100644 --- a/infra/identities.tf +++ b/infra/identities.tf @@ -2,6 +2,7 @@ User Provides access for the current identity (user etc) to Storage Account */ + resource "azurerm_role_assignment" "deployer_file_share_contributor" { scope = azurerm_storage_account.main.id role_definition_name = "Storage File Data Privileged Contributor" @@ -24,6 +25,7 @@ resource "azurerm_cosmosdb_sql_role_assignment" "deployer_to_cosmos" { Group (ML Engineers) Provides access for the Security Group (ML Engineers) to Storage Account, AI Foundry and Cosmos DB */ + resource "azurerm_role_assignment" "ai_studio_developer" { for_each = var.ml_engineers scope = azapi_resource.ai_hub.id @@ -69,21 +71,22 @@ resource "azurerm_cosmosdb_sql_role_assignment" "cosmos_user" { Web App Provides access for App Services to access AI Foundry, Storage Account and CosmosDB */ + resource "azurerm_role_assignment" "app_to_prompt_flow" { scope = azapi_resource.ai_project.id role_definition_name = "AzureML Data Scientist" - principal_id = azurerm_linux_web_app.main.identity[0].principal_id + principal_id = azurerm_user_assigned_identity.api.principal_id } resource "azurerm_role_assignment" "app_to_storage" { scope = azurerm_storage_account.main.id role_definition_name = "Storage Blob Data Contributor" - principal_id = azurerm_linux_web_app.main.identity[0].principal_id + principal_id = azurerm_user_assigned_identity.api.principal_id } resource "azurerm_cosmosdb_sql_role_assignment" "webapp_to_cosmos" { resource_group_name = azurerm_resource_group.main.name account_name = azurerm_cosmosdb_account.main.name role_definition_id = data.azurerm_cosmosdb_sql_role_definition.data_contributor.id - principal_id = azurerm_linux_web_app.main.identity[0].principal_id + principal_id = azurerm_user_assigned_identity.api.principal_id scope = "${azurerm_cosmosdb_account.main.id}/dbs/${azurerm_cosmosdb_sql_database.state.name}" } @@ -91,6 +94,7 @@ resource "azurerm_cosmosdb_sql_role_assignment" "webapp_to_cosmos" { AI Foundry Provides access for AI Foundry to access Storage Account */ + resource "azurerm_role_assignment" "doc_intel_blob_data_contributor" { scope = azurerm_storage_account.main.id role_definition_name = "Storage Blob Data Contributor" @@ -102,17 +106,17 @@ resource "azurerm_role_assignment" "ai_ws_file_share_contributor" { principal_id = azapi_resource.ai_project.identity[0].principal_id } - /* - Managed Identity (User) - Provides access for Managed Identity (AI Compute) to access the required resources + Managed Identity + Provides access for AI Compute to access the required resources https://learn.microsoft.com/en-us/azure/machine-learning/prompt-flow/how-to-manage-compute-session */ + resource "azurerm_user_assigned_identity" "ai_compute" { resource_group_name = azurerm_resource_group.main.name location = azurerm_resource_group.main.location - name = "uid-${var.name}-${var.environment}" + name = local.resource_name.umi_ai_compute } resource "azurerm_role_assignment" "uid_to_ai_hub" { @@ -194,3 +198,14 @@ resource "azurerm_cosmosdb_sql_role_assignment" "uid_to_cosmos" { principal_id = azurerm_user_assigned_identity.ai_compute.principal_id scope = "${azurerm_cosmosdb_account.main.id}/dbs/${azurerm_cosmosdb_sql_database.state.name}" } + +/* + Managed Identity + Provides access for the App Services to access the resources +*/ + +resource "azurerm_user_assigned_identity" "api" { + resource_group_name = azurerm_resource_group.main.name + location = azurerm_resource_group.main.location + name = local.resource_name.umi_api +} diff --git a/infra/locals.tf b/infra/locals.tf index 817dc53..6897d2a 100644 --- a/infra/locals.tf +++ b/infra/locals.tf @@ -6,15 +6,18 @@ locals { ai_services = "ais-${var.name}-${var.environment}" ai_hub = "aih-${var.name}-${var.environment}" ai_project = "aip-${var.name}-${var.environment}" - ai_online_endpoint = "ept-${var.name}-${var.environment}" storage_account = "str${var.name}${var.environment}" container_registry = "acr${var.name}${var.environment}" cosmos_db = "cdb-${var.name}-${var.environment}" app_service_plan = "asp-${var.name}-${var.environment}" - web_app = "app-${var.name}-${var.environment}" + web_api_app = "as-app-${var.name}-${var.environment}" + web_flow_app = "as-flw-${var.name}-${var.environment}" application_insights = "ai-${var.name}-${var.environment}" aad_api_app = "adr-api-${var.name}-${var.environment}" aad_client_app = "adr-client-${var.name}-${var.environment}" + aad_flow_app = "adr-flow-${var.name}-${var.environment}" + umi_ai_compute = "uid-aic-${var.name}-${var.environment}" + umi_api = "uid-api-${var.name}-${var.environment}" } common_tags = {} diff --git a/infra/outputs.tf b/infra/outputs.tf index 4cd167f..4de4647 100644 --- a/infra/outputs.tf +++ b/infra/outputs.tf @@ -1,51 +1,9 @@ -# Client App Vars -output "vite_tenant_id" { - value = data.azurerm_client_config.current.tenant_id -} - -output "vite_client_id" { - value = azuread_application.client_app.client_id -} - -output "vite_api_scope" { - value = "${tolist(azuread_application.api_app.identifier_uris)[0]}/user_impersonation" -} - -output "vite_storage_account" { - value = azurerm_storage_account.main.primary_blob_endpoint -} - -output "vite_storage_document_container" { - value = azurerm_storage_container.documents.name -} - -# API App Vars output "webapp_name" { - value = azurerm_linux_web_app.main.name + value = azurerm_linux_web_app.app.name } output "webapp_url" { - value = azurerm_linux_web_app.main.default_hostname -} - -output "aad_client_id" { - value = azuread_application.api_app.client_id -} - -output "aad_tenant_id" { - value = data.azurerm_client_config.current.tenant_id -} - -output "cosmos_url" { - value = azurerm_cosmosdb_account.main.endpoint -} - -output "database_name" { - value = azurerm_cosmosdb_sql_database.state.name -} - -output "subscription_id" { - value = data.azurerm_subscription.primary.subscription_id + value = azurerm_linux_web_app.app.default_hostname } output "resource_group" { @@ -56,35 +14,10 @@ output "ai_hub_project_name" { value = azapi_resource.ai_project.name } -output "ai_hub_region" { - value = azapi_resource.ai_hub.location -} - -output "aml_endpoint_name" { - value = azapi_resource.ai_online_endpoint.name -} - -output "document_intelligence_endpoint" { - value = "https://ais${var.name}${var.environment}.cognitiveservices.azure.com" +output "flowapp_name" { + value = azurerm_linux_web_app.flow.name } -output "azure_openai_endpoint" { - value = "https://ais${var.name}${var.environment}.openai.azure.com" -} - -output "storage_url_prefix" { - value = "${azurerm_storage_account.main.primary_blob_endpoint}/${azurerm_storage_container.documents.name}" -} - -output "appinsights_instrumentation_key" { - value = azurerm_application_insights.main.instrumentation_key - sensitive = true -} - -# Flow vars -output "identity_client_id" { - value = azurerm_user_assigned_identity.ai_compute.client_id -} -output "identity_resource_id" { - value = azurerm_user_assigned_identity.ai_compute.id +output "flow_app_id_uri" { + value = "api://adr-flow-${var.name}-${var.environment}" } diff --git a/infra/resources.tf b/infra/resources.tf index 3e6661f..9bc56cd 100644 --- a/infra/resources.tf +++ b/infra/resources.tf @@ -1,3 +1,8 @@ +/* + Resource + - Resource Group +*/ + resource "azurerm_resource_group" "main" { name = local.resource_name.resource_group location = var.location @@ -64,7 +69,6 @@ resource "azapi_resource" "nsp_association" { - AI Foundry - AI Hub - AI Project - - AI Online Endpoint - AI Connections */ @@ -85,7 +89,8 @@ resource "azapi_resource" "ai_services" { apiProperties = { statisticsEnabled = false, } - restore = contains([for service in data.azapi_resource_list.ai_services.output.value : service.name], local.resource_name.ai_services) + restore = contains([for service in data.azapi_resource_list.ai_services.output.value : service.name], local.resource_name.ai_services) + publicNetworkAccess = "Enabled" } kind = "AIServices" sku = { @@ -173,27 +178,6 @@ resource "azapi_resource" "ai_project" { tags = merge(local.common_tags, {}) } -resource "azapi_resource" "ai_online_endpoint" { - type = "Microsoft.MachineLearningServices/workspaces/onlineEndpoints@2024-10-01" - name = local.resource_name.ai_online_endpoint - parent_id = azapi_resource.ai_project.id - - body = { - properties = { - authMode = "AADToken" - } - kind = "Managed" - location = azurerm_resource_group.main.location - } - - identity { - type = "UserAssigned" - identity_ids = [azurerm_user_assigned_identity.ai_compute.id] - } - - response_export_values = ["*"] -} - resource "azapi_resource" "ai_services_connection" { type = "Microsoft.MachineLearningServices/workspaces/connections@2024-10-01" name = "aisconns" @@ -247,13 +231,13 @@ resource "azurerm_service_plan" "main" { resource_group_name = azurerm_resource_group.main.name os_type = "Linux" - sku_name = "B1" + sku_name = "P0v3" tags = merge(local.common_tags, {}) } -resource "azurerm_linux_web_app" "main" { - name = local.resource_name.web_app +resource "azurerm_linux_web_app" "app" { + name = local.resource_name.web_api_app location = azurerm_resource_group.main.location resource_group_name = azurerm_resource_group.main.name @@ -264,7 +248,8 @@ resource "azurerm_linux_web_app" "main" { webdeploy_publish_basic_authentication_enabled = false identity { - type = "SystemAssigned" + type = "UserAssigned" + identity_ids = [azurerm_user_assigned_identity.api.id] } site_config { @@ -285,19 +270,109 @@ resource "azurerm_linux_web_app" "main" { "DATABASE_NAME" = azurerm_cosmosdb_sql_database.state.name "AAD_CLIENT_ID" = azuread_application.api_app.client_id "AAD_TENANT_ID" = data.azurerm_client_config.current.tenant_id + "AZURE_CLIENT_ID" = azurerm_user_assigned_identity.api.client_id "SUBSCRIPTION_ID" = data.azurerm_subscription.primary.subscription_id "RESOURCE_GROUP" = azurerm_resource_group.main.name "AI_HUB_PROJECT_NAME" = azapi_resource.ai_project.name "AI_HUB_REGION" = azapi_resource.ai_hub.location - "AML_ENDPOINT_NAME" = azapi_resource.ai_online_endpoint.name - "AML_STREAMING_BATCH_SIZE" = 10 + "FLOW_CLIENT_ID" = azuread_application.flow_app.client_id + "FLOW_ENDPOINT_NAME" = azurerm_linux_web_app.flow.name + "FLOW_APP_NAME" = azuread_application.flow_app.display_name + "FLOW_STREAMING_BATCH_SIZE" = 100 "APPINSIGHTS_INSTRUMENTATION_KEY" = azurerm_application_insights.main.instrumentation_key "LOG_LEVEL" = "INFO" } + logs { + application_logs { + file_system_level = "Verbose" + } + http_logs { + file_system { + retention_in_days = "1" + retention_in_mb = "35" + } + } + detailed_error_messages = true + failed_request_tracing = true + } + tags = merge(local.common_tags, {}) } +resource "azurerm_linux_web_app" "flow" { + name = local.resource_name.web_flow_app + location = azurerm_resource_group.main.location + resource_group_name = azurerm_resource_group.main.name + + service_plan_id = azurerm_service_plan.main.id + public_network_access_enabled = true + https_only = true + ftp_publish_basic_authentication_enabled = false + webdeploy_publish_basic_authentication_enabled = false + + identity { + type = "UserAssigned" + identity_ids = [azurerm_user_assigned_identity.ai_compute.id] + } + + site_config { + websockets_enabled = true + app_command_line = "pf flow serve --source . --port 8000 --host 0.0.0.0 --engine fastapi --skip-open-browser" + + application_stack { + python_version = "3.12" + } + } + + app_settings = { + "SCM_DO_BUILD_DURING_DEPLOYMENT" = "true" + "DEBUG" = "True" + "DOCUMENT_INTELLIGENCE_ENDPOINT" = "https://ais${var.name}${var.environment}.cognitiveservices.azure.com" + "STORAGE_URL_PREFIX" = "${azurerm_storage_account.main.primary_blob_endpoint}/${azurerm_storage_container.documents.name}" + "AZURE_OPENAI_ENDPOINT" = "https://ais${var.name}${var.environment}.openai.azure.com" + "AZURE_CLIENT_ID" = azurerm_user_assigned_identity.ai_compute.client_id + "MICROSOFT_PROVIDER_AUTHENTICATION_SECRET" = azuread_application_password.flow_app.value + "USER_AGENT" = "promptflow-appservice" + "APPLICATIONINSIGHTS_CONNECTION_STRING" = azurerm_application_insights.main.connection_string + } + + auth_settings_v2 { + auth_enabled = true + require_authentication = true + unauthenticated_action = "Return401" + require_https = true + active_directory_v2 { + tenant_auth_endpoint = "https://login.microsoftonline.com/${data.azurerm_client_config.current.tenant_id}/v2.0" + client_id = azuread_application.flow_app.client_id + client_secret_setting_name = "MICROSOFT_PROVIDER_AUTHENTICATION_SECRET" + allowed_audiences = ["${azuread_application.flow_app.client_id}"] + allowed_applications = [ + "${azuread_application.flow_app.client_id}", + "04b07795-8ddb-461a-bbee-02f9e1bf7b46" # Azure CLI + ] + www_authentication_disabled = true + } + login { + token_store_enabled = true + } + } + + logs { + application_logs { + file_system_level = "Verbose" + } + http_logs { + file_system { + retention_in_days = "1" + retention_in_mb = "35" + } + } + detailed_error_messages = true + failed_request_tracing = true + } +} + resource "azurerm_application_insights" "main" { name = local.resource_name.application_insights location = azurerm_resource_group.main.location @@ -320,7 +395,6 @@ resource "azurerm_key_vault" "main" { tags = merge(local.common_tags, {}) } - resource "azurerm_storage_account" "main" { name = local.resource_name.storage_account location = azurerm_resource_group.main.location @@ -344,7 +418,7 @@ resource "azurerm_storage_account" "main" { "https://ai.azure.com", "https://*.ai.azure.com", "http://localhost:5173", - "https://${local.resource_name.web_app}.azurewebsites.net" + "https://${local.resource_name.web_api_app}.azurewebsites.net" ] exposed_headers = ["*"] max_age_in_seconds = 3600