diff --git a/.github/workflows/CAdeploy.yml b/.github/workflows/CAdeploy.yml
index 67752116c..e32db645e 100644
--- a/.github/workflows/CAdeploy.yml
+++ b/.github/workflows/CAdeploy.yml
@@ -8,75 +8,85 @@ on:
- cron: '0 6,18 * * *' # Runs at 6:00 AM and 6:00 PM GMT
env:
- GPT_MIN_CAPACITY: 10
- TEXT_EMBEDDING_MIN_CAPACITY: 10
+ GPT_MIN_CAPACITY: 250
+ TEXT_EMBEDDING_MIN_CAPACITY: 40
+ BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
jobs:
deploy:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-22.04
+ outputs:
+ RESOURCE_GROUP_NAME: ${{ steps.check_create_rg.outputs.RESOURCE_GROUP_NAME }}
+ WEBAPP_URL: ${{ steps.get_output.outputs.WEBAPP_URL }}
+ DEPLOYMENT_SUCCESS: ${{ steps.deployment_status.outputs.SUCCESS }}
+ AI_SERVICES_NAME: ${{ steps.get_ai_services_name.outputs.AI_SERVICES_NAME }}
+ KEYVAULTS: ${{ steps.list_keyvaults.outputs.KEYVAULTS }}
+ AZURE_LOCATION: ${{ steps.set_region.outputs.AZURE_LOCATION }}
+ SOLUTION_PREFIX: ${{ steps.generate_solution_prefix.outputs.SOLUTION_PREFIX }}
steps:
- - name: Checkout Code
- uses: actions/checkout@v3
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Install ODBC Driver 18 for SQL Server
+ run: |
+ curl https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add -
+ sudo add-apt-repository "$(curl https://packages.microsoft.com/config/ubuntu/$(lsb_release -rs)/prod.list)"
+ sudo apt-get update
+ sudo ACCEPT_EULA=Y apt-get install -y msodbcsql18
+ sudo apt-get install -y unixodbc-dev
+
+ - name: Setup Azure CLI
+ run: curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
+
+ - name: Login to Azure
+ run: |
+ az login --service-principal -u ${{ secrets.AZURE_CLIENT_ID }} -p ${{ secrets.AZURE_CLIENT_SECRET }} --tenant ${{ secrets.AZURE_TENANT_ID }}
+ az account set --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+
- name: Run Quota Check
id: quota-check
run: |
- export AZURE_CLIENT_ID=${{ secrets.AZURE_CLIENT_ID }}
- export AZURE_TENANT_ID=${{ secrets.AZURE_TENANT_ID }}
- export AZURE_CLIENT_SECRET=${{ secrets.AZURE_CLIENT_SECRET }}
+ export AZURE_CLIENT_ID="${{ secrets.AZURE_CLIENT_ID }}"
+ export AZURE_TENANT_ID="${{ secrets.AZURE_TENANT_ID }}"
+ export AZURE_CLIENT_SECRET="${{ secrets.AZURE_CLIENT_SECRET }}"
export AZURE_SUBSCRIPTION_ID="${{ secrets.AZURE_SUBSCRIPTION_ID }}"
- export GPT_MIN_CAPACITY=${{ env.GPT_MIN_CAPACITY }}
- export TEXT_EMBEDDING_MIN_CAPACITY=${{ env.TEXT_EMBEDDING_MIN_CAPACITY }}
+ export GPT_MIN_CAPACITY="150"
+ export TEXT_EMBEDDING_MIN_CAPACITY="80"
export AZURE_REGIONS="${{ vars.AZURE_REGIONS_CA }}"
-
chmod +x infra/scripts/checkquota.sh
if ! infra/scripts/checkquota.sh; then
- # If quota check fails due to insufficient quota, set the flag
- if grep -q "No region with sufficient quota found" infra/scripts/checkquota.sh; then
+ if grep -q "No region with sufficient quota found" infra/scripts/checkquota_ca.sh; then
echo "QUOTA_FAILED=true" >> $GITHUB_ENV
fi
- exit 1 # Fail the pipeline if any other failure occurs
+ exit 1
fi
-
-
- - name: Send Notification on Quota Failure
+
+ - name: Notify on Quota Failure
if: env.QUOTA_FAILED == 'true'
run: |
RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
- EMAIL_BODY=$(cat <Dear Team,
The quota check has failed, and the pipeline cannot proceed.
Build URL: ${RUN_URL}
Please take necessary action.
Best regards,
Your Automation Team
"
- }
- EOF
- )
-
curl -X POST "${{ secrets.LOGIC_APP_URL }}" \
-H "Content-Type: application/json" \
- -d "$EMAIL_BODY" || echo "Failed to send notification"
+ -d '{
+ "subject": "CA Deployment - Quota Check Failed",
+ "body": "The quota check failed for CA deployment.
View run
"
+ }'
- - name: Fail Pipeline if Quota Check Fails
+ - name: Fail on Quota Check
if: env.QUOTA_FAILED == 'true'
run: exit 1
-
- - name: Set Deployment Region
- run: |
- echo "Deployment Region: $VALID_REGION"
- echo "AZURE_LOCATION=$VALID_REGION" >> $GITHUB_ENV
-
- - name: Setup Azure CLI
- run: |
- curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
- az --version # Verify installation
-
- - name: Login to Azure
- run: |
- az login --service-principal -u ${{ secrets.AZURE_CLIENT_ID }} -p ${{ secrets.AZURE_CLIENT_SECRET }} --tenant ${{ secrets.AZURE_TENANT_ID }}
- name: Install Bicep CLI
run: az bicep install
+
+ - name: Set Deployment Region
+ id: set_region
+ run: |
+ echo "Selected Region: $VALID_REGION"
+ echo "AZURE_LOCATION=$VALID_REGION" >> $GITHUB_ENV
+ echo "AZURE_LOCATION=$VALID_REGION" >> $GITHUB_OUTPUT
-
- name: Generate Resource Group Name
id: generate_rg_name
run: |
@@ -100,6 +110,8 @@ jobs:
else
echo "Resource group already exists."
fi
+ # Set output for other jobs
+ echo "RESOURCE_GROUP_NAME=${{ env.RESOURCE_GROUP_NAME }}" >> $GITHUB_OUTPUT
- name: Generate Unique Solution Prefix
id: generate_solution_prefix
@@ -110,16 +122,136 @@ jobs:
UPDATED_TIMESTAMP=$(echo $TIMESTAMP | tail -c 3)
UNIQUE_SOLUTION_PREFIX="${COMMON_PART}${UPDATED_TIMESTAMP}"
echo "SOLUTION_PREFIX=${UNIQUE_SOLUTION_PREFIX}" >> $GITHUB_ENV
+ echo "SOLUTION_PREFIX=${UNIQUE_SOLUTION_PREFIX}" >> $GITHUB_OUTPUT
echo "Generated SOLUTION_PREFIX: ${UNIQUE_SOLUTION_PREFIX}"
-
- - name: Deploy Bicep Template
- id: deploy
+
+ - name: Determine Tag
+ id: determine_tag
+ run: |
+ BRANCH=${{ github.ref_name }}
+ if [[ "$BRANCH" == "main" ]]; then TAG="latest"
+ elif [[ "$BRANCH" == "dev" ]]; then TAG="dev"
+ elif [[ "$BRANCH" == "demo" ]]; then TAG="demo"
+ else TAG="default"; fi
+ echo "tagname=$TAG" >> $GITHUB_OUTPUT
+
+ - name: Get Deployment Output and extract Values
+ id: get_output
run: |
set -e
- az deployment group create \
+ echo "Fetching deployment output..."
+ # Install azd (Azure Developer CLI) - required by process_sample_data.sh
+ curl -fsSL https://aka.ms/install-azd.sh | bash
+
+ DEPLOY_OUTPUT=$(az deployment group create \
--resource-group ${{ env.RESOURCE_GROUP_NAME }} \
--template-file infra/main.bicep \
- --parameters AzureOpenAILocation=${{ env.AZURE_LOCATION }} environmentName=${{ env.SOLUTION_PREFIX }} cosmosLocation=eastus2 gptDeploymentCapacity=${{ env.GPT_MIN_CAPACITY }} embeddingDeploymentCapacity=${{ env.TEXT_EMBEDDING_MIN_CAPACITY }} \
+ --parameters AzureOpenAILocation=${{ env.AZURE_LOCATION }} environmentName=${{ env.SOLUTION_PREFIX }} cosmosLocation=westus gptDeploymentCapacity=${{ env.GPT_MIN_CAPACITY }} embeddingDeploymentCapacity=${{ env.TEXT_EMBEDDING_MIN_CAPACITY }} \
+ --query "properties.outputs" -o json)
+
+
+
+ echo "Deployment output: $DEPLOY_OUTPUT"
+ if [[ -z "$DEPLOY_OUTPUT" ]]; then
+ echo "Error: Deployment output is empty. Please check the deployment logs."
+ exit 1
+ fi
+
+ export AI_FOUNDARY_NAME=$(echo "$DEPLOY_OUTPUT" | jq -r '.aI_FOUNDRY_NAME.value')
+ echo "AI_FOUNDARY_NAME=$AI_FOUNDARY_NAME" >> $GITHUB_ENV
+ export SEARCH_SERVICE_NAME=$(echo "$DEPLOY_OUTPUT" | jq -r '.aI_SEARCH_SERVICE_NAME.value')
+ echo "SEARCH_SERVICE_NAME=$SEARCH_SERVICE_NAME" >> $GITHUB_ENV
+ export COSMOS_DB_ACCOUNT_NAME=$(echo "$DEPLOY_OUTPUT" | jq -r '.cosmosdB_ACCOUNT_NAME.value')
+ echo "COSMOS_DB_ACCOUNT_NAME=$COSMOS_DB_ACCOUNT_NAME" >> $GITHUB_ENV
+ export STORAGE_ACCOUNT=$(echo "$DEPLOY_OUTPUT" | jq -r '.storagE_ACCOUNT_NAME.value')
+ echo "STORAGE_ACCOUNT=$STORAGE_ACCOUNT" >> $GITHUB_ENV
+ export STORAGE_CONTAINER=$(echo "$DEPLOY_OUTPUT" | jq -r '.storagE_CONTAINER_NAME.value')
+ echo "STORAGE_CONTAINER=$STORAGE_CONTAINER" >> $GITHUB_ENV
+ export KEYVAULT_NAME=$(echo "$DEPLOY_OUTPUT" | jq -r '.keY_VAULT_NAME.value')
+ echo "KEYVAULT_NAME=$KEYVAULT_NAME" >> $GITHUB_ENV
+ export SQL_SERVER=$(echo "$DEPLOY_OUTPUT" | jq -r '.sqldB_SERVER.value')
+ echo "SQL_SERVER=$SQL_SERVER" >> $GITHUB_ENV
+ export SQL_DATABASE=$(echo "$DEPLOY_OUTPUT" | jq -r '.sqldB_DATABASE.value')
+ echo "SQL_DATABASE=$SQL_DATABASE" >> $GITHUB_ENV
+ export CLIENT_ID=$(echo "$DEPLOY_OUTPUT" | jq -r '.managedidentitY_WEBAPP_CLIENTID.value')
+ echo "CLIENT_ID=$CLIENT_ID" >> $GITHUB_ENV
+ export CLIENT_NAME=$(echo "$DEPLOY_OUTPUT" | jq -r '.managedidentitY_WEBAPP_NAME.value')
+ echo "CLIENT_NAME=$CLIENT_NAME" >> $GITHUB_ENV
+ export RG_NAME=$(echo "$DEPLOY_OUTPUT" | jq -r '.resourcE_GROUP_NAME.value')
+ echo "RG_NAME=$RG_NAME" >> $GITHUB_ENV
+ WEBAPP_URL=$(echo $DEPLOY_OUTPUT | jq -r '.weB_APP_URL.value')
+ echo "WEBAPP_URL=$WEBAPP_URL" >> $GITHUB_OUTPUT
+ WEB_APP_NAME=$(echo $DEPLOY_OUTPUT | jq -r '.weB_APP_NAME.value')
+ echo "WEB_APP_NAME=$WEB_APP_NAME" >> $GITHUB_ENV
+ echo "Deployment output: $DEPLOY_OUTPUT"
+
+
+ echo "🔧 Disabling AUTH_ENABLED for the web app..."
+ az webapp config appsettings set -g "$RG_NAME" -n "$WEB_APP_NAME" --settings AUTH_ENABLED=false
+
+ sleep 30
+
+ export CLIENT_OBJECT_ID=$(az identity show \
+ --name "$CLIENT_NAME" \
+ --resource-group "$RG_NAME" \
+ --query 'principalId' -o tsv)
+ echo "CLIENT_OBJECT_ID=$CLIENT_OBJECT_ID" >> $GITHUB_ENV
+
+
+
+ - name: Deploy Infra and Import Sample Data
+ run: |
+ set -e
+ az account set --subscription "${{ secrets.AZURE_SUBSCRIPTION_ID }}"
+
+ export AZURE_CLIENT_OBJECT_ID=$(az ad sp show --id ${{ secrets.AZURE_CLIENT_ID }} --query id -o tsv)
+ echo "AZURE_CLIENT_OBJECT_ID=$AZURE_CLIENT_OBJECT_ID" >> $GITHUB_ENV
+
+ az role assignment create \
+ --assignee-object-id $AZURE_CLIENT_OBJECT_ID \
+ --assignee-principal-type ServicePrincipal \
+ --role "Cognitive Services OpenAI User" \
+ --scope /subscriptions/${{ secrets.AZURE_SUBSCRIPTION_ID }}/resourceGroups/${{ env.RG_NAME }}/providers/Microsoft.CognitiveServices/accounts/${{ env.AI_FOUNDARY_NAME }}
+
+ sleep 30
+
+ az role assignment create \
+ --assignee-object-id $AZURE_CLIENT_OBJECT_ID \
+ --assignee-principal-type ServicePrincipal \
+ --role "Search Index Data Contributor" \
+ --scope /subscriptions/${{ secrets.AZURE_SUBSCRIPTION_ID }}/resourceGroups/${{ env.RG_NAME }}/providers/Microsoft.Search/searchServices/${{ env.SEARCH_SERVICE_NAME }}
+
+ echo "Running post-deployment script..."
+ bash ./infra/scripts/add_cosmosdb_access.sh \
+ "${{ env.RG_NAME }}" \
+ "${{ env.COSMOS_DB_ACCOUNT_NAME }}" \
+ "${{ secrets.AZURE_CLIENT_ID }}"
+ bash ./infra/scripts/copy_kb_files.sh \
+ "${{ env.STORAGE_ACCOUNT }}" \
+ "${{ env.STORAGE_CONTAINER }}" \
+ "" \
+ "${{ secrets.AZURE_CLIENT_ID }}"
+ bash ./infra/scripts/run_create_index_scripts.sh \
+ "${{ env.KEYVAULT_NAME }}" \
+ "" \
+ "${{ secrets.AZURE_CLIENT_ID }}" \
+ "${{ env.RG_NAME }}" \
+ "${{ env.SQL_SERVER }}"
+
+
+ user_roles_json='[
+ {"clientId":"${{ env.CLIENT_ID }}","displayName":"${{ env.CLIENT_NAME }}","role":"db_datareader"},
+ {"clientId":"${{ env.CLIENT_ID }}","displayName":"${{ env.CLIENT_NAME }}","role":"db_owner"}
+ ]'
+
+ bash ./infra/scripts/add_user_scripts/create_sql_user_and_role.sh \
+ "${{ env.SQL_SERVER }}.database.windows.net" \
+ "${{ env.SQL_DATABASE }}" \
+ "$user_roles_json" \
+ "${{ secrets.AZURE_CLIENT_ID }}"
+
+ echo "=== Post-Deployment Script Completed Successfully ==="
+
- name: Get AI Services name and store in variable
if: always() && steps.check_create_rg.outcome == 'success'
@@ -131,9 +263,9 @@ jobs:
ai_services_name=$(az cognitiveservices account list -g ${{ env.RESOURCE_GROUP_NAME }} --query "[0].name" -o tsv)
if [ -z "$ai_services_name" ]; then
echo "No AI Services resource found in the resource group."
- echo "AI_SERVICES_NAME=" >> $GITHUB_ENV
+ echo "AI_SERVICES_NAME=" >> $GITHUB_OUTPUT
else
- echo "AI_SERVICES_NAME=${ai_services_name}" >> $GITHUB_ENV
+ echo "AI_SERVICES_NAME=${ai_services_name}" >> $GITHUB_OUTPUT
echo "Found AI Services resource: $ai_services_name"
fi
@@ -141,16 +273,15 @@ jobs:
if: always() && steps.check_create_rg.outcome == 'success'
id: list_keyvaults
run: |
-
set -e
- echo "Listing all KeyVaults in the resource group ${RESOURCE_GROUP_NAME}..."
+ echo "Listing all KeyVaults in the resource group ${{ env.RESOURCE_GROUP_NAME }}..."
# Get the list of KeyVaults in the specified resource group
keyvaults=$(az resource list --resource-group ${{ env.RESOURCE_GROUP_NAME }} --query "[?type=='Microsoft.KeyVault/vaults'].name" -o tsv)
if [ -z "$keyvaults" ]; then
- echo "No KeyVaults found in resource group ${RESOURCE_GROUP_NAME}."
- echo "KEYVAULTS=[]" >> $GITHUB_ENV # If no KeyVaults found, set an empty array
+ echo "No KeyVaults found in resource group ${{ env.RESOURCE_GROUP_NAME }}."
+ echo "KEYVAULTS=[]" >> $GITHUB_OUTPUT # If no KeyVaults found, set an empty array
else
echo "KeyVaults found: $keyvaults"
@@ -167,67 +298,104 @@ jobs:
done
keyvault_array="$keyvault_array]"
- # Output the formatted array and save it to the environment variable
- echo "KEYVAULTS=$keyvault_array" >> $GITHUB_ENV
+ # Output the formatted array and save it to the job output
+ echo "KEYVAULTS=$keyvault_array" >> $GITHUB_OUTPUT
fi
- # - name: Update PowerBI URL
- # if: success()
- # run: |
- # set -e
-
- # COMMON_PART="-app-service"
- # application_name="${{ env.SOLUTION_PREFIX }}${COMMON_PART}"
- # echo "Updating application: $application_name"
-
- # # Log the Power BI URL being set
- # echo "Setting Power BI URL: ${{ vars.VITE_POWERBI_EMBED_URL }}"
-
- # # Update the application settings
- # az webapp config appsettings set --name "$application_name" --resource-group "${{ env.RESOURCE_GROUP_NAME }}" --settings VITE_POWERBI_EMBED_URL="${{ vars.VITE_POWERBI_EMBED_URL }}"
+ - name: Set Deployment Status
+ id: deployment_status
+ if: always()
+ run: |
+ if [ "${{ job.status }}" == "success" ]; then
+ echo "SUCCESS=true" >> $GITHUB_OUTPUT
+ else
+ echo "SUCCESS=false" >> $GITHUB_OUTPUT
+ fi
- # # Restart the web app
- # az webapp restart --resource-group "${{ env.RESOURCE_GROUP_NAME }}" --name "$application_name"
+ - name: Logout
+ if: always()
+ run: az logout
+
+ e2e-test:
+ needs: deploy
+ if: needs.deploy.outputs.DEPLOYMENT_SUCCESS == 'true'
+ uses: ./.github/workflows/test_automation.yml
+ with:
+ CA_WEB_URL: ${{ needs.deploy.outputs.WEBAPP_URL }}
+ secrets: inherit
+
+ cleanup:
+ if: always()
+ needs: [deploy, e2e-test]
+ runs-on: ubuntu-latest
+ env:
+ RESOURCE_GROUP_NAME: ${{ needs.deploy.outputs.RESOURCE_GROUP_NAME }}
+ AI_SERVICES_NAME: ${{ needs.deploy.outputs.AI_SERVICES_NAME }}
+ KEYVAULTS: ${{ needs.deploy.outputs.KEYVAULTS }}
+ AZURE_LOCATION: ${{ needs.deploy.outputs.AZURE_LOCATION }}
+ SOLUTION_PREFIX: ${{ needs.deploy.outputs.SOLUTION_PREFIX }}
+ steps:
+ - name: Setup Azure CLI
+ run: curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
- # echo "Power BI URL updated successfully for application: $application_name."
+ - name: Login to Azure
+ run: |
+ az login --service-principal -u ${{ secrets.AZURE_CLIENT_ID }} -p ${{ secrets.AZURE_CLIENT_SECRET }} --tenant ${{ secrets.AZURE_TENANT_ID }}
+ az account set --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Delete Bicep Deployment
if: always()
run: |
set -e
echo "Checking if resource group exists..."
- rg_exists=$(az group exists --name ${{ env.RESOURCE_GROUP_NAME }})
+ echo "Resource group name: ${{ env.RESOURCE_GROUP_NAME }}"
+
+ if [ -z "${{ env.RESOURCE_GROUP_NAME }}" ]; then
+ echo "Resource group name is empty. Skipping deletion."
+ exit 0
+ fi
+
+ rg_exists=$(az group exists --name "${{ env.RESOURCE_GROUP_NAME }}")
if [ "$rg_exists" = "true" ]; then
- echo "Resource group exist. Cleaning..."
+ echo "Resource group exists. Cleaning..."
az group delete \
- --name ${{ env.RESOURCE_GROUP_NAME }} \
+ --name "${{ env.RESOURCE_GROUP_NAME }}" \
--yes \
--no-wait
- echo "Resource group deleted... ${{ env.RESOURCE_GROUP_NAME }}"
+ echo "Resource group deletion initiated: ${{ env.RESOURCE_GROUP_NAME }}"
else
- echo "Resource group does not exists."
+ echo "Resource group does not exist."
fi
- name: Wait for resource deletion to complete
- if: always() && steps.check_create_rg.outcome == 'success'
+ if: always()
run: |
+ # Check if resource group name is available
+ if [ -z "${{ env.RESOURCE_GROUP_NAME }}" ]; then
+ echo "Resource group name is empty. Skipping resource check."
+ exit 0
+ fi
# List of keyvaults
KEYVAULTS="${{ env.KEYVAULTS }}"
- # Remove the surrounding square brackets, if they exist
- stripped_keyvaults=$(echo "$KEYVAULTS" | sed 's/\[\|\]//g')
+ # Remove the surrounding square brackets and quotes, if they exist
+ stripped_keyvaults=$(echo "$KEYVAULTS" | sed 's/\[\|\]//g' | sed 's/"//g')
# Convert the comma-separated string into an array
IFS=',' read -r -a resources_to_check <<< "$stripped_keyvaults"
- # Append new resources to the array
- # resources_to_check+=("${{ env.SOLUTION_PREFIX }}-openai" "${{ env.SOLUTION_PREFIX }}-cogser")
-
echo "List of resources to check: ${resources_to_check[@]}"
+ # Check if resource group still exists before listing resources
+ rg_exists=$(az group exists --name "${{ env.RESOURCE_GROUP_NAME }}")
+ if [ "$rg_exists" = "false" ]; then
+ echo "Resource group no longer exists. Skipping resource check."
+ exit 0
+ fi
+
# Get the list of resources in YAML format
- resource_list=$(az resource list --resource-group ${{ env.RESOURCE_GROUP_NAME }} --output yaml)
+ resource_list=$(az resource list --resource-group "${{ env.RESOURCE_GROUP_NAME }}" --output yaml || echo "")
# Maximum number of retries
max_retries=3
@@ -240,8 +408,20 @@ jobs:
while true; do
resource_found=false
+ # Check if resource group still exists
+ rg_exists=$(az group exists --name "${{ env.RESOURCE_GROUP_NAME }}")
+ if [ "$rg_exists" = "false" ]; then
+ echo "Resource group no longer exists. Exiting resource check."
+ break
+ fi
+
# Iterate through the resources to check
for resource in "${resources_to_check[@]}"; do
+ # Skip empty resource names
+ if [ -z "$resource" ]; then
+ continue
+ fi
+
echo "Checking resource: $resource"
if echo "$resource_list" | grep -q "name: $resource"; then
echo "Resource '$resource' exists in the resource group."
@@ -261,6 +441,8 @@ jobs:
# Wait for the appropriate interval for the current retry
echo "Waiting for ${retry_intervals[$retries-1]} seconds before retrying..."
sleep ${retry_intervals[$retries-1]}
+ # Refresh resource list
+ resource_list=$(az resource list --resource-group "${{ env.RESOURCE_GROUP_NAME }}" --output yaml || echo "")
fi
else
echo "No resources found. Exiting."
@@ -269,60 +451,51 @@ jobs:
done
- name: Purging the Resources
- if: always() && steps.check_create_rg.outcome == 'success'
+ if: always()
run: |
-
set -e
- # Define variables
- # OPENAI_COMMON_PART="-openai"
- # openai_name="${{ env.SOLUTION_PREFIX }}${OPENAI_COMMON_PART}"
- # echo "Azure OpenAI: $openai_name"
-
- # MULTISERVICE_COMMON_PART="-cogser"
- # multiservice_account_name="${{ env.SOLUTION_PREFIX }}${MULTISERVICE_COMMON_PART}"
- # echo "Azure MultiService Account: $multiservice_account_name"
-
- # # Purge OpenAI Resource
- # echo "Purging the OpenAI Resource..."
- # if ! az resource delete --ids /subscriptions/${{ secrets.AZURE_SUBSCRIPTION_ID }}/providers/Microsoft.CognitiveServices/locations/uksouth/resourceGroups/${{ env.RESOURCE_GROUP_NAME }}/deletedAccounts/$openai_name --verbose; then
- # echo "Failed to purge openai resource: $openai_name"
- # else
- # echo "Purged the openai resource: $openai_name"
- # fi
-
- # # Purge MultiService Account Resource
- # echo "Purging the MultiService Account Resource..."
- # if ! az resource delete --ids /subscriptions/${{ secrets.AZURE_SUBSCRIPTION_ID }}/providers/Microsoft.CognitiveServices/locations/uksouth/resourceGroups/${{ env.RESOURCE_GROUP_NAME }}/deletedAccounts/$multiservice_account_name --verbose; then
- # echo "Failed to purge multiService account resource: $multiservice_account_name"
- # else
- # echo "Purged the multiService account resource: $multiservice_account_name"
- # fi
+
+ # Check if resource group name is available
+ if [ -z "${{ env.RESOURCE_GROUP_NAME }}" ]; then
+ echo "Resource group name is empty. Skipping resource purging."
+ exit 0
+ fi
# Purge AI Services
if [ -z "${{ env.AI_SERVICES_NAME }}" ]; then
- echo "AI_SERVICES_NAME is not set. Skipping purge."
+ echo "AI_SERVICES_NAME is not set. Skipping AI Services purge."
else
echo "Purging AI Services..."
- if [ -n "$(az cognitiveservices account list-deleted --query "[?name=='${{env.AI_SERVICES_NAME}}']" -o tsv)" ]; then
- echo "AI Services '${{env.AI_SERVICES_NAME}}' is soft-deleted. Proceeding to purge..."
- az cognitiveservices account purge --location ${{ env.AZURE_LOCATION }} --resource-group ${{env.RESOURCE_GROUP_NAME}} --name ${{ env.AI_SERVICES_NAME }}
+ if [ -n "$(az cognitiveservices account list-deleted --query "[?name=='${{ env.AI_SERVICES_NAME }}']" -o tsv)" ]; then
+ echo "AI Services '${{ env.AI_SERVICES_NAME }}' is soft-deleted. Proceeding to purge..."
+ az cognitiveservices account purge --location "${{ env.AZURE_LOCATION }}" --resource-group "${{ env.RESOURCE_GROUP_NAME }}" --name "${{ env.AI_SERVICES_NAME }}"
else
- echo "AI Services '${{env.AI_SERVICES_NAME}}' is not soft-deleted. No action taken."
+ echo "AI Services '${{ env.AI_SERVICES_NAME }}' is not soft-deleted. No action taken."
fi
fi
-
# Ensure KEYVAULTS is properly formatted as a comma-separated string
KEYVAULTS="${{ env.KEYVAULTS }}"
- # Remove the surrounding square brackets, if they exist
- stripped_keyvaults=$(echo "$KEYVAULTS" | sed 's/\[\|\]//g')
+ # Check if KEYVAULTS is empty or null
+ if [ -z "$KEYVAULTS" ] || [ "$KEYVAULTS" = "[]" ]; then
+ echo "No KeyVaults to purge."
+ exit 0
+ fi
+
+ # Remove the surrounding square brackets and quotes, if they exist
+ stripped_keyvaults=$(echo "$KEYVAULTS" | sed 's/\[\|\]//g' | sed 's/"//g')
# Convert the comma-separated string into an array
IFS=',' read -r -a keyvault_array <<< "$stripped_keyvaults"
echo "Using KeyVaults Array..."
for keyvault_name in "${keyvault_array[@]}"; do
+ # Skip empty keyvault names
+ if [ -z "$keyvault_name" ]; then
+ continue
+ fi
+
echo "Processing KeyVault: $keyvault_name"
# Check if the KeyVault is soft-deleted
deleted_vaults=$(az keyvault list-deleted --query "[?name=='$keyvault_name']" -o json --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }})
@@ -337,22 +510,18 @@ jobs:
done
echo "Resource purging completed successfully"
-
- - name: Send Notification on Failure
- if: failure()
- run: |
+ - name: Logout
+ if: always()
+ run: az logout
+
+ - name: Notify on Failure
+ if: failure() || needs.deploy.result == 'failure' || needs.e2e-test.result == 'failure'
+ run: |
RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
-
- # Construct the email body
- EMAIL_BODY=$(cat <Dear Team,We would like to inform you that the Client Advisor Automation process has encountered an issue and has failed to complete successfully.
Build URL: ${RUN_URL}
${OUTPUT}
Please investigate the matter at your earliest convenience.
Best regards,
Your Automation Team
"
- }
- EOF
- )
-
- # Send the notification
curl -X POST "${{ secrets.LOGIC_APP_URL }}" \
-H "Content-Type: application/json" \
- -d "$EMAIL_BODY" || echo "Failed to send notification"
+ -d '{
+ "subject": "CA Deployment Failed",
+ "body": "The CA Deployment pipeline failed.
View Run
"
+ }'
\ No newline at end of file
diff --git a/.github/workflows/test_automation.yml b/.github/workflows/test_automation.yml
index 64be66e1d..1a401bb43 100644
--- a/.github/workflows/test_automation.yml
+++ b/.github/workflows/test_automation.yml
@@ -11,9 +11,14 @@ on:
schedule:
- cron: '0 13 * * *' # Runs at 1 PM UTC
workflow_dispatch:
+ workflow_call:
+ inputs:
+ CA_WEB_URL:
+ required: true
+ type: string
env:
- url: ${{ vars.CLIENT_ADVISOR_URL }}
+ url: ${{ inputs.CA_WEB_URL }}
accelerator_name: "Client Advisor"
jobs:
@@ -36,6 +41,42 @@ jobs:
- name: Ensure browsers are installed
run: python -m playwright install --with-deps chromium
+
+ - name: Validate URL
+ run: |
+ if [ -z "${{ env.url }}" ]; then
+ echo "ERROR: No URL provided for testing"
+ exit 1
+
+ fi
+
+ echo "Testing URL: ${{ env.url }}"
+
+
+ - name: Wait for Application to be Ready
+ run: |
+ echo "Waiting for application to be ready at ${{ env.url }} "
+ max_attempts=10
+ attempt=1
+
+ while [ $attempt -le $max_attempts ]; do
+ echo "Attempt $attempt: Checking if application is ready..."
+ if curl -f -s "${{ env.url }}" > /dev/null; then
+ echo "Application is ready!"
+ break
+
+ fi
+
+ if [ $attempt -eq $max_attempts ]; then
+ echo "Application is not ready after $max_attempts attempts"
+ exit 1
+ fi
+
+ echo "Application not ready, waiting 30 seconds..."
+ sleep 30
+ attempt=$((attempt + 1))
+ done
+
- name: Run tests(1)
id: test1
run: |
@@ -108,4 +149,4 @@ jobs:
# Send the notification
curl -X POST "${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA}}" \
-H "Content-Type: application/json" \
- -d "$EMAIL_BODY" || echo "Failed to send notification"
+ -d "$EMAIL_BODY" || echo "Failed to send notification"
\ No newline at end of file
diff --git a/docs/CustomizingAzdParameters.md b/docs/CustomizingAzdParameters.md
index 49d98701a..fb0aa4947 100644
--- a/docs/CustomizingAzdParameters.md
+++ b/docs/CustomizingAzdParameters.md
@@ -21,6 +21,7 @@ By default this template will use the environment name as the prefix to prevent
| `AZURE_ENV_OPENAI_LOCATION` | string | `eastus2` | Location of the Azure OpenAI resource. Choose from (allowed values: `swedencentral`, `australiaeast`). |
| `AZURE_LOCATION` | string | `japaneast` | Sets the Azure region for resource deployment. |
| `AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID` | string | `` | Reuses an existing Log Analytics Workspace instead of provisioning a new one. |
+| `RESOURCE_GROUP_NAME_FOUNDRY` | string | `` | Reuses an existing AI Foundry Project instead of provisioning a new one. |
## How to Set a Parameter
To customize any of the above values, run the following command **before** `azd up`:
diff --git a/docs/DeploymentGuide.md b/docs/DeploymentGuide.md
index 823452049..865612fd8 100644
--- a/docs/DeploymentGuide.md
+++ b/docs/DeploymentGuide.md
@@ -119,6 +119,7 @@ When you start the deployment, most parameters will have **default values**, but
| **Azure OpenAI API Version** | Set the API version for OpenAI model deployments. | `2025-04-01-preview` |
| **AZURE\_LOCATION** | Sets the Azure region for resource deployment. | `japaneast` |
| **Existing Log Analytics Workspace** | To reuse an existing Log Analytics Workspace ID instead of creating a new one. | *(empty)* |
+| **Existing AI Foundry Project Resource ID** | To reuse an existing AI Foundry Project Resource ID instead of creating a new one. | *(empty)* |
diff --git a/infra/deploy_ai_foundry.bicep b/infra/deploy_ai_foundry.bicep
index 43a713c71..677f09d15 100644
--- a/infra/deploy_ai_foundry.bicep
+++ b/infra/deploy_ai_foundry.bicep
@@ -9,6 +9,7 @@ param gptDeploymentCapacity int
param embeddingModel string
param embeddingDeploymentCapacity int
param existingLogAnalyticsWorkspaceId string = ''
+param azureExistingAIProjectResourceId string = ''
// Load the abbrevations file required to name the azure resources.
var abbrs = loadJsonContent('./abbreviations.json')
@@ -52,6 +53,31 @@ var existingLawSubscription = useExisting ? split(existingLogAnalyticsWorkspaceI
var existingLawResourceGroup = useExisting ? split(existingLogAnalyticsWorkspaceId, '/')[4] : ''
var existingLawName = useExisting ? split(existingLogAnalyticsWorkspaceId, '/')[8] : ''
+var existingOpenAIEndpoint = !empty(azureExistingAIProjectResourceId)
+ ? format('https://{0}.openai.azure.com/', split(azureExistingAIProjectResourceId, '/')[8])
+ : ''
+var existingProjEndpoint = !empty(azureExistingAIProjectResourceId)
+ ? format(
+ 'https://{0}.services.ai.azure.com/api/projects/{1}',
+ split(azureExistingAIProjectResourceId, '/')[8],
+ split(azureExistingAIProjectResourceId, '/')[10]
+ )
+ : ''
+var existingAIFoundryName = !empty(azureExistingAIProjectResourceId)
+ ? split(azureExistingAIProjectResourceId, '/')[8]
+ : ''
+var existingAIProjectName = !empty(azureExistingAIProjectResourceId)
+ ? split(azureExistingAIProjectResourceId, '/')[10]
+ : ''
+var existingAIServiceSubscription = !empty(azureExistingAIProjectResourceId)
+ ? split(azureExistingAIProjectResourceId, '/')[2]
+ : ''
+var existingAIServiceResourceGroup = !empty(azureExistingAIProjectResourceId)
+ ? split(azureExistingAIProjectResourceId, '/')[4]
+ : ''
+var aiSearchConnectionName = 'foundry-search-connection-${solutionName}'
+var aiAppInsightConnectionName = 'foundry-app-insights-connection-${solutionName}'
+
resource existingLogAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' existing = if (useExisting) {
name: existingLawName
scope: resourceGroup(existingLawSubscription, existingLawResourceGroup)
@@ -69,25 +95,6 @@ resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = if
}
}
-// resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = {
-// name: applicationInsightsName
-// location: location
-// kind: 'web'
-// properties: {
-// Application_Type: 'web'
-// DisableIpMasking: false
-// DisableLocalAuth: false
-// Flow_Type: 'Bluefield'
-// ForceCustomerStorageForProfiler: false
-// ImmediatePurgeDataOn30Days: true
-// IngestionMode: 'ApplicationInsights'
-// publicNetworkAccessForIngestion: 'Enabled'
-// publicNetworkAccessForQuery: 'Disabled'
-// Request_Source: 'rest'
-// WorkspaceResourceId: logAnalytics.id
-// }
-// }
-
resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = {
name: applicationInsightsName
location: location
@@ -100,7 +107,7 @@ resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = {
}
}
-resource aiFoundry 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' = {
+resource aiFoundry 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' = if (empty(azureExistingAIProjectResourceId)) {
name: aiFoundryName
location: location
sku: {
@@ -123,7 +130,7 @@ resource aiFoundry 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' = {
}
}
-resource aiFoundryProject 'Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview' = {
+resource aiFoundryProject 'Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview' = if (empty(azureExistingAIProjectResourceId)) {
parent: aiFoundry
name: aiProjectName
location: location
@@ -138,7 +145,7 @@ resource aiFoundryProject 'Microsoft.CognitiveServices/accounts/projects@2025-04
@batchSize(1)
resource aiFModelDeployments 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = [
- for aiModeldeployment in aiModelDeployments: {
+ for aiModeldeployment in aiModelDeployments: if (empty(azureExistingAIProjectResourceId)) {
parent: aiFoundry
name: aiModeldeployment.name
properties: {
@@ -185,8 +192,8 @@ resource aiSearch 'Microsoft.Search/searchServices@2025-02-01-preview' = {
}
}
-resource aiSearchFoundryConnection 'Microsoft.CognitiveServices/accounts/connections@2025-04-01-preview' ={
- name: 'foundry-search-connection'
+resource aiSearchFoundryConnection 'Microsoft.CognitiveServices/accounts/connections@2025-04-01-preview' = if (empty(azureExistingAIProjectResourceId)) {
+ name: aiSearchConnectionName
parent: aiFoundry
properties: {
category: 'CognitiveSearch'
@@ -201,18 +208,56 @@ resource aiSearchFoundryConnection 'Microsoft.CognitiveServices/accounts/connect
}
}
+module existing_AIProject_SearchConnectionModule 'deploy_aifp_aisearch_connection.bicep' = if (!empty(azureExistingAIProjectResourceId)) {
+ name: 'aiProjectSearchConnectionDeployment'
+ scope: resourceGroup(existingAIServiceSubscription, existingAIServiceResourceGroup)
+ params: {
+ existingAIProjectName: existingAIProjectName
+ existingAIFoundryName: existingAIFoundryName
+ aiSearchName: aiSearchName
+ aiSearchResourceId: aiSearch.id
+ aiSearchLocation: aiSearch.location
+ aiSearchConnectionName: aiSearchConnectionName
+ }
+}
+
+resource cognitiveServicesOpenAIUser 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
+ name: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd'
+}
+
+module assignOpenAIRoleToAISearch 'deploy_foundry_role_assignment.bicep' = {
+ name: 'assignOpenAIRoleToAISearch'
+ scope: resourceGroup(existingAIServiceSubscription, existingAIServiceResourceGroup)
+ params: {
+ roleDefinitionId: cognitiveServicesOpenAIUser.id
+ roleAssignmentName: guid(resourceGroup().id, aiSearch.id, cognitiveServicesOpenAIUser.id, 'openai-foundry')
+ aiFoundryName: !empty(azureExistingAIProjectResourceId) ? existingAIFoundryName : aiFoundryName
+ aiProjectName: !empty(azureExistingAIProjectResourceId) ? existingAIProjectName : aiProjectName
+ principalId: aiSearch.identity.principalId
+ }
+}
+
@description('This is the built-in Search Index Data Reader role.')
resource searchIndexDataReaderRoleDefinition 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
scope: aiSearch
name: '1407120a-92aa-4202-b7e9-c0e197c71c8f'
}
-resource searchIndexDataReaderRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
- name: guid(aiSearch.id, aiFoundry.id, searchIndexDataReaderRoleDefinition.id)
+resource searchIndexDataReaderRoleAssignmentToAIFP 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (empty(azureExistingAIProjectResourceId)) {
+ name: guid(aiSearch.id, aiFoundryProject.id, searchIndexDataReaderRoleDefinition.id)
scope: aiSearch
properties: {
roleDefinitionId: searchIndexDataReaderRoleDefinition.id
- principalId: aiFoundry.identity.principalId
+ principalId: aiFoundryProject.identity.principalId
+ principalType: 'ServicePrincipal'
+ }
+}
+resource assignSearchIndexDataReaderToExistingAiProject 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(azureExistingAIProjectResourceId)) {
+ name: guid(resourceGroup().id, existingAIProjectName, searchIndexDataReaderRoleDefinition.id, 'Existing')
+ scope: aiSearch
+ properties: {
+ roleDefinitionId: searchIndexDataReaderRoleDefinition.id
+ principalId: assignOpenAIRoleToAISearch.outputs.aiProjectPrincipalId
principalType: 'ServicePrincipal'
}
}
@@ -223,18 +268,28 @@ resource searchServiceContributorRoleDefinition 'Microsoft.Authorization/roleDef
name: '7ca78c08-252a-4471-8644-bb5ff32d4ba0'
}
-resource searchServiceContributorRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
- name: guid(aiSearch.id, aiFoundry.id, searchServiceContributorRoleDefinition.id)
+resource searchServiceContributorRoleAssignmentToAIFP 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (empty(azureExistingAIProjectResourceId)) {
+ name: guid(aiSearch.id, aiFoundryProject.id, searchServiceContributorRoleDefinition.id)
scope: aiSearch
properties: {
roleDefinitionId: searchServiceContributorRoleDefinition.id
- principalId: aiFoundry.identity.principalId
+ principalId: aiFoundryProject.identity.principalId
principalType: 'ServicePrincipal'
}
}
-resource appInsightsFoundryConnection 'Microsoft.CognitiveServices/accounts/connections@2025-04-01-preview' = {
- name: 'foundry-app-insights-connection'
+resource searchServiceContributorRoleAssignmentExisting 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(azureExistingAIProjectResourceId)) {
+ name: guid(resourceGroup().id, existingAIProjectName, searchServiceContributorRoleDefinition.id, 'Existing')
+ scope: aiSearch
+ properties: {
+ roleDefinitionId: searchServiceContributorRoleDefinition.id
+ principalId: assignOpenAIRoleToAISearch.outputs.aiProjectPrincipalId
+ principalType: 'ServicePrincipal'
+ }
+}
+
+resource appInsightsFoundryConnection 'Microsoft.CognitiveServices/accounts/connections@2025-04-01-preview' = if (empty(azureExistingAIProjectResourceId)) {
+ name: aiAppInsightConnectionName
parent: aiFoundry
properties: {
category: 'AppInsights'
@@ -251,14 +306,6 @@ resource appInsightsFoundryConnection 'Microsoft.CognitiveServices/accounts/conn
}
}
-// resource azureOpenAIApiKeyEntry 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = {
-// parent: keyVault
-// name: 'AZURE-OPENAI-KEY'
-// properties: {
-// value: aiFoundry.listKeys().key1 //aiServices_m.listKeys().key1
-// }
-// }
-
resource azureOpenAIApiVersionEntry 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = {
parent: keyVault
name: 'AZURE-OPENAI-PREVIEW-API-VERSION'
@@ -271,7 +318,10 @@ resource azureOpenAIEndpointEntry 'Microsoft.KeyVault/vaults/secrets@2021-11-01-
parent: keyVault
name: 'AZURE-OPENAI-ENDPOINT'
properties: {
- value: aiFoundry.properties.endpoints['OpenAI Language Model Instance API'] //aiServices_m.properties.endpoint
+ // value: aiFoundry.properties.endpoints['OpenAI Language Model Instance API'] //aiServices_m.properties.endpoint
+ value: !empty(existingOpenAIEndpoint)
+ ? existingOpenAIEndpoint
+ : aiFoundry.properties.endpoints['OpenAI Language Model Instance API']
}
}
@@ -283,14 +333,6 @@ resource azureOpenAIEmbeddingModelEntry 'Microsoft.KeyVault/vaults/secrets@2021-
}
}
-// resource azureSearchAdminKeyEntry 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = {
-// parent: keyVault
-// name: 'AZURE-SEARCH-KEY'
-// properties: {
-// value: aiSearch.listAdminKeys().primaryKey
-// }
-// }
-
resource azureSearchServiceEndpointEntry 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = {
parent: keyVault
name: 'AZURE-SEARCH-ENDPOINT'
@@ -310,21 +352,27 @@ resource azureSearchIndexEntry 'Microsoft.KeyVault/vaults/secrets@2021-11-01-pre
output keyvaultName string = keyvaultName
output keyvaultId string = keyVault.id
-output aiFoundryProjectEndpoint string = aiFoundryProject.properties.endpoints['AI Foundry API']
-output aiServicesTarget string = aiFoundry.properties.endpoint //aiServices_m.properties.endpoint
-output aoaiEndpoint string = aiFoundry.properties.endpoints['OpenAI Language Model Instance API'] //aiServices_m.properties.endpoint
-output aiFoundryName string = aiFoundryName //aiServicesName_m
-output aiFoundryId string = aiFoundry.id //aiServices_m.id
+output resourceGroupNameFoundry string = !empty(existingAIServiceResourceGroup)
+ ? existingAIServiceResourceGroup
+ : resourceGroup().name
+output aiFoundryProjectEndpoint string = !empty(existingProjEndpoint)
+ ? existingProjEndpoint
+ : aiFoundryProject.properties.endpoints['AI Foundry API']
+output aoaiEndpoint string = !empty(existingOpenAIEndpoint)
+ ? existingOpenAIEndpoint
+ : aiFoundry.properties.endpoints['OpenAI Language Model Instance API'] //aiServices_m.properties.endpoint
+output aiFoundryName string = !empty(existingAIFoundryName) ? existingAIFoundryName : aiFoundryName //aiServicesName_m
output aiSearchName string = aiSearchName
output aiSearchId string = aiSearch.id
output aiSearchTarget string = 'https://${aiSearch.name}.search.windows.net'
output aiSearchService string = aiSearch.name
-output aiFoundryProjectName string = aiFoundryProject.name
+output aiFoundryProjectName string = !empty(existingAIProjectName) ? existingAIProjectName : aiFoundryProject.name
output applicationInsightsId string = applicationInsights.id
output logAnalyticsWorkspaceResourceName string = useExisting ? existingLogAnalyticsWorkspace.name : logAnalytics.name
output logAnalyticsWorkspaceResourceGroup string = useExisting ? existingLawResourceGroup : resourceGroup().name
-
output applicationInsightsConnectionString string = applicationInsights.properties.ConnectionString
+
+output aiSearchFoundryConnectionName string = aiSearchConnectionName
diff --git a/infra/deploy_aifp_aisearch_connection.bicep b/infra/deploy_aifp_aisearch_connection.bicep
new file mode 100644
index 000000000..0dec1b9bb
--- /dev/null
+++ b/infra/deploy_aifp_aisearch_connection.bicep
@@ -0,0 +1,21 @@
+param existingAIProjectName string
+param existingAIFoundryName string
+param aiSearchName string
+param aiSearchResourceId string
+param aiSearchLocation string
+param aiSearchConnectionName string
+
+resource projectAISearchConnection 'Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview' = {
+ name: '${existingAIFoundryName}/${existingAIProjectName}/${aiSearchConnectionName}'
+ properties: {
+ category: 'CognitiveSearch'
+ target: 'https://${aiSearchName}.search.windows.net'
+ authType: 'AAD'
+ isSharedToAll: true
+ metadata: {
+ ApiType: 'Azure'
+ ResourceId: aiSearchResourceId
+ location: aiSearchLocation
+ }
+ }
+}
diff --git a/infra/deploy_app_service.bicep b/infra/deploy_app_service.bicep
index 3ad3b0ff2..648fbf16f 100644
--- a/infra/deploy_app_service.bicep
+++ b/infra/deploy_app_service.bicep
@@ -2,12 +2,10 @@
targetScope = 'resourceGroup'
@description('Solution Location')
- param solutionLocation string
+param solutionLocation string
@description('The pricing tier for the App Service plan')
-@allowed(
- ['F1', 'D1', 'B1', 'B2', 'B3', 'S1', 'S2', 'S3', 'P1', 'P2', 'P3', 'P4','P0v3']
-)
+@allowed(['F1', 'D1', 'B1', 'B2', 'B3', 'S1', 'S2', 'S3', 'P1', 'P2', 'P3', 'P4', 'P0v3'])
param HostingPlanSku string = 'B2'
param HostingPlanName string
@@ -77,9 +75,7 @@ param AzureOpenAIApiVersion string = '2024-02-15-preview'
param AzureOpenAIStream string = 'True'
@description('Azure Search Query Type')
-@allowed(
- ['simple', 'semantic', 'vector', 'vectorSimpleHybrid', 'vectorSemanticHybrid']
-)
+@allowed(['simple', 'semantic', 'vector', 'vectorSimpleHybrid', 'vectorSemanticHybrid'])
param AzureSearchQueryType string = 'simple'
@description('Azure Search Vector Fields')
@@ -139,9 +135,10 @@ param streamTextSystemPrompt string
param aiFoundryProjectEndpoint string
param useAIProjectClientFlag string = 'false'
-param aiFoundryProjectName string
+
param aiFoundryName string
param applicationInsightsConnectionString string
+param aiSearchProjectConnectionName string
// var WebAppImageName = 'DOCKER|byoaiacontainer.azurecr.io/byoaia-app:latest'
@@ -149,6 +146,18 @@ param applicationInsightsConnectionString string
var WebAppImageName = 'DOCKER|bycwacontainerreg.azurecr.io/byc-wa-app:${imageTag}'
+param azureExistingAIProjectResourceId string = ''
+
+var existingAIServiceSubscription = !empty(azureExistingAIProjectResourceId)
+ ? split(azureExistingAIProjectResourceId, '/')[2]
+ : subscription().subscriptionId
+var existingAIServiceResourceGroup = !empty(azureExistingAIProjectResourceId)
+ ? split(azureExistingAIProjectResourceId, '/')[4]
+ : resourceGroup().name
+var existingAIServicesName = !empty(azureExistingAIProjectResourceId)
+ ? split(azureExistingAIProjectResourceId, '/')[8]
+ : ''
+
resource HostingPlan 'Microsoft.Web/serverfarms@2020-06-01' = {
name: HostingPlanName
location: solutionLocation
@@ -354,6 +363,10 @@ resource Website 'Microsoft.Web/sites@2020-06-01' = {
name: 'AZURE_AI_AGENT_API_VERSION'
value: AzureOpenAIApiVersion
}
+ {
+ name: 'AZURE_SEARCH_CONNECTION_NAME'
+ value: aiSearchProjectConnectionName
+ }
]
linuxFxVersion: WebAppImageName
}
@@ -377,7 +390,6 @@ resource contributorRoleDefinition 'Microsoft.DocumentDB/databaseAccounts/sqlRol
name: '${AZURE_COSMOSDB_ACCOUNT}/00000000-0000-0000-0000-000000000002'
}
-
module cosmosUserRole 'core/database/cosmos/cosmos-role-assign.bicep' = {
name: 'cosmos-sql-user-role-${WebsiteName}'
params: {
@@ -392,11 +404,7 @@ module cosmosUserRole 'core/database/cosmos/cosmos-role-assign.bicep' = {
resource aiFoundry 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = {
name: aiFoundryName
-}
-
-resource aiFoundryProject 'Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview' existing = {
- parent: aiFoundry
- name: aiFoundryProjectName
+ scope: resourceGroup(existingAIServiceSubscription, existingAIServiceResourceGroup)
}
@description('This is the built-in Azure AI User role.')
@@ -405,30 +413,16 @@ resource aiUserRoleDefinitionFoundry 'Microsoft.Authorization/roleDefinitions@20
name: '53ca6127-db72-4b80-b1b0-d745d6d5456d'
}
-resource aiUserRoleAssignmentFoundry 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
- name: guid(Website.id, aiFoundry.id, aiUserRoleDefinitionFoundry.id)
- scope: aiFoundry
- properties: {
- roleDefinitionId: aiUserRoleDefinitionFoundry.id
- principalId: Website.identity.principalId
- principalType: 'ServicePrincipal'
- }
-}
-
-@description('This is the built-in Azure AI User role.')
-resource aiUserRoleDefinitionFoundryProject 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
- scope: aiFoundryProject
- name: '53ca6127-db72-4b80-b1b0-d745d6d5456d'
-}
-
-resource aiUserRoleAssignmentFoundryProject 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
- name: guid(Website.id, aiFoundryProject.id, aiUserRoleDefinitionFoundryProject.id)
- scope: aiFoundryProject
- properties: {
- roleDefinitionId: aiUserRoleDefinitionFoundryProject.id
+module assignAiUserRoleToAiProject 'deploy_foundry_role_assignment.bicep' = {
+ name: 'assignAiUserRoleToAiProject'
+ scope: resourceGroup(existingAIServiceSubscription, existingAIServiceResourceGroup)
+ params: {
principalId: Website.identity.principalId
- principalType: 'ServicePrincipal'
+ roleDefinitionId: aiUserRoleDefinitionFoundry.id
+ roleAssignmentName: guid(Website.name, aiFoundry.id, aiUserRoleDefinitionFoundry.id)
+ aiFoundryName: !empty(azureExistingAIProjectResourceId) ? existingAIServicesName : aiFoundryName
}
}
output webAppUrl string = 'https://${WebsiteName}.azurewebsites.net'
+output webAppName string = WebsiteName
diff --git a/infra/deploy_foundry_role_assignment.bicep b/infra/deploy_foundry_role_assignment.bicep
new file mode 100644
index 000000000..a2a6b246f
--- /dev/null
+++ b/infra/deploy_foundry_role_assignment.bicep
@@ -0,0 +1,26 @@
+param principalId string = ''
+param roleDefinitionId string
+param roleAssignmentName string = ''
+param aiFoundryName string
+param aiProjectName string = ''
+
+resource aiServices 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = {
+ name: aiFoundryName
+}
+
+resource aiProject 'Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview' existing = if (!empty(aiProjectName)) {
+ name: aiProjectName
+ parent: aiServices
+}
+
+resource roleAssignmentToFoundry 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: roleAssignmentName
+ scope: aiServices
+ properties: {
+ roleDefinitionId: roleDefinitionId
+ principalId: principalId
+ }
+}
+
+output aiServicesPrincipalId string = aiServices.identity.principalId
+output aiProjectPrincipalId string = !empty(aiProjectName) ? aiProject.identity.principalId : ''
diff --git a/infra/main.bicep b/infra/main.bicep
index 3a3e65a30..868410526 100644
--- a/infra/main.bicep
+++ b/infra/main.bicep
@@ -9,6 +9,9 @@ param environmentName string
@description('Optional: Existing Log Analytics Workspace Resource ID')
param existingLogAnalyticsWorkspaceId string = ''
+@description('Use this parameter to use an existing AI project resource ID')
+param azureExistingAIProjectResourceId string = ''
+
@description('CosmosDB Location')
param cosmosLocation string = 'eastus2'
@@ -42,7 +45,6 @@ param gptDeploymentCapacity int = 200
])
param embeddingModel string = 'text-embedding-ada-002'
-
@minValue(10)
@description('Capacity of the Embedding Model deployment')
param embeddingDeploymentCapacity int = 80
@@ -69,7 +71,7 @@ param imageTag string = 'latest'
param aiDeploymentsLocation string
@description('Set this if you want to deploy to a different region than the resource group. Otherwise, it will use the resource group location by default.')
-param AZURE_LOCATION string=''
+param AZURE_LOCATION string = ''
var solutionLocation = empty(AZURE_LOCATION) ? resourceGroup().location : AZURE_LOCATION
var uniqueId = toLower(uniqueString(environmentName, subscription().id, solutionLocation))
@@ -83,7 +85,7 @@ var abbrs = loadJsonContent('./abbreviations.json')
//var solutionLocation = resourceGroupLocation
// var baseUrl = 'https://raw.githubusercontent.com/microsoft/Build-your-own-copilot-Solution-Accelerator/main/'
-var functionAppSqlPrompt ='''Generate a valid T-SQL query to find {query} for tables and columns provided below:
+var functionAppSqlPrompt = '''Generate a valid T-SQL query to find {query} for tables and columns provided below:
1. Table: Clients
Columns: ClientId, Client, Email, Occupation, MaritalStatus, Dependents
2. Table: InvestmentGoals
@@ -106,7 +108,7 @@ var functionAppSqlPrompt ='''Generate a valid T-SQL query to find {query} for ta
ALWAYS select Client Name (Column: Client) in the query.
Query filters are IMPORTANT. Add filters like AssetType, AssetDate, etc. if needed.
Only return the generated SQL query. Do not return anything else.'''
-
+
var functionAppCallTranscriptSystemPrompt = '''You are an assistant who supports wealth advisors in preparing for client meetings.
You have access to the client’s past meeting call transcripts.
When answering questions, especially summary requests, provide a detailed and structured response that includes key topics, concerns, decisions, and trends.
@@ -114,7 +116,7 @@ var functionAppCallTranscriptSystemPrompt = '''You are an assistant who supports
var functionAppStreamTextSystemPrompt = '''The currently selected client's name is '{SelectedClientName}'. Treat any case-insensitive or partial mention as referring to this client.
If the user mentions no name, assume they are asking about '{SelectedClientName}'.
- If the user references a name that clearly differs from '{SelectedClientName}', respond only with: 'Please only ask questions about the selected client or select another client.' Otherwise, provide thorough answers for every question using only data from SQL or call transcripts.'
+ If the user references a name that clearly differs from '{SelectedClientName}' or comparing with other clients, respond only with: 'Please only ask questions about the selected client or select another client.' Otherwise, provide thorough answers for every question using only data from SQL or call transcripts.'
If no data is found, respond with 'No data found for that client.' Remove any client identifiers from the final response.
Always send clientId as '{client_id}'.'''
@@ -135,7 +137,7 @@ module keyvaultModule 'deploy_keyvault.bicep' = {
params: {
solutionName: solutionPrefix
solutionLocation: solutionLocation
- managedIdentityObjectId:managedIdentityModule.outputs.managedIdentityOutput.objectId
+ managedIdentityObjectId: managedIdentityModule.outputs.managedIdentityOutput.objectId
kvName: '${abbrs.security.keyVault}${solutionPrefix}'
}
scope: resourceGroup(resourceGroup().name)
@@ -155,6 +157,7 @@ module aifoundry 'deploy_ai_foundry.bicep' = {
embeddingModel: embeddingModel
embeddingDeploymentCapacity: embeddingDeploymentCapacity
existingLogAnalyticsWorkspaceId: existingLogAnalyticsWorkspaceId
+ azureExistingAIProjectResourceId: azureExistingAIProjectResourceId
}
scope: resourceGroup(resourceGroup().name)
}
@@ -164,20 +167,19 @@ module cosmosDBModule 'deploy_cosmos_db.bicep' = {
name: 'deploy_cosmos_db'
params: {
solutionLocation: cosmosLocation
- cosmosDBName:'${abbrs.databases.cosmosDBDatabase}${solutionPrefix}'
+ cosmosDBName: '${abbrs.databases.cosmosDBDatabase}${solutionPrefix}'
}
scope: resourceGroup(resourceGroup().name)
}
-
// ========== Storage Account Module ========== //
module storageAccountModule 'deploy_storage_account.bicep' = {
name: 'deploy_storage_account'
params: {
solutionLocation: solutionLocation
- managedIdentityObjectId:managedIdentityModule.outputs.managedIdentityOutput.objectId
+ managedIdentityObjectId: managedIdentityModule.outputs.managedIdentityOutput.objectId
saName: '${abbrs.storage.storageAccount}${solutionPrefix}'
- keyVaultName:keyvaultModule.outputs.keyvaultName
+ keyVaultName: keyvaultModule.outputs.keyvaultName
}
scope: resourceGroup(resourceGroup().name)
}
@@ -187,9 +189,9 @@ module sqlDBModule 'deploy_sql_db.bicep' = {
name: 'deploy_sql_db'
params: {
solutionLocation: solutionLocation
- keyVaultName:keyvaultModule.outputs.keyvaultName
- managedIdentityObjectId:managedIdentityModule.outputs.managedIdentityOutput.objectId
- managedIdentityName:managedIdentityModule.outputs.managedIdentityOutput.name
+ keyVaultName: keyvaultModule.outputs.keyvaultName
+ managedIdentityObjectId: managedIdentityModule.outputs.managedIdentityOutput.objectId
+ managedIdentityName: managedIdentityModule.outputs.managedIdentityOutput.name
serverName: '${abbrs.databases.sqlDatabaseServer}${solutionPrefix}'
sqlDBName: '${abbrs.databases.sqlDatabase}${solutionPrefix}'
}
@@ -209,51 +211,53 @@ module appserviceModule 'deploy_app_service.bicep' = {
solutionLocation: solutionLocation
HostingPlanName: '${abbrs.compute.appServicePlan}${solutionPrefix}'
WebsiteName: '${abbrs.compute.webApp}${solutionPrefix}'
- AzureSearchService:aifoundry.outputs.aiSearchService
- AzureSearchIndex:'transcripts_index'
- AzureSearchUseSemanticSearch:'True'
- AzureSearchSemanticSearchConfig:'my-semantic-config'
- AzureSearchTopK:'5'
- AzureSearchContentColumns:'content'
- AzureSearchFilenameColumn:'chunk_id'
- AzureSearchTitleColumn:'client_id'
- AzureSearchUrlColumn:'sourceurl'
- AzureOpenAIResource:aifoundry.outputs.aiFoundryName
- AzureOpenAIEndpoint:aifoundry.outputs.aoaiEndpoint
- AzureOpenAIModel:gptModelName
- AzureOpenAITemperature:'0'
- AzureOpenAITopP:'1'
- AzureOpenAIMaxTokens:'1000'
- AzureOpenAIStopSequence:''
- AzureOpenAISystemMessage:'''You are a helpful Wealth Advisor assistant'''
- AzureOpenAIApiVersion:azureOpenaiAPIVersion
- AzureOpenAIStream:'True'
- AzureSearchQueryType:'simple'
- AzureSearchVectorFields:'contentVector'
- AzureSearchPermittedGroupsField:''
- AzureSearchStrictness:'3'
- AzureOpenAIEmbeddingName:embeddingModel
- AzureOpenAIEmbeddingEndpoint:aifoundry.outputs.aoaiEndpoint
- USE_INTERNAL_STREAM:'True'
- SQLDB_SERVER:'${sqlDBModule.outputs.sqlServerName}.database.windows.net'
- SQLDB_DATABASE:sqlDBModule.outputs.sqlDbName
+ AzureSearchService: aifoundry.outputs.aiSearchService
+ AzureSearchIndex: 'transcripts_index'
+ AzureSearchUseSemanticSearch: 'True'
+ AzureSearchSemanticSearchConfig: 'my-semantic-config'
+ AzureSearchTopK: '5'
+ AzureSearchContentColumns: 'content'
+ AzureSearchFilenameColumn: 'chunk_id'
+ AzureSearchTitleColumn: 'client_id'
+ AzureSearchUrlColumn: 'sourceurl'
+ AzureOpenAIResource: aifoundry.outputs.aiFoundryName
+ AzureOpenAIEndpoint: aifoundry.outputs.aoaiEndpoint
+ AzureOpenAIModel: gptModelName
+ AzureOpenAITemperature: '0'
+ AzureOpenAITopP: '1'
+ AzureOpenAIMaxTokens: '1000'
+ AzureOpenAIStopSequence: ''
+ AzureOpenAISystemMessage: '''You are a helpful Wealth Advisor assistant'''
+ AzureOpenAIApiVersion: azureOpenaiAPIVersion
+ AzureOpenAIStream: 'True'
+ AzureSearchQueryType: 'simple'
+ AzureSearchVectorFields: 'contentVector'
+ AzureSearchPermittedGroupsField: ''
+ AzureSearchStrictness: '3'
+ AzureOpenAIEmbeddingName: embeddingModel
+ AzureOpenAIEmbeddingEndpoint: aifoundry.outputs.aoaiEndpoint
+ USE_INTERNAL_STREAM: 'True'
+ SQLDB_SERVER: '${sqlDBModule.outputs.sqlServerName}.database.windows.net'
+ SQLDB_DATABASE: sqlDBModule.outputs.sqlDbName
AZURE_COSMOSDB_ACCOUNT: cosmosDBModule.outputs.cosmosAccountName
AZURE_COSMOSDB_CONVERSATIONS_CONTAINER: cosmosDBModule.outputs.cosmosContainerName
AZURE_COSMOSDB_DATABASE: cosmosDBModule.outputs.cosmosDatabaseName
AZURE_COSMOSDB_ENABLE_FEEDBACK: 'True'
//VITE_POWERBI_EMBED_URL: 'TBD'
imageTag: imageTag
- userassignedIdentityClientId:managedIdentityModule.outputs.managedIdentityWebAppOutput.clientId
- userassignedIdentityId:managedIdentityModule.outputs.managedIdentityWebAppOutput.id
+ userassignedIdentityClientId: managedIdentityModule.outputs.managedIdentityWebAppOutput.clientId
+ userassignedIdentityId: managedIdentityModule.outputs.managedIdentityWebAppOutput.id
applicationInsightsId: aifoundry.outputs.applicationInsightsId
- azureSearchServiceEndpoint:aifoundry.outputs.aiSearchTarget
+ azureSearchServiceEndpoint: aifoundry.outputs.aiSearchTarget
sqlSystemPrompt: functionAppSqlPrompt
callTranscriptSystemPrompt: functionAppCallTranscriptSystemPrompt
streamTextSystemPrompt: functionAppStreamTextSystemPrompt
- aiFoundryProjectName:aifoundry.outputs.aiFoundryProjectName
+ //aiFoundryProjectName:aifoundry.outputs.aiFoundryProjectName
aiFoundryProjectEndpoint: aifoundry.outputs.aiFoundryProjectEndpoint
aiFoundryName: aifoundry.outputs.aiFoundryName
- applicationInsightsConnectionString:aifoundry.outputs.applicationInsightsConnectionString
+ applicationInsightsConnectionString: aifoundry.outputs.applicationInsightsConnectionString
+ azureExistingAIProjectResourceId: azureExistingAIProjectResourceId
+ aiSearchProjectConnectionName: aifoundry.outputs.aiSearchFoundryConnectionName
}
scope: resourceGroup(resourceGroup().name)
}
@@ -264,9 +268,11 @@ output STORAGE_CONTAINER_NAME string = storageAccountModule.outputs.storageConta
output KEY_VAULT_NAME string = keyvaultModule.outputs.keyvaultName
output COSMOSDB_ACCOUNT_NAME string = cosmosDBModule.outputs.cosmosAccountName
output RESOURCE_GROUP_NAME string = resourceGroup().name
+output RESOURCE_GROUP_NAME_FOUNDRY string = aifoundry.outputs.resourceGroupNameFoundry
output SQLDB_SERVER string = sqlDBModule.outputs.sqlServerName
output SQLDB_DATABASE string = sqlDBModule.outputs.sqlDbName
output MANAGEDIDENTITY_WEBAPP_NAME string = managedIdentityModule.outputs.managedIdentityWebAppOutput.name
output MANAGEDIDENTITY_WEBAPP_CLIENTID string = managedIdentityModule.outputs.managedIdentityWebAppOutput.clientId
output AI_FOUNDRY_NAME string = aifoundry.outputs.aiFoundryName
output AI_SEARCH_SERVICE_NAME string = aifoundry.outputs.aiSearchService
+output WEB_APP_NAME string = appserviceModule.outputs.webAppName
diff --git a/infra/main.json b/infra/main.json
index b1483eb4e..6dcdaec21 100644
--- a/infra/main.json
+++ b/infra/main.json
@@ -5,7 +5,7 @@
"_generator": {
"name": "bicep",
"version": "0.36.1.42791",
- "templateHash": "461277054460209703"
+ "templateHash": "539400033229136375"
}
},
"parameters": {
@@ -24,8 +24,16 @@
"description": "Optional: Existing Log Analytics Workspace Resource ID"
}
},
+ "azureExistingAIProjectResourceId": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Use this parameter to use an existing AI project resource ID"
+ }
+ },
"cosmosLocation": {
"type": "string",
+ "defaultValue": "eastus2",
"metadata": {
"description": "CosmosDB Location"
}
@@ -59,7 +67,7 @@
},
"gptDeploymentCapacity": {
"type": "int",
- "defaultValue": 30,
+ "defaultValue": 200,
"minValue": 10,
"metadata": {
"description": "Capacity of the GPT deployment:"
@@ -88,9 +96,8 @@
"type": "string",
"defaultValue": "latest"
},
- "AzureOpenAILocation": {
+ "aiDeploymentsLocation": {
"type": "string",
- "defaultValue": "eastus2",
"allowedValues": [
"australiaeast",
"eastus",
@@ -103,7 +110,14 @@
"westus3"
],
"metadata": {
- "description": "Azure OpenAI Location"
+ "azd": {
+ "type": "location",
+ "usageName": [
+ "OpenAI.GlobalStandard.gpt-4o-mini,200",
+ "OpenAI.Standard.text-embedding-ada-002,80"
+ ]
+ },
+ "description": "Location for AI Foundry deployment. This is the location where the AI Foundry resources will be deployed."
}
},
"AZURE_LOCATION": {
@@ -350,7 +364,7 @@
"abbrs": "[variables('$fxv#0')]",
"functionAppSqlPrompt": "Generate a valid T-SQL query to find {query} for tables and columns provided below:\n 1. Table: Clients\n Columns: ClientId, Client, Email, Occupation, MaritalStatus, Dependents\n 2. Table: InvestmentGoals\n Columns: ClientId, InvestmentGoal\n 3. Table: Assets\n Columns: ClientId, AssetDate, Investment, ROI, Revenue, AssetType\n 4. Table: ClientSummaries\n Columns: ClientId, ClientSummary\n 5. Table: InvestmentGoalsDetails\n Columns: ClientId, InvestmentGoal, TargetAmount, Contribution\n 6. Table: Retirement\n Columns: ClientId, StatusDate, RetirementGoalProgress, EducationGoalProgress\n 7. Table: ClientMeetings\n Columns: ClientId, ConversationId, Title, StartTime, EndTime, Advisor, ClientEmail\n Always use the Investment column from the Assets table as the value.\n Assets table has snapshots of values by date. Do not add numbers across different dates for total values.\n Do not use client name in filters.\n Do not include assets values unless asked for.\n ALWAYS use ClientId = {clientid} in the query filter.\n ALWAYS select Client Name (Column: Client) in the query.\n Query filters are IMPORTANT. Add filters like AssetType, AssetDate, etc. if needed.\n Only return the generated SQL query. Do not return anything else.",
"functionAppCallTranscriptSystemPrompt": "You are an assistant who supports wealth advisors in preparing for client meetings. \n You have access to the client’s past meeting call transcripts. \n When answering questions, especially summary requests, provide a detailed and structured response that includes key topics, concerns, decisions, and trends. \n If no data is available, state 'No relevant data found for previous meetings.",
- "functionAppStreamTextSystemPrompt": "The currently selected client's name is '{SelectedClientName}'. Treat any case-insensitive or partial mention as referring to this client.\n If the user mentions no name, assume they are asking about '{SelectedClientName}'.\n If the user references a name that clearly differs from '{SelectedClientName}', respond only with: 'Please only ask questions about the selected client or select another client.' Otherwise, provide thorough answers for every question using only data from SQL or call transcripts.'\n If no data is found, respond with 'No data found for that client.' Remove any client identifiers from the final response.\n Always send clientId as '{client_id}'."
+ "functionAppStreamTextSystemPrompt": "The currently selected client's name is '{SelectedClientName}'. Treat any case-insensitive or partial mention as referring to this client.\n If the user mentions no name, assume they are asking about '{SelectedClientName}'.\n If the user references a name that clearly differs from '{SelectedClientName}' or comparing with other clients, respond only with: 'Please only ask questions about the selected client or select another client.' Otherwise, provide thorough answers for every question using only data from SQL or call transcripts.'\n If no data is found, respond with 'No data found for that client.' Remove any client identifiers from the final response.\n Always send clientId as '{client_id}'."
},
"resources": [
{
@@ -683,7 +697,7 @@
"value": "[variables('solutionPrefix')]"
},
"solutionLocation": {
- "value": "[parameters('AzureOpenAILocation')]"
+ "value": "[parameters('aiDeploymentsLocation')]"
},
"keyVaultName": {
"value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_keyvault'), '2022-09-01').outputs.keyvaultName.value]"
@@ -708,6 +722,9 @@
},
"existingLogAnalyticsWorkspaceId": {
"value": "[parameters('existingLogAnalyticsWorkspaceId')]"
+ },
+ "azureExistingAIProjectResourceId": {
+ "value": "[parameters('azureExistingAIProjectResourceId')]"
}
},
"template": {
@@ -717,7 +734,7 @@
"_generator": {
"name": "bicep",
"version": "0.36.1.42791",
- "templateHash": "15647067587936233417"
+ "templateHash": "13634339460279357495"
}
},
"parameters": {
@@ -751,6 +768,10 @@
"existingLogAnalyticsWorkspaceId": {
"type": "string",
"defaultValue": ""
+ },
+ "azureExistingAIProjectResourceId": {
+ "type": "string",
+ "defaultValue": ""
}
},
"variables": {
@@ -1016,7 +1037,15 @@
"useExisting": "[not(empty(parameters('existingLogAnalyticsWorkspaceId')))]",
"existingLawSubscription": "[if(variables('useExisting'), split(parameters('existingLogAnalyticsWorkspaceId'), '/')[2], '')]",
"existingLawResourceGroup": "[if(variables('useExisting'), split(parameters('existingLogAnalyticsWorkspaceId'), '/')[4], '')]",
- "existingLawName": "[if(variables('useExisting'), split(parameters('existingLogAnalyticsWorkspaceId'), '/')[8], '')]"
+ "existingLawName": "[if(variables('useExisting'), split(parameters('existingLogAnalyticsWorkspaceId'), '/')[8], '')]",
+ "existingOpenAIEndpoint": "[if(not(empty(parameters('azureExistingAIProjectResourceId'))), format('https://{0}.openai.azure.com/', split(parameters('azureExistingAIProjectResourceId'), '/')[8]), '')]",
+ "existingProjEndpoint": "[if(not(empty(parameters('azureExistingAIProjectResourceId'))), format('https://{0}.services.ai.azure.com/api/projects/{1}', split(parameters('azureExistingAIProjectResourceId'), '/')[8], split(parameters('azureExistingAIProjectResourceId'), '/')[10]), '')]",
+ "existingAIFoundryName": "[if(not(empty(parameters('azureExistingAIProjectResourceId'))), split(parameters('azureExistingAIProjectResourceId'), '/')[8], '')]",
+ "existingAIProjectName": "[if(not(empty(parameters('azureExistingAIProjectResourceId'))), split(parameters('azureExistingAIProjectResourceId'), '/')[10], '')]",
+ "existingAIServiceSubscription": "[if(not(empty(parameters('azureExistingAIProjectResourceId'))), split(parameters('azureExistingAIProjectResourceId'), '/')[2], '')]",
+ "existingAIServiceResourceGroup": "[if(not(empty(parameters('azureExistingAIProjectResourceId'))), split(parameters('azureExistingAIProjectResourceId'), '/')[4], '')]",
+ "aiSearchConnectionName": "[format('foundry-search-connection-{0}', parameters('solutionName'))]",
+ "aiAppInsightConnectionName": "[format('foundry-app-insights-connection-{0}', parameters('solutionName'))]"
},
"resources": [
{
@@ -1050,6 +1079,7 @@
]
},
{
+ "condition": "[empty(parameters('azureExistingAIProjectResourceId'))]",
"type": "Microsoft.CognitiveServices/accounts",
"apiVersion": "2025-04-01-preview",
"name": "[variables('aiFoundryName')]",
@@ -1074,6 +1104,7 @@
}
},
{
+ "condition": "[empty(parameters('azureExistingAIProjectResourceId'))]",
"type": "Microsoft.CognitiveServices/accounts/projects",
"apiVersion": "2025-04-01-preview",
"name": "[format('{0}/{1}', variables('aiFoundryName'), variables('aiProjectName'))]",
@@ -1096,6 +1127,7 @@
"mode": "serial",
"batchSize": 1
},
+ "condition": "[empty(parameters('azureExistingAIProjectResourceId'))]",
"type": "Microsoft.CognitiveServices/accounts/deployments",
"apiVersion": "2023-05-01",
"name": "[format('{0}/{1}', variables('aiFoundryName'), variables('aiModelDeployments')[copyIndex()].name)]",
@@ -1146,9 +1178,10 @@
}
},
{
+ "condition": "[empty(parameters('azureExistingAIProjectResourceId'))]",
"type": "Microsoft.CognitiveServices/accounts/connections",
"apiVersion": "2025-04-01-preview",
- "name": "[format('{0}/{1}', variables('aiFoundryName'), 'foundry-search-connection')]",
+ "name": "[format('{0}/{1}', variables('aiFoundryName'), variables('aiSearchConnectionName'))]",
"properties": {
"category": "CognitiveSearch",
"target": "[reference(resourceId('Microsoft.Search/searchServices', variables('aiSearchName')), '2025-02-01-preview').endpoint]",
@@ -1166,39 +1199,74 @@
]
},
{
+ "condition": "[empty(parameters('azureExistingAIProjectResourceId'))]",
"type": "Microsoft.Authorization/roleAssignments",
"apiVersion": "2022-04-01",
"scope": "[format('Microsoft.Search/searchServices/{0}', variables('aiSearchName'))]",
- "name": "[guid(resourceId('Microsoft.Search/searchServices', variables('aiSearchName')), resourceId('Microsoft.CognitiveServices/accounts', variables('aiFoundryName')), extensionResourceId(resourceId('Microsoft.Search/searchServices', variables('aiSearchName')), 'Microsoft.Authorization/roleDefinitions', '1407120a-92aa-4202-b7e9-c0e197c71c8f'))]",
+ "name": "[guid(resourceId('Microsoft.Search/searchServices', variables('aiSearchName')), resourceId('Microsoft.CognitiveServices/accounts/projects', variables('aiFoundryName'), variables('aiProjectName')), extensionResourceId(resourceId('Microsoft.Search/searchServices', variables('aiSearchName')), 'Microsoft.Authorization/roleDefinitions', '1407120a-92aa-4202-b7e9-c0e197c71c8f'))]",
"properties": {
"roleDefinitionId": "[extensionResourceId(resourceId('Microsoft.Search/searchServices', variables('aiSearchName')), 'Microsoft.Authorization/roleDefinitions', '1407120a-92aa-4202-b7e9-c0e197c71c8f')]",
- "principalId": "[reference(resourceId('Microsoft.CognitiveServices/accounts', variables('aiFoundryName')), '2025-04-01-preview', 'full').identity.principalId]",
+ "principalId": "[reference(resourceId('Microsoft.CognitiveServices/accounts/projects', variables('aiFoundryName'), variables('aiProjectName')), '2025-04-01-preview', 'full').identity.principalId]",
"principalType": "ServicePrincipal"
},
"dependsOn": [
- "[resourceId('Microsoft.CognitiveServices/accounts', variables('aiFoundryName'))]",
+ "[resourceId('Microsoft.CognitiveServices/accounts/projects', variables('aiFoundryName'), variables('aiProjectName'))]",
"[resourceId('Microsoft.Search/searchServices', variables('aiSearchName'))]"
]
},
{
+ "condition": "[not(empty(parameters('azureExistingAIProjectResourceId')))]",
+ "type": "Microsoft.Authorization/roleAssignments",
+ "apiVersion": "2022-04-01",
+ "scope": "[format('Microsoft.Search/searchServices/{0}', variables('aiSearchName'))]",
+ "name": "[guid(resourceGroup().id, variables('existingAIProjectName'), extensionResourceId(resourceId('Microsoft.Search/searchServices', variables('aiSearchName')), 'Microsoft.Authorization/roleDefinitions', '1407120a-92aa-4202-b7e9-c0e197c71c8f'), 'Existing')]",
+ "properties": {
+ "roleDefinitionId": "[extensionResourceId(resourceId('Microsoft.Search/searchServices', variables('aiSearchName')), 'Microsoft.Authorization/roleDefinitions', '1407120a-92aa-4202-b7e9-c0e197c71c8f')]",
+ "principalId": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('existingAIServiceSubscription'), variables('existingAIServiceResourceGroup')), 'Microsoft.Resources/deployments', 'assignOpenAIRoleToAISearch'), '2022-09-01').outputs.aiProjectPrincipalId.value]",
+ "principalType": "ServicePrincipal"
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Search/searchServices', variables('aiSearchName'))]",
+ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('existingAIServiceSubscription'), variables('existingAIServiceResourceGroup')), 'Microsoft.Resources/deployments', 'assignOpenAIRoleToAISearch')]"
+ ]
+ },
+ {
+ "condition": "[empty(parameters('azureExistingAIProjectResourceId'))]",
"type": "Microsoft.Authorization/roleAssignments",
"apiVersion": "2022-04-01",
"scope": "[format('Microsoft.Search/searchServices/{0}', variables('aiSearchName'))]",
- "name": "[guid(resourceId('Microsoft.Search/searchServices', variables('aiSearchName')), resourceId('Microsoft.CognitiveServices/accounts', variables('aiFoundryName')), extensionResourceId(resourceId('Microsoft.Search/searchServices', variables('aiSearchName')), 'Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0'))]",
+ "name": "[guid(resourceId('Microsoft.Search/searchServices', variables('aiSearchName')), resourceId('Microsoft.CognitiveServices/accounts/projects', variables('aiFoundryName'), variables('aiProjectName')), extensionResourceId(resourceId('Microsoft.Search/searchServices', variables('aiSearchName')), 'Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0'))]",
"properties": {
"roleDefinitionId": "[extensionResourceId(resourceId('Microsoft.Search/searchServices', variables('aiSearchName')), 'Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0')]",
- "principalId": "[reference(resourceId('Microsoft.CognitiveServices/accounts', variables('aiFoundryName')), '2025-04-01-preview', 'full').identity.principalId]",
+ "principalId": "[reference(resourceId('Microsoft.CognitiveServices/accounts/projects', variables('aiFoundryName'), variables('aiProjectName')), '2025-04-01-preview', 'full').identity.principalId]",
"principalType": "ServicePrincipal"
},
"dependsOn": [
- "[resourceId('Microsoft.CognitiveServices/accounts', variables('aiFoundryName'))]",
+ "[resourceId('Microsoft.CognitiveServices/accounts/projects', variables('aiFoundryName'), variables('aiProjectName'))]",
"[resourceId('Microsoft.Search/searchServices', variables('aiSearchName'))]"
]
},
{
+ "condition": "[not(empty(parameters('azureExistingAIProjectResourceId')))]",
+ "type": "Microsoft.Authorization/roleAssignments",
+ "apiVersion": "2022-04-01",
+ "scope": "[format('Microsoft.Search/searchServices/{0}', variables('aiSearchName'))]",
+ "name": "[guid(resourceGroup().id, variables('existingAIProjectName'), extensionResourceId(resourceId('Microsoft.Search/searchServices', variables('aiSearchName')), 'Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0'), 'Existing')]",
+ "properties": {
+ "roleDefinitionId": "[extensionResourceId(resourceId('Microsoft.Search/searchServices', variables('aiSearchName')), 'Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0')]",
+ "principalId": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('existingAIServiceSubscription'), variables('existingAIServiceResourceGroup')), 'Microsoft.Resources/deployments', 'assignOpenAIRoleToAISearch'), '2022-09-01').outputs.aiProjectPrincipalId.value]",
+ "principalType": "ServicePrincipal"
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Search/searchServices', variables('aiSearchName'))]",
+ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('existingAIServiceSubscription'), variables('existingAIServiceResourceGroup')), 'Microsoft.Resources/deployments', 'assignOpenAIRoleToAISearch')]"
+ ]
+ },
+ {
+ "condition": "[empty(parameters('azureExistingAIProjectResourceId'))]",
"type": "Microsoft.CognitiveServices/accounts/connections",
"apiVersion": "2025-04-01-preview",
- "name": "[format('{0}/{1}', variables('aiFoundryName'), 'foundry-app-insights-connection')]",
+ "name": "[format('{0}/{1}', variables('aiFoundryName'), variables('aiAppInsightConnectionName'))]",
"properties": {
"category": "AppInsights",
"target": "[resourceId('Microsoft.Insights/components', variables('applicationInsightsName'))]",
@@ -1230,7 +1298,7 @@
"apiVersion": "2021-11-01-preview",
"name": "[format('{0}/{1}', parameters('keyVaultName'), 'AZURE-OPENAI-ENDPOINT')]",
"properties": {
- "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', variables('aiFoundryName')), '2025-04-01-preview').endpoints['OpenAI Language Model Instance API']]"
+ "value": "[if(not(empty(variables('existingOpenAIEndpoint'))), variables('existingOpenAIEndpoint'), reference(resourceId('Microsoft.CognitiveServices/accounts', variables('aiFoundryName')), '2025-04-01-preview').endpoints['OpenAI Language Model Instance API'])]"
},
"dependsOn": [
"[resourceId('Microsoft.CognitiveServices/accounts', variables('aiFoundryName'))]"
@@ -1262,6 +1330,174 @@
"properties": {
"value": "transcripts_index"
}
+ },
+ {
+ "condition": "[not(empty(parameters('azureExistingAIProjectResourceId')))]",
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2022-09-01",
+ "name": "aiProjectSearchConnectionDeployment",
+ "subscriptionId": "[variables('existingAIServiceSubscription')]",
+ "resourceGroup": "[variables('existingAIServiceResourceGroup')]",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "existingAIProjectName": {
+ "value": "[variables('existingAIProjectName')]"
+ },
+ "existingAIFoundryName": {
+ "value": "[variables('existingAIFoundryName')]"
+ },
+ "aiSearchName": {
+ "value": "[variables('aiSearchName')]"
+ },
+ "aiSearchResourceId": {
+ "value": "[resourceId('Microsoft.Search/searchServices', variables('aiSearchName'))]"
+ },
+ "aiSearchLocation": {
+ "value": "[reference(resourceId('Microsoft.Search/searchServices', variables('aiSearchName')), '2025-02-01-preview', 'full').location]"
+ },
+ "aiSearchConnectionName": {
+ "value": "[variables('aiSearchConnectionName')]"
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.36.1.42791",
+ "templateHash": "4784003223337407725"
+ }
+ },
+ "parameters": {
+ "existingAIProjectName": {
+ "type": "string"
+ },
+ "existingAIFoundryName": {
+ "type": "string"
+ },
+ "aiSearchName": {
+ "type": "string"
+ },
+ "aiSearchResourceId": {
+ "type": "string"
+ },
+ "aiSearchLocation": {
+ "type": "string"
+ },
+ "aiSearchConnectionName": {
+ "type": "string"
+ }
+ },
+ "resources": [
+ {
+ "type": "Microsoft.CognitiveServices/accounts/projects/connections",
+ "apiVersion": "2025-04-01-preview",
+ "name": "[format('{0}/{1}/{2}', parameters('existingAIFoundryName'), parameters('existingAIProjectName'), parameters('aiSearchConnectionName'))]",
+ "properties": {
+ "category": "CognitiveSearch",
+ "target": "[format('https://{0}.search.windows.net', parameters('aiSearchName'))]",
+ "authType": "AAD",
+ "isSharedToAll": true,
+ "metadata": {
+ "ApiType": "Azure",
+ "ResourceId": "[parameters('aiSearchResourceId')]",
+ "location": "[parameters('aiSearchLocation')]"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Search/searchServices', variables('aiSearchName'))]"
+ ]
+ },
+ {
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2022-09-01",
+ "name": "assignOpenAIRoleToAISearch",
+ "subscriptionId": "[variables('existingAIServiceSubscription')]",
+ "resourceGroup": "[variables('existingAIServiceResourceGroup')]",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "roleDefinitionId": {
+ "value": "[resourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')]"
+ },
+ "roleAssignmentName": {
+ "value": "[guid(resourceGroup().id, resourceId('Microsoft.Search/searchServices', variables('aiSearchName')), resourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd'), 'openai-foundry')]"
+ },
+ "aiFoundryName": "[if(not(empty(parameters('azureExistingAIProjectResourceId'))), createObject('value', variables('existingAIFoundryName')), createObject('value', variables('aiFoundryName')))]",
+ "aiProjectName": "[if(not(empty(parameters('azureExistingAIProjectResourceId'))), createObject('value', variables('existingAIProjectName')), createObject('value', variables('aiProjectName')))]",
+ "principalId": {
+ "value": "[reference(resourceId('Microsoft.Search/searchServices', variables('aiSearchName')), '2025-02-01-preview', 'full').identity.principalId]"
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.36.1.42791",
+ "templateHash": "1709475957170755318"
+ }
+ },
+ "parameters": {
+ "principalId": {
+ "type": "string",
+ "defaultValue": ""
+ },
+ "roleDefinitionId": {
+ "type": "string"
+ },
+ "roleAssignmentName": {
+ "type": "string",
+ "defaultValue": ""
+ },
+ "aiFoundryName": {
+ "type": "string"
+ },
+ "aiProjectName": {
+ "type": "string",
+ "defaultValue": ""
+ }
+ },
+ "resources": [
+ {
+ "type": "Microsoft.Authorization/roleAssignments",
+ "apiVersion": "2022-04-01",
+ "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('aiFoundryName'))]",
+ "name": "[parameters('roleAssignmentName')]",
+ "properties": {
+ "roleDefinitionId": "[parameters('roleDefinitionId')]",
+ "principalId": "[parameters('principalId')]"
+ }
+ }
+ ],
+ "outputs": {
+ "aiServicesPrincipalId": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('aiFoundryName')), '2025-04-01-preview', 'full').identity.principalId]"
+ },
+ "aiProjectPrincipalId": {
+ "type": "string",
+ "value": "[if(not(empty(parameters('aiProjectName'))), reference(resourceId('Microsoft.CognitiveServices/accounts/projects', parameters('aiFoundryName'), parameters('aiProjectName')), '2025-04-01-preview', 'full').identity.principalId, '')]"
+ }
+ }
+ }
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Search/searchServices', variables('aiSearchName'))]"
+ ]
}
],
"outputs": {
@@ -1273,25 +1509,21 @@
"type": "string",
"value": "[resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName'))]"
},
- "aiFoundryProjectEndpoint": {
+ "resourceGroupNameFoundry": {
"type": "string",
- "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts/projects', variables('aiFoundryName'), variables('aiProjectName')), '2025-04-01-preview').endpoints['AI Foundry API']]"
+ "value": "[if(not(empty(variables('existingAIServiceResourceGroup'))), variables('existingAIServiceResourceGroup'), resourceGroup().name)]"
},
- "aiServicesTarget": {
+ "aiFoundryProjectEndpoint": {
"type": "string",
- "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', variables('aiFoundryName')), '2025-04-01-preview').endpoint]"
+ "value": "[if(not(empty(variables('existingProjEndpoint'))), variables('existingProjEndpoint'), reference(resourceId('Microsoft.CognitiveServices/accounts/projects', variables('aiFoundryName'), variables('aiProjectName')), '2025-04-01-preview').endpoints['AI Foundry API'])]"
},
"aoaiEndpoint": {
"type": "string",
- "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', variables('aiFoundryName')), '2025-04-01-preview').endpoints['OpenAI Language Model Instance API']]"
+ "value": "[if(not(empty(variables('existingOpenAIEndpoint'))), variables('existingOpenAIEndpoint'), reference(resourceId('Microsoft.CognitiveServices/accounts', variables('aiFoundryName')), '2025-04-01-preview').endpoints['OpenAI Language Model Instance API'])]"
},
"aiFoundryName": {
"type": "string",
- "value": "[variables('aiFoundryName')]"
- },
- "aiFoundryId": {
- "type": "string",
- "value": "[resourceId('Microsoft.CognitiveServices/accounts', variables('aiFoundryName'))]"
+ "value": "[if(not(empty(variables('existingAIFoundryName'))), variables('existingAIFoundryName'), variables('aiFoundryName'))]"
},
"aiSearchName": {
"type": "string",
@@ -1311,7 +1543,7 @@
},
"aiFoundryProjectName": {
"type": "string",
- "value": "[variables('aiProjectName')]"
+ "value": "[if(not(empty(variables('existingAIProjectName'))), variables('existingAIProjectName'), variables('aiProjectName'))]"
},
"applicationInsightsId": {
"type": "string",
@@ -1328,6 +1560,10 @@
"applicationInsightsConnectionString": {
"type": "string",
"value": "[reference(resourceId('Microsoft.Insights/components', variables('applicationInsightsName')), '2020-02-02').ConnectionString]"
+ },
+ "aiSearchFoundryConnectionName": {
+ "type": "string",
+ "value": "[variables('aiSearchConnectionName')]"
}
}
}
@@ -1974,9 +2210,6 @@
"streamTextSystemPrompt": {
"value": "[variables('functionAppStreamTextSystemPrompt')]"
},
- "aiFoundryProjectName": {
- "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_ai_foundry'), '2022-09-01').outputs.aiFoundryProjectName.value]"
- },
"aiFoundryProjectEndpoint": {
"value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_ai_foundry'), '2022-09-01').outputs.aiFoundryProjectEndpoint.value]"
},
@@ -1985,6 +2218,12 @@
},
"applicationInsightsConnectionString": {
"value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_ai_foundry'), '2022-09-01').outputs.applicationInsightsConnectionString.value]"
+ },
+ "azureExistingAIProjectResourceId": {
+ "value": "[parameters('azureExistingAIProjectResourceId')]"
+ },
+ "aiSearchProjectConnectionName": {
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_ai_foundry'), '2022-09-01').outputs.aiSearchFoundryConnectionName.value]"
}
},
"template": {
@@ -1994,7 +2233,7 @@
"_generator": {
"name": "bicep",
"version": "0.36.1.42791",
- "templateHash": "6657678385477724168"
+ "templateHash": "4144537398413637557"
}
},
"parameters": {
@@ -2315,18 +2554,25 @@
"type": "string",
"defaultValue": "false"
},
- "aiFoundryProjectName": {
- "type": "string"
- },
"aiFoundryName": {
"type": "string"
},
"applicationInsightsConnectionString": {
"type": "string"
+ },
+ "aiSearchProjectConnectionName": {
+ "type": "string"
+ },
+ "azureExistingAIProjectResourceId": {
+ "type": "string",
+ "defaultValue": ""
}
},
"variables": {
- "WebAppImageName": "[format('DOCKER|bycwacontainerreg.azurecr.io/byc-wa-app:{0}', parameters('imageTag'))]"
+ "WebAppImageName": "[format('DOCKER|bycwacontainerreg.azurecr.io/byc-wa-app:{0}', parameters('imageTag'))]",
+ "existingAIServiceSubscription": "[if(not(empty(parameters('azureExistingAIProjectResourceId'))), split(parameters('azureExistingAIProjectResourceId'), '/')[2], subscription().subscriptionId)]",
+ "existingAIServiceResourceGroup": "[if(not(empty(parameters('azureExistingAIProjectResourceId'))), split(parameters('azureExistingAIProjectResourceId'), '/')[4], resourceGroup().name)]",
+ "existingAIServicesName": "[if(not(empty(parameters('azureExistingAIProjectResourceId'))), split(parameters('azureExistingAIProjectResourceId'), '/')[8], '')]"
},
"resources": [
{
@@ -2533,6 +2779,10 @@
{
"name": "AZURE_AI_AGENT_API_VERSION",
"value": "[parameters('AzureOpenAIApiVersion')]"
+ },
+ {
+ "name": "AZURE_SEARCH_CONNECTION_NAME",
+ "value": "[parameters('aiSearchProjectConnectionName')]"
}
],
"linuxFxVersion": "[variables('WebAppImageName')]"
@@ -2542,34 +2792,6 @@
"[resourceId('Microsoft.Web/serverfarms', parameters('HostingPlanName'))]"
]
},
- {
- "type": "Microsoft.Authorization/roleAssignments",
- "apiVersion": "2022-04-01",
- "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('aiFoundryName'))]",
- "name": "[guid(resourceId('Microsoft.Web/sites', parameters('WebsiteName')), resourceId('Microsoft.CognitiveServices/accounts', parameters('aiFoundryName')), extensionResourceId(resourceId('Microsoft.CognitiveServices/accounts', parameters('aiFoundryName')), 'Microsoft.Authorization/roleDefinitions', '53ca6127-db72-4b80-b1b0-d745d6d5456d'))]",
- "properties": {
- "roleDefinitionId": "[extensionResourceId(resourceId('Microsoft.CognitiveServices/accounts', parameters('aiFoundryName')), 'Microsoft.Authorization/roleDefinitions', '53ca6127-db72-4b80-b1b0-d745d6d5456d')]",
- "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('WebsiteName')), '2020-06-01', 'full').identity.principalId]",
- "principalType": "ServicePrincipal"
- },
- "dependsOn": [
- "[resourceId('Microsoft.Web/sites', parameters('WebsiteName'))]"
- ]
- },
- {
- "type": "Microsoft.Authorization/roleAssignments",
- "apiVersion": "2022-04-01",
- "scope": "[format('Microsoft.CognitiveServices/accounts/{0}/projects/{1}', parameters('aiFoundryName'), parameters('aiFoundryProjectName'))]",
- "name": "[guid(resourceId('Microsoft.Web/sites', parameters('WebsiteName')), resourceId('Microsoft.CognitiveServices/accounts/projects', parameters('aiFoundryName'), parameters('aiFoundryProjectName')), extensionResourceId(resourceId('Microsoft.CognitiveServices/accounts/projects', parameters('aiFoundryName'), parameters('aiFoundryProjectName')), 'Microsoft.Authorization/roleDefinitions', '53ca6127-db72-4b80-b1b0-d745d6d5456d'))]",
- "properties": {
- "roleDefinitionId": "[extensionResourceId(resourceId('Microsoft.CognitiveServices/accounts/projects', parameters('aiFoundryName'), parameters('aiFoundryProjectName')), 'Microsoft.Authorization/roleDefinitions', '53ca6127-db72-4b80-b1b0-d745d6d5456d')]",
- "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('WebsiteName')), '2020-06-01', 'full').identity.principalId]",
- "principalType": "ServicePrincipal"
- },
- "dependsOn": [
- "[resourceId('Microsoft.Web/sites', parameters('WebsiteName'))]"
- ]
- },
{
"type": "Microsoft.Resources/deployments",
"apiVersion": "2022-09-01",
@@ -2630,12 +2852,97 @@
"dependsOn": [
"[resourceId('Microsoft.Web/sites', parameters('WebsiteName'))]"
]
+ },
+ {
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2022-09-01",
+ "name": "assignAiUserRoleToAiProject",
+ "subscriptionId": "[variables('existingAIServiceSubscription')]",
+ "resourceGroup": "[variables('existingAIServiceResourceGroup')]",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "principalId": {
+ "value": "[reference(resourceId('Microsoft.Web/sites', parameters('WebsiteName')), '2020-06-01', 'full').identity.principalId]"
+ },
+ "roleDefinitionId": {
+ "value": "[extensionResourceId(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('existingAIServiceSubscription'), variables('existingAIServiceResourceGroup')), 'Microsoft.CognitiveServices/accounts', parameters('aiFoundryName')), 'Microsoft.Authorization/roleDefinitions', '53ca6127-db72-4b80-b1b0-d745d6d5456d')]"
+ },
+ "roleAssignmentName": {
+ "value": "[guid(parameters('WebsiteName'), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('existingAIServiceSubscription'), variables('existingAIServiceResourceGroup')), 'Microsoft.CognitiveServices/accounts', parameters('aiFoundryName')), extensionResourceId(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('existingAIServiceSubscription'), variables('existingAIServiceResourceGroup')), 'Microsoft.CognitiveServices/accounts', parameters('aiFoundryName')), 'Microsoft.Authorization/roleDefinitions', '53ca6127-db72-4b80-b1b0-d745d6d5456d'))]"
+ },
+ "aiFoundryName": "[if(not(empty(parameters('azureExistingAIProjectResourceId'))), createObject('value', variables('existingAIServicesName')), createObject('value', parameters('aiFoundryName')))]"
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.36.1.42791",
+ "templateHash": "1709475957170755318"
+ }
+ },
+ "parameters": {
+ "principalId": {
+ "type": "string",
+ "defaultValue": ""
+ },
+ "roleDefinitionId": {
+ "type": "string"
+ },
+ "roleAssignmentName": {
+ "type": "string",
+ "defaultValue": ""
+ },
+ "aiFoundryName": {
+ "type": "string"
+ },
+ "aiProjectName": {
+ "type": "string",
+ "defaultValue": ""
+ }
+ },
+ "resources": [
+ {
+ "type": "Microsoft.Authorization/roleAssignments",
+ "apiVersion": "2022-04-01",
+ "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('aiFoundryName'))]",
+ "name": "[parameters('roleAssignmentName')]",
+ "properties": {
+ "roleDefinitionId": "[parameters('roleDefinitionId')]",
+ "principalId": "[parameters('principalId')]"
+ }
+ }
+ ],
+ "outputs": {
+ "aiServicesPrincipalId": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('aiFoundryName')), '2025-04-01-preview', 'full').identity.principalId]"
+ },
+ "aiProjectPrincipalId": {
+ "type": "string",
+ "value": "[if(not(empty(parameters('aiProjectName'))), reference(resourceId('Microsoft.CognitiveServices/accounts/projects', parameters('aiFoundryName'), parameters('aiProjectName')), '2025-04-01-preview', 'full').identity.principalId, '')]"
+ }
+ }
+ }
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Web/sites', parameters('WebsiteName'))]"
+ ]
}
],
"outputs": {
"webAppUrl": {
"type": "string",
"value": "[format('https://{0}.azurewebsites.net', parameters('WebsiteName'))]"
+ },
+ "webAppName": {
+ "type": "string",
+ "value": "[parameters('WebsiteName')]"
}
}
}
@@ -2673,6 +2980,10 @@
"type": "string",
"value": "[resourceGroup().name]"
},
+ "RESOURCE_GROUP_NAME_FOUNDRY": {
+ "type": "string",
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_ai_foundry'), '2022-09-01').outputs.resourceGroupNameFoundry.value]"
+ },
"SQLDB_SERVER": {
"type": "string",
"value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_sql_db'), '2022-09-01').outputs.sqlServerName.value]"
@@ -2696,6 +3007,10 @@
"AI_SEARCH_SERVICE_NAME": {
"type": "string",
"value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_ai_foundry'), '2022-09-01').outputs.aiSearchService.value]"
+ },
+ "WEB_APP_NAME": {
+ "type": "string",
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_app_service'), '2022-09-01').outputs.webAppName.value]"
}
}
}
\ No newline at end of file
diff --git a/infra/main.parameters.json b/infra/main.parameters.json
index a9a35d824..10822f1f7 100644
--- a/infra/main.parameters.json
+++ b/infra/main.parameters.json
@@ -34,6 +34,9 @@
},
"existingLogAnalyticsWorkspaceId": {
"value": "${AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID}"
+ },
+ "azureExistingAIProjectResourceId": {
+ "value": "${AZURE_EXISTING_AI_PROJECT_RESOURCE_ID}"
}
}
}
\ No newline at end of file
diff --git a/infra/scripts/add_cosmosdb_access.sh b/infra/scripts/add_cosmosdb_access.sh
index 957e49e61..b75801067 100644
--- a/infra/scripts/add_cosmosdb_access.sh
+++ b/infra/scripts/add_cosmosdb_access.sh
@@ -23,6 +23,16 @@ fi
echo "Getting signed in user id"
signed_user_id=$(az ad signed-in-user show --query id -o tsv)
+if [ $? -ne 0 ]; then
+ if [ -z "$managedIdentityClientId" ]; then
+ echo "Error: Failed to get signed in user id."
+ exit 1
+ else
+ signed_user_id=$managedIdentityClientId
+ signed_user_id=$(az ad sp show --id $managedIdentityClientId --query id -o tsv)
+
+ fi
+fi
# Check if the user has the Cosmos DB Built-in Data Contributor role
echo "Checking if user has the Cosmos DB Built-in Data Contributor role"
diff --git a/infra/scripts/add_user_scripts/create_sql_user_and_role.sh b/infra/scripts/add_user_scripts/create_sql_user_and_role.sh
index 65526819c..db4f561bd 100644
--- a/infra/scripts/add_user_scripts/create_sql_user_and_role.sh
+++ b/infra/scripts/add_user_scripts/create_sql_user_and_role.sh
@@ -4,7 +4,9 @@
SqlServerName="$1"
SqlDatabaseName="$2"
UserRoleJSONArray="$3"
-ManagedIdentityClientId="$6"
+ManagedIdentityClientId="$4"
+
+echo "Script Started"
# Function to check if a command exists or runs successfully
function check_command() {
@@ -22,6 +24,7 @@ check_command "sqlcmd '-?'"
if az account show &> /dev/null; then
echo "Already authenticated with Azure."
else
+ echo "Not authenticated with Azure. Attempting to authenticate..."
if [ -n "$ManagedIdentityClientId" ]; then
# Use managed identity if running in Azure
echo "Authenticating with Managed Identity..."
@@ -31,13 +34,28 @@ else
echo "Authenticating with Azure CLI..."
az login
fi
- echo "Not authenticated with Azure. Attempting to authenticate..."
+fi
+
+echo "Getting signed in user id"
+signed_user_id=$(az ad signed-in-user show --query id -o tsv)
+if [ $? -ne 0 ]; then
+ if [ -z "$ManagedIdentityClientId" ]; then
+ echo "Error: Failed to get signed in user id."
+ exit 1
+ else
+ signed_user_id=$ManagedIdentityClientId
+ # signed_user_id=$(az ad sp show --id $ManagedIdentityClientId --query id -o tsv)
+
+ fi
fi
SQL_QUERY=""
#loop through the JSON array and create users and assign roles using grep and sed
count=1
+echo "Processing JSON object"
while read -r json_object; do
+
+ # echo "Processing JSON object: $json_object"
# Extract fields from the JSON object using grep and sed
clientId=$(echo "$json_object" | grep -o '"clientId": *"[^"]*"' | sed 's/"clientId": *"\([^"]*\)"/\1/')
displayName=$(echo "$json_object" | grep -o '"displayName": *"[^"]*"' | sed 's/"displayName": *"\([^"]*\)"/\1/')
diff --git a/infra/scripts/checkquota.sh b/infra/scripts/checkquota.sh
old mode 100644
new mode 100755
diff --git a/infra/scripts/copy_kb_files.sh b/infra/scripts/copy_kb_files.sh
index 09b8148a8..1d94772b9 100644
--- a/infra/scripts/copy_kb_files.sh
+++ b/infra/scripts/copy_kb_files.sh
@@ -12,6 +12,7 @@ echo "Script Started"
if az account show &> /dev/null; then
echo "Already authenticated with Azure."
else
+ echo "Not authenticated with Azure. Attempting to authenticate..."
if [ -n "$managedIdentityClientId" ]; then
# Use managed identity if running in Azure
echo "Authenticating with Managed Identity..."
@@ -21,51 +22,59 @@ else
echo "Authenticating with Azure CLI..."
az login
fi
- echo "Not authenticated with Azure. Attempting to authenticate..."
+fi
+
+echo "Getting signed in user id"
+signed_user_id=$(az ad signed-in-user show --query id -o tsv)
+if [ $? -ne 0 ]; then
+ if [ -z "$managedIdentityClientId" ]; then
+ echo "Error: Failed to get signed in user id."
+ exit 1
+ else
+ signed_user_id=$managedIdentityClientId
+ fi
fi
# if using managed identity, skip role assignments as its already provided via bicep
-if [ -n "$managedIdentityClientId" ]; then
- echo "Skipping role assignments as managed identity is used"
-else
- echo "Getting signed in user id"
- signed_user_id=$(az ad signed-in-user show --query id -o tsv)
-
- echo "Getting storage account resource id"
- storage_account_resource_id=$(az storage account show --name $storageAccount --query id --output tsv)
-
- #check if user has the Storage Blob Data Contributor role, add it if not
- echo "Checking if user has the Storage Blob Data Contributor role"
- role_assignment=$(MSYS_NO_PATHCONV=1 az role assignment list --assignee $signed_user_id --role "Storage Blob Data Contributor" --scope $storage_account_resource_id --query "[].roleDefinitionId" -o tsv)
- if [ -z "$role_assignment" ]; then
- echo "User does not have the Storage Blob Data Contributor role. Assigning the role."
- MSYS_NO_PATHCONV=1 az role assignment create --assignee $signed_user_id --role "Storage Blob Data Contributor" --scope $storage_account_resource_id --output none
- if [ $? -eq 0 ]; then
- echo "Role assignment completed successfully."
- retries=3
- while [ $retries -gt 0 ]; do
- # Check if the role assignment was successful
- role_assignment_check=$(MSYS_NO_PATHCONV=1 az role assignment list --assignee $signed_user_id --role "Storage Blob Data Contributor" --scope $storage_account_resource_id --query "[].roleDefinitionId" -o tsv)
- if [ -n "$role_assignment_check" ]; then
- echo "Role assignment verified successfully."
- break
- else
- echo "Role assignment not found, retrying..."
- ((retries--))
- sleep 10
- fi
- done
- if [ $retries -eq 0 ]; then
- echo "Error: Role assignment verification failed after multiple attempts. Try rerunning the script."
- exit 1
+
+# echo "Getting signed in user id"
+# signed_user_id=$(az ad signed-in-user show --query id -o tsv)
+
+echo "Getting storage account resource id"
+storage_account_resource_id=$(az storage account show --name $storageAccount --query id --output tsv)
+
+#check if user has the Storage Blob Data Contributor role, add it if not
+echo "Checking if user has the Storage Blob Data Contributor role"
+role_assignment=$(MSYS_NO_PATHCONV=1 az role assignment list --assignee $signed_user_id --role "Storage Blob Data Contributor" --scope $storage_account_resource_id --query "[].roleDefinitionId" -o tsv)
+if [ -z "$role_assignment" ]; then
+ echo "User does not have the Storage Blob Data Contributor role. Assigning the role."
+ MSYS_NO_PATHCONV=1 az role assignment create --assignee $signed_user_id --role "Storage Blob Data Contributor" --scope $storage_account_resource_id --output none
+ if [ $? -eq 0 ]; then
+ echo "Role assignment completed successfully."
+ retries=3
+ while [ $retries -gt 0 ]; do
+ # Check if the role assignment was successful
+ role_assignment_check=$(MSYS_NO_PATHCONV=1 az role assignment list --assignee $signed_user_id --role "Storage Blob Data Contributor" --scope $storage_account_resource_id --query "[].roleDefinitionId" -o tsv)
+ if [ -n "$role_assignment_check" ]; then
+ echo "Role assignment verified successfully."
+ sleep 60
+ break
+ else
+ echo "Role assignment not found, retrying..."
+ ((retries--))
+ sleep 10
fi
- else
- echo "Error: Role assignment failed."
+ done
+ if [ $retries -eq 0 ]; then
+ echo "Error: Role assignment verification failed after multiple attempts. Try rerunning the script."
exit 1
fi
else
- echo "User already has the Storage Blob Data Contributor role."
+ echo "Error: Role assignment failed."
+ exit 1
fi
+else
+ echo "User already has the Storage Blob Data Contributor role."
fi
zipFileName1="clientdata.zip"
@@ -86,7 +95,7 @@ extractionPath1=""
extractionPath2=""
# Check if running in Azure Container App
-if !([ -z "$baseUrl" ] && [ -z "$managedIdentityClientId" ]); then
+if [ -n "$baseUrl" ] && [ -n "$managedIdentityClientId" ]; then
extractionPath1="/mnt/azscripts/azscriptinput/$extractedFolder1"
extractionPath2="/mnt/azscripts/azscriptinput/$extractedFolder2"
@@ -110,8 +119,52 @@ else
unzip -o $zipUrl2 -d $extractionPath2
fi
+echo "Uploading files to Azure Blob Storage"
# Using az storage blob upload-batch to upload files with managed identity authentication, as the az storage fs directory upload command is not working with managed identity authentication.
az storage blob upload-batch --account-name "$storageAccount" --destination data/"$extractedFolder1" --source $extractionPath1 --auth-mode login --pattern '*' --overwrite --output none
+if [ $? -ne 0 ]; then
+ retries=3
+ sleepTime=10
+ echo "Error: Failed to upload files to Azure Blob Storage. Retrying upload...($((4 - retries)) of 3)"
+ while [ $retries -gt 0 ]; do
+ sleep $sleepTime
+ az storage blob upload-batch --account-name "$storageAccount" --destination data/"$extractedFolder1" --source $extractionPath1 --auth-mode login --pattern '*' --overwrite --output none
+ if [ $? -eq 0 ]; then
+ echo "Files uploaded successfully to Azure Blob Storage."
+ break
+ else
+ ((retries--))
+ echo "Retrying upload... ($((4 - retries)) of 3)"
+ sleepTime=$((sleepTime * 2))
+ sleep $sleepTime
+ fi
+ done
+ exit 1
+else
+ echo "Files uploaded successfully to Azure Blob Storage."
+fi
+
az storage blob upload-batch --account-name "$storageAccount" --destination data/"$extractedFolder2" --source $extractionPath2 --auth-mode login --pattern '*' --overwrite --output none
+if [ $? -ne 0 ]; then
+ retries=3
+ sleepTime=10
+ echo "Error: Failed to upload files to Azure Blob Storage. Retrying upload...($((4 - retries)) of 3)"
+ while [ $retries -gt 0 ]; do
+ sleep $sleepTime
+ az storage blob upload-batch --account-name "$storageAccount" --destination data/"$extractedFolder2" --source $extractionPath2 --auth-mode login --pattern '*' --overwrite --output none
+ if [ $? -eq 0 ]; then
+ echo "Files uploaded successfully to Azure Blob Storage."
+ break
+ else
+ ((retries--))
+ echo "Retrying upload... ($((4 - retries)) of 3)"
+ sleepTime=$((sleepTime * 2))
+ sleep $sleepTime
+ fi
+ done
+ exit 1
+else
+ echo "Files uploaded successfully to Azure Blob Storage."
+fi
# az storage fs directory upload -f "$fileSystem" --account-name "$storageAccount" -s "$extractedFolder1" --account-key "$accountKey" --recursive
# az storage fs directory upload -f "$fileSystem" --account-name "$storageAccount" -s "$extractedFolder2" --account-key "$accountKey" --recursive
diff --git a/infra/scripts/index_scripts/create_search_index.py b/infra/scripts/index_scripts/create_search_index.py
index 42316feff..b429a6456 100644
--- a/infra/scripts/index_scripts/create_search_index.py
+++ b/infra/scripts/index_scripts/create_search_index.py
@@ -22,6 +22,8 @@
SimpleField,
VectorSearch,
VectorSearchProfile,
+ AzureOpenAIVectorizer,
+ AzureOpenAIVectorizerParameters
)
from azure.storage.filedatalake import (
DataLakeDirectoryClient,
@@ -93,8 +95,19 @@
VectorSearchProfile(
name="myHnswProfile",
algorithm_configuration_name="myHnsw",
+ vectorizer_name="aoai-ada-002-vectorizer",
)
],
+ vectorizers= [
+ AzureOpenAIVectorizer(
+ vectorizer_name="aoai-ada-002-vectorizer",
+ parameters=AzureOpenAIVectorizerParameters(
+ resource_url=openai_api_base,
+ deployment_name=openai_embedding_model or "text-embedding-ada-002",
+ model_name=openai_embedding_model or "text-embedding-ada-002",
+ )
+ )
+ ]
)
semantic_config = SemanticConfiguration(
@@ -121,9 +134,7 @@
# Function: Get Embeddings
def get_embeddings(text: str, openai_api_base, openai_api_version, azure_token_provider):
- model_id = (
- openai_embedding_model if openai_embedding_model else "text-embedding-ada-002"
- )
+ model_id = openai_embedding_model or "text-embedding-ada-002"
client = AzureOpenAI(
api_version=openai_api_version,
azure_endpoint=openai_api_base,
@@ -201,7 +212,7 @@ def chunk_data(text):
print(paths)
search_client = SearchClient(search_endpoint, index_name, credential)
-index_client = SearchIndexClient(endpoint=search_endpoint, credential=credential)
+# index_client = SearchIndexClient(endpoint=search_endpoint, credential=credential)
# metadata_filepath = f'Data/{foldername}/meeting_transcripts_metadata/transcripts_metadata.csv'
# # df_metadata = spark.read.format("csv").option("header","true").option("multiLine", "true").option("quote", "\"").option("escape", "\"").load(metadata_filepath).toPandas()
diff --git a/infra/scripts/process_sample_data.sh b/infra/scripts/process_sample_data.sh
index 62f260f0c..7b6213eee 100644
--- a/infra/scripts/process_sample_data.sh
+++ b/infra/scripts/process_sample_data.sh
@@ -12,12 +12,17 @@ webAppManagedIdentityClientId="$8"
webAppManagedIdentityDisplayName="$9"
aiFoundryName="${10}"
aiSearchName="${11}"
+resourceGroupNameFoundry="${12}"
# get parameters from azd env, if not provided
if [ -z "$resourceGroupName" ]; then
resourceGroupName=$(azd env get-value RESOURCE_GROUP_NAME)
fi
+if [ -z "$resourceGroupNameFoundry" ]; then
+ resourceGroupNameFoundry=$(azd env get-value RESOURCE_GROUP_NAME_FOUNDRY)
+fi
+
if [ -z "$cosmosDbAccountName" ]; then
cosmosDbAccountName=$(azd env get-value COSMOSDB_ACCOUNT_NAME)
fi
@@ -58,12 +63,72 @@ if [ -z "$aiSearchName" ]; then
aiSearchName=$(azd env get-value AI_SEARCH_SERVICE_NAME)
fi
+azSubscriptionId=$(azd env get-value AZURE_SUBSCRIPTION_ID)
+
# Check if all required arguments are provided
-if [ -z "$resourceGroupName" ] || [ -z "$cosmosDbAccountName" ] || [ -z "$storageAccount" ] || [ -z "$fileSystem" ] || [ -z "$keyvaultName" ] || [ -z "$sqlServerName" ] || [ -z "$SqlDatabaseName" ] || [ -z "$webAppManagedIdentityClientId" ] || [ -z "$webAppManagedIdentityDisplayName" ] || [ -z "$aiFoundryName" ] || [ -z "$aiSearchName" ]; then
- echo "Usage: $0 "
+if [ -z "$resourceGroupName" ] || [ -z "$cosmosDbAccountName" ] || [ -z "$storageAccount" ] || [ -z "$fileSystem" ] || [ -z "$keyvaultName" ] || [ -z "$sqlServerName" ] || [ -z "$SqlDatabaseName" ] || [ -z "$webAppManagedIdentityClientId" ] || [ -z "$webAppManagedIdentityDisplayName" ] || [ -z "$aiFoundryName" ] || [ -z "$aiSearchName" ] || [ -z "$resourceGroupNameFoundry" ]; then
+ echo "Usage: $0 "
exit 1
fi
+# Authenticate with Azure
+if az account show &> /dev/null; then
+ echo "Already authenticated with Azure."
+else
+ echo "Not authenticated with Azure. Attempting to authenticate..."
+ if [ -n "$managedIdentityClientId" ]; then
+ # Use managed identity if running in Azure
+ echo "Authenticating with Managed Identity..."
+ az login --identity --client-id ${managedIdentityClientId}
+ else
+ # Use Azure CLI login if running locally
+ echo "Authenticating with Azure CLI..."
+ az login
+ fi
+fi
+
+#check if user has selected the correct subscription
+currentSubscriptionId=$(az account show --query id -o tsv)
+currentSubscriptionName=$(az account show --query name -o tsv)
+if [ "$currentSubscriptionId" != "$azSubscriptionId" ]; then
+ echo "Current selected subscription is $currentSubscriptionName ( $currentSubscriptionId )."
+ read -rp "Do you want to continue with this subscription?(y/n): " confirmation
+ if [[ "$confirmation" != "y" && "$confirmation" != "Y" ]]; then
+ echo "Fetching available subscriptions..."
+ availableSubscriptions=$(az account list --query "[?state=='Enabled'].[name,id]" --output tsv)
+ while true; do
+ echo ""
+ echo "Available Subscriptions:"
+ echo "========================"
+ echo "$availableSubscriptions" | awk '{printf "%d. %s ( %s )\n", NR, $1, $2}'
+ echo "========================"
+ echo ""
+ read -rp "Enter the number of the subscription (1-$(echo "$availableSubscriptions" | wc -l)) to use: " subscriptionIndex
+ if [[ "$subscriptionIndex" =~ ^[0-9]+$ ]] && [ "$subscriptionIndex" -ge 1 ] && [ "$subscriptionIndex" -le $(echo "$availableSubscriptions" | wc -l) ]; then
+ selectedSubscription=$(echo "$availableSubscriptions" | sed -n "${subscriptionIndex}p")
+ selectedSubscriptionName=$(echo "$selectedSubscription" | cut -f1)
+ selectedSubscriptionId=$(echo "$selectedSubscription" | cut -f2)
+
+ # Set the selected subscription
+ if az account set --subscription "$selectedSubscriptionId"; then
+ echo "Switched to subscription: $selectedSubscriptionName ( $selectedSubscriptionId )"
+ break
+ else
+ echo "Failed to switch to subscription: $selectedSubscriptionName ( $selectedSubscriptionId )."
+ fi
+ else
+ echo "Invalid selection. Please try again."
+ fi
+ done
+ else
+ echo "Proceeding with the current subscription: $currentSubscriptionName ( $currentSubscriptionId )"
+ az account set --subscription "$currentSubscriptionId"
+ fi
+else
+ echo "Proceeding with the subscription: $currentSubscriptionName ( $currentSubscriptionId )"
+ az account set --subscription "$currentSubscriptionId"
+fi
+
# Call add_cosmosdb_access.sh
echo "Running add_cosmosdb_access.sh"
bash infra/scripts/add_cosmosdb_access.sh "$resourceGroupName" "$cosmosDbAccountName"
@@ -84,7 +149,7 @@ echo "copy_kb_files.sh completed successfully."
# Call run_create_index_scripts.sh
echo "Running run_create_index_scripts.sh"
-bash infra/scripts/run_create_index_scripts.sh "$keyvaultName" "" "" "$resourceGroupName" "$sqlServerName" "$aiFoundryName" "$aiSearchName"
+bash infra/scripts/run_create_index_scripts.sh "$keyvaultName" "" "" "$resourceGroupName" "$sqlServerName" "$aiFoundryName" "$aiSearchName" "$resourceGroupNameFoundry"
if [ $? -ne 0 ]; then
echo "Error: run_create_index_scripts.sh failed."
exit 1
diff --git a/infra/scripts/quota_check_params.sh b/infra/scripts/quota_check_params.sh
old mode 100644
new mode 100755
diff --git a/infra/scripts/run_create_index_scripts.sh b/infra/scripts/run_create_index_scripts.sh
index dbe33af00..fcf775723 100644
--- a/infra/scripts/run_create_index_scripts.sh
+++ b/infra/scripts/run_create_index_scripts.sh
@@ -1,5 +1,4 @@
#!/bin/bash
-echo "started the script"
# Variables
keyvaultName="$1"
@@ -9,6 +8,7 @@ resourceGroupName="$4"
sqlServerName="$5"
aiFoundryName="$6"
aiSearchName="$7"
+resourceGroupNameFoundry="$8"
echo "Script Started"
@@ -16,6 +16,7 @@ echo "Script Started"
if az account show &> /dev/null; then
echo "Already authenticated with Azure."
else
+ echo "Not authenticated with Azure. Attempting to authenticate..."
if [ -n "$managedIdentityClientId" ]; then
# Use managed identity if running in Azure
echo "Authenticating with Managed Identity..."
@@ -25,111 +26,117 @@ else
echo "Authenticating with Azure CLI..."
az login
fi
- echo "Not authenticated with Azure. Attempting to authenticate..."
fi
-# if using managed identity, skip role assignments as its already provided via bicep
-if [ -n "$managedIdentityClientId" ]; then
- echo "Skipping role assignments as managed identity is used"
-else
- # Get signed in user and store the output
- echo "Getting signed in user id and display name"
- signed_user=$(az ad signed-in-user show --query "{id:id, displayName:displayName}" -o json)
-
- # Extract id and displayName using grep and sed
- signed_user_id=$(echo "$signed_user" | grep -oP '"id":\s*"\K[^"]+')
- signed_user_display_name=$(echo "$signed_user" | grep -oP '"displayName":\s*"\K[^"]+')
-
- # echo "Getting signed in user id"
- # signed_user_id=$(az ad signed-in-user show --query id -o tsv)
-
- ### Assign Key Vault Administrator role to the signed in user ###
-
- echo "Getting key vault resource id"
- key_vault_resource_id=$(az keyvault show --name $keyvaultName --query id --output tsv)
-
- # Check if the user has the Key Vault Administrator role
- echo "Checking if user has the Key Vault Administrator role"
- role_assignment=$(MSYS_NO_PATHCONV=1 az role assignment list --assignee $signed_user_id --role "Key Vault Administrator" --scope $key_vault_resource_id --query "[].roleDefinitionId" -o tsv)
- if [ -z "$role_assignment" ]; then
- echo "User does not have the Key Vault Administrator role. Assigning the role."
- MSYS_NO_PATHCONV=1 az role assignment create --assignee $signed_user_id --role "Key Vault Administrator" --scope $key_vault_resource_id --output none
- if [ $? -eq 0 ]; then
- echo "Key Vault Administrator role assigned successfully."
- else
- echo "Failed to assign Key Vault Administrator role."
- exit 1
- fi
+# Get signed in user and store the output
+echo "Getting signed in user id and display name"
+signed_user=$(az ad signed-in-user show --query "{id:id, displayName:displayName}" -o json)
+
+# Extract id and displayName using grep and sed
+signed_user_id=$(echo "$signed_user" | grep -oP '"id":\s*"\K[^"]+')
+signed_user_display_name=$(echo "$signed_user" | grep -oP '"displayName":\s*"\K[^"]+')
+
+if [ $? -ne 0 ]; then
+ if [ -z "$managedIdentityClientId" ]; then
+ echo "Error: Failed to get signed in user id."
+ exit 1
else
- echo "User already has the Key Vault Administrator role."
+ signed_user_id=$managedIdentityClientId
+ signed_user_display_name=$(az ad sp show --id "$signed_user_id" --query displayName -o tsv)
fi
+fi
+
+### Assign Key Vault Administrator role to the signed in user ###
- ### Assign Azure AI User role to the signed in user ###
-
- echo "Getting Azure AI resource id"
- aif_resource_id=$(az cognitiveservices account show --name $aiFoundryName --resource-group $resourceGroupName --query id --output tsv)
-
- # Check if the user has the Azure AI User role
- echo "Checking if user has the Azure AI User role"
- role_assignment=$(MSYS_NO_PATHCONV=1 az role assignment list --role 53ca6127-db72-4b80-b1b0-d745d6d5456d --scope $aif_resource_id --assignee $signed_user_id --query "[].roleDefinitionId" -o tsv)
- if [ -z "$role_assignment" ]; then
- echo "User does not have the Azure AI User role. Assigning the role."
- MSYS_NO_PATHCONV=1 az role assignment create --assignee $signed_user_id --role 53ca6127-db72-4b80-b1b0-d745d6d5456d --scope $aif_resource_id --output none
- if [ $? -eq 0 ]; then
- echo "Azure AI User role assigned successfully."
- else
- echo "Failed to assign Azure AI User role."
- exit 1
- fi
+echo "Getting key vault resource id"
+key_vault_resource_id=$(az keyvault show --name $keyvaultName --query id --output tsv)
+
+# Check if the user has the Key Vault Administrator role
+echo "Checking if user has the Key Vault Administrator role"
+role_assignment=$(MSYS_NO_PATHCONV=1 az role assignment list --assignee $signed_user_id --role "Key Vault Administrator" --scope $key_vault_resource_id --query "[].roleDefinitionId" -o tsv)
+if [ -z "$role_assignment" ]; then
+ echo "User does not have the Key Vault Administrator role. Assigning the role."
+ MSYS_NO_PATHCONV=1 az role assignment create --assignee $signed_user_id --role "Key Vault Administrator" --scope $key_vault_resource_id --output none
+ if [ $? -eq 0 ]; then
+ echo "Key Vault Administrator role assigned successfully."
else
- echo "User already has the Azure AI User role."
+ echo "Failed to assign Key Vault Administrator role."
+ exit 1
fi
+else
+ echo "User already has the Key Vault Administrator role."
+fi
+
+### Assign Azure AI User role to the signed in user ###
+
+echo "Getting Azure AI Foundry resource id"
+aif_resource_id=$(az cognitiveservices account show --name $aiFoundryName --resource-group $resourceGroupNameFoundry --query id --output tsv)
- ### Assign Search Index Data Contributor role to the signed in user ###
-
- echo "Getting Azure Search resource id"
- search_resource_id=$(az search service show --name $aiSearchName --resource-group $resourceGroupName --query id --output tsv)
-
- # Check if the user has the Search Index Data Contributor role
- echo "Checking if user has the Search Index Data Contributor role"
- role_assignment=$(MSYS_NO_PATHCONV=1 az role assignment list --assignee $signed_user_id --role 8ebe5a00-799e-43f5-93ac-243d3dce84a7 --scope $search_resource_id --query "[].roleDefinitionId" -o tsv)
- if [ -z "$role_assignment" ]; then
- echo "User does not have the Search Index Data Contributor role. Assigning the role."
- MSYS_NO_PATHCONV=1 az role assignment create --assignee $signed_user_id --role 8ebe5a00-799e-43f5-93ac-243d3dce84a7 --scope $search_resource_id --output none
- if [ $? -eq 0 ]; then
- echo "Search Index Data Contributor role assigned successfully."
- else
- echo "Failed to assign Search Index Data Contributor role."
- exit 1
- fi
+# Check if the user has the Azure AI User role
+echo "Checking if user has the Azure AI User role"
+role_assignment=$(MSYS_NO_PATHCONV=1 az role assignment list --role 53ca6127-db72-4b80-b1b0-d745d6d5456d --scope $aif_resource_id --assignee $signed_user_id --query "[].roleDefinitionId" -o tsv)
+if [ -z "$role_assignment" ]; then
+ echo "User does not have the Azure AI User role. Assigning the role."
+ MSYS_NO_PATHCONV=1 az role assignment create --assignee $signed_user_id --role 53ca6127-db72-4b80-b1b0-d745d6d5456d --scope $aif_resource_id --output none
+ if [ $? -eq 0 ]; then
+ echo "Azure AI User role assigned successfully."
else
- echo "User already has the Search Index Data Contributor role."
+ echo "Failed to assign Azure AI User role."
+ exit 1
fi
+else
+ echo "User already has the Azure AI User role."
+fi
- ### Assign signed in user as SQL Server Admin ###
- echo "Getting Azure SQL Server resource id"
- sql_server_resource_id=$(az sql server show --name $sqlServerName --resource-group $resourceGroupName --query id --output tsv)
+### Assign Search Index Data Contributor role to the signed in user ###
- # Check if the user is Azure SQL Server Admin
- echo "Checking if user is Azure SQL Server Admin"
- admin=$(MSYS_NO_PATHCONV=1 az sql server ad-admin list --ids $sql_server_resource_id --query "[?sid == '$signed_user_id']" -o tsv)
+echo "Getting Azure Search resource id"
+search_resource_id=$(az search service show --name $aiSearchName --resource-group $resourceGroupName --query id --output tsv)
- # Check if the role exists
- if [ -n "$admin" ]; then
- echo "User is already Azure SQL Server Admin"
+# Check if the user has the Search Index Data Contributor role
+echo "Checking if user has the Search Index Data Contributor role"
+role_assignment=$(MSYS_NO_PATHCONV=1 az role assignment list --assignee $signed_user_id --role 8ebe5a00-799e-43f5-93ac-243d3dce84a7 --scope $search_resource_id --query "[].roleDefinitionId" -o tsv)
+if [ -z "$role_assignment" ]; then
+ echo "User does not have the Search Index Data Contributor role. Assigning the role."
+ MSYS_NO_PATHCONV=1 az role assignment create --assignee $signed_user_id --role 8ebe5a00-799e-43f5-93ac-243d3dce84a7 --scope $search_resource_id --output none
+ if [ $? -eq 0 ]; then
+ echo "Search Index Data Contributor role assigned successfully."
else
- echo "User is not Azure SQL Server Admin. Assigning the role."
- MSYS_NO_PATHCONV=1 az sql server ad-admin create --display-name "$signed_user_display_name" --object-id $signed_user_id --resource-group $resourceGroupName --server $sqlServerName --output none
- if [ $? -eq 0 ]; then
- echo "Assigned user as Azure SQL Server Admin."
- else
- echo "Failed to assign Azure SQL Server Admin role."
- exit 1
- fi
+ echo "Failed to assign Search Index Data Contributor role."
+ exit 1
fi
+else
+ echo "User already has the Search Index Data Contributor role."
fi
+
+### Assign signed in user as SQL Server Admin ###
+
+echo "Getting Azure SQL Server resource id"
+sql_server_resource_id=$(az sql server show --name $sqlServerName --resource-group $resourceGroupName --query id --output tsv)
+
+# Check if the user is Azure SQL Server Admin
+echo "Checking if user is Azure SQL Server Admin"
+admin=$(MSYS_NO_PATHCONV=1 az sql server ad-admin list --ids $sql_server_resource_id --query "[?sid == '$signed_user_id']" -o tsv)
+
+# Check if the role exists
+if [ -n "$admin" ]; then
+ echo "User is already Azure SQL Server Admin"
+else
+ echo "User is not Azure SQL Server Admin. Assigning the role."
+ MSYS_NO_PATHCONV=1 az sql server ad-admin create --display-name "$signed_user_display_name" --object-id $signed_user_id --resource-group $resourceGroupName --server $sqlServerName --output none
+ if [ $? -eq 0 ]; then
+ echo "Assigned user as Azure SQL Server Admin."
+ else
+ echo "Failed to assign Azure SQL Server Admin role."
+ exit 1
+ fi
+fi
+
+# echo "Getting signed in user id"
+# signed_user_id=$(az ad signed-in-user show --query id -o tsv)
+
# RUN apt-get update
# RUN apt-get install python3 python3-dev g++ unixodbc-dev unixodbc libpq-dev
# apk add python3 python3-dev g++ unixodbc-dev unixodbc libpq-dev
@@ -140,7 +147,7 @@ fi
pythonScriptPath="infra/scripts/index_scripts/"
# Check if running in Azure Container App
-if !([ -z "$baseUrl" ] && [ -z "$managedIdentityClientId" ]); then
+if [ -n "$baseUrl" ] && [ -n "$managedIdentityClientId" ]; then
requirementFile="requirements.txt"
requirementFileUrl=${baseUrl}${pythonScriptPath}"requirements.txt"
@@ -158,7 +165,7 @@ if !([ -z "$baseUrl" ] && [ -z "$managedIdentityClientId" ]); then
fi
-#Replace key vault name
+# Replace key vault name
sed -i "s/kv_to-be-replaced/${keyvaultName}/g" ${pythonScriptPath}"create_search_index.py"
sed -i "s/kv_to-be-replaced/${keyvaultName}/g" ${pythonScriptPath}"create_sql_tables.py"
if [ -n "$managedIdentityClientId" ]; then
diff --git a/src/App/app.py b/src/App/app.py
index 6127e0268..901494b2b 100644
--- a/src/App/app.py
+++ b/src/App/app.py
@@ -78,14 +78,19 @@ def create_app():
# Setup agent initialization and cleanup
@app.before_serving
async def startup():
- app.agent = await AgentFactory.get_instance()
- logging.info("Agent initialized during application startup")
+ app.wealth_advisor_agent = await AgentFactory.get_wealth_advisor_agent()
+ logging.info("Wealth Advisor Agent initialized during application startup")
+ app.search_agent = await AgentFactory.get_search_agent()
+ logging.info(
+ "Call Transcript Search Agent initialized during application startup"
+ )
@app.after_serving
async def shutdown():
- await AgentFactory.delete_instance()
- app.agent = None
- logging.info("Agent cleaned up during application shutdown")
+ await AgentFactory.delete_all_agent_instance()
+ app.wealth_advisor_agent = None
+ app.search_agent = None
+ logging.info("Agents cleaned up during application shutdown")
# app.secret_key = secrets.token_hex(16)
# app.session_interface = SecureCookieSessionInterface()
diff --git a/src/App/backend/agents/agent_factory.py b/src/App/backend/agents/agent_factory.py
index 604f38f05..92c291852 100644
--- a/src/App/backend/agents/agent_factory.py
+++ b/src/App/backend/agents/agent_factory.py
@@ -7,28 +7,33 @@
"""
import asyncio
+from typing import Optional
+from azure.ai.projects import AIProjectClient
+from azure.identity import DefaultAzureCredential as DefaultAzureCredentialSync
from azure.identity.aio import DefaultAzureCredential
from semantic_kernel.agents import AzureAIAgent, AzureAIAgentSettings
+from backend.common.config import config
from backend.plugins.chat_with_data_plugin import ChatWithDataPlugin
class AgentFactory:
"""
- Singleton factory for creating and managing an AzureAIAgent instance.
+ Singleton factory for creating and managing an AzureAIAgent instances.
"""
- _instance = None
_lock = asyncio.Lock()
+ _wealth_advisor_agent: Optional[AzureAIAgent] = None
+ _search_agent: Optional[dict] = None
@classmethod
- async def get_instance(cls):
+ async def get_wealth_advisor_agent(cls):
"""
- Get or create the singleton AzureAIAgent instance.
+ Get or create the singleton WealthAdvisor AzureAIAgent instance.
"""
async with cls._lock:
- if cls._instance is None:
+ if cls._wealth_advisor_agent is None:
ai_agent_settings = AzureAIAgentSettings()
creds = DefaultAzureCredential()
client = AzureAIAgent.create_client(
@@ -48,16 +53,55 @@ async def get_instance(cls):
definition=agent_definition,
plugins=[ChatWithDataPlugin()],
)
- cls._instance = agent
- return cls._instance
+ cls._wealth_advisor_agent = agent
+ return cls._wealth_advisor_agent
@classmethod
- async def delete_instance(cls):
+ async def get_search_agent(cls):
"""
- Delete the singleton AzureAIAgent instance if it exists.
- Also deletes all threads in ChatService.thread_cache.
+ Get or create the singleton CallTranscriptSearch AzureAIAgent instance.
"""
async with cls._lock:
- if cls._instance is not None:
- await cls._instance.client.agents.delete_agent(cls._instance.id)
- cls._instance = None
+ if cls._search_agent is None:
+
+ agent_instructions = config.CALL_TRANSCRIPT_SYSTEM_PROMPT
+ if not agent_instructions:
+ agent_instructions = (
+ "You are an assistant who supports wealth advisors in preparing for client meetings. "
+ "You have access to the client's past meeting call transcripts via AI Search tool. "
+ "When answering questions, especially summary requests, provide a detailed and structured response that includes key topics, concerns, decisions, and trends. "
+ "If no data is available, state 'No relevant data found for previous meetings.'"
+ )
+
+ project_client = AIProjectClient(
+ endpoint=config.AI_PROJECT_ENDPOINT,
+ credential=DefaultAzureCredentialSync(),
+ api_version="2025-05-01",
+ )
+
+ agent = project_client.agents.create_agent(
+ model=config.AZURE_OPENAI_MODEL,
+ instructions=agent_instructions,
+ name="CallTranscriptSearchAgent",
+ )
+ cls._search_agent = {"agent": agent, "client": project_client}
+ return cls._search_agent
+
+ @classmethod
+ async def delete_all_agent_instance(cls):
+ """
+ Delete the singleton AzureAIAgent instances if it exists.
+ """
+ async with cls._lock:
+ if cls._wealth_advisor_agent is not None:
+ await cls._wealth_advisor_agent.client.agents.delete_agent(
+ cls._wealth_advisor_agent.id
+ )
+ cls._wealth_advisor_agent = None
+
+ if cls._search_agent is not None:
+ cls._search_agent["client"].agents.delete_agent(
+ cls._search_agent["agent"].id
+ )
+ cls._search_agent["client"].close()
+ cls._search_agent = None
diff --git a/src/App/backend/common/config.py b/src/App/backend/common/config.py
index 38afe161b..06073a427 100644
--- a/src/App/backend/common/config.py
+++ b/src/App/backend/common/config.py
@@ -72,6 +72,9 @@ def __init__(self):
"AZURE_SEARCH_PERMITTED_GROUPS_COLUMN"
)
self.AZURE_SEARCH_STRICTNESS = os.environ.get("AZURE_SEARCH_STRICTNESS", 3)
+ self.AZURE_SEARCH_CONNECTION_NAME = os.environ.get(
+ "AZURE_SEARCH_CONNECTION_NAME", "foundry-search-connection"
+ )
# AOAI Integration Settings
self.AZURE_OPENAI_RESOURCE = os.environ.get("AZURE_OPENAI_RESOURCE")
diff --git a/src/App/backend/plugins/chat_with_data_plugin.py b/src/App/backend/plugins/chat_with_data_plugin.py
index 13f3952ae..83bedd264 100644
--- a/src/App/backend/plugins/chat_with_data_plugin.py
+++ b/src/App/backend/plugins/chat_with_data_plugin.py
@@ -1,6 +1,13 @@
+import logging
from typing import Annotated
import openai
+from azure.ai.agents.models import (
+ Agent,
+ AzureAISearchQueryType,
+ AzureAISearchTool,
+ MessageRole,
+)
from azure.ai.projects import AIProjectClient
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
from semantic_kernel.functions.kernel_function_decorator import kernel_function
@@ -19,7 +26,7 @@ class ChatWithDataPlugin:
name="GreetingsResponse",
description="Respond to any greeting or general questions",
)
- def greeting(
+ async def greeting(
self, input: Annotated[str, "the question"]
) -> Annotated[str, "The output is a string"]:
"""
@@ -55,7 +62,7 @@ def greeting(
name="ChatWithSQLDatabase",
description="Given a query about client assets, investments and scheduled meetings (including upcoming or next meeting dates/times), get details from the database based on the provided question and client id",
)
- def get_SQL_Response(
+ async def get_SQL_Response(
self,
input: Annotated[str, "the question"],
ClientId: Annotated[str, "the ClientId"],
@@ -155,7 +162,7 @@ def get_SQL_Response(
name="ChatWithCallTranscripts",
description="given a query about meetings summary or actions or notes, get answer from search index for a given ClientId",
)
- def get_answers_from_calltranscripts(
+ async def get_answers_from_calltranscripts(
self,
question: Annotated[str, "the question"],
ClientId: Annotated[str, "the ClientId"],
@@ -169,73 +176,90 @@ def get_answers_from_calltranscripts(
return "Error: Question input is required"
try:
- client = self.get_openai_client()
-
- system_message = config.CALL_TRANSCRIPT_SYSTEM_PROMPT
- if not system_message:
- system_message = (
- "You are an assistant who supports wealth advisors in preparing for client meetings. "
- "You have access to the client's past meeting call transcripts. "
- "When answering questions, especially summary requests, provide a detailed and structured response that includes key topics, concerns, decisions, and trends. "
- "If no data is available, state 'No relevant data found for previous meetings.'"
+ response_text = ""
+
+ from backend.agents.agent_factory import AgentFactory
+
+ agent_info: dict = await AgentFactory.get_search_agent()
+
+ agent: Agent = agent_info["agent"]
+ project_client: AIProjectClient = agent_info["client"]
+
+ try:
+ field_mapping = {
+ "contentFields": ["content"],
+ "urlField": "sourceurl",
+ "titleField": "chunk_id",
+ "vector_fields": ["contentVector"],
+ }
+
+ project_index = project_client.indexes.create_or_update(
+ name=f"project-index-{config.AZURE_SEARCH_INDEX}",
+ version="1",
+ body={
+ "connectionName": config.AZURE_SEARCH_CONNECTION_NAME,
+ "indexName": config.AZURE_SEARCH_INDEX,
+ "type": "AzureSearch",
+ "fieldMapping": field_mapping,
+ },
)
- completion = client.chat.completions.create(
- model=config.AZURE_OPENAI_MODEL,
- messages=[
- {"role": "system", "content": system_message},
- {"role": "user", "content": question},
- ],
- seed=42,
- temperature=0,
- top_p=1,
- n=1,
- max_tokens=800,
- extra_body={
- "data_sources": [
- {
- "type": "azure_search",
- "parameters": {
- "endpoint": config.AZURE_SEARCH_ENDPOINT,
- "index_name": "transcripts_index",
- "query_type": "vector_simple_hybrid",
- "fields_mapping": {
- "content_fields_separator": "\n",
- "content_fields": ["content"],
- "filepath_field": "chunk_id",
- "title_field": "",
- "url_field": "sourceurl",
- "vector_fields": ["contentVector"],
- },
- "semantic_configuration": "my-semantic-config",
- "in_scope": "true",
- # "role_information": system_message,
- "filter": f"client_id eq '{ClientId}'",
- "strictness": 3,
- "top_n_documents": 5,
- "authentication": {
- "type": "system_assigned_managed_identity"
- },
- "embedding_dependency": {
- "type": "deployment_name",
- "deployment_name": "text-embedding-ada-002",
- },
- },
- }
- ]
- },
- )
+ ai_search_tool = AzureAISearchTool(
+ index_asset_id=f"{project_index.name}/versions/{project_index.version}",
+ index_connection_id=None,
+ index_name=None,
+ query_type=AzureAISearchQueryType.VECTOR_SIMPLE_HYBRID,
+ filter=f"client_id eq '{ClientId}'",
+ )
- if not completion.choices:
- return "No data found for that client."
+ agent = project_client.agents.update_agent(
+ agent_id=agent.id,
+ tools=ai_search_tool.definitions,
+ tool_resources=ai_search_tool.resources,
+ )
+
+ thread = project_client.agents.threads.create()
+
+ project_client.agents.messages.create(
+ thread_id=thread.id,
+ role=MessageRole.USER,
+ content=question,
+ )
+
+ run = project_client.agents.runs.create_and_process(
+ thread_id=thread.id,
+ agent_id=agent.id,
+ tool_choice={"type": "azure_ai_search"},
+ temperature=0.0,
+ )
+
+ if run.status == "failed":
+ logging.error(f"AI Search Agent Run failed: {run.last_error}")
+ return "Error retrieving data from call transcripts"
+ else:
+ message = (
+ project_client.agents.messages.get_last_message_text_by_role(
+ thread_id=thread.id, role=MessageRole.AGENT
+ )
+ )
+ if message:
+ response_text = message.text.value
+
+ except Exception as e:
+ logging.error(f"Error in AI Search Tool: {str(e)}")
+ return "Error retrieving data from call transcripts"
+
+ finally:
+ if thread:
+ project_client.agents.threads.delete(thread.id)
- response_text = completion.choices[0].message.content
if not response_text.strip():
return "No data found for that client."
return response_text
except Exception as e:
- return f"Error retrieving data from call transcripts: {str(e)}"
+ logging.error(f"Error in get_answers_from_calltranscripts: {str(e)}")
+ return "Error retrieving data from call transcripts"
def get_openai_client(self):
token_provider = get_bearer_token_provider(
diff --git a/src/App/backend/services/chat_service.py b/src/App/backend/services/chat_service.py
index 8dc8375a4..56a1daf08 100644
--- a/src/App/backend/services/chat_service.py
+++ b/src/App/backend/services/chat_service.py
@@ -24,7 +24,7 @@ async def stream_response_from_wealth_assistant(query: str, client_id: str):
additional_instructions = (
"The currently selected client's name is '{SelectedClientName}'. Treat any case-insensitive or partial mention as referring to this client."
"If the user mentions no name, assume they are asking about '{SelectedClientName}'."
- "If the user references a name that clearly differs from '{SelectedClientName}', respond only with: 'Please only ask questions about the selected client or select another client.' Otherwise, provide thorough answers for every question using only data from SQL or call transcripts."
+ "If the user references a name that clearly differs from '{SelectedClientName}' or comparing with other clients, respond only with: 'Please only ask questions about the selected client or select another client.' Otherwise, provide thorough answers for every question using only data from SQL or call transcripts."
"If no data is found, respond with 'No data found for that client.' Remove any client identifiers from the final response."
"Always send clientId as '{client_id}'."
)
@@ -37,7 +37,7 @@ async def stream_response_from_wealth_assistant(query: str, client_id: str):
"{client_id}", client_id
)
- agent: AzureAIAgent = current_app.agent
+ agent: AzureAIAgent = current_app.wealth_advisor_agent
thread: AzureAIAgentThread = None
message = ChatMessageContent(role=AuthorRole.USER, content=query)
diff --git a/src/App/tests/backend/agents/test_agent_factory.py b/src/App/tests/backend/agents/test_agent_factory.py
index a5af29787..dcae796ad 100644
--- a/src/App/tests/backend/agents/test_agent_factory.py
+++ b/src/App/tests/backend/agents/test_agent_factory.py
@@ -1,4 +1,4 @@
-from unittest.mock import AsyncMock, patch
+from unittest.mock import AsyncMock, MagicMock, patch
import pytest
@@ -11,51 +11,156 @@ class TestAgentFactory:
@pytest.fixture
def reset_singleton(self):
"""Fixture to reset the singleton between tests"""
- original_instance = AgentFactory._instance
- AgentFactory._instance = None
+ original_wealth_advisor = AgentFactory._wealth_advisor_agent
+ original_search_agent = AgentFactory._search_agent
+ AgentFactory._wealth_advisor_agent = None
+ AgentFactory._search_agent = None
yield
- AgentFactory._instance = original_instance
+ AgentFactory._wealth_advisor_agent = original_wealth_advisor
+ AgentFactory._search_agent = original_search_agent
@pytest.mark.asyncio
@patch("backend.agents.agent_factory.AzureAIAgent")
@patch("backend.agents.agent_factory.DefaultAzureCredential")
@patch("backend.agents.agent_factory.AzureAIAgentSettings")
- async def test_get_instance_creates_agent_when_none_exists(
- self, mock_settings, mock_credential, mock_agent, reset_singleton
+ @patch("backend.agents.agent_factory.ChatWithDataPlugin")
+ async def test_get_wealth_advisor_agent_creates_agent_when_none_exists(
+ self, mock_plugin, mock_settings, mock_credential, mock_agent, reset_singleton
):
- """Test that get_instance creates a new agent when none exists."""
+ """Test that get_wealth_advisor_agent creates a new agent when none exists."""
# Arrange
mock_agent_instance = AsyncMock()
mock_agent.return_value = mock_agent_instance
mock_client = AsyncMock()
mock_agent.create_client.return_value = mock_client
+ mock_agent_definition = AsyncMock()
+ mock_client.agents.create_agent.return_value = mock_agent_definition
+ mock_settings_instance = MagicMock()
+ mock_settings_instance.endpoint = "https://test.endpoint.com"
+ mock_settings_instance.model_deployment_name = "test-model"
+ mock_settings.return_value = mock_settings_instance
# Act
- result = await AgentFactory.get_instance()
+ result = await AgentFactory.get_wealth_advisor_agent()
# Assert
assert result is not None
- assert AgentFactory._instance is not None
- assert AgentFactory._instance is result
- assert mock_agent.create_client.called
- assert mock_agent.called
+ assert AgentFactory._wealth_advisor_agent is not None
+ assert AgentFactory._wealth_advisor_agent is result
+ mock_agent.create_client.assert_called_once()
+ mock_client.agents.create_agent.assert_called_once_with(
+ model="test-model",
+ name="WealthAdvisor",
+ instructions="You are a helpful assistant to a Wealth Advisor.",
+ )
+ mock_agent.assert_called_once()
@pytest.mark.asyncio
- async def test_get_instance_returns_existing_agent(self, reset_singleton):
- """Test that get_instance returns existing agent when one exists."""
+ async def test_get_wealth_advisor_agent_returns_existing_agent(
+ self, reset_singleton
+ ):
+ """Test that get_wealth_advisor_agent returns existing agent when one exists."""
# Arrange
mock_instance = AsyncMock()
- AgentFactory._instance = mock_instance
+ AgentFactory._wealth_advisor_agent = mock_instance
# Act
- result = await AgentFactory.get_instance()
+ result = await AgentFactory.get_wealth_advisor_agent()
# Assert
assert result is mock_instance
@pytest.mark.asyncio
- async def test_multiple_calls_return_same_instance(self, reset_singleton):
- """Test that multiple calls to get_instance return the same instance."""
+ @patch("backend.agents.agent_factory.config")
+ @patch("backend.agents.agent_factory.AIProjectClient")
+ @patch("backend.agents.agent_factory.DefaultAzureCredentialSync")
+ async def test_get_search_agent_creates_agent_when_none_exists(
+ self, mock_credential_sync, mock_ai_project_client, mock_config, reset_singleton
+ ):
+ """Test that get_search_agent creates a new agent when none exists."""
+ # Arrange
+ mock_config.CALL_TRANSCRIPT_SYSTEM_PROMPT = "Test search agent instructions"
+ mock_config.AI_PROJECT_ENDPOINT = "https://test.ai.endpoint.com"
+ mock_config.AZURE_OPENAI_MODEL = "test-search-model"
+
+ mock_project_client_instance = MagicMock()
+ mock_ai_project_client.return_value = mock_project_client_instance
+ mock_agent = MagicMock()
+ mock_project_client_instance.agents.create_agent.return_value = mock_agent
+
+ # Act
+ result = await AgentFactory.get_search_agent()
+
+ # Assert
+ assert result is not None
+ assert AgentFactory._search_agent is not None
+ assert AgentFactory._search_agent is result
+ assert result["agent"] is mock_agent
+ assert result["client"] is mock_project_client_instance
+ mock_ai_project_client.assert_called_once_with(
+ endpoint="https://test.ai.endpoint.com",
+ credential=mock_credential_sync.return_value,
+ api_version="2025-05-01",
+ )
+ mock_project_client_instance.agents.create_agent.assert_called_once_with(
+ model="test-search-model",
+ instructions="Test search agent instructions",
+ name="CallTranscriptSearchAgent",
+ )
+
+ @pytest.mark.asyncio
+ @patch("backend.agents.agent_factory.config")
+ @patch("backend.agents.agent_factory.AIProjectClient")
+ @patch("backend.agents.agent_factory.DefaultAzureCredentialSync")
+ async def test_get_search_agent_with_default_instructions(
+ self, mock_credential_sync, mock_ai_project_client, mock_config, reset_singleton
+ ):
+ """Test that get_search_agent uses default instructions when config is empty."""
+ # Arrange
+ mock_config.CALL_TRANSCRIPT_SYSTEM_PROMPT = None
+ mock_config.AI_PROJECT_ENDPOINT = "https://test.ai.endpoint.com"
+ mock_config.AZURE_OPENAI_MODEL = "test-search-model"
+
+ mock_project_client_instance = MagicMock()
+ mock_ai_project_client.return_value = mock_project_client_instance
+ mock_agent = MagicMock()
+ mock_project_client_instance.agents.create_agent.return_value = mock_agent
+
+ # Act
+ result = await AgentFactory.get_search_agent()
+
+ # Assert
+ assert result is not None
+ expected_default_instructions = (
+ "You are an assistant who supports wealth advisors in preparing for client meetings. "
+ "You have access to the client's past meeting call transcripts via AI Search tool. "
+ "When answering questions, especially summary requests, provide a detailed and structured response that includes key topics, concerns, decisions, and trends. "
+ "If no data is available, state 'No relevant data found for previous meetings.'"
+ )
+ mock_project_client_instance.agents.create_agent.assert_called_once_with(
+ model="test-search-model",
+ instructions=expected_default_instructions,
+ name="CallTranscriptSearchAgent",
+ )
+
+ @pytest.mark.asyncio
+ async def test_get_search_agent_returns_existing_agent(self, reset_singleton):
+ """Test that get_search_agent returns existing agent when one exists."""
+ # Arrange
+ mock_agent_dict = {"agent": MagicMock(), "client": MagicMock()}
+ AgentFactory._search_agent = mock_agent_dict
+
+ # Act
+ result = await AgentFactory.get_search_agent()
+
+ # Assert
+ assert result is mock_agent_dict
+
+ @pytest.mark.asyncio
+ async def test_multiple_calls_return_same_wealth_advisor_instance(
+ self, reset_singleton
+ ):
+ """Test that multiple calls to get_wealth_advisor_agent return the same instance."""
# Arrange
mock_client = AsyncMock()
mock_agent_definition = AsyncMock()
@@ -70,37 +175,133 @@ async def test_multiple_calls_return_same_instance(self, reset_singleton):
with patch("backend.agents.agent_factory.DefaultAzureCredential"):
with patch("backend.agents.agent_factory.AzureAIAgentSettings"):
- # Act
- instance1 = await AgentFactory.get_instance()
- instance2 = await AgentFactory.get_instance()
+ with patch("backend.agents.agent_factory.ChatWithDataPlugin"):
+ # Act
+ instance1 = await AgentFactory.get_wealth_advisor_agent()
+ instance2 = await AgentFactory.get_wealth_advisor_agent()
# Assert
assert instance1 is instance2
@pytest.mark.asyncio
- async def test_delete_instance_when_none_exists(self, reset_singleton):
- """Test that delete_instance handles when no agent exists."""
+ async def test_multiple_calls_return_same_search_agent_instance(
+ self, reset_singleton
+ ):
+ """Test that multiple calls to get_search_agent return the same instance."""
+ with patch("backend.agents.agent_factory.config") as mock_config:
+ with patch(
+ "backend.agents.agent_factory.AIProjectClient"
+ ) as mock_ai_project_client:
+ with patch("backend.agents.agent_factory.DefaultAzureCredentialSync"):
+ mock_config.CALL_TRANSCRIPT_SYSTEM_PROMPT = "Test instructions"
+ mock_config.AI_PROJECT_ENDPOINT = "https://test.endpoint.com"
+ mock_config.AZURE_OPENAI_MODEL = "test-model"
+
+ mock_project_client_instance = MagicMock()
+ mock_ai_project_client.return_value = mock_project_client_instance
+ mock_agent = MagicMock()
+ mock_project_client_instance.agents.create_agent.return_value = (
+ mock_agent
+ )
+
+ # Act
+ instance1 = await AgentFactory.get_search_agent()
+ instance2 = await AgentFactory.get_search_agent()
+
+ # Assert
+ assert instance1 is instance2
+
+ @pytest.mark.asyncio
+ async def test_delete_all_agent_instance_when_none_exists(self, reset_singleton):
+ """Test that delete_all_agent_instance handles when no agents exist."""
+ # Arrange
+ AgentFactory._wealth_advisor_agent = None
+ AgentFactory._search_agent = None
+
+ # Act
+ await AgentFactory.delete_all_agent_instance()
+
+ # Assert
+ assert AgentFactory._wealth_advisor_agent is None
+ assert AgentFactory._search_agent is None
+
+ @pytest.mark.asyncio
+ async def test_delete_all_agent_instance_removes_existing_agents(
+ self, reset_singleton
+ ):
+ """Test that delete_all_agent_instance properly removes existing agents."""
+ # Arrange
+ mock_wealth_advisor_agent = AsyncMock()
+ mock_wealth_advisor_agent.client = AsyncMock()
+ mock_wealth_advisor_agent.id = "test-wealth-advisor-id"
+ AgentFactory._wealth_advisor_agent = mock_wealth_advisor_agent
+
+ mock_search_client = MagicMock()
+ mock_search_agent = MagicMock()
+ mock_search_agent.id = "test-search-agent-id"
+ AgentFactory._search_agent = {
+ "agent": mock_search_agent,
+ "client": mock_search_client,
+ }
+
+ # Act
+ await AgentFactory.delete_all_agent_instance()
+
+ # Assert
+ assert AgentFactory._wealth_advisor_agent is None
+ assert AgentFactory._search_agent is None
+ mock_wealth_advisor_agent.client.agents.delete_agent.assert_called_once_with(
+ "test-wealth-advisor-id"
+ )
+ mock_search_client.agents.delete_agent.assert_called_once_with(
+ "test-search-agent-id"
+ )
+ mock_search_client.close.assert_called_once()
+
+ @pytest.mark.asyncio
+ async def test_delete_all_agent_instance_handles_only_wealth_advisor(
+ self, reset_singleton
+ ):
+ """Test that delete_all_agent_instance handles when only wealth advisor exists."""
# Arrange
- AgentFactory._instance = None
+ mock_wealth_advisor_agent = AsyncMock()
+ mock_wealth_advisor_agent.client = AsyncMock()
+ mock_wealth_advisor_agent.id = "test-wealth-advisor-id"
+ AgentFactory._wealth_advisor_agent = mock_wealth_advisor_agent
+ AgentFactory._search_agent = None
# Act
- await AgentFactory.delete_instance()
+ await AgentFactory.delete_all_agent_instance()
# Assert
- assert AgentFactory._instance is None
+ assert AgentFactory._wealth_advisor_agent is None
+ assert AgentFactory._search_agent is None
+ mock_wealth_advisor_agent.client.agents.delete_agent.assert_called_once_with(
+ "test-wealth-advisor-id"
+ )
@pytest.mark.asyncio
- async def test_delete_instance_removes_existing_agent(self, reset_singleton):
- """Test that delete_instance properly removes an existing agent."""
+ async def test_delete_all_agent_instance_handles_only_search_agent(
+ self, reset_singleton
+ ):
+ """Test that delete_all_agent_instance handles when only search agent exists."""
# Arrange
- mock_agent = AsyncMock()
- mock_agent.client = AsyncMock()
- mock_agent.id = "test-agent-id"
- AgentFactory._instance = mock_agent
+ mock_search_client = MagicMock()
+ mock_search_agent = MagicMock()
+ mock_search_agent.id = "test-search-agent-id"
+ AgentFactory._wealth_advisor_agent = None
+ AgentFactory._search_agent = {
+ "agent": mock_search_agent,
+ "client": mock_search_client,
+ }
# Act
- await AgentFactory.delete_instance()
+ await AgentFactory.delete_all_agent_instance()
# Assert
- assert AgentFactory._instance is None
- mock_agent.client.agents.delete_agent.assert_called_once_with(mock_agent.id)
+ assert AgentFactory._wealth_advisor_agent is None
+ assert AgentFactory._search_agent is None
+ mock_search_client.agents.delete_agent.assert_called_once_with(
+ "test-search-agent-id"
+ )
+ mock_search_client.close.assert_called_once()
diff --git a/src/App/tests/backend/plugins/test_chat_with_data_plugin.py b/src/App/tests/backend/plugins/test_chat_with_data_plugin.py
index d38a2c320..826cf4c5f 100644
--- a/src/App/tests/backend/plugins/test_chat_with_data_plugin.py
+++ b/src/App/tests/backend/plugins/test_chat_with_data_plugin.py
@@ -1,5 +1,7 @@
from unittest.mock import MagicMock, patch
+import pytest
+
from backend.plugins.chat_with_data_plugin import ChatWithDataPlugin
@@ -10,8 +12,9 @@ def setup_method(self):
"""Setup method to initialize plugin instance for each test."""
self.plugin = ChatWithDataPlugin()
+ @pytest.mark.asyncio
@patch.object(ChatWithDataPlugin, "get_openai_client")
- def test_greeting_returns_response(self, mock_get_openai_client):
+ async def test_greeting_returns_response(self, mock_get_openai_client):
"""Test that greeting method calls OpenAI and returns response."""
# Setup mock
mock_client = MagicMock()
@@ -24,7 +27,7 @@ def test_greeting_returns_response(self, mock_get_openai_client):
)
mock_client.chat.completions.create.return_value = mock_completion
- result = self.plugin.greeting("Hello")
+ result = await self.plugin.greeting("Hello")
assert result == "Hello! I'm your Wealth Assistant. How can I help you today?"
mock_client.chat.completions.create.assert_called_once()
@@ -97,9 +100,10 @@ def test_get_project_openai_client_success(
api_version="2025-04-01-preview"
)
+ @pytest.mark.asyncio
@patch("backend.plugins.chat_with_data_plugin.get_connection")
@patch.object(ChatWithDataPlugin, "get_openai_client")
- def test_get_sql_response_success(
+ async def test_get_sql_response_success(
self, mock_get_openai_client, mock_get_connection
):
"""Test successful SQL response generation with AAD authentication."""
@@ -122,7 +126,7 @@ def test_get_sql_response_success(
mock_connection.cursor.return_value = mock_cursor
mock_get_connection.return_value = mock_connection
- result = self.plugin.get_SQL_Response("Find client details", "client123")
+ result = await self.plugin.get_SQL_Response("Find client details", "client123")
# Verify the result
assert "John Doe" in result
@@ -138,9 +142,10 @@ def test_get_sql_response_success(
mock_cursor.fetchall.assert_called_once()
mock_connection.close.assert_called_once()
+ @pytest.mark.asyncio
@patch("backend.plugins.chat_with_data_plugin.get_connection")
@patch.object(ChatWithDataPlugin, "get_openai_client")
- def test_get_sql_response_database_error(
+ async def test_get_sql_response_database_error(
self, mock_get_openai_client, mock_get_connection
):
"""Test SQL response when database connection fails."""
@@ -155,13 +160,14 @@ def test_get_sql_response_database_error(
# Simulate database connection error
mock_get_connection.side_effect = Exception("Database connection failed")
- result = self.plugin.get_SQL_Response("Get all clients", "client123")
+ result = await self.plugin.get_SQL_Response("Get all clients", "client123")
assert "Error retrieving data from SQL" in result
assert "Database connection failed" in result
+ @pytest.mark.asyncio
@patch.object(ChatWithDataPlugin, "get_openai_client")
- def test_get_sql_response_openai_error(self, mock_get_openai_client):
+ async def test_get_sql_response_openai_error(self, mock_get_openai_client):
"""Test SQL response when OpenAI call fails."""
mock_client = MagicMock()
mock_get_openai_client.return_value = mock_client
@@ -169,27 +175,54 @@ def test_get_sql_response_openai_error(self, mock_get_openai_client):
# Simulate OpenAI error
mock_client.chat.completions.create.side_effect = Exception("OpenAI API error")
- result = self.plugin.get_SQL_Response("Get client data", "client123")
+ result = await self.plugin.get_SQL_Response("Get client data", "client123")
assert "Error retrieving data from SQL" in result
assert "OpenAI API error" in result
- @patch.object(ChatWithDataPlugin, "get_openai_client")
- def test_get_answers_from_calltranscripts_success(self, mock_get_openai_client):
- """Test successful retrieval of answers from call transcripts using AAD authentication."""
- # Setup mocks
- mock_client = MagicMock()
- mock_get_openai_client.return_value = mock_client
-
- # Mock OpenAI response (this method uses extra_body with data_sources)
- mock_completion = MagicMock()
- mock_completion.choices = [MagicMock()]
- mock_completion.choices[0].message.content = (
- "Based on call transcripts, the customer discussed investment options and risk tolerance."
+ @pytest.mark.asyncio
+ @patch("backend.agents.agent_factory.AgentFactory.get_search_agent")
+ async def test_get_answers_from_calltranscripts_success(
+ self, mock_get_search_agent
+ ):
+ """Test successful retrieval of answers from call transcripts using AI Search Agent."""
+ # Setup mocks for agent factory
+ mock_agent = MagicMock()
+ mock_agent.id = "test-agent-id"
+
+ mock_project_client = MagicMock()
+ mock_get_search_agent.return_value = {
+ "agent": mock_agent,
+ "client": mock_project_client,
+ }
+
+ # Mock project index creation
+ mock_index = MagicMock()
+ mock_index.name = "project-index-test"
+ mock_index.version = "1"
+ mock_project_client.indexes.create_or_update.return_value = mock_index
+
+ # Mock agent update
+ mock_project_client.agents.update_agent.return_value = mock_agent
+
+ # Mock thread creation
+ mock_thread = MagicMock()
+ mock_thread.id = "test-thread-id"
+ mock_project_client.agents.threads.create.return_value = mock_thread
+
+ # Mock run creation and processing
+ mock_run = MagicMock()
+ mock_run.status = "completed"
+ mock_project_client.agents.runs.create_and_process.return_value = mock_run
+
+ # Mock message response
+ mock_message = MagicMock()
+ mock_message.text.value = "Based on call transcripts, the customer discussed investment options and risk tolerance."
+ mock_project_client.agents.messages.get_last_message_text_by_role.return_value = (
+ mock_message
)
- mock_client.chat.completions.create.return_value = mock_completion
- result = self.plugin.get_answers_from_calltranscripts(
+ result = await self.plugin.get_answers_from_calltranscripts(
"What did the customer discuss?", "client123"
)
@@ -197,80 +230,234 @@ def test_get_answers_from_calltranscripts_success(self, mock_get_openai_client):
assert "Based on call transcripts" in result
assert "investment options" in result
- # Verify OpenAI was called with data_sources for Azure Search
- mock_client.chat.completions.create.assert_called_once()
- call_args = mock_client.chat.completions.create.call_args
- assert "extra_body" in call_args[1]
- assert "data_sources" in call_args[1]["extra_body"]
+ # Verify agent factory was called
+ mock_get_search_agent.assert_called_once()
- # Verify the filter contains the client ID
- data_sources = call_args[1]["extra_body"]["data_sources"]
- assert len(data_sources) > 0
- assert "client_id eq 'client123'" in data_sources[0]["parameters"]["filter"]
+ # Verify project index was created/updated
+ mock_project_client.indexes.create_or_update.assert_called_once()
- @patch.object(ChatWithDataPlugin, "get_openai_client")
- def test_get_answers_from_calltranscripts_no_results(self, mock_get_openai_client):
- """Test call transcripts search with no results."""
- mock_client = MagicMock()
- mock_get_openai_client.return_value = mock_client
+ # Verify agent was updated with search tool
+ mock_project_client.agents.update_agent.assert_called_once()
- # Mock empty response
- mock_completion = MagicMock()
- mock_completion.choices = []
- mock_client.chat.completions.create.return_value = mock_completion
+ # Verify thread was created and deleted
+ mock_project_client.agents.threads.create.assert_called_once()
+ mock_project_client.agents.threads.delete.assert_called_once_with(
+ "test-thread-id"
+ )
+
+ # Verify message was created and run was processed
+ mock_project_client.agents.messages.create.assert_called_once()
+ mock_project_client.agents.runs.create_and_process.assert_called_once()
- result = self.plugin.get_answers_from_calltranscripts(
+ @pytest.mark.asyncio
+ @patch("backend.agents.agent_factory.AgentFactory.get_search_agent")
+ async def test_get_answers_from_calltranscripts_no_results(
+ self, mock_get_search_agent
+ ):
+ """Test call transcripts search with no results."""
+ # Setup mocks for agent factory
+ mock_agent = MagicMock()
+ mock_agent.id = "test-agent-id"
+
+ mock_project_client = MagicMock()
+ mock_get_search_agent.return_value = {
+ "agent": mock_agent,
+ "client": mock_project_client,
+ }
+
+ # Mock project index creation
+ mock_index = MagicMock()
+ mock_index.name = "project-index-test"
+ mock_index.version = "1"
+ mock_project_client.indexes.create_or_update.return_value = mock_index
+
+ # Mock agent update
+ mock_project_client.agents.update_agent.return_value = mock_agent
+
+ # Mock thread creation
+ mock_thread = MagicMock()
+ mock_thread.id = "test-thread-id"
+ mock_project_client.agents.threads.create.return_value = mock_thread
+
+ # Mock run creation and processing
+ mock_run = MagicMock()
+ mock_run.status = "completed"
+ mock_project_client.agents.runs.create_and_process.return_value = mock_run
+
+ # Mock empty message response
+ mock_project_client.agents.messages.get_last_message_text_by_role.return_value = (
+ None
+ )
+
+ result = await self.plugin.get_answers_from_calltranscripts(
"Nonexistent query", "client123"
)
assert "No data found for that client." in result
- @patch.object(ChatWithDataPlugin, "get_openai_client")
- def test_get_answers_from_calltranscripts_openai_error(
- self, mock_get_openai_client
+ @pytest.mark.asyncio
+ @patch("backend.agents.agent_factory.AgentFactory.get_search_agent")
+ async def test_get_answers_from_calltranscripts_openai_error(
+ self, mock_get_search_agent
):
- """Test call transcripts with OpenAI processing error."""
- mock_client = MagicMock()
- mock_get_openai_client.return_value = mock_client
+ """Test call transcripts with AI Search processing error."""
+ # Setup mocks for agent factory
+ mock_agent = MagicMock()
+ mock_agent.id = "test-agent-id"
+
+ mock_project_client = MagicMock()
+ mock_get_search_agent.return_value = {
+ "agent": mock_agent,
+ "client": mock_project_client,
+ }
+
+ # Mock project index creation
+ mock_index = MagicMock()
+ mock_index.name = "project-index-test"
+ mock_index.version = "1"
+ mock_project_client.indexes.create_or_update.return_value = mock_index
+
+ # Mock agent update
+ mock_project_client.agents.update_agent.return_value = mock_agent
+
+ # Mock thread creation
+ mock_thread = MagicMock()
+ mock_thread.id = "test-thread-id"
+ mock_project_client.agents.threads.create.return_value = mock_thread
+
+ # Simulate AI Search error
+ mock_project_client.agents.runs.create_and_process.side_effect = Exception(
+ "AI Search processing failed"
+ )
- # Simulate OpenAI error
- mock_client.chat.completions.create.side_effect = Exception(
- "OpenAI processing failed"
+ result = await self.plugin.get_answers_from_calltranscripts(
+ "Test query", "client123"
)
- result = self.plugin.get_answers_from_calltranscripts("Test query", "client123")
+ assert "Error retrieving data from call transcripts" in result
+
+ @pytest.mark.asyncio
+ @patch("backend.agents.agent_factory.AgentFactory.get_search_agent")
+ async def test_get_answers_from_calltranscripts_failed_run(
+ self, mock_get_search_agent
+ ):
+ """Test call transcripts with failed AI Search run."""
+ # Setup mocks for agent factory
+ mock_agent = MagicMock()
+ mock_agent.id = "test-agent-id"
+
+ mock_project_client = MagicMock()
+ mock_get_search_agent.return_value = {
+ "agent": mock_agent,
+ "client": mock_project_client,
+ }
+
+ # Mock project index creation
+ mock_index = MagicMock()
+ mock_index.name = "project-index-test"
+ mock_index.version = "1"
+ mock_project_client.indexes.create_or_update.return_value = mock_index
+
+ # Mock agent update
+ mock_project_client.agents.update_agent.return_value = mock_agent
+
+ # Mock thread creation
+ mock_thread = MagicMock()
+ mock_thread.id = "test-thread-id"
+ mock_project_client.agents.threads.create.return_value = mock_thread
+
+ # Mock failed run
+ mock_run = MagicMock()
+ mock_run.status = "failed"
+ mock_run.last_error = "AI Search run failed"
+ mock_project_client.agents.runs.create_and_process.return_value = mock_run
+
+ result = await self.plugin.get_answers_from_calltranscripts(
+ "Test query", "client123"
+ )
assert "Error retrieving data from call transcripts" in result
- assert "OpenAI processing failed" in result
- def test_get_sql_response_missing_client_id(self):
+ @pytest.mark.asyncio
+ @patch("backend.agents.agent_factory.AgentFactory.get_search_agent")
+ async def test_get_answers_from_calltranscripts_empty_response(
+ self, mock_get_search_agent
+ ):
+ """Test call transcripts with empty response text."""
+ # Setup mocks for agent factory
+ mock_agent = MagicMock()
+ mock_agent.id = "test-agent-id"
+
+ mock_project_client = MagicMock()
+ mock_get_search_agent.return_value = {
+ "agent": mock_agent,
+ "client": mock_project_client,
+ }
+
+ # Mock project index creation
+ mock_index = MagicMock()
+ mock_index.name = "project-index-test"
+ mock_index.version = "1"
+ mock_project_client.indexes.create_or_update.return_value = mock_index
+
+ # Mock agent update
+ mock_project_client.agents.update_agent.return_value = mock_agent
+
+ # Mock thread creation
+ mock_thread = MagicMock()
+ mock_thread.id = "test-thread-id"
+ mock_project_client.agents.threads.create.return_value = mock_thread
+
+ # Mock run creation and processing
+ mock_run = MagicMock()
+ mock_run.status = "completed"
+ mock_project_client.agents.runs.create_and_process.return_value = mock_run
+
+ # Mock message with empty response
+ mock_message = MagicMock()
+ mock_message.text.value = " " # Empty/whitespace response
+ mock_project_client.agents.messages.get_last_message_text_by_role.return_value = (
+ mock_message
+ )
+
+ result = await self.plugin.get_answers_from_calltranscripts(
+ "Test query", "client123"
+ )
+
+ assert "No data found for that client." in result
+
+ @pytest.mark.asyncio
+ async def test_get_sql_response_missing_client_id(self):
"""Test SQL response with missing ClientId."""
- result = self.plugin.get_SQL_Response("Test query", "")
+ result = await self.plugin.get_SQL_Response("Test query", "")
assert "Error: ClientId is required" in result
- result = self.plugin.get_SQL_Response("Test query", None)
+ result = await self.plugin.get_SQL_Response("Test query", None)
assert "Error: ClientId is required" in result
- def test_get_sql_response_missing_input(self):
+ @pytest.mark.asyncio
+ async def test_get_sql_response_missing_input(self):
"""Test SQL response with missing input query."""
- result = self.plugin.get_SQL_Response("", "client123")
+ result = await self.plugin.get_SQL_Response("", "client123")
assert "Error: Query input is required" in result
- result = self.plugin.get_SQL_Response(None, "client123")
+ result = await self.plugin.get_SQL_Response(None, "client123")
assert "Error: Query input is required" in result
- def test_get_answers_from_calltranscripts_missing_client_id(self):
+ @pytest.mark.asyncio
+ async def test_get_answers_from_calltranscripts_missing_client_id(self):
"""Test call transcripts search with missing ClientId."""
- result = self.plugin.get_answers_from_calltranscripts("Test query", "")
+ result = await self.plugin.get_answers_from_calltranscripts("Test query", "")
assert "Error: ClientId is required" in result
- result = self.plugin.get_answers_from_calltranscripts("Test query", None)
+ result = await self.plugin.get_answers_from_calltranscripts("Test query", None)
assert "Error: ClientId is required" in result
- def test_get_answers_from_calltranscripts_missing_question(self):
+ @pytest.mark.asyncio
+ async def test_get_answers_from_calltranscripts_missing_question(self):
"""Test call transcripts search with missing question."""
- result = self.plugin.get_answers_from_calltranscripts("", "client123")
+ result = await self.plugin.get_answers_from_calltranscripts("", "client123")
assert "Error: Question input is required" in result
- result = self.plugin.get_answers_from_calltranscripts(None, "client123")
+ result = await self.plugin.get_answers_from_calltranscripts(None, "client123")
assert "Error: Question input is required" in result
diff --git a/src/App/tests/backend/services/test_chat_service.py b/src/App/tests/backend/services/test_chat_service.py
index effa70c2b..70ce3dc10 100644
--- a/src/App/tests/backend/services/test_chat_service.py
+++ b/src/App/tests/backend/services/test_chat_service.py
@@ -31,9 +31,9 @@ async def mock_stream():
# Mock invoke_stream to return the async generator
mock_agent.invoke_stream = MagicMock(return_value=mock_stream())
- # Mock current_app.agent
+ # Mock current_app.wealth_advisor_agent
mock_current_app = MagicMock()
- mock_current_app.agent = mock_agent
+ mock_current_app.wealth_advisor_agent = mock_agent
# Mock config
mock_config = MagicMock()
@@ -79,7 +79,7 @@ async def test_stream_response_exception_handling(self):
mock_agent.invoke_stream.side_effect = Exception("Test exception")
mock_current_app = MagicMock()
- mock_current_app.agent = mock_agent
+ mock_current_app.wealth_advisor_agent = mock_agent
mock_config = MagicMock()
mock_config.STREAM_TEXT_SYSTEM_PROMPT = "Test prompt"
@@ -116,7 +116,7 @@ async def mock_stream():
mock_agent.invoke_stream = MagicMock(return_value=mock_stream())
mock_current_app = MagicMock()
- mock_current_app.agent = mock_agent
+ mock_current_app.wealth_advisor_agent = mock_agent
mock_config = MagicMock()
mock_config.STREAM_TEXT_SYSTEM_PROMPT = ""
@@ -162,7 +162,7 @@ async def mock_stream():
mock_agent.invoke_stream = MagicMock(return_value=mock_stream())
mock_current_app = MagicMock()
- mock_current_app.agent = mock_agent
+ mock_current_app.wealth_advisor_agent = mock_agent
mock_config = MagicMock()
mock_config.STREAM_TEXT_SYSTEM_PROMPT = "" # Empty, should use default
diff --git a/src/App/tests/test_app.py b/src/App/tests/test_app.py
index ffa747097..8828f9b67 100644
--- a/src/App/tests/test_app.py
+++ b/src/App/tests/test_app.py
@@ -3,8 +3,6 @@
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
-from quart import Response
-
from app import (
create_app,
delete_all_conversations,
@@ -13,6 +11,7 @@
init_openai_client,
stream_chat_request,
)
+from quart import Response
# Constants for testing
INVALID_API_VERSION = "2022-01-01"