diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 1fa56b85..1a773971 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,10 +1,11 @@ { "name": "azd-template", - "image": "mcr.microsoft.com/devcontainers/python:3.11-bullseye", + "image": "mcr.microsoft.com/devcontainers/python:3.12-bullseye", "forwardPorts": [50505], "features": { "ghcr.io/azure/azure-dev/azd:latest": {}, - "azure-cli": "latest" + "ghcr.io/devcontainers/features/azure-cli:latest": {}, + "ghcr.io/devcontainers/features/docker-in-docker:latest": {} }, "customizations": { "vscode": { @@ -16,7 +17,7 @@ ] } }, - "postStartCommand": "git pull origin main && echo 'Recommended: run setup script to choose region, models, and capacities:' && echo ' az login' && echo ' source infra/setup_azd_parameters.sh'", + "postStartCommand": "git pull origin main && azd init -e conv-agent", "remoteUser": "vscode", "hostRequirements": { "memory": "4gb" diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml index 3738fc97..c2eb74a7 100644 --- a/.github/workflows/azure-dev.yml +++ b/.github/workflows/azure-dev.yml @@ -24,5 +24,16 @@ jobs: AZURE_LOCATION: ${{ secrets.AZURE_LOCATION }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + IS_GITHUB_WORKFLOW_RUN: 'true' + SKIP_LANGUAGE_SETUP: 'true' + + AZD_PARAM_ROUTER_TYPE: ${{ vars.AZD_PARAM_ROUTER_TYPE }} + AZD_PARAM_GPT_MODEL_NAME: ${{ vars.AZD_PARAM_GPT_MODEL_NAME }} + AZD_PARAM_GPT_MODEL_DEPLOYMENT_TYPE: ${{ vars.AZD_PARAM_GPT_MODEL_DEPLOYMENT_TYPE }} + AZD_PARAM_GPT_MODEL_CAPACITY: ${{ vars.AZD_PARAM_GPT_MODEL_CAPACITY }} + AZD_PARAM_EMBEDDING_MODEL_NAME: ${{ vars.AZD_PARAM_EMBEDDING_MODEL_NAME }} + AZD_PARAM_EMBEDDING_MODEL_DEPLOYMENT_TYPE: ${{ vars.AZD_PARAM_EMBEDDING_MODEL_DEPLOYMENT_TYPE }} + AZD_PARAM_EMBEDDING_MODEL_CAPACITY: ${{ vars.AZD_PARAM_EMBEDDING_MODEL_CAPACITY }} + - name: print result - run: cat ${{ steps.validation.outputs.resultFile }} \ No newline at end of file + run: cat ${{ steps.validation.outputs.resultFile }} diff --git a/README.md b/README.md index f7a92412..b2620d04 100644 --- a/README.md +++ b/README.md @@ -81,12 +81,12 @@ This displays the "better together" story when using Azure AI Language with Azur **Sample Data:** This project includes sample data to create project dependencies. Sample data is in the context of a fictional outdoor product company: Contoso Outdoors. **Routing strategies:** -- 'TRIAGE_AGENT': Route to an intent routing agent that uses 'CLU' and 'CQA' as tools. +- `TRIAGE_AGENT`: Route to an intent routing agent that uses 'CLU' and 'CQA' as tools. - `FUNCTION_CALLING`: Route to either `CLU` or `CQA` runtime using AOAI GPT function-calling to decide. - `CLU`: Route to `CLU` runtime only. - `CQA`: Route to `CQA` runtime only. - `ORCHESTRATION`: Route to either `CQA` or `CLU` runtime using an Azure AI Language [Orchestration](https://learn.microsoft.com/en-us/azure/ai-services/language-service/orchestration-workflow/overview) project to decide. -- BYPASS: No routing. Only call fallback function. +- `BYPASS`: No routing. Only call fallback function. In any case, the fallback function is called if routing "failed". `CLU` route is considered "failed" is confidence threshold is not met or no intent is recognized. `CQA` route is considered "failed" if confidence threhsold is not met or no answer is found. `TRIAGE_AGENT`, `FUNCTION_CALLING` and `ORCHESTRATION` route depend on the return value of the runtime they call. @@ -104,7 +104,7 @@ QUICK DEPLOY To deploy this solution accelerator, ensure you have access to an [Azure subscription](https://azure.microsoft.com/free/) with the necessary permissions to create **resource groups and resources** as well as being able to create role assignments. Follow the steps in [Azure Account Set Up](./docs/azure_account_set_up.md). We recommend you have `Owner` permissions on the subscription you are deploying this template to (though more minimal permissions will suffice as well). ### Region Selection -The following regions should support all Azure dependencies (except possibly AOAI model support) for this template: +The following regions should support all Azure dependencies (except possibly `AOAI` model support) for this template: - `australiaeast` - `centralindia` - `eastus` @@ -123,6 +123,7 @@ For best latency performance, we recommend you choose a region close to your phy | **Setting** | **Description** | **Default value** | |------------|----------------| ------------| +| **Router Type** | Configure routing strategy | `TRIAGE_AGENT` | | **GPT Model Name** | `gpt-4o` or `gpt-4o-mini` | `gpt-4o-mini` | | **GPT Model Deployment Capacity** | Configure capacity for **GPT model deployment** | `100k` | | **GPT Deployment Type** | `GlobalStandard` or `Standard` | `GlobalStandard` | @@ -130,15 +131,10 @@ For best latency performance, we recommend you choose a region close to your phy | **Embedding Model Capacity** | Configure capacity for **embedding model deployment** | `100k` | | **Embedding Deployment Type** | `GlobalStandard` or `Standard` | `GlobalStandard` | -The models, deployment types, and capacities you choose may depend on the region you select. To help you in choosing what is available in your subscription, please run the helper script: -``` -az login -source infra/setup_azd_parameters.sh -``` -This will populate the above deployment settings based on your selections. The script will automatically be run when you deploy through GitHub Codespaces or VS Code Dev Containers. When you finally run `azd up`, ensure you are choosing the same region you selected here. +The models, deployment types, and capacities you choose may depend on the region you select. ### [Optional] Quota Recommendations -For demo/test purposes, we recommend model deployment capacities to be **20k tokens**. This small value ensures an adequate testing/demo experience, but is not meant for production workloads. +For demo/test purposes, we recommend model deployment capacities to be **100k tokens**. This value ensures an adequate testing/demo experience, but is not meant for production workloads. We also recommend selecting `GlobalStandard` (as opposed to `Standard`) model deployments when possible. > **We recommend increasing the capacity for optimal performance under large loads.** **⚠️ Warning:** **Insufficient quota can cause deployment errors.** Please ensure you have the recommended capacity or request for additional capacity before deploying this solution. @@ -159,8 +155,9 @@ You can run this solution using GitHub Codespaces. The button will open a web-ba 2. Accept the default values on the create Codespaces page. 3. Open a terminal window if it is not already open. -4. Follow the instructions in the helper script to populate deployment variables. -5. Continue with the [deploying steps](#deploying). +4. Continue with the [deploying steps](#deploying). + + > **Note:** due to Dev Container constraints, the `azd` environment name is set for you by default ("conv-agent"). To change this, update the `postStartCommand` in `devcontainer.json`. @@ -178,8 +175,9 @@ You can run this solution in VS Code Dev Containers, which will open the project 3. In the VS Code window that opens, once the project files show up (this may take several minutes), open a terminal window. -4. Follow the instructions in the helper script to populate deployment variables. -5. Continue with the [deploying steps](#deploying). +4. Continue with the [deploying steps](#deploying). + + > **Note:** due to Dev Container constraints, the `azd` environment name is set for you by default ("conv-agent"). To change this, update the `postStartCommand` in `devcontainer.json`. @@ -192,8 +190,11 @@ If you're not using one of the above options for opening the project, then you'l 1. Make sure the following tools are installed: - * `bash` + * `bash` (if on a Windows system, we recommend running this in Git Bash) + * [Python](https://www.python.org/downloads/) (3.12) + * [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/?view=azure-cli-latest) * [Azure Developer CLI (azd)](https://aka.ms/install-azd) + * [Docker](https://docs.docker.com/get-started/docker-overview/) 2. Download the project code: @@ -201,52 +202,46 @@ If you're not using one of the above options for opening the project, then you'l azd init -t Azure-Samples/Azure-Language-OpenAI-Conversational-Agent-Accelerator/ ``` **Note:** the above command should be run in a new folder of your choosing. You do not need to run `git clone` to download the project source code. `azd init` handles this for you. - -3. Open the project folder in your terminal or editor. -4. Run the helper script to populate deployment variables: -``` -az login -source infra/setup_azd_parameters.sh -``` +3. Provide an `azd` environment name (like "conv-agent"). +4. Open the project folder in your terminal or editor. 5. Continue with the [deploying steps](#deploying). ### Deploying -Once you've opened the project in [Codespaces](#github-codespaces) or in [Dev Containers](#vs-code-dev-containers) or [locally](#local-environment), you can deploy it to Azure following the following steps. - -To change the `azd` parameters from the default values, follow the steps [here](./docs/customizing_azd_parameters.md). - - -1. Login to Azure: +Once you've opened the project in [Codespaces](#github-codespaces) or in [Dev Containers](#vs-code-dev-containers) or [locally](#local-environment), you can deploy it to Azure with the following steps. We recommend running this in a Python virtual environment: +``` +python -m venv +source /bin/activate +``` +1. Login to Azure (ensure you select the account and subscription you are deploying resources to): ```shell + az login azd auth login ``` - 2. Provision and deploy all the resources: ```shell azd up ``` - -3. Provide an `azd` environment name (like "conv-agent") -4. Select a subscription from your Azure account, and select a location which has quota for all the resources. - * This deployment will take *10-15 minutes* to provision the resources in your account and set up the solution with sample data. - +3. Now, you will be prompted to select deployment parameters, such as which models to deploy and with what capacity. Follow the prompts to select your customized parameters. +4. Next, follow the `azd` prompts: + - Select the *same* subscription and region you selected earlier during `az login`. + - Either select an existing resource group, or create a new one. + > **Note:** If you selected a model region earlier, ensure that your resource group is in the same region. + - Provisioning resources will take *5-15 minutes*. > **Tip:** A link to view the deployment's detailed progress in the Azure Portal shows up in your terminal window. You can open this link to see the deployment progress and go to the resource group. - - * If you get an error or timeout with deployment, changing the location can help, as there may be availability constraints for the resources. - -5. Once the deployment has completed successfully, **wait a few minutes (~5) to let the app finish setting up dependencies**. Then, open the [Azure Portal](https://portal.azure.com/), go to the deployed resource group, find the Container Group resource (`cg-`) and get the app URL from `FQDN`. +5. Once provisioning is complete, a few setup scripts will run to create various project dependencies in your AI Foundry resource. +6. Lastly, source code will be deployed. Code in the `src` folder will be built into a Docker image, pushed to your container registry, and a container instance will be created. +7. Once the deployment has completed successfully, open the [Azure Portal](https://portal.azure.com/), go to the deployed resource group, find the container instance resource (`ci-`) and get the app URL from the `FQDN` property. -6. Test the app locally with the sample question: _What is your return policy?_. For more sample questions you can test in the application, see [Sample Questions](#sample-questions). +8. Test the app with the sample question: _What is your return policy?_ For more sample inputs, see [Sample Questions](#sample-questions). - 6a. If you are encountering issues viewing the web app, try waiting a few more minutes for the app to set up. If you still have issues, view the container logs of the Container Group resource in the Azure Portal. Make sure to include this information when opening any issues on the template. + 8a. If you are encountering issues viewing the web app, view the container logs of the container instance resource in the Azure Portal. Make sure to include this information when opening any issues on the template. -7. You can now delete the resources by running `azd down`, if you are done trying out the application. - +9. You can now delete the provisioned resources by running `azd down` (if you are done trying out the application). ### Additional Steps @@ -260,16 +255,16 @@ To change the `azd` parameters from the default values, follow the steps [here]( 2. **Updating Example Data** - If you wish to update the existing example project data, you can do so by updaing the files in `infra/data/`: + If you wish to update the existing example project data, you can do so by updaing the files in `infra/scripts/data/`: - update the intents, entities, and utterances in `clu_import.json` to modify the app's CLU project. - update the questions and answers in `cqa_import.json` to modify the app's CQA project. - update the files in `product_info.tar.gz` to modify the grounding data the app uses to populate a search index for `RAG`. - If you update these files, ensure that you reference new project/index names in `infra/resources/container_group.bicep`: - - update `param clu_project_name` if you updated CLU data. - - update `param cqa_project_name` if you updated CQA data. - - update `param orchestration_project_name` if you updated CLU or CQA data. - - update `param search_index_name` if you updated grounding data. + If you update these files, ensure that you reference new project/index names in `infra/main.bicep`: + - update `OUTPUT CLU_PROJECT_NAME` if you updated CLU data. + - update `OUTPUT CQA_PROJECT_NAME` if you updated CQA data. + - update `OUTPUT ORCHESTRATION_PROJECT_NAME` if you updated CLU or CQA data. + - update `OUTPUT SEARCH_INDEX_NAME` if you updated grounding data. When updating example data, ensure that it adheres to [RAI guidelines](#responsible-ai-transparency-faq). Run `azd up` again to update the web app. @@ -292,7 +287,6 @@ Please refer to [Transparency FAQ](./RAI_FAQ.md) for responsible AI transparency Pricing varies per region and usage, so it isn't possible to predict exact costs for your usage. The majority of the Azure resources used in this infrastructure are on usage-based pricing tiers. -However, Azure Container Registry has a fixed cost per registry per day. You can try the [Azure pricing calculator](https://azure.microsoft.com/en-us/pricing/calculator) for the resources: @@ -301,6 +295,8 @@ You can try the [Azure pricing calculator](https://azure.microsoft.com/en-us/pri * Azure OpenAI: S0 tier, defaults to gpt-4o-mini and text-embedding-ada-002 models. Pricing is based on token count. [Pricing](https://azure.microsoft.com/en-us/pricing/details/cognitive-services/openai-service/?msockid=3d25d5a7fe346936111ec024ff8e685c) * Azure Container Instances: Pay as you go. Container has default settings of 1 vCPU and 1 GB. [Pricing](https://azure.microsoft.com/en-us/pricing/details/container-instances/?msockid=3d25d5a7fe346936111ec024ff8e685c) * Azure AI Language: S tier. [Pricing](https://azure.microsoft.com/en-us/pricing/details/cognitive-services/language-service/?msockid=3d25d5a7fe346936111ec024ff8e685c) +* Azure AI Foundry Agent Service: S0 tier. [Pricing](https://azure.microsoft.com/en-us/pricing/details/azure-ai-agent-service/?cdn=disable) +* Azure Container Registry: Standard tier. [Pricing](https://azure.microsoft.com/en-gb/pricing/details/container-registry/?msockid=3d25d5a7fe346936111ec024ff8e685c) ⚠️ To avoid unnecessary costs, remember to take down your app if it's no longer in use, @@ -324,6 +320,9 @@ Supporting documentation: - [Azure AI Language](https://learn.microsoft.com/en-us/azure/ai-services/language-service/overview) - [CLU](https://learn.microsoft.com/en-us/azure/ai-services/language-service/conversational-language-understanding/overview) - [CQA](https://learn.microsoft.com/en-us/azure/ai-services/language-service/question-answering/overview) +- [Azure AI Foundry Agent Service](https://azure.microsoft.com/en-us/products/ai-agent-service?msockid=3d25d5a7fe346936111ec024ff8e685c) +- [Azure Storage](https://learn.microsoft.com/en-us/azure/storage/common/storage-account-overview) +- [Azure Container Registry](https://azure.microsoft.com/en-us/products/container-registry/?msockid=3d25d5a7fe346936111ec024ff8e685c) ## Disclaimers To the extent that the Software includes components or code used in or derived from Microsoft products or services, including without limitation Microsoft Azure Services (collectively, “Microsoft Products and Services”), you must also comply with the Product Terms applicable to such Microsoft Products and Services. You acknowledge and agree that the license governing the Software does not grant you a license or other right to use Microsoft Products and Services. Nothing in the license or this ReadMe file will serve to supersede, amend, terminate or modify any terms in the Product Terms for any Microsoft Products and Services. diff --git a/azure.yaml b/azure.yaml index b7925491..0f061481 100644 --- a/azure.yaml +++ b/azure.yaml @@ -3,28 +3,35 @@ name: azure-language-openai-conversational-agent metadata: template: azure-language-openai-conversational-agent@1.0 -# workflows: -# up: -# steps: -# - azd: provision # azd deploy not needed, as we directly provision the app below -# services: -# app: -# project: src -# language: python -# host: containerapp # containerinstance not supported hooks: + preup: + run: | + chmod u+r+x ./infra/scripts/preup_customize_parameters.sh; ./infra/scripts/preup_customize_parameters.sh + shell: sh + continueOnError: false + interactive: true + preprovision: + run: | + bash infra/scripts/preprovision_validate_parameters.sh + shell: sh + continueOnError: false + interactive: true postprovision: - windows: - run: | - Write-Host "Web app URL: " - Write-Host "$env:WEB_APP_URL" -ForegroundColor Cyan - shell: pwsh - continueOnError: false - interactive: true - posix: - run: | - echo "Web app URL: " - echo $WEB_APP_URL - shell: sh - continueOnError: false - interactive: true + run: | + bash infra/scripts/postprovision_populate_env.sh + bash infra/scripts/postprovision_run_setup.sh + shell: sh + continueOnError: false + interactive: true + predeploy: + run: | + bash infra/scripts/predeploy_create_container.sh + shell: sh + continueOnError: false + interactive: true + postdown: + run: | + bash infra/scripts/postdown_purge_ai_foundry.sh + shell: sh + continueOnError: true + interactive: true diff --git a/infra/main.bicep b/infra/main.bicep index 15a9fbad..49ef7c28 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -1,24 +1,38 @@ // ========== main.bicep ========== // targetScope = 'resourceGroup' +// Fetch parameters: +param configured_parameters object = loadJsonContent('parameters.json') + +// Conv-Agent: +@allowed([ + 'BYPASS' + 'CLU' + 'CQA' + 'ORCHESTRATION' + 'FUNCTION_CALLING' + 'TRIAGE_AGENT' +]) +param router_type string = configured_parameters.router_type + // GPT model: @description('Name of GPT model to deploy.') @allowed([ 'gpt-4o-mini' 'gpt-4o' ]) -param gpt_model_name string - -@description('Capacity of GPT model deployment.') -@minValue(1) -param gpt_deployment_capacity int +param gpt_model_name string = configured_parameters.gpt_model_name @description('GPT model deployment type.') @allowed([ 'Standard' 'GlobalStandard' ]) -param gpt_deployment_type string +param gpt_model_deployment_type string = configured_parameters.gpt_model_deployment_type + +@description('Capacity of GPT model deployment.') +@minValue(1) +param gpt_model_capacity int = int(configured_parameters.gpt_model_capacity) // Embedding model: @description('Name of Embedding model to deploy.') @@ -26,18 +40,18 @@ param gpt_deployment_type string 'text-embedding-ada-002' 'text-embedding-3-small' ]) -param embedding_model_name string - -@description('Capacity of embedding model deployment.') -@minValue(1) -param embedding_deployment_capacity int +param embedding_model_name string = configured_parameters.embedding_model_name @description('Embedding model deployment type.') @allowed([ 'Standard' 'GlobalStandard' ]) -param embedding_deployment_type string +param embedding_model_deployment_type string = configured_parameters.embedding_model_deployment_type + +@description('Capacity of embedding model deployment.') +@minValue(1) +param embedding_model_capacity int = int(configured_parameters.embedding_model_capacity) // Variables: var suffix = uniqueString(subscription().id, resourceGroup().id, resourceGroup().location) @@ -50,6 +64,13 @@ module managed_identity 'resources/managed_identity.bicep' = { } } +module container_registry 'resources/container_registry.bicep' = { + name: 'deploy_container_registry' + params: { + suffix: suffix + } +} + module storage_account 'resources/storage_account.bicep' = { name: 'deploy_storage_account' params: { @@ -71,11 +92,11 @@ module ai_foundry 'resources/ai_foundry.bicep' = { managed_identity_name: managed_identity.outputs.name search_service_name: search_service.outputs.name gpt_model_name: gpt_model_name - gpt_deployment_capacity: gpt_deployment_capacity - gpt_deployment_type: gpt_deployment_type + gpt_model_deployment_type: gpt_model_deployment_type + gpt_model_capacity: gpt_model_capacity embedding_model_name: embedding_model_name - embedding_deployment_capacity: embedding_deployment_capacity - embedding_deployment_type: embedding_deployment_type + embedding_model_deployment_type: embedding_model_deployment_type + embedding_model_capacity: embedding_model_capacity } } @@ -83,30 +104,70 @@ module role_assignments 'resources/role_assignments.bicep' = { name: 'create_role_assignments' params: { managed_identity_name: managed_identity.outputs.name + container_registry_name: container_registry.outputs.name ai_foundry_name: ai_foundry.outputs.name search_service_name: search_service.outputs.name storage_account_name: storage_account.outputs.name } } -//----------- Deploy App -----------// -module container_instance 'resources/container_instance.bicep' = { - name: 'deploy_container_group' - params: { - suffix: suffix - agents_project_endpoint: ai_foundry.outputs.agents_project_endpoint - aoai_deployment: ai_foundry.outputs.gpt_deployment_name - aoai_endpoint: ai_foundry.outputs.openai_endpoint - language_endpoint: ai_foundry.outputs.language_endpoint - managed_identity_name: managed_identity.outputs.name - search_endpoint: search_service.outputs.endpoint - blob_container_name: storage_account.outputs.blob_container_name - embedding_deployment_name: ai_foundry.outputs.embedding_deployment_name - embedding_model_dimensions: ai_foundry.outputs.embedding_model_dimensions - embedding_model_name: ai_foundry.outputs.embedding_model_name - storage_account_connection_string: storage_account.outputs.connection_string - storage_account_name: storage_account.outputs.name - } -} +//----------- Outputs -----------// + +// Resource Group: +output RG_SUBSCRIPTION_ID string = subscription().subscriptionId +output RG_LOCATION string = resourceGroup().location +output RG_NAME string = resourceGroup().name +output RG_SUFFIX string = suffix + +// Managed Identity: +output MI_ID string = managed_identity.outputs.id +output MI_CLIENT_ID string = managed_identity.outputs.client_id + +// Language: +output LANGUAGE_ENDPOINT string = ai_foundry.outputs.language_endpoint +output CLU_PROJECT_NAME string = 'conv-agent-clu' +output CLU_MODEL_NAME string = 'clu-m1' +output CLU_DEPLOYMENT_NAME string = 'clu-m1-d1' +output CLU_CONFIDENCE_THRESHOLD string = '0.5' +output CLU_API_VERSION string = '2023-04-01' +output CQA_PROJECT_NAME string = 'conv-agent-cqa' +output CQA_DEPLOYMENT_NAME string = 'production' +output CQA_CONFIDENCE_THRESHOLD string = '0.5' +output CQA_API_VERSION string = '2023-04-01' +output ORCHESTRATION_PROJECT_NAME string = 'conv-agent-orch' +output ORCHESTRATION_MODEL_NAME string = 'orch-m1' +output ORCHESTRATION_DEPLOYMENT_NAME string = 'orch-m1-d1' +output ORCHESTRATION_CONFIDENCE_THRESHOLD string = '0.5' +output PII_ENABLED string = 'true' +output PII_CATEGORIES string = 'organization,person' +output PII_CONFIDENCE_THRESHOLD string = '0.5' + +// AI Foundry: +output AI_FOUNDRY_NAME string = ai_foundry.outputs.name + +// AOAI: +output AOAI_ENDPOINT string = ai_foundry.outputs.openai_endpoint +output AOAI_DEPLOYMENT string = ai_foundry.outputs.gpt_deployment_name +output EMBEDDING_DEPLOYMENT_NAME string = ai_foundry.outputs.embedding_deployment_name +output EMBEDDING_MODEL_NAME string = ai_foundry.outputs.embedding_model_name +output EMBEDDING_MODEL_DIMENSIONS string = string(ai_foundry.outputs.embedding_model_dimensions) + +// Agents: +output AGENTS_PROJECT_ENDPOINT string = ai_foundry.outputs.agents_project_endpoint +output MAX_AGENT_RETRY string = '5' +output DELETE_OLD_AGENTS string = 'true' + +// Search: +output SEARCH_ENDPOINT string = search_service.outputs.endpoint +output SEARCH_INDEX_NAME string = 'conv-agent-manuals-idx' + +// Storage: +output STORAGE_ACCOUNT_NAME string = storage_account.outputs.name +output STORAGE_ACCOUNT_CONNECTION_STRING string = storage_account.outputs.connection_string +output BLOB_CONTAINER_NAME string = storage_account.outputs.blob_container_name + +// ACR: +output ACR_NAME string = container_registry.outputs.name -output WEB_APP_URL string = container_instance.outputs.fqdn +// Conv-Agent: +output ROUTER_TYPE string = router_type diff --git a/infra/main.bicepparam b/infra/main.bicepparam deleted file mode 100644 index 3855e0b6..00000000 --- a/infra/main.bicepparam +++ /dev/null @@ -1,9 +0,0 @@ -using 'main.bicep' - -param gpt_model_name = readEnvironmentVariable('AZURE_ENV_GPT_MODEL_NAME', 'gpt-4o-mini') -param gpt_deployment_capacity = int(readEnvironmentVariable('AZURE_ENV_GPT_MODEL_CAPACITY', '100')) -param gpt_deployment_type = readEnvironmentVariable('AZURE_ENV_GPT_MODEL_DEPLOYMENT_TYPE', 'GlobalStandard') - -param embedding_model_name = readEnvironmentVariable('AZURE_ENV_EMBEDDING_MODEL_NAME', 'text-embedding-ada-002') -param embedding_deployment_capacity = int(readEnvironmentVariable('AZURE_ENV_EMBEDDING_MODEL_CAPACITY', '100')) -param embedding_deployment_type = readEnvironmentVariable('AZURE_ENV_EMBEDDING_MODEL_DEPLOYMENT_TYPE', 'GlobalStandard') diff --git a/infra/main.json b/infra/main.json deleted file mode 100644 index 35c958be..00000000 --- a/infra/main.json +++ /dev/null @@ -1,1180 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.35.1.17967", - "templateHash": "196729111068933069" - } - }, - "parameters": { - "gpt_model_name": { - "type": "string", - "allowedValues": [ - "gpt-4o-mini", - "gpt-4o" - ], - "metadata": { - "description": "Name of GPT model to deploy." - } - }, - "gpt_deployment_capacity": { - "type": "int", - "minValue": 1, - "metadata": { - "description": "Capacity of GPT model deployment." - } - }, - "gpt_deployment_type": { - "type": "string", - "allowedValues": [ - "Standard", - "GlobalStandard" - ], - "metadata": { - "description": "GPT model deployment type." - } - }, - "embedding_model_name": { - "type": "string", - "allowedValues": [ - "text-embedding-ada-002", - "text-embedding-3-small" - ], - "metadata": { - "description": "Name of Embedding model to deploy." - } - }, - "embedding_deployment_capacity": { - "type": "int", - "minValue": 1, - "metadata": { - "description": "Capacity of embedding model deployment." - } - }, - "embedding_deployment_type": { - "type": "string", - "allowedValues": [ - "Standard", - "GlobalStandard" - ], - "metadata": { - "description": "Embedding model deployment type." - } - } - }, - "variables": { - "suffix": "[uniqueString(subscription().id, resourceGroup().id, resourceGroup().location)]" - }, - "resources": [ - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "deploy_managed_identity", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "suffix": { - "value": "[variables('suffix')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.35.1.17967", - "templateHash": "3386278368474724985" - } - }, - "parameters": { - "suffix": { - "type": "string", - "metadata": { - "description": "Resource name suffix." - } - }, - "name": { - "type": "string", - "defaultValue": "[format('id-{0}', parameters('suffix'))]", - "metadata": { - "description": "Name of Managed Identity resource." - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Location for all resources." - } - } - }, - "resources": [ - { - "type": "Microsoft.ManagedIdentity/userAssignedIdentities", - "apiVersion": "2023-01-31", - "name": "[parameters('name')]", - "location": "[parameters('location')]" - } - ], - "outputs": { - "name": { - "type": "string", - "value": "[parameters('name')]" - } - } - } - } - }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "deploy_storage_account", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "suffix": { - "value": "[variables('suffix')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.35.1.17967", - "templateHash": "7745946228910384922" - } - }, - "parameters": { - "suffix": { - "type": "string", - "metadata": { - "description": "Resource name suffix." - } - }, - "name": { - "type": "string", - "defaultValue": "[format('st{0}', parameters('suffix'))]", - "metadata": { - "description": "Name of Storage Account resource." - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Location for all resources." - } - }, - "blob_container_name": { - "type": "string", - "defaultValue": "contoso-outdoors-manuals", - "metadata": { - "description": "Blob container name." - } - } - }, - "resources": [ - { - "type": "Microsoft.Storage/storageAccounts/blobServices/containers", - "apiVersion": "2023-05-01", - "name": "[format('{0}/{1}/{2}', parameters('name'), 'default', parameters('blob_container_name'))]", - "properties": { - "publicAccess": "None" - }, - "dependsOn": [ - "[resourceId('Microsoft.Storage/storageAccounts/blobServices', parameters('name'), 'default')]" - ] - }, - { - "type": "Microsoft.Storage/storageAccounts/blobServices", - "apiVersion": "2023-05-01", - "name": "[format('{0}/{1}', parameters('name'), 'default')]", - "dependsOn": [ - "[resourceId('Microsoft.Storage/storageAccounts', parameters('name'))]" - ] - }, - { - "type": "Microsoft.Storage/storageAccounts", - "apiVersion": "2023-05-01", - "name": "[parameters('name')]", - "location": "[parameters('location')]", - "kind": "StorageV2", - "properties": { - "allowSharedKeyAccess": false, - "allowBlobPublicAccess": false, - "publicNetworkAccess": "Enabled", - "networkAcls": { - "defaultAction": "Allow", - "bypass": "AzureServices" - } - }, - "sku": { - "name": "Standard_LRS" - } - } - ], - "outputs": { - "name": { - "type": "string", - "value": "[parameters('name')]" - }, - "connection_string": { - "type": "string", - "value": "[format('ResourceId={0};', resourceId('Microsoft.Storage/storageAccounts', parameters('name')))]" - }, - "blob_container_name": { - "type": "string", - "value": "[parameters('blob_container_name')]" - } - } - } - } - }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "deploy_search_service", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "suffix": { - "value": "[variables('suffix')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.35.1.17967", - "templateHash": "2757599015930864551" - } - }, - "parameters": { - "suffix": { - "type": "string", - "metadata": { - "description": "Resource name suffix." - } - }, - "name": { - "type": "string", - "defaultValue": "[format('srch-{0}', parameters('suffix'))]", - "metadata": { - "description": "Name of AI Search resource." - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Location for all resources." - } - } - }, - "resources": [ - { - "type": "Microsoft.Search/searchServices", - "apiVersion": "2024-06-01-preview", - "name": "[parameters('name')]", - "location": "[parameters('location')]", - "identity": { - "type": "SystemAssigned" - }, - "properties": { - "disableLocalAuth": false, - "semanticSearch": "free", - "publicNetworkAccess": "enabled", - "networkRuleSet": { - "bypass": "AzureServices", - "ipRules": [] - }, - "authOptions": { - "aadOrApiKey": { - "aadAuthFailureMode": "http401WithBearerChallenge" - } - } - }, - "sku": { - "name": "basic" - } - } - ], - "outputs": { - "name": { - "type": "string", - "value": "[parameters('name')]" - }, - "endpoint": { - "type": "string", - "value": "[format('https://{0}.search.windows.net', parameters('name'))]" - } - } - } - } - }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "deploy_ai_foundry", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "suffix": { - "value": "[variables('suffix')]" - }, - "managed_identity_name": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'deploy_managed_identity'), '2022-09-01').outputs.name.value]" - }, - "search_service_name": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'deploy_search_service'), '2022-09-01').outputs.name.value]" - }, - "gpt_model_name": { - "value": "[parameters('gpt_model_name')]" - }, - "gpt_deployment_capacity": { - "value": "[parameters('gpt_deployment_capacity')]" - }, - "gpt_deployment_type": { - "value": "[parameters('gpt_deployment_type')]" - }, - "embedding_model_name": { - "value": "[parameters('embedding_model_name')]" - }, - "embedding_deployment_capacity": { - "value": "[parameters('embedding_deployment_capacity')]" - }, - "embedding_deployment_type": { - "value": "[parameters('embedding_deployment_type')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.35.1.17967", - "templateHash": "3093109981521093431" - } - }, - "parameters": { - "suffix": { - "type": "string", - "metadata": { - "description": "Resource name suffix." - } - }, - "name": { - "type": "string", - "defaultValue": "[format('aif-{0}', parameters('suffix'))]", - "metadata": { - "description": "Name of AI Foundry resource." - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Location for all resources." - } - }, - "agents_project_name": { - "type": "string", - "defaultValue": "[format('{0}-agents', parameters('name'))]", - "metadata": { - "description": "Agents AI Foundry project name." - } - }, - "gpt_model_name": { - "type": "string", - "metadata": { - "description": "Name of GPT model to deploy." - } - }, - "gpt_deployment_capacity": { - "type": "int", - "minValue": 1, - "metadata": { - "description": "Capacity of GPT model deployment." - } - }, - "gpt_deployment_type": { - "type": "string", - "allowedValues": [ - "Standard", - "GlobalStandard" - ] - }, - "embedding_model_name": { - "type": "string", - "metadata": { - "description": "Name of embedding model to deploy." - } - }, - "embedding_deployment_capacity": { - "type": "int", - "minValue": 1, - "metadata": { - "description": "Capacity of embedding model deployment." - } - }, - "embedding_model_dimensions": { - "type": "int", - "defaultValue": 1536, - "metadata": { - "description": "Model dimensions of embedding model to deploy." - } - }, - "embedding_deployment_type": { - "type": "string", - "allowedValues": [ - "Standard", - "GlobalStandard" - ] - }, - "search_service_name": { - "type": "string", - "metadata": { - "description": "Name of AI Search resource" - } - }, - "managed_identity_name": { - "type": "string", - "metadata": { - "description": "Name of managed identity to use for Container Apps." - } - } - }, - "variables": { - "language_endpoint_key": "Language" - }, - "resources": [ - { - "type": "Microsoft.CognitiveServices/accounts/projects", - "apiVersion": "2025-04-01-preview", - "name": "[format('{0}/{1}', parameters('name'), parameters('agents_project_name'))]", - "location": "[parameters('location')]", - "identity": { - "type": "UserAssigned", - "userAssignedIdentities": { - "[format('{0}', resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('managed_identity_name')))]": {} - } - }, - "properties": {}, - "dependsOn": [ - "[resourceId('Microsoft.CognitiveServices/accounts', parameters('name'))]" - ] - }, - { - "type": "Microsoft.CognitiveServices/accounts/deployments", - "apiVersion": "2025-04-01-preview", - "name": "[format('{0}/{1}', parameters('name'), parameters('gpt_model_name'))]", - "properties": { - "model": { - "format": "OpenAI", - "name": "[parameters('gpt_model_name')]" - } - }, - "sku": { - "name": "[parameters('gpt_deployment_type')]", - "capacity": "[parameters('gpt_deployment_capacity')]" - }, - "dependsOn": [ - "[resourceId('Microsoft.CognitiveServices/accounts', parameters('name'))]" - ] - }, - { - "type": "Microsoft.CognitiveServices/accounts/deployments", - "apiVersion": "2025-04-01-preview", - "name": "[format('{0}/{1}', parameters('name'), parameters('embedding_model_name'))]", - "properties": { - "model": { - "format": "OpenAI", - "name": "[parameters('embedding_model_name')]" - } - }, - "sku": { - "name": "[parameters('embedding_deployment_type')]", - "capacity": "[parameters('embedding_deployment_capacity')]" - }, - "dependsOn": [ - "[resourceId('Microsoft.CognitiveServices/accounts', parameters('name'))]", - "[resourceId('Microsoft.CognitiveServices/accounts/deployments', parameters('name'), parameters('gpt_model_name'))]" - ] - }, - { - "type": "Microsoft.CognitiveServices/accounts", - "apiVersion": "2025-04-01-preview", - "name": "[parameters('name')]", - "location": "[parameters('location')]", - "kind": "AIServices", - "identity": { - "type": "SystemAssigned" - }, - "properties": { - "allowProjectManagement": true, - "disableLocalAuth": true, - "customSubDomainName": "[parameters('name')]", - "publicNetworkAccess": "Enabled", - "networkAcls": { - "defaultAction": "Allow" - }, - "apiProperties": { - "qnaAzureSearchEndpointId": "[resourceId('Microsoft.Search/searchServices', parameters('search_service_name'))]", - "qnaAzureSearchEndpointKey": "[listAdminKeys(resourceId('Microsoft.Search/searchServices', parameters('search_service_name')), '2023-11-01').primaryKey]" - } - }, - "sku": { - "name": "S0" - } - } - ], - "outputs": { - "name": { - "type": "string", - "value": "[parameters('name')]" - }, - "agents_project_endpoint": { - "type": "string", - "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts/projects', parameters('name'), parameters('agents_project_name')), '2025-04-01-preview').endpoints['AI Foundry API']]" - }, - "language_endpoint": { - "type": "string", - "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('name')), '2025-04-01-preview').endpoints[variables('language_endpoint_key')]]" - }, - "openai_endpoint": { - "type": "string", - "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('name')), '2025-04-01-preview').endpoints['OpenAI Language Model Instance API']]" - }, - "gpt_deployment_name": { - "type": "string", - "value": "[parameters('gpt_model_name')]" - }, - "embedding_deployment_name": { - "type": "string", - "value": "[parameters('embedding_model_name')]" - }, - "embedding_model_name": { - "type": "string", - "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts/deployments', parameters('name'), parameters('embedding_model_name')), '2025-04-01-preview').model.name]" - }, - "embedding_model_dimensions": { - "type": "int", - "value": "[parameters('embedding_model_dimensions')]" - } - } - } - }, - "dependsOn": [ - "[resourceId('Microsoft.Resources/deployments', 'deploy_managed_identity')]", - "[resourceId('Microsoft.Resources/deployments', 'deploy_search_service')]" - ] - }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "create_role_assignments", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "managed_identity_name": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'deploy_managed_identity'), '2022-09-01').outputs.name.value]" - }, - "ai_foundry_name": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'deploy_ai_foundry'), '2022-09-01').outputs.name.value]" - }, - "search_service_name": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'deploy_search_service'), '2022-09-01').outputs.name.value]" - }, - "storage_account_name": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'deploy_storage_account'), '2022-09-01').outputs.name.value]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.35.1.17967", - "templateHash": "9127949922929976192" - } - }, - "parameters": { - "managed_identity_name": { - "type": "string", - "metadata": { - "description": "Name of Managed Identity resource." - } - }, - "storage_account_name": { - "type": "string", - "metadata": { - "description": "Name of Storage Account resource." - } - }, - "ai_foundry_name": { - "type": "string", - "metadata": { - "description": "Name of AI Foundry resource." - } - }, - "search_service_name": { - "type": "string", - "metadata": { - "description": "Name of Search Service resource." - } - } - }, - "resources": [ - { - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('storage_account_name'))]", - "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('storage_account_name')), resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('managed_identity_name')), resourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'))]", - "properties": { - "principalId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('managed_identity_name')), '2023-01-31').principalId]", - "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')]", - "principalType": "ServicePrincipal" - } - }, - { - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('storage_account_name'))]", - "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('storage_account_name')), resourceId('Microsoft.Search/searchServices', parameters('search_service_name')), resourceId('Microsoft.Authorization/roleDefinitions', '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1'))]", - "properties": { - "principalId": "[reference(resourceId('Microsoft.Search/searchServices', parameters('search_service_name')), '2023-11-01', 'full').identity.principalId]", - "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1')]", - "principalType": "ServicePrincipal" - } - }, - { - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Search/searchServices/{0}', parameters('search_service_name'))]", - "name": "[guid(resourceId('Microsoft.Search/searchServices', parameters('search_service_name')), resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('managed_identity_name')), resourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7'))]", - "properties": { - "principalId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('managed_identity_name')), '2023-01-31').principalId]", - "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7')]", - "principalType": "ServicePrincipal" - } - }, - { - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Search/searchServices/{0}', parameters('search_service_name'))]", - "name": "[guid(resourceId('Microsoft.Search/searchServices', parameters('search_service_name')), resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('managed_identity_name')), resourceId('Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0'))]", - "properties": { - "principalId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('managed_identity_name')), '2023-01-31').principalId]", - "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0')]", - "principalType": "ServicePrincipal" - } - }, - { - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Search/searchServices/{0}', parameters('search_service_name'))]", - "name": "[guid(resourceId('Microsoft.Search/searchServices', parameters('search_service_name')), resourceId('Microsoft.CognitiveServices/accounts', parameters('ai_foundry_name')), resourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7'))]", - "properties": { - "principalId": "[reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('ai_foundry_name')), '2025-04-01-preview', 'full').identity.principalId]", - "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7')]", - "principalType": "ServicePrincipal" - } - }, - { - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('ai_foundry_name'))]", - "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('ai_foundry_name')), resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('managed_identity_name')), resourceId('Microsoft.Authorization/roleDefinitions', 'a001fd3d-188f-4b5d-821b-7da978bf7442'))]", - "properties": { - "principalId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('managed_identity_name')), '2023-01-31').principalId]", - "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', 'a001fd3d-188f-4b5d-821b-7da978bf7442')]", - "principalType": "ServicePrincipal" - } - }, - { - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('ai_foundry_name'))]", - "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('ai_foundry_name')), resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('managed_identity_name')), resourceId('Microsoft.Authorization/roleDefinitions', 'f07febfe-79bc-46b1-8b37-790e26e6e498'))]", - "properties": { - "principalId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('managed_identity_name')), '2023-01-31').principalId]", - "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', 'f07febfe-79bc-46b1-8b37-790e26e6e498')]", - "principalType": "ServicePrincipal" - } - }, - { - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('ai_foundry_name'))]", - "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('ai_foundry_name')), resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('managed_identity_name')), resourceId('Microsoft.Authorization/roleDefinitions', 'e47c6f54-e4a2-4754-9501-8e0985b135e1'))]", - "properties": { - "principalId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('managed_identity_name')), '2023-01-31').principalId]", - "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', 'e47c6f54-e4a2-4754-9501-8e0985b135e1')]", - "principalType": "ServicePrincipal" - } - }, - { - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('ai_foundry_name'))]", - "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('ai_foundry_name')), resourceId('Microsoft.Search/searchServices', parameters('search_service_name')), resourceId('Microsoft.Authorization/roleDefinitions', 'a001fd3d-188f-4b5d-821b-7da978bf7442'))]", - "properties": { - "principalId": "[reference(resourceId('Microsoft.Search/searchServices', parameters('search_service_name')), '2023-11-01', 'full').identity.principalId]", - "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', 'a001fd3d-188f-4b5d-821b-7da978bf7442')]", - "principalType": "ServicePrincipal" - } - } - ], - "outputs": { - "name": { - "type": "string", - "value": "[parameters('managed_identity_name')]" - } - } - } - }, - "dependsOn": [ - "[resourceId('Microsoft.Resources/deployments', 'deploy_ai_foundry')]", - "[resourceId('Microsoft.Resources/deployments', 'deploy_managed_identity')]", - "[resourceId('Microsoft.Resources/deployments', 'deploy_search_service')]", - "[resourceId('Microsoft.Resources/deployments', 'deploy_storage_account')]" - ] - }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "deploy_container_group", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "suffix": { - "value": "[variables('suffix')]" - }, - "agents_project_endpoint": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'deploy_ai_foundry'), '2022-09-01').outputs.agents_project_endpoint.value]" - }, - "aoai_deployment": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'deploy_ai_foundry'), '2022-09-01').outputs.gpt_deployment_name.value]" - }, - "aoai_endpoint": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'deploy_ai_foundry'), '2022-09-01').outputs.openai_endpoint.value]" - }, - "language_endpoint": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'deploy_ai_foundry'), '2022-09-01').outputs.language_endpoint.value]" - }, - "managed_identity_name": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'deploy_managed_identity'), '2022-09-01').outputs.name.value]" - }, - "search_endpoint": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'deploy_search_service'), '2022-09-01').outputs.endpoint.value]" - }, - "blob_container_name": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'deploy_storage_account'), '2022-09-01').outputs.blob_container_name.value]" - }, - "embedding_deployment_name": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'deploy_ai_foundry'), '2022-09-01').outputs.embedding_deployment_name.value]" - }, - "embedding_model_dimensions": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'deploy_ai_foundry'), '2022-09-01').outputs.embedding_model_dimensions.value]" - }, - "embedding_model_name": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'deploy_ai_foundry'), '2022-09-01').outputs.embedding_model_name.value]" - }, - "storage_account_connection_string": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'deploy_storage_account'), '2022-09-01').outputs.connection_string.value]" - }, - "storage_account_name": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'deploy_storage_account'), '2022-09-01').outputs.name.value]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.35.1.17967", - "templateHash": "185017665825100238" - } - }, - "parameters": { - "suffix": { - "type": "string", - "metadata": { - "description": "Resource name suffix." - } - }, - "name": { - "type": "string", - "defaultValue": "[format('ci-{0}', parameters('suffix'))]", - "metadata": { - "description": "Name of Container Group resource." - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Location for all resources." - } - }, - "language_endpoint": { - "type": "string" - }, - "clu_project_name": { - "type": "string", - "defaultValue": "conv-assistant-clu" - }, - "clu_model_name": { - "type": "string", - "defaultValue": "clu-m1" - }, - "clu_deployment_name": { - "type": "string", - "defaultValue": "clu-m1-d1" - }, - "clu_confidence_threshold": { - "type": "string", - "defaultValue": "0.5" - }, - "cqa_project_name": { - "type": "string", - "defaultValue": "conv-assistant-cqa" - }, - "cqa_deployment_name": { - "type": "string", - "defaultValue": "production" - }, - "cqa_confidence_threshold": { - "type": "string", - "defaultValue": "0.5" - }, - "orchestration_project_name": { - "type": "string", - "defaultValue": "conv-assistant-orch" - }, - "orchestration_model_name": { - "type": "string", - "defaultValue": "orch-m1" - }, - "orchestration_deployment_name": { - "type": "string", - "defaultValue": "orch-m1-d1" - }, - "orchestration_confidence_threshold": { - "type": "string", - "defaultValue": "0.5" - }, - "pii_enabled": { - "type": "string", - "defaultValue": "true" - }, - "pii_categories": { - "type": "string", - "defaultValue": "organization,person" - }, - "pii_confidence_threshold": { - "type": "string", - "defaultValue": "0.5" - }, - "aoai_endpoint": { - "type": "string" - }, - "aoai_deployment": { - "type": "string" - }, - "embedding_deployment_name": { - "type": "string" - }, - "embedding_model_name": { - "type": "string" - }, - "embedding_model_dimensions": { - "type": "int" - }, - "storage_account_name": { - "type": "string" - }, - "storage_account_connection_string": { - "type": "string" - }, - "blob_container_name": { - "type": "string" - }, - "search_endpoint": { - "type": "string" - }, - "search_index_name": { - "type": "string", - "defaultValue": "conv-assistant-manuals-idx" - }, - "agents_project_endpoint": { - "type": "string" - }, - "router_type": { - "type": "string", - "defaultValue": "ORCHESTRATION", - "allowedValues": [ - "BYPASS", - "CLU", - "CQA", - "ORCHESTRATION", - "FUNCTION_CALLING" - ] - }, - "image": { - "type": "string", - "defaultValue": "mcr.microsoft.com/azure-cli" - }, - "port": { - "type": "int", - "defaultValue": 80 - }, - "repository": { - "type": "string", - "defaultValue": "https://github.com/Azure-Samples/Azure-Language-OpenAI-Conversational-Agent-Accelerator" - }, - "managed_identity_name": { - "type": "string", - "metadata": { - "description": "Name of managed identity to use for Container Apps." - } - } - }, - "resources": [ - { - "type": "Microsoft.ContainerInstance/containerGroups", - "apiVersion": "2024-10-01-preview", - "name": "[parameters('name')]", - "location": "[parameters('location')]", - "identity": { - "type": "UserAssigned", - "userAssignedIdentities": { - "[format('{0}', resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('managed_identity_name')))]": {} - } - }, - "properties": { - "restartPolicy": "Never", - "volumes": [ - { - "name": "repo", - "gitRepo": { - "directory": "repo", - "repository": "[parameters('repository')]" - } - } - ], - "osType": "Linux", - "ipAddress": { - "dnsNameLabel": "conv-agent-app", - "autoGeneratedDomainNameLabelScope": "Noreuse", - "type": "Public", - "ports": [ - { - "port": "[parameters('port')]", - "protocol": "TCP" - } - ] - }, - "containers": [ - { - "name": "conv-agent-app", - "properties": { - "image": "[parameters('image')]", - "resources": { - "requests": { - "cpu": 1, - "memoryInGB": 1 - } - }, - "volumeMounts": [ - { - "mountPath": "/mnt", - "name": "repo" - } - ], - "ports": [ - { - "port": "[parameters('port')]", - "protocol": "TCP" - } - ], - "command": [ - "/bin/bash", - "-c", - "chmod +x mnt/repo/infra/scripts/run_container_app.sh && bash mnt/repo/infra/scripts/run_container_app.sh" - ], - "environmentVariables": [ - { - "name": "AGENTS_PROJECT_ENDPOINT", - "value": "[parameters('agents_project_endpoint')]" - }, - { - "name": "USE_MI_AUTH", - "value": "true" - }, - { - "name": "MI_CLIENT_ID", - "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('managed_identity_name')), '2023-01-31').clientId]" - }, - { - "name": "AOAI_ENDPOINT", - "value": "[parameters('aoai_endpoint')]" - }, - { - "name": "AOAI_DEPLOYMENT", - "value": "[parameters('aoai_deployment')]" - }, - { - "name": "SEARCH_ENDPOINT", - "value": "[parameters('search_endpoint')]" - }, - { - "name": "SEARCH_INDEX_NAME", - "value": "[parameters('search_index_name')]" - }, - { - "name": "EMBEDDING_DEPLOYMENT_NAME", - "value": "[parameters('embedding_deployment_name')]" - }, - { - "name": "EMBEDDING_MODEL_NAME", - "value": "[parameters('embedding_model_name')]" - }, - { - "name": "EMBEDDING_MODEL_DIMENSIONS", - "value": "[string(parameters('embedding_model_dimensions'))]" - }, - { - "name": "STORAGE_ACCOUNT_NAME", - "value": "[parameters('storage_account_name')]" - }, - { - "name": "STORAGE_ACCOUNT_CONNECTION_STRING", - "value": "[parameters('storage_account_connection_string')]" - }, - { - "name": "BLOB_CONTAINER_NAME", - "value": "[parameters('blob_container_name')]" - }, - { - "name": "LANGUAGE_ENDPOINT", - "value": "[parameters('language_endpoint')]" - }, - { - "name": "CLU_PROJECT_NAME", - "value": "[parameters('clu_project_name')]" - }, - { - "name": "CLU_MODEL_NAME", - "value": "[parameters('clu_model_name')]" - }, - { - "name": "CLU_DEPLOYMENT_NAME", - "value": "[parameters('clu_deployment_name')]" - }, - { - "name": "CLU_CONFIDENCE_THRESHOLD", - "value": "[parameters('clu_confidence_threshold')]" - }, - { - "name": "CQA_PROJECT_NAME", - "value": "[parameters('cqa_project_name')]" - }, - { - "name": "CQA_DEPLOYMENT_NAME", - "value": "[parameters('cqa_deployment_name')]" - }, - { - "name": "CQA_CONFIDENCE_THRESHOLD", - "value": "[parameters('cqa_confidence_threshold')]" - }, - { - "name": "ORCHESTRATION_PROJECT_NAME", - "value": "[parameters('orchestration_project_name')]" - }, - { - "name": "ORCHESTRATION_MODEL_NAME", - "value": "[parameters('orchestration_model_name')]" - }, - { - "name": "ORCHESTRATION_DEPLOYMENT_NAME", - "value": "[parameters('orchestration_deployment_name')]" - }, - { - "name": "ORCHESTRATION_CONFIDENCE_THRESHOLD", - "value": "[parameters('orchestration_confidence_threshold')]" - }, - { - "name": "PII_ENABLED", - "value": "[parameters('pii_enabled')]" - }, - { - "name": "PII_CATEGORIES", - "value": "[parameters('pii_categories')]" - }, - { - "name": "PII_CONFIDENCE_THRESHOLD", - "value": "[parameters('pii_confidence_threshold')]" - }, - { - "name": "ROUTER_TYPE", - "value": "[parameters('router_type')]" - } - ] - } - } - ] - } - } - ], - "outputs": { - "fqdn": { - "type": "string", - "value": "[reference(resourceId('Microsoft.ContainerInstance/containerGroups', parameters('name')), '2024-10-01-preview').ipAddress.fqdn]" - } - } - } - }, - "dependsOn": [ - "[resourceId('Microsoft.Resources/deployments', 'deploy_ai_foundry')]", - "[resourceId('Microsoft.Resources/deployments', 'deploy_managed_identity')]", - "[resourceId('Microsoft.Resources/deployments', 'deploy_search_service')]", - "[resourceId('Microsoft.Resources/deployments', 'deploy_storage_account')]" - ] - } - ], - "outputs": { - "WEB_APP_URL": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'deploy_container_group'), '2022-09-01').outputs.fqdn.value]" - } - } -} \ No newline at end of file diff --git a/infra/parameters.json b/infra/parameters.json new file mode 100644 index 00000000..e6b634dc --- /dev/null +++ b/infra/parameters.json @@ -0,0 +1,10 @@ +{ + "router_type": "TRIAGE_AGENT", + "gpt_model_name": "gpt-4o-mini", + "gpt_model_deployment_type": "GlobalStandard", + "gpt_model_capacity": "100", + "embedding_model_name": "text-embedding-ada-002", + "embedding_model_deployment_type": "GlobalStandard", + "embedding_model_capacity": "100", + "model_region": "" +} diff --git a/infra/resources/ai_foundry.bicep b/infra/resources/ai_foundry.bicep index aaa20b28..d208e06c 100644 --- a/infra/resources/ai_foundry.bicep +++ b/infra/resources/ai_foundry.bicep @@ -11,36 +11,16 @@ param location string = resourceGroup().location param agents_project_name string = '${name}-agents' // GPT model: -@description('Name of GPT model to deploy.') param gpt_model_name string - -@description('Capacity of GPT model deployment.') -@minValue(1) -param gpt_deployment_capacity int - -@allowed([ - 'Standard' - 'GlobalStandard' -]) -param gpt_deployment_type string +param gpt_model_deployment_type string +param gpt_model_capacity int // Embedding model: -@description('Name of embedding model to deploy.') param embedding_model_name string - -@description('Capacity of embedding model deployment.') -@minValue(1) -param embedding_deployment_capacity int - -@description('Model dimensions of embedding model to deploy.') +param embedding_model_deployment_type string +param embedding_model_capacity int param embedding_model_dimensions int = 1536 -@allowed([ - 'Standard' - 'GlobalStandard' -]) -param embedding_deployment_type string - // Search service: @description('Name of AI Search resource') param search_service_name string @@ -105,8 +85,8 @@ resource ai_foundry 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' = } } sku: { - name: gpt_deployment_type - capacity: gpt_deployment_capacity + name: gpt_model_deployment_type + capacity: gpt_model_capacity } } @@ -119,8 +99,8 @@ resource ai_foundry 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' = } } sku: { - name: embedding_deployment_type - capacity: embedding_deployment_capacity + name: embedding_model_deployment_type + capacity: embedding_model_capacity } dependsOn: [ gpt_deployment diff --git a/infra/resources/container_instance.bicep b/infra/resources/container_instance.bicep deleted file mode 100644 index 492fa51f..00000000 --- a/infra/resources/container_instance.bicep +++ /dev/null @@ -1,260 +0,0 @@ -@description('Resource name suffix.') -param suffix string - -@description('Name of Container Group resource.') -param name string = 'ci-${suffix}' - -@description('Location for all resources.') -param location string = resourceGroup().location - -// Language: -param language_endpoint string -param clu_project_name string = 'conv-assistant-clu' -param clu_model_name string = 'clu-m1' -param clu_deployment_name string = 'clu-m1-d1' -param clu_confidence_threshold string = '0.5' -param cqa_project_name string = 'conv-assistant-cqa' -param cqa_deployment_name string = 'production' -param cqa_confidence_threshold string = '0.5' -param orchestration_project_name string = 'conv-assistant-orch' -param orchestration_model_name string = 'orch-m1' -param orchestration_deployment_name string = 'orch-m1-d1' -param orchestration_confidence_threshold string = '0.5' -param pii_enabled string = 'true' -param pii_categories string = 'organization,person' -param pii_confidence_threshold string = '0.5' - -// Search/AOAI: -param aoai_endpoint string -param aoai_deployment string -param embedding_deployment_name string -param embedding_model_name string -param embedding_model_dimensions int -param storage_account_name string -param storage_account_connection_string string -param blob_container_name string -param search_endpoint string -param search_index_name string = 'conv-assistant-manuals-idx' - -// Agents: -param agents_project_endpoint string -param delete_old_agents string = 'false' -param max_agent_retry string = '5' - -// App: -@allowed([ - 'BYPASS' - 'CLU' - 'CQA' - 'ORCHESTRATION' - 'FUNCTION_CALLING' - 'TRIAGE_AGENT' -]) -param router_type string = 'TRIAGE_AGENT' -param image string = 'mcr.microsoft.com/azure-cli' -param port int = 8000 -param repository string = 'https://github.com/Azure-Samples/Azure-Language-OpenAI-Conversational-Agent-Accelerator' - -// Managed Identity: -@description('Name of managed identity to use for Container Apps.') -param managed_identity_name string - -resource managed_identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = { - name: managed_identity_name -} - -//----------- Container Instance Resource -----------// -resource container_instance 'Microsoft.ContainerInstance/containerGroups@2024-10-01-preview' = { - name: name - location: location - identity: { - type: 'UserAssigned' - userAssignedIdentities: { - '${managed_identity.id}' : {} - } - } - properties: { - restartPolicy: 'Never' - volumes: [ - { - name: 'repo' - gitRepo: { - directory: 'repo' - repository: repository - } - } - ] - osType: 'Linux' - ipAddress: { - dnsNameLabel: 'conv-agent-app' - autoGeneratedDomainNameLabelScope: 'Noreuse' - type: 'Public' - ports: [ - { - port: port - protocol: 'TCP' - } - ] - } - containers: [ - { - name: 'conv-agent-app' - properties: { - image: image - resources: { - requests: { - cpu: 1 - memoryInGB: 1 - } - } - volumeMounts: [ - { - mountPath: '/mnt' - name: 'repo' - } - ] - ports: [ - { - port: port - protocol: 'TCP' - } - ] - command: [ - '/bin/bash' - '-c' - 'chmod +x mnt/repo/infra/scripts/run_container_app.sh && bash mnt/repo/infra/scripts/run_container_app.sh' - ] - environmentVariables: [ - { - name: 'AGENTS_PROJECT_ENDPOINT' - value: agents_project_endpoint - } - { - name: 'USE_MI_AUTH' - value: 'true' - } - { - name: 'MI_CLIENT_ID' - value: managed_identity.properties.clientId - } - { - name: 'AOAI_ENDPOINT' - value: aoai_endpoint - } - { - name: 'AOAI_DEPLOYMENT' - value: aoai_deployment - } - { - name: 'SEARCH_ENDPOINT' - value: search_endpoint - } - { - name: 'SEARCH_INDEX_NAME' - value: search_index_name - } - { - name: 'EMBEDDING_DEPLOYMENT_NAME' - value: embedding_deployment_name - } - { - name: 'EMBEDDING_MODEL_NAME' - value: embedding_model_name - } - { - name: 'EMBEDDING_MODEL_DIMENSIONS' - value: string(embedding_model_dimensions) - } - { - name: 'STORAGE_ACCOUNT_NAME' - value: storage_account_name - } - { - name: 'STORAGE_ACCOUNT_CONNECTION_STRING' - value: storage_account_connection_string - } - { - name: 'BLOB_CONTAINER_NAME' - value: blob_container_name - } - { - name: 'LANGUAGE_ENDPOINT' - value: language_endpoint - } - { - name: 'CLU_PROJECT_NAME' - value: clu_project_name - } - { - name: 'CLU_MODEL_NAME' - value: clu_model_name - } - { - name: 'CLU_DEPLOYMENT_NAME' - value: clu_deployment_name - } - { - name: 'CLU_CONFIDENCE_THRESHOLD' - value: clu_confidence_threshold - } - { - name: 'CQA_PROJECT_NAME' - value: cqa_project_name - } - { - name: 'CQA_DEPLOYMENT_NAME' - value: cqa_deployment_name - } - { - name: 'CQA_CONFIDENCE_THRESHOLD' - value: cqa_confidence_threshold - } - { - name: 'ORCHESTRATION_PROJECT_NAME' - value: orchestration_project_name - } - { - name: 'ORCHESTRATION_MODEL_NAME' - value: orchestration_model_name - } - { - name: 'ORCHESTRATION_DEPLOYMENT_NAME' - value: orchestration_deployment_name - } - { - name: 'ORCHESTRATION_CONFIDENCE_THRESHOLD' - value: orchestration_confidence_threshold - } - { - name: 'PII_ENABLED' - value: pii_enabled - } - { - name: 'PII_CATEGORIES' - value: pii_categories - } - { - name: 'PII_CONFIDENCE_THRESHOLD' - value: pii_confidence_threshold - } - { - name: 'ROUTER_TYPE' - value: router_type - } - { - name: 'DELETE_OLD_AGENTS' - value: delete_old_agents - } - { - name: 'MAX_AGENT_RETRY' - value: max_agent_retry - } - ] - } - } - ] - } -} - -//----------- Outputs -----------// -output fqdn string = container_instance.properties.ipAddress.fqdn diff --git a/infra/resources/container_registry.bicep b/infra/resources/container_registry.bicep new file mode 100644 index 00000000..b62d34b5 --- /dev/null +++ b/infra/resources/container_registry.bicep @@ -0,0 +1,20 @@ +@description('Resource name suffix.') +param suffix string + +@description('Name of Container Registry resource.') +param name string = 'acr${suffix}' + +@description('Location for all resources.') +param location string = resourceGroup().location + +//----------- Container Registry Resource -----------// +resource container_registry 'Microsoft.ContainerRegistry/registries@2025-04-01' = { + name: name + location: location + sku: { + name: 'Standard' + } +} + +//----------- Outputs -----------// +output name string = container_registry.name diff --git a/infra/resources/managed_identity.bicep b/infra/resources/managed_identity.bicep index fc1a9967..2324c7aa 100644 --- a/infra/resources/managed_identity.bicep +++ b/infra/resources/managed_identity.bicep @@ -15,3 +15,5 @@ resource managed_identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023 //----------- Outputs -----------// output name string = managed_identity.name +output id string = managed_identity.id +output client_id string = managed_identity.properties.clientId diff --git a/infra/resources/role_assignments.bicep b/infra/resources/role_assignments.bicep index f5452a58..b944c754 100644 --- a/infra/resources/role_assignments.bicep +++ b/infra/resources/role_assignments.bicep @@ -1,6 +1,9 @@ @description('Name of Managed Identity resource.') param managed_identity_name string +@description('Name of Container Registry resource.') +param container_registry_name string + @description('Name of Storage Account resource.') param storage_account_name string @@ -15,23 +18,30 @@ resource managed_identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023 name: managed_identity_name } -//----------- SCOPE: Storage Account Role Assignments -----------// -resource storage_account 'Microsoft.Storage/storageAccounts@2023-05-01' existing = { - name: storage_account_name +//----------- SCOPE: Container Registry Role Assignments -----------// +resource container_registry 'Microsoft.ContainerRegistry/registries@2025-04-01' existing = { + name: container_registry_name } // PRINCIPAL: Managed Identity -resource mi_storage_blob_data_contributor_role_assignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(storage_account.id, managed_identity.id, storage_blob_data_contributor_role.id) - scope: storage_account +// Allow container instance to pull docker image from ACR using MI. +resource mi_acr_pull_role_assignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(container_registry.id, managed_identity.id, acr_pull_role.id) + scope: container_registry properties: { principalId: managed_identity.properties.principalId - roleDefinitionId: storage_blob_data_contributor_role.id + roleDefinitionId: acr_pull_role.id principalType: 'ServicePrincipal' } } +//----------- SCOPE: Storage Account Role Assignments -----------// +resource storage_account 'Microsoft.Storage/storageAccounts@2023-05-01' existing = { + name: storage_account_name +} + // PRINCIPAL: Search service +// Allow search service to access blob container data to run indexing pipeline. resource search_storage_blob_data_reader_role_assignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: guid(storage_account.id, search_service.id, storage_blob_data_reader_role.id) scope: storage_account @@ -48,34 +58,13 @@ resource search_service 'Microsoft.Search/searchServices@2023-11-01' existing = } // PRINCIPAL: Managed Identity -resource mi_search_index_data_contributor_role_assignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(search_service.id, managed_identity.id, search_index_data_contributor_role.id) - scope: search_service - properties: { - principalId: managed_identity.properties.principalId - roleDefinitionId: search_index_data_contributor_role.id - principalType: 'ServicePrincipal' - } -} - -// PRINCIPAL: Managed Identity -resource mi_search_service_contributor_role_assignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(search_service.id, managed_identity.id, search_service_contributor_role.id) +// Allow container instance to fetch RAG grounding data from search index using MI. +resource mi_search_index_data_reader_role_assignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(search_service.id, managed_identity.id, search_index_data_reader_role.id) scope: search_service properties: { principalId: managed_identity.properties.principalId - roleDefinitionId: search_service_contributor_role.id - principalType: 'ServicePrincipal' - } -} - -// PRINCIPAL: AI Foundry (OpenAI) -resource foundry_search_index_data_contributor_role_assignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(search_service.id, ai_foundry.id, search_index_data_contributor_role.id) - scope: search_service - properties: { - principalId: ai_foundry.identity.principalId - roleDefinitionId: search_index_data_contributor_role.id + roleDefinitionId: search_index_data_reader_role.id principalType: 'ServicePrincipal' } } @@ -86,75 +75,69 @@ resource ai_foundry 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' ex } // PRINCIPAL: Managed Identity -resource mi_cognitive_services_openai_contributor_role_assignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(ai_foundry.id, managed_identity.id, cognitive_services_openai_contributor_role.id) +// Allow container instance to call AOAI chat completions using MI. +resource mi_cognitive_services_openai_user_role_assignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(ai_foundry.id, managed_identity.id, cognitive_services_openai_user_role.id) scope: ai_foundry properties: { principalId: managed_identity.properties.principalId - roleDefinitionId: cognitive_services_openai_contributor_role.id + roleDefinitionId: cognitive_services_openai_user_role.id principalType: 'ServicePrincipal' } } // PRINCIPAL: Managed Identity -resource mi_cognitive_services_language_owner_role_assignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(ai_foundry.id, managed_identity.id, cognitive_services_language_owner_role.id) +// Allow container instance to call language APIs using MI. +resource mi_cognitive_services_language_reader_role_role_assignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(ai_foundry.id, managed_identity.id, cognitive_services_language_reader_role.id) scope: ai_foundry properties: { principalId: managed_identity.properties.principalId - roleDefinitionId: cognitive_services_language_owner_role.id - principalType: 'ServicePrincipal' - } -} - -// PRINCIPAL: AI Foundry (OpenAI) -resource foundry_cognitive_services_language_owner_role_assignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(ai_foundry.id, ai_foundry.id, cognitive_services_language_owner_role.id) - scope: ai_foundry - properties: { - principalId: ai_foundry.identity.principalId - roleDefinitionId: cognitive_services_language_owner_role.id + roleDefinitionId: cognitive_services_language_reader_role.id principalType: 'ServicePrincipal' } } // PRINCIPAL: Managed Identity -resource mi_azure_ai_account_owner_role_assignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(ai_foundry.id, managed_identity.id, azure_ai_account_owner_role.id) +// Allow container instance to call agents API using MI. +resource mi_azure_ai_account_user_role_assignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(ai_foundry.id, managed_identity.id, azure_ai_account_user_role.id) scope: ai_foundry properties: { principalId: managed_identity.properties.principalId - roleDefinitionId: azure_ai_account_owner_role.id + roleDefinitionId: azure_ai_account_user_role.id principalType: 'ServicePrincipal' } } -// PRINCIPAL: Managed Identity -resource mi_azure_ai_account_user_role_assignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(ai_foundry.id, managed_identity.id, azure_ai_account_user_role.id) +// PRINCIPAL: AI Foundry (OpenAI) +// Allow triage agent to call language APIs. +resource foundry_cognitive_services_language_reader_role_role_assignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(ai_foundry.id, ai_foundry.id, cognitive_services_language_reader_role.id) scope: ai_foundry properties: { - principalId: managed_identity.properties.principalId - roleDefinitionId: azure_ai_account_user_role.id + principalId: ai_foundry.identity.principalId + roleDefinitionId: cognitive_services_language_reader_role.id principalType: 'ServicePrincipal' } } // PRINCIPAL: Search Service -resource search_cognitive_services_openai_contributor_role_assignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(ai_foundry.id, search_service.id, cognitive_services_openai_contributor_role.id) +// Allow search service to run AOAI embedding model in indexing pipeline. +resource search_cognitive_services_openai_user_role_role_assignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(ai_foundry.id, search_service.id, cognitive_services_openai_user_role.id) scope: ai_foundry properties: { principalId: search_service.identity.principalId - roleDefinitionId: cognitive_services_openai_contributor_role.id + roleDefinitionId: cognitive_services_openai_user_role.id principalType: 'ServicePrincipal' } } //----------- Built-in Roles -----------// -@description('Built-in Storage Blob Data Contributor role (https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles/storage#storage-blob-data-contributor).') -resource storage_blob_data_contributor_role 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { - name: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' +@description('Built-in Acr Pull role (https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles/containers#acrpull).') +resource acr_pull_role 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { + name: '7f951dda-4ed3-4680-a7ca-43fe172d538d' } @description('Built-in Storage Blob Data Reader role (https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles/storage#storage-blob-data-reader).') @@ -162,29 +145,19 @@ resource storage_blob_data_reader_role 'Microsoft.Authorization/roleDefinitions@ name: '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1' } -@description('Built-in Search Service Contributor role (https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles/ai-machine-learning#search-service-contributor).') -resource search_service_contributor_role 'Microsoft.Authorization/roleDefinitions@2018-01-01-preview' existing = { - name: '7ca78c08-252a-4471-8644-bb5ff32d4ba0' -} - -@description('Built-in Search Index Data Contributor role (https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles/ai-machine-learning#search-index-data-contributor).') -resource search_index_data_contributor_role 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { - name: '8ebe5a00-799e-43f5-93ac-243d3dce84a7' -} - -@description('Built-in Cognitive Services OpenAI Contributor role (https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles/ai-machine-learning#cognitive-services-openai-contributor).') -resource cognitive_services_openai_contributor_role 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { - name: 'a001fd3d-188f-4b5d-821b-7da978bf7442' +@description('Built-in Search Index Data Reader role (https://docs.azure.cn/en-us/role-based-access-control/built-in-roles/ai-machine-learning#search-index-data-reader).') +resource search_index_data_reader_role 'Microsoft.Authorization/roleDefinitions@2018-01-01-preview' existing = { + name: '1407120a-92aa-4202-b7e9-c0e197c71c8f' } -@description('Built-in Cognitive Services Language Owner role (https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles/ai-machine-learning#cognitive-services-language-owner).') -resource cognitive_services_language_owner_role 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { - name: 'f07febfe-79bc-46b1-8b37-790e26e6e498' +@description('Built-in Cognitive Services OpenAI User role (https://docs.azure.cn/en-us/role-based-access-control/built-in-roles/ai-machine-learning#cognitive-services-openai-user).') +resource cognitive_services_openai_user_role 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { + name: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' } -@description('Built-in Azure AI Account Owner role (https://learn.microsoft.com/en-us/azure/ai-foundry/concepts/rbac-azure-ai-foundry?pivots=fdp-project#azure-ai-account-owner).') -resource azure_ai_account_owner_role 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { - name: 'e47c6f54-e4a2-4754-9501-8e0985b135e1' +@description('Built-in Cognitive Services Language Reader role (https://docs.azure.cn/en-us/role-based-access-control/built-in-roles/ai-machine-learning#cognitive-services-language-reader).') +resource cognitive_services_language_reader_role 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { + name: '7628b7b8-a8b2-4cdc-b46f-e9b35248918e' } @description('Built-in Azure AI Account User role (https://learn.microsoft.com/en-us/azure/ai-foundry/concepts/rbac-azure-ai-foundry?pivots=fdp-project#azure-ai-user).') diff --git a/infra/scripts/agents/agents_config.yaml b/infra/scripts/agents/agents_config.yaml new file mode 100644 index 00000000..6a6ba94d --- /dev/null +++ b/infra/scripts/agents/agents_config.yaml @@ -0,0 +1,71 @@ +triage: + name: 'TriageAgent' + env_var: 'TRIAGE_AGENT_ID' + openapi_tools: + - name: 'clu_api' + spec: 'clu.json' + description: 'An API to extract intent from a given message' + - name: 'cqa_api' + spec: 'cqa.json' + description: 'An API to answer questions from a knowledge-base' + instructions: | + You are a triage agent designed to determine whether a given utterance is related to a pre-configured knowledge-base or to pre-registered intents. You have at your disposition two tools but you can only use ONE for a given input: + 1. 'cqa_api': this tools uses a pre-configured knowledge-base to answer questions. + - Here are a few examples of questions/topics a user utterance may relate to where 'cqa_api' should be called: ${cqa_example_questions}. + - When you return answers from 'cqa_api', format the response as JSON: {"type": "cqa_result", "response": {cqa_response}, "terminated": "True"}, where 'cqa_response' is the full JSON API response from 'cqa_api'. + + 2. 'clu_api': this tool uses a pre-trained model to extract the intent and entities of an utterance. + - Here are a few examples of intents/actions a user utterance may relate to where 'clu_api' should be called: ${clu_example_intents}. + - When you return answers from 'clu_api', format the response as JSON: {"type": "clu_result", "response": {clu_response}, "terminated": "False"}, where 'clu_response' is the full JSON API response from 'clu_api'. + - An example of a valid clu_response is {"kind": "ConversationResult", "result": {"query": "what's the status of order 1234", "prediction": {"topIntent": "OrderStatus", "projectKind": "Conversation", "intents": [{"category": "OrderStatus", "confidenceScore": 0.8545539}, {"category": "CancelOrder", "confidenceScore": 0.59596604}, {"category": "RefundStatus", "confidenceScore": 0.5501976}, {"category": "None", "confidenceScore": 0.33382362}], "entities": [{"category": "OrderId", "text": "1234", "offset": 27, "length": 4, "confidenceScore": 1, "resolutions": [{"resolutionKind": "NumberResolution", "numberKind": "Integer", "value": 1234}], "extraInformation": [{"extraInformationKind": "EntitySubtype", "value": "quantity.number"}]}]}}} + - To call the `clu_api`, the following parameter values **must** be used in the payload as a valid JSON object: {"analysisInput":{"conversationItem":{"id":,"participantId":,"text":}},"parameters":{"projectName":"${CLU_PROJECT_NAME}","deploymentName":"${CLU_DEPLOYMENT_NAME}"},"kind":"Conversation"}. You **must** include '${CLU_API_VERSION}' as the value for query parameter 'api-version'. + - You must validate the input to ensure it is a valid JSON object before calling the `clu_api`. + + Safety Information: + - You must use ONE of the two tools to perform your task. + - You should only use one tool at a time, and do NOT chain the tools together. + - You must return the full API response for either tool and ensure it's valid JSON. + - You should not rewrite or remove any info from the JSON API response. + - You should return immediately after calling a tool. + - You should always reference user input when determining which tool to call. + - Your responses should NOT generate any information after the tool call. +head_support: + name: 'HeadSupportAgent' + env_var: 'HEAD_SUPPORT_AGENT_ID' + openapi_tools: [] + instructions: | + You are a head support agent that routes inquiries to the proper custom agent based on the provided intent and entities from the triage agent. + You must choose between the following agents: ${custom_intent_agent_names} + + You must return the response in the following valid JSON format: {"target_agent": "","intent": "","entities": [], "terminated": "False"} + + Where: + - "target_agent" is the name of the agent you are routing to (must match one of the agent names above based on the extracted "intent" below). + - "intent" is the top-level intent extracted from the CLU result. + - "entities" is a list of all entities extracted from the CLU result, including their category and value. + +# Custom intent agents: +order_status: + name: 'OrderStatusAgent' + env_var: 'ORDER_STATUS_AGENT_ID' + openapi_tools: [] + instructions: | + You are a customer support agent that checks order statuses. You must use the 'OrderStatusPlugin' to check the status of an order. The plugin will return a string, which you must use as the . + If you need more information from the user, you must return a response with "need_more_info": "True", otherwise you must return "need_more_info": "False". + You must return the response in the following valid JSON format: {"response": , "terminated": "True", "need_more_info": <"True" or "False">} +order_cancel: + name: 'OrderCancelAgent' + env_var: 'ORDER_CANCEL_AGENT_ID' + openapi_tools: [] + instructions: | + You are a customer support agent that handles order cancellations. You must use the 'OrderCancelPlugin' to handle order cancellation requests. The plugin will return a string, which you must use as the . + If you need more information from the user, you must return a response with "need_more_info": "True", otherwise you must return "need_more_info": "False". + You must return the response in the following valid JSON format: {"response": , "terminated": "True", "need_more_info": <"True" or "False">} +order_refund: + name: 'OrderRefundAgent' + env_var: 'ORDER_REFUND_AGENT_ID' + openapi_tools: [] + instructions: | + You are a customer support agent that handles order refunds. You must use the 'OrderRefundPlugin' to handle order refund requests. The plugin will return a string, which you must use as the . + If you need more information from the user, you must return a response with "need_more_info": "True", otherwise you must return "need_more_info": "False". + You must return the response in the following valid JSON format: {"response": , "terminated": "True", "need_more_info": <"True" or "False">} \ No newline at end of file diff --git a/infra/scripts/agents/agents_setup.py b/infra/scripts/agents/agents_setup.py new file mode 100644 index 00000000..a2c4e4e1 --- /dev/null +++ b/infra/scripts/agents/agents_setup.py @@ -0,0 +1,105 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +import os +import json +import yaml +from azure.identity import AzureCliCredential +from azure.ai.agents import AgentsClient +from azure.ai.agents.models import ( + OpenApiTool, OpenApiManagedAuthDetails, OpenApiManagedSecurityScheme +) +from utils import camel_to_snake, bind_parameters, get_clu_intents, get_cqa_questions + +ENV_FILE = os.environ.get('ENV_FILE') +DELETE_OLD_AGENTS = os.environ.get('DELETE_OLD_AGENTS', 'false').lower() == 'true' +AGENTS_PROJECT_ENDPOINT = os.environ.get('AGENTS_PROJECT_ENDPOINT') +AGENTS_API_VERSION = '2025-05-15-preview' +AGENTS_MODEL_NAME = os.environ.get('AOAI_DEPLOYMENT') +AGENTS_CONFIG_FILE = 'agents_config.yaml' + +# Create agents client: +AGENTS_CLIENT = AgentsClient( + endpoint=AGENTS_PROJECT_ENDPOINT, + credential=AzureCliCredential(), + api_version=AGENTS_API_VERSION +) + + +def create_agent(agent_config: dict, parameters: dict = {}): + # Authentication details for OpenAPI connection: + auth = OpenApiManagedAuthDetails( + security_scheme=OpenApiManagedSecurityScheme( + audience="https://cognitiveservices.azure.com/" + ) + ) + + # Create OpenAPI tools: + tools = [] + for tool in agent_config['openapi_tools']: + with open(f'openapi_specs/{tool['spec']}', 'r') as fp: + spec = json.loads(bind_parameters(fp.read(), parameters)) + tools.append(OpenApiTool( + name=tool['name'], + spec=spec, + description=tool['description'], + auth=auth + )) + tool_defs = None if not tools else [ + tool_def for tool in tools for tool_def in tool.definitions + ] + + # Generate instructions: + instructions = bind_parameters(agent_config['instructions'], parameters) + + # Create agent: + agent = AGENTS_CLIENT.create_agent( + model=AGENTS_MODEL_NAME, + name=agent_config['name'], + instructions=instructions, + tools=tool_defs, + temperature=0.2 + ) + + print(f'Agent created: {agent_config['name']}, {agent.id}') + + # Update env file: + with open(ENV_FILE, 'a') as fp: + fp.write(f'export {agent_config['env_var']}="{agent.id}"\n') + + +if DELETE_OLD_AGENTS: + print("Deleting all existing agents in project...") + to_delete = [agent for agent in AGENTS_CLIENT.list_agents()] + for agent in to_delete: + print(f"Deleting agent {agent.name}: {agent.id}") + AGENTS_CLIENT.delete_agent(agent.id) + +# Fetch agents config: +with open(AGENTS_CONFIG_FILE, 'r') as fp: + agents_config = yaml.safe_load(fp) + +# Query language projects for context: +clu_intents = get_clu_intents() +cqa_questions = get_cqa_questions() + +# Create TriageAgent: +triage_agent_parameters = { + 'clu_example_intents': ', '.join(clu_intents), + 'cqa_example_questions': ', '.join(cqa_questions) +} +create_agent(agents_config['triage'], triage_agent_parameters) + +# Create HeadSupportAgent (CLU custom intent routing): +head_support_agent_parameters = { + 'custom_intent_agent_names': ', '.join( + [f'{intent}Agent' for intent in clu_intents] + ) +} +create_agent(agents_config['head_support'], head_support_agent_parameters) + +# Create custom intent agents: +for agent_key in [camel_to_snake(intent) for intent in clu_intents]: + create_agent(agents_config[agent_key]) + +# Cleanup: +AGENTS_CLIENT.close() diff --git a/infra/openapi_specs/clu.json b/infra/scripts/agents/openapi_specs/clu.json similarity index 96% rename from infra/openapi_specs/clu.json rename to infra/scripts/agents/openapi_specs/clu.json index 796b30a5..a0a5a886 100644 --- a/infra/openapi_specs/clu.json +++ b/infra/scripts/agents/openapi_specs/clu.json @@ -12,7 +12,7 @@ }, "servers": [ { - "url": "${language_resource_url}/language" + "url": "${LANGUAGE_ENDPOINT}/language" } ], "tags": [], @@ -25,12 +25,12 @@ { "name": "api-version", "in": "query", - "description": "The version must be '2023-04-01'", + "description": "The API version to use for this operation. Value must be '${CLU_API_VERSION}'.", "required": true, "schema": { "minLength": 1, "type": "string", - "default": "2023-04-01" + "default": "${CLU_API_VERSION}" } } ], @@ -3026,7 +3026,7 @@ }, "text": { "type": "string", - "description": "The text input" + "description": "The text input." } }, "description": "The text modality of an input conversation." diff --git a/infra/openapi_specs/cqa.json b/infra/scripts/agents/openapi_specs/cqa.json similarity index 93% rename from infra/openapi_specs/cqa.json rename to infra/scripts/agents/openapi_specs/cqa.json index 0309fb58..9f906f4d 100644 --- a/infra/openapi_specs/cqa.json +++ b/infra/scripts/agents/openapi_specs/cqa.json @@ -7,13 +7,13 @@ }, "servers": [ { - "url": "${language_resource_url}/language" + "url": "${LANGUAGE_ENDPOINT}/language" } ], "paths": { "/:query-knowledgebases": { "post": { - "description": "Answers the specified question using your knowledge base.", + "description": "Answers the specified question using project knowledge base.", "operationId": "CQA_Query_Knowledgebases", "parameters": [ { @@ -22,9 +22,9 @@ "required": true, "schema": { "type": "string", - "default": "${cqa_project_name}" + "default": "${CQA_PROJECT_NAME}" }, - "description": "The name of the project to use. value must be '${cqa_project_name}'" + "description": "The name of the project to use. Value must be `${CQA_PROJECT_NAME}`." }, { "name": "deploymentName", @@ -32,19 +32,19 @@ "required": true, "schema": { "type": "string", - "default": "${cqa_deployment_name}" + "default": "${CQA_DEPLOYMENT_NAME}" }, - "description": "The name of the specific deployment of the project to use. value must be '${cqa_deployment_name}'" + "description": "The name of the specific deployment of the project to use. Value must be `${CQA_DEPLOYMENT_NAME}`." }, { "name": "api-version", "in": "query", - "description": "API version. Value must be '2023-04-01'", "required": true, "schema": { "type": "string", - "default": "2023-04-01" - } + "default": "${CQA_API_VERSION}" + }, + "description": "API version. Value must be `${CQA_API_VERSION}`." } ], "requestBody": { @@ -100,7 +100,7 @@ "properties": { "question": { "type": "string", - "description": "User question to query against the knowledge base." + "description": "User question to query against the knowledge base. Value must be the exact user input." }, "top": { "type": "integer", diff --git a/infra/scripts/agents/requirements.txt b/infra/scripts/agents/requirements.txt new file mode 100644 index 00000000..c644093b --- /dev/null +++ b/infra/scripts/agents/requirements.txt @@ -0,0 +1,5 @@ +pyyaml +azure-identity +azure-ai-agents +azure-ai-language-conversations +azure-ai-language-questionanswering \ No newline at end of file diff --git a/infra/scripts/agents/run_agents_setup.sh b/infra/scripts/agents/run_agents_setup.sh new file mode 100644 index 00000000..bc6b1631 --- /dev/null +++ b/infra/scripts/agents/run_agents_setup.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# `az login` should have been run before executing this script: + +set -e + +cwd=$(pwd) +script_dir=$(dirname $(realpath "$0")) +cd ${script_dir} + +echo "Running agents setup..." + +export ENV_FILE="${script_dir}/../.env" +python3 -m pip install -r requirements.txt +python3 agents_setup.py + +cd ${cwd} + +echo "Agents setup complete" diff --git a/infra/scripts/agents/utils.py b/infra/scripts/agents/utils.py new file mode 100644 index 00000000..d33afbbc --- /dev/null +++ b/infra/scripts/agents/utils.py @@ -0,0 +1,106 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +import os +import re +from azure.identity import AzureCliCredential +from azure.ai.language.conversations.authoring import ConversationAuthoringClient +from azure.ai.language.questionanswering.authoring import AuthoringClient +from azure.core.rest import HttpRequest + +IS_GITHUB_WORKFLOW_RUN = os.environ.get('IS_GITHUB_WORKFLOW_RUN', 'false').lower() == 'true' + + +def camel_to_snake(camel_str): + # Insert underscores before capital letters and convert to lowercase + snake_str = re.sub(r'(? str: + """ + Replace occurrences of '${}' in the input string. + + Replace with value of in `parameters`. + If does not exist in `parameters`, check for env var. + If does not exist as an env var, perform no replacement. + """ + def replacer(match): + key = match.group(1) + return str(parameters.get(key, os.environ.get(key, match.group(0)))) + + pattern = re.compile(r'\$\{([^}]+)\}') + return pattern.sub(replacer, input_string) + + +def get_clu_intents() -> list[str]: + """ + Get all intents registered in CLU project. + """ + project_name = os.environ['CLU_PROJECT_NAME'] + client = ConversationAuthoringClient( + endpoint=os.environ['LANGUAGE_ENDPOINT'], + credential=AzureCliCredential() + ) + + try: + print(f'Getting intents from CLU project {project_name}...') + + poller = client.begin_export_project( + project_name=project_name, + string_index_type='Utf16CodeUnit', + exported_project_format='Conversation' + ) + + job_state = poller.result() + request = HttpRequest('GET', job_state['resultUrl']) + response = client.send_request(request) + exported_project = response.json() + + intents = [ + i['category'] for i in exported_project['assets']['intents'] + ] + intents = list(filter(lambda x: x != 'None', intents)) + return intents + + except Exception as e: + print(f'Unable to get intents: {e}') + raise e + + +def get_cqa_questions() -> list[str]: + """ + Get all registered questions in CQA project. + """ + project_name = os.environ['CQA_PROJECT_NAME'] + client = AuthoringClient( + endpoint=os.environ['LANGUAGE_ENDPOINT'], + credential=AzureCliCredential() + ) + + if IS_GITHUB_WORKFLOW_RUN: + # Due to auth issues when running in GitHub workflow, skip: + print('Skipping CQA project polling...') + return [] + + try: + print(f'Getting questions from CQA project {project_name}...') + + poller = client.begin_export( + project_name=project_name, + file_format='json' + ) + + job_state = poller.result() + request = HttpRequest('GET', job_state['resultUrl']) + response = client.send_request(request) + exported_project = response.json() + + questions = set() + for item in exported_project['Assets']['Qnas']: + for q in item['Questions']: + questions.add(q) + return list(questions) + + except Exception as e: + print(f'Unable to get questions: {e}') + raise e diff --git a/infra/data/LICENSE-DATA.txt b/infra/scripts/data/LICENSE-DATA.txt similarity index 100% rename from infra/data/LICENSE-DATA.txt rename to infra/scripts/data/LICENSE-DATA.txt diff --git a/infra/data/clu_import.json b/infra/scripts/data/clu_import.json similarity index 93% rename from infra/data/clu_import.json rename to infra/scripts/data/clu_import.json index a1fdfc59..95281fe5 100644 --- a/infra/data/clu_import.json +++ b/infra/scripts/data/clu_import.json @@ -19,10 +19,10 @@ "category": "OrderStatus" }, { - "category": "RefundStatus" + "category": "OrderRefund" }, { - "category": "CancelOrder" + "category": "OrderCancel" } ], "entities": [ @@ -76,7 +76,7 @@ { "text": "was i refunded for order 12344444", "language": "en-us", - "intent": "RefundStatus", + "intent": "OrderRefund", "entities": [ { "category": "OrderId", @@ -88,7 +88,7 @@ { "text": "can i refund order 56784567", "language": "en-us", - "intent": "RefundStatus", + "intent": "OrderRefund", "entities": [ { "category": "OrderId", @@ -100,7 +100,7 @@ { "text": "when will i be refunded for order 89089034", "language": "en-us", - "intent": "RefundStatus", + "intent": "OrderRefund", "entities": [ { "category": "OrderId", @@ -112,7 +112,7 @@ { "text": "when will i get my refund for 89898989", "language": "en-us", - "intent": "RefundStatus", + "intent": "OrderRefund", "entities": [ { "category": "OrderId", @@ -124,7 +124,7 @@ { "text": "did my refund for 12312344 go through", "language": "en-us", - "intent": "RefundStatus", + "intent": "OrderRefund", "entities": [ { "category": "OrderId", @@ -196,7 +196,7 @@ { "text": "Undo 12334444", "language": "en-us", - "intent": "CancelOrder", + "intent": "OrderCancel", "entities": [ { "category": "OrderId", @@ -208,7 +208,7 @@ { "text": "please stop order 55556666", "language": "en-us", - "intent": "CancelOrder", + "intent": "OrderCancel", "entities": [ { "category": "OrderId", @@ -220,7 +220,7 @@ { "text": "Can you cancel 12345678", "language": "en-us", - "intent": "CancelOrder", + "intent": "OrderCancel", "entities": [ { "category": "OrderId", @@ -232,7 +232,7 @@ { "text": "Cancel 888888", "language": "en-us", - "intent": "CancelOrder", + "intent": "OrderCancel", "entities": [ { "category": "OrderId", @@ -244,7 +244,7 @@ { "text": "Please cancel order 27787724", "language": "en-us", - "intent": "CancelOrder", + "intent": "OrderCancel", "entities": [ { "category": "OrderId", diff --git a/infra/data/cqa_import.json b/infra/scripts/data/cqa_import.json similarity index 98% rename from infra/data/cqa_import.json rename to infra/scripts/data/cqa_import.json index eb1962a0..05ac8211 100644 --- a/infra/data/cqa_import.json +++ b/infra/scripts/data/cqa_import.json @@ -7,6 +7,7 @@ "answer": "Contoso Outdoors is proud to offer a 30 day refund policy. Return unopened, unused products within 30 days of purchase to any Contoso Outdoors store for a full refund.", "questions": [ "Refund Policy", + "Return Policy", "What is your refund policy?", "Contoso Outdoors refund policy" ] diff --git a/infra/data/orchestration_import.json b/infra/scripts/data/orchestration_import.json similarity index 100% rename from infra/data/orchestration_import.json rename to infra/scripts/data/orchestration_import.json diff --git a/infra/data/product_info.tar.gz b/infra/scripts/data/product_info.tar.gz similarity index 100% rename from infra/data/product_info.tar.gz rename to infra/scripts/data/product_info.tar.gz diff --git a/infra/data/product_info/product_info_1.md b/infra/scripts/data/product_info/product_info_1.md similarity index 100% rename from infra/data/product_info/product_info_1.md rename to infra/scripts/data/product_info/product_info_1.md diff --git a/infra/data/product_info/product_info_10.md b/infra/scripts/data/product_info/product_info_10.md similarity index 100% rename from infra/data/product_info/product_info_10.md rename to infra/scripts/data/product_info/product_info_10.md diff --git a/infra/data/product_info/product_info_11.md b/infra/scripts/data/product_info/product_info_11.md similarity index 100% rename from infra/data/product_info/product_info_11.md rename to infra/scripts/data/product_info/product_info_11.md diff --git a/infra/data/product_info/product_info_12.md b/infra/scripts/data/product_info/product_info_12.md similarity index 100% rename from infra/data/product_info/product_info_12.md rename to infra/scripts/data/product_info/product_info_12.md diff --git a/infra/data/product_info/product_info_13.md b/infra/scripts/data/product_info/product_info_13.md similarity index 100% rename from infra/data/product_info/product_info_13.md rename to infra/scripts/data/product_info/product_info_13.md diff --git a/infra/data/product_info/product_info_14.md b/infra/scripts/data/product_info/product_info_14.md similarity index 100% rename from infra/data/product_info/product_info_14.md rename to infra/scripts/data/product_info/product_info_14.md diff --git a/infra/data/product_info/product_info_15.md b/infra/scripts/data/product_info/product_info_15.md similarity index 100% rename from infra/data/product_info/product_info_15.md rename to infra/scripts/data/product_info/product_info_15.md diff --git a/infra/data/product_info/product_info_16.md b/infra/scripts/data/product_info/product_info_16.md similarity index 100% rename from infra/data/product_info/product_info_16.md rename to infra/scripts/data/product_info/product_info_16.md diff --git a/infra/data/product_info/product_info_17.md b/infra/scripts/data/product_info/product_info_17.md similarity index 100% rename from infra/data/product_info/product_info_17.md rename to infra/scripts/data/product_info/product_info_17.md diff --git a/infra/data/product_info/product_info_18.md b/infra/scripts/data/product_info/product_info_18.md similarity index 100% rename from infra/data/product_info/product_info_18.md rename to infra/scripts/data/product_info/product_info_18.md diff --git a/infra/data/product_info/product_info_19.md b/infra/scripts/data/product_info/product_info_19.md similarity index 100% rename from infra/data/product_info/product_info_19.md rename to infra/scripts/data/product_info/product_info_19.md diff --git a/infra/data/product_info/product_info_2.md b/infra/scripts/data/product_info/product_info_2.md similarity index 100% rename from infra/data/product_info/product_info_2.md rename to infra/scripts/data/product_info/product_info_2.md diff --git a/infra/data/product_info/product_info_20.md b/infra/scripts/data/product_info/product_info_20.md similarity index 100% rename from infra/data/product_info/product_info_20.md rename to infra/scripts/data/product_info/product_info_20.md diff --git a/infra/data/product_info/product_info_3.md b/infra/scripts/data/product_info/product_info_3.md similarity index 100% rename from infra/data/product_info/product_info_3.md rename to infra/scripts/data/product_info/product_info_3.md diff --git a/infra/data/product_info/product_info_4.md b/infra/scripts/data/product_info/product_info_4.md similarity index 100% rename from infra/data/product_info/product_info_4.md rename to infra/scripts/data/product_info/product_info_4.md diff --git a/infra/data/product_info/product_info_5.md b/infra/scripts/data/product_info/product_info_5.md similarity index 100% rename from infra/data/product_info/product_info_5.md rename to infra/scripts/data/product_info/product_info_5.md diff --git a/infra/data/product_info/product_info_6.md b/infra/scripts/data/product_info/product_info_6.md similarity index 100% rename from infra/data/product_info/product_info_6.md rename to infra/scripts/data/product_info/product_info_6.md diff --git a/infra/data/product_info/product_info_7.md b/infra/scripts/data/product_info/product_info_7.md similarity index 100% rename from infra/data/product_info/product_info_7.md rename to infra/scripts/data/product_info/product_info_7.md diff --git a/infra/data/product_info/product_info_8.md b/infra/scripts/data/product_info/product_info_8.md similarity index 100% rename from infra/data/product_info/product_info_8.md rename to infra/scripts/data/product_info/product_info_8.md diff --git a/infra/data/product_info/product_info_9.md b/infra/scripts/data/product_info/product_info_9.md similarity index 100% rename from infra/data/product_info/product_info_9.md rename to infra/scripts/data/product_info/product_info_9.md diff --git a/infra/scripts/language/README.md b/infra/scripts/language/README.md deleted file mode 100644 index 31d99b7d..00000000 --- a/infra/scripts/language/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# Conversational-Agent: Language Setup - -## Environment Variables -Expected environment variables: -``` -LANGUAGE_ENDPOINT= - -CLU_PROJECT_NAME= -CLU_MODEL_NAME= -CLU_DEPLOYMENT_NAME= - -CQA_PROJECT_NAME= -CQA_DEPLOYMENT_NAME=production - -ORCHESTRATION_PROJECT_NAME= -ORCHESTRATION_MODEL_NAME= -ORCHESTRATION_DEPLOYMENT_NAME= -``` - -## Running Setup (local) -``` -az login -bash run_language_setup.sh -``` \ No newline at end of file diff --git a/infra/scripts/language/agent_setup.py b/infra/scripts/language/agent_setup.py deleted file mode 100644 index f2e9b360..00000000 --- a/infra/scripts/language/agent_setup.py +++ /dev/null @@ -1,175 +0,0 @@ -import json -import os -from azure.ai.agents import AgentsClient -from azure.ai.agents.models import OpenApiTool, OpenApiManagedAuthDetails,OpenApiManagedSecurityScheme -from azure.identity import DefaultAzureCredential, ManagedIdentityCredential -from utils import bind_parameters - -config = {} - -DELETE_OLD_AGENTS = os.environ.get("DELETE_OLD_AGENTS", "false").lower() == "true" -PROJECT_ENDPOINT = os.environ.get("AGENTS_PROJECT_ENDPOINT") -MODEL_NAME = os.environ.get("AOAI_DEPLOYMENT") -CONFIG_DIR = os.environ.get("CONFIG_DIR", ".") -config_file = os.path.join(CONFIG_DIR, "config.json") - -config['language_resource_url'] = os.environ.get("LANGUAGE_ENDPOINT") -config['clu_project_name'] = os.environ.get("CLU_PROJECT_NAME") -config['clu_deployment_name'] = os.environ.get("CLU_DEPLOYMENT_NAME") -config['cqa_project_name'] = os.environ.get("CQA_PROJECT_NAME") -config['cqa_deployment_name'] = os.environ.get("CQA_DEPLOYMENT_NAME") - - -# Create agent client -agents_client = AgentsClient( - endpoint=PROJECT_ENDPOINT, - credential=DefaultAzureCredential(), - api_version="2025-05-15-preview" -) - -def create_tools(config): - # Set up the auth details for the OpenAPI connection - auth = OpenApiManagedAuthDetails(security_scheme=OpenApiManagedSecurityScheme(audience="https://cognitiveservices.azure.com/")) - - # Read in the OpenAPI spec from a file - with open("clu.json", "r") as f: - clu_openapi_spec = json.loads(bind_parameters(f.read(), config)) - - clu_api_tool = OpenApiTool( - name="clu_api", - spec=clu_openapi_spec, - description= "An API to extract intent from a given message - you MUST use version \"2023-04-01\" as this is extremely critical", - auth=auth - ) - - # Read in the OpenAPI spec from a file - with open("cqa.json", "r") as f: - cqa_openapi_spec = json.loads(bind_parameters(f.read(), config)) - - # Initialize an Agent OpenApi tool using the read in OpenAPI spec - cqa_api_tool = OpenApiTool( - name="cqa_api", - spec=cqa_openapi_spec, - description= "An API to get answer to questions related to business operation", - auth=auth - ) - - return clu_api_tool, cqa_api_tool - -with agents_client: - # If DELETE_OLD_AGENTS is set to true, delete all existing agents in the project - if DELETE_OLD_AGENTS: - print("Deleting all existing agents in the project...") - agents = agents_client.list_agents() - for agent in agents: - print(f"Deleting agent: {agent.name} with ID: {agent.id}") - agents_client.delete_agent(agent.id) - - # 1) Create the triage agent which can use CLU or CQA tools to answer questions or extract intent - clu_api_tool, cqa_api_tool = create_tools(config) - TRIAGE_AGENT_NAME = "TriageAgent" - TRIAGE_AGENT_INSTRUCTIONS = """ - You are a triage agent. Your goal is to answer questions and redirect message according to their intent. You have at your disposition 2 tools but can only use ONE: - 1. cqa_api: to answer customer questions such as procedures and FAQs. - 2. clu_api: to extract the intent of the message. - You must use the ONE of the tools to perform your task. You should only use one tool at a time, and do NOT chain the tools together. Only if the tools are not able to provide the information, you can answer according to your general knowledge. You must return the full API response for either tool and ensure it's a valid JSON. - - When you return answers from the clu_api, format the response as JSON: {"type": "clu_result", "response": {clu_response}, "terminated": "False"}, where clu_response is the full JSON API response from the clu_api without rewriting or removing any info. Return immediately. Do not call the cqa_api afterwards. - - An example of a valid clu_response is {"kind": "ConversationResult", "result": {"query": "what's the status of order 1234", "prediction": {"topIntent": "OrderStatus", "projectKind": "Conversation", "intents": [{"category": "OrderStatus", "confidenceScore": 0.8545539}, {"category": "CancelOrder", "confidenceScore": 0.59596604}, {"category": "RefundStatus", "confidenceScore": 0.5501976}, {"category": "None", "confidenceScore": 0.33382362}], "entities": [{"category": "OrderId", "text": "1234", "offset": 27, "length": 4, "confidenceScore": 1, "resolutions": [{"resolutionKind": "NumberResolution", "numberKind": "Integer", "value": 1234}], "extraInformation": [{"extraInformationKind": "EntitySubtype", "value": "quantity.number"}]}]}}} - - To call the clu_api, the following parameter values **must** be used in the payload as a valid JSON object: {"api-version":"2023-04-01", "analysisInput":{"conversationItem":{"id":,"participantId":,"text":}},"parameters":{"projectName":"conv-assistant-clu","deploymentName":"clu-m1-d1"},"kind":"Conversation"} - - You must validate the input to ensure it is a valid JSON object before calling the clu_api. - - When you return answers from the cqa_api, format the response as JSON: {"type": "cqa_result", "response": {cqa_response}, "terminated": "True"} where cqa_response is the full JSON API response from the cqa_api without rewriting or removing any info. Return immediately - """ - - triage_agent_definition = agents_client.create_agent( - model=MODEL_NAME, - name=TRIAGE_AGENT_NAME, - instructions= TRIAGE_AGENT_INSTRUCTIONS, - tools=clu_api_tool.definitions + cqa_api_tool.definitions, - temperature=0.2, - ) - - # 2) Create the head support agent which takes in CLU intents and entities and routes the request to the appropriate support agent - HEAD_SUPPORT_AGENT_NAME = "HeadSupportAgent" - HEAD_SUPPORT_AGENT_INSTRUCTIONS = """ - You are a head support agent that routes inquiries to the proper custom agent based on the provided intent and entities from the triage agent. - You must choose between the following agents: - - OrderStatusAgent: for order status inquiries - - OrderCancelAgent: for order cancellation inquiries - - OrderRefundAgent: for order refund inquiries - - You must return the response in the following valid JSON format: {"target_agent": "","intent": "","entities": [],"terminated": "False"} - - Where: - - "target_agent" is the name of the agent you are routing to (must match one of the agent names above). - - "intent" is the top-level intent extracted from the CLU result. - - "entities" is a list of all entities extracted from the CLU result, including their category and value. - """ - - head_support_agent_definition = agents_client.create_agent( - model=MODEL_NAME, - name=HEAD_SUPPORT_AGENT_NAME, - instructions=HEAD_SUPPORT_AGENT_INSTRUCTIONS, - ) - - # 3) Create the custom agents for handling specific intents (our examples are OrderStatus, OrderCancel, and OrderRefund). Plugin tools will be added to these agents when we turn them into Semantic Kernel agents. - ORDER_STATUS_AGENT_NAME = "OrderStatusAgent" - ORDER_STATUS_AGENT_INSTRUCTIONS = """ - You are a customer support agent that checks order status. You must use the OrderStatusPlugin to check the status of an order. The plugin will return a string, which you must use as the . - If you need more information from the user, you must return a response with "need_more_info": "True", otherwise you must return "need_more_info": "False". - You must return the response in the following valid JSON format: {"response": , "terminated": "True", "need_more_info": <"True" or "False">} - """ - - order_status_agent_definition = agents_client.create_agent( - model=MODEL_NAME, - name=ORDER_STATUS_AGENT_NAME, - instructions=ORDER_STATUS_AGENT_INSTRUCTIONS, - ) - - ORDER_CANCEL_AGENT_NAME = "OrderCancelAgent" - ORDER_CANCEL_AGENT_INSTRUCTIONS = """ - You are a customer support agent that handles order cancellations. You must use the OrderCancellationPlugin to handle order cancellation requests. The plugin will return a string, which you must use as the . - If you need more information from the user, you must return a response with "need_more_info": "True", otherwise you must return "need_more_info": "False". - You must return the response in the following valid JSON format: {"response": , "terminated": "True", "need_more_info": <"True" or "False">} - """ - - order_cancel_agent_definition = agents_client.create_agent( - model=MODEL_NAME, - name=ORDER_CANCEL_AGENT_NAME, - instructions=ORDER_CANCEL_AGENT_INSTRUCTIONS, - ) - - ORDER_REFUND_AGENT_NAME = "OrderRefundAgent" - ORDER_REFUND_AGENT_INSTRUCTIONS = """ - You are a customer support agent that handles order refunds. You must use the OrderRefundPlugin to handle order refund requests. The plugin will return a string, which you must use as the . - If you need more information from the user, you must return a response with "need_more_info": "True", otherwise you must return "need_more_info": "False". - You must return the response in the following valid JSON format: {"response": , "terminated": "True", "need_more_info": <"True" or "False">} - """ - - order_refund_agent_definition = agents_client.create_agent( - model=MODEL_NAME, - name=ORDER_REFUND_AGENT_NAME, - instructions=ORDER_REFUND_AGENT_INSTRUCTIONS, - ) - - # Output the agent IDs in a JSON format to be captured as env variables - agent_ids = { - "TRIAGE_AGENT_ID": triage_agent_definition.id, - "HEAD_SUPPORT_AGENT_ID": head_support_agent_definition.id, - "ORDER_STATUS_AGENT_ID": order_status_agent_definition.id, - "ORDER_CANCEL_AGENT_ID": order_cancel_agent_definition.id, - "ORDER_REFUND_AGENT_ID": order_refund_agent_definition.id, - } - - # Write to config.json file - try: - # Ensure the config directory exists - os.makedirs(CONFIG_DIR, exist_ok=True) - - with open(config_file, 'w') as f: - json.dump(agent_ids, f, indent=2) - print(f"Agent IDs written to {config_file}") - print(json.dumps(agent_ids, indent=2)) - except Exception as e: - print(f"Error writing to {config_file}: {e}") - print(json.dumps(agent_ids, indent=2)) - \ No newline at end of file diff --git a/infra/scripts/language/clu_setup.py b/infra/scripts/language/clu_setup.py index 4f25b1af..8512b3a9 100644 --- a/infra/scripts/language/clu_setup.py +++ b/infra/scripts/language/clu_setup.py @@ -2,28 +2,16 @@ # Licensed under the MIT License. import os import json -from azure.identity import DefaultAzureCredential, ManagedIdentityCredential +from azure.identity import AzureCliCredential from azure.ai.language.conversations.authoring import ConversationAuthoringClient -def get_azure_credential(): - use_mi_auth = os.environ.get('USE_MI_AUTH', 'false').lower() == 'true' - - if use_mi_auth: - mi_client_id = os.environ['MI_CLIENT_ID'] - return ManagedIdentityCredential( - client_id=mi_client_id - ) - - return DefaultAzureCredential() - - project_name = os.environ['CLU_PROJECT_NAME'] model_name = os.environ['CLU_MODEL_NAME'] deployment_name = os.environ['CLU_DEPLOYMENT_NAME'] endpoint = os.environ['LANGUAGE_ENDPOINT'] -credential = get_azure_credential() +credential = AzureCliCredential() client = ConversationAuthoringClient(endpoint, credential) diff --git a/infra/scripts/language/cqa_setup.py b/infra/scripts/language/cqa_setup.py index 987cec60..010397d3 100644 --- a/infra/scripts/language/cqa_setup.py +++ b/infra/scripts/language/cqa_setup.py @@ -2,27 +2,15 @@ # Licensed under the MIT License. import os import json -from azure.identity import DefaultAzureCredential, ManagedIdentityCredential +from azure.identity import AzureCliCredential from azure.ai.language.questionanswering.authoring import AuthoringClient -def get_azure_credential(): - use_mi_auth = os.environ.get('USE_MI_AUTH', 'false').lower() == 'true' - - if use_mi_auth: - mi_client_id = os.environ['MI_CLIENT_ID'] - return ManagedIdentityCredential( - client_id=mi_client_id - ) - - return DefaultAzureCredential() - - project_name = os.environ['CQA_PROJECT_NAME'] deployment_name = os.environ['CQA_DEPLOYMENT_NAME'] endpoint = os.environ['LANGUAGE_ENDPOINT'] -credential = get_azure_credential() +credential = AzureCliCredential() client = AuthoringClient(endpoint, credential) diff --git a/infra/scripts/language/orchestration_setup.py b/infra/scripts/language/orchestration_setup.py index cd96f173..7a402cc3 100644 --- a/infra/scripts/language/orchestration_setup.py +++ b/infra/scripts/language/orchestration_setup.py @@ -2,22 +2,10 @@ # Licensed under the MIT License. import os import json -from azure.identity import DefaultAzureCredential, ManagedIdentityCredential +from azure.identity import AzureCliCredential from azure.ai.language.conversations.authoring import ConversationAuthoringClient -def get_azure_credential(): - use_mi_auth = os.environ.get('USE_MI_AUTH', 'false').lower() == 'true' - - if use_mi_auth: - mi_client_id = os.environ['MI_CLIENT_ID'] - return ManagedIdentityCredential( - client_id=mi_client_id - ) - - return DefaultAzureCredential() - - project_name = os.environ['ORCHESTRATION_PROJECT_NAME'] model_name = os.environ['ORCHESTRATION_MODEL_NAME'] deployment_name = os.environ['ORCHESTRATION_DEPLOYMENT_NAME'] @@ -27,7 +15,7 @@ def get_azure_credential(): cqa_project_name = os.environ['CQA_PROJECT_NAME'] endpoint = os.environ['LANGUAGE_ENDPOINT'] -credential = get_azure_credential() +credential = AzureCliCredential() client = ConversationAuthoringClient(endpoint, credential) diff --git a/infra/scripts/language/requirements.txt b/infra/scripts/language/requirements.txt index 8630c96d..0276c945 100644 --- a/infra/scripts/language/requirements.txt +++ b/infra/scripts/language/requirements.txt @@ -1,4 +1,3 @@ azure-identity azure-ai-language-conversations -azure-ai-language-questionanswering -azure-ai-agents \ No newline at end of file +azure-ai-language-questionanswering \ No newline at end of file diff --git a/infra/scripts/language/run_agent_setup.sh b/infra/scripts/language/run_agent_setup.sh deleted file mode 100644 index b15ef505..00000000 --- a/infra/scripts/language/run_agent_setup.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash - -set -e - -cwd=$(pwd) - -if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then - # Script is being sourced - script_dir=$(dirname $(realpath "${BASH_SOURCE[0]}")) -else - # Script is being executed - script_dir=$(dirname $(realpath "$0")) -fi - -cd ${script_dir} - -# Fetch data: -cp ../../data/*.json . -cp ../../openapi_specs/*.json . - -# Run agent setup: -echo "Running agent setup..." -python3 agent_setup.py -echo "Agent setup complete" diff --git a/infra/scripts/language/run_language_setup.sh b/infra/scripts/language/run_language_setup.sh index 4adfa9d4..0f3b5a11 100644 --- a/infra/scripts/language/run_language_setup.sh +++ b/infra/scripts/language/run_language_setup.sh @@ -1,33 +1,25 @@ #!/bin/bash +# `az login` should have been run before executing this script: set -e cwd=$(pwd) +script_dir=$(dirname $(realpath "$0")) +cd ${script_dir} -if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then - # Script is being sourced - script_dir=$(dirname $(realpath "${BASH_SOURCE[0]}")) -else - # Script is being executed - script_dir=$(dirname $(realpath "$0")) +if [ "$SKIP_LANGUAGE_SETUP" = "true" ]; then + echo "Skipping language setup..." + exit 0 fi -cd ${script_dir} +echo "Running language setup..." # Fetch data: -cp ../../data/*.json . -cp ../../openapi_specs/*.json . +cp ../data/*.json . -# Install requirements: -echo "Installing requirements..." python3 -m pip install -r requirements.txt - -# Run setup: -echo "Running CLU setup..." python3 clu_setup.py -echo "Running CQA setup..." python3 cqa_setup.py -echo "Running Orchestration setup..." python3 orchestration_setup.py # Cleanup: diff --git a/infra/scripts/language/utils.py b/infra/scripts/language/utils.py deleted file mode 100644 index e42da01d..00000000 --- a/infra/scripts/language/utils.py +++ /dev/null @@ -1,21 +0,0 @@ -import re - -def bind_parameters(input_string: str, parameters: dict) -> str: - """ - Replace occurrences of '${key}' in the input string with the value of the key in the parameters dictionary. - - :param input_string: The string containing keys of value to replace. - :param parameters: A dictionary containing the values to substitute in the input string. - :return: The modified string with parameters replaced. - """ - if parameters is None: - return input_string - - # Define the regex pattern to match '${key}' - parameter_binding_regex = re.compile(r"\$\{([^}]+)\}") - - # Replace matches with corresponding values from the dictionary - return parameter_binding_regex.sub( - lambda match: parameters.get(match.group(1), match.group(0)), - input_string - ) diff --git a/infra/scripts/postdown_purge_ai_foundry.sh b/infra/scripts/postdown_purge_ai_foundry.sh new file mode 100644 index 00000000..f4389c5d --- /dev/null +++ b/infra/scripts/postdown_purge_ai_foundry.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# `az login` should have been run before executing this script: + +cwd=$(pwd) +script_dir=$(dirname $(realpath "$0")) +cd ${script_dir} + +az account set --subscription "$AZURE_SUBSCRIPTION_ID" + +source ${script_dir}/.env + +echo "Post-down: purging AI Foundry resource..." + +az resource delete --ids /subscriptions/${RG_SUBSCRIPTION_ID}/providers/Microsoft.CognitiveServices/locations/${RG_LOCATION}/resourceGroups/${RG_NAME}/deletedAccounts/${AI_FOUNDRY_NAME} + +cd ${cwd} + +echo "Post-down: AI Foundry resource purged" diff --git a/infra/scripts/postprovision_populate_env.sh b/infra/scripts/postprovision_populate_env.sh new file mode 100644 index 00000000..704db409 --- /dev/null +++ b/infra/scripts/postprovision_populate_env.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# env vars should have been set during provisioning: + +set -e + +cwd=$(pwd) +script_dir=$(dirname $(realpath "$0")) +cd ${script_dir} + +echo "Post-provision: populating .env file..." + +cat << EOF > ${script_dir}/.env +export RG_SUBSCRIPTION_ID="$RG_SUBSCRIPTION_ID" +export RG_LOCATION="$RG_LOCATION" +export RG_NAME="$RG_NAME" +export RG_SUFFIX="$RG_SUFFIX" + +export MI_ID="$MI_ID" +export MI_CLIENT_ID="$MI_CLIENT_ID" + +export LANGUAGE_ENDPOINT="$LANGUAGE_ENDPOINT" +export CLU_PROJECT_NAME="$CLU_PROJECT_NAME" +export CLU_MODEL_NAME="$CLU_MODEL_NAME" +export CLU_DEPLOYMENT_NAME="$CLU_DEPLOYMENT_NAME" +export CLU_CONFIDENCE_THRESHOLD="$CLU_CONFIDENCE_THRESHOLD" +export CLU_API_VERSION="$CLU_API_VERSION" +export CQA_PROJECT_NAME="$CQA_PROJECT_NAME" +export CQA_DEPLOYMENT_NAME="$CQA_DEPLOYMENT_NAME" +export CQA_CONFIDENCE_THRESHOLD="$CQA_CONFIDENCE_THRESHOLD" +export CQA_API_VERSION="$CQA_API_VERSION" +export ORCHESTRATION_PROJECT_NAME="$ORCHESTRATION_PROJECT_NAME" +export ORCHESTRATION_MODEL_NAME="$ORCHESTRATION_MODEL_NAME" +export ORCHESTRATION_DEPLOYMENT_NAME="$ORCHESTRATION_DEPLOYMENT_NAME" +export ORCHESTRATION_CONFIDENCE_THRESHOLD="$ORCHESTRATION_CONFIDENCE_THRESHOLD" +export PII_ENABLED="$PII_ENABLED" +export PII_CATEGORIES="$PII_CATEGORIES" +export PII_CONFIDENCE_THRESHOLD="$PII_CONFIDENCE_THRESHOLD" + +export AI_FOUNDRY_NAME="$AI_FOUNDRY_NAME" + +export AOAI_ENDPOINT="$AOAI_ENDPOINT" +export AOAI_DEPLOYMENT="$AOAI_DEPLOYMENT" +export EMBEDDING_DEPLOYMENT_NAME="$EMBEDDING_DEPLOYMENT_NAME" +export EMBEDDING_MODEL_NAME="$EMBEDDING_MODEL_NAME" +export EMBEDDING_MODEL_DIMENSIONS="$EMBEDDING_MODEL_DIMENSIONS" + +export AGENTS_PROJECT_ENDPOINT="$AGENTS_PROJECT_ENDPOINT" +export MAX_AGENT_RETRY="$MAX_AGENT_RETRY" +export DELETE_OLD_AGENTS="$DELETE_OLD_AGENTS" + +export SEARCH_ENDPOINT="$SEARCH_ENDPOINT" +export SEARCH_INDEX_NAME="$SEARCH_INDEX_NAME" + +export STORAGE_ACCOUNT_NAME="$STORAGE_ACCOUNT_NAME" +export STORAGE_ACCOUNT_CONNECTION_STRING="$STORAGE_ACCOUNT_CONNECTION_STRING" +export BLOB_CONTAINER_NAME="$BLOB_CONTAINER_NAME" + +export ACR_NAME="$ACR_NAME" + +export ROUTER_TYPE="$ROUTER_TYPE" +EOF + +cd ${cwd} + +echo "Post-provision: .env file populated" diff --git a/infra/scripts/postprovision_run_setup.sh b/infra/scripts/postprovision_run_setup.sh new file mode 100644 index 00000000..a42c1811 --- /dev/null +++ b/infra/scripts/postprovision_run_setup.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# `az login` should have been run before executing this script: + +set -e + +cwd=$(pwd) +script_dir=$(dirname $(realpath "$0")) +cd ${script_dir} + +az account set --subscription "$AZURE_SUBSCRIPTION_ID" + +source ${script_dir}/.env + +echo "Post-provision: running setup scripts..." + +bash language/run_language_setup.sh +bash search/run_search_setup.sh +bash agents/run_agents_setup.sh + +cd ${cwd} + +echo "Post-provision: setup scripts complete" diff --git a/infra/scripts/predeploy_create_container.sh b/infra/scripts/predeploy_create_container.sh new file mode 100644 index 00000000..3094e92a --- /dev/null +++ b/infra/scripts/predeploy_create_container.sh @@ -0,0 +1,93 @@ +#!/bin/bash +# `az login` should have been run before executing this script: + +set -e + +cwd=$(pwd) +script_dir=$(dirname $(realpath "$0")) +src_dir="${script_dir}/../../src" +cd $src_dir + +az account set --subscription "$AZURE_SUBSCRIPTION_ID" + +source ${script_dir}/.env + +# Build docker image: +echo "Pre-deploy: building app image..." + +repo="conv-agent" +image="app" +tag=$(date '+%Y%m%d-%H%M') + +# No Docker dependency: +# az acr build \ +# -r ${ACR_NAME} \ +# -t ${repo}/${image}:${tag} \ +# . + +docker build . -t ${ACR_NAME}.azurecr.io/${repo}/${image}:${tag} + +# Push image to ACR: +echo "Pre-deploy: pushing image to acr..." + +az acr login --name ${ACR_NAME} +docker push ${ACR_NAME}.azurecr.io/${repo}/${image}:${tag} + +# Create container instance: +echo "Pre-deploy: creating container instance..." + +result=$(az container create \ + --resource-group ${RG_NAME} \ + --name "ci-${RG_SUFFIX}" \ + --location ${RG_LOCATION} \ + --image ${ACR_NAME}.azurecr.io/${repo}/${image}:${tag} \ + --assign-identity ${MI_ID} \ + --acr-identity ${MI_ID} \ + --restart-policy "Never" \ + --ports 80 \ + --protocol "TCP" \ + --cpu 1 \ + --memory 1 \ + --dns-name-label "conv-agent-app-${RG_SUFFIX}" \ + --os-type "Linux" \ + --ip-address "Public" \ + --environment-variables \ + AGENTS_PROJECT_ENDPOINT=$AGENTS_PROJECT_ENDPOINT \ + USE_MI_AUTH=true \ + MI_CLIENT_ID=$MI_CLIENT_ID \ + AOAI_ENDPOINT=$AOAI_ENDPOINT \ + AOAI_DEPLOYMENT=$AOAI_DEPLOYMENT \ + SEARCH_ENDPOINT=$SEARCH_ENDPOINT \ + SEARCH_INDEX_NAME=$SEARCH_INDEX_NAME \ + EMBEDDING_DEPLOYMENT_NAME=$EMBEDDING_DEPLOYMENT_NAME \ + EMBEDDING_MODEL_NAME=$EMBEDDING_MODEL_NAME \ + EMBEDDING_MODEL_DIMENSIONS=$EMBEDDING_MODEL_DIMENSIONS \ + STORAGE_ACCOUNT_NAME=$STORAGE_ACCOUNT_NAME \ + STORAGE_ACCOUNT_CONNECTION_STRING=$STORAGE_ACCOUNT_CONNECTION_STRING \ + BLOB_CONTAINER_NAME=$BLOB_CONTAINER_NAME \ + LANGUAGE_ENDPOINT=$LANGUAGE_ENDPOINT \ + CLU_PROJECT_NAME=$CLU_PROJECT_NAME \ + CLU_MODEL_NAME=$CLU_MODEL_NAME \ + CLU_DEPLOYMENT_NAME=$CLU_DEPLOYMENT_NAME \ + CLU_CONFIDENCE_THRESHOLD=$CLU_CONFIDENCE_THRESHOLD \ + CQA_PROJECT_NAME=$CQA_PROJECT_NAME \ + CQA_DEPLOYMENT_NAME=$CQA_DEPLOYMENT_NAME \ + CQA_CONFIDENCE_THRESHOLD=$CQA_CONFIDENCE_THRESHOLD \ + ORCHESTRATION_PROJECT_NAME=$ORCHESTRATION_PROJECT_NAME \ + ORCHESTRATION_MODEL_NAME=$ORCHESTRATION_MODEL_NAME \ + ORCHESTRATION_DEPLOYMENT_NAME=$ORCHESTRATION_DEPLOYMENT_NAME \ + ORCHESTRATION_CONFIDENCE_THRESHOLD=$ORCHESTRATION_CONFIDENCE_THRESHOLD \ + PII_ENABLED=$PII_ENABLED \ + PII_CATEGORIES=$PII_CATEGORIES \ + PII_CONFIDENCE_THRESHOLD=$PII_CONFIDENCE_THRESHOLD \ + ROUTER_TYPE=$ROUTER_TYPE \ + MAX_AGENT_RETRY=$MAX_AGENT_RETRY \ + TRIAGE_AGENT_ID=$TRIAGE_AGENT_ID) + +fqdn=$(echo "$result" | grep -m1 '"fqdn": ' "-" | awk '{print $2 }' | tr -d ',"') + +echo -e "\nWeb-App URL: ${fqdn}" + +echo "Pre-deploy: container instance spawned" + +cd ${cwd} diff --git a/infra/scripts/preprovision_validate_parameters.sh b/infra/scripts/preprovision_validate_parameters.sh new file mode 100644 index 00000000..e048a363 --- /dev/null +++ b/infra/scripts/preprovision_validate_parameters.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# `az login` should have been run before executing this script: + +set -e + +if [ "$IS_GITHUB_WORKFLOW_RUN" = "true" ]; then + # Skip parameter validation during GitHub workflow run: + echo "Pre-provision: skipping parameter validation..." + az account set --subscription "$AZURE_SUBSCRIPTION_ID" + exit 0 +fi + +cwd=$(pwd) +script_dir=$(dirname $(realpath "$0")) +cd ${script_dir} + +echo "Pre-provision: validating parameters..." + +selected_subscription_id=$(az account show --query id --output tsv) +if [ "$selected_subscription_id" != "$AZURE_SUBSCRIPTION_ID" ]; then + echo "Subscription selected during authentication does NOT match subscription selected in azd" + echo "$selected_subscription_id != $AZURE_SUBSCRIPTION_ID" + echo "Aborting..." + exit 1 +fi + +# model_region=$(grep -m1 'model_region' ${script_dir}/../parameters.json | awk '{ print $2 }' | tr -d '"') +# if [ -n "$model_region" ] && [ "$model_region" != "$AZURE_LOCATION" ]; then +# echo "Region selected during parameter customization does NOT match region selected in azd" +# echo "$model_region != $AZURE_LOCATION" +# echo "Aborting..." +# exit 1 +# fi + +cd ${cwd} + +echo "Pre-provision: parameters validated" diff --git a/infra/scripts/preup_customize_parameters.sh b/infra/scripts/preup_customize_parameters.sh new file mode 100644 index 00000000..b07431f3 --- /dev/null +++ b/infra/scripts/preup_customize_parameters.sh @@ -0,0 +1,269 @@ +#!/bin/bash +# `az login` should have been run before executing this script: + +set -e + +cwd=$(pwd) +script_dir=$(dirname $(realpath "$0")) +cd ${script_dir} + +selected_subscription=$(az account show --query name --output tsv) +model_region=$(grep -m1 'model_region' ${script_dir}/../parameters.json | awk '{ print $2 }' | tr -d '"') + +function print_summary { + echo -e "--------------------------\nSUMMARY:" + echo "Subscription: $selected_subscription" + echo "Parameters:" + cat ${script_dir}/../parameters.json + + echo "ENSURE THAT YOU SELECT THE FOLLOWING SUBSCRIPTION: ${selected_subscription}" + echo "ENSURE THAT YOU SELECT THE FOLLOWING REGION: ${model_region}" +} + +function generate_parameters { + cat << EOF > ${script_dir}/../parameters.json +{ + "router_type": "$AZD_PARAM_ROUTER_TYPE", + "gpt_model_name": "$AZD_PARAM_GPT_MODEL_NAME", + "gpt_model_deployment_type": "$AZD_PARAM_GPT_MODEL_DEPLOYMENT_TYPE", + "gpt_model_capacity": "$AZD_PARAM_GPT_MODEL_CAPACITY", + "embedding_model_name": "$AZD_PARAM_EMBEDDING_MODEL_NAME", + "embedding_model_deployment_type": "$AZD_PARAM_EMBEDDING_MODEL_DEPLOYMENT_TYPE", + "embedding_model_capacity": "$AZD_PARAM_EMBEDDING_MODEL_CAPACITY", + "model_region": "$model_region" +} +EOF +} + +if [ "$IS_GITHUB_WORKFLOW_RUN" = "true" ]; then + # Skip parameter customization during GitHub workflow run: + echo "Pre-up: using configured workflow variables..." + generate_parameters + cd ${cwd} + exit 0 +fi + +read -p "Pre-up: would you like to skip parameter customization and use the values found in parameters.json? (y/n): " user_response +if [ "$user_response" = "y" ]; then + echo "Pre-up: skipping parameter customization..." + print_summary + cd ${cwd} + exit 0 +fi + +echo "Pre-up: customizing parameters..." + +declare -a routers=( + "BYPASS" + "CLU" + "CQA" + "ORCHESTRATION" + "FUNCTION_CALLING" + "TRIAGE_AGENT" +) + +declare -a regions=( + "australiaeast" + "centralindia" + "eastus" + "eastus2" + "northeurope" + "southcentralus" + "switzerlandnorth" + "uksouth" + "westeurope" + "westus2" + "westus3" +) + +declare -a models=( + "OpenAI.GlobalStandard.gpt-4o" + "OpenAI.GlobalStandard.gpt-4o-mini" + "OpenAI.GlobalStandard.text-embedding-3-small" + "OpenAI.GlobalStandard.text-embedding-ada-002" + "OpenAI.Standard.gpt-4o" + "OpenAI.Standard.gpt-4o-mini" + "OpenAI.Standard.text-embedding-3-small" + "OpenAI.Standard.text-embedding-ada-002" +) + +declare -A available_regions + +# Fetch quota information per region per model: +for region in "${regions[@]}"; do + echo "----------------------------------------" + echo "Checking region: $region" + + quota_info="$(az cognitiveservices usage list --location "$region" --output json)" + + if [ -z "$quota_info" ]; then + echo "WARNING: failed to retrieve quota information for region $region. Skipping." + continue + fi + + gpt_available="false" + embedding_available="false" + region_model_info="" + + for model in "${models[@]}"; do + model_info="$(echo "$quota_info" | awk -v model="\"value\": \"$model\"" ' + BEGIN { RS="},"; FS="," } + $0 ~ model { print $0 } + ')" + + if [ -z "$model_info" ]; then + echo "WARNING: no quota information found for model $model in region $region. Skipping." + continue + fi + + current_value="$(echo "$model_info" | awk -F': ' '/"currentValue"/ {print $2}' | tr -d ',' | tr -d ' ')" + limit="$(echo "$model_info" | awk -F': ' '/"limit"/ {print $2}' | tr -d ',' | tr -d ' ')" + + current_value="$(echo "${current_value:-0}" | cut -d'.' -f1)" + limit="$(echo "${limit:-0}" | cut -d'.' -f1)" + available=$(($limit - $current_value)) + + if [ "$available" -gt 0 ]; then + region_model_info+="$model=$available " + if grep -q "gpt" <<< "$model"; then + gpt_available="true" + elif grep -q "embedding" <<< "$model"; then + embedding_available="true" + fi + fi + + echo "Model: $model | Used: $current_value | Limit: $limit | Available: $available" + done + + if [ "$gpt_available" = "true" ] && [ "$embedding_available" = "true" ]; then + available_regions[$region]="$region_model_info" + fi +done + +# Select region: +while true; do + echo -e "\nAvailable regions: " + for region_option in "${!available_regions[@]}"; do + echo "-> $region_option" + done + + read -p "Select a region: " selected_region + if [[ -v available_regions[$selected_region] ]]; then + break + else + echo "Invalid selection" + fi +done + +# Get model information from selected region: +declare -A available_gpt_models +declare -A available_embedding_models +region_model_info="${available_regions[$selected_region]}" + +for model_info in $region_model_info; do + model_name="$(echo "$model_info" | cut -d "=" -f1)" + available_quota="$(echo "$model_info" | cut -d "=" -f2)" + + if grep -q "gpt" <<< "$model_name"; then + available_gpt_models[$model_name]="$available_quota" + elif grep -q "embedding" <<< "$model_name"; then + available_embedding_models[$model_name]="$available_quota" + fi + done + +# Select GPT model: +while true; do + echo -e "\nAvailable GPT models in $selected_region:" + for model_option in "${!available_gpt_models[@]}"; do + available_quota=${available_gpt_models[$model_option]} + echo "-> $model_option ($available_quota quota available)" + done + + read -p "Select a GPT model: " selected_gpt_model + if [[ -v available_gpt_models[$selected_gpt_model] ]]; then + break + else + echo "Invalid selection" + fi +done + +# Select GPT model quota: +while true; do + available_quota=${available_gpt_models[$selected_gpt_model]} + echo -e "\nAvailable quota for $selected_gpt_model in $selected_region: $available_quota" + + read -p "Select capacity for $selected_gpt_model deployment: " selected_gpt_quota + + if [ 0 -lt $selected_gpt_quota ] && [ $selected_gpt_quota -le $available_quota ]; then + break + else + echo "Invalid selection" + fi +done + +# Select embedding model: +while true; do + echo -e "\nAvailable embedding models in $selected_region:" + for model_option in "${!available_embedding_models[@]}"; do + available_quota=${available_embedding_models[$model_option]} + echo "-> $model_option ($available_quota quota available)" + done + + read -p "Select an embedding model: " selected_embedding_model + if [[ -v available_embedding_models[$selected_embedding_model] ]]; then + break + else + echo "Invalid selection" + fi +done + +# Select embedding model quota: +while true; do + available_quota=${available_embedding_models[$selected_embedding_model]} + echo -e "\nAvailable quota for $selected_embedding_model in $selected_region: $available_quota" + + read -p "Select capacity for $selected_embedding_model deployment: " selected_embedding_quota + + if [ 0 -lt $selected_embedding_quota ] && [ $selected_embedding_quota -le $available_quota ]; then + break + else + echo "Invalid selection" + fi +done + +# Select router type: +while true; do + echo -e "\nAvailable router types:" + for router in "${routers[@]}"; do + echo "-> $router" + done + + read -p "Select a router type: " selected_router_type + if [[ ${routers[@]} =~ $selected_router_type ]]; then + break + else + echo "Invalid selection" + fi +done + +# Set variables: +gpt_model_name=$(echo "$selected_gpt_model" | cut -d "." -f3) +gpt_model_deployment_type=$(echo "$selected_gpt_model" | cut -d "." -f2) + +embedding_model_name=$(echo "$selected_embedding_model" | cut -d "." -f3) +embedding_model_deployment_type=$(echo "$selected_embedding_model" | cut -d "." -f2) + +AZD_PARAM_ROUTER_TYPE="$selected_router_type" +AZD_PARAM_GPT_MODEL_NAME="$gpt_model_name" +AZD_PARAM_GPT_MODEL_DEPLOYMENT_TYPE="$gpt_model_deployment_type" +AZD_PARAM_GPT_MODEL_CAPACITY="$selected_gpt_quota" +AZD_PARAM_EMBEDDING_MODEL_NAME="$embedding_model_name" +AZD_PARAM_EMBEDDING_MODEL_DEPLOYMENT_TYPE="$embedding_model_deployment_type" +AZD_PARAM_EMBEDDING_MODEL_CAPACITY="$selected_embedding_quota" + +model_region="$selected_region" + +generate_parameters +print_summary + +cd ${cwd} diff --git a/infra/scripts/run_container_app.sh b/infra/scripts/run_container_app.sh deleted file mode 100644 index 40d4488e..00000000 --- a/infra/scripts/run_container_app.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/bash - -set -e - -cwd=$(pwd) -script_dir=$(dirname $(realpath "$0")) -src_dir="${script_dir}/../../src" -frontend_dir="${src_dir}/frontend" -backend_dir="${src_dir}/backend" - -cd ${script_dir} - -# Authenticate: -az login --identity - -# Ensure pip: -python3 -m ensurepip --upgrade - -# Install deps: -tdnf install -y tar -tdnf install -y awk - -# Install nodejs: -curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.2/install.sh | bash -\. "$HOME/.nvm/nvm.sh" -nvm install 22 -node -v -nvm current -npm -v - -# Run setup: -export CONFIG_DIR="$(pwd)/config_dir" -mkdir -p $CONFIG_DIR -echo "Running setup..." -source language/run_language_setup.sh -bash search/run_search_setup.sh ${STORAGE_ACCOUNT_NAME} ${BLOB_CONTAINER_NAME} -source language/run_agent_setup.sh - -# Build UI: -echo "Building UI..." -cd ${frontend_dir} -npm install -npm run build - -# Run app: -echo "Running uvicorn app..." -cd ${backend_dir} -python3 -m pip install -r requirements.txt -cd src -cp -r ${frontend_dir}/dist . - -# Run the uvicorn server -python3 -m uvicorn app:app --host 0.0.0.0 --port 8000 diff --git a/infra/scripts/search/README.md b/infra/scripts/search/README.md deleted file mode 100644 index 7c1e9845..00000000 --- a/infra/scripts/search/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# Conversational-Agent: Search Index Setup - -## Environment Variables -Expected environment variables: -``` -AOAI_ENDPOINT= -EMBEDDING_DEPLOYMENT_NAME= -EMBEDDING_MODEL_NAME= -EMBEDDING_MODEL_DIMENSIONS= - -STORAGE_ACCOUNT_CONNECTION_STRING= -BLOB_CONTAINER_NAME= - -SEARCH_ENDPOINT= -SEARCH_INDEX_NAME= -``` - -## Running Setup (local) -``` -az login -bash run_search_setup.sh -``` \ No newline at end of file diff --git a/infra/scripts/search/index_setup.py b/infra/scripts/search/index_setup.py index bb4b25aa..17f38588 100644 --- a/infra/scripts/search/index_setup.py +++ b/infra/scripts/search/index_setup.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. import os -from azure.identity import DefaultAzureCredential, ManagedIdentityCredential +from azure.identity import AzureCliCredential from azure.search.documents.indexes import SearchIndexClient, SearchIndexerClient from azure.search.documents.indexes.models import ( SearchField, @@ -27,16 +27,6 @@ FieldMapping ) -def get_azure_credential(): - use_mi_auth = os.environ.get('USE_MI_AUTH', 'false').lower() == 'true' - - if use_mi_auth: - mi_client_id = os.environ['MI_CLIENT_ID'] - return ManagedIdentityCredential( - client_id=mi_client_id - ) - - return DefaultAzureCredential() aoai_endpoint = os.environ['AOAI_ENDPOINT'] embedding_deployment_name = os.environ['EMBEDDING_DEPLOYMENT_NAME'] @@ -52,7 +42,7 @@ def get_azure_credential(): indexer_name = index_name + '-idxr' endpoint = os.environ['SEARCH_ENDPOINT'] -credential = get_azure_credential() +credential = AzureCliCredential() # Search index: index_client = SearchIndexClient(endpoint=endpoint, credential=credential) diff --git a/infra/scripts/search/run_search_setup.sh b/infra/scripts/search/run_search_setup.sh index 9eb8d052..4b6913a9 100644 --- a/infra/scripts/search/run_search_setup.sh +++ b/infra/scripts/search/run_search_setup.sh @@ -1,4 +1,5 @@ #!/bin/bash +# `az login` should have been run before executing this script: set -e @@ -7,12 +8,10 @@ cwd=$(pwd) script_dir=$(dirname $(realpath "$0")) cd ${script_dir} -# Arguments: -storage_account_name=$1 -blob_container_name=$2 +echo "Running search setup..." # Fetch data: -cp ../../data/${product_info_file} . +cp ../data/${product_info_file} . # Unzip data: mkdir product_info && mv ${product_info_file} product_info/ @@ -22,18 +21,13 @@ cd product_info && tar -xvzf ${product_info_file} && cd .. echo "Uploading files to blob container..." az storage blob upload-batch \ --auth-mode login \ - --destination ${blob_container_name} \ - --account-name ${storage_account_name} \ + --destination ${BLOB_CONTAINER_NAME} \ + --account-name ${STORAGE_ACCOUNT_NAME} \ --source "product_info" \ --pattern "*.md" \ --overwrite -# Install requirements: -echo "Installing requirements..." python3 -m pip install -r requirements.txt - -# Run setup: -echo "Running index setup..." python3 index_setup.py # Cleanup: diff --git a/infra/setup_azd_parameters.sh b/infra/setup_azd_parameters.sh deleted file mode 100644 index d65e2b74..00000000 --- a/infra/setup_azd_parameters.sh +++ /dev/null @@ -1,196 +0,0 @@ -#!/bin/bash - -declare -a regions=( - "australiaeast" - "centralindia" - "eastus" - "eastus2" - "northeurope" - "southcentralus" - "switzerlandnorth" - "uksouth" - "westeurope" - "westus2" - "westus3" -) - -declare -a models=( - "OpenAI.GlobalStandard.gpt-4o" - "OpenAI.GlobalStandard.gpt-4o-mini" - "OpenAI.GlobalStandard.text-embedding-3-small" - "OpenAI.GlobalStandard.text-embedding-ada-002" - "OpenAI.Standard.gpt-4o" - "OpenAI.Standard.gpt-4o-mini" - "OpenAI.Standard.text-embedding-3-small" - "OpenAI.Standard.text-embedding-ada-002" -) - -declare -A valid_regions - -# Fetch quota information per region per model: -for region in "${regions[@]}"; do - echo "----------------------------------------" - echo "Checking region: $region" - - quota_info="$(az cognitiveservices usage list --location "$region" --output json)" - - if [ -z "$quota_info" ]; then - echo "WARNING: failed to retrieve quota information for region $region. Skipping." - continue - fi - - gpt_available="false" - embedding_available="false" - region_quota_info="" - - for model in "${models[@]}"; do - model_info="$(echo "$quota_info" | awk -v model="\"value\": \"$model\"" ' - BEGIN { RS="},"; FS="," } - $0 ~ model { print $0 } - ')" - - if [ -z "$model_info" ]; then - echo "WARNING: no quota information found for model $model in region $region. Skipping." - continue - fi - - current_value="$(echo "$model_info" | awk -F': ' '/"currentValue"/ {print $2}' | tr -d ',' | tr -d ' ')" - limit="$(echo "$model_info" | awk -F': ' '/"limit"/ {print $2}' | tr -d ',' | tr -d ' ')" - - current_value="$(echo "${current_value:-0}" | cut -d'.' -f1)" - limit="$(echo "${limit:-0}" | cut -d'.' -f1)" - available=$(($limit - $current_value)) - - if [ "$available" -gt 0 ]; then - region_quota_info+="$model=$available " - if grep -q "gpt" <<< "$model"; then - gpt_available="true" - elif grep -q "embedding" <<< "$model"; then - embedding_available="true" - fi - fi - - echo "Model: $model | Used: $current_value | Limit: $limit | Available: $available" - done - - if [ "$gpt_available" = "true" ] && [ "$embedding_available" = "true" ]; then - valid_regions[$region]="$region_quota_info" - fi -done - -# Select region: -while true; do - echo -e "\nAvailable regions: " - for region_option in "${!valid_regions[@]}"; do - echo "-> $region_option" - done - - read -p "Select a region: " selected_region - if [[ -v valid_regions[$selected_region] ]]; then - break - else - echo "Invalid selection" - fi -done - -# Get model information from selected region: -declare -A valid_gpt_models -declare -A valid_embedding_models -region_quota_info="${valid_regions[$selected_region]}" - -for model_info in $region_quota_info; do - model_name="$(echo "$model_info" | cut -d "=" -f1)" - available="$(echo "$model_info" | cut -d "=" -f2)" - - if grep -q "gpt" <<< "$model_name"; then - valid_gpt_models[$model_name]="$available" - elif grep -q "embedding" <<< "$model_name"; then - valid_embedding_models[$model_name]="$available" - fi - done - -# Select GPT model: -while true; do - echo -e "\nAvailable GPT models in $selected_region:" - for model_option in "${!valid_gpt_models[@]}"; do - echo "-> $model_option (${valid_gpt_models[$model_option]} quota available)" - done - - read -p "Select a GPT model: " selected_gpt_model - if [[ -v valid_gpt_models[$selected_gpt_model] ]]; then - break - else - echo "Invalid selection" - fi -done - -# Select GPT model quota: -while true; do - available=${valid_gpt_models[$selected_gpt_model]} - echo -e "\nAvailable quota for $selected_gpt_model in $selected_region: $available" - - read -p "Select capacity for $selected_gpt_model deployment: " selected_gpt_quota - - if [ 0 -lt $selected_gpt_quota ] && [ $selected_gpt_quota -le $available ]; then - break - else - echo "Invalid selection" - fi -done - -# Select embedding model: -while true; do - echo -e "\nAvailable embedding models in $selected_region:" - for model_option in "${!valid_embedding_models[@]}"; do - echo "-> $model_option (${valid_embedding_models[$model_option]} quota available)" - done - - read -p "Select an embedding model: " selected_embedding_model - if [[ -v valid_embedding_models[$selected_embedding_model] ]]; then - break - else - echo "Invalid selection" - fi -done - -# Select embedding model quota: -while true; do - available=${valid_embedding_models[$selected_embedding_model]} - echo -e "\nAvailable quota for $selected_embedding_model in $selected_region: $available" - - read -p "Select capacity for $selected_embedding_model deployment: " selected_embedding_quota - - if [ 0 -lt $selected_embedding_quota ] && [ $selected_embedding_quota -le $available ]; then - break - else - echo "Invalid selection" - fi -done - -# Fetch summary: -gpt_model_name=$(echo "$selected_gpt_model" | cut -d "." -f3) -gpt_deployment_type=$(echo "$selected_gpt_model" | cut -d "." -f2) - -embedding_model_name=$(echo "$selected_embedding_model" | cut -d "." -f3) -embedding_deployment_type=$(echo "$selected_embedding_model" | cut -d "." -f2) - -echo -e "\n--------------------------\nSummary:" -echo "Region: $selected_region" -echo "GPT model name: $gpt_model_name" -echo "GPT model deployment type: $gpt_deployment_type" -echo "GPT model capacity: $selected_gpt_quota" -echo "Embedding model name: $embedding_model_name" -echo "Embedding model deployment type: $embedding_deployment_type" -echo "Embedding model capacity: $selected_embedding_quota" - -# Set AZD env variables: -export AZURE_ENV_GPT_MODEL_NAME=$gpt_model_name -export AZURE_ENV_GPT_MODEL_CAPACITY=$selected_gpt_quota -export AZURE_ENV_GPT_MODEL_DEPLOYMENT_TYPE=$gpt_deployment_type - -export AZURE_ENV_EMBEDDING_MODEL_NAME=$embedding_model_name -export AZURE_ENV_EMBEDDING_MODEL_CAPACITY=$selected_embedding_quota -export AZURE_ENV_EMBEDDING_MODEL_DEPLOYMENT_TYPE=$embedding_deployment_type - -echo -e "\nazd parameters set" -echo "Ensure that you deploy to $selected_region when running: azd up" diff --git a/src/Dockerfile b/src/Dockerfile index 0731ab53..01757f73 100644 --- a/src/Dockerfile +++ b/src/Dockerfile @@ -10,7 +10,7 @@ COPY frontend/index.html /app RUN npm install RUN npm run build -FROM mcr.microsoft.com/azurelinux/base/python:3 +FROM mcr.microsoft.com/azurelinux/base/python:3.12 WORKDIR /app @@ -21,6 +21,6 @@ COPY backend/requirements.txt /app RUN pip install -r requirements.txt -EXPOSE 7000 +EXPOSE 80 -CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7000"] +CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "80"] diff --git a/src/README.md b/src/README.md deleted file mode 100644 index ad880b3e..00000000 --- a/src/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# Azure-Language-OpenAI-Conversational-Agent-Accelerator - -## Environment Variables: -Expected environment variables: -``` -AOAI_ENDPOINT= -AOAI_DEPLOYMENT= - -SEARCH_ENDPOINT= -SEARCH_INDEX_NAME= - -LANGUAGE_ENDPOINT= - -CLU_PROJECT_NAME= -CLU_DEPLOYMENT_NAME= -CLU_CONFIDENCE_THRESHOLD= # float - -CQA_PROJECT_NAME= -CQA_DEPLOYMENT_NAME=production # default -CQA_CONFIDENCE_THRESHOLD= # float - -ORCHESTRATION_PROJECT_NAME= -ORCHESTRATION_DEPLOYMENT_NAME= -ORCHESTRATION_CONFIDENCE_THRESHOLD= # float - -PII_ENABLED= # bool -PII_CATEGORIES= # comma-separated -PII_CONFIDENCE_THRESHOLD= # float - -ROUTER_TYPE= # BYPASS | CLU | CQA | ORCHESTRATION | FUNCTION_CALLING - -USE_MI_AUTH= # bool, false for local runs (run az login beforehand) -MI_CLIENT_ID= -``` - -## Running App -``` -cd frontend -npm install -npm run build - -cd ../backend -pip install -r requirements.txt -cd src -mv ../../frontend/dist . - -flask --app server run --host=0.0.0.0 --port 7000 -``` \ No newline at end of file diff --git a/src/backend/src/agents/order_cancel_plugin.py b/src/backend/src/agents/order_cancel_plugin.py index 614e4197..203690e7 100644 --- a/src/backend/src/agents/order_cancel_plugin.py +++ b/src/backend/src/agents/order_cancel_plugin.py @@ -2,14 +2,15 @@ # Licensed under the MIT License. from semantic_kernel.functions import kernel_function -""" -Sample plugin for processing cancellations in a customer support system - this plugin simulates the cancellation process -and is used with a chat completion agent in a handoff orchestration system. -""" -class OrderCancellationPlugin: + +class OrderCancelPlugin: + """ + Sample plugin for processing cancellations in a customer support system - this plugin simulates the cancellation process + and is used with a chat completion agent in a handoff orchestration system. + """ @kernel_function def process_cancellation(self, order_id: str) -> str: """Process a cancellation for an order.""" # Simulate processing a cancellation print(f"[CancellationPlugin] Processing cancellation for order {order_id}") - return f"Cancellation for order {order_id} has been processed successfully." \ No newline at end of file + return f"Cancellation for order {order_id} has been processed successfully." diff --git a/src/backend/src/agents/order_refund_plugin.py b/src/backend/src/agents/order_refund_plugin.py index 65065ec8..eb5005c0 100644 --- a/src/backend/src/agents/order_refund_plugin.py +++ b/src/backend/src/agents/order_refund_plugin.py @@ -2,14 +2,15 @@ # Licensed under the MIT License. from semantic_kernel.functions import kernel_function -""" -Sample plugin for processing refunds in a customer support system - this plugin simulates the refund process -and is used with a chat completion agent in a handoff orchestration system. -""" + class OrderRefundPlugin: + """ + Sample plugin for processing refunds in a customer support system - this plugin simulates the refund process + and is used with a chat completion agent in a handoff orchestration system. + """ @kernel_function def process_refund(self, order_id: str) -> str: """Process a refund for an order.""" # Simulate processing a refund print(f"[RefundPlugin] Processing refund for order {order_id}") - return f"Refund for order {order_id} has been processed successfully." \ No newline at end of file + return f"Refund for order {order_id} has been processed successfully." diff --git a/src/backend/src/agents/order_status_plugin.py b/src/backend/src/agents/order_status_plugin.py index 8e1f195e..10d4549a 100644 --- a/src/backend/src/agents/order_status_plugin.py +++ b/src/backend/src/agents/order_status_plugin.py @@ -2,13 +2,14 @@ # Licensed under the MIT License. from semantic_kernel.functions import kernel_function -""" -Sample plugin for returning order status in a customer support system - this plugin states order status -and is used with a chat completion agent in a handoff orchestration system. -""" + class OrderStatusPlugin: + """ + Sample plugin for returning order status in a customer support system - this plugin states order status + and is used with a chat completion agent in a handoff orchestration system. + """ @kernel_function def check_order_status(self, order_id: str) -> str: """Check the status of an order.""" print(f"[OrderStatusPlugin] Checking status for order {order_id}") - return f"Order {order_id} is shipped and will arrive in 2-3 days." \ No newline at end of file + return f"Order {order_id} is shipped and will arrive in 2-3 days." diff --git a/src/backend/src/app.py b/src/backend/src/app.py index 50599185..2a374cab 100644 --- a/src/backend/src/app.py +++ b/src/backend/src/app.py @@ -11,62 +11,38 @@ from fastapi.responses import JSONResponse, FileResponse from pydantic import BaseModel from semantic_kernel_orchestrator import SemanticKernelOrchestrator -from azure.identity.aio import DefaultAzureCredential from semantic_kernel.agents import AzureAIAgent +from azure.identity.aio import DefaultAzureCredential from utils import get_azure_credential from aoai_client import AOAIClient, get_prompt - from azure.search.documents import SearchClient -# Run locally with `uvicorn app:app --reload --host 127.0.0.1 --port 7000` +# Run locally with `uvicorn app:app --reload --host 127.0.0.1 --port 80` # Comment out for local testing: # from dotenv import load_dotenv # load_dotenv() # Environment variables -PROJECT_ENDPOINT = os.environ.get("AGENTS_PROJECT_ENDPOINT") -MODEL_NAME = os.environ.get("AOAI_DEPLOYMENT") -CONFIG_DIR = os.environ.get("CONFIG_DIR", ".") -config_file = os.path.join(CONFIG_DIR, "config.json") - -# Read config.json file from the config directory -if os.path.exists(config_file): - with open(config_file, "r") as f: - AGENT_IDS = json.load(f) -else: - AGENT_IDS = {} +AGENTS_PROJECT_ENDPOINT = os.environ.get("AGENTS_PROJECT_ENDPOINT") +AGENTS_MODEL_NAME = os.environ.get("AOAI_DEPLOYMENT") +AGENT_IDS = { + "TRIAGE_AGENT_ID": os.environ.get("TRIAGE_AGENT_ID"), + "HEAD_SUPPORT_AGENT_ID": os.environ.get("HEAD_SUPPORT_AGENT_ID"), + "ORDER_STATUS_AGENT_ID": os.environ.get("ORDER_STATUS_AGENT_ID"), + "ORDER_CANCEL_AGENT_ID": os.environ.get("ORDER_CANCEL_AGENT_ID"), + "ORDER_REFUND_AGENT_ID": os.environ.get("ORDER_REFUND_AGENT_ID"), +} -# Comment out for local testing: -# AGENT_IDS = { -# "TRIAGE_AGENT_ID": os.environ.get("TRIAGE_AGENT_ID"), -# "HEAD_SUPPORT_AGENT_ID": os.environ.get("HEAD_SUPPORT_AGENT_ID"), -# "ORDER_STATUS_AGENT_ID": os.environ.get("ORDER_STATUS_AGENT_ID"), -# "ORDER_CANCEL_AGENT_ID": os.environ.get("ORDER_CANCEL_AGENT_ID"), -# "ORDER_REFUND_AGENT_ID": os.environ.get("ORDER_REFUND_AGENT_ID"), -# } - -# Check if all required agent IDs are present -required_agents = [ - "TRIAGE_AGENT_ID", - "HEAD_SUPPORT_AGENT_ID", - "ORDER_STATUS_AGENT_ID", - "ORDER_CANCEL_AGENT_ID", - "ORDER_REFUND_AGENT_ID" -] - -missing_agents = [agent for agent in required_agents if not AGENT_IDS.get(agent)] -if missing_agents: - error_msg = f"Missing required agent IDs: {', '.join(missing_agents)}" - logging.error(error_msg) - raise ValueError(error_msg) DIST_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "dist")) # log dist_dir print(f"DIST_DIR: {DIST_DIR}") + class ChatRequest(BaseModel): message: str + # Initialize the Azure Search client search_client = SearchClient( endpoint=os.environ.get("SEARCH_ENDPOINT"), @@ -96,6 +72,7 @@ class ChatRequest(BaseModel): PII_ENABLED = os.environ.get("PII_ENABLED", "false").lower() == "true" print(f"PII_ENABLED: {PII_ENABLED}") + # Fallback function (RAG) definition: def fallback_function( query: str, @@ -116,6 +93,7 @@ def fallback_function( return rag_client.chat_completion(query) + # Function to handle processing and orchestrating a chat message with utterance extraction, fallback handling, and PII redaction async def orchestrate_chat(message: str, orchestrator: SemanticKernelOrchestrator, chat_id: int) -> list[str]: responses = [] @@ -156,7 +134,7 @@ async def orchestrate_chat(message: str, orchestrator: SemanticKernelOrchestrato # Try semantic kernel orchestration first orchestrator = app.state.orchestrator response = await orchestrator.process_message(utterance) - + if isinstance(response, dict) and response.get("error"): # If semantic kernel fails, use fallback print(f"Semantic kernel failed, using fallback for: {utterance}") @@ -184,23 +162,24 @@ async def orchestrate_chat(message: str, orchestrator: SemanticKernelOrchestrato return responses + @asynccontextmanager async def lifespan(app: FastAPI): # Setup app try: logging.basicConfig(level=logging.WARNING) print("Setting up Azure credentials and client...") - print(f"Using PROJECT_ENDPOINT: {PROJECT_ENDPOINT}") - print(f"Using MODEL_NAME: {MODEL_NAME}") + print(f"Using AGENTS_PROJECT_ENDPOINT: {AGENTS_PROJECT_ENDPOINT}") + print(f"Using AGENTS_MODEL_NAME: {AGENTS_MODEL_NAME}") - async with DefaultAzureCredential(exclude_interactive_browser_credential=False) as creds: - async with AzureAIAgent.create_client(credential=creds, endpoint=PROJECT_ENDPOINT) as client: + async with DefaultAzureCredential() as creds: + async with AzureAIAgent.create_client(credential=creds, endpoint=AGENTS_PROJECT_ENDPOINT) as client: orchestrator = SemanticKernelOrchestrator( - client, - MODEL_NAME, - PROJECT_ENDPOINT, - AGENT_IDS, - fallback_function, + client, + AGENTS_MODEL_NAME, + AGENTS_PROJECT_ENDPOINT, + AGENT_IDS, + fallback_function, 3 ) await orchestrator.create_agent_group_chat() @@ -222,10 +201,12 @@ async def lifespan(app: FastAPI): await client.__aexit__(None, None, None) await creds.__aexit__(None, None, None) + # Create FastAPI app with lifespan app = FastAPI(lifespan=lifespan) app.mount("/assets", StaticFiles(directory=os.path.join(DIST_DIR, "assets")), name="assets") + # In order to test uvicorn app locally: # 1) run `npm run build` in the frontend directory to generate the static files # 2) move the `dist` directory to `src/backend/src/` @@ -233,6 +214,7 @@ async def lifespan(app: FastAPI): async def serve_frontend(): return FileResponse(os.path.join(DIST_DIR, "index.html")) + # Define the chat endpoint @app.post("/chat") async def chat_endpoint(request: ChatRequest): @@ -241,7 +223,7 @@ async def chat_endpoint(request: ChatRequest): orchestrator = app.state.orchestrator responses = await orchestrate_chat(request.message, orchestrator, chat_id=0) return JSONResponse(content={"messages": responses}, status_code=200) - + except Exception as e: logging.error(f"Error in chat endpoint: {e}") return JSONResponse( diff --git a/src/backend/src/router/cqa_router.py b/src/backend/src/router/cqa_router.py index 960c326d..311d15b2 100644 --- a/src/backend/src/router/cqa_router.py +++ b/src/backend/src/router/cqa_router.py @@ -95,25 +95,29 @@ def parse_response( Parse CQA runtime response (JSON output). """ confidence_threshold = float(os.environ.get("CQA_CONFIDENCE_THRESHOLD", "0.5")) - top_answer = response["answers"][0] - confidence = top_answer["confidenceScore"] - answer = top_answer["answer"] - answer_id = top_answer["id"] + + answer = None question = None + confidence = None error = None - # Filter based on confidence threshold: - if confidence < confidence_threshold: - _logger.warning("CQA confidence threshold not met") - error = "CQA confidence threshold not met" - # Filter based on answer id: - if answer_id == -1: + if len(response["answers"]) == 0 or response["answers"][0]["id"] == -1: # -1 means default answer was returned. _logger.warning("No answer found") error = "No answer found" + + # Filter based on confidence threshold: + elif response["answers"][0]["confidenceScore"] < confidence_threshold: + _logger.warning("CQA confidence threshold not met") + error = "CQA confidence threshold not met" + + # Happy path: else: + top_answer = response["answers"][0] + answer = top_answer["answer"] question = top_answer["questions"][0] + confidence = top_answer["confidenceScore"] return { "kind": "cqa_result", diff --git a/src/backend/src/router/triage_agent_router.py b/src/backend/src/router/triage_agent_router.py index 51209cb7..3768b57d 100644 --- a/src/backend/src/router/triage_agent_router.py +++ b/src/backend/src/router/triage_agent_router.py @@ -41,12 +41,12 @@ def triage_agent_router( """ # Process the agent run and handle retries max_retries = int(os.environ.get("MAX_AGENT_RETRY", 3)) - + # Initialize error return value error_return_value = { "error": ValueError("The run did not complete successfully.") } - + # Create thread and process agent run with retries for attempt in range(1, max_retries + 1): try: @@ -67,7 +67,7 @@ def triage_agent_router( _logger.error(f"Logging error {e}") error_return_value["error"] = e _logger.error(f"Agent run {attempt + 1} failed with exception: {e}. Retrying...") - + # If all attempts fail, return the error return error_return_value @@ -92,12 +92,13 @@ def create_thread( content=utterance, ) _logger.info(f"Created message: {message['id']}") - + return thread + def handle_successful_run( agents_client: AgentsClient, - thread: AgentThread, + thread: AgentThread, attempt: int ) -> dict: """ @@ -118,16 +119,17 @@ def handle_successful_run( _logger.info(f"Agent response parsed successfully: {data}") parsed_result = parse_response(data) return parsed_result - + # Raise error if agent response cannot be parsed except Exception as e: _logger.error(f"Agent response failed with error: {e}") raise ValueError(f"Failed to parse agent response: {e}") - + # If no valid response found, raise an error to be handled by the caller _logger.error("No valid agent response found in the thread.") raise ValueError("No valid agent response found in the thread.") + def parse_response( response: dict ) -> dict: @@ -156,4 +158,3 @@ def parse_response( parsed_result["api_response"] = response["response"] return parsed_result - diff --git a/src/backend/src/semantic_kernel_orchestrator.py b/src/backend/src/semantic_kernel_orchestrator.py index b96ae739..805948c4 100644 --- a/src/backend/src/semantic_kernel_orchestrator.py +++ b/src/backend/src/semantic_kernel_orchestrator.py @@ -6,7 +6,7 @@ from semantic_kernel.agents.strategies import TerminationStrategy, SequentialSelectionStrategy from agents.order_status_plugin import OrderStatusPlugin from agents.order_refund_plugin import OrderRefundPlugin -from agents.order_cancel_plugin import OrderCancellationPlugin +from agents.order_cancel_plugin import OrderCancelPlugin from semantic_kernel.contents import AuthorRole, ChatMessageContent from azure.ai.projects import AIProjectClient from typing import Callable @@ -124,7 +124,7 @@ def __init__( # Initialize plugins for custom agents self.order_status_plugin = OrderStatusPlugin() self.order_refund_plugin = OrderRefundPlugin() - self.order_cancel_plugin = OrderCancellationPlugin() + self.order_cancel_plugin = OrderCancelPlugin() async def initialize_agents(self) -> list: """ @@ -150,8 +150,8 @@ async def initialize_agents(self) -> list: order_cancel_agent = AzureAIAgent( client=self.client, definition=order_cancel_agent_definition, - description="An agent that checks on cancellations and it must use the OrderCancellationPlugin to handle order cancellation requests. If you need more information from the user, you must return a response with 'need_more_info': 'True', otherwise you must return 'need_more_info': 'False'. You must return the response in the following valid JSON format: {'response': , 'terminated': 'True', 'need_more_info': <'True' or 'False'>}", - plugins=[OrderCancellationPlugin()], + description="An agent that checks on cancellations and it must use the OrderCancelPlugin to handle order cancellation requests. If you need more information from the user, you must return a response with 'need_more_info': 'True', otherwise you must return 'need_more_info': 'False'. You must return the response in the following valid JSON format: {'response': , 'terminated': 'True', 'need_more_info': <'True' or 'False'>}", + plugins=[OrderCancelPlugin()], ) order_refund_agent_definition = await self.client.agents.get_agent(self.agent_ids["ORDER_REFUND_AGENT_ID"]) diff --git a/src/backend/src/utils.py b/src/backend/src/utils.py index d9d6c64c..1554e3f4 100644 --- a/src/backend/src/utils.py +++ b/src/backend/src/utils.py @@ -1,16 +1,22 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. import os -from azure.identity import DefaultAzureCredential, ManagedIdentityCredential +from azure.identity import AzureCliCredential, ManagedIdentityCredential +from azure.identity.aio import ( + AzureCliCredential as AsyncAzureCliCredential, + ManagedIdentityCredential as AsyncManagedIdentityCredential +) -def get_azure_credential(): +def get_azure_credential(is_async: bool = False): use_mi_auth = os.environ.get('USE_MI_AUTH', 'false').lower() == 'true' if use_mi_auth: mi_client_id = os.environ['MI_CLIENT_ID'] return ManagedIdentityCredential( client_id=mi_client_id + ) if not is_async else AsyncManagedIdentityCredential( + client_id=mi_client_id ) - return DefaultAzureCredential() + return AzureCliCredential() if not is_async else AsyncAzureCliCredential()