├── test.sh ├── .funcignore ├── pipeline ├── README.md ├── configuration │ ├── __init__.py │ └── configuration.py ├── config-test.py ├── main.py ├── activities │ ├── sharepointLookup.py │ ├── getBlobContent.py │ ├── callAoai.py │ ├── writeToBlob.py │ ├── runDocIntel.py │ ├── callAoaiMultiModal.py │ └── speechToText.py ├── .funcignore ├── pipelineUtils │ ├── __init__.py │ ├── db.py │ ├── blob_functions.py │ ├── prompts.py │ └── azure_openai.py ├── host.json ├── requirements.txt ├── .gitignore └── function_app.py ├── .gitattributes ├── commandUtils ├── admin │ ├── az_login.sh │ ├── moveResource.sh │ ├── getPrincipalId.sh │ ├── getUPN.sh │ ├── listSoftDeleted.sh │ ├── role_definition_list.sh │ ├── listRoleAssignmentsbyObjectId.sh │ └── role-assignment.sh ├── staticWebApp │ ├── listStaticEnvVariables.sh │ ├── listSecrets.sh │ ├── deploy.sh │ └── setStaticEnvVars.sh ├── host.json ├── aoai │ └── getQuota.sh ├── functions │ ├── zipDeploy.sh │ ├── publishFunction.sh │ ├── getDeploymentLogs.sh │ ├── functionApp.sh │ ├── call_process_uploads.sh │ └── updateEnv.sh └── createLocalSettings.sh ├── scripts ├── AzuriteConfig ├── startLocal.sh ├── getBlobConnectionStrings.sh ├── postDeploy.sh ├── postDeploy.ps1 ├── startLocal.ps1 ├── postprovision.ps1 ├── postprovision.sh ├── getRemoteSettings.sh └── getRemoteSettings.ps1 ├── data ├── config.json ├── hello.wav ├── role_library-3.pdf ├── sampleRequest.json ├── promptscontainer.json └── prompts.yaml ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── docs ├── customizations.md ├── promptConfiguration.md └── troubleShootingGuide.md ├── infra ├── modules │ ├── modules │ │ └── fetch-container-image.bicep │ ├── rbac │ │ ├── keyvault-access.bicep │ │ ├── role.bicep │ │ ├── keyvault-access-policy.bicep │ │ ├── aiservices-user.bicep │ │ ├── cogservices-openai-user.bicep │ │ ├── blob-queue-contributor.bicep │ │ ├── blob-contributor.bicep │ │ ├── cosmos-contributor.bicep │ │ ├── appconfig-access.bicep │ │ ├── search-access.bicep │ │ └── blob-dataowner.bicep │ ├── util │ │ ├── virtualNetworkData.bicep │ │ ├── delay.bicep │ │ ├── metricAlerts.bicep │ │ └── dnsZoneData.bicep │ ├── storage │ │ ├── storage-queue.bicep │ │ ├── storage-blob-container.bicep │ │ ├── storage-private-endpoints.bicep │ │ └── storage-account.bicep │ ├── compute │ │ ├── hosting-plan.bicep │ │ └── functionApp.bicep │ ├── security │ │ ├── resource-group-role-assignment.bicep │ │ ├── key-vault-secret-environment-variables.bicep │ │ ├── key-vault-secret.bicep │ │ ├── private-link-scope.bicep │ │ ├── managed-identity.bicep │ │ ├── resource-role-assignment.json │ │ └── key-vault.bicep │ ├── network │ │ ├── private-dns-zones.bicep │ │ ├── private-endpoint.bicep │ │ └── vnet-vpn-gateway.bicep │ ├── ai_ml │ │ ├── modelDeployment.bicep │ │ ├── aoai-account.bicep │ │ ├── aimultiservices.bicep │ │ └── aifoundry.bicep │ ├── app_config │ │ └── appconfig.bicep │ ├── management_governance │ │ ├── application-insights.bicep │ │ └── log-analytics-workspace.bicep │ ├── containers │ │ ├── container-registry.bicep │ │ ├── container-apps-environment.bicep │ │ └── container-app.bicep │ ├── vm │ │ └── dsvm.bicep │ └── db │ │ └── cosmos.bicep ├── deployment-outputs.json ├── install.ps1 ├── main.parameters.json ├── README.md └── abbreviations.json ├── CODE_OF_CONDUCT.md ├── azure.yaml ├── LICENSE ├── contributing.md ├── SUPPORT.md ├── localScripts └── grantRole.sh ├── SECURITY.md ├── .gitignore ├── troubleshoot-functions.sh ├── troubleshoot-functions.ps1 ├── test_client.ipynb └── deploy-testvm.sh /test.sh: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.funcignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pipeline/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | .github/* export-ignore 2 | -------------------------------------------------------------------------------- /commandUtils/admin/az_login.sh: -------------------------------------------------------------------------------- 1 | az login --tenant -------------------------------------------------------------------------------- /scripts/AzuriteConfig: -------------------------------------------------------------------------------- 1 | {"instaceID":"ad040f0b-8a26-4a79-8311-2c6afd16740a"} -------------------------------------------------------------------------------- /commandUtils/admin/moveResource.sh: -------------------------------------------------------------------------------- 1 | az resource move --destination-group --ids -------------------------------------------------------------------------------- /pipeline/configuration/__init__.py: -------------------------------------------------------------------------------- 1 | from .configuration import Configuration -------------------------------------------------------------------------------- /commandUtils/admin/getPrincipalId.sh: -------------------------------------------------------------------------------- 1 | az ad signed-in-user show --query id -o tsv -------------------------------------------------------------------------------- /data/config.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "id": "live_prompt_config", 3 | "prompt_id": "hash1" 4 | }] -------------------------------------------------------------------------------- /data/hello.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/ai-document-processor/HEAD/data/hello.wav -------------------------------------------------------------------------------- /pipeline/config-test.py: -------------------------------------------------------------------------------- 1 | from configuration import Configuration 2 | 3 | config = Configuration() -------------------------------------------------------------------------------- /commandUtils/admin/getUPN.sh: -------------------------------------------------------------------------------- 1 | az ad user show --id ${principalId} --query "{UPN: userPrincipalName}" -o json -------------------------------------------------------------------------------- /commandUtils/staticWebApp/listStaticEnvVariables.sh: -------------------------------------------------------------------------------- 1 | az staticwebapp appsettings list -n $AZURE_STATIC_WEB_APP_NAME -------------------------------------------------------------------------------- /data/role_library-3.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/ai-document-processor/HEAD/data/role_library-3.pdf -------------------------------------------------------------------------------- /commandUtils/admin/listSoftDeleted.sh: -------------------------------------------------------------------------------- 1 | az cognitiveservices account list-deleted --subscription ${subscription_id} 2 | -------------------------------------------------------------------------------- /pipeline/main.py: -------------------------------------------------------------------------------- 1 | def main(): 2 | print("Hello from pipeline!") 3 | 4 | 5 | if __name__ == "__main__": 6 | main() 7 | -------------------------------------------------------------------------------- /commandUtils/admin/role_definition_list.sh: -------------------------------------------------------------------------------- 1 | az cosmosdb sql role definition list --resource-group "${resource_group}" --account-name $cosmos -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions", 4 | "ms-python.python" 5 | ] 6 | } -------------------------------------------------------------------------------- /docs/customizations.md: -------------------------------------------------------------------------------- 1 | ## AZURE AI VISION 2 | azd env set AI_VISION_ENABLED 3 | 4 | ## AZURE AI MULTI MODAL 5 | azd env set AOAI_MULTI_MODAL -------------------------------------------------------------------------------- /data/sampleRequest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "uri": "https://.blob.core.windows.net/bronze/" 4 | } -------------------------------------------------------------------------------- /pipeline/activities/sharepointLookup.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | # @bp.function_name(name) 4 | # @bp.activity_trigger(input_name="vendor_id") 5 | # def sharepointLookup(: dict): -------------------------------------------------------------------------------- /commandUtils/staticWebApp/listSecrets.sh: -------------------------------------------------------------------------------- 1 | 2 | az staticwebapp secrets list --name ${STATIC_WEB_APP_NAME} --resource-group ${AZURE_RESOURCE_GROUP} --query "properties.apiKey" -------------------------------------------------------------------------------- /pipeline/.funcignore: -------------------------------------------------------------------------------- 1 | .git* 2 | .vscode 3 | __azurite_db*__.json 4 | __blobstorage__ 5 | __queuestorage__ 6 | local.settings.json 7 | test 8 | *local.settings.json 9 | .venv -------------------------------------------------------------------------------- /commandUtils/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "extensionBundle": { 4 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 5 | "version": "[4.*, 5.0.0)" 6 | } 7 | } -------------------------------------------------------------------------------- /data/promptscontainer.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "id": "hash1", 3 | "name": "first_prompt", 4 | "system_prompt": "This is a new prompt", 5 | "user_prompt": "This is a new user prompt" 6 | }] -------------------------------------------------------------------------------- /commandUtils/aoai/getQuota.sh: -------------------------------------------------------------------------------- 1 | az cognitiveservices account list --subscription "" \ 2 | --query "[].{Name:name, Location:location, SKU:skuName, ResourceGroup:resourceGroup, Type:kind}" 3 | -------------------------------------------------------------------------------- /commandUtils/functions/zipDeploy.sh: -------------------------------------------------------------------------------- 1 | az functionapp deployment source config-zip \ 2 | --resource-group rg-demo \ 3 | --name processing-dbfbvcgqgrid6 \ 4 | --src functionapp.zip 5 | -------------------------------------------------------------------------------- /pipeline/pipelineUtils/__init__.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | def get_month_date(): 3 | current_date = datetime.date.today() 4 | month = current_date.month 5 | day = current_date.day 6 | return month, day -------------------------------------------------------------------------------- /commandUtils/functions/publishFunction.sh: -------------------------------------------------------------------------------- 1 | RESOURCE_GROUP_NAME="rg-demo" 2 | FUNCTION_APP_NAME="processing-dbfbvcgqgrid6" 3 | 4 | func azure functionapp publish $FUNCTION_APP_NAME --resource-group $RESOURCE_GROUP -------------------------------------------------------------------------------- /commandUtils/admin/listRoleAssignmentsbyObjectId.sh: -------------------------------------------------------------------------------- 1 | az role assignment list \ 2 | --assignee ${principal_id} \ 3 | --scope ${resource_id} \ 4 | --query "[].{Role:roleDefinitionName, Scope:scope}" \ 5 | -o table 6 | -------------------------------------------------------------------------------- /docs/promptConfiguration.md: -------------------------------------------------------------------------------- 1 | ### Prompt Configuration 2 | 3 | The system prompt and user prompt can be updated in data/prompts.yaml. You should upload this file into the "prompts" container in the azure storage account associated with this deployment -------------------------------------------------------------------------------- /commandUtils/admin/role-assignment.sh: -------------------------------------------------------------------------------- 1 | az cosmosdb sql role assignment create \ 2 | --account-name ${account_name} \ 3 | --resource-group ${resource_group_name} \ 4 | --role-definition-id ${role_definition_id} \ 5 | --principal-id ${principal_id} \ 6 | --scope ${scope} -------------------------------------------------------------------------------- /infra/modules/modules/fetch-container-image.bicep: -------------------------------------------------------------------------------- 1 | param exists bool 2 | param name string 3 | 4 | resource existingApp 'Microsoft.App/containerApps@2023-05-02-preview' existing = if (exists) { 5 | name: name 6 | } 7 | 8 | output containers array = exists ? existingApp.properties.template.containers : [] 9 | -------------------------------------------------------------------------------- /commandUtils/functions/getDeploymentLogs.sh: -------------------------------------------------------------------------------- 1 | eval "$(azd env get-values)" 2 | echo $FUNCTION_APP_NAME 3 | 4 | az functionapp log deployment show --name $FUNCTION_APP_NAME --resource-group $RESOURCE_GROUP --output table 5 | # # az functionapp log deployment list --name $FUNCTION_APP_NAME --resource-group $RESOURCE_GROUP -------------------------------------------------------------------------------- /commandUtils/functions/functionApp.sh: -------------------------------------------------------------------------------- 1 | az functionapp function list --name $FUNCTION_APP_NAME --resource-group $RESOURCE_GROUP --query "[].{Name:name}" --output table 2 | # az functionapp function list --name $FUNCTION_APP_NAME --resource-group $RESOURCE_GROUP --function-name $FUNCTION_NAME --query properties.config.disabled -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach to Python Functions", 6 | "type": "debugpy", 7 | "request": "attach", 8 | "connect": { 9 | "host": "localhost", 10 | "port": 9091 11 | }, 12 | "preLaunchTask": "func: host start" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /commandUtils/functions/call_process_uploads.sh: -------------------------------------------------------------------------------- 1 | curl -X POST http://localhost:7071/api/client \ 2 | -H "Content-Type: application/json" \ 3 | -d '{ 4 | "blobs": [ 5 | { 6 | "name": "bronze/sample1.pdf", 7 | "url": "https://.blob.core.windows.net/bronze/role_library-3.pdf?", 8 | "container": "bronze" 9 | } 10 | ] 11 | }' -------------------------------------------------------------------------------- /commandUtils/staticWebApp/deploy.sh: -------------------------------------------------------------------------------- 1 | eval $(azd env get-values) 2 | 3 | az login --use-device-code 4 | 5 | token=$(az staticwebapp secrets list \ 6 | --name "${STATIC_WEB_APP_NAME}" \ 7 | --resource-group "${AZURE_RESOURCE_GROUP}" \ 8 | --query "properties.apiKey" \ 9 | -o tsv) 10 | echo "token: ${token}" 11 | cd frontend 12 | swa init 13 | swa build 14 | swa deploy --env Production -d "${token}" -------------------------------------------------------------------------------- /pipeline/activities/getBlobContent.py: -------------------------------------------------------------------------------- 1 | import azure.durable_functions as df 2 | from pipelineUtils.blob_functions import get_blob_content 3 | import logging 4 | 5 | name = "getBlobContent" 6 | bp = df.Blueprint() 7 | 8 | @bp.function_name(name) 9 | @bp.activity_trigger(input_name="blobObj") 10 | def run(blobObj: dict): 11 | logging.info(f"BlobObj: {blobObj}") 12 | return f"Get Blob Content: {blobObj}" -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "azureFunctions.projectSubpath": "pipeline", 3 | "azureFunctions.deploySubpath": "pipeline", 4 | "azureFunctions.scmDoBuildDuringDeployment": true, 5 | "azureFunctions.pythonVenv": ".venv", 6 | "azureFunctions.projectLanguage": "Python", 7 | "azureFunctions.projectRuntime": "~4", 8 | "debug.internalConsoleOptions": "neverOpen", 9 | "azureFunctions.projectLanguageModel": 2 10 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /scripts/startLocal.sh: -------------------------------------------------------------------------------- 1 | # Resolve directory of this script (handles sourcing vs execution and spaces) 2 | SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")" &>/dev/null && pwd)" 3 | # Repo root assumed one level up 4 | REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" 5 | 6 | echo "Starting local Azure Functions environment from $REPO_ROOT" 7 | echo "Using script directory: $SCRIPT_DIR" 8 | 9 | cd "$REPO_ROOT/pipeline" 10 | python -m venv .venv 11 | 12 | source ./.venv/bin/activate 13 | pip install -r requirements.txt 14 | 15 | func start --build 16 | 17 | # func start --build 18 | -------------------------------------------------------------------------------- /infra/modules/rbac/keyvault-access.bicep: -------------------------------------------------------------------------------- 1 | param principalId string 2 | param roleDefinitionId string 3 | param principalType string 4 | param resourceName string 5 | 6 | resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { 7 | name: resourceName 8 | } 9 | 10 | resource assignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 11 | name: guid(keyVault.id, principalId, roleDefinitionId) 12 | scope: keyVault 13 | properties: { 14 | principalId: principalId 15 | roleDefinitionId: roleDefinitionId 16 | principalType: principalType 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /infra/modules/util/virtualNetworkData.bicep: -------------------------------------------------------------------------------- 1 | param vnetName string 2 | param subnetNames array 3 | 4 | resource network 'Microsoft.Network/virtualNetworks@2024-05-01' existing = { 5 | name: vnetName 6 | } 7 | 8 | resource subnet 'Microsoft.Network/virtualNetworks/subnets@2024-05-01' existing = [for subnetName in subnetNames: { 9 | name: subnetName 10 | parent: network 11 | }] 12 | 13 | output id string = network.id 14 | output subnets array = [for (subnetName, i) in subnetNames: { 15 | addressPrefix: subnet[i].properties.addressPrefix 16 | id: subnet[i].id 17 | name: subnet[i].name 18 | }] 19 | -------------------------------------------------------------------------------- /infra/modules/rbac/role.bicep: -------------------------------------------------------------------------------- 1 | param principalId string 2 | 3 | @allowed([ 4 | 'Device' 5 | 'ForeignGroup' 6 | 'Group' 7 | 'ServicePrincipal' 8 | 'User' 9 | ]) 10 | param principalType string = 'ServicePrincipal' 11 | param roleDefinitionId string 12 | 13 | resource role 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 14 | name: guid(resourceGroup().id, principalId, roleDefinitionId) 15 | properties: { 16 | principalId: principalId 17 | principalType: principalType 18 | roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionId) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /infra/modules/rbac/keyvault-access-policy.bicep: -------------------------------------------------------------------------------- 1 | param permissions object = { secrets: [ 'get', 'list', 'set', 'delete' ] } 2 | param principalId string 3 | param resourceName string 4 | 5 | resource resource 'Microsoft.KeyVault/vaults@2022-07-01' existing = { 6 | name: resourceName 7 | } 8 | 9 | resource keyVaultAccessPolicies 'Microsoft.KeyVault/vaults/accessPolicies@2022-07-01' = { 10 | parent: resource 11 | name: 'add' 12 | properties: { 13 | accessPolicies: [ { 14 | objectId: principalId 15 | tenantId: subscription().tenantId 16 | permissions: permissions 17 | } ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /infra/modules/storage/storage-queue.bicep: -------------------------------------------------------------------------------- 1 | @description('Name of the resource.') 2 | param name string 3 | 4 | @description('Name for the Storage Account associated with the queue.') 5 | param storageAccountName string 6 | 7 | resource queue 'Microsoft.Storage/storageAccounts/queueServices/queues@2024-01-01' = { 8 | name: '${storageAccountName}/default/${name}' 9 | properties: { 10 | metadata: {} 11 | } 12 | } 13 | 14 | @description('ID for the deployed Storage queue resource.') 15 | output id string = queue.id 16 | @description('Name for the deployed Storage queue resource.') 17 | output name string = queue.name 18 | -------------------------------------------------------------------------------- /azure.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json 2 | 3 | name: ai-document-processor 4 | 5 | infra: 6 | bicep: 7 | file: ./infra/main.bicep 8 | 9 | services: 10 | processing: 11 | project: ./pipeline 12 | language: python 13 | host: function 14 | 15 | hooks: 16 | postprovision: 17 | posix: 18 | run: scripts/postprovision.sh 19 | windows: 20 | run: scripts/postprovision.ps1 21 | # postdeploy: 22 | # posix: 23 | # run: scripts/postDeploy.sh 24 | # windows: 25 | # run: scripts/postDeploy.ps1 -------------------------------------------------------------------------------- /commandUtils/createLocalSettings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Ensure environment variables are loaded 4 | eval "$(azd env get-values)" 5 | 6 | # Define the JSON structure dynamically 7 | cat < local.settings.json 8 | { 9 | "IsEncrypted": false, 10 | "Values": { 11 | "FUNCTIONS_WORKER_RUNTIME": ${FUNCTIONS_WORKER_RUNTIME}, 12 | "AzureWebJobsStorage": ${AzureWebJobsStorage}, 13 | "BLOB_ENDPOINT": ${BLOB_ENDPOINT}, 14 | "OPENAI_API_VERSION": ${OPENAI_API_VERSION}, 15 | "OPENAI_API_BASE": ${OPENAI_API_BASE}, 16 | "OPENAI_MODEL": ${OPENAI_MODEL}, 17 | } 18 | } 19 | EOF 20 | 21 | echo "✅ local.settings.json has been created successfully!" -------------------------------------------------------------------------------- /commandUtils/staticWebApp/setStaticEnvVars.sh: -------------------------------------------------------------------------------- 1 | echo "Setting static web app environment variables" 2 | echo "Current Path: $(pwd)" 3 | 4 | eval "$(azd env get-values)" 5 | 6 | echo "$AZURE_STATIC_WEB_APP_NAME" 7 | echo "$AZURE_RESOURCE_GROUP" 8 | echo "$AZURE_STORAGE_ACCOUNT" 9 | echo "$FUNCTION_APP_NAME" 10 | echo "$PROMPT_FILE" 11 | echo "$FUNCTION_URL" 12 | 13 | az staticwebapp appsettings set \ 14 | --name $AZURE_STATIC_WEB_APP_NAME \ 15 | --resource-group $AZURE_RESOURCE_GROUP \ 16 | --setting-names REACT_APP_STORAGE_ACCOUNT_NAME=$AZURE_STORAGE_ACCOUNT REACT_APP_FUNCTION_APP_NAME=$FUNCTION_APP_NAME REACT_APP_PROMPT_FILE=$PROMPT_FILE REACT_APP_FUNCTION_URL=$FUNCTION_URL -------------------------------------------------------------------------------- /infra/modules/compute/hosting-plan.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param kind string = 'linux' 4 | param sku string 5 | 6 | @description('Tags.') 7 | param tags object 8 | 9 | resource hostingPlan 'Microsoft.Web/serverfarms@2024-04-01' = { 10 | name: name 11 | location: location 12 | sku: { 13 | name: sku 14 | capacity: 1 15 | } 16 | properties: { 17 | reserved: true 18 | } 19 | kind: kind 20 | tags : tags 21 | } 22 | 23 | output id string = hostingPlan.id 24 | output name string = hostingPlan.name 25 | output location string = hostingPlan.location 26 | output skuName string = hostingPlan.sku.name 27 | -------------------------------------------------------------------------------- /infra/modules/rbac/aiservices-user.bicep: -------------------------------------------------------------------------------- 1 | param principalId string 2 | param resourceName string 3 | 4 | resource resource 'Microsoft.CognitiveServices/accounts@2023-05-01' existing = { 5 | name: resourceName 6 | } 7 | 8 | var roleDefinitionId = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a97b65f3-24c7-4388-baec-2e87135dc908') // Cognitive Services User 9 | 10 | resource roleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { 11 | name: guid(resourceGroup().id, principalId, roleDefinitionId) 12 | scope: resource 13 | properties: { 14 | roleDefinitionId: roleDefinitionId 15 | principalId: principalId 16 | principalType: 'ServicePrincipal' 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /infra/modules/security/resource-group-role-assignment.bicep: -------------------------------------------------------------------------------- 1 | import { roleAssignmentInfo } from '../security/managed-identity.bicep' 2 | 3 | @description('Role assignments to create for the Resource Group.') 4 | param roleAssignments roleAssignmentInfo[] = [] 5 | 6 | resource assignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = [ 7 | for roleAssignment in roleAssignments: { 8 | name: guid(resourceGroup().id, roleAssignment.principalId, roleAssignment.roleDefinitionId) 9 | scope: resourceGroup() 10 | properties: { 11 | principalId: roleAssignment.principalId 12 | roleDefinitionId: roleAssignment.roleDefinitionId 13 | principalType: roleAssignment.principalType 14 | } 15 | } 16 | ] 17 | -------------------------------------------------------------------------------- /infra/modules/rbac/cogservices-openai-user.bicep: -------------------------------------------------------------------------------- 1 | param principalId string 2 | param resourceName string 3 | 4 | resource resource 'Microsoft.CognitiveServices/accounts@2023-05-01' existing = { 5 | name: resourceName 6 | } 7 | 8 | // Cognitive Services OpenAI User Role 9 | var roleDefinitionId = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd') 10 | 11 | resource roleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { 12 | name: guid(resourceGroup().id, principalId, roleDefinitionId) 13 | scope: resource 14 | properties: { 15 | roleDefinitionId: roleDefinitionId 16 | principalId: principalId 17 | principalType: 'ServicePrincipal' 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pipeline/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "logLevel": { 5 | "default": "Warning", 6 | "Host.Aggregator": "Error", 7 | "Host.Results": "Error", 8 | "Function": "Information", 9 | "DurableTask": "Warning", 10 | "Worker": "Warning" 11 | }, 12 | "applicationInsights": { 13 | "samplingSettings": { 14 | "isEnabled": true, 15 | "excludedTypes": "Request" 16 | } 17 | } 18 | }, 19 | "durableTask": { 20 | "tracing": { 21 | "traceInputsAndOutputs": false, 22 | "traceReplayEvents": false 23 | } 24 | }, 25 | "extensionBundle": { 26 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 27 | "version": "[4.*, 5.0.0)" 28 | } 29 | } -------------------------------------------------------------------------------- /scripts/getBlobConnectionStrings.sh: -------------------------------------------------------------------------------- 1 | set -euo pipefail 2 | 3 | eval $(azd env get-values) 4 | 5 | cd ./pipeline 6 | # func azure functionapp fetch-app-settings $PROCESSING_FUNCTION_APP_NAME --decrypt 7 | 8 | BLOB_FUNC_CONN_STRING=$(az storage account show-connection-string \ 9 | --name $AZURE_STORAGE_ACCOUNT \ 10 | --resource-group $RESOURCE_GROUP \ 11 | --query connectionString \ 12 | -o tsv) 13 | 14 | BLOB_DATA_STORAGE_CONN_STRING=$(az storage account show-connection-string \ 15 | --name $AZURE_STORAGE_ACCOUNT \ 16 | --resource-group $RESOURCE_GROUP \ 17 | --query connectionString \ 18 | -o tsv) 19 | 20 | echo "Blob connection string: $BLOB_FUNC_CONN_STRING" 21 | echo "Data Storage connection string: $BLOB_DATA_STORAGE_CONN_STRING" 22 | -------------------------------------------------------------------------------- /infra/modules/rbac/blob-queue-contributor.bicep: -------------------------------------------------------------------------------- 1 | param principalId string 2 | param resourceName string 3 | 4 | resource resource 'Microsoft.Storage/storageAccounts@2021-04-01' existing = { 5 | name: resourceName 6 | } 7 | 8 | var queueContributorRoleId = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88') // Storage Queue Data Contributor role 9 | 10 | resource queueRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { 11 | name: guid(resourceGroup().id, principalId, queueContributorRoleId) 12 | scope: resource 13 | properties: { 14 | roleDefinitionId: queueContributorRoleId 15 | principalId: principalId 16 | principalType: 'ServicePrincipal' 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /infra/modules/rbac/blob-contributor.bicep: -------------------------------------------------------------------------------- 1 | param principalId string 2 | param resourceName string 3 | param principalType string = 'ServicePrincipal' 4 | 5 | resource resource 'Microsoft.Storage/storageAccounts@2021-04-01' existing = { 6 | name: resourceName 7 | } 8 | 9 | var roleDefinitionId = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') // Storage Blob Data Contributor role 10 | 11 | resource roleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { 12 | name: guid(resourceGroup().id, principalId, roleDefinitionId) 13 | scope: resource 14 | properties: { 15 | roleDefinitionId: roleDefinitionId 16 | principalId: principalId 17 | principalType: principalType 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /scripts/postDeploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Set up logging 4 | LOG_FILE="postdeploy.log" 5 | # Redirect stdout and stderr to tee, appending to the log file 6 | exec > >(tee -a "$LOG_FILE") 2> >(tee -a "$LOG_FILE" >&2) 7 | 8 | echo "Post-provision script started." 9 | 10 | echo "Current Path: $(pwd)" 11 | eval "$(azd env get-values)" 12 | eval "$(azd env get-values | sed 's/^/export /')" 13 | echo "Uploading Blob to Azure Storage Account: $AZURE_STORAGE_ACCOUNT" 14 | 15 | { 16 | az storage blob upload \ 17 | --account-name $AZURE_STORAGE_ACCOUNT \ 18 | --container-name "prompts" \ 19 | --name prompts.yaml \ 20 | --file ./data/prompts.yaml \ 21 | --auth-mode login 22 | echo "Upload of prompts.yaml completed successfully to $AZURE_STORAGE_ACCOUNT." 23 | } || { 24 | echo "file prompts.yaml may already exist. Skipping upload" 25 | } -------------------------------------------------------------------------------- /commandUtils/functions/updateEnv.sh: -------------------------------------------------------------------------------- 1 | RESOURCE_GROUP_NAME="rg-demo" 2 | FUNCTION_APP_NAME="processing-dbfbvcgqgrid6" 3 | 4 | 5 | az functionapp config appsettings set \ 6 | --name $FUNCTION_APP_NAME \ 7 | --resource-group $RESOURCE_GROUP_NAME \ 8 | --settings SCM_DO_BUILD_DURING_DEPLOYMENT="true" ENABLE_ORYX_BUILD="true" 9 | 10 | 11 | az functionapp config appsettings set \ 12 | --name processing-dbfbvcgqgrid6 \ 13 | --resource-group rg-demo \ 14 | --settings SCM_DO_BUILD_DURING_DEPLOYMENT="true" ENABLE_ORYX_BUILD="true" 15 | 16 | 17 | az functionapp config appsettings delete \ 18 | --name processing-dbfbvcgqgrid6 \ 19 | --resource-group rg-demo \ 20 | --setting-names BUILD_FLAGS 21 | 22 | az functionapp config appsettings delete \ 23 | --name processing-dbfbvcgqgrid6 \ 24 | --resource-group rg-demo \ 25 | --setting-names XDG_CACHE_HOME -------------------------------------------------------------------------------- /infra/modules/util/delay.bicep: -------------------------------------------------------------------------------- 1 | param location string = resourceGroup().location 2 | param utcValue string = utcNow() 3 | param sleepName string = 'sleep-1' 4 | param sleepSeconds int = 120 5 | resource sleepDelay 'Microsoft.Resources/deploymentScripts@2020-10-01' = { 6 | name: sleepName 7 | location: location 8 | kind: 'AzurePowerShell' 9 | properties: { 10 | forceUpdateTag: utcValue 11 | azPowerShellVersion: '8.3' 12 | timeout: 'PT10M' 13 | arguments: '-seconds ${sleepSeconds}' 14 | scriptContent: ''' 15 | param ( [string] $seconds ) 16 | Write-Output Sleeping for: $seconds .... 17 | Start-Sleep -Seconds $seconds 18 | Write-Output Sleep over - resuming .... 19 | ''' 20 | cleanupPreference: 'OnSuccess' 21 | retentionInterval: 'P1D' 22 | } 23 | } 24 | output location string = sleepDelay.location 25 | -------------------------------------------------------------------------------- /infra/modules/rbac/cosmos-contributor.bicep: -------------------------------------------------------------------------------- 1 | param principalId string 2 | param resourceName string 3 | param roleDefinitionGuid string = '00000000-0000-0000-0000-000000000002' // Cosmos DB Built-in Data Contributor 4 | 5 | resource cosmosDbAccount 'Microsoft.DocumentDB/databaseAccounts@2021-04-15' existing = { 6 | name: resourceName 7 | } 8 | 9 | var computedRoleDefinitionId = resourceId(resourceGroup().name, 'Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', resourceName, roleDefinitionGuid) 10 | 11 | resource roleAssignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2024-05-15' = { 12 | name: guid(resourceGroup().id, principalId, computedRoleDefinitionId) 13 | parent: cosmosDbAccount 14 | properties: { 15 | roleDefinitionId: computedRoleDefinitionId 16 | principalId: principalId 17 | scope: cosmosDbAccount.id 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /infra/modules/storage/storage-blob-container.bicep: -------------------------------------------------------------------------------- 1 | @description('Name of the resource.') 2 | param name string 3 | 4 | @description('Name for the Storage Account associated with the blob container.') 5 | param storageAccountName string 6 | @description('Public access level for the blob container.') 7 | @allowed([ 8 | 'Blob' 9 | 'Container' 10 | 'None' 11 | ]) 12 | param publicAccess string = 'None' 13 | 14 | resource container 'Microsoft.Storage/storageAccounts/blobServices/containers@2024-01-01' = { 15 | name: '${storageAccountName}/default/${name}' 16 | properties: { 17 | publicAccess: publicAccess 18 | metadata: {} 19 | } 20 | } 21 | 22 | @description('ID for the deployed Storage blob container resource.') 23 | output id string = container.id 24 | @description('Name for the deployed Storage blob container resource.') 25 | output name string = name 26 | -------------------------------------------------------------------------------- /infra/modules/network/private-dns-zones.bicep: -------------------------------------------------------------------------------- 1 | param dnsZoneName string 2 | param virtualNetworkName string 3 | param tags object = {} 4 | 5 | resource vnet 'Microsoft.Network/virtualNetworks@2024-05-01' existing = { 6 | name: virtualNetworkName 7 | } 8 | 9 | resource dnsZone 'Microsoft.Network/privateDnsZones@2024-06-01' = { 10 | name: dnsZoneName 11 | location: 'global' 12 | tags: tags 13 | dependsOn: [ 14 | vnet 15 | ] 16 | } 17 | 18 | resource privateDnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = { 19 | name: '${virtualNetworkName}-dnslink' 20 | parent: dnsZone 21 | location: 'global' 22 | tags: tags 23 | properties: { 24 | virtualNetwork: { 25 | id:vnet.id 26 | } 27 | registrationEnabled: false 28 | } 29 | } 30 | 31 | 32 | output privateDnsZoneName string = dnsZone.name 33 | output id string = dnsZone.id 34 | -------------------------------------------------------------------------------- /infra/modules/ai_ml/modelDeployment.bicep: -------------------------------------------------------------------------------- 1 | @description('Azure OpenAI account name.') 2 | param aiServicesName string 3 | 4 | @description('Azure OpenAI model deployment name.') 5 | param deploymentName string = 'gpt-4o' 6 | 7 | @description('Azure OpenAI model name, e.g. "gpt-35-turbo".') 8 | param modelName string = 'gpt-4o' 9 | 10 | resource openAIAccount 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = { 11 | name: aiServicesName 12 | } 13 | 14 | resource openAIDeployment 'Microsoft.CognitiveServices/accounts/deployments@2024-10-01' = { 15 | name: deploymentName 16 | parent: openAIAccount 17 | sku: { 18 | name: 'Standard' 19 | capacity: 30 20 | } 21 | properties: { 22 | model: { 23 | format: 'OpenAI' 24 | name: modelName 25 | // version: '0301' // Optionally specify version 26 | } 27 | } 28 | } 29 | 30 | output deploymentName string = openAIDeployment.name 31 | -------------------------------------------------------------------------------- /infra/modules/security/key-vault-secret-environment-variables.bicep: -------------------------------------------------------------------------------- 1 | @description('URI for the Key Vault associated with the environment variables.') 2 | param keyVaultSecretUri string 3 | @description('Names of the environment variables to retrieve from Key Vault Secrets.') 4 | param variableNames array 5 | 6 | @export() 7 | @description('Information about the environment variables containing the name and a value represented as a Key Vault Secret URI.') 8 | type environmentVariableInfo = { 9 | name: string 10 | value: string 11 | } 12 | 13 | var keyVaultSettings = [ 14 | for setting in variableNames: { 15 | name: setting 16 | value: '@Microsoft.KeyVault(SecretUri=${keyVaultSecretUri}secrets/${setting})' 17 | } 18 | ] 19 | 20 | @description('Environment variables containing the name and a value represented as a Key Vault Secret URI.') 21 | output environmentVariables environmentVariableInfo[] = keyVaultSettings 22 | -------------------------------------------------------------------------------- /infra/modules/rbac/appconfig-access.bicep: -------------------------------------------------------------------------------- 1 | param principalId string 2 | param resourceName string 3 | 4 | resource resource 'Microsoft.AppConfiguration/configurationStores@2024-05-01' existing = { 5 | name: resourceName 6 | } 7 | 8 | //var roleDefinitionId = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'fe86443c-f201-4fc4-9d2a-ac61149fbda0') // App Configuration Contributor 9 | var roleDefinitionId = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5ae67dd6-50cb-40e7-96ff-dc2bfa4b606b') // App Configuration Data Owner 10 | 11 | // Create a role assignment for the web app managed identity to access the function app 12 | resource roleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { 13 | name: guid(resource.id, principalId, 'Data Owner') 14 | scope: resource 15 | properties: { 16 | roleDefinitionId: roleDefinitionId 17 | principalId: principalId 18 | principalType: 'ServicePrincipal' 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /infra/modules/security/key-vault-secret.bicep: -------------------------------------------------------------------------------- 1 | @description('Name of the secret.') 2 | param name string 3 | 4 | @description('Name of the Key Vault associated with the secret.') 5 | param keyVaultName string 6 | @description('Value of the secret.') 7 | @secure() 8 | param value string 9 | 10 | resource keyVault 'Microsoft.KeyVault/vaults@2024-12-01-preview' existing = { 11 | name: keyVaultName 12 | 13 | resource keyVaultSecret 'secrets' = { 14 | name: name 15 | properties: { 16 | value: value 17 | attributes: { 18 | enabled: true 19 | } 20 | } 21 | } 22 | } 23 | 24 | @description('ID for the deployed Key Vault Secret resource.') 25 | output id string = keyVault::keyVaultSecret.id 26 | @description('Name for the deployed Key Vault Secret resource.') 27 | output name string = keyVault::keyVaultSecret.name 28 | @description('URI for the deployed Key Vault Secret resource.') 29 | output uri string = keyVault::keyVaultSecret.properties.secretUri 30 | -------------------------------------------------------------------------------- /scripts/postDeploy.ps1: -------------------------------------------------------------------------------- 1 | write-host "Post-provisioning script started." 2 | 3 | # Load azd environment values (emulates: eval $(azd env get-values)) 4 | azd env get-values | ForEach-Object { 5 | if ($_ -match '^(?[^=]+)=(?.*)$') { 6 | $k = $matches.key.Trim() 7 | $v = $matches.val 8 | 9 | # Remove exactly one outer pair of double quotes if present 10 | if ($v.Length -ge 2 -and $v.StartsWith('"') -and $v.EndsWith('"')) { 11 | $v = $v.Substring(1, $v.Length - 2) 12 | # Unescape any embedded \" (azd usually doesn’t emit these, but safe) 13 | $v = $v -replace '\\"','"' 14 | } 15 | 16 | [Environment]::SetEnvironmentVariable($k, $v) 17 | Set-Variable -Name $k -Value $v -Scope Script -Force 18 | } 19 | } 20 | 21 | # Upload initial blob and prompt file 22 | az storage blob upload --account-name $env:AZURE_STORAGE_ACCOUNT --container-name "prompts" --name prompts.yaml --file ./data/prompts.yaml --auth-mode login -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "func", 6 | "label": "func: host start", 7 | "command": "host start", 8 | "problemMatcher": "$func-python-watch", 9 | "isBackground": true, 10 | "dependsOn": "pip install (functions)", 11 | "options": { 12 | "cwd": "${workspaceFolder}/pipeline" 13 | } 14 | }, 15 | { 16 | "label": "pip install (functions)", 17 | "type": "shell", 18 | "osx": { 19 | "command": "${config:azureFunctions.pythonVenv}/bin/python -m pip install -r requirements.txt" 20 | }, 21 | "windows": { 22 | "command": "${config:azureFunctions.pythonVenv}/Scripts/python -m pip install -r requirements.txt" 23 | }, 24 | "linux": { 25 | "command": "${config:azureFunctions.pythonVenv}/bin/python -m pip install -r requirements.txt" 26 | }, 27 | "problemMatcher": [], 28 | "options": { 29 | "cwd": "${workspaceFolder}/pipeline" 30 | } 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /scripts/startLocal.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Starts the Azure Functions app locally (PowerShell version of startLocal.sh) 4 | 5 | .Run from: repo root or scripts folder 6 | #> 7 | 8 | Set-StrictMode -Version Latest 9 | $ErrorActionPreference = "Stop" 10 | 11 | # Ensure we run relative to this script's location 12 | $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path 13 | Set-Location $scriptDir 14 | 15 | # Move into pipeline directory 16 | Set-Location ..\pipeline 17 | 18 | # Create venv if missing 19 | if (-not (Test-Path .venv)) { 20 | Write-Host "Creating virtual environment..." 21 | python -m venv .venv 22 | } 23 | 24 | # Activate venv (Windows / PowerShell) 25 | $activate = Join-Path .venv "Scripts\Activate.ps1" 26 | if (-not (Test-Path $activate)) { 27 | Write-Error "Activation script not found at $activate" 28 | exit 1 29 | } 30 | & $activate 31 | 32 | # Upgrade pip (optional but helpful) 33 | python -m pip install --upgrade pip 34 | 35 | # Install dependencies 36 | pip install -r requirements.txt 37 | 38 | # Start Azure Functions host 39 | func start --build -------------------------------------------------------------------------------- /infra/modules/security/private-link-scope.bicep: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | param privateLinkScopeName string 5 | param privateLinkScopedResources array = [] 6 | param queryAccessMode string = 'Open' 7 | param ingestionAccessMode string = 'PrivateOnly' 8 | 9 | resource privateLinkScope 'Microsoft.Insights/privateLinkScopes@2023-06-01-preview' = { 10 | name: privateLinkScopeName 11 | location: 'global' 12 | properties: { 13 | accessModeSettings: { 14 | queryAccessMode: queryAccessMode 15 | ingestionAccessMode: ingestionAccessMode 16 | } 17 | } 18 | } 19 | 20 | resource scopedResources 'Microsoft.Insights/privateLinkScopes/scopedResources@2023-06-01-preview' = [ 21 | for id in privateLinkScopedResources: { 22 | name: uniqueString(id) 23 | parent: privateLinkScope 24 | properties: { 25 | kind: 'Resource' 26 | linkedResourceId: id 27 | subscriptionLocation: resourceGroup().location 28 | } 29 | } 30 | ] 31 | 32 | output name string = privateLinkScope.name 33 | output id string = privateLinkScope.id 34 | -------------------------------------------------------------------------------- /infra/modules/network/private-endpoint.bicep: -------------------------------------------------------------------------------- 1 | param location string 2 | param name string 3 | param tags object = {} 4 | param serviceId string 5 | param subnetId string 6 | param groupIds array = [] 7 | param dnsZoneId string 8 | 9 | resource privateEndpoint 'Microsoft.Network/privateEndpoints@2024-05-01' = { 10 | name: name 11 | location: location 12 | tags: tags 13 | properties: { 14 | subnet: { 15 | id: subnetId 16 | } 17 | privateLinkServiceConnections: [ 18 | { 19 | name: 'privatelinkServiceonnection' 20 | properties: { 21 | privateLinkServiceId: serviceId 22 | groupIds: groupIds 23 | } 24 | } 25 | ] 26 | } 27 | } 28 | 29 | resource privateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2024-05-01' = { 30 | parent: privateEndpoint 31 | name: '${name}-group' 32 | properties:{ 33 | privateDnsZoneConfigs:[ 34 | { 35 | name:'config1' 36 | properties:{ 37 | privateDnsZoneId: dnsZoneId 38 | } 39 | } 40 | ] 41 | } 42 | } 43 | 44 | output name string = privateEndpoint.name 45 | -------------------------------------------------------------------------------- /scripts/postprovision.ps1: -------------------------------------------------------------------------------- 1 | write-host "Post-provisioning script started." 2 | 3 | # Load azd environment values (emulates: eval $(azd env get-values)) 4 | azd env get-values | ForEach-Object { 5 | if ($_ -match '^(?[^=]+)=(?.*)$') { 6 | $k = $matches.key.Trim() 7 | $v = $matches.val 8 | 9 | # Remove exactly one outer pair of double quotes if present 10 | if ($v.Length -ge 2 -and $v.StartsWith('"') -and $v.EndsWith('"')) { 11 | $v = $v.Substring(1, $v.Length - 2) 12 | # Unescape any embedded \" (azd usually doesn’t emit these, but safe) 13 | $v = $v -replace '\\"','"' 14 | } 15 | 16 | [Environment]::SetEnvironmentVariable($k, $v) 17 | Set-Variable -Name $k -Value $v -Scope Script -Force 18 | } 19 | } 20 | 21 | # Upload initial blob and prompt file 22 | az storage blob upload --account-name $env:AZURE_STORAGE_ACCOUNT --container-name "prompts" --name prompts.yaml --file ./data/prompts.yaml --auth-mode login 23 | az storage blob upload --account-name $env:AZURE_STORAGE_ACCOUNT --container-name "bronze" --name role_library-3.pdf --file ./data/role_library-3.pdf --auth-mode login -------------------------------------------------------------------------------- /data/prompts.yaml: -------------------------------------------------------------------------------- 1 | system_prompt: | 2 | Generate a structured JSON object representing the roles within a company. Each role should include the following fields: 3 | 4 | role (the job title), 5 | company (the company name), 6 | location (the job location or flexibility), 7 | qualifications (required qualifications for the role), 8 | responsibilities (key duties and expectations for the role). 9 | The JSON should follow this structure: 10 | 11 | [ 12 | { 13 | "role": "Role Name", 14 | "company": "Company Name", 15 | "location": "Location of the Role", 16 | "qualifications": [ 17 | "Qualification 1", 18 | "Qualification 2", 19 | "Qualification 3" 20 | ], 21 | "responsibilities": [ 22 | "Responsibility 1", 23 | "Responsibility 2", 24 | "Responsibility 3" 25 | ] 26 | }, 27 | ... 28 | ] 29 | 30 | Provide a complete JSON object for the roles within the company. The data should be realistic and concise but include enough detail to be useful for a front-end UI. 31 | 32 | 33 | user_prompt: 34 | "Read the following text and generate the table as previously instructed.\n\nText: \n" -------------------------------------------------------------------------------- /infra/modules/rbac/search-access.bicep: -------------------------------------------------------------------------------- 1 | param principalId string 2 | param resourceName string 3 | 4 | resource resource 'Microsoft.Search/searchServices@2021-04-01-preview' existing = { 5 | name: resourceName 6 | } 7 | 8 | // Search Index Contributor 9 | resource roleAssignmentSIDC 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { 10 | name: guid(resourceGroup().id, resource.id, principalId, 'SIDC') 11 | scope: resource 12 | properties: { 13 | roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7') // Search Index Data Contributor 14 | principalId: principalId 15 | principalType: 'ServicePrincipal' 16 | } 17 | } 18 | 19 | // Search Service Contributor 20 | resource roleAssignmentSSC 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { 21 | name: guid(resourceGroup().id, resource.id, principalId, 'SSC') 22 | scope: resource 23 | properties: { 24 | roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0') // Search Service Contributor 25 | principalId: principalId 26 | principalType: 'ServicePrincipal' 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | 1. Code Review Frequency - 1-2 business days for comments on PRs 2 | 2. Code Review required for any udpate to main 3 | 4 | 3. Standard Contribution workflow 5 | - Fork repo 6 | - Pull upstream changes 7 | - Make changes, test updates 8 | 9 | 4. PR Best Practices 10 | - PRs must not be behind upstream main 11 | - PRs should be isolated to a specific area (infra updates, static web app feature, etc.) 12 | - Contributor should ensure that code deploys properly and pipeline runs with no errors before submitting1. Code Review Frequency - 1 BD for comment 13 | 2. Mandate code reviews for all changes 14 | 3. Code Owners for different directories 15 | 16 | 4. Standard Contribution workflow 17 | - Fork repo 18 | - Pull upstream changes 19 | - Make changes, test updates 20 | 21 | 5. PR Best Practices 22 | - PRs must not be behind upstream main 23 | - PR Templates1. Code Review Frequency - 1 BD for comment 24 | 2. Mandate code reviews for all changes 25 | 3. Code Owners for different directories 26 | 27 | 4. Standard Contribution workflow 28 | - Fork repo 29 | - Pull upstream changes 30 | - Make changes, test updates 31 | 32 | 5. PR Best Practices 33 | - PRs must not be behind upstream main 34 | - PR Templates -------------------------------------------------------------------------------- /infra/modules/ai_ml/aoai-account.bicep: -------------------------------------------------------------------------------- 1 | @description('That name is the name of our application. It has to be unique.Type a name followed by your resource group name. (-)') 2 | param aoaiName string 3 | 4 | @description('Location for all resources.') 5 | param location string = resourceGroup().location 6 | param customSubDomainName string = aoaiName 7 | @allowed([ 8 | 'S0' 9 | ]) 10 | param sku string = 'S0' 11 | param kind string = 'OpenAI' 12 | param publicNetworkAccess string = 'Enabled' 13 | 14 | resource openAIAccount 'Microsoft.CognitiveServices/accounts@2024-10-01' = { 15 | name: aoaiName 16 | location: location 17 | identity: { 18 | type: 'SystemAssigned' 19 | } 20 | sku: { 21 | name: sku 22 | } 23 | kind: kind 24 | properties: { 25 | customSubDomainName: customSubDomainName 26 | publicNetworkAccess: publicNetworkAccess 27 | networkAcls: { 28 | defaultAction: 'Allow' 29 | virtualNetworkRules: [] 30 | ipRules: [] 31 | } 32 | } 33 | } 34 | 35 | output AOAI_ENDPOINT string = openAIAccount.properties.endpoint 36 | // output AOAI_API_KEY string = openAIAccount.listKeys().key1 37 | output name string = openAIAccount.name 38 | output id string = openAIAccount.id 39 | -------------------------------------------------------------------------------- /scripts/postprovision.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Set up logging 4 | LOG_FILE="postprovision.log" 5 | # Redirect stdout and stderr to tee, appending to the log file 6 | exec > >(tee -a "$LOG_FILE") 2> >(tee -a "$LOG_FILE" >&2) 7 | 8 | echo "Post-provision script started." 9 | 10 | echo "Current Path: $(pwd)" 11 | eval "$(azd env get-values)" 12 | eval "$(azd env get-values | sed 's/^/export /')" 13 | echo "Uploading Blob to Azure Storage Account: $AZURE_STORAGE_ACCOUNT" 14 | 15 | { 16 | az storage blob upload \ 17 | --account-name $AZURE_STORAGE_ACCOUNT \ 18 | --container-name "prompts" \ 19 | --name prompts.yaml \ 20 | --file ./data/prompts.yaml \ 21 | --auth-mode login 22 | echo "Upload of prompts.yaml completed successfully to $AZURE_STORAGE_ACCOUNT." 23 | } || { 24 | echo "file prompts.yaml may already exist. Skipping upload" 25 | } 26 | 27 | 28 | { 29 | az storage blob upload \ 30 | --account-name $AZURE_STORAGE_ACCOUNT \ 31 | --container-name "bronze" \ 32 | --name role_library-3.pdf \ 33 | --file ./data/role_library-3.pdf \ 34 | --auth-mode login 35 | echo "Upload of role_library-3.pdf completed successfully to $AZURE_STORAGE_ACCOUNT." 36 | } || { 37 | echo "file role_library-3.pdf may already exist. Skipping upload" 38 | } -------------------------------------------------------------------------------- /infra/modules/rbac/blob-dataowner.bicep: -------------------------------------------------------------------------------- 1 | param principalId string 2 | param resourceName string 3 | 4 | resource storageAccount 'Microsoft.Storage/storageAccounts@2021-09-01' existing = { 5 | name: resourceName 6 | } 7 | 8 | var storageBlobDataOwnerRoleId = 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b' // Storage Blob Data Owner role 9 | // var ownerRoleId = '8e3af657-a8ff-443c-a75c-2fe8c4bcb635' // Owner role 10 | 11 | // Assign Storage Blob Data Owner role 12 | resource storageBlobDataOwnerAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { 13 | name: guid(storageAccount.id, principalId, storageBlobDataOwnerRoleId) 14 | scope: storageAccount 15 | properties: { 16 | roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', storageBlobDataOwnerRoleId) 17 | principalId: principalId 18 | principalType: 'ServicePrincipal' 19 | } 20 | } 21 | 22 | // Assign Owner role 23 | // resource ownerRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { 24 | // name: guid(storageAccount.id, principalID, ownerRoleId) 25 | // scope: storageAccount 26 | // properties: { 27 | // roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', ownerRoleId) 28 | // principalId: principalID 29 | // principalType: 'ServicePrincipal' 30 | // } 31 | // } 32 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # TODO: The maintainer of this repo has not yet edited this file 2 | 3 | **REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project? 4 | 5 | - **No CSS support:** Fill out this template with information about how to file issues and get help. 6 | - **Yes CSS support:** Fill out an intake form at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). CSS will work with/help you to determine next steps. 7 | - **Not sure?** Fill out an intake as though the answer were "Yes". CSS will help you decide. 8 | 9 | *Then remove this first heading from this SUPPORT.MD file before publishing your repo.* 10 | 11 | # Support 12 | 13 | ## How to file issues and get help 14 | 15 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 16 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 17 | feature request as a new Issue. 18 | 19 | For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE 20 | FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER 21 | CHANNEL. WHERE WILL YOU HELP PEOPLE?**. 22 | 23 | ## Microsoft Support Policy 24 | 25 | Support for this **PROJECT or PRODUCT** is limited to the resources listed above. 26 | -------------------------------------------------------------------------------- /docs/troubleShootingGuide.md: -------------------------------------------------------------------------------- 1 | # Common Issues 2 | 3 | 1. Azure functions don't show up in portal 4 | - Description: azd deploy executes successfully in the terminal, but the functions are not found in the Azure portal 5 | Troubleshooting Steps 6 | - The issue could be that your function is unable to authenticate to the default blob storage account 7 | - Check the log stream - are there any authorization issues? 8 | - Check the networking of the Storage account - is public access is enabled? 9 | - Check environment vars, is the storage account named correclty? 10 | - Ensure that you are not deploying a local.settings.json with your function package 11 | - Check to ensure that your function app's managed identity has Storage Blob Data Contributor, Storage Queue Data Contributor, and Storage Tables Data Contributor role (Check in Azure portal under IAM) 12 | 13 | 2. 14 | E.g. 15 | "2025-08-08T21:03:05Z [Warning] Error response [991829f9-8ce5-415f-a7e5-5d26086b4183] 404 The specified queue does not exist. (00.1s) 16 | Server:Windows-Azure-Queue/1.0 Microsoft-HTTPAPI/2.0 17 | x-ms-request-id:9e809390-f003-0052-78a7-08c575000000 18 | x-ms-client-request-id:991829f9-8ce5-415f-a7e5-5d26086b4183 19 | x-ms-version:2025-05-05 20 | x-ms-error-code:QueueNotFound 21 | Date:Fri, 08 Aug 2025 21:03:05 GMT 22 | Content-Length:217 23 | Content-Type:application/xml" -------------------------------------------------------------------------------- /infra/modules/ai_ml/aimultiservices.bicep: -------------------------------------------------------------------------------- 1 | @description('Azure AI Multi Services name. It has to be unique. Type a name followed by your resource group name. (-)') 2 | param aiMultiServicesName string 3 | 4 | @description('Location for all resources.') 5 | param location string = resourceGroup().location 6 | 7 | param identityId string 8 | param publicNetworkAccess string = 'Enabled' 9 | 10 | @allowed([ 11 | 'S0' 12 | ]) 13 | param sku string = 'S0' 14 | 15 | resource aiMultiServices 'Microsoft.CognitiveServices/accounts@2024-10-01' = { 16 | name: aiMultiServicesName 17 | location: location 18 | sku: { 19 | name: sku 20 | } 21 | identity: { 22 | type: 'SystemAssigned, UserAssigned' 23 | userAssignedIdentities: identityId == null 24 | ? null 25 | : { 26 | '${identityId}': { 27 | // principalId: identityPrincipalId 28 | // clientId: identityClientId 29 | } 30 | } 31 | } 32 | kind: 'CognitiveServices' 33 | properties: { 34 | customSubDomainName: aiMultiServicesName 35 | publicNetworkAccess: publicNetworkAccess 36 | networkAcls: { 37 | defaultAction: 'Allow' 38 | } 39 | } 40 | } 41 | 42 | output id string = aiMultiServices.id 43 | output aiMultiServicesName string = aiMultiServices.name 44 | output aiMultiServicesEndpoint string = aiMultiServices.properties.endpoint 45 | output aimsaSystemAssignedPrincipalId string = aiMultiServices.identity.principalId 46 | -------------------------------------------------------------------------------- /scripts/getRemoteSettings.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | eval $(azd env get-values) 5 | 6 | cd ./pipeline 7 | func azure functionapp fetch-app-settings $PROCESSING_FUNCTION_APP_NAME --decrypt 8 | 9 | func settings decrypt 10 | 11 | CONFIG_CONN_STRING=$(az appconfig credential list \ 12 | --name "$APP_CONFIG_NAME" \ 13 | --query "[?name=='Primary'].connectionString" \ 14 | -o tsv) 15 | 16 | BLOB_FUNC_CONN_STRING=$(az storage account show-connection-string \ 17 | --name $AZURE_STORAGE_ACCOUNT \ 18 | --resource-group $RESOURCE_GROUP \ 19 | --query connectionString \ 20 | -o tsv) 21 | 22 | BLOB_DATA_STORAGE_CONN_STRING=$(az storage account show-connection-string \ 23 | --name $AZURE_STORAGE_ACCOUNT \ 24 | --resource-group $RESOURCE_GROUP \ 25 | --query connectionString \ 26 | -o tsv) 27 | 28 | jq \ 29 | --arg CONFIG_CONN_STRING "$CONFIG_CONN_STRING" \ 30 | --arg BLOB_FUNC_CONN_STRING "$BLOB_FUNC_CONN_STRING" \ 31 | --arg BLOB_DATA_STORAGE_CONN_STRING "$BLOB_DATA_STORAGE_CONN_STRING" \ 32 | ' 33 | .Values.AZURE_APPCONFIG_CONNECTION_STRING = $CONFIG_CONN_STRING 34 | | .Values.AzureWebJobsStorage = $BLOB_FUNC_CONN_STRING 35 | | .Values.DataStorage = $BLOB_DATA_STORAGE_CONN_STRING 36 | ' local.settings.json > local.settings.tmp && mv local.settings.tmp local.settings.json 37 | 38 | 39 | echo "Updated local.settings.json with following values: ${CONFIG_CONN_STRING}, ${BLOB_FUNC_CONN_STRING}, ${BLOB_DATA_STORAGE_CONN_STRING}" -------------------------------------------------------------------------------- /pipeline/pipelineUtils/db.py: -------------------------------------------------------------------------------- 1 | # backendUtils/db.py 2 | import os 3 | import logging 4 | import json 5 | from azure.cosmos import CosmosClient, PartitionKey, exceptions 6 | from azure.identity import DefaultAzureCredential 7 | from datetime import datetime 8 | import uuid 9 | # Set up logging 10 | logging.basicConfig(level=logging.INFO) 11 | 12 | from configuration import Configuration 13 | config = Configuration() 14 | 15 | # Retrieve Cosmos DB settings from environment variables 16 | COSMOS_DB_URI = config.get_value("COSMOS_DB_URI") 17 | COSMOS_DB_DATABASE = config.get_value("COSMOS_DB_DATABASE_NAME") 18 | COSMOS_DB_CONVERSATION_CONTAINER = config.get_value("COSMOS_DB_CONVERSATION_HISTORY_CONTAINER") 19 | 20 | 21 | def save_chat_message(conversation_id: str, role: str, content: str, usage: dict = None): 22 | client = CosmosClient(COSMOS_DB_URI, credential=config.credential) 23 | db = client.get_database_client(COSMOS_DB_DATABASE) 24 | container = db.get_container_client(COSMOS_DB_CONVERSATION_CONTAINER) 25 | 26 | item = { 27 | "id": str(uuid.uuid4()), 28 | "conversationId": conversation_id, 29 | "role": role, 30 | "content": content, 31 | "timestamp": datetime.utcnow().isoformat() + "Z" 32 | } 33 | if usage: 34 | item.update({ 35 | "promptTokens": usage.get("prompt_tokens"), 36 | "completionTokens": usage.get("completion_tokens"), 37 | "totalTokens": usage.get("total_tokens"), 38 | "model": usage.get("model") 39 | }) 40 | 41 | return container.create_item(body=item) -------------------------------------------------------------------------------- /infra/modules/util/metricAlerts.bicep: -------------------------------------------------------------------------------- 1 | param actionGroupId string 2 | param alerts array 3 | param metricNamespace string 4 | param nameSuffix string 5 | param serviceId string 6 | param tags object 7 | 8 | /* 9 | This resource block creates a metric alert for a resource. 10 | It iterates over the 'alerts' array and creates a metric alert for each alert object. 11 | The alert properties are defined based on the values provided in the alert object. 12 | */ 13 | resource alert 'Microsoft.Insights/metricAlerts@2018-03-01' = [for alert in alerts: { 14 | name: 'alert-${alert.name}-${nameSuffix}' 15 | location: 'global' 16 | tags: tags 17 | properties: { 18 | autoMitigate: true 19 | description: alert.description 20 | enabled: true 21 | evaluationFrequency: alert.evaluationFrequency 22 | scopes: [ serviceId ] 23 | severity: alert.severity 24 | windowSize: alert.windowSize 25 | 26 | actions: [ 27 | { 28 | actionGroupId: actionGroupId 29 | webHookProperties: {} 30 | } 31 | ] 32 | 33 | criteria: { 34 | 'odata.type': 'Microsoft.Azure.Monitor.MultipleResourceMultipleMetricCriteria' 35 | allOf: [ 36 | { 37 | criterionType: 'StaticThresholdCriterion' 38 | metricName: alert.metricName 39 | metricNamespace: metricNamespace 40 | name: alert.name 41 | operator: alert.operator 42 | skipMetricValidation: false 43 | threshold: alert.threshold 44 | timeAggregation: alert.timeAggregation 45 | } 46 | ] 47 | } 48 | } 49 | }] 50 | -------------------------------------------------------------------------------- /localScripts/grantRole.sh: -------------------------------------------------------------------------------- 1 | eval $(azd env get-values) 2 | 3 | functionAppId=$(az functionapp identity show --name $PROCESSING_FUNCTION_APP_NAME --resource-group $RESOURCE_GROUP | jq -r '.userAssignedIdentities[] | .principalId') 4 | echo "Function App ID: $functionAppId" 5 | scope=$(az group show --name $RESOURCE_GROUP --resource-group $RESOURCE_GROUP | jq -r '.id') 6 | echo "Scope: $scope" 7 | # Example: grant App Config Data Reader 8 | echo "Granting App Configuration Data Reader role to Function App..." 9 | az role assignment create \ 10 | --assignee-object-id $functionAppId \ 11 | --role "App Configuration Data Reader" \ 12 | --scope $scope 13 | 14 | echo "Granting Key Vault Secrets User role to Function App..." 15 | # Example: grant Key Vault Secrets User (RBAC model) 16 | az role assignment create \ 17 | --assignee-object-id $functionAppId \ 18 | --role "Key Vault Secrets User" \ 19 | --scope $scope 20 | 21 | # Grant Storage Blob Data Owner role to Function App 22 | echo "Granting Storage Blob Data Owner role to Function App..." 23 | az role assignment create \ 24 | --assignee-object-id $functionAppId \ 25 | --role "Storage Blob Data Owner" \ 26 | --scope $scope 27 | 28 | # Grant Storage Queue Data Contributor role to Function App 29 | echo "Granting Storage Queue Data Contributor role to Function App..." 30 | az role assignment create \ 31 | --assignee-object-id $functionAppId \ 32 | --role "Storage Queue Data Contributor" \ 33 | --scope $scope 34 | 35 | # Grant Storage Table Data Contributor role to Function App 36 | echo "Granting Storage Table Data Contributor role to Function App..." 37 | az role assignment create \ 38 | --assignee-object-id $functionAppId \ 39 | --role "Storage Table Data Contributor" \ 40 | --scope $scope -------------------------------------------------------------------------------- /pipeline/activities/callAoai.py: -------------------------------------------------------------------------------- 1 | import azure.durable_functions as df 2 | 3 | import logging 4 | import os 5 | from pipelineUtils.prompts import load_prompts 6 | from pipelineUtils.blob_functions import get_blob_content, write_to_blob 7 | from pipelineUtils.azure_openai import run_prompt 8 | import json 9 | 10 | name = "callAoai" 11 | bp = df.Blueprint() 12 | 13 | @bp.function_name(name) 14 | @bp.activity_trigger(input_name="inputData") 15 | def run(inputData: dict): 16 | """ 17 | Calls the Azure OpenAI service with the provided text result. 18 | 19 | Args: 20 | text_result (str): The text result to be processed by the Azure OpenAI service. 21 | 22 | Returns: 23 | str: The response from the Azure OpenAI service. 24 | """ 25 | try: 26 | # Load the prompt 27 | text_result = inputData.get('text_result') 28 | instance_id = inputData.get('instance_id') 29 | 30 | prompt_json = load_prompts() 31 | 32 | full_user_prompt = prompt_json['user_prompt'] + "\n\n" + text_result 33 | # Call the Azure OpenAI service 34 | logging.info(f"callAoai.py: Full user prompt: {full_user_prompt}") 35 | response_content = run_prompt(instance_id, prompt_json['system_prompt'], full_user_prompt) 36 | if response_content.startswith('```json') and response_content.endswith('```'): 37 | response_content = response_content.strip('`') 38 | response_content = response_content.replace('json', '', 1).strip() 39 | 40 | json_str = response_content 41 | # Return the response 42 | return json_str 43 | 44 | except Exception as e: 45 | logging.error(f"Error processing Sub Orchestration (callAoai): {instance_id}: {e}") 46 | return None -------------------------------------------------------------------------------- /infra/modules/security/managed-identity.bicep: -------------------------------------------------------------------------------- 1 | @description('Name of the resource.') 2 | param name string 3 | @description('Location to deploy the resource. Defaults to the location of the resource group.') 4 | param location string = resourceGroup().location 5 | @description('Tags for the resource.') 6 | param tags object = {} 7 | 8 | @export() 9 | @description('Role assignment information for an identity.') 10 | type roleAssignmentInfo = { 11 | @description('Role definition ID for the RBAC role to assign to the identity.') 12 | roleDefinitionId: string 13 | @description('Principal ID of the identity to assign to.') 14 | principalId: string 15 | @description('Type of the principal ID.') 16 | principalType: 'Device' | 'User' | 'Group' | 'ServicePrincipal' | 'ForeignGroup' 17 | } 18 | 19 | @export() 20 | @description('Identity information to use for role assignments.') 21 | type identityInfo = { 22 | @description('Principal ID of the identity to assign to.') 23 | principalId: string 24 | @description('Type of the principal ID.') 25 | principalType: 'Device' | 'User' | 'Group' | 'ServicePrincipal' | 'ForeignGroup' 26 | } 27 | 28 | resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = { 29 | name: name 30 | location: location 31 | tags: tags 32 | } 33 | 34 | @description('ID for the deployed Managed Identity resource.') 35 | output id string = identity.id 36 | @description('Name for the deployed Managed Identity resource.') 37 | output name string = identity.name 38 | @description('Principal ID for the deployed Managed Identity resource.') 39 | output principalId string = identity.properties.principalId 40 | @description('Client ID for the deployed Managed Identity resource.') 41 | output clientId string = identity.properties.clientId 42 | -------------------------------------------------------------------------------- /infra/modules/ai_ml/aifoundry.bicep: -------------------------------------------------------------------------------- 1 | param aiFoundryName string = 'uniquename' 2 | param aiProjectName string = '${aiFoundryName}-proj' 3 | param location string = 'eastus2' 4 | 5 | /* 6 | An AI Foundry resources is a variant of a CognitiveServices/account resource type 7 | */ 8 | resource aiFoundry 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' = { 9 | name: aiFoundryName 10 | location: location 11 | identity: { 12 | type: 'SystemAssigned' 13 | } 14 | sku: { 15 | name: 'S0' 16 | } 17 | kind: 'AIServices' 18 | properties: { 19 | // required to work in AI Foundry 20 | allowProjectManagement: true 21 | 22 | // Defines developer API endpoint subdomain 23 | customSubDomainName: aiFoundryName 24 | 25 | disableLocalAuth: true 26 | } 27 | } 28 | 29 | /* 30 | Developer APIs are exposed via a project, which groups in- and outputs that relate to one use case, including files. 31 | Its advisable to create one project right away, so development teams can directly get started. 32 | Projects may be granted individual RBAC permissions and identities on top of what account provides. 33 | */ 34 | resource aiProject 'Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview' = { 35 | name: aiProjectName 36 | parent: aiFoundry 37 | location: location 38 | identity: { 39 | type: 'SystemAssigned' 40 | } 41 | properties: {} 42 | } 43 | 44 | /* 45 | Optionally deploy a model to use in playground, agents and other tools. 46 | */ 47 | resource modelDeployment 'Microsoft.CognitiveServices/accounts/deployments@2024-10-01'= { 48 | parent: aiFoundry 49 | name: 'gpt-4o' 50 | sku : { 51 | capacity: 1 52 | name: 'GlobalStandard' 53 | } 54 | properties: { 55 | model:{ 56 | name: 'gpt-4o' 57 | format: 'OpenAI' 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /infra/modules/util/dnsZoneData.bicep: -------------------------------------------------------------------------------- 1 | /** Inputs **/ 2 | param location string 3 | 4 | /** Locals **/ 5 | @description('Private DNS Zones to read.') 6 | var privateDnsZone = { 7 | agentsvc: 'privatelink.agentsvc.azure-automation.net' 8 | aks: 'privatelink.${location}.azmk8s.io' 9 | blob: 'privatelink.blob.${environment().suffixes.storage}' 10 | cognitiveservices: 'privatelink.cognitiveservices.azure.com' 11 | configuration_stores: 'privatelink.azconfig.io' 12 | cosmosdb: 'privatelink.documents.azure.com' 13 | cr: 'privatelink.azurecr.io' 14 | cr_region: '${location}.privatelink.azurecr.io' 15 | dfs: 'privatelink.dfs.${environment().suffixes.storage}' 16 | eventgrid: 'privatelink.eventgrid.azure.net' 17 | file: 'privatelink.file.${environment().suffixes.storage}' 18 | monitor: 'privatelink.monitor.azure.com' 19 | ods: 'privatelink.ods.opinsights.azure.com' 20 | oms: 'privatelink.oms.opinsights.azure.com' 21 | openai: 'privatelink.openai.azure.com' 22 | queue: 'privatelink.queue.${environment().suffixes.storage}' 23 | search: 'privatelink.search.windows.net' 24 | sites: 'privatelink.azurewebsites.net' 25 | sql_server: 'privatelink${environment().suffixes.sqlServerHostname}' 26 | table: 'privatelink.table.${environment().suffixes.storage}' 27 | vault: 'privatelink.vaultcore.azure.net' 28 | } 29 | 30 | var zoneIds = [for (zone, i) in items(privateDnsZone): { 31 | id: main[i].id 32 | key: zone.key 33 | name: main[i].name 34 | }] 35 | 36 | /** Outputs **/ 37 | @description('Private DNS Zones to use in other modules.') 38 | output ids array = zoneIds 39 | 40 | @description('Private DNS Zones for Storage Accounts') 41 | output idsStorage array = filter( 42 | zoneIds, 43 | (zone) => contains([ 'blob', 'dfs', 'file', 'queue', 'table', 'web' ], zone.key) 44 | ) 45 | 46 | /** Nested Modules **/ 47 | @description('Read the specified private DNS zones.') 48 | resource main 'Microsoft.Network/privateDnsZones@2024-06-01' existing = [for zone in items(privateDnsZone): { 49 | name: zone.value 50 | }] 51 | -------------------------------------------------------------------------------- /pipeline/requirements.txt: -------------------------------------------------------------------------------- 1 | aiohappyeyeballs==2.6.1 2 | aiohttp==3.12.14 3 | aiosignal==1.4.0 4 | annotated-types==0.7.0 5 | anyio==4.9.0 6 | asttokens==3.0.1 7 | async-timeout==5.0.1 8 | attrs==25.3.0 9 | azure-ai-documentintelligence==1.0.2 10 | azure-ai-textanalytics==5.2.0 11 | azure-appconfiguration==1.7.1 12 | azure-appconfiguration-provider==2.0.1 13 | azure-common==1.1.28 14 | azure-core==1.32.0 15 | azure-cosmos==4.9.0 16 | azure-functions==1.21.3 17 | azure-functions-durable==1.2.10 18 | azure-identity==1.19.0 19 | azure-keyvault-secrets==4.10.0 20 | azure-storage-blob==12.24.0 21 | certifi==2025.1.31 22 | cffi==1.17.1 23 | charset-normalizer==3.4.1 24 | comm==0.2.3 25 | cryptography==44.0.2 26 | debugpy==1.8.17 27 | decorator==5.2.1 28 | distro==1.9.0 29 | dnspython==2.8.0 30 | exceptiongroup==1.2.2 31 | executing==2.2.1 32 | frozenlist==1.5.0 33 | furl==2.1.4 34 | h11==0.16.0 35 | httpcore==1.0.9 36 | httpx==0.28.1 37 | idna==3.10 38 | ipykernel==7.1.0 39 | ipython==9.7.0 40 | ipython_pygments_lexers==1.1.1 41 | isodate==0.7.2 42 | jedi==0.19.2 43 | jiter==0.9.0 44 | jupyter_client==8.6.3 45 | jupyter_core==5.9.1 46 | matplotlib-inline==0.2.1 47 | msal==1.32.0 48 | msal-extensions==1.3.1 49 | msrest==0.7.1 50 | multidict==6.3.0 51 | nest-asyncio==1.6.0 52 | oauthlib==3.3.1 53 | openai==1.70.0 54 | orderedmultidict==1.0.1 55 | packaging==25.0 56 | parso==0.8.5 57 | pexpect==4.9.0 58 | platformdirs==4.5.0 59 | prompt_toolkit==3.0.52 60 | propcache==0.3.1 61 | psutil==7.1.3 62 | ptyprocess==0.7.0 63 | pure_eval==0.2.3 64 | pycparser==2.22 65 | pydantic==2.11.2 66 | pydantic_core==2.33.1 67 | Pygments==2.19.2 68 | PyJWT==2.10.1 69 | PyMuPDF==1.26.6 70 | PyPDF2==3.0.1 71 | python-dateutil==2.9.0.post0 72 | PyYAML==6.0.2 73 | pyzmq==27.1.0 74 | requests==2.32.3 75 | requests-oauthlib==2.0.0 76 | six==1.17.0 77 | sniffio==1.3.1 78 | stack-data==0.6.3 79 | tenacity==9.0.0 80 | tornado==6.5.2 81 | tqdm==4.67.1 82 | traitlets==5.14.3 83 | typing-inspection==0.4.0 84 | typing_extensions==4.13.0 85 | urllib3==2.5.0 86 | wcwidth==0.2.14 87 | yarl==1.18.3 88 | -------------------------------------------------------------------------------- /infra/deployment-outputs.json: -------------------------------------------------------------------------------- 1 | { 2 | "aimultiserviceS_ENDPOINT": { 3 | "type": "String", 4 | "value": "https://aimsa-2g7fenk3mteey.cognitiveservices.azure.com/" 5 | }, 6 | "aimultiserviceS_NAME": { 7 | "type": "String", 8 | "value": "aimsa-2g7fenk3mteey" 9 | }, 10 | "apP_CONFIG_NAME": { 11 | "type": "String", 12 | "value": "appconfig-2g7fenk3mteey" 13 | }, 14 | "azurE_STORAGE_ACCOUNT": { 15 | "type": "String", 16 | "value": "st2g7fenk3mteey" 17 | }, 18 | "cosmoS_DB_ACCOUNT_NAME": { 19 | "type": "String", 20 | "value": "cosmos-2g7fenk3mteey" 21 | }, 22 | "cosmoS_DB_CONVERSATION_CONTAINER": { 23 | "type": "String", 24 | "value": "conversationhistory" 25 | }, 26 | "cosmoS_DB_DATABASE_NAME": { 27 | "type": "String", 28 | "value": "conversationHistoryDB" 29 | }, 30 | "cosmoS_DB_URI": { 31 | "type": "String", 32 | "value": "https://cosmos-2g7fenk3mteey.documents.azure.com:443/" 33 | }, 34 | "functioN_APP_NAME": { 35 | "type": "String", 36 | "value": "func-processing-2g7fenk3mteey" 37 | }, 38 | "functioN_STORAGE_ACCOUNT": { 39 | "type": "String", 40 | "value": "st2g7fenk3mteeyfunc" 41 | }, 42 | "functionS_WORKER_RUNTIME": { 43 | "type": "String", 44 | "value": "python" 45 | }, 46 | "keY_VAULT_NAME": { 47 | "type": "String", 48 | "value": "kv-2g7fenk3mteey" 49 | }, 50 | "openaI_API_BASE": { 51 | "type": "String", 52 | "value": "https://oai-2g7fenk3mteey.openai.azure.com/" 53 | }, 54 | "openaI_API_VERSION": { 55 | "type": "String", 56 | "value": "2024-05-01-preview" 57 | }, 58 | "openaI_MODEL": { 59 | "type": "String", 60 | "value": "gpt-4o" 61 | }, 62 | "processinG_FUNCTION_APP_NAME": { 63 | "type": "String", 64 | "value": "func-processing-2g7fenk3mteey" 65 | }, 66 | "processinG_FUNCTION_URL": { 67 | "type": "String", 68 | "value": "https://func-processing-2g7fenk3mteey.azurewebsites.net" 69 | }, 70 | "resourcE_GROUP": { 71 | "type": "String", 72 | "value": "rg-dev" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /pipeline/pipelineUtils/blob_functions.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | from dataclasses import dataclass 4 | import json 5 | 6 | from azure.identity import DefaultAzureCredential 7 | from azure.storage.blob import BlobServiceClient 8 | 9 | from configuration import Configuration 10 | config = Configuration() 11 | 12 | BLOB_ENDPOINT=config.get_value("DATA_STORAGE_ENDPOINT") 13 | 14 | # if os.environ.get("AZURE_FUNCTIONS_ENVIRONMENT") == "Development": 15 | # BLOB_ENDPOINT = os.getenv("AzureWebJobsStorage") 16 | 17 | token = config.credential.get_token("https://storage.azure.com/.default") 18 | 19 | blob_service_client = BlobServiceClient(account_url=BLOB_ENDPOINT, credential=config.credential) 20 | 21 | @dataclass 22 | class BlobMetadata: 23 | name: str 24 | uri: str 25 | container: str 26 | 27 | def to_dict(self): 28 | return {"name": self.name, "uri": self.uri, "container": self.container} 29 | 30 | def to_json(self): 31 | return json.dumps(self.to_dict(), ensure_ascii=False) 32 | 33 | 34 | def write_to_blob(container_name, blob_path, data): 35 | 36 | blob_client = blob_service_client.get_blob_client(container=container_name, blob=blob_path) 37 | blob_client.upload_blob(data, overwrite=True) 38 | return True 39 | 40 | def get_blob_content(container_name, blob_path): 41 | 42 | blob_client = blob_service_client.get_blob_client(container=container_name, blob=blob_path) 43 | # Download the blob content 44 | blob_content = blob_client.download_blob().readall() 45 | return blob_content 46 | 47 | def list_blobs(container_name): 48 | container_client = blob_service_client.get_container_client(container_name) 49 | blob_list = container_client.list_blobs() 50 | return blob_list 51 | 52 | def delete_all_blobs_in_container(container_name): 53 | container_client = blob_service_client.get_container_client(container_name) 54 | blob_list = container_client.list_blobs() 55 | for blob in blob_list: 56 | blob_client = container_client.get_blob_client(blob.name) 57 | blob_client.delete_blob() -------------------------------------------------------------------------------- /pipeline/pipelineUtils/prompts.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from pipelineUtils.blob_functions import get_blob_content 4 | import yaml 5 | import logging 6 | 7 | from configuration import Configuration 8 | config = Configuration() 9 | 10 | def load_prompts_from_blob(prompt_file): 11 | """Load the prompt from YAML file in blob storage and return as a dictionary.""" 12 | try: 13 | prompt_yaml = get_blob_content("prompts", prompt_file).decode('utf-8') 14 | prompts = yaml.safe_load(prompt_yaml) 15 | prompts_json = json.dumps(prompts, indent=4) 16 | prompts = json.loads(prompts_json) 17 | return prompts 18 | except Exception as e: 19 | raise RuntimeError(f"Failed to load prompts file: {prompt_file} from blob storage. Prompt File should be a valid Blob path stored in the prompts container. Error: {e}") 20 | 21 | 22 | def load_prompts_from_blob(prompt_file): 23 | """Load the prompt from YAML file in blob storage and return as a dictionary.""" 24 | try: 25 | prompt_yaml = get_blob_content("prompts", prompt_file).decode('utf-8') 26 | prompts = yaml.safe_load(prompt_yaml) 27 | prompts_json = json.dumps(prompts, indent=4) 28 | prompts = json.loads(prompts_json) 29 | return prompts 30 | except Exception as e: 31 | raise RuntimeError(f"Failed to load prompts from blob storage: {e}") 32 | 33 | 34 | def load_prompts(): 35 | """Fetch prompts JSON from blob storage and return as a dictionary.""" 36 | prompt_file = config.get_value("PROMPT_FILE") 37 | 38 | if not prompt_file: 39 | raise ValueError("Environment variable PROMPT_FILE is not set.") 40 | 41 | if prompt_file=="COSMOS": 42 | prompts = load_prompts_from_cosmos() 43 | else: 44 | prompts = load_prompts_from_blob(prompt_file) 45 | 46 | # Validate required fields 47 | required_keys = ["system_prompt", "user_prompt"] 48 | for key in required_keys: 49 | if key not in prompts: 50 | raise KeyError(f"Missing required prompt key: {key}") 51 | 52 | return prompts -------------------------------------------------------------------------------- /pipeline/activities/writeToBlob.py: -------------------------------------------------------------------------------- 1 | import azure.durable_functions as df 2 | import logging 3 | from pipelineUtils.blob_functions import list_blobs, get_blob_content, write_to_blob 4 | import os 5 | 6 | from configuration import Configuration 7 | config = Configuration() 8 | 9 | FINAL_OUTPUT_CONTAINER = config.get_value("FINAL_OUTPUT_CONTAINER") 10 | 11 | logging.info(f"writeToBlob.py: FINAL_OUTPUT_CONTAINER is {FINAL_OUTPUT_CONTAINER}") 12 | 13 | name = "writeToBlob" 14 | bp = df.Blueprint() 15 | 16 | @bp.function_name(name) 17 | @bp.activity_trigger(input_name="args") 18 | def write_to_blob_activity(args: dict): 19 | """ 20 | Writes the JSON bytes to a blob storage. 21 | Args: 22 | args (dict): A dictionary containing the blob name and JSON bytes. 23 | """ 24 | try: 25 | # Parse arguments 26 | blob_name = args['blob_name'] 27 | final_output_container = args['final_output_container'] 28 | json_str = args['json_str'] 29 | 30 | args['json_bytes'] = json_str.encode('utf-8') 31 | 32 | sourcefile = os.path.splitext(os.path.basename(blob_name))[0] 33 | logging.info(f"writeToBlob.py: Writing output to blob {sourcefile}-output.json with source file {sourcefile} and FINAL_OUTPUT_CONTAINER {final_output_container}") 34 | result = write_to_blob(final_output_container, f"{sourcefile}-output.json", args['json_bytes']) 35 | logging.info(f"writeToBlob.py: Result of write_to_blob: {result}") 36 | if result: 37 | logging.info(f"writeToBlob.py: Successfully wrote output to blob {blob_name}") 38 | return { 39 | "success": True, 40 | "blob_name": blob_name, 41 | "output_blob": f"{sourcefile}-output.json" 42 | } 43 | else: 44 | logging.error(f"Failed to write output to blob {blob_name}") 45 | return { 46 | "success": False, 47 | "error": "Failed to write output" 48 | } 49 | except Exception as e: 50 | error_msg = f"Error writing output for blob {blob_name}: {str(e)}" 51 | logging.error(error_msg) 52 | return { 53 | "success": False, 54 | "error": error_msg 55 | } 56 | -------------------------------------------------------------------------------- /infra/modules/app_config/appconfig.bicep: -------------------------------------------------------------------------------- 1 | /** Inputs **/ 2 | @description('Location for all resources') 3 | param name string 4 | 5 | @description('MSI id for resource.') 6 | param identityId string? 7 | 8 | @description('Location for all resources') 9 | param location string 10 | 11 | @description('Resource suffix for all resources') 12 | param resourceToken string 13 | 14 | @description('Tags for all resources') 15 | param tags object 16 | 17 | @description('Keys to add to App Configuration') 18 | param appSettings array 19 | 20 | @description('Secret Keys to add to App Configuration') 21 | param secureAppSettings array 22 | 23 | @description('Whether to enable public network access. Defaults to Enabled.') 24 | @allowed([ 25 | 'Enabled' 26 | 'Disabled' 27 | ]) 28 | param publicNetworkAccess string = 'Enabled' 29 | 30 | @description('App Configuration') 31 | resource main 'Microsoft.AppConfiguration/configurationStores@2024-05-01' = { 32 | identity: { 33 | type: identityId == null ? 'SystemAssigned' : 'UserAssigned' 34 | userAssignedIdentities: identityId == null 35 | ? null 36 | : { 37 | '${identityId}': {} 38 | } 39 | } 40 | location: location 41 | name: name 42 | properties: { 43 | disableLocalAuth: false 44 | enablePurgeProtection: true 45 | encryption: {} 46 | publicNetworkAccess: publicNetworkAccess 47 | softDeleteRetentionInDays: 7 48 | } 49 | sku: { 50 | name: 'standard' 51 | } 52 | tags: tags 53 | } 54 | 55 | resource keyValue 'Microsoft.AppConfiguration/configurationStores/keyValues@2024-05-01' = [for (config, i) in appSettings: { 56 | parent: main 57 | name: config.name 58 | properties: { 59 | contentType: '' 60 | tags: {} 61 | value: config.value 62 | } 63 | } 64 | ] 65 | 66 | resource secureKeyValue 'Microsoft.AppConfiguration/configurationStores/keyValues@2024-05-01' = [for (config, i) in secureAppSettings: { 67 | parent: main 68 | name: config.name 69 | properties: { 70 | contentType: 'application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8' 71 | tags: {} 72 | value: config.value 73 | } 74 | } 75 | ] 76 | 77 | @description('App Configuration resource Id') 78 | output id string = main.id 79 | @description('App Configuration resource Name') 80 | output name string = main.name 81 | @description('App Configuration resource EndPoint') 82 | output endpoint string = main.properties.endpoint 83 | -------------------------------------------------------------------------------- /infra/modules/security/resource-role-assignment.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 3 | "languageVersion": "2.0", 4 | "contentVersion": "1.0.0.0", 5 | "definitions": { 6 | "roleAssignmentInfo": { 7 | "type": "object", 8 | "properties": { 9 | "roleDefinitionId": { 10 | "type": "string", 11 | "metadata": { 12 | "description": "Role definition ID for the RBAC role to assign to the identity." 13 | } 14 | }, 15 | "principalId": { 16 | "type": "string", 17 | "metadata": { 18 | "description": "Principal ID of the identity to assign to." 19 | } 20 | }, 21 | "principalType": { 22 | "type": "string", 23 | "allowedValues": [ 24 | "Device", 25 | "ForeignGroup", 26 | "Group", 27 | "ServicePrincipal", 28 | "User" 29 | ], 30 | "metadata": { 31 | "description": "Type of the principal ID." 32 | } 33 | } 34 | }, 35 | "metadata": { 36 | "description": "Role assignment information for an identity." 37 | } 38 | } 39 | }, 40 | "parameters": { 41 | "resourceId": { 42 | "type": "string", 43 | "metadata": { 44 | "description": "Resource ID of the resource to assign the role to." 45 | } 46 | }, 47 | "roleAssignments": { 48 | "type": "array", 49 | "items": { 50 | "$ref": "#/definitions/roleAssignmentInfo" 51 | }, 52 | "defaultValue": [], 53 | "metadata": { 54 | "description": "Role assignments to create for the resource." 55 | } 56 | } 57 | }, 58 | "resources": { 59 | "assignment": { 60 | "copy": { 61 | "name": "assignment", 62 | "count": "[length(parameters('roleAssignments'))]" 63 | }, 64 | "type": "Microsoft.Authorization/roleAssignments", 65 | "apiVersion": "2022-04-01", 66 | "name": "[guid(parameters('resourceId'), parameters('roleAssignments')[copyIndex()].principalId, parameters('roleAssignments')[copyIndex()].roleDefinitionId)]", 67 | "scope": "[parameters('resourceId')]", 68 | "properties": { 69 | "principalId": "[parameters('roleAssignments')[copyIndex()].principalId]", 70 | "roleDefinitionId": "[parameters('roleAssignments')[copyIndex()].roleDefinitionId]", 71 | "principalType": "[parameters('roleAssignments')[copyIndex()].principalType]" 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /pipeline/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # pipenv 86 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 87 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 88 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not 89 | # install all needed dependencies. 90 | #Pipfile.lock 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | .venv 101 | env/ 102 | venv/ 103 | ENV/ 104 | env.bak/ 105 | venv.bak/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | .spyproject 110 | 111 | # Rope project settings 112 | .ropeproject 113 | 114 | # mkdocs documentation 115 | /site 116 | 117 | # mypy 118 | .mypy_cache/ 119 | .dmypy.json 120 | dmypy.json 121 | 122 | # Pyre type checker 123 | .pyre/ 124 | 125 | # Azure Functions artifacts 126 | bin 127 | obj 128 | appsettings.json 129 | local.settings.json 130 | 131 | # Azurite artifacts 132 | __blobstorage__ 133 | __queuestorage__ 134 | __azurite_db*__.json 135 | .python_packages -------------------------------------------------------------------------------- /pipeline/activities/runDocIntel.py: -------------------------------------------------------------------------------- 1 | import azure.durable_functions as df 2 | import logging 3 | from pipelineUtils.blob_functions import list_blobs, get_blob_content, write_to_blob 4 | from pipelineUtils import get_month_date 5 | # Libraries used in the future Document Processing client code 6 | from azure.identity import DefaultAzureCredential 7 | from azure.ai.documentintelligence import DocumentIntelligenceClient 8 | from azure.ai.documentintelligence.models import AnalyzeResult, AnalyzeDocumentRequest 9 | import base64 10 | import json 11 | import os 12 | import requests 13 | 14 | from configuration import Configuration 15 | config = Configuration() 16 | 17 | 18 | name = "runDocIntel" 19 | bp = df.Blueprint() 20 | 21 | def normalize_blob_name(container: str, raw_name: str) -> str: 22 | """Strip container prefix if included in the name.""" 23 | if raw_name.startswith(container + "/"): 24 | return raw_name[len(container) + 1:] 25 | return raw_name 26 | 27 | @bp.function_name(name) 28 | @bp.activity_trigger(input_name="blob_input") 29 | def extract_text_from_blob(blob_input: dict): 30 | 31 | blob_name = blob_input.get('name') 32 | container = blob_input.get('container') 33 | 34 | endpoint = config.get_value("AIMULTISERVICES_ENDPOINT") # Add the AI Services Endpoint value from Azure Function App settings 35 | 36 | try: 37 | 38 | client = DocumentIntelligenceClient( 39 | endpoint=endpoint, credential=config.credential 40 | ) 41 | 42 | normalized_blob_name = normalize_blob_name(container, blob_name) 43 | logging.info(f"Normalized Blob Name: {normalized_blob_name}") 44 | blob_content = get_blob_content( 45 | container_name=blob_input["container"], 46 | blob_path=normalized_blob_name 47 | ) 48 | 49 | 50 | logging.info(f"Starting analyze document: {blob_content[:100]}...") # Log the first 50 bytes of the file for debugging}") 51 | poller = client.begin_analyze_document( 52 | # AnalyzeDocumentRequest Class: https://learn.microsoft.com/en-us/python/api/azure-ai-documentintelligence/azure.ai.documentintelligence.models.analyzedocumentrequest?view=azure-python 53 | "prebuilt-read", AnalyzeDocumentRequest(bytes_source=blob_content) 54 | ) 55 | 56 | result: AnalyzeResult = poller.result() 57 | logging.info(f"Analyze document completed with status: {result}") 58 | if result.paragraphs: 59 | paragraphs = "\n".join([paragraph.content for paragraph in result.paragraphs]) 60 | 61 | return paragraphs 62 | 63 | except Exception as e: 64 | logging.error(f"Error processing {blob_input}: {e}") 65 | return None 66 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | localScripts/* 2 | *.bicepparam 3 | local.settings.json 4 | oldsettings.json 5 | commandUtils 6 | *.ipynb 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | # C extensions 12 | *.so 13 | service_principal 14 | check_status.py 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | pip-wheel-metadata/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | transcribe/ 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | utility_commands 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | .hypothesis/ 57 | .pytest_cache/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # celery beat schedule file 99 | celerybeat-schedule 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | infra-2 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Azure Functions artifacts 132 | bin 133 | obj 134 | appsettings.json 135 | local.settings.json 136 | 137 | # Azurite artifacts 138 | __blobstorage__ 139 | __queuestorage__ 140 | __azurite_db*__.json 141 | .python_packages 142 | .azure 143 | remote_appsettings.json -------------------------------------------------------------------------------- /pipeline/pipelineUtils/azure_openai.py: -------------------------------------------------------------------------------- 1 | from openai import AzureOpenAI 2 | import os 3 | import logging 4 | from azure.identity import DefaultAzureCredential, get_bearer_token_provider 5 | from pipelineUtils.db import save_chat_message 6 | from configuration import Configuration 7 | config = Configuration() 8 | 9 | # OPENAI_API_KEY = config.get_value("OPENAI_API_KEY") 10 | OPENAI_API_BASE = config.get_value("OPENAI_API_BASE") 11 | OPENAI_MODEL = config.get_value("OPENAI_MODEL") 12 | OPENAI_API_VERSION = config.get_value("OPENAI_API_VERSION") 13 | OPENAI_API_EMBEDDING_MODEL = config.get_value("OPENAI_API_EMBEDDING_MODEL") 14 | 15 | def get_embeddings(text): 16 | token_provider = get_bearer_token_provider( 17 | config.credential, 18 | "https://cognitiveservices.azure.com/.default" 19 | ) 20 | 21 | token = config.credential.get_token("https://cognitiveservices.azure.com/.default").token 22 | openai_client = AzureOpenAI( 23 | azure_ad_token=token, 24 | api_version = OPENAI_API_VERSION, 25 | azure_endpoint =OPENAI_API_BASE 26 | ) 27 | 28 | embedding = openai_client.embeddings.create( 29 | input = text, 30 | model= OPENAI_API_EMBEDDING_MODEL 31 | ).data[0].embedding 32 | 33 | return embedding 34 | 35 | 36 | def run_prompt(pipeline_id, system_prompt, user_prompt): 37 | token_provider = get_bearer_token_provider( 38 | config.credential, 39 | "https://cognitiveservices.azure.com/.default" 40 | ) 41 | 42 | token = config.credential.get_token("https://cognitiveservices.azure.com/.default").token 43 | 44 | openai_client = AzureOpenAI( 45 | azure_ad_token=token, 46 | api_version = OPENAI_API_VERSION, 47 | azure_endpoint =OPENAI_API_BASE 48 | ) 49 | 50 | logging.info(f"User Prompt: {user_prompt}") 51 | logging.info(f"System Prompt: {system_prompt}") 52 | 53 | save_chat_message(pipeline_id, "system", system_prompt) 54 | save_chat_message(pipeline_id, "user", user_prompt) 55 | 56 | try: 57 | response = openai_client.chat.completions.create( 58 | model=OPENAI_MODEL, 59 | messages=[{ "role": "system", "content": system_prompt}, 60 | {"role":"user","content":user_prompt}]) 61 | assistant_msg = response.choices[0].message.content 62 | usage = { 63 | "prompt_tokens": response.usage.prompt_tokens, 64 | "completion_tokens": response.usage.completion_tokens, 65 | "total_tokens": response.usage.total_tokens, 66 | "model": response.model 67 | } 68 | 69 | # 2) log the assistant’s response + usage 70 | save_chat_message(pipeline_id, "assistant", assistant_msg, usage) 71 | return assistant_msg 72 | 73 | except Exception as e: 74 | logging.error(f"Error calling OpenAI API: {e}") 75 | return None 76 | 77 | 78 | -------------------------------------------------------------------------------- /infra/modules/management_governance/application-insights.bicep: -------------------------------------------------------------------------------- 1 | @description('Name of the resource.') 2 | param name string 3 | @description('Location to deploy the resource. Defaults to the location of the resource group.') 4 | param location string = resourceGroup().location 5 | @description('Tags for the resource.') 6 | param tags object = {} 7 | 8 | param appInsightsReuse bool 9 | param logAnalyticsReuse bool 10 | param existingAppInsightsResourceGroupName string 11 | 12 | param publicNetworkAccessForIngestion string = 'Enabled' 13 | param publicNetworkAccessForQuery string = 'Enabled' 14 | 15 | param suffix string 16 | 17 | @description('Name for the Log Analytics Workspace resource associated with the Application Insights instance.') 18 | param logAnalyticsWorkspaceId string 19 | 20 | var abbrs = loadJsonContent('../../abbreviations.json') 21 | 22 | resource existinglogAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' existing = { 23 | name: '${abbrs.managementGovernance.logAnalyticsWorkspace}${suffix}' 24 | } 25 | 26 | resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' = if ( !appInsightsReuse && empty(logAnalyticsWorkspaceId) ) { 27 | name: '${abbrs.managementGovernance.logAnalyticsWorkspace}${suffix}' 28 | location: location 29 | properties: { 30 | sku: { 31 | name: 'pergb2018' 32 | } 33 | retentionInDays: 30 34 | } 35 | } 36 | 37 | // If reusing an existing App Insights resource, reference it (assumed to already be workspace‐based) 38 | resource existingApplicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (appInsightsReuse) { 39 | scope: resourceGroup(existingAppInsightsResourceGroupName) 40 | name: name 41 | } 42 | 43 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { 44 | name: name 45 | location: location 46 | tags: tags 47 | kind: 'web' 48 | properties: { 49 | Application_Type: 'web' 50 | WorkspaceResourceId: appInsightsReuse ? existinglogAnalyticsWorkspace.id : logAnalyticsWorkspace.id 51 | publicNetworkAccessForIngestion: publicNetworkAccessForIngestion 52 | publicNetworkAccessForQuery: publicNetworkAccessForQuery 53 | } 54 | } 55 | 56 | @description('ID for the deployed Application Insights resource.') 57 | output id string = appInsightsReuse ? existingApplicationInsights.id : applicationInsights.id 58 | @description('Name for the deployed Application Insights resource.') 59 | output name string = appInsightsReuse ? existingApplicationInsights.name : applicationInsights.name 60 | @description('Instrumentation Key for the deployed Application Insights resource.') 61 | output instrumentationKey string = appInsightsReuse ? existingApplicationInsights.properties.InstrumentationKey : applicationInsights.properties.InstrumentationKey 62 | @description('Connection string for the deployed Application Insights resource.') 63 | output connectionString string = appInsightsReuse ? existingApplicationInsights.properties.ConnectionString : applicationInsights.properties.ConnectionString 64 | -------------------------------------------------------------------------------- /pipeline/activities/callAoaiMultiModal.py: -------------------------------------------------------------------------------- 1 | import azure.durable_functions as df 2 | 3 | from pipelineUtils.prompts import load_prompts 4 | from pipelineUtils.blob_functions import get_blob_content, write_to_blob 5 | from pipelineUtils.azure_openai import run_prompt 6 | import base64 7 | import json 8 | import fitz # PyMuPDF 9 | from PyPDF2 import PdfReader, PdfWriter # 👈 for PDF trimming 10 | import logging 11 | 12 | from pipelineUtils.prompts import load_prompts 13 | from pipelineUtils.blob_functions import get_blob_content, write_to_blob 14 | from pipelineUtils.azure_openai import run_prompt 15 | 16 | name = "callAoaiMultimodal" 17 | bp = df.Blueprint() 18 | 19 | def convert_to_base64_images(blob_input: dict): 20 | """Convert PDF pages or PNG image to base64-encoded images.""" 21 | blob_name = blob_input.get("name") 22 | container = blob_input.get('container') 23 | blob_content = get_blob_content( 24 | container_name=container, 25 | blob_path=blob_name 26 | ) 27 | 28 | if blob_name.lower().endswith('.pdf'): 29 | # Process PDF: Convert each page to base64-encoded image 30 | try: 31 | 32 | base64_images = [] 33 | with fitz.open(stream=blob_content, filetype='pdf') as doc: 34 | for page in doc: 35 | # Render page to a pixmap (image) 36 | pix = page.get_pixmap() 37 | # Convert pixmap to PNG bytes 38 | img_bytes = pix.tobytes("png") 39 | # Encode to base64 40 | b64 = base64.b64encode(img_bytes).decode("utf-8") 41 | base64_images.append(b64) 42 | 43 | except Exception as e: 44 | logging.error(f"[Silver] PDF trimming or encoding failed: {e}") 45 | raise 46 | 47 | elif blob_name.lower().endswith('.png'): 48 | 49 | # Process PNG: Directly encode the image to base64 50 | try: 51 | b64 = base64.b64encode(blob_content).decode("utf-8") 52 | base64_images = [b64] 53 | 54 | except Exception as e: 55 | logging.error(f"[Silver] PNG encoding failed: {e}") 56 | raise 57 | return base64_images 58 | 59 | @bp.function_name(name) 60 | @bp.activity_trigger(input_name="blob_input") 61 | def run(blob_input: dict): 62 | # Parse args 63 | blob_name = blob_input.get("name") 64 | container = blob_input.get('container') 65 | instance_id = blob_input.get('instance_id', '') 66 | 67 | blob_content = get_blob_content( 68 | container_name=container, 69 | blob_path=blob_name 70 | ) 71 | 72 | 73 | base64_images = convert_to_base64_images(blob_input) 74 | 75 | prompt_json = load_prompts() 76 | 77 | system_prompt = prompt_json['system_prompt'] 78 | 79 | full_user_prompt = ( 80 | f"{prompt_json['user_prompt']}\n\n" 81 | ) 82 | response_content = run_prompt(instance_id, system_prompt, full_user_prompt, base64_images=base64_images) 83 | 84 | return response_content -------------------------------------------------------------------------------- /infra/modules/containers/container-registry.bicep: -------------------------------------------------------------------------------- 1 | import { roleAssignmentInfo } from '../security/managed-identity.bicep' 2 | 3 | @description('Name of the resource.') 4 | param name string 5 | @description('Location to deploy the resource. Defaults to the location of the resource group.') 6 | param location string = resourceGroup().location 7 | @description('Tags for the resource.') 8 | param tags object = {} 9 | @description('MSI Id.') 10 | param identityId string? 11 | 12 | param containerRegistryReuse bool 13 | param existingContainerRegistryResourceGroupName string 14 | 15 | @export() 16 | @description('SKU information for Container Registry.') 17 | type skuInfo = { 18 | @description('Name of the SKU.') 19 | name: 'Basic' | 'Premium' | 'Standard' 20 | } 21 | 22 | @description('Whether to enable an admin user that has push and pull access. Defaults to false.') 23 | param adminUserEnabled bool = false 24 | @description('Whether to allow public network access. Defaults to Enabled.') 25 | @allowed([ 26 | 'Disabled' 27 | 'Enabled' 28 | ]) 29 | param publicNetworkAccess string = 'Enabled' 30 | @description('Container Registry SKU. Defaults to Basic.') 31 | param sku skuInfo = { 32 | name: 'Basic' 33 | } 34 | @description('Role assignments to create for the Container Registry.') 35 | param roleAssignments roleAssignmentInfo[] = [] 36 | 37 | resource existingContainerRegistry 'Microsoft.ContainerRegistry/registries@2024-11-01-preview' existing = if (containerRegistryReuse) { 38 | scope: resourceGroup(existingContainerRegistryResourceGroupName) 39 | name: name 40 | } 41 | 42 | resource containerRegistry 'Microsoft.ContainerRegistry/registries@2024-11-01-preview' = if (!containerRegistryReuse) { 43 | name: name 44 | location: location 45 | tags: tags 46 | identity: { 47 | type: identityId == null ? 'SystemAssigned' : 'UserAssigned' 48 | userAssignedIdentities: identityId == null 49 | ? null 50 | : { 51 | '${identityId}': {} 52 | } 53 | } 54 | sku: sku 55 | properties: { 56 | adminUserEnabled: adminUserEnabled 57 | publicNetworkAccess: publicNetworkAccess 58 | } 59 | } 60 | 61 | resource assignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = [ 62 | for roleAssignment in roleAssignments: { 63 | name: guid(containerRegistry.id, roleAssignment.principalId, roleAssignment.roleDefinitionId) 64 | scope: containerRegistry 65 | properties: { 66 | principalId: roleAssignment.principalId 67 | roleDefinitionId: roleAssignment.roleDefinitionId 68 | principalType: roleAssignment.principalType 69 | } 70 | } 71 | ] 72 | 73 | @description('ID for the deployed Container Registry resource.') 74 | output id string = containerRegistryReuse ? existingContainerRegistry.id: containerRegistry.id 75 | @description('Name for the deployed Container Registry resource.') 76 | output name string = containerRegistryReuse ? existingContainerRegistry.name : containerRegistry.name 77 | @description('Login server for the deployed Container Registry resource.') 78 | output loginServer string = containerRegistryReuse ? existingContainerRegistry.properties.loginServer : containerRegistry.properties.loginServer 79 | -------------------------------------------------------------------------------- /infra/modules/storage/storage-private-endpoints.bicep: -------------------------------------------------------------------------------- 1 | @description('Name of the resource.') 2 | param name string 3 | @description('Location to deploy the resource. Defaults to the location of the resource group.') 4 | param location string = resourceGroup().location 5 | @description('Tags for the resource.') 6 | param tags object = {} 7 | 8 | param vnetName string 9 | 10 | var abbrs = loadJsonContent('../../abbreviations.json') 11 | var roles = loadJsonContent('../../roles.json') 12 | 13 | resource storage 'Microsoft.Storage/storageAccounts@2023-01-01' existing = { 14 | name: name 15 | } 16 | 17 | resource vnet 'Microsoft.Network/virtualNetworks@2024-05-01' existing = { 18 | name: vnetName 19 | } 20 | 21 | resource blobDnsZone 'Microsoft.Network/privateDnsZones@2024-06-01' existing = { 22 | name: 'privatelink.blob.${environment().suffixes.storage}' 23 | } 24 | 25 | resource tableDnsZone 'Microsoft.Network/privateDnsZones@2024-06-01' existing = { 26 | name: 'privatelink.table.${environment().suffixes.storage}' 27 | } 28 | 29 | resource queueDnsZone 'Microsoft.Network/privateDnsZones@2024-06-01' existing = { 30 | name: 'privatelink.queue.${environment().suffixes.storage}' 31 | } 32 | 33 | resource fileDnsZone 'Microsoft.Network/privateDnsZones@2024-06-01' existing = { 34 | name: 'privatelink.file.${environment().suffixes.storage}' 35 | } 36 | 37 | var subnets = reduce( 38 | map(vnet.properties.subnets, subnet => { 39 | '${subnet.name}': { 40 | id: subnet.id 41 | addressPrefix: subnet.properties.addressPrefix 42 | } 43 | }), 44 | {}, 45 | (cur, acc) => union(cur, acc) 46 | ) 47 | 48 | module storageblobpe '../network/private-endpoint.bicep' = { 49 | name: '${name}-storage-blob-pe' 50 | params: { 51 | location: location 52 | name: '${name}${abbrs.storage.storageAccount}${abbrs.security.privateEndpoint}blob' 53 | tags: tags 54 | subnetId: subnets['aiSubnet'].id 55 | serviceId: storage.id 56 | groupIds: ['blob'] 57 | dnsZoneId: blobDnsZone.id 58 | } 59 | } 60 | 61 | module storagetablepe '../network/private-endpoint.bicep' = { 62 | name: '${name}-storage-table-pe' 63 | params: { 64 | location: location 65 | name: '${name}${abbrs.storage.storageAccount}${abbrs.security.privateEndpoint}table' 66 | tags: tags 67 | subnetId: subnets['aiSubnet'].id 68 | serviceId: storage.id 69 | groupIds: ['table'] 70 | dnsZoneId: tableDnsZone.id 71 | } 72 | } 73 | 74 | module storagequeuepe '../network/private-endpoint.bicep' = { 75 | name: '${name}-storage-queue-pe' 76 | params: { 77 | location: location 78 | name: '${name}${abbrs.storage.storageAccount}${abbrs.security.privateEndpoint}queue' 79 | tags: tags 80 | subnetId: subnets['aiSubnet'].id 81 | serviceId: storage.id 82 | groupIds: ['queue'] 83 | dnsZoneId: queueDnsZone.id 84 | } 85 | } 86 | 87 | module storagefilepe '../network/private-endpoint.bicep' = { 88 | name: '${name}-storage-file-pe' 89 | params: { 90 | location: location 91 | name: '${name}${abbrs.storage.storageAccount}${abbrs.security.privateEndpoint}file' 92 | tags: tags 93 | subnetId: subnets['aiSubnet'].id 94 | serviceId: storage.id 95 | groupIds: ['file'] 96 | dnsZoneId: fileDnsZone.id 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /infra/install.ps1: -------------------------------------------------------------------------------- 1 | Param ( 2 | [Parameter(Mandatory = $true)] 3 | [string] 4 | $azureTenantID, 5 | 6 | [string] 7 | $azureSubscriptionID, 8 | 9 | [string] 10 | $AzureResourceGroupName, 11 | 12 | [string] 13 | $AzdEnvName 14 | ) 15 | 16 | Start-Transcript -Path C:\WindowsAzure\Logs\CMFAI_CustomScriptExtension.txt -Append 17 | 18 | [Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls 19 | [Net.ServicePointManager]::SecurityProtocol = "tls12, tls11, tls" 20 | 21 | Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) 22 | 23 | write-host "Installing Visual Studio Code"; 24 | choco upgrade vscode -y --ignoredetectedreboot --force 25 | 26 | write-host "Installing Azure CLI"; 27 | choco upgrade azure-cli -y --ignoredetectedreboot --force 28 | 29 | write-host "Installing GIT"; 30 | choco upgrade git -y --ignoredetectedreboot --force 31 | 32 | write-host "Installing NODEJS"; 33 | choco upgrade nodejs -y --ignoredetectedreboot --force 34 | 35 | write-host "Installing Python311"; 36 | choco install python311 -y --ignoredetectedreboot --force 37 | #choco install visualstudio2022enterprise -y --ignoredetectedreboot --force 38 | write-host "Installing AZD"; 39 | choco install azd -y --ignoredetectedreboot --force --version 1.14.100 40 | 41 | write-host "Installing Powershell Core"; 42 | choco install powershell-core -y --ignoredetectedreboot --force 43 | 44 | write-host "Installing Chrome"; 45 | #choco install googlechrome -y --ignoredetectedreboot --force 46 | 47 | write-host "Installing Notepad++"; 48 | choco install notepadplusplus -y --ignoredetectedreboot --force 49 | 50 | write-host "Installing Github Desktop"; 51 | choco install github-desktop -y --ignoredetectedreboot --force 52 | 53 | #install extenstions 54 | Start-Process "C:\Program Files\Microsoft VS Code\bin\code.cmd" -ArgumentList "--install-extension","ms-azuretools.vscode-bicep","--force" -wait 55 | Start-Process "C:\Program Files\Microsoft VS Code\bin\code.cmd" -ArgumentList "--install-extension","ms-azuretools.vscode-azurefunctions","--force" -wait 56 | Start-Process "C:\Program Files\Microsoft VS Code\bin\code.cmd" -ArgumentList "--install-extension","ms-python.python","--force" -wait 57 | 58 | write-host "Updating WSL"; 59 | wsl.exe --update 60 | 61 | write-host "Downloading repository"; 62 | mkdir C:\github -ea SilentlyContinue 63 | cd C:\github 64 | git clone https://github.com/azure/ai-document-processor 65 | #git checkout cjg-zta 66 | cd ai-document-processor 67 | 68 | git config --global --add safe.directory C:/github/ai-document-processor 69 | 70 | #add azd to path 71 | $env:Path += ";C:\Program Files\Azure Dev CLI" 72 | 73 | write-host "Logging into Azure CLI and AZD"; 74 | az login --identity --tenant $azureTenantID 75 | azd auth login --managed-identity --tenant-id $azureTenantID 76 | 77 | write-host "Installing NPM packages"; 78 | npm install -g @azure/static-web-apps-cli 79 | npm install -g typescript 80 | 81 | write-host "Initializing AZD"; 82 | azd init -e $AzdEnvName 83 | 84 | write-host "Restarting the machine to complete installation"; 85 | shutdown /r 86 | 87 | Stop-Transcript -------------------------------------------------------------------------------- /troubleshoot-functions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Troubleshooting script for Azure Function App in private network 4 | # Run this script from your VM inside the virtual network 5 | 6 | set -e 7 | 8 | # Function App details from your debug.log 9 | FUNCTION_APP_NAME="func-processing-vwlecyttb6bcs" 10 | FUNCTION_APP_URL="https://${FUNCTION_APP_NAME}.azurewebsites.net" 11 | 12 | echo "=== Azure Function App Troubleshooting ===" 13 | echo "Function App: $FUNCTION_APP_NAME" 14 | echo "URL: $FUNCTION_APP_URL" 15 | echo "" 16 | 17 | # Test 1: DNS Resolution 18 | echo "1. Testing DNS Resolution..." 19 | echo "----------------------------" 20 | nslookup $FUNCTION_APP_NAME.azurewebsites.net || echo "❌ DNS resolution failed" 21 | echo "" 22 | 23 | # Test 2: Network Connectivity 24 | echo "2. Testing Network Connectivity..." 25 | echo "-----------------------------------" 26 | # Test basic connectivity 27 | nc -zv $FUNCTION_APP_NAME.azurewebsites.net 443 2>&1 && echo "✅ Port 443 is reachable" || echo "❌ Port 443 is not reachable" 28 | echo "" 29 | 30 | # Test 3: Function App Health 31 | echo "3. Testing Function App Health..." 32 | echo "----------------------------------" 33 | curl -s -o /dev/null -w "HTTP Status: %{http_code}\n" $FUNCTION_APP_URL || echo "❌ Function App is not responding" 34 | echo "" 35 | 36 | # Test 4: Admin Endpoints (requires master key) 37 | echo "4. Testing Admin Endpoints..." 38 | echo "------------------------------" 39 | echo "Note: Admin endpoints require authentication. If you have the master key, you can test:" 40 | echo "curl -H 'x-functions-key: YOUR_MASTER_KEY' $FUNCTION_APP_URL/admin/functions" 41 | echo "" 42 | 43 | # Test 5: Function Discovery 44 | echo "5. Testing Function Discovery..." 45 | echo "--------------------------------" 46 | echo "Testing available endpoints:" 47 | 48 | # Test the main HTTP function 49 | echo "Testing /api/client endpoint:" 50 | curl -s -o /dev/null -w "HTTP Status: %{http_code}\n" $FUNCTION_APP_URL/api/client || echo "❌ /api/client endpoint failed" 51 | 52 | # Test root API 53 | echo "Testing /api/ endpoint:" 54 | curl -s -o /dev/null -w "HTTP Status: %{http_code}\n" $FUNCTION_APP_URL/api/ || echo "❌ /api/ endpoint failed" 55 | 56 | echo "" 57 | 58 | # Test 6: Check if SCM site is accessible (Kudu) 59 | echo "6. Testing SCM Site (Kudu)..." 60 | echo "------------------------------" 61 | SCM_URL="https://${FUNCTION_APP_NAME}.scm.azurewebsites.net" 62 | echo "SCM URL: $SCM_URL" 63 | curl -s -o /dev/null -w "HTTP Status: %{http_code}\n" $SCM_URL || echo "❌ SCM site is not accessible from private network" 64 | echo "" 65 | 66 | # Test 7: Check Function Runtime Status 67 | echo "7. Testing Function Runtime Status..." 68 | echo "--------------------------------------" 69 | echo "Testing runtime status endpoint:" 70 | curl -s -o /dev/null -w "HTTP Status: %{http_code}\n" $FUNCTION_APP_URL/admin/host/status || echo "❌ Runtime status endpoint failed" 71 | echo "" 72 | 73 | echo "=== Troubleshooting Complete ===" 74 | echo "" 75 | echo "Next steps if functions are not visible:" 76 | echo "1. If DNS resolution fails, check private DNS zone configuration" 77 | echo "2. If connectivity fails, check NSG and firewall rules" 78 | echo "3. If admin endpoints fail, the functions might not be properly deployed" 79 | echo "4. Check Azure Portal for function deployment status" 80 | echo "5. Review Function App logs in Azure Portal" 81 | -------------------------------------------------------------------------------- /pipeline/activities/speechToText.py: -------------------------------------------------------------------------------- 1 | import azure.durable_functions as df 2 | 3 | import requests 4 | import time 5 | import logging 6 | 7 | from configuration import Configuration 8 | 9 | name = "speechToText" 10 | bp = df.Blueprint() 11 | 12 | 13 | def wait_for_transcription(transcription_url, headers, check_interval=10): 14 | """Poll the transcription status until it's complete""" 15 | while True: 16 | status_response = requests.get(transcription_url, headers=headers) 17 | status = status_response.json() 18 | 19 | current_status = status['status'] 20 | print(f"Status: {current_status}") 21 | 22 | if current_status == 'Succeeded': 23 | print("Transcription completed successfully!") 24 | return status 25 | elif current_status == 'Failed': 26 | print("Transcription failed!") 27 | print(f"Error: {status.get('properties', {}).get('error', 'Unknown error')}") 28 | return status 29 | else: 30 | print(f"Waiting {check_interval} seconds before checking again...") 31 | time.sleep(check_interval) 32 | 33 | 34 | @bp.function_name(name) 35 | @bp.activity_trigger(input_name="blob_input") 36 | def run(blob_input: dict): 37 | # Parse Arguments 38 | try: 39 | blob_name = blob_input.get('name') 40 | container = blob_input.get('container') 41 | blob_uri = blob_input.get('uri') 42 | 43 | 44 | config = Configuration() 45 | credential = config.credential 46 | token = credential.get_token("https://cognitiveservices.azure.com/.default").token 47 | 48 | endpoint = config.get_value("AIMULTISERVICES_ENDPOINT") # e.g., "https://your-resource.cognitiveservices.azure.com" 49 | api_version = "2025-10-15" 50 | url = f"{endpoint}/speechtotext/transcriptions:submit?api-version={api_version}" 51 | 52 | headers = { 53 | 'Content-Type': 'application/json', 54 | "Authorization": f"Bearer {token}", 55 | } 56 | 57 | payload = { 58 | "displayName": "Transcription", 59 | "locale": "en-US", 60 | "contentUrls": [blob_uri], # 61 | "properties": { 62 | "wordLevelTimestampsEnabled": False, 63 | "displayFormWordLevelTimestampsEnabled": False, 64 | "punctuationMode": "DictatedAndAutomatic", 65 | "profanityFilterMode": "Masked", 66 | "timeToLiveHours": 48 67 | } 68 | } 69 | 70 | logging.info(f"Submitting transcription request for blob: {blob_name} in container: {container} with payload: {payload}") 71 | response = requests.post(url, json=payload, headers=headers) 72 | transcription_url = response.json()['self'] 73 | 74 | # Wait for completion 75 | final_status = wait_for_transcription(transcription_url, headers) 76 | 77 | files_url = final_status['links']['files'] 78 | 79 | files_response = requests.get(files_url, headers=headers) 80 | content_url = files_response.json()['values'][0]['links']['contentUrl'] 81 | content_response = requests.get(content_url).json() 82 | # content_response.json() 83 | full_text = content_response['combinedRecognizedPhrases'][0]['display'] 84 | 85 | except Exception as e: 86 | logging.error(f"Error during speech-to-text processing: {e}") 87 | full_text = "Error during speech-to-text processing." 88 | 89 | return full_text -------------------------------------------------------------------------------- /infra/modules/management_governance/log-analytics-workspace.bicep: -------------------------------------------------------------------------------- 1 | @description('Name of the resource.') 2 | param name string 3 | @description('Location to deploy the resource. Defaults to the location of the resource group.') 4 | param location string = resourceGroup().location 5 | @description('Tags for the resource.') 6 | param tags object = {} 7 | 8 | param publicNetworkAccessForIngestion string = 'Enabled' 9 | param publicNetworkAccessForQuery string = 'Enabled' 10 | 11 | @export() 12 | @description('SKU information for Log Analytics Workspace.') 13 | type skuInfo = { 14 | @description('Name of the SKU.') 15 | name: 'CapacityReservation' | 'Free' | 'LACluster' | 'PerGB2018' | 'PerNode' | 'Premium' | 'Standalone' | 'Standard' 16 | } 17 | 18 | @export() 19 | @description('Diagnostic settings configuration info for logs.') 20 | type diagnosticSettingsLogConfigInfo = { 21 | @description('Name of the diagnostic log setting. Required if categoryGroup is not specified.') 22 | category: string? 23 | @description('Name of the category group of diagnostic logs. Required if category is not specified.') 24 | categoryGroup: string? 25 | @description('Flag indicating whether the diagnostic setting is enabled.') 26 | enabled: bool 27 | @description('Retention policy for the logs.') 28 | retentionPolicy: { 29 | @description('Flag indicating whether the retention policy is enabled.') 30 | enabled: bool 31 | @description('Number of days to retain the logs.') 32 | days: int 33 | }? 34 | } 35 | 36 | @export() 37 | @description('Diagnostic settings configuration info for metrics.') 38 | type diagnosticSettingsMetricConfigInfo = { 39 | @description('Name of the diagnostic metric setting.') 40 | category: string 41 | @description('Flag indicating whether the diagnostic setting is enabled.') 42 | enabled: bool 43 | @description('Retention policy for the metrics.') 44 | retentionPolicy: { 45 | @description('Flag indicating whether the retention policy is enabled.') 46 | enabled: bool 47 | @description('Number of days to retain the metrics.') 48 | days: int 49 | }? 50 | } 51 | 52 | @export() 53 | @description('Diagnostic settings info for supported resources.') 54 | type diagnosticSettingsInfo = { 55 | @description('Diagnostic settings for logs.') 56 | logs: diagnosticSettingsLogConfigInfo[] 57 | @description('Diagnostic settings for metrics.') 58 | metrics: diagnosticSettingsMetricConfigInfo[] 59 | } 60 | 61 | @description('Log Analytics Workspace SKU. Defaults to PerGB2018.') 62 | param sku skuInfo = { 63 | name: 'PerGB2018' 64 | } 65 | @description('Retention period (in days) for the Log Analytics Workspace. Defaults to 30.') 66 | param retentionInDays int = 30 67 | 68 | resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { 69 | name: name 70 | location: location 71 | tags: tags 72 | properties: { 73 | retentionInDays: retentionInDays 74 | features: { 75 | enableLogAccessUsingOnlyResourcePermissions: true 76 | } 77 | sku: sku 78 | publicNetworkAccessForIngestion: publicNetworkAccessForIngestion 79 | publicNetworkAccessForQuery: publicNetworkAccessForQuery 80 | } 81 | } 82 | 83 | @description('ID for the deployed Log Analytics Workspace resource.') 84 | output id string = logAnalyticsWorkspace.id 85 | @description('Name for the deployed Log Analytics Workspace resource.') 86 | output name string = logAnalyticsWorkspace.name 87 | @description('Customer ID for the deployed Log Analytics Workspace resource.') 88 | output customerId string = logAnalyticsWorkspace.properties.customerId 89 | -------------------------------------------------------------------------------- /infra/modules/network/vnet-vpn-gateway.bicep: -------------------------------------------------------------------------------- 1 | param vnetName string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | param gatewayName string = 'vnetGateway' 6 | param gatewayPublicIPName string = 'vnetGatewayPublicIP' 7 | 8 | param vnetAddressPrefix string = '10.0.0.0/23' 9 | // param vnetAddress string = '10.0.0.0/23' 10 | param gatewaySubnetPrefix string = '10.0.1.64/26' 11 | var gatewaySubPrefix = gatewaySubnetPrefix 12 | 13 | @description('The SKU of the Gateway. This must be either Standard or HighPerformance to work with OpenVPN') 14 | @allowed([ 15 | 'Standard' 16 | 'HighPerformance' 17 | 'VpnGw1AZ' 18 | 'VpnGw2AZ' 19 | 'VpnGw3AZ' 20 | 'VpnGw4AZ' 21 | 'VpnGw5AZ' 22 | 'VpnGw1' 23 | 'VpnGw2' 24 | 'VpnGw3' 25 | 'VpnGw4' 26 | 'VpnGw5' 27 | ]) 28 | param gatewaySku string = 'VpnGw1' 29 | 30 | @description('Route based (Dynamic Gateway) or Policy based (Static Gateway)') 31 | @allowed([ 32 | 'RouteBased' 33 | 'PolicyBased' 34 | ]) 35 | param vpnType string = 'RouteBased' 36 | 37 | @description('The IP address range from which VPN clients will receive an IP address when connected. Range specified must not overlap with on-premise network') 38 | param vpnClientAddressPool string = '172.16.0.0/24' 39 | 40 | var audienceMap = { 41 | AzureCloud: '41b23e61-6c1e-4545-b367-cd054e0ed4b4' 42 | AzureUSGovernment: '51bb15d4-3a4f-4ebf-9dca-40096fe32426' 43 | AzureGermanCloud: '538ee9e6-310a-468d-afef-ea97365856a9' 44 | AzureChinaCloud: '49f817b6-84ae-4cc0-928c-73f27289b3aa' 45 | } 46 | 47 | var tenantId = subscription().tenantId 48 | var cloud = environment().name 49 | var audience = audienceMap[cloud] 50 | var tenant = uri(environment().authentication.loginEndpoint, tenantId) 51 | var issuer = 'https://sts.windows.net/${tenantId}/' 52 | 53 | resource virtualNetwork 'Microsoft.Network/virtualNetworks@2024-05-01' existing = { 54 | name: vnetName 55 | scope: resourceGroup() 56 | } 57 | 58 | resource gatewaySubnet 'Microsoft.Network/virtualNetworks/subnets@2024-05-01' = { 59 | name: 'GatewaySubnet' 60 | parent: virtualNetwork 61 | properties:{ 62 | addressPrefix: gatewaySubPrefix 63 | } 64 | } 65 | 66 | resource publicIp 'Microsoft.Network/publicIPAddresses@2021-02-01' = { 67 | name: gatewayPublicIPName 68 | location: location 69 | sku: { 70 | name: 'Standard' 71 | tier: 'Regional' 72 | } 73 | properties: { 74 | publicIPAllocationMethod: 'Static' 75 | } 76 | tags: tags 77 | } 78 | 79 | resource vpnGateway 'Microsoft.Network/virtualNetworkGateways@2021-02-01' = { 80 | name: gatewayName 81 | location: location 82 | properties: { 83 | ipConfigurations: [ 84 | { 85 | properties: { 86 | privateIPAllocationMethod: 'Dynamic' 87 | subnet: { 88 | id: gatewaySubnet.id 89 | } 90 | publicIPAddress: { 91 | id: publicIp.id 92 | } 93 | } 94 | name: 'vnetGatewayConfig' 95 | } 96 | ] 97 | sku: { 98 | name: gatewaySku 99 | tier: gatewaySku 100 | } 101 | gatewayType: 'Vpn' 102 | vpnType: vpnType 103 | vpnClientConfiguration: { 104 | vpnClientAddressPool: { 105 | addressPrefixes: [ 106 | vpnClientAddressPool 107 | ] 108 | } 109 | vpnClientProtocols: [ 110 | 'OpenVPN' 111 | ] 112 | vpnAuthenticationTypes: [ 113 | 'AAD' 114 | ] 115 | aadTenant: tenant 116 | aadAudience: audience 117 | aadIssuer: issuer 118 | } 119 | customRoutes: { 120 | addressPrefixes: [ 121 | vnetAddressPrefix 122 | ] 123 | } 124 | } 125 | } 126 | 127 | output name string = vpnGateway.name 128 | output id string = vpnGateway.id 129 | output publicIp string = publicIp.properties.ipAddress 130 | -------------------------------------------------------------------------------- /troubleshoot-functions.ps1: -------------------------------------------------------------------------------- 1 | # Troubleshooting script for Azure Function App in private network 2 | # Run this script from your VM inside the virtual network 3 | 4 | # Function App details from your debug.log 5 | $FUNCTION_APP_NAME = "func-processing-vwlecyttb6bcs" 6 | $FUNCTION_APP_URL = "https://$FUNCTION_APP_NAME.azurewebsites.net" 7 | 8 | Write-Host "=== Azure Function App Troubleshooting ===" -ForegroundColor Yellow 9 | Write-Host "Function App: $FUNCTION_APP_NAME" 10 | Write-Host "URL: $FUNCTION_APP_URL" 11 | Write-Host "" 12 | 13 | # Test 1: DNS Resolution 14 | Write-Host "1. Testing DNS Resolution..." -ForegroundColor Green 15 | Write-Host "----------------------------" 16 | try { 17 | $dnsResult = Resolve-DnsName "$FUNCTION_APP_NAME.azurewebsites.net" -ErrorAction Stop 18 | Write-Host "✅ DNS Resolution successful. IP: $($dnsResult.IPAddress -join ', ')" -ForegroundColor Green 19 | } catch { 20 | Write-Host "❌ DNS resolution failed: $($_.Exception.Message)" -ForegroundColor Red 21 | } 22 | Write-Host "" 23 | 24 | # Test 2: Network Connectivity 25 | Write-Host "2. Testing Network Connectivity..." -ForegroundColor Green 26 | Write-Host "-----------------------------------" 27 | try { 28 | $tcpTest = Test-NetConnection -ComputerName "$FUNCTION_APP_NAME.azurewebsites.net" -Port 443 -WarningAction SilentlyContinue 29 | if ($tcpTest.TcpTestSucceeded) { 30 | Write-Host "✅ Port 443 is reachable" -ForegroundColor Green 31 | } else { 32 | Write-Host "❌ Port 443 is not reachable" -ForegroundColor Red 33 | } 34 | } catch { 35 | Write-Host "❌ Network connectivity test failed: $($_.Exception.Message)" -ForegroundColor Red 36 | } 37 | Write-Host "" 38 | 39 | # Test 3: Function App Health 40 | Write-Host "3. Testing Function App Health..." -ForegroundColor Green 41 | Write-Host "----------------------------------" 42 | try { 43 | $response = Invoke-WebRequest -Uri $FUNCTION_APP_URL -Method GET -UseBasicParsing -ErrorAction Stop 44 | Write-Host "✅ Function App is responding. Status: $($response.StatusCode)" -ForegroundColor Green 45 | } catch { 46 | Write-Host "❌ Function App is not responding: $($_.Exception.Message)" -ForegroundColor Red 47 | } 48 | Write-Host "" 49 | 50 | # Test 4: Function Discovery 51 | Write-Host "4. Testing Function Discovery..." -ForegroundColor Green 52 | Write-Host "--------------------------------" 53 | 54 | # Test the main HTTP function 55 | Write-Host "Testing /api/client endpoint:" 56 | try { 57 | $response = Invoke-WebRequest -Uri "$FUNCTION_APP_URL/api/client" -Method GET -UseBasicParsing -ErrorAction Stop 58 | Write-Host "✅ /api/client endpoint responding. Status: $($response.StatusCode)" -ForegroundColor Green 59 | } catch { 60 | Write-Host "❌ /api/client endpoint failed: $($_.Exception.Message)" -ForegroundColor Red 61 | } 62 | 63 | # Test API root 64 | Write-Host "Testing /api/ endpoint:" 65 | try { 66 | $response = Invoke-WebRequest -Uri "$FUNCTION_APP_URL/api/" -Method GET -UseBasicParsing -ErrorAction Stop 67 | Write-Host "✅ /api/ endpoint responding. Status: $($response.StatusCode)" -ForegroundColor Green 68 | } catch { 69 | Write-Host "❌ /api/ endpoint failed: $($_.Exception.Message)" -ForegroundColor Red 70 | } 71 | Write-Host "" 72 | 73 | # Test 5: Check if SCM site is accessible (Kudu) 74 | Write-Host "5. Testing SCM Site (Kudu)..." -ForegroundColor Green 75 | Write-Host "------------------------------" 76 | $SCM_URL = "https://$FUNCTION_APP_NAME.scm.azurewebsites.net" 77 | Write-Host "SCM URL: $SCM_URL" 78 | try { 79 | $response = Invoke-WebRequest -Uri $SCM_URL -Method GET -UseBasicParsing -ErrorAction Stop 80 | Write-Host "✅ SCM site is accessible. Status: $($response.StatusCode)" -ForegroundColor Green 81 | } catch { 82 | Write-Host "❌ SCM site is not accessible from private network: $($_.Exception.Message)" -ForegroundColor Red 83 | } 84 | Write-Host "" 85 | 86 | Write-Host "=== Troubleshooting Complete ===" -ForegroundColor Yellow 87 | Write-Host "" 88 | Write-Host "Next steps if functions are not visible:" -ForegroundColor Cyan 89 | Write-Host "1. If DNS resolution fails, check private DNS zone configuration" 90 | Write-Host "2. If connectivity fails, check NSG and firewall rules" 91 | Write-Host "3. If admin endpoints fail, the functions might not be properly deployed" 92 | Write-Host "4. Check Azure Portal for function deployment status" 93 | Write-Host "5. Review Function App logs in Azure Portal" 94 | -------------------------------------------------------------------------------- /scripts/getRemoteSettings.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | Set-StrictMode -Version Latest 3 | $ErrorActionPreference = 'Stop' 4 | Write-Host "Loading remote settings into local.settings.json from Azure resources..." 5 | # Load azd environment values (emulates: eval $(azd env get-values)) 6 | azd env get-values | ForEach-Object { 7 | if ($_ -match '^(?[^=]+)=(?.*)$') { 8 | $k = $matches.key.Trim() 9 | $v = $matches.val 10 | 11 | # Remove exactly one outer pair of double quotes if present 12 | if ($v.Length -ge 2 -and $v.StartsWith('"') -and $v.EndsWith('"')) { 13 | $v = $v.Substring(1, $v.Length - 2) 14 | # Unescape any embedded \" (azd usually doesn’t emit these, but safe) 15 | $v = $v -replace '\\"','"' 16 | } 17 | 18 | [Environment]::SetEnvironmentVariable($k, $v) 19 | Set-Variable -Name $k -Value $v -Scope Script -Force 20 | } 21 | } 22 | 23 | # (Optional) Quick debug echo – comment out when stable 24 | Write-Host "PROCESSING_FUNCTION_APP_NAME => '$($env:PROCESSING_FUNCTION_APP_NAME)'" 25 | Write-Host "APP_CONFIG_NAME => '$($env:APP_CONFIG_NAME)'" 26 | Write-Host "AZURE_STORAGE_ACCOUNT => '$($env:AZURE_STORAGE_ACCOUNT)'" 27 | Write-Host "RESOURCE_GROUP => '$($env:RESOURCE_GROUP)'" 28 | 29 | # Move into pipeline directory relative to script location 30 | $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path 31 | Set-Location (Join-Path $scriptDir '../pipeline') 32 | 33 | if (-not $env:PROCESSING_FUNCTION_APP_NAME) { throw "PROCESSING_FUNCTION_APP_NAME not set." } 34 | 35 | func azure functionapp fetch-app-settings $env:PROCESSING_FUNCTION_APP_NAME --decrypt 36 | func settings decrypt 37 | 38 | # Get App Configuration primary connection string 39 | if (-not $env:APP_CONFIG_NAME) { 40 | throw "APP_CONFIG_NAME not set." 41 | } 42 | $connString = az appconfig credential list ` 43 | --name $env:APP_CONFIG_NAME ` 44 | --query "[?name=='Primary'].connectionString" ` 45 | -o tsv 46 | 47 | if (-not $connString) { 48 | throw "Failed to retrieve App Configuration connection string." 49 | } 50 | 51 | # Retrieve Storage account connection strings (function + data storage) 52 | $storageAccount = $env:AZURE_STORAGE_ACCOUNT 53 | $resourceGroup = $env:RESOURCE_GROUP 54 | 55 | if (-not $storageAccount) { throw "AZURE_STORAGE_ACCOUNT not set." } 56 | if (-not $resourceGroup) { throw "RESOURCE_GROUP not set." } 57 | 58 | $blobFuncConnString = az storage account show-connection-string ` 59 | --name $storageAccount ` 60 | --resource-group $resourceGroup ` 61 | --query connectionString ` 62 | -o tsv 63 | 64 | if (-not $blobFuncConnString) { throw "Failed to retrieve function storage account connection string." } 65 | 66 | # If a distinct data storage account is desired in future, adjust here. For now we mirror the same account like the bash script. 67 | $blobDataStorageConnString = az storage account show-connection-string ` 68 | --name $storageAccount ` 69 | --resource-group $resourceGroup ` 70 | --query connectionString ` 71 | -o tsv 72 | 73 | if (-not $blobDataStorageConnString) { throw "Failed to retrieve data storage account connection string." } 74 | 75 | # Update local.settings.json (replace jq operation) 76 | $localSettingsPath = "local.settings.json" 77 | if (-not (Test-Path $localSettingsPath)) { 78 | throw "File not found: $localSettingsPath" 79 | } 80 | 81 | # Parse as hashtable so we can add keys easily 82 | $json = Get-Content $localSettingsPath -Raw | ConvertFrom-Json -AsHashtable 83 | 84 | if (-not $json.ContainsKey('Values') -or -not $json['Values']) { 85 | $json['Values'] = @{} 86 | } 87 | 88 | # Assign (creates or overwrites keys) 89 | $json['Values']['AZURE_APPCONFIG_CONNECTION_STRING'] = $connString 90 | $json['Values']['AzureWebJobsStorage'] = $blobFuncConnString 91 | $json['Values']['DataStorage'] = $blobDataStorageConnString 92 | 93 | # Write back 94 | $json | ConvertTo-Json -Depth 10 | Set-Content $localSettingsPath -Encoding UTF8 95 | 96 | Write-Host "Updated local.settings.json with:" 97 | Write-Host " AZURE_APPCONFIG_CONNECTION_STRING => (length: $($connString.Length))" 98 | Write-Host " AzureWebJobsStorage => (length: $($blobFuncConnString.Length))" 99 | Write-Host " DataStorage => (length: $($blobDataStorageConnString.Length))" 100 | -------------------------------------------------------------------------------- /infra/main.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "environmentName": { 6 | "value": "${AZURE_ENV_NAME}" 7 | }, 8 | "location": { 9 | "value": "${AZURE_LOCATION}" 10 | }, 11 | "principalId": { 12 | "value": "${AZURE_PRINCIPAL_ID}" 13 | }, 14 | "resourceGroup": { 15 | "value": "${AZURE_RESOURCE_GROUP}" 16 | }, 17 | "networkIsolation": { 18 | "value": "${AZURE_NETWORK_ISOLATION}" 19 | }, 20 | "ai_vision_enabled": { 21 | "value": "${AI_VISION_ENABLED}" 22 | }, 23 | "multimodal": { 24 | "value": "${AOAI_MULTI_MODAL}" 25 | }, 26 | "deploymentTags":{ 27 | "value": {} 28 | }, 29 | "deployVM": { 30 | "value": "${AZURE_DEPLOY_VM}" 31 | }, 32 | "deployVPN": { 33 | "value": "${AZURE_DEPLOY_VPN}" 34 | }, 35 | "aoaiLocation": { 36 | "value": "${AOAI_LOCATION}" 37 | }, 38 | "azureReuseConfig":{ 39 | "value": { 40 | "aoaiReuse": "${AOAI_REUSE}", 41 | "existingAoaiResourceGroupName": "${AOAI_RESOURCE_GROUP_NAME}", 42 | "existingAoaiName": "${AOAI_NAME}", 43 | "appInsightsReuse": "${APP_INSIGHTS_REUSE}", 44 | "existingAppInsightsResourceGroupName": "${APP_INSIGHTS_RESOURCE_GROUP_NAME}", 45 | "existingAppInsightsName": "${APP_INSIGHTS_NAME}", 46 | "logAnalyticsWorkspaceReuse": "${LOG_ANALYTICS_WORKSPACE_REUSE}", 47 | "existingLogAnalyticsWorkspaceResourceId": "${LOG_ANALYTICS_WORKSPACE_ID}", 48 | "appServicePlanReuse": "${APP_SERVICE_PLAN_REUSE}", 49 | "existingAppServicePlanResourceGroupName": "${APP_SERVICE_PLAN_RESOURCE_GROUP_NAME}", 50 | "existingAppServicePlanName": "${APP_SERVICE_PLAN_NAME}", 51 | "aiSearchReuse": "${AI_SEARCH_REUSE}", 52 | "existingAiSearchResourceGroupName": "${AI_SEARCH_RESOURCE_GROUP_NAME}", 53 | "existingAiSearchName": "${AI_SEARCH_NAME}", 54 | "orchestratorFunctionAppReuse": "${ORCHESTRATOR_FUNCTION_APP_REUSE}", 55 | "existingOrchestratorFunctionAppResourceGroupName": "${ORCHESTRATOR_FUNCTION_APP_RESOURCE_GROUP_NAME}", 56 | "existingOrchestratorFunctionAppName": "${ORCHESTRATOR_FUNCTION_APP_NAME}", 57 | "dataIngestionFunctionAppReuse": "${DATA_INGESTION_FUNCTION_APP_REUSE}", 58 | "existingDataIngestionFunctionAppResourceGroupName": "${DATA_INGESTION_FUNCTION_APP_RESOURCE_GROUP_NAME}", 59 | "existingDataIngestionFunctionAppName": "${DATA_INGESTION_FUNCTION_APP_NAME}", 60 | "appServiceReuse": "${APP_SERVICE_REUSE}", 61 | "existingAppServiceName": "${APP_SERVICE_NAME}", 62 | "existingAppServiceNameResourceGroupName": "${APP_SERVICE_RESOURCE_GROUP_NAME}", 63 | "orchestratorFunctionAppStorageReuse": "${ORCHESTRATOR_FUNCTION_APP_STORAGE_REUSE}", 64 | "existingOrchestratorFunctionAppStorageName": "${ORCHESTRATOR_FUNCTION_APP_STORAGE_NAME}", 65 | "existingOrchestratorFunctionAppStorageResourceGroupName": "${ORCHESTRATOR_FUNCTION_APP_STORAGE_RESOURCE_GROUP_NAME}", 66 | "dataIngestionFunctionAppStorageReuse": "${DATA_INGESTION_FUNCTION_APP_STORAGE_REUSE}", 67 | "existingDataIngestionFunctionAppStorageName": "${DATA_INGESTION_FUNCTION_APP_STORAGE_NAME}", 68 | "existingDataIngestionFunctionAppStorageResourceGroupName": "${DATA_INGESTION_FUNCTION_APP_STORAGE_RESOURCE_GROUP_NAME}", 69 | "aiServicesReuse": "${AI_SERVICES_REUSE}", 70 | "existingAiServicesResourceGroupName": "${AI_SERVICES_RESOURCE_GROUP_NAME}", 71 | "existingAiServicesName": "${AI_SERVICES_NAME}", 72 | "cosmosDbReuse": "${COSMOS_DB_REUSE}", 73 | "existingCosmosDbResourceGroupName": "${COSMOS_DB_RESOURCE_GROUP_NAME}", 74 | "existingCosmosDbAccountName": "${COSMOS_DB_ACCOUNT_NAME}", 75 | "existingCosmosDbDatabaseName": "${COSMOS_DB_DATABASE_NAME}", 76 | "keyVaultReuse": "${KEY_VAULT_REUSE}", 77 | "existingKeyVaultResourceGroupName": "${KEY_VAULT_RESOURCE_GROUP_NAME}", 78 | "existingKeyVaultName": "${KEY_VAULT_NAME}", 79 | "storageReuse": "${STORAGE_REUSE}", 80 | "existingStorageResourceGroupName": "${STORAGE_RESOURCE_GROUP_NAME}", 81 | "existingStorageName": "${STORAGE_NAME}", 82 | "vnetReuse": "${VNET_REUSE}", 83 | "existingVnetResourceGroupName": "${VNET_RESOURCE_GROUP_NAME}", 84 | "existingVnetName": "${VNET_NAME}" 85 | } 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /infra/modules/security/key-vault.bicep: -------------------------------------------------------------------------------- 1 | import { roleAssignmentInfo } from '../security/managed-identity.bicep' 2 | import { diagnosticSettingsInfo } from '../management_governance/log-analytics-workspace.bicep' 3 | 4 | @description('Name of the resource.') 5 | param name string 6 | @description('Location to deploy the resource. Defaults to the location of the resource group.') 7 | param location string = resourceGroup().location 8 | @description('Tags for the resource.') 9 | param tags object = {} 10 | 11 | param keyVaultReuse bool 12 | param existingKeyVaultResourceGroupName string 13 | 14 | param publicNetworkAccess string = 'Enabled' 15 | 16 | @description('Secret Keys to add to App Configuration') 17 | param secureAppSettings array = [] 18 | 19 | // param subnets array = [] 20 | 21 | @description('Key Vault SKU name. Defaults to standard.') 22 | @allowed([ 23 | 'standard' 24 | 'premium' 25 | ]) 26 | param skuName string = 'standard' 27 | @description('Whether soft deletion is enabled. Defaults to true.') 28 | param enableSoftDelete bool = true 29 | @description('Number of days to retain soft-deleted keys, secrets, and certificates. Defaults to 90.') 30 | param retentionInDays int = 90 31 | @description('Whether purge protection is enabled. Defaults to true.') 32 | param enablePurgeProtection bool = true 33 | @description('Role assignments to create for the Key Vault.') 34 | param roleAssignments roleAssignmentInfo[] = [] 35 | @description('Name of the Log Analytics Workspace to use for diagnostic settings.') 36 | param logAnalyticsWorkspaceName string? 37 | @description('Diagnostic settings to configure for the Key Vault instance. Defaults to all logs and metrics.') 38 | param diagnosticSettings diagnosticSettingsInfo = { 39 | logs: [ 40 | { 41 | categoryGroup: 'allLogs' 42 | enabled: true 43 | } 44 | ] 45 | metrics: [ 46 | { 47 | category: 'AllMetrics' 48 | enabled: true 49 | } 50 | ] 51 | } 52 | 53 | resource existingKeyVault 'Microsoft.KeyVault/vaults@2024-04-01-preview' existing = if (keyVaultReuse) { 54 | scope: resourceGroup(existingKeyVaultResourceGroupName) 55 | name: name 56 | } 57 | 58 | resource keyVault 'Microsoft.KeyVault/vaults@2024-04-01-preview' = if (!keyVaultReuse) { 59 | name: name 60 | location: location 61 | tags: tags 62 | properties: { 63 | sku: { 64 | family: 'A' 65 | name: skuName 66 | } 67 | tenantId: subscription().tenantId 68 | networkAcls: { 69 | defaultAction: 'Allow' 70 | bypass: 'AzureServices' 71 | ipRules: [] 72 | virtualNetworkRules: [ 73 | 74 | ] 75 | } 76 | accessPolicies: [] 77 | enableSoftDelete: enableSoftDelete 78 | enabledForTemplateDeployment: true 79 | enableRbacAuthorization: true 80 | enablePurgeProtection: enablePurgeProtection 81 | softDeleteRetentionInDays: retentionInDays 82 | publicNetworkAccess: publicNetworkAccess 83 | } 84 | } 85 | 86 | resource assignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = [ 87 | for roleAssignment in roleAssignments: { 88 | name: guid(keyVault.id, roleAssignment.principalId, roleAssignment.roleDefinitionId) 89 | scope: keyVault 90 | properties: { 91 | principalId: roleAssignment.principalId 92 | roleDefinitionId: roleAssignment.roleDefinitionId 93 | principalType: roleAssignment.principalType 94 | } 95 | } 96 | ] 97 | 98 | resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' existing = if (logAnalyticsWorkspaceName != null) { 99 | name: logAnalyticsWorkspaceName! 100 | } 101 | 102 | resource keyVaultDiagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (logAnalyticsWorkspaceName != null) { 103 | name: '${keyVault.name}-diagnostic-settings' 104 | scope: keyVault 105 | properties: { 106 | workspaceId: logAnalyticsWorkspace.id 107 | logs: diagnosticSettings!.logs 108 | metrics: diagnosticSettings!.metrics 109 | } 110 | } 111 | 112 | // Secret in Key Vault 113 | resource secret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = [for (config, i) in secureAppSettings: { 114 | parent: keyVault 115 | name: replace(config.name, '_', '-') 116 | properties: { 117 | contentType: 'string' 118 | value: config.value 119 | } 120 | tags: {} 121 | } 122 | ] 123 | 124 | @description('ID for the deployed Key Vault resource.') 125 | output id string = keyVaultReuse ? existingKeyVault.id: keyVault.id 126 | @description('Name for the deployed Key Vault resource.') 127 | output name string = keyVaultReuse ? existingKeyVault.name: keyVault.name 128 | @description('URI for the deployed Key Vault resource.') 129 | output uri string = keyVaultReuse ? existingKeyVault.properties.vaultUri: keyVault.properties.vaultUri 130 | @description('Urls to the secrets created in the Key Vault for app config') 131 | output secrets array = [for (config, i) in secureAppSettings: { 132 | name: config.name 133 | value: concat('{"uri":"',secret[i].properties.secretUri, '"}') 134 | }] 135 | -------------------------------------------------------------------------------- /infra/modules/containers/container-apps-environment.bicep: -------------------------------------------------------------------------------- 1 | @description('Name of the resource.') 2 | param name string 3 | @description('Location to deploy the resource. Defaults to the location of the resource group.') 4 | param location string = resourceGroup().location 5 | @description('Tags for the resource.') 6 | param tags object = {} 7 | @description('MSI Id.') 8 | param identityId string? 9 | 10 | @description('Whether to enable public network access. Defaults to Enabled.') 11 | @allowed([ 12 | 'Enabled' 13 | 'Disabled' 14 | ]) 15 | param publicNetworkAccess string = 'Enabled' 16 | 17 | @export() 18 | @description('Information about a workload profile for the environment.') 19 | type workloadProfileInfo = { 20 | @description('Friendly name of the workload profile.') 21 | name: string 22 | @description('Type of the workload profile.') 23 | workloadProfileType: 24 | | 'D4' 25 | | 'D8' 26 | | 'D16' 27 | | 'D32' 28 | | 'E4' 29 | | 'E8' 30 | | 'E16' 31 | | 'E32' 32 | | 'NC24-A100' 33 | | 'NC48-A100' 34 | | 'NC96-A100' 35 | @description('Minimum number of nodes for the workload profile.') 36 | minimumCount: int 37 | @description('Maximum number of nodes for the workload profile.') 38 | maximumCount: int 39 | } 40 | 41 | @export() 42 | @description('Information about the configuration for a custom domain in the environment.') 43 | type customDomainConfigInfo = { 44 | @description('Name of the custom domain.') 45 | dnsSuffix: string 46 | @description('Value of the custom domain certificate.') 47 | certificateValue: string 48 | @description('Password for the custom domain certificate.') 49 | certificatePassword: string 50 | } 51 | 52 | @export() 53 | @description('Information about the configuration for a virtual network in the environment.') 54 | type vnetConfigInfo = { 55 | @description('Resource ID of a subnet for infrastructure components.') 56 | infrastructureSubnetId: string 57 | @description('Value indicating whether the environment only has an internal load balancer.') 58 | internal: bool 59 | } 60 | 61 | @description('Additional workload profiles. Includes Consumption by default.') 62 | param workloadProfiles workloadProfileInfo[] = [] 63 | @description('Name of the Log Analytics Workspace to store application logs.') 64 | param logAnalyticsWorkspaceName string 65 | @description('Name of the Application Insights resource.') 66 | param applicationInsightsName string 67 | @description('Custom domain configuration for the environment.') 68 | param customDomainConfig customDomainConfigInfo = { 69 | dnsSuffix: '' 70 | certificateValue: '' 71 | certificatePassword: '' 72 | } 73 | @description('Virtual network configuration for the environment.') 74 | param vnetConfig vnetConfigInfo = { 75 | infrastructureSubnetId: '' 76 | internal: true 77 | } 78 | @description('Value indicating whether the environment is zone-redundant. Defaults to false.') 79 | param zoneRedundant bool = false 80 | 81 | resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' existing = { 82 | name: logAnalyticsWorkspaceName 83 | } 84 | 85 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = { 86 | name: applicationInsightsName 87 | } 88 | 89 | resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2024-10-02-preview' = { 90 | name: name 91 | location: location 92 | tags: tags 93 | identity: { 94 | type: identityId == null ? 'SystemAssigned' : 'UserAssigned' 95 | userAssignedIdentities: identityId == null 96 | ? null 97 | : { 98 | '${identityId}': {} 99 | } 100 | } 101 | properties: { 102 | publicNetworkAccess: publicNetworkAccess 103 | appLogsConfiguration: { 104 | destination: 'log-analytics' 105 | logAnalyticsConfiguration: { 106 | customerId: logAnalyticsWorkspace.properties.customerId 107 | sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey 108 | } 109 | } 110 | workloadProfiles: concat( 111 | [ 112 | { 113 | name: 'Consumption' 114 | workloadProfileType: 'Consumption' 115 | } 116 | ], 117 | workloadProfiles 118 | ) 119 | customDomainConfiguration: !empty(customDomainConfig.dnsSuffix) ? customDomainConfig : {} 120 | vnetConfiguration: !empty(vnetConfig.infrastructureSubnetId) ? vnetConfig : {} 121 | zoneRedundant: zoneRedundant 122 | daprAIConnectionString: applicationInsights.properties.ConnectionString 123 | } 124 | } 125 | 126 | @description('ID for the deployed Container Apps Environment resource.') 127 | output id string = containerAppsEnvironment.id 128 | @description('Name for the deployed Container Apps Environment resource.') 129 | output name string = containerAppsEnvironment.name 130 | @description('Default domain for the deployed Container Apps Environment resource.') 131 | output defaultDomain string = containerAppsEnvironment.properties.defaultDomain 132 | @description('Static IP for the deployed Container Apps Environment resource.') 133 | output staticIp string = containerAppsEnvironment.properties.staticIp 134 | 135 | @description('Static IP for the deployed Container Apps Environment resource.') 136 | output properties object = containerAppsEnvironment.properties 137 | -------------------------------------------------------------------------------- /test_client.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "ee9103b5", 6 | "metadata": {}, 7 | "source": [ 8 | "# Durable Functions Client Test\n", 9 | "\n", 10 | "This notebook sends a POST request to the local Azure Functions endpoint `http://localhost:7071/api/client` using the Python `requests` library.\n", 11 | "\n", 12 | "Steps:\n", 13 | "1. Define the JSON payload with a `records` array.\n", 14 | "2. Send POST request; receive status URLs from Durable Functions (includes `statusQueryGetUri`).\n", 15 | "3. Poll the status URL until the orchestration finishes.\n", 16 | "4. Display final output.\n", 17 | "\n", 18 | "Run the cells in order. Ensure the function host is running locally (e.g. via `func start` or your `startLocal.sh`)." 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "id": "a002448d", 25 | "metadata": {}, 26 | "outputs": [], 27 | "source": [ 28 | "# Define the payload matching sampleJSONrequest.json\n", 29 | "payload = {\n", 30 | " \"name\": \"role_library-3.pdf\",\n", 31 | " \"container\": \"bronze\"\n", 32 | "}\n", 33 | "payload" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": null, 39 | "id": "489c2ee7", 40 | "metadata": {}, 41 | "outputs": [], 42 | "source": [ 43 | "import requests, json, time\n", 44 | "\n", 45 | "FUNCTION_ENDPOINT = \"http://localhost:7071/api/client\"\n", 46 | "# FUNCTION_ENDPOINT = \"https://func-processing-yt7giyre5immc.azurewebsites.net/api/client?code=\"\n", 47 | "# Send POST request to start orchestration\n", 48 | "response = requests.post(FUNCTION_ENDPOINT, json=payload)\n", 49 | "print(\"Status Code:\", response.status_code)\n", 50 | "\n", 51 | "if response.status_code != 202:\n", 52 | " print(\"Unexpected response:\", response.text)\n", 53 | "else:\n", 54 | " # Durable Functions returns JSON with status URLs\n", 55 | " start_info = response.json()\n", 56 | " # Display keys of interest\n", 57 | " for k in [\"id\", \"statusQueryGetUri\", \"sendEventPostUri\", \"terminatePostUri\", \"purgeHistoryDeleteUri\"]:\n", 58 | " print(f\"{k}: {start_info.get(k)}\")\n", 59 | "\n", 60 | "print(start_info)" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": null, 66 | "id": "904e41d1", 67 | "metadata": {}, 68 | "outputs": [], 69 | "source": [ 70 | "import requests, time, json\n", 71 | "\n", 72 | "# Helper to poll the orchestration status until completion or timeout\n", 73 | "TERMINAL_STATUSES = {\"Completed\", \"Failed\", \"Terminated\"}\n", 74 | "\n", 75 | "def poll_status(status_url: str, interval_seconds: float = 2.0, timeout_seconds: float = 120.0):\n", 76 | " \"\"\"Poll the Durable Functions statusQueryGetUri until a terminal status.\n", 77 | " Returns the final status document (dict) or raises TimeoutError.\n", 78 | " \"\"\"\n", 79 | " start = time.time()\n", 80 | " attempt = 0\n", 81 | " while True:\n", 82 | " attempt += 1\n", 83 | " r = requests.get(status_url)\n", 84 | " if r.status_code != 200:\n", 85 | " print(f\"Attempt {attempt}: Unexpected status code {r.status_code} -> {r.text[:200]}\")\n", 86 | " else:\n", 87 | " status_doc = r.json()\n", 88 | " runtime_status = status_doc.get(\"runtimeStatus\")\n", 89 | " print(f\"Attempt {attempt}: runtimeStatus={runtime_status}\")\n", 90 | " if runtime_status in TERMINAL_STATUSES:\n", 91 | " return status_doc\n", 92 | " if time.time() - start > timeout_seconds:\n", 93 | " raise TimeoutError(f\"Polling exceeded {timeout_seconds} seconds without terminal status.\")\n", 94 | " time.sleep(interval_seconds)\n", 95 | "\n", 96 | "# Extract the status URL from the earlier cell output (start_info)\n", 97 | "status_url = start_info.get(\"statusQueryGetUri\")\n", 98 | "if not status_url:\n", 99 | " raise ValueError(\"statusQueryGetUri not found in start_info. Run the previous cell first.\")\n", 100 | "\n", 101 | "final_status = poll_status(status_url)\n", 102 | "\n", 103 | "print(\"\\nFinal Status Document:\\n\")\n", 104 | "print(json.dumps(final_status, indent=2))\n", 105 | "\n", 106 | "# If Completed, the orchestrator's output is in 'output'\n", 107 | "if final_status.get(\"runtimeStatus\") == \"Completed\":\n", 108 | " print(\"\\nOrchestrator Output:\\n\")\n", 109 | " print(json.dumps(final_status.get(\"output\"), indent=2))\n", 110 | "else:\n", 111 | " print(\"\\nOrchestration did not complete successfully.\")" 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": null, 117 | "id": "332192e7", 118 | "metadata": {}, 119 | "outputs": [], 120 | "source": [] 121 | } 122 | ], 123 | "metadata": { 124 | "kernelspec": { 125 | "display_name": ".venv", 126 | "language": "python", 127 | "name": "python3" 128 | }, 129 | "language_info": { 130 | "codemirror_mode": { 131 | "name": "ipython", 132 | "version": 3 133 | }, 134 | "file_extension": ".py", 135 | "mimetype": "text/x-python", 136 | "name": "python", 137 | "nbconvert_exporter": "python", 138 | "pygments_lexer": "ipython3", 139 | "version": "3.12.3" 140 | } 141 | }, 142 | "nbformat": 4, 143 | "nbformat_minor": 5 144 | } 145 | -------------------------------------------------------------------------------- /infra/modules/storage/storage-account.bicep: -------------------------------------------------------------------------------- 1 | import { roleAssignmentInfo } from '../security/managed-identity.bicep' 2 | 3 | @description('Name of the resource.') 4 | param name string 5 | @description('Location to deploy the resource. Defaults to the location of the resource group.') 6 | param location string = resourceGroup().location 7 | @description('Tags for the resource.') 8 | param tags object = {} 9 | @description('MSI id for resource.') 10 | param identityId string? 11 | @description('Whether to enable public network access. Defaults to Enabled.') 12 | @allowed([ 13 | 'Enabled' 14 | 'Disabled' 15 | ]) 16 | param publicNetworkAccess string = 'Enabled' 17 | param existingStorageResourceGroupName string 18 | param storageReuse bool 19 | param deployStorageAccount bool = true 20 | 21 | param allowBlobPublicAccess bool = false 22 | param allowCrossTenantReplication bool = true 23 | param allowSharedKeyAccess bool = false 24 | param defaultToOAuthAuthentication bool = false 25 | param deleteRetentionPolicy object = {} 26 | @allowed([ 'AzureDnsZone', 'Standard' ]) 27 | param dnsEndpointType string = 'Standard' 28 | param kind string = 'StorageV2' 29 | param minimumTlsVersion string = 'TLS1_2' 30 | param containers array = [] 31 | param networkAcls object = { 32 | defaultAction: 'Allow' 33 | bypass: 'AzureServices' 34 | ipRules: [] 35 | virtualNetworkRules: [] 36 | resourceAccessRules: [] 37 | } 38 | 39 | @export() 40 | @description('SKU information for Storage Account.') 41 | type skuInfo = { 42 | @description('Name of the SKU.') 43 | name: 44 | | 'Premium_LRS' 45 | | 'Premium_ZRS' 46 | | 'Standard_GRS' 47 | | 'Standard_GZRS' 48 | | 'Standard_LRS' 49 | | 'Standard_RAGRS' 50 | | 'Standard_RAGZRS' 51 | | 'Standard_ZRS' 52 | } 53 | 54 | @export() 55 | @description('Information about the blob container retention policy for the Storage Account.') 56 | type blobContainerRetentionInfo = { 57 | @description('Indicates whether permanent deletion is allowed for blob containers.') 58 | allowPermanentDelete: bool 59 | @description('Number of days to retain blobs.') 60 | days: int 61 | @description('Indicates whether the retention policy is enabled.') 62 | enabled: bool 63 | } 64 | 65 | @description('Storage Account SKU. Defaults to Standard_LRS.') 66 | param sku skuInfo = { 67 | name: 'Standard_LRS' 68 | } 69 | 70 | @description('Access tier for the Storage Account. If the sku is a premium SKU, this will be ignored. Defaults to Hot.') 71 | @allowed([ 'Hot', 'Cool', 'Premium' ]) 72 | param accessTier string = 'Hot' 73 | 74 | @description('Blob container retention policy for the Storage Account. Defaults to disabled.') 75 | param blobContainerRetention blobContainerRetentionInfo = { 76 | allowPermanentDelete: false 77 | days: 7 78 | enabled: false 79 | } 80 | @description('Whether to disable local (key-based) authentication. Defaults to true.') 81 | param disableLocalAuth bool = false 82 | @description('Role assignments to create for the Storage Account.') 83 | param roleAssignments roleAssignmentInfo[] = [] 84 | 85 | resource existingStorage 'Microsoft.Storage/storageAccounts@2024-01-01' existing = if (storageReuse && deployStorageAccount) { 86 | scope: resourceGroup(existingStorageResourceGroupName) 87 | name: name 88 | } 89 | 90 | resource newStorageAccount 'Microsoft.Storage/storageAccounts@2024-01-01' = { 91 | name: name 92 | location: location 93 | tags: tags 94 | kind: 'StorageV2' 95 | sku: sku 96 | identity: { 97 | type: identityId == null ? 'SystemAssigned' : 'UserAssigned' 98 | userAssignedIdentities: identityId == null 99 | ? null 100 | : { 101 | '${identityId}': {} 102 | } 103 | } 104 | properties: { 105 | accessTier: startsWith(sku.name, 'Premium') ? 'Premium' : accessTier 106 | networkAcls: networkAcls 107 | publicNetworkAccess: publicNetworkAccess 108 | allowBlobPublicAccess: allowBlobPublicAccess 109 | allowCrossTenantReplication: allowCrossTenantReplication 110 | allowSharedKeyAccess: !disableLocalAuth 111 | supportsHttpsTrafficOnly: true 112 | dnsEndpointType: dnsEndpointType 113 | minimumTlsVersion: minimumTlsVersion 114 | encryption: { 115 | services: { 116 | blob: { 117 | enabled: true 118 | } 119 | file: { 120 | enabled: true 121 | } 122 | table: { 123 | enabled: true 124 | } 125 | queue: { 126 | enabled: true 127 | } 128 | } 129 | keySource: 'Microsoft.Storage' 130 | } 131 | } 132 | 133 | resource blobServices 'blobServices@2024-01-01' = { 134 | name: 'default' 135 | properties: { 136 | containerDeleteRetentionPolicy: blobContainerRetention 137 | } 138 | resource container 'containers' = [for container in containers: { 139 | name: container.name 140 | properties: { 141 | publicAccess: contains(container, 'publicAccess') ? container.publicAccess : 'None' 142 | } 143 | }] 144 | } 145 | } 146 | 147 | resource assignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = [ 148 | for roleAssignment in roleAssignments: { 149 | name: guid(newStorageAccount.id, roleAssignment.principalId, roleAssignment.roleDefinitionId) 150 | scope: newStorageAccount 151 | properties: { 152 | principalId: roleAssignment.principalId 153 | roleDefinitionId: roleAssignment.roleDefinitionId 154 | principalType: roleAssignment.principalType 155 | } 156 | } 157 | ] 158 | 159 | @description('ID for the deployed Storage Account resource.') 160 | output id string = !deployStorageAccount ? '' : storageReuse ? existingStorage.id : newStorageAccount.id 161 | @description('Name for the deployed Storage Account resource.') 162 | output name string = !deployStorageAccount ? '' : storageReuse ? existingStorage.name : newStorageAccount.name 163 | 164 | output primaryEndpoints object = !deployStorageAccount ? {} : storageReuse ? existingStorage.properties.primaryEndpoints: newStorageAccount.properties.primaryEndpoints 165 | -------------------------------------------------------------------------------- /deploy-testvm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # deploy-testvm.sh - Standalone test VM deployment (VM only, no Bastion) 3 | 4 | set -e 5 | 6 | # Color codes 7 | RED='\033[0;31m' 8 | GREEN='\033[0;32m' 9 | YELLOW='\033[1;33m' 10 | NC='\033[0m' 11 | 12 | echo -e "${GREEN}=== Test VM Deployment Script (VM Only) ===${NC}" 13 | 14 | # ============================================================================= 15 | # CONFIGURATION - Set your parameters here 16 | # ============================================================================= 17 | 18 | # Option 1: Read from deployment-outputs.json (if it exists) 19 | DEPLOYMENT_OUTPUTS="./infra/deployment-outputs.json" 20 | 21 | if [ -f "$DEPLOYMENT_OUTPUTS" ]; then 22 | echo -e "${GREEN}Reading configuration from deployment-outputs.json...${NC}" 23 | RESOURCE_GROUP=$(jq -r '.resourcE_GROUP.value' "$DEPLOYMENT_OUTPUTS") 24 | 25 | # Try to get vnet name from outputs, fallback to constructed name 26 | ENVIRONMENT_NAME=$(az group show --name "$RESOURCE_GROUP" --query "tags.\"azd-env-name\"" -o tsv 2>/dev/null || echo "dev") 27 | else 28 | # Option 2: Set manually 29 | echo -e "${YELLOW}No deployment-outputs.json found. Using manual configuration.${NC}" 30 | RESOURCE_GROUP="rg-dev" # Change this to your resource group name 31 | ENVIRONMENT_NAME="dev" # Change this to your environment name 32 | fi 33 | 34 | # Common parameters 35 | LOCATION="eastus2" # Change this to your desired location 36 | VNET_NAME="" # Leave empty to create a new VNet automatically 37 | SUBNET_NAME="default" # Subnet name (will be created if VNet is created) 38 | 39 | # VM Configuration 40 | VM_USERNAME="adp-user" 41 | VM_SIZE="Standard_D8s_v5" 42 | VM_IMAGE_SKU="win11-25h2-ent" 43 | VM_IMAGE_PUBLISHER="MicrosoftWindowsDesktop" 44 | VM_IMAGE_OFFER="windows-11" 45 | 46 | # VNet Configuration (only used if creating new VNet) 47 | VNET_ADDRESS_PREFIX="10.0.0.0/16" 48 | SUBNET_ADDRESS_PREFIX="10.0.0.0/24" 49 | 50 | # Password (will prompt if not set) 51 | VM_PASSWORD="${1:-}" # Can pass as first argument 52 | 53 | # ============================================================================= 54 | # Script Logic 55 | # ============================================================================= 56 | 57 | # Prompt for password if not provided 58 | if [ -z "$VM_PASSWORD" ]; then 59 | echo -e "${YELLOW}Enter VM admin password (6-72 characters):${NC}" 60 | read -s VM_PASSWORD 61 | echo "" 62 | fi 63 | 64 | # Validate password 65 | if [ ${#VM_PASSWORD} -lt 6 ] || [ ${#VM_PASSWORD} -gt 72 ]; then 66 | echo -e "${RED}Error: Password must be between 6-72 characters${NC}" 67 | exit 1 68 | fi 69 | 70 | # Create resource group if it doesn't exist 71 | echo -e "${GREEN}Checking resource group...${NC}" 72 | if ! az group show --name "$RESOURCE_GROUP" &>/dev/null; then 73 | echo -e "${YELLOW}Resource group '$RESOURCE_GROUP' does not exist. Creating...${NC}" 74 | az group create --name "$RESOURCE_GROUP" --location "$LOCATION" --tags "azd-env-name=$ENVIRONMENT_NAME" 75 | echo -e "${GREEN}Resource group created.${NC}" 76 | else 77 | echo -e "${GREEN}Resource group '$RESOURCE_GROUP' exists.${NC}" 78 | # Get location from existing resource group 79 | LOCATION=$(az group show --name "$RESOURCE_GROUP" --query location -o tsv) 80 | fi 81 | 82 | # Check if VNet should be auto-detected 83 | if [ -z "$VNET_NAME" ]; then 84 | echo -e "${GREEN}Auto-detecting VNet...${NC}" 85 | DETECTED_VNET=$(az network vnet list --resource-group "$RESOURCE_GROUP" --query "[0].name" -o tsv 2>/dev/null) 86 | 87 | if [ -n "$DETECTED_VNET" ] && [ "$DETECTED_VNET" != "null" ]; then 88 | VNET_NAME="$DETECTED_VNET" 89 | echo -e "${GREEN}Found existing VNet: ${NC}$VNET_NAME" 90 | 91 | # Auto-detect subnet 92 | DETECTED_SUBNET=$(az network vnet subnet list --resource-group "$RESOURCE_GROUP" --vnet-name "$VNET_NAME" --query "[0].name" -o tsv 2>/dev/null) 93 | if [ -n "$DETECTED_SUBNET" ] && [ "$DETECTED_SUBNET" != "null" ]; then 94 | SUBNET_NAME="$DETECTED_SUBNET" 95 | echo -e "${GREEN}Found existing subnet: ${NC}$SUBNET_NAME" 96 | fi 97 | else 98 | echo -e "${YELLOW}No VNet found. A new VNet will be created automatically.${NC}" 99 | fi 100 | fi 101 | 102 | echo "" 103 | echo -e "${GREEN}Deployment Configuration:${NC}" 104 | echo -e " Resource Group: ${YELLOW}$RESOURCE_GROUP${NC}" 105 | echo -e " Location: ${YELLOW}$LOCATION${NC}" 106 | if [ -n "$VNET_NAME" ]; then 107 | echo -e " VNet Name: ${YELLOW}$VNET_NAME (existing)${NC}" 108 | echo -e " Subnet Name: ${YELLOW}$SUBNET_NAME${NC}" 109 | else 110 | echo -e " VNet: ${YELLOW}Will be created automatically${NC}" 111 | echo -e " VNet Address: ${YELLOW}$VNET_ADDRESS_PREFIX${NC}" 112 | echo -e " Subnet Address: ${YELLOW}$SUBNET_ADDRESS_PREFIX${NC}" 113 | fi 114 | echo -e " Environment: ${YELLOW}$ENVIRONMENT_NAME${NC}" 115 | echo -e " VM Size: ${YELLOW}$VM_SIZE${NC}" 116 | echo -e " VM Image: ${YELLOW}$VM_IMAGE_PUBLISHER/$VM_IMAGE_OFFER/$VM_IMAGE_SKU${NC}" 117 | echo "" 118 | 119 | # Build parameters 120 | PARAMS="location=\"$LOCATION\" environmentName=\"$ENVIRONMENT_NAME\" vmUserName=\"$VM_USERNAME\" vmUserInitialPassword=\"$VM_PASSWORD\" vmSize=\"$VM_SIZE\" vmImageSku=\"$VM_IMAGE_SKU\" vmImagePublisher=\"$VM_IMAGE_PUBLISHER\" vmImageOffer=\"$VM_IMAGE_OFFER\"" 121 | 122 | if [ -n "$VNET_NAME" ]; then 123 | PARAMS="$PARAMS vnetName=\"$VNET_NAME\" subnetName=\"$SUBNET_NAME\"" 124 | else 125 | PARAMS="$PARAMS vnetAddressPrefix=\"$VNET_ADDRESS_PREFIX\" subnetAddressPrefix=\"$SUBNET_ADDRESS_PREFIX\"" 126 | fi 127 | 128 | # Deploy 129 | echo -e "${GREEN}Deploying Test VM...${NC}" 130 | eval "az deployment group create \ 131 | --resource-group \"$RESOURCE_GROUP\" \ 132 | --template-file ./infra/deploy-testvm.bicep \ 133 | --parameters $PARAMS" 134 | 135 | echo "" 136 | echo -e "${GREEN}=== Deployment Complete ===${NC}" 137 | echo "" 138 | echo -e "${YELLOW}VM deployed without Bastion.${NC}" 139 | echo "" 140 | echo -e "${YELLOW}To connect to the VM:${NC}" 141 | echo "Option 1: Deploy Azure Bastion separately" 142 | echo "Option 2: Add a public IP to the VM" 143 | echo "Option 3: Use VPN/ExpressRoute to connect to the VNet" 144 | echo "" 145 | echo "VM Name: Look for 'testvm-' in resource group: $RESOURCE_GROUP" -------------------------------------------------------------------------------- /infra/modules/vm/dsvm.bicep: -------------------------------------------------------------------------------- 1 | param location string 2 | // var _location = 'westus' 3 | param name string 4 | param tags object = {} 5 | param subnetId string 6 | param bastionSubId string 7 | @secure() 8 | param vmUserPassword string 9 | param vmUserName string 10 | param authenticationType string = 'password' //'sshPublicKey' 11 | @secure() 12 | param vmUserPasswordKey string 13 | param keyVaultName string 14 | param principalId string 15 | param azdEnvironmentName string 16 | 17 | var vmSize = { 18 | 'CPU-4GB': 'Standard_B2s' 19 | 'CPU-7GB': 'Standard_D2s_v3' 20 | 'CPU-8GB': 'Standard_D2s_v3' 21 | 'CPU-14GB': 'Standard_D4s_v3' 22 | 'CPU-16GB': 'Standard_D4s_v3' 23 | 'GPU-56GB': 'Standard_NC6_Promo' 24 | } 25 | var publicIpName = '${name}PublicIp' 26 | var nicName = '${name}Nic' 27 | var diskName = '${name}Disk' 28 | var bastionName = '${name}Bastion' 29 | 30 | var bastionZones = [ 31 | '1' 32 | '2' 33 | '3' 34 | ] 35 | 36 | 37 | var linuxConfiguration = { 38 | disablePasswordAuthentication: true 39 | ssh: { 40 | publicKeys: [ 41 | { 42 | path: '/home/${vmUserName}/.ssh/authorized_keys' 43 | keyData: vmUserPassword 44 | } 45 | ] 46 | } 47 | } 48 | 49 | resource bastionPublicIp 'Microsoft.Network/publicIPAddresses@2024-05-01' = { 50 | name: publicIpName 51 | location: location 52 | sku: { 53 | name: 'Standard' 54 | } 55 | zones: bastionZones 56 | properties: { 57 | publicIPAllocationMethod: 'Static' 58 | } 59 | } 60 | 61 | resource nic 'Microsoft.Network/networkInterfaces@2024-05-01' = { 62 | name: nicName 63 | location: location 64 | properties: { 65 | ipConfigurations: [ 66 | { 67 | name: 'ipconfig1' 68 | properties: { 69 | privateIPAddressVersion: 'IPv4' 70 | privateIPAllocationMethod: 'Dynamic' 71 | subnet: { 72 | id: subnetId 73 | } 74 | } 75 | } 76 | ] 77 | } 78 | } 79 | 80 | 81 | 82 | 83 | resource virtualMachine 'Microsoft.Compute/virtualMachines@2024-11-01' = { 84 | name: name 85 | location: location 86 | tags: tags 87 | identity: { 88 | type: 'SystemAssigned' 89 | } 90 | properties: { 91 | hardwareProfile: { 92 | vmSize: vmSize['CPU-16GB'] 93 | } 94 | storageProfile: { 95 | imageReference: { 96 | publisher: 'microsoft-dsvm' 97 | offer: 'dsvm-win-2022' 98 | sku: 'winserver-2022' 99 | version: 'latest' 100 | } 101 | osDisk: { 102 | name: diskName 103 | createOption: 'FromImage' 104 | } 105 | } 106 | osProfile: { 107 | computerName: 'adp-vm' 108 | adminUsername: vmUserName 109 | adminPassword: vmUserPassword 110 | linuxConfiguration: ((authenticationType == 'password') ? json('null') : linuxConfiguration) 111 | } 112 | networkProfile: { 113 | networkInterfaces: [ 114 | { 115 | id: nic.id 116 | } 117 | ] 118 | } 119 | } 120 | } 121 | 122 | var fileUris = [ 123 | 'https://raw.githubusercontent.com/givenscj/ai-document-processor/refs/heads/cjg-zta-durable/infra/install.ps1' 124 | ] 125 | 126 | resource cse 'Microsoft.Compute/virtualMachines/extensions@2024-11-01' = { 127 | parent: virtualMachine 128 | name: 'cse' 129 | location: location 130 | properties: { 131 | publisher: 'Microsoft.Compute' 132 | type: 'CustomScriptExtension' 133 | typeHandlerVersion: '1.10' 134 | autoUpgradeMinorVersion: true 135 | forceUpdateTag: 'alwaysRun' 136 | settings: { 137 | fileUris: fileUris 138 | commandToExecute: 'powershell.exe -ExecutionPolicy Unrestricted -File install.ps1 -AzureTenantId ${subscription().tenantId} -AzureSubscriptionId ${subscription().subscriptionId} -AzureResourceGroupName ${resourceGroup().name} -AzdEnvName ${azdEnvironmentName}' 139 | } 140 | protectedSettings: { 141 | 142 | } 143 | } 144 | } 145 | 146 | output vmPrincipalId string = virtualMachine.identity.principalId 147 | 148 | resource cy 'Microsoft.Network/bastionHosts@2024-05-01' = { 149 | name: bastionName 150 | location: location 151 | sku: { 152 | name: 'Standard' 153 | } 154 | zones : bastionZones 155 | properties: { 156 | ipConfigurations: [ 157 | { 158 | name: 'ipconfig1' 159 | properties: { 160 | privateIPAllocationMethod: 'Dynamic' 161 | subnet: { 162 | id: bastionSubId 163 | } 164 | publicIPAddress: { 165 | id: bastionPublicIp.id // use a public IP address for the bastion 166 | } 167 | } 168 | } 169 | ] 170 | } 171 | } 172 | 173 | 174 | resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 175 | name: guid(subscription().id, resourceGroup().id, 'Contributor') 176 | scope: resourceGroup() 177 | properties: { 178 | roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') 179 | principalId: virtualMachine.identity.principalId 180 | } 181 | } 182 | 183 | // Using key vault to store the password. 184 | // Not using the application key vault as it is set with no public network access for zero trust, but Bastion need the public network access 185 | // to pul the secret from the key vault. 186 | resource keyVault 'Microsoft.KeyVault/vaults@2024-11-01' = { 187 | name: keyVaultName 188 | location: location 189 | tags: tags 190 | properties: { 191 | sku: { 192 | name: 'standard' 193 | family: 'A' 194 | } 195 | tenantId: subscription().tenantId 196 | accessPolicies: [ 197 | ] 198 | enableRbacAuthorization: true 199 | } 200 | } 201 | 202 | resource vmUserPasswordSecret 'Microsoft.KeyVault/vaults/secrets@2024-11-01' = { 203 | parent: keyVault 204 | name: vmUserPasswordKey 205 | properties: { 206 | value: vmUserPassword 207 | } 208 | } 209 | 210 | resource KeyVaultAccessRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 211 | name: guid(subscription().id, resourceGroup().id, principalId, keyVault.id, 'Key Vault Secrets Officer') 212 | scope: keyVault 213 | properties: { 214 | principalId: principalId 215 | roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b86a8fe4-44ce-4948-aee5-eccb2c155cd7') 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /pipeline/configuration/configuration.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | from azure.identity import DefaultAzureCredential 4 | from azure.appconfiguration.provider import ( 5 | AzureAppConfigurationKeyVaultOptions, 6 | load 7 | ) 8 | import logging 9 | 10 | 11 | logging.basicConfig( 12 | level=logging.INFO, 13 | format='%(asctime)s %(levelname)s %(name)s: %(message)s', 14 | handlers=[logging.StreamHandler()] 15 | ) 16 | logger = logging.getLogger(__name__) 17 | azure_id_logger = logging.getLogger("azure.identity") 18 | azure_id_logger.setLevel(logging.DEBUG) 19 | 20 | from tenacity import retry, wait_random_exponential, stop_after_attempt, RetryError 21 | 22 | class Configuration: 23 | 24 | credential = None 25 | 26 | def __init__(self): 27 | logger.info("Configuration initialization started") 28 | try: 29 | self.tenant_id = os.environ.get('AZURE_TENANT_ID', "*") 30 | except Exception as e: 31 | raise e 32 | 33 | if os.environ.get("AZURE_FUNCTIONS_ENVIRONMENT") == "Development": 34 | self.credential = DefaultAzureCredential( 35 | additionally_allowed_tenants=self.tenant_id, 36 | exclude_environment_credential=True, 37 | exclude_managed_identity_credential=True, 38 | exclude_cli_credential=False, 39 | exclude_powershell_credential=False, 40 | exclude_shared_token_cache_credential=True, 41 | exclude_developer_cli_credential=False, 42 | exclude_interactive_browser_credential=True 43 | ) 44 | else: 45 | self.credential = DefaultAzureCredential( 46 | additionally_allowed_tenants=self.tenant_id, 47 | exclude_environment_credential=True, 48 | exclude_managed_identity_credential=False, 49 | exclude_cli_credential=True, 50 | exclude_powershell_credential=True, 51 | exclude_shared_token_cache_credential=True, 52 | exclude_developer_cli_credential=True, 53 | exclude_interactive_browser_credential=True 54 | ) 55 | 56 | logger.info(f"Using DefaultAzureCredential with tenant ID: {self.tenant_id}") 57 | 58 | try: 59 | logger.info("Attempting APP_CONFIGURATION_URI for configuration.") 60 | logger.info(f"Using APP_CONFIGURATION_URI: {os.environ['APP_CONFIGURATION_URI']}") 61 | app_config_uri = os.environ['APP_CONFIGURATION_URI'] 62 | logger.info(f"Using endpoint: {app_config_uri} and credential: {self.credential} and key vault credenial: {self.credential}") 63 | self.config = load(endpoint=app_config_uri, credential=self.credential,key_vault_options=AzureAppConfigurationKeyVaultOptions(credential=self.credential)) 64 | except Exception as e: 65 | try: 66 | 67 | logger.info(f"Using AZURE_APPCONFIG_CONNECTION_STRING for configuration: {os.environ['AZURE_APPCONFIG_CONNECTION_STRING']}") 68 | connection_string = os.environ["AZURE_APPCONFIG_CONNECTION_STRING"] 69 | logging.info(f"Using connection string: {connection_string}") 70 | # Connect to Azure App Configuration using a connection string. 71 | self.config = load( 72 | connection_string=connection_string, 73 | key_vault_options=AzureAppConfigurationKeyVaultOptions(credential=self.credential) 74 | ) 75 | except Exception as e: 76 | raise Exception("Unable to connect to Azure App Configuration. Please check your connection string or endpoint. Error: " + str(e)) 77 | 78 | # Connect to Azure App Configuration. 79 | 80 | def get_value(self, key: str, default: str = None) -> str: 81 | 82 | if key is None: 83 | raise Exception('The key parameter is required for get_value().') 84 | 85 | value = None 86 | 87 | allow_env_vars = False 88 | if "allow_environment_variables" in os.environ: 89 | allow_env_vars = bool(os.environ[ 90 | "allow_environment_variables" 91 | ]) 92 | 93 | if allow_env_vars is True: 94 | value = os.environ.get(key) 95 | 96 | if value is None: 97 | try: 98 | value = self.get_config_with_retry(name=key) 99 | except Exception as e: 100 | pass 101 | 102 | if value is not None: 103 | return value 104 | else: 105 | if default is not None: 106 | return default 107 | 108 | raise Exception(f'The configuration variable {key} not found.') 109 | 110 | def retry_before_sleep(self, retry_state): 111 | # Log the outcome of each retry attempt. 112 | message = f"""Retrying {retry_state.fn}: 113 | attempt {retry_state.attempt_number} 114 | ended with: {retry_state.outcome}""" 115 | if retry_state.outcome.failed: 116 | ex = retry_state.outcome.exception() 117 | message += f"; Exception: {ex.__class__.__name__}: {ex}" 118 | if retry_state.attempt_number < 1: 119 | logging.info(message) 120 | else: 121 | logging.warning(message) 122 | 123 | @retry( 124 | wait=wait_random_exponential(multiplier=1, max=5), 125 | stop=stop_after_attempt(5), 126 | before_sleep=retry_before_sleep 127 | ) 128 | def get_config_with_retry(self, name): 129 | try: 130 | return self.config[name] 131 | except RetryError: 132 | pass 133 | 134 | # Helper functions for reading environment variables 135 | def read_env_variable(self, var_name, default=None): 136 | value = self.get_value(var_name, default) 137 | return value.strip() if value else default 138 | 139 | def read_env_list(self, var_name): 140 | value = self.get_value(var_name, "") 141 | return [item.strip() for item in value.split(",") if item.strip()] 142 | 143 | def read_env_boolean(self, var_name, default=False): 144 | value = self.get_value(var_name, str(default)).strip().lower() 145 | return value in ['true', '1', 'yes'] -------------------------------------------------------------------------------- /pipeline/function_app.py: -------------------------------------------------------------------------------- 1 | import azure.functions as func 2 | import azure.durable_functions as df 3 | 4 | 5 | from activities import getBlobContent, runDocIntel, callAoai, writeToBlob, speechToText, callAoaiMultiModal 6 | from configuration import Configuration 7 | 8 | from pipelineUtils.blob_functions import BlobMetadata 9 | 10 | config = Configuration() 11 | 12 | # NEXT_STAGE = config.get_value("NEXT_STAGE") 13 | FINAL_OUTPUT_CONTAINER = config.get_value("FINAL_OUTPUT_CONTAINER") 14 | 15 | app = df.DFApp(http_auth_level=func.AuthLevel.FUNCTION) 16 | 17 | import logging 18 | 19 | # # Blob-triggered starter 20 | @app.function_name(name="start_orchestrator_on_blob") 21 | @app.blob_trigger( 22 | arg_name="blob", 23 | path="bronze/{name}", 24 | connection="DataStorage", 25 | ) 26 | @app.durable_client_input(client_name="client") 27 | async def start_orchestrator_blob( 28 | blob: func.InputStream, 29 | client: df.DurableOrchestrationClient, 30 | ): 31 | logging.info(f" Blob Trigger (start_orchestrator_blob) - Blob Received: {blob}") 32 | logging.info(f"path: {blob.name}") 33 | logging.info(f"Size: {blob.length} bytes") 34 | logging.info(f"URI: {blob.uri}") 35 | 36 | blob_metadata = BlobMetadata( 37 | name=blob.name, 38 | container="bronze", 39 | uri=blob.uri 40 | ) 41 | logging.info(f"Blob Metadata: {blob_metadata}") 42 | logging.info(f"Blob Metadata JSON: {blob_metadata.to_dict()}") 43 | instance_id = await client.start_new("process_blob", client_input=blob_metadata.to_dict()) 44 | logging.info(f"Started orchestration {instance_id} for blob {blob.name}") 45 | 46 | 47 | # An HTTP-triggered function with a Durable Functions client binding 48 | @app.route(route="client") 49 | @app.durable_client_input(client_name="client") 50 | async def start_orchestrator_http(req: func.HttpRequest, client): 51 | """ 52 | Starts a new orchestration instance and returns a response to the client. 53 | 54 | args: 55 | req (func.HttpRequest): The HTTP request object. Contains an array of JSONs with fields: name, and container 56 | client (DurableOrchestrationClient): The Durable Functions client. 57 | response: 58 | func.HttpResponse: The HTTP response object. 59 | """ 60 | 61 | #Perform basic validation on the request body 62 | try: 63 | body = req.get_json() 64 | blob_name = body.get("name") 65 | blob_uri = body.get("uri") 66 | 67 | except ValueError: 68 | return func.HttpResponse("Invalid JSON.", status_code=400) 69 | 70 | 71 | blob_input = { 72 | "name": blob_name, 73 | "container": "bronze", 74 | "uri": blob_uri 75 | } 76 | 77 | #invoke the process_blob function with the list of blobs 78 | instance_id = await client.start_new('process_blob', client_input=blob_input) 79 | logging.info(f"Started orchestration with Batch ID = '{instance_id}'.") 80 | 81 | response = client.create_check_status_response(req, instance_id) 82 | return response 83 | 84 | 85 | #Sub orchestrator 86 | @app.function_name(name="process_blob") 87 | @app.orchestration_trigger(context_name="context") 88 | def process_blob(context): 89 | blob_input = context.get_input() 90 | sub_orchestration_id = context.instance_id 91 | logging.info(f"Process Blob sub Orchestration - Processing blob_metadata: {blob_input} with sub orchestration id: {sub_orchestration_id}") 92 | # Get file extensions 93 | blob_name = blob_input.get("name", "") 94 | file_extension = blob_name.lower().split('.')[-1] if '.' in blob_name else "" 95 | # Audio file extensions 96 | audio_extensions = ['wav', 'mp3', 'opus', 'ogg', 'flac', 'wma', 'aac', 'webm'] 97 | # Document file extensions 98 | document_extensions = ['pdf', 'docx', 'doc', 'xlsx', 'pptx', 'jpg', 'jpeg', 'png', 'tiff', 'bmp'] 99 | 100 | 101 | # 1. Process Data Source based on file type 102 | if config.get_value("AOAI_MULTI_MODAL", "false").lower() == "true" and file_extension in document_extensions: 103 | aoai_input = { 104 | "name": blob_input.get("name"), 105 | "container": blob_input.get("container"), 106 | "uri": blob_input.get("uri"), 107 | "instance_id": sub_orchestration_id 108 | } 109 | 110 | text_result = yield context.call_activity("callAoaiMultiModal", aoai_input) 111 | 112 | 113 | elif config.get_value("AI_VISION_ENABLED", "false").lower() == "true": 114 | pass 115 | 116 | elif file_extension in audio_extensions: 117 | # Process audio with speech-to-text 118 | logging.info(f"Processing audio file: {blob_name}") 119 | text_result = yield context.call_activity("speechToText", blob_input) 120 | 121 | elif file_extension in document_extensions: 122 | # Process document with Document Intelligence 123 | logging.info(f"Processing document file: {blob_name}") 124 | text_result = yield context.call_activity("runDocIntel", blob_input) 125 | 126 | else: 127 | # Unsupported file type 128 | logging.warning(f"Unsupported file type: {file_extension} for blob: {blob_name}") 129 | return { 130 | "blob": blob_input, 131 | "error": f"Unsupported file type: {file_extension}", 132 | "status": "skipped" 133 | } 134 | 135 | # 2. Feed Output into AOAI to get insights 136 | # Package the data into a dictionary 137 | call_aoai_input = { 138 | "text_result": text_result, 139 | "instance_id": sub_orchestration_id 140 | } 141 | 142 | aoai_output = yield context.call_activity("callAoai", call_aoai_input) 143 | 144 | 145 | # 3. Write AOAI output to Blob Storage 146 | task_result = yield context.call_activity( 147 | "writeToBlob", 148 | { 149 | "json_str": aoai_output, 150 | "blob_name": blob_input["name"], 151 | "final_output_container": FINAL_OUTPUT_CONTAINER 152 | } 153 | ) 154 | return { 155 | "blob": blob_input, 156 | "text_result": aoai_output, 157 | "task_result": task_result 158 | } 159 | 160 | app.register_functions(getBlobContent.bp) 161 | app.register_functions(runDocIntel.bp) 162 | app.register_functions(callAoai.bp) 163 | app.register_functions(writeToBlob.bp) 164 | app.register_functions(speechToText.bp) 165 | app.register_functions(callAoaiMultiModal.bp) -------------------------------------------------------------------------------- /infra/modules/compute/functionApp.bicep: -------------------------------------------------------------------------------- 1 | @description('The name of the function app that you wish to create.') 2 | param appName string 3 | param appPurpose string 4 | @description('Storage Account type') 5 | @allowed([ 6 | 'Standard_LRS' 7 | 'Standard_GRS' 8 | 'Standard_RAGRS' 9 | ]) 10 | param storageAccountType string = 'Standard_LRS' 11 | 12 | @description('Location for all resources.') 13 | param location string 14 | 15 | @description('Tags.') 16 | param tags object 17 | 18 | param linuxFxVersion string = 'Python|3.11' 19 | 20 | param appSettings array = [] 21 | 22 | param networkIsolation bool = false 23 | param identityId string 24 | param principalId string 25 | param clientId string 26 | 27 | @description('The language worker runtime to load in the function app.') 28 | param runtime string = 'python' 29 | param aoaiEndpoint string 30 | param storageAccountName string 31 | param appConfigName string 32 | param hostingPlanName string 33 | param applicationInsightsName string 34 | param virtualNetworkSubnetId string 35 | param funcStorageName string 36 | var functionAppName = appName 37 | var functionWorkerRuntime = runtime 38 | 39 | 40 | var openaiApiVersion = '2024-05-01-preview' 41 | var openaiApiBase = aoaiEndpoint 42 | var openaiModel = 'gpt-4o' 43 | 44 | resource hostingPlan 'Microsoft.Web/serverfarms@2024-04-01' existing = { 45 | name: hostingPlanName 46 | } 47 | 48 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = { 49 | name: applicationInsightsName 50 | } 51 | 52 | resource functionApp 'Microsoft.Web/sites@2024-04-01' = { 53 | name: functionAppName 54 | location: location 55 | kind: 'functionapp,linux' 56 | identity: { 57 | type: 'UserAssigned' 58 | userAssignedIdentities: { 59 | '${identityId}': {} 60 | } 61 | } 62 | tags: tags 63 | properties: { 64 | serverFarmId: hostingPlan.id 65 | publicNetworkAccess: 'Enabled' //this stays enabled even if network isolation is set to true 66 | virtualNetworkSubnetId: networkIsolation ? virtualNetworkSubnetId : null 67 | siteConfig: { 68 | cors: {allowedOrigins: ['https://ms.portal.azure.com', 'https://portal.azure.com'] } 69 | alwaysOn: true 70 | publicNetworkAccess: networkIsolation ? null : 'Enabled' 71 | ipSecurityRestrictionsDefaultAction : networkIsolation ? 'Deny' : 'Allow' 72 | ipSecurityRestrictions: networkIsolation ? [ 73 | { 74 | ipAddress: 'AzureCloud' 75 | tag: 'ServiceTag' 76 | action: 'Allow' 77 | priority: 100 78 | name: 'AllowAzureCloud' 79 | headers: { 80 | } 81 | } 82 | ] : null 83 | appSettings: concat(appSettings, [ 84 | { 85 | name: 'AZURE_CLIENT_ID' 86 | value: clientId 87 | } 88 | { 89 | name: 'AZURE_TENANT_ID' 90 | value: subscription().tenantId 91 | } 92 | { 93 | name:'allow_environment_variables' 94 | value: 'true' 95 | } 96 | { 97 | name: 'AzureWebJobsStorage__credential' 98 | value: 'managedidentity' 99 | } 100 | { 101 | name: 'AzureWebJobsStorage__clientId' 102 | value: clientId 103 | } 104 | { 105 | name: 'AzureWebJobsStorage__accountName' 106 | value: funcStorageName 107 | } 108 | { 109 | name: 'DataStorage__clientId' 110 | value: clientId 111 | } 112 | { 113 | name: 'DataStorage__accountName' 114 | value: storageAccountName 115 | } 116 | { 117 | name: 'DataStorage__credential' 118 | value: 'managedidentity' 119 | } 120 | { 121 | name: 'FUNCTIONS_EXTENSION_VERSION' 122 | value: '~4' 123 | } 124 | { 125 | name: 'APPINSIGHTS_INSTRUMENTATIONKEY' 126 | value: applicationInsights.properties.InstrumentationKey 127 | } 128 | { 129 | name: 'ApplicationInsights__InstrumentationKey' 130 | value: applicationInsights.properties.InstrumentationKey 131 | } 132 | { 133 | name: 'FUNCTIONS_WORKER_RUNTIME' 134 | value: functionWorkerRuntime 135 | } 136 | 137 | { 138 | name: 'ENABLE_ORYX_BUILD' 139 | value: 'true' 140 | } 141 | { 142 | name: 'SCM_DO_BUILD_DURING_DEPLOYMENT' 143 | value: 'true' 144 | } 145 | { 146 | name: 'APP_CONFIGURATION_URI' 147 | value: concat('https://', appConfigName, '.azconfig.io') 148 | } 149 | networkIsolation ? { 150 | name: 'WEBSITE_VNET_ROUTE_ALL' 151 | value: '1' 152 | } : { 153 | name: 'WEBSITE_VNET_ROUTE_ALL' 154 | value: '0' 155 | } 156 | networkIsolation ? { 157 | name: 'WEBSITE_DNS_SERVER' 158 | value: '168.63.129.16' 159 | } : { 160 | name: 'WEBSITE_DNS_SERVER' 161 | value: '' 162 | } 163 | { 164 | name: 'WEBSITE_HTTPLOGGING_RETENTION_DAYS' 165 | value: '7' 166 | } 167 | ], appPurpose == 'processing' ? [ 168 | { 169 | name: 'AzureWebJobsFeatureFlags' 170 | value: 'EnableWorkerIndexing' 171 | } 172 | ] : []) 173 | ftpsState: 'FtpsOnly' 174 | linuxFxVersion: linuxFxVersion 175 | minTlsVersion: '1.2' 176 | } 177 | httpsOnly: true 178 | } 179 | } 180 | 181 | resource authConfig 'Microsoft.Web/sites/config@2024-04-01' = { 182 | parent: functionApp 183 | name: 'authsettingsV2' 184 | properties: { 185 | globalValidation: { 186 | requireAuthentication: false // ✅ Disables authentication (allows anonymous access) 187 | } 188 | platform: { 189 | enabled: false // ✅ Ensures platform authentication is disabled 190 | } 191 | } 192 | } 193 | 194 | output id string = functionApp.id 195 | output name string = functionApp.name 196 | output uri string = 'https://${functionApp.properties.defaultHostName}' 197 | output identityPrincipalId string = principalId 198 | output location string = functionApp.location 199 | output funcStorageName string = funcStorageName 200 | output openaiApiVersion string = openaiApiVersion 201 | output openaiApiBase string = openaiApiBase 202 | output openaiModel string = openaiModel 203 | output functionWorkerRuntime string = functionWorkerRuntime 204 | output hostingPlanName string = hostingPlan.name 205 | output hostingPlanId string = hostingPlan.id 206 | -------------------------------------------------------------------------------- /infra/modules/db/cosmos.bicep: -------------------------------------------------------------------------------- 1 | @description('Cosmos DB account name, max length 44 characters, lowercase') 2 | param accountName string 3 | 4 | @description('Enable/disable public network access for the Cosmos DB account.') 5 | param publicNetworkAccess string = 'Disabled' 6 | 7 | @description('Location for the Cosmos DB account.') 8 | param location string = resourceGroup().location 9 | 10 | param cosmosDbReuse bool 11 | param existingCosmosDbResourceGroupName string 12 | param existingCosmosDbAccountName string 13 | 14 | param deployCosmosDb bool = true 15 | 16 | param conversationContainerName string 17 | param datasourcesContainerName string 18 | 19 | param tags object = {} 20 | 21 | @description('The default consistency level of the Cosmos DB account.') 22 | @allowed([ 23 | 'Eventual' 24 | 'ConsistentPrefix' 25 | 'Session' 26 | 'BoundedStaleness' 27 | 'Strong' 28 | ]) 29 | param defaultConsistencyLevel string = 'Session' 30 | 31 | @description('Max stale requests. Required for BoundedStaleness. Valid ranges, Single Region: 10 to 2147483647. Multi Region: 100000 to 2147483647.') 32 | @minValue(10) 33 | @maxValue(2147483647) 34 | param maxStalenessPrefix int = 100000 35 | 36 | @description('Max lag time (minutes). Required for BoundedStaleness. Valid ranges, Single Region: 5 to 84600. Multi Region: 300 to 86400.') 37 | @minValue(5) 38 | @maxValue(86400) 39 | param maxIntervalInSeconds int = 300 40 | 41 | @description('Enable system managed failover for regions') 42 | param systemManagedFailover bool = true 43 | 44 | @description('The name for the database') 45 | param databaseName string 46 | 47 | @description('The name for the container') 48 | param containerName string = 'promptscontainer' 49 | param configContainerName string = 'config' 50 | 51 | @description('The partition key for the container') 52 | param partitionKeyPath string = '/id' 53 | 54 | @description('Maximum autoscale throughput for the container') 55 | @minValue(1000) 56 | @maxValue(1000000) 57 | param autoscaleMaxThroughput int = 1000 58 | 59 | @description('Time to Live for data in analytical store. (-1 no expiry)') 60 | @minValue(-1) 61 | @maxValue(2147483647) 62 | param analyticalStoreTTL int = -1 63 | 64 | param secretName string = 'azureDBkey' 65 | 66 | param keyVaultName string 67 | 68 | var consistencyPolicy = { 69 | Eventual: { 70 | defaultConsistencyLevel: 'Eventual' 71 | } 72 | ConsistentPrefix: { 73 | defaultConsistencyLevel: 'ConsistentPrefix' 74 | } 75 | Session: { 76 | defaultConsistencyLevel: 'Session' 77 | } 78 | BoundedStaleness: { 79 | defaultConsistencyLevel: 'BoundedStaleness' 80 | maxStalenessPrefix: maxStalenessPrefix 81 | maxIntervalInSeconds: maxIntervalInSeconds 82 | } 83 | Strong: { 84 | defaultConsistencyLevel: 'Strong' 85 | } 86 | } 87 | var locations = [ 88 | { 89 | locationName: location 90 | failoverPriority: 0 91 | isZoneRedundant: false 92 | } 93 | ] 94 | 95 | resource existingAccount 'Microsoft.DocumentDB/databaseAccounts@2024-12-01-preview' existing = if (cosmosDbReuse && deployCosmosDb) { 96 | scope: resourceGroup(existingCosmosDbResourceGroupName) 97 | name: existingCosmosDbAccountName 98 | } 99 | 100 | resource newAccount 'Microsoft.DocumentDB/databaseAccounts@2024-12-01-preview' = if (!cosmosDbReuse && deployCosmosDb) { 101 | name: toLower(accountName) 102 | kind: 'GlobalDocumentDB' 103 | location: location 104 | tags: tags 105 | properties: { 106 | consistencyPolicy: consistencyPolicy[defaultConsistencyLevel] 107 | locations: locations 108 | databaseAccountOfferType: 'Standard' 109 | enableAutomaticFailover: systemManagedFailover 110 | publicNetworkAccess: publicNetworkAccess 111 | enableAnalyticalStorage: true 112 | } 113 | } 114 | 115 | resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2024-12-01-preview' = if (!cosmosDbReuse && deployCosmosDb) { 116 | parent: newAccount 117 | name: databaseName 118 | properties: { 119 | resource: { 120 | id: databaseName 121 | } 122 | } 123 | } 124 | 125 | resource conversationsContainer 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2024-12-01-preview' = if (!cosmosDbReuse && deployCosmosDb) { 126 | parent: database 127 | name: conversationContainerName 128 | properties: { 129 | resource: { 130 | id: conversationContainerName 131 | partitionKey: { 132 | paths: [ 133 | '/id' 134 | ] 135 | kind: 'Hash' 136 | } 137 | analyticalStorageTtl: analyticalStoreTTL 138 | indexingPolicy: { 139 | indexingMode: 'consistent' 140 | includedPaths: [ 141 | { 142 | path: '/*' 143 | } 144 | ] 145 | } 146 | defaultTtl: 86400 147 | } 148 | options: { 149 | autoscaleSettings: { 150 | maxThroughput: autoscaleMaxThroughput 151 | } 152 | } 153 | } 154 | } 155 | 156 | resource modelsContainer 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2024-12-01-preview' = if (!cosmosDbReuse && deployCosmosDb) { 157 | parent: database 158 | name: datasourcesContainerName 159 | properties: { 160 | resource: { 161 | id: datasourcesContainerName 162 | partitionKey: { 163 | paths: [ 164 | '/id' 165 | ] 166 | kind: 'Hash' 167 | } 168 | analyticalStorageTtl: analyticalStoreTTL 169 | indexingPolicy: { 170 | indexingMode: 'none' 171 | automatic: false 172 | } 173 | } 174 | options: { 175 | autoscaleSettings: { 176 | maxThroughput: autoscaleMaxThroughput 177 | } 178 | } 179 | } 180 | } 181 | 182 | resource keyVault 'Microsoft.KeyVault/vaults@2024-12-01-preview' existing = { 183 | name: keyVaultName 184 | } 185 | 186 | resource keyVaultSecret 'Microsoft.KeyVault/vaults/secrets@2024-12-01-preview' = { 187 | name: secretName 188 | tags: tags 189 | parent: keyVault 190 | properties: { 191 | attributes: { 192 | enabled: true 193 | exp: 0 194 | nbf: 0 195 | } 196 | contentType: 'string' 197 | value: !deployCosmosDb ? '' : cosmosDbReuse ? existingAccount.listKeys().primaryMasterKey : newAccount.listKeys().primaryMasterKey 198 | } 199 | } 200 | 201 | 202 | output id string = !deployCosmosDb ? '' : cosmosDbReuse ? existingAccount.id : newAccount.id 203 | output resourceGroupName string = resourceGroup().name 204 | output name string = !deployCosmosDb ? '' : cosmosDbReuse ? existingAccount.name : newAccount.name 205 | output cosmosResourceId string = !deployCosmosDb ? '' : cosmosDbReuse ? existingAccount.id : newAccount.id 206 | output accountName string = !deployCosmosDb ? '' : cosmosDbReuse ? existingAccount.name : newAccount.name 207 | output databaseName string = database.name 208 | output containerName string = conversationsContainer.name 209 | -------------------------------------------------------------------------------- /infra/modules/containers/container-app.bicep: -------------------------------------------------------------------------------- 1 | @description('Name of the resource.') 2 | param name string 3 | @description('Location to deploy the resource. Defaults to the location of the resource group.') 4 | param location string = resourceGroup().location 5 | @description('Tags for the resource.') 6 | param tags object = {} 7 | 8 | @export() 9 | @description('Information about the ingress configuration for the container app.') 10 | type ingressConfigInfo = { 11 | @description('Whether the container app can be accessed externally.') 12 | external: bool 13 | @description('Port to target for the container.') 14 | targetPort: int 15 | @description('Transport protocol for the container app.') 16 | transport: string? 17 | @description('Whether to allow insecure connections to the container app.') 18 | allowInsecure: bool 19 | @description('IP security restrictions for the container app.') 20 | ipSecurityRestrictions: array? 21 | } 22 | 23 | @export() 24 | @description('Information about the resource configuration for the container app.') 25 | type resourceConfigInfo = { 26 | @description('CPU limit for the container.') 27 | cpu: string 28 | @description('Memory limit for the container.') 29 | memory: string 30 | } 31 | 32 | @export() 33 | @description('Information about the scale configuration for the container app.') 34 | type scaleConfigInfo = { 35 | @description('Minimum number of replicas for the container.') 36 | minReplicas: int 37 | @description('Maximum number of replicas for the container.') 38 | maxReplicas: int 39 | @description('Scaling rules for the container.') 40 | rules: array? 41 | } 42 | 43 | @export() 44 | @description('Information about the secret variables for the container app.') 45 | type secretInfo = { 46 | @description('Name of the secret.') 47 | name: string 48 | @description('Value of the secret.') 49 | value: string? 50 | @description('Azure Key Vault secret URI for the secret value.') 51 | keyVaultUrl: string? 52 | @description('Managed Identity ID for accessing the Azure Key Vault.') 53 | identity: string? 54 | } 55 | 56 | @export() 57 | @description('Information about the environment variables for the container app.') 58 | type environmentVariableInfo = { 59 | @description('Name of the environment variable.') 60 | name: string 61 | @description('Value of the environment variable.') 62 | value: string? 63 | @description('Azure Key Vault secret URI for the environment variable value.') 64 | secretRef: string? 65 | } 66 | 67 | @description('ID for the Container Apps Environment associated with the Container App.') 68 | param containerAppsEnvironmentId string 69 | @description('ID for the Managed Identity associated with the Container App.') 70 | param containerAppIdentityId string 71 | @description('Name for the Workload Profile associated with the Container App. Defaults to Consumption.') 72 | param workloadProfileName string = 'Consumption' 73 | @description('Name for the Container Registry associated with the Container App.') 74 | param containerRegistryName string = '' 75 | @description('Whether the container image exists in the Container Registry. Defaults to true.') 76 | param imageInContainerRegistry bool = true 77 | @description('Name for the container image (incl. :tag) associated with the Container App.') 78 | param containerImageName string 79 | @description('Ingress configuration for the container. Defaults to external, target port 80, auto transport, and disallowing insecure connections.') 80 | param containerIngress ingressConfigInfo = { 81 | external: true 82 | targetPort: 80 83 | transport: 'auto' 84 | allowInsecure: false 85 | ipSecurityRestrictions: [] 86 | } 87 | @description('Resource configuration for the container. Defaults to 0.5 CPU and 1.0Gi memory.') 88 | param containerResources resourceConfigInfo = { 89 | cpu: '0.5' 90 | memory: '1.0Gi' 91 | } 92 | @description('Scale configuration for the container. Defaults to min 1 replica, max 3 replicas, with HTTP rule for 20 concurrent requests.') 93 | param containerScale scaleConfigInfo = { 94 | minReplicas: 1 95 | maxReplicas: 3 96 | rules: [ 97 | { 98 | name: 'http' 99 | http: { 100 | metadata: { 101 | concurrentRequests: '20' 102 | } 103 | } 104 | } 105 | ] 106 | } 107 | @description('Environment variables for the container.') 108 | param environmentVariables environmentVariableInfo[] = [] 109 | @description('Secrets for the container.') 110 | param secrets secretInfo[] = [] 111 | @description('Volume definitions for the container.') 112 | param volumes array = [] 113 | @description('Volume mounts for the container.') 114 | param volumeMounts array = [] 115 | @description('Whether Dapr is enabled for the Container App. Defaults to false.') 116 | param daprEnabled bool = false 117 | @description('Name for the Dapr App ID. Required if Dapr is enabled. Defaults to empty.') 118 | param daprAppId string = '' 119 | 120 | resource containerApp 'Microsoft.App/containerApps@2024-10-02-preview' = { 121 | name: name 122 | location: location 123 | tags: tags 124 | identity: { 125 | type: 'UserAssigned' 126 | userAssignedIdentities: { 127 | '${containerAppIdentityId}': {} 128 | } 129 | } 130 | properties: { 131 | environmentId: containerAppsEnvironmentId 132 | workloadProfileName: workloadProfileName 133 | configuration: { 134 | secrets: secrets 135 | registries: imageInContainerRegistry 136 | ? [ 137 | { 138 | server: '${containerRegistryName}.azurecr.io' 139 | identity: containerAppIdentityId 140 | } 141 | ] 142 | : [] 143 | dapr: daprEnabled 144 | ? { 145 | enabled: true 146 | appId: daprAppId 147 | appPort: containerIngress.targetPort 148 | } 149 | : { 150 | enabled: false 151 | } 152 | ingress: containerIngress 153 | } 154 | template: { 155 | containers: [ 156 | { 157 | image: imageInContainerRegistry 158 | ? '${containerRegistryName}.azurecr.io/${containerImageName}' 159 | : containerImageName 160 | name: name 161 | resources: containerResources 162 | env: environmentVariables 163 | volumeMounts: volumeMounts 164 | } 165 | ] 166 | scale: containerScale 167 | volumes: volumes 168 | } 169 | } 170 | } 171 | 172 | @description('ID for the deployed Container App resource.') 173 | output id string = containerApp.id 174 | @description('Name for the deployed Container App resource.') 175 | output name string = containerApp.name 176 | @description('FQDN for the deployed Container App resource.') 177 | output fqdn string = containerApp.properties.configuration.ingress.fqdn 178 | @description('URL for the deployed Container App resource.') 179 | output url string = 'https://${containerApp.properties.configuration.ingress.fqdn}' 180 | @description('Latest revision FQDN for the deployed Container App resource.') 181 | output latestRevisionFqdn string = containerApp.properties.configuration.ingress.fqdn 182 | @description('Latest revision URL for the deployed Container App resource.') 183 | output latestRevisionUrl string = 'https://${containerApp.properties.latestRevisionFqdn}' 184 | -------------------------------------------------------------------------------- /infra/README.md: -------------------------------------------------------------------------------- 1 | # Azure Infrastructure Deployment 2 | 3 | This directory contains the infrastructure-as-code (Bicep) templates and deployment scripts for the Azure AI Document Processor. 4 | 5 | ## Files 6 | 7 | - `main.bicep` - Main infrastructure template 8 | - `main.parameters.json` - Parameter template (used with azd) 9 | - `deploy.sh` - Standalone deployment script 10 | - `.env.example` - Example environment configuration file 11 | 12 | ## Prerequisites 13 | 14 | 1. **Azure CLI** - Install from https://docs.microsoft.com/en-us/cli/azure/install-azure-cli 15 | 2. **Azure Subscription** - Active Azure subscription with appropriate permissions 16 | 3. **Bash Shell** - Linux, macOS, or WSL on Windows 17 | 18 | ## Deployment Options 19 | 20 | ### Option 1: Using the deploy.sh script (Recommended) 21 | 22 | #### Quick Start 23 | 24 | 1. **Copy the example environment file:** 25 | ```bash 26 | cp .env.example .env 27 | ``` 28 | 29 | 2. **Edit `.env` with your values:** 30 | ```bash 31 | # Minimum required configuration 32 | AZURE_SUBSCRIPTION_ID=your-subscription-id 33 | AZURE_ENV_NAME=dev 34 | AZURE_LOCATION=eastus2 35 | AZURE_RESOURCE_GROUP=rg-ai-doc-processor-dev 36 | AOAI_LOCATION=East US 37 | ``` 38 | 39 | 3. **Login to Azure:** 40 | ```bash 41 | az login 42 | ``` 43 | 44 | 4. **Run the deployment:** 45 | ```bash 46 | ./deploy.sh 47 | ``` 48 | 49 | #### Using Environment Variables 50 | 51 | You can also set environment variables directly instead of using a `.env` file: 52 | 53 | ```bash 54 | export AZURE_SUBSCRIPTION_ID=your-subscription-id 55 | export AZURE_ENV_NAME=dev 56 | export AZURE_LOCATION=eastus2 57 | export AZURE_RESOURCE_GROUP=rg-ai-doc-processor-dev 58 | export AOAI_LOCATION="East US" 59 | export FUNCTION_APP_HOST_PLAN=FlexConsumption 60 | export FUNCTION_APP_SKU=FC1 61 | 62 | ./deploy.sh 63 | ``` 64 | 65 | #### Advanced Configuration 66 | 67 | **Network Isolation with VM:** 68 | ```bash 69 | export AZURE_NETWORK_ISOLATION=true 70 | export AZURE_DEPLOY_VM=true 71 | export VM_USER_PASSWORD='YourSecureP@ssw0rd!' 72 | ./deploy.sh 73 | ``` 74 | 75 | **Using Dedicated Function App Plan:** 76 | ```bash 77 | export FUNCTION_APP_HOST_PLAN=Dedicated 78 | export FUNCTION_APP_SKU=S2 79 | ./deploy.sh 80 | ``` 81 | 82 | **Reusing Existing Resources:** 83 | ```bash 84 | export STORAGE_REUSE=true 85 | export STORAGE_RESOURCE_GROUP_NAME=rg-existing 86 | export STORAGE_NAME=mystorageaccount 87 | ./deploy.sh 88 | ``` 89 | 90 | ### Option 2: Using Azure Developer CLI (azd) 91 | 92 | If you're using the full project with azd: 93 | 94 | ```bash 95 | # Initialize the environment 96 | azd env new dev 97 | 98 | # Set required parameters 99 | azd env set AZURE_LOCATION eastus2 100 | azd env set AOAI_LOCATION "East US" 101 | 102 | # Deploy 103 | azd up 104 | ``` 105 | 106 | ### Option 3: Direct Azure CLI Deployment 107 | 108 | ```bash 109 | # Set variables 110 | SUBSCRIPTION_ID=your-subscription-id 111 | ENV_NAME=dev 112 | LOCATION=eastus2 113 | PRINCIPAL_ID=$(az ad signed-in-user show --query id -o tsv) 114 | 115 | # Deploy 116 | az deployment sub create \ 117 | --name main-$ENV_NAME-$(date +%Y%m%d-%H%M%S) \ 118 | --location $LOCATION \ 119 | --template-file main.bicep \ 120 | --parameters \ 121 | environmentName=$ENV_NAME \ 122 | location=$LOCATION \ 123 | aoaiLocation="East US" \ 124 | principalId=$PRINCIPAL_ID \ 125 | userPrincipalId=$PRINCIPAL_ID \ 126 | networkIsolation=false \ 127 | deployVM=false \ 128 | deployVPN=false \ 129 | functionAppHostPlan=FlexConsumption \ 130 | functionAppSKU=FC1 131 | ``` 132 | 133 | ## Configuration Parameters 134 | 135 | ### Required Parameters 136 | 137 | | Parameter | Description | Example | 138 | |-----------|-------------|---------| 139 | | `AZURE_ENV_NAME` | Environment name | `dev`, `staging`, `prod` | 140 | | `AZURE_LOCATION` | Azure region for most resources | `eastus2`, `westus2` | 141 | | `AOAI_LOCATION` | Azure OpenAI region (see allowed list) | `East US`, `West Europe` | 142 | 143 | ### Optional Parameters 144 | 145 | | Parameter | Default | Description | 146 | |-----------|---------|-------------| 147 | | `AZURE_NETWORK_ISOLATION` | `false` | Enable private endpoints and network isolation | 148 | | `AZURE_DEPLOY_VM` | `false` | Deploy jump box VM for network isolation | 149 | | `AZURE_DEPLOY_VPN` | `false` | Deploy VPN gateway | 150 | | `AI_VISION_ENABLED` | `false` | Enable AI Vision services | 151 | | `AOAI_MULTI_MODAL` | `false` | Enable multi-modal AI capabilities | 152 | | `FUNCTION_APP_HOST_PLAN` | `FlexConsumption` | Function app hosting plan (`FlexConsumption` or `Dedicated`) | 153 | | `FUNCTION_APP_SKU` | `FC1` | Function app SKU | 154 | | `VM_USER_PASSWORD` | - | Password for VM (required if `AZURE_DEPLOY_VM=true`) | 155 | 156 | ### Allowed Azure OpenAI Locations 157 | 158 | - East US 159 | - East US 2 160 | - France Central 161 | - Germany West Central 162 | - Japan East 163 | - Korea Central 164 | - North Central US 165 | - Norway East 166 | - Poland Central 167 | - South Africa North 168 | - South Central US 169 | - South India 170 | - Southeast Asia 171 | - Spain Central 172 | - Sweden Central 173 | - Switzerland North 174 | - Switzerland West 175 | - UAE North 176 | - UK South 177 | - West Europe 178 | - West US 179 | - West US 3 180 | 181 | ## Deployment Outputs 182 | 183 | After successful deployment, the script creates a `deployment-outputs.json` file containing: 184 | 185 | - Resource group name 186 | - Function app names and URLs 187 | - Storage account names 188 | - Azure OpenAI endpoint 189 | - Cosmos DB details 190 | - App Configuration name 191 | - Key Vault name 192 | 193 | ## Resource Reuse 194 | 195 | You can reuse existing Azure resources by setting the appropriate reuse flags. This is useful for: 196 | 197 | - Sharing resources across environments 198 | - Reducing costs 199 | - Maintaining existing data 200 | 201 | Example: 202 | ```bash 203 | export COSMOS_DB_REUSE=true 204 | export COSMOS_DB_RESOURCE_GROUP_NAME=rg-shared 205 | export COSMOS_DB_ACCOUNT_NAME=cosmos-shared 206 | export COSMOS_DB_DATABASE_NAME=shared-db 207 | ``` 208 | 209 | ## Troubleshooting 210 | 211 | ### Login Issues 212 | ```bash 213 | az login 214 | az account set --subscription 215 | ``` 216 | 217 | ### Permission Errors 218 | Ensure you have: 219 | - Contributor or Owner role on the subscription 220 | - Ability to create role assignments 221 | - Ability to create service principals 222 | 223 | ### Deployment Validation 224 | ```bash 225 | # Validate the template without deploying 226 | az deployment sub validate \ 227 | --location eastus2 \ 228 | --template-file main.bicep \ 229 | --parameters environmentName=dev ... 230 | ``` 231 | 232 | ### Check Deployment Status 233 | ```bash 234 | # List recent deployments 235 | az deployment sub list --output table 236 | 237 | # Show specific deployment 238 | az deployment sub show --name 239 | ``` 240 | 241 | ## Clean Up 242 | 243 | To delete all resources: 244 | 245 | ```bash 246 | # Delete the resource group 247 | az group delete --name --yes --no-wait 248 | ``` 249 | 250 | ## Support 251 | 252 | For issues and questions: 253 | - Check the [troubleshooting guide](../docs/troubleShootingGuide.md) 254 | - Review deployment logs in `postprovision.log` 255 | - Check Azure Portal for deployment status 256 | 257 | ## License 258 | 259 | See [LICENSE](../LICENSE) file in the root directory. 260 | -------------------------------------------------------------------------------- /infra/abbreviations.json: -------------------------------------------------------------------------------- 1 | { 2 | "ai": { 3 | "aiSearch": "srch-", 4 | "aiServices": "aisa-", 5 | "aiMultiServices": "aimsa-", 6 | "aiVideoIndexer": "avi-", 7 | "machineLearningWorkspace": "mlw-", 8 | "openAIService": "oai-", 9 | "botService": "bot-", 10 | "computerVision": "cv-", 11 | "contentModerator": "cm-", 12 | "contentSafety": "cs-", 13 | "customVisionPrediction": "cstv-", 14 | "customVisionTraining": "cstvt-", 15 | "documentIntelligence": "di-", 16 | "faceApi": "face-", 17 | "healthInsights": "hi-", 18 | "immersiveReader": "ir-", 19 | "languageService": "lang-", 20 | "speechService": "spch-", 21 | "translator": "trsl-", 22 | "aiHub": "aih-", 23 | "aiHubProject": "aihp-" 24 | }, 25 | "analytics": { 26 | "analysisServicesServer": "as", 27 | "databricksWorkspace": "dbw-", 28 | "dataExplorerCluster": "dec", 29 | "dataExplorerClusterDatabase": "dedb", 30 | "dataFactory": "adf-", 31 | "digitalTwin": "dt-", 32 | "streamAnalytics": "asa-", 33 | "synapseAnalyticsPrivateLinkHub": "synplh-", 34 | "synapseAnalyticsSQLDedicatedPool": "syndp", 35 | "synapseAnalyticsSparkPool": "synsp", 36 | "synapseAnalyticsWorkspaces": "synw", 37 | "dataLakeStoreAccount": "dls", 38 | "dataLakeAnalyticsAccount": "dla", 39 | "eventHubsNamespace": "evhns-", 40 | "eventHub": "evh-", 41 | "eventGridDomain": "evgd-", 42 | "eventGridSubscriptions": "evgs-", 43 | "eventGridTopic": "evgt-", 44 | "eventGridSystemTopic": "egst-", 45 | "hdInsightHadoopCluster": "hadoop-", 46 | "hdInsightHBaseCluster": "hbase-", 47 | "hdInsightKafkaCluster": "kafka-", 48 | "hdInsightSparkCluster": "spark-", 49 | "hdInsightStormCluster": "storm-", 50 | "hdInsightMLServicesCluster": "mls-", 51 | "iotHub": "iot-", 52 | "provisioningServices": "provs-", 53 | "provisioningServicesCertificate": "pcert-", 54 | "powerBIEmbedded": "pbi-", 55 | "timeSeriesInsightsEnvironment": "tsi-" 56 | }, 57 | "configuration" : { 58 | "appConfiguration": "appconfig-" 59 | }, 60 | "compute": { 61 | "appServiceEnvironment": "ase-", 62 | "appServicePlan": "asp-", 63 | "loadTesting": "lt-", 64 | "availabilitySet": "avail-", 65 | "arcEnabledServer": "arcs-", 66 | "arcEnabledKubernetesCluster": "arck", 67 | "batchAccounts": "ba-", 68 | "cloudService": "cld-", 69 | "communicationServices": "acs-", 70 | "diskEncryptionSet": "des", 71 | "functionApp": "func-", 72 | "gallery": "gal", 73 | "hostingEnvironment": "host-", 74 | "imageTemplate": "it-", 75 | "managedDiskOS": "osdisk", 76 | "managedDiskData": "disk", 77 | "notificationHubs": "ntf-", 78 | "notificationHubsNamespace": "ntfns-", 79 | "proximityPlacementGroup": "ppg-", 80 | "restorePointCollection": "rpc-", 81 | "snapshot": "snap-", 82 | "staticWebApp": "stapp-", 83 | "virtualMachine": "vm-", 84 | "virtualMachineScaleSet": "vmss-", 85 | "virtualMachineMaintenanceConfiguration": "mc-", 86 | "virtualMachineStorageAccount": "stvm", 87 | "webApp": "app-" 88 | }, 89 | "containers": { 90 | "aksCluster": "aks-", 91 | "aksSystemNodePool": "npsystem-", 92 | "aksUserNodePool": "np-", 93 | "containerApp": "ca-", 94 | "containerAppsEnvironment": "cae-", 95 | "containerRegistry": "cr", 96 | "containerInstance": "ci", 97 | "serviceFabricCluster": "sf-", 98 | "serviceFabricManagedCluster": "sfmc-" 99 | }, 100 | "databases": { 101 | "cosmosDBDatabase": "cosmos-", 102 | "cosmosDBApacheCassandra": "coscas-", 103 | "cosmosDBMongoDB": "cosmon-", 104 | "cosmosDBNoSQL": "cosno-", 105 | "cosmosDBTable": "costab-", 106 | "cosmosDBGremlin": "cosgrm-", 107 | "cosmosDBPostgreSQL": "cospos-", 108 | "cacheForRedis": "redis-", 109 | "sqlDatabaseServer": "sql-", 110 | "sqlDatabase": "sqldb-", 111 | "sqlElasticJobAgent": "sqlja-", 112 | "sqlElasticPool": "sqlep-", 113 | "mariaDBServer": "maria-", 114 | "mariaDBDatabase": "mariadb-", 115 | "mySQLDatabase": "mysql-", 116 | "postgreSQLDatabase": "psql-", 117 | "sqlServerStretchDatabase": "sqlstrdb-", 118 | "sqlManagedInstance": "sqlmi-" 119 | }, 120 | "developerTools": { 121 | "appConfigurationStore": "appcs-", 122 | "mapsAccount": "map-", 123 | "signalR": "sigr", 124 | "webPubSub": "wps-" 125 | }, 126 | "devOps": { 127 | "managedGrafana": "amg-" 128 | }, 129 | "integration": { 130 | "apiManagementService": "apim-", 131 | "integrationAccount": "ia-", 132 | "logicApp": "logic-", 133 | "serviceBusNamespace": "sbns-", 134 | "serviceBusQueue": "sbq-", 135 | "serviceBusTopic": "sbt-", 136 | "serviceBusTopicSubscription": "sbts-" 137 | }, 138 | "managementGovernance": { 139 | "automationAccount": "aa-", 140 | "applicationInsights": "appi-", 141 | "monitorActionGroup": "ag-", 142 | "monitorDataCollectionRules": "dcr-", 143 | "monitorAlertProcessingRule": "apr-", 144 | "blueprint": "bp-", 145 | "blueprintAssignment": "bpa-", 146 | "dataCollectionEndpoint": "dce-", 147 | "logAnalyticsWorkspace": "log-", 148 | "logAnalyticsQueryPacks": "pack-", 149 | "managementGroup": "mg-", 150 | "purviewInstance": "pview-", 151 | "resourceGroup": "rg-", 152 | "templateSpecsName": "ts-" 153 | }, 154 | "migration": { 155 | "migrateProject": "migr-", 156 | "databaseMigrationService": "dms-", 157 | "recoveryServicesVault": "rsv-" 158 | }, 159 | "networking": { 160 | "aivnet" : "aivnet-", 161 | "applicationGateway": "agw-", 162 | "applicationSecurityGroup": "asg-", 163 | "cdnProfile": "cdnp-", 164 | "cdnEndpoint": "cdne-", 165 | "connections": "con-", 166 | "dnsForwardingRuleset": "dnsfrs-", 167 | "dnsPrivateResolver": "dnspr-", 168 | "dnsPrivateResolverInboundEndpoint": "in-", 169 | "dnsPrivateResolverOutboundEndpoint": "out-", 170 | "firewall": "afw-", 171 | "firewallPolicy": "afwp-", 172 | "expressRouteCircuit": "erc-", 173 | "expressRouteGateway": "ergw-", 174 | "frontDoorProfile": "afd-", 175 | "frontDoorEndpoint": "fde-", 176 | "frontDoorFirewallPolicy": "fdfp-", 177 | "ipGroups": "ipg-", 178 | "loadBalancerInternal": "lbi-", 179 | "loadBalancerExternal": "lbe-", 180 | "loadBalancerRule": "rule-", 181 | "localNetworkGateway": "lgw-", 182 | "natGateway": "ng-", 183 | "networkInterface": "nic-", 184 | "networkSecurityGroup": "nsg-", 185 | "networkSecurityGroupSecurityRules": "nsgsr-", 186 | "networkWatcher": "nw-", 187 | "privateLink": "pl-", 188 | "privateEndpoint": "pep-", 189 | "publicIPAddress": "pip-", 190 | "publicIPAddressPrefix": "ippre-", 191 | "routeFilter": "rf-", 192 | "routeServer": "rtserv-", 193 | "routeTable": "rt-", 194 | "serviceEndpointPolicy": "se-", 195 | "trafficManagerProfile": "traf-", 196 | "userDefinedRoute": "udr-", 197 | "virtualNetwork": "vnet-", 198 | "virtualNetworkGateway": "vgw-", 199 | "virtualNetworkManager": "vnm-", 200 | "virtualNetworkPeering": "peer-", 201 | "virtualNetworkSubnet": "snet-", 202 | "virtualWAN": "vwan-", 203 | "virtualWANHub": "vhub-" 204 | }, 205 | "security": { 206 | "bastion": "bas-", 207 | "keyVault": "kv-", 208 | "privateEndpoint": "pe-", 209 | "keyVaultManagedHSM": "kvmhsm-", 210 | "managedIdentity": "uai-", 211 | "sshKey": "sshkey-", 212 | "vpnGateway": "vpng-", 213 | "vpnConnection": "vcn-", 214 | "vpnSite": "vst-", 215 | "webApplicationFirewallPolicy": "waf", 216 | "webApplicationFirewallPolicyRuleGroup": "wafrg" 217 | }, 218 | "storage": { 219 | "storSimple": "ssimp", 220 | "backupVault": "bvault-", 221 | "backupVaultPolicy": "bkpol-", 222 | "fileShare": "share-", 223 | "storageAccount": "st", 224 | "storageSyncService": "sss-" 225 | }, 226 | "virtualDesktop": { 227 | "labServicesPlan": "lp-", 228 | "virtualDesktopHostPool": "vdpool-", 229 | "virtualDesktopApplicationGroup": "vdag-", 230 | "virtualDesktopWorkspace": "vdws-", 231 | "virtualDesktopScalingPlan": "vdscaling-" 232 | } 233 | } 234 | --------------------------------------------------------------------------------