diff --git a/azure.yaml b/azure.yaml index ee4810b1c..f41dc0dc6 100644 --- a/azure.yaml +++ b/azure.yaml @@ -1,4 +1,6 @@ # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json name: multi-agent-custom-automation-engine-solution-accelerator metadata: - template: multi-agent-custom-automation-engine-solution-accelerator@1.0 \ No newline at end of file + template: multi-agent-custom-automation-engine-solution-accelerator@1.0 +requiredVersions: + azd: '>= 1.15.0' \ No newline at end of file diff --git a/docs/CustomizingAzdParameters.md b/docs/CustomizingAzdParameters.md index 2dab381d9..1efd8accd 100644 --- a/docs/CustomizingAzdParameters.md +++ b/docs/CustomizingAzdParameters.md @@ -13,7 +13,9 @@ By default this template will use the environment name as the prefix to prevent | `AZURE_ENV_OPENAI_LOCATION` | string | `swedencentral` | Specifies the region for OpenAI resource deployment. | | `AZURE_ENV_MODEL_DEPLOYMENT_TYPE` | string | `GlobalStandard` | Defines the deployment type for the AI model (e.g., Standard, GlobalStandard). | | `AZURE_ENV_MODEL_NAME` | string | `gpt-4o` | Specifies the name of the GPT model to be deployed. | +| `AZURE_ENV_FOUNDRY_PROJECT_ID` | string | `` | Set this if you want to reuse an AI Foundry Project instead of creating a new one. | | `AZURE_ENV_MODEL_VERSION` | string | `2024-08-06` | Version of the GPT model to be used for deployment. | +| `AZURE_ENV_MODEL_CAPACITY` | int | `150` | Sets the GPT model capacity. | | `AZURE_ENV_IMAGETAG` | string | `latest` | Docker image tag used for container deployments. | | `AZURE_ENV_ENABLE_TELEMETRY` | bool | `true` | Enables telemetry for monitoring and diagnostics. | | `AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID` | string | `` | Set this if you want to reuse an existing Log Analytics Workspace instead of creating a new one. | diff --git a/docs/DeploymentGuide.md b/docs/DeploymentGuide.md index 01855293f..362c64c5a 100644 --- a/docs/DeploymentGuide.md +++ b/docs/DeploymentGuide.md @@ -117,7 +117,7 @@ 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: - [PowerShell](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell?view=powershell-7.5) (v7.0+) - available for Windows, macOS, and Linux. - - [Azure Developer CLI (azd)](https://aka.ms/install-azd) + - [Azure Developer CLI (azd)](https://aka.ms/install-azd) (v1.15.0+) - version - [Python 3.9+](https://www.python.org/downloads/) - [Docker Desktop](https://www.docker.com/products/docker-desktop/) - [Git](https://git-scm.com/downloads) @@ -150,6 +150,7 @@ When you start the deployment, most parameters will have **default values**, but | **Model Deployment Type** | Defines the deployment type for the AI model (e.g., Standard, GlobalStandard). | GlobalStandard | | **GPT Model Name** | Specifies the name of the GPT model to be deployed. | gpt-4o | | **GPT Model Version** | Version of the GPT model to be used for deployment. | 2024-08-06 | +| **GPT Model Capacity** | Sets the GPT model capacity. | 150 | | **Image Tag** | Docker image tag used for container deployments. | latest | | **Enable Telemetry** | Enables telemetry for monitoring and diagnostics. | true | diff --git a/infra/main.bicep b/infra/main.bicep index 4c5c3dd1f..d4f544261 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -4,6 +4,9 @@ metadata description = 'This module contains the resources required to deploy th @description('Set to true if you want to deploy WAF-aligned infrastructure.') param useWafAlignedArchitecture bool +@description('Use this parameter to use an existing AI project resource ID') +param existingFoundryProjectResourceId string = '' + @description('Optional. The prefix to add in the default names given to all deployed Azure resources.') @maxLength(19) param solutionPrefix string = 'macae${uniqueString(deployer().objectId, deployer().tenantId, subscription().subscriptionId, resourceGroup().id)}' @@ -45,10 +48,6 @@ param gptModelCapacity int = 150 @description('Set the image tag for the container images used in the solution. Default is "latest".') param imageTag string = 'latest' -// @description('Set this if you want to deploy to a different region than the resource group. Otherwise, it will use the resource group location by default.') -// param AZURE_LOCATION string='' -// param solutionLocation string = empty(AZURE_LOCATION) ? resourceGroup().location - @description('Optional. The tags to apply to all deployed Azure resources.') param tags object = { app: solutionPrefix @@ -233,32 +232,6 @@ param webSiteConfiguration webSiteConfigurationType = { environmentResourceId: null //Default value set on module configuration } -// -// Add your parameters here -// - -// ============== // -// Resources // -// ============== // - -/* #disable-next-line no-deployments-resources -resource avmTelemetry 'Microsoft.Resources/deployments@2024-03-01' = if (enableTelemetry) { - name: '46d3xbcp.[[REPLACE WITH TELEMETRY IDENTIFIER]].${replace('-..--..-', '.', '-')}.${substring(uniqueString(deployment().name, location), 0, 4)}' - properties: { - mode: 'Incremental' - template: { - '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#' - contentVersion: '1.0.0.0' - resources: [] - outputs: { - telemetry: { - type: 'String' - value: 'For more information, see https://aka.ms/avm/TelemetryInfo' - } - } - } - } -} */ // ========== Log Analytics Workspace ========== // // WAF best practices for Log Analytics: https://learn.microsoft.com/en-us/azure/well-architected/service-guides/azure-log-analytics @@ -595,8 +568,6 @@ module virtualNetwork 'br/public:avm/res/network/virtual-network:0.6.1' = if (vi name: 'administration' addressPrefix: '10.0.0.32/27' networkSecurityGroupResourceId: networkSecurityGroupAdministration.outputs.resourceId - //defaultOutboundAccess: false TODO: check this configuration for a more restricted outbound access - //natGatewayResourceId: natGateway.outputs.resourceId } { // For Azure Bastion resources deployed on or after November 2, 2021, the minimum AzureBastionSubnet size is /26 or larger (/25, /24, etc.). @@ -610,7 +581,6 @@ module virtualNetwork 'br/public:avm/res/network/virtual-network:0.6.1' = if (vi // https://learn.microsoft.com/en-us/azure/container-apps/networking?tabs=workload-profiles-env%2Cazure-cli#custom-vnw-configuration name: 'containers' addressPrefix: '10.0.2.0/23' //subnet of size /23 is required for container app - //defaultOutboundAccess: false TODO: check this configuration for a more restricted outbound access delegation: 'Microsoft.App/environments' networkSecurityGroupResourceId: networkSecurityGroupContainers.outputs.resourceId privateEndpointNetworkPolicies: 'Disabled' @@ -640,9 +610,7 @@ module bastionHost 'br/public:avm/res/network/bastion-host:0.6.1' = if (virtualN disableCopyPaste: false enableFileCopy: false enableIpConnect: true - //enableKerberos: bastionConfiguration.?enableKerberos enableShareableLink: true - //scaleUnits: bastionConfiguration.?scaleUnits } } @@ -664,8 +632,6 @@ module virtualMachine 'br/public:avm/res/compute/virtual-machine:0.13.0' = if (v nicConfigurations: [ { name: 'nic-${virtualMachineResourceName}' - //networkSecurityGroupResourceId: virtualMachineConfiguration.?nicConfigurationConfiguration.networkSecurityGroupResourceId - //nicSuffix: 'nic-${virtualMachineResourceName}' diagnosticSettings: [{ workspaceResourceId: logAnalyticsWorkspaceId }] ipConfigurations: [ { @@ -691,7 +657,6 @@ module virtualMachine 'br/public:avm/res/compute/virtual-machine:0.13.0' = if (v diskSizeGB: 128 caching: 'ReadWrite' } - //patchMode: virtualMachineConfiguration.?patchMode osType: 'Windows' encryptionAtHost: false //The property 'securityProfile.encryptionAtHost' is not valid because the 'Microsoft.Compute/EncryptionAtHost' feature is not enabled for this subscription. zone: 0 @@ -699,10 +664,6 @@ module virtualMachine 'br/public:avm/res/compute/virtual-machine:0.13.0' = if (v enabled: true typeHandlerVersion: '1.0' } - // extensionMonitoringAgentConfig: { - // enabled: true - // } - // maintenanceConfigurationResourceId: virtualMachineConfiguration.?maintenanceConfigurationResourceId } } @@ -736,7 +697,9 @@ module privateDnsZonesAiServices 'br/public:avm/res/network/private-dns-zone:0.7 ] // NOTE: Required version 'Microsoft.CognitiveServices/accounts@2024-04-01-preview' not available in AVM -var aiFoundryAiServicesResourceName = aiFoundryAiServicesConfiguration.?name ?? 'aisa-${solutionPrefix}' +var useExistingFoundryProject = !empty(existingFoundryProjectResourceId) +var existingAiFoundryName = useExistingFoundryProject?split( existingFoundryProjectResourceId,'/')[8]:'' +var aiFoundryAiServicesResourceName = useExistingFoundryProject? existingAiFoundryName : aiFoundryAiServicesConfiguration.?name ?? 'aisa-${solutionPrefix}' var aiFoundryAIservicesEnabled = aiFoundryAiServicesConfiguration.?enabled ?? true var aiFoundryAiServicesModelDeployment = { format: 'OpenAI' @@ -750,17 +713,20 @@ var aiFoundryAiServicesModelDeployment = { raiPolicyName: 'Microsoft.Default' } -module aiFoundryAiServices 'br/public:avm/res/cognitive-services/account:0.11.0' = if (aiFoundryAIservicesEnabled) { +module aiFoundryAiServices 'modules/account/main.bicep' = if (aiFoundryAIservicesEnabled) { name: take('avm.res.cognitive-services.account.${aiFoundryAiServicesResourceName}', 64) params: { name: aiFoundryAiServicesResourceName tags: aiFoundryAiServicesConfiguration.?tags ?? tags location: aiFoundryAiServicesConfiguration.?location ?? aiDeploymentsLocation enableTelemetry: enableTelemetry + projectName: 'aifp-${solutionPrefix}' + projectDescription: 'aifp-${solutionPrefix}' + existingFoundryProjectResourceId: existingFoundryProjectResourceId diagnosticSettings: [{ workspaceResourceId: logAnalyticsWorkspaceId }] sku: aiFoundryAiServicesConfiguration.?sku ?? 'S0' kind: 'AIServices' - disableLocalAuth: false //Should be set to true for WAF aligned configuration + disableLocalAuth: true //Should be set to true for WAF aligned configuration customSubDomainName: aiFoundryAiServicesResourceName apiProperties: { //staticsEnabled: false @@ -769,10 +735,12 @@ module aiFoundryAiServices 'br/public:avm/res/cognitive-services/account:0.11.0' managedIdentities: { systemAssigned: true } - //publicNetworkAccess: virtualNetworkEnabled ? 'Disabled' : 'Enabled' - //publicNetworkAccess: virtualNetworkEnabled ? 'Disabled' : 'Enabled' - publicNetworkAccess: 'Enabled' //TODO: connection via private endpoint is not working from containers network. Change this when fixed - privateEndpoints: virtualNetworkEnabled + publicNetworkAccess: virtualNetworkEnabled ? 'Disabled' : 'Enabled' + networkAcls: { + bypass: 'AzureServices' + defaultAction: (virtualNetworkEnabled) ? 'Deny' : 'Allow' + } + privateEndpoints: virtualNetworkEnabled && !useExistingFoundryProject ? ([ { name: 'pep-${aiFoundryAiServicesResourceName}' @@ -786,19 +754,7 @@ module aiFoundryAiServices 'br/public:avm/res/cognitive-services/account:0.11.0' } } ]) - : [] - // roleAssignments: [ - // // { - // // principalId: userAssignedIdentity.outputs.principalId - // // principalType: 'ServicePrincipal' - // // roleDefinitionIdOrName: 'Cognitive Services OpenAI User' - // // } - // { - // principalId: containerApp.outputs.?systemAssignedMIPrincipalId! - // principalType: 'ServicePrincipal' - // roleDefinitionIdOrName: 'Cognitive Services OpenAI User' - // } - // ] + : [] deployments: aiFoundryAiServicesConfiguration.?deployments ?? [ { name: aiFoundryAiServicesModelDeployment.name @@ -819,76 +775,27 @@ module aiFoundryAiServices 'br/public:avm/res/cognitive-services/account:0.11.0' // AI Foundry: AI Project // WAF best practices for Open AI: https://learn.microsoft.com/en-us/azure/well-architected/service-guides/azure-openai -// var aiFoundryAiProjectEnabled = aiFoundryAiProjectConfiguration.?enabled ?? true -var aiFoundryAiProjectName = aiFoundryAiProjectConfiguration.?name ?? 'aifp-${solutionPrefix}' -var aiProjectDescription = 'AI Foundry Project' - -resource aiServices 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = { - name: aiFoundryAiServicesResourceName - dependsOn:[ - aiFoundryAiServices - ] -} +var existingAiFounryProjectName = useExistingFoundryProject ? last(split( existingFoundryProjectResourceId,'/')) : '' +var aiFoundryAiProjectName = useExistingFoundryProject ? existingAiFounryProjectName : aiFoundryAiProjectConfiguration.?name ?? 'aifp-${solutionPrefix}' -resource aiFoundryProject 'Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview' = { - parent: aiServices - name: aiFoundryAiProjectName - location: aiFoundryAiProjectConfiguration.?location ?? aiDeploymentsLocation - identity: { - type: 'SystemAssigned' - } - properties: { - description: aiProjectDescription - displayName: aiFoundryAiProjectName - } -} - -resource aiUser 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { - name: '53ca6127-db72-4b80-b1b0-d745d6d5456d' -} +var useExistingResourceId = !empty(existingFoundryProjectResourceId) -resource aiUserAccessProj 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(containerApp.name, aiFoundryProject.id, aiUser.id) - scope: aiFoundryProject - properties: { - roleDefinitionId: aiUser.id - principalId: containerApp.outputs.?systemAssignedMIPrincipalId! - } -} - -resource aiUserAccessFoundry 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(containerApp.name, aiServices.id, aiUser.id) - scope: aiServices - properties: { - roleDefinitionId: aiUser.id - principalId: containerApp.outputs.?systemAssignedMIPrincipalId! - } -} - -resource aiDeveloper 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { - name: '64702f94-c441-49e6-a78b-ef80e0188fee' -} - -resource aiDeveloperAccessFoundry 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(containerApp.name, aiServices.id, aiDeveloper.id) - scope: aiFoundryProject - properties: { - roleDefinitionId: aiDeveloper.id +module cogServiceRoleAssignmentsNew './modules/role.bicep' = if(!useExistingResourceId) { + params: { + name: 'new-${guid(containerApp.name, aiFoundryAiServices.outputs.resourceId)}' principalId: containerApp.outputs.?systemAssignedMIPrincipalId! + aiServiceName: aiFoundryAiServices.outputs.name } + scope: resourceGroup(subscription().subscriptionId, resourceGroup().name) } -resource cognitiveServiceOpenAIUser 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { - name: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' -} - -resource cognitiveServiceOpenAIUserAccessFoundry 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(containerApp.name, aiServices.id, cognitiveServiceOpenAIUser.id) - scope: aiServices - properties: { - roleDefinitionId: cognitiveServiceOpenAIUser.id +module cogServiceRoleAssignmentsExisting './modules/role.bicep' = if(useExistingResourceId) { + params: { + name: 'reuse-${guid(containerApp.name, aiFoundryAiServices.outputs.aiProjectInfo.resourceId)}' principalId: containerApp.outputs.?systemAssignedMIPrincipalId! + aiServiceName: aiFoundryAiServices.outputs.name } + scope: resourceGroup( split(existingFoundryProjectResourceId, '/')[2], split(existingFoundryProjectResourceId, '/')[4]) } // ========== Cosmos DB ========== // @@ -966,7 +873,6 @@ module cosmosDb 'br/public:avm/res/document-db/database-account:0.12.0' = if (co 'EnableServerless' ] sqlRoleAssignmentsPrincipalIds: [ - //userAssignedIdentity.outputs.principalId containerApp.outputs.?systemAssignedMIPrincipalId ] sqlRoleDefinitions: [ @@ -1003,13 +909,6 @@ module containerAppEnvironment 'modules/container-app-environment.bicep' = if (c subnetResourceId: virtualNetworkEnabled ? containerAppEnvironmentConfiguration.?subnetResourceId ?? virtualNetwork.?outputs.?subnetResourceIds[3] ?? '' : '' - //aspireDashboardEnabled: !virtualNetworkEnabled - // vnetConfiguration: virtualNetworkEnabled - // ? { - // internal: false - // infrastructureSubnetId: containerAppEnvironmentConfiguration.?subnetResourceId ?? virtualNetwork.?outputs.?subnetResourceIds[3] ?? '' - // } - // : {} } } @@ -1117,7 +1016,7 @@ module containerApp 'br/public:avm/res/app/container-app:0.14.2' = if (container } { name: 'AZURE_AI_AGENT_ENDPOINT' - value: aiFoundryProject.properties.endpoints['AI Foundry API'] + value: aiFoundryAiServices.outputs.aiProjectInfo.apiEndpoint } { name: 'AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME' @@ -1189,19 +1088,6 @@ module webSite 'br/public:avm/res/web/site:0.15.1' = if (webSiteEnabled) { @description('The default url of the website to connect to the Multi-Agent Custom Automation Engine solution.') output webSiteDefaultHostname string = webSite.outputs.defaultHostname -// @description('The name of the resource.') -// output name string = .name - -// @description('The location the resource was deployed into.') -// output location string = .location - -// ================ // -// Definitions // -// ================ // -// -// Add your User-defined-types here, if any -// - @export() @description('The type for the Multi-Agent Custom Automation Engine Log Analytics Workspace resource configuration.') type logAnalyticsWorkspaceConfigurationType = { diff --git a/infra/main.parameters.json b/infra/main.parameters.json index a1d690070..5a22eb389 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -39,6 +39,9 @@ "gptModelCapacity": { "value": "${AZURE_ENV_MODEL_CAPACITY}" }, + "existingFoundryProjectResourceId": { + "value": "${AZURE_ENV_FOUNDRY_PROJECT_ID}" + }, "imageTag": { "value": "${AZURE_ENV_IMAGE_TAG}" }, diff --git a/infra/modules/account/main.bicep b/infra/modules/account/main.bicep new file mode 100644 index 000000000..b1fad4456 --- /dev/null +++ b/infra/modules/account/main.bicep @@ -0,0 +1,421 @@ +metadata name = 'Cognitive Services' +metadata description = 'This module deploys a Cognitive Service.' + +@description('Required. The name of Cognitive Services account.') +param name string + +@description('Optional: Name for the project which needs to be created.') +param projectName string + +@description('Optional: Description for the project which needs to be created.') +param projectDescription string + +param existingFoundryProjectResourceId string = '' + +@description('Required. Kind of the Cognitive Services account. Use \'Get-AzCognitiveServicesAccountSku\' to determine a valid combinations of \'kind\' and \'SKU\' for your Azure region.') +@allowed([ + 'AIServices' + 'AnomalyDetector' + 'CognitiveServices' + 'ComputerVision' + 'ContentModerator' + 'ContentSafety' + 'ConversationalLanguageUnderstanding' + 'CustomVision.Prediction' + 'CustomVision.Training' + 'Face' + 'FormRecognizer' + 'HealthInsights' + 'ImmersiveReader' + 'Internal.AllInOne' + 'LUIS' + 'LUIS.Authoring' + 'LanguageAuthoring' + 'MetricsAdvisor' + 'OpenAI' + 'Personalizer' + 'QnAMaker.v2' + 'SpeechServices' + 'TextAnalytics' + 'TextTranslation' +]) +param kind string + +@description('Optional. SKU of the Cognitive Services account. Use \'Get-AzCognitiveServicesAccountSku\' to determine a valid combinations of \'kind\' and \'SKU\' for your Azure region.') +@allowed([ + 'C2' + 'C3' + 'C4' + 'F0' + 'F1' + 'S' + 'S0' + 'S1' + 'S10' + 'S2' + 'S3' + 'S4' + 'S5' + 'S6' + 'S7' + 'S8' + 'S9' +]) +param sku string = 'S0' + +@description('Optional. Location for all Resources.') +param location string = resourceGroup().location + +import { diagnosticSettingFullType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('Optional. The diagnostic settings of the service.') +param diagnosticSettings diagnosticSettingFullType[]? + +@description('Optional. Whether or not public network access is allowed for this resource. For security reasons it should be disabled. If not specified, it will be disabled by default if private endpoints are set and networkAcls are not set.') +@allowed([ + 'Enabled' + 'Disabled' +]) +param publicNetworkAccess string? + +@description('Conditional. Subdomain name used for token-based authentication. Required if \'networkAcls\' or \'privateEndpoints\' are set.') +param customSubDomainName string? + +@description('Optional. A collection of rules governing the accessibility from specific network locations.') +param networkAcls object? + +import { privateEndpointSingleServiceType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('Optional. Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible.') +param privateEndpoints privateEndpointSingleServiceType[]? + +import { lockType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('Optional. The lock settings of the service.') +param lock lockType? + +import { roleAssignmentType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('Optional. Array of role assignments to create.') +param roleAssignments roleAssignmentType[]? + +@description('Optional. Tags of the resource.') +param tags object? + +@description('Optional. List of allowed FQDN.') +param allowedFqdnList array? + +@description('Optional. The API properties for special APIs.') +param apiProperties object? + +@description('Optional. Allow only Azure AD authentication. Should be enabled for security reasons.') +param disableLocalAuth bool = true + +import { customerManagedKeyType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('Optional. The customer managed key definition.') +param customerManagedKey customerManagedKeyType? + +@description('Optional. The flag to enable dynamic throttling.') +param dynamicThrottlingEnabled bool = false + +@secure() +@description('Optional. Resource migration token.') +param migrationToken string? + +@description('Optional. Restore a soft-deleted cognitive service at deployment time. Will fail if no such soft-deleted resource exists.') +param restore bool = false + +@description('Optional. Restrict outbound network access.') +param restrictOutboundNetworkAccess bool = true + +@description('Optional. The storage accounts for this resource.') +param userOwnedStorage array? + +import { managedIdentityAllType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('Optional. The managed identity definition for this resource.') +param managedIdentities managedIdentityAllType? + +@description('Optional. Enable/Disable usage telemetry for module.') +param enableTelemetry bool = true + +@description('Optional. Array of deployments about cognitive service accounts to create.') +param deployments deploymentType[]? + +@description('Optional. Key vault reference and secret settings for the module\'s secrets export.') +param secretsExportConfiguration secretsExportConfigurationType? + +@description('Optional. Enable/Disable project management feature for AI Foundry.') +param allowProjectManagement bool? + +var formattedUserAssignedIdentities = reduce( + map((managedIdentities.?userAssignedResourceIds ?? []), (id) => { '${id}': {} }), + {}, + (cur, next) => union(cur, next) +) // Converts the flat array to an object like { '${id1}': {}, '${id2}': {} } + +var identity = !empty(managedIdentities) + ? { + type: (managedIdentities.?systemAssigned ?? false) + ? (!empty(managedIdentities.?userAssignedResourceIds ?? {}) ? 'SystemAssigned, UserAssigned' : 'SystemAssigned') + : (!empty(managedIdentities.?userAssignedResourceIds ?? {}) ? 'UserAssigned' : null) + userAssignedIdentities: !empty(formattedUserAssignedIdentities) ? formattedUserAssignedIdentities : null + } + : null + +#disable-next-line no-deployments-resources +resource avmTelemetry 'Microsoft.Resources/deployments@2024-03-01' = if (enableTelemetry) { + name: '46d3xbcp.res.cognitiveservices-account.${replace('-..--..-', '.', '-')}.${substring(uniqueString(deployment().name, location), 0, 4)}' + properties: { + mode: 'Incremental' + template: { + '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#' + contentVersion: '1.0.0.0' + resources: [] + outputs: { + telemetry: { + type: 'String' + value: 'For more information, see https://aka.ms/avm/TelemetryInfo' + } + } + } + } +} + +resource cMKKeyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = if (!empty(customerManagedKey.?keyVaultResourceId)) { + name: last(split(customerManagedKey.?keyVaultResourceId!, '/')) + scope: resourceGroup( + split(customerManagedKey.?keyVaultResourceId!, '/')[2], + split(customerManagedKey.?keyVaultResourceId!, '/')[4] + ) + + resource cMKKey 'keys@2023-07-01' existing = if (!empty(customerManagedKey.?keyVaultResourceId) && !empty(customerManagedKey.?keyName)) { + name: customerManagedKey.?keyName! + } +} + +resource cMKUserAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2025-01-31-preview' existing = if (!empty(customerManagedKey.?userAssignedIdentityResourceId)) { + name: last(split(customerManagedKey.?userAssignedIdentityResourceId!, '/')) + scope: resourceGroup( + split(customerManagedKey.?userAssignedIdentityResourceId!, '/')[2], + split(customerManagedKey.?userAssignedIdentityResourceId!, '/')[4] + ) +} + +var useExistingService = !empty(existingFoundryProjectResourceId) + +resource cognitiveServiceNew 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' = if(!useExistingService) { + name: name + kind: kind + identity: identity + location: location + tags: tags + sku: { + name: sku + } + properties: { + allowProjectManagement: allowProjectManagement // allows project management for Cognitive Services accounts in AI Foundry - FDP updates + customSubDomainName: customSubDomainName + networkAcls: !empty(networkAcls ?? {}) + ? { + defaultAction: networkAcls.?defaultAction + virtualNetworkRules: networkAcls.?virtualNetworkRules ?? [] + ipRules: networkAcls.?ipRules ?? [] + } + : null + publicNetworkAccess: publicNetworkAccess != null + ? publicNetworkAccess + : (!empty(networkAcls) ? 'Enabled' : 'Disabled') + allowedFqdnList: allowedFqdnList + apiProperties: apiProperties + disableLocalAuth: disableLocalAuth + encryption: !empty(customerManagedKey) + ? { + keySource: 'Microsoft.KeyVault' + keyVaultProperties: { + identityClientId: !empty(customerManagedKey.?userAssignedIdentityResourceId ?? '') + ? cMKUserAssignedIdentity.properties.clientId + : null + keyVaultUri: cMKKeyVault.properties.vaultUri + keyName: customerManagedKey!.keyName + keyVersion: !empty(customerManagedKey.?keyVersion ?? '') + ? customerManagedKey!.?keyVersion + : last(split(cMKKeyVault::cMKKey.properties.keyUriWithVersion, '/')) + } + } + : null + migrationToken: migrationToken + restore: restore + restrictOutboundNetworkAccess: restrictOutboundNetworkAccess + userOwnedStorage: userOwnedStorage + dynamicThrottlingEnabled: dynamicThrottlingEnabled + } +} + +var existingCognitiveServiceDetails = split(existingFoundryProjectResourceId, '/') + +resource cognitiveServiceExisting 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = if(useExistingService) { + name: existingCognitiveServiceDetails[8] + scope: resourceGroup(existingCognitiveServiceDetails[2], existingCognitiveServiceDetails[4]) +} + +module cognigive_service_dependencies 'modules/dependencies.bicep' = if(!useExistingService) { + params: { + projectName: projectName + projectDescription: projectDescription + name: cognitiveServiceNew.name + location: location + deployments: deployments + diagnosticSettings: diagnosticSettings + lock: lock + privateEndpoints: privateEndpoints + roleAssignments: roleAssignments + secretsExportConfiguration: secretsExportConfiguration + sku: sku + tags: tags + } +} + +module existing_cognigive_service_dependencies 'modules/dependencies.bicep' = if(useExistingService) { + params: { + name: cognitiveServiceExisting.name + projectName: projectName + projectDescription: projectDescription + azureExistingAIProjectResourceId: existingFoundryProjectResourceId + location: location + deployments: deployments + diagnosticSettings: diagnosticSettings + lock: lock + privateEndpoints: privateEndpoints + roleAssignments: roleAssignments + secretsExportConfiguration: secretsExportConfiguration + sku: sku + tags: tags + } + scope: resourceGroup(existingCognitiveServiceDetails[2], existingCognitiveServiceDetails[4]) +} + +var cognitiveService = useExistingService ? cognitiveServiceExisting : cognitiveServiceNew + +@description('The name of the cognitive services account.') +output name string = useExistingService ? cognitiveServiceExisting.name : cognitiveServiceNew.name + +@description('The resource ID of the cognitive services account.') +output resourceId string = useExistingService ? cognitiveServiceExisting.id : cognitiveServiceNew.id + +@description('The resource group the cognitive services account was deployed into.') +output subscriptionId string = useExistingService ? existingCognitiveServiceDetails[2] : subscription().subscriptionId + +@description('The resource group the cognitive services account was deployed into.') +output resourceGroupName string = useExistingService ? existingCognitiveServiceDetails[4] : resourceGroup().name + +@description('The service endpoint of the cognitive services account.') +output endpoint string = useExistingService ? cognitiveServiceExisting.properties.endpoint : cognitiveService.properties.endpoint + +@description('All endpoints available for the cognitive services account, types depends on the cognitive service kind.') +output endpoints endpointType = useExistingService ? cognitiveServiceExisting.properties.endpoints : cognitiveService.properties.endpoints + +@description('The principal ID of the system assigned identity.') +output systemAssignedMIPrincipalId string? = useExistingService ? cognitiveServiceExisting.identity.principalId : cognitiveService.?identity.?principalId + +@description('The location the resource was deployed into.') +output location string = useExistingService ? cognitiveServiceExisting.location : cognitiveService.location + +import { secretsOutputType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('A hashtable of references to the secrets exported to the provided Key Vault. The key of each reference is each secret\'s name.') +output exportedSecrets secretsOutputType = useExistingService ? existing_cognigive_service_dependencies.outputs.exportedSecrets : cognigive_service_dependencies.outputs.exportedSecrets + +@description('The private endpoints of the congitive services account.') +output privateEndpoints privateEndpointOutputType[] = useExistingService ? existing_cognigive_service_dependencies.outputs.privateEndpoints : cognigive_service_dependencies.outputs.privateEndpoints + +import { aiProjectOutputType } from './modules/project.bicep' +output aiProjectInfo aiProjectOutputType = useExistingService ? existing_cognigive_service_dependencies.outputs.aiProjectInfo : cognigive_service_dependencies.outputs.aiProjectInfo + +// ================ // +// Definitions // +// ================ // + +@export() +@description('The type for the private endpoint output.') +type privateEndpointOutputType = { + @description('The name of the private endpoint.') + name: string + + @description('The resource ID of the private endpoint.') + resourceId: string + + @description('The group Id for the private endpoint Group.') + groupId: string? + + @description('The custom DNS configurations of the private endpoint.') + customDnsConfigs: { + @description('FQDN that resolves to private endpoint IP address.') + fqdn: string? + + @description('A list of private IP addresses of the private endpoint.') + ipAddresses: string[] + }[] + + @description('The IDs of the network interfaces associated with the private endpoint.') + networkInterfaceResourceIds: string[] +} + +@export() +@description('The type for a cognitive services account deployment.') +type deploymentType = { + @description('Optional. Specify the name of cognitive service account deployment.') + name: string? + + @description('Required. Properties of Cognitive Services account deployment model.') + model: { + @description('Required. The name of Cognitive Services account deployment model.') + name: string + + @description('Required. The format of Cognitive Services account deployment model.') + format: string + + @description('Required. The version of Cognitive Services account deployment model.') + version: string + } + + @description('Optional. The resource model definition representing SKU.') + sku: { + @description('Required. The name of the resource model definition representing SKU.') + name: string + + @description('Optional. The capacity of the resource model definition representing SKU.') + capacity: int? + + @description('Optional. The tier of the resource model definition representing SKU.') + tier: string? + + @description('Optional. The size of the resource model definition representing SKU.') + size: string? + + @description('Optional. The family of the resource model definition representing SKU.') + family: string? + }? + + @description('Optional. The name of RAI policy.') + raiPolicyName: string? + + @description('Optional. The version upgrade option.') + versionUpgradeOption: string? +} + +@export() +@description('The type for a cognitive services account endpoint.') +type endpointType = { + @description('Type of the endpoint.') + name: string? + @description('The endpoint URI.') + endpoint: string? +} + +@export() +@description('The type of the secrets exported to the provided Key Vault.') +type secretsExportConfigurationType = { + @description('Required. The key vault name where to store the keys and connection strings generated by the modules.') + keyVaultResourceId: string + + @description('Optional. The name for the accessKey1 secret to create.') + accessKey1Name: string? + + @description('Optional. The name for the accessKey2 secret to create.') + accessKey2Name: string? +} diff --git a/infra/modules/account/modules/dependencies.bicep b/infra/modules/account/modules/dependencies.bicep new file mode 100644 index 000000000..c2d7de6f8 --- /dev/null +++ b/infra/modules/account/modules/dependencies.bicep @@ -0,0 +1,479 @@ +@description('Required. The name of Cognitive Services account.') +param name string + +@description('Optional. SKU of the Cognitive Services account. Use \'Get-AzCognitiveServicesAccountSku\' to determine a valid combinations of \'kind\' and \'SKU\' for your Azure region.') +@allowed([ + 'C2' + 'C3' + 'C4' + 'F0' + 'F1' + 'S' + 'S0' + 'S1' + 'S10' + 'S2' + 'S3' + 'S4' + 'S5' + 'S6' + 'S7' + 'S8' + 'S9' +]) +param sku string = 'S0' + +@description('Optional. Location for all Resources.') +param location string = resourceGroup().location + +@description('Optional. Tags of the resource.') +param tags object? + +@description('Optional. Array of deployments about cognitive service accounts to create.') +param deployments deploymentType[]? + +@description('Optional. Key vault reference and secret settings for the module\'s secrets export.') +param secretsExportConfiguration secretsExportConfigurationType? + +import { privateEndpointSingleServiceType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('Optional. Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible.') +param privateEndpoints privateEndpointSingleServiceType[]? + +import { lockType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('Optional. The lock settings of the service.') +param lock lockType? + +import { roleAssignmentType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('Optional. Array of role assignments to create.') +param roleAssignments roleAssignmentType[]? + +import { diagnosticSettingFullType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('Optional. The diagnostic settings of the service.') +param diagnosticSettings diagnosticSettingFullType[]? + +@description('Optional: Name for the project which needs to be created.') +param projectName string + +@description('Optional: Description for the project which needs to be created.') +param projectDescription string + +@description('Optional: Provide the existing project resource id in case if it needs to be reused') +param azureExistingAIProjectResourceId string = '' + +var builtInRoleNames = { + 'Cognitive Services Contributor': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68' + ) + 'Cognitive Services Custom Vision Contributor': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'c1ff6cc2-c111-46fe-8896-e0ef812ad9f3' + ) + 'Cognitive Services Custom Vision Deployment': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '5c4089e1-6d96-4d2f-b296-c1bc7137275f' + ) + 'Cognitive Services Custom Vision Labeler': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '88424f51-ebe7-446f-bc41-7fa16989e96c' + ) + 'Cognitive Services Custom Vision Reader': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '93586559-c37d-4a6b-ba08-b9f0940c2d73' + ) + 'Cognitive Services Custom Vision Trainer': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '0a5ae4ab-0d65-4eeb-be61-29fc9b54394b' + ) + 'Cognitive Services Data Reader (Preview)': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'b59867f0-fa02-499b-be73-45a86b5b3e1c' + ) + 'Cognitive Services Face Recognizer': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '9894cab4-e18a-44aa-828b-cb588cd6f2d7' + ) + 'Cognitive Services Immersive Reader User': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'b2de6794-95db-4659-8781-7e080d3f2b9d' + ) + 'Cognitive Services Language Owner': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'f07febfe-79bc-46b1-8b37-790e26e6e498' + ) + 'Cognitive Services Language Reader': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '7628b7b8-a8b2-4cdc-b46f-e9b35248918e' + ) + 'Cognitive Services Language Writer': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'f2310ca1-dc64-4889-bb49-c8e0fa3d47a8' + ) + 'Cognitive Services LUIS Owner': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'f72c8140-2111-481c-87ff-72b910f6e3f8' + ) + 'Cognitive Services LUIS Reader': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '18e81cdc-4e98-4e29-a639-e7d10c5a6226' + ) + 'Cognitive Services LUIS Writer': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '6322a993-d5c9-4bed-b113-e49bbea25b27' + ) + 'Cognitive Services Metrics Advisor Administrator': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'cb43c632-a144-4ec5-977c-e80c4affc34a' + ) + 'Cognitive Services Metrics Advisor User': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '3b20f47b-3825-43cb-8114-4bd2201156a8' + ) + 'Cognitive Services OpenAI Contributor': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'a001fd3d-188f-4b5d-821b-7da978bf7442' + ) + 'Cognitive Services OpenAI User': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' + ) + 'Cognitive Services QnA Maker Editor': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'f4cc2bf9-21be-47a1-bdf1-5c5804381025' + ) + 'Cognitive Services QnA Maker Reader': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '466ccd10-b268-4a11-b098-b4849f024126' + ) + 'Cognitive Services Speech Contributor': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '0e75ca1e-0464-4b4d-8b93-68208a576181' + ) + 'Cognitive Services Speech User': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'f2dc8367-1007-4938-bd23-fe263f013447' + ) + 'Cognitive Services User': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'a97b65f3-24c7-4388-baec-2e87135dc908' + ) + 'Azure AI Developer': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '64702f94-c441-49e6-a78b-ef80e0188fee' + ) + Contributor: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') + Owner: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635') + Reader: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7') + 'Role Based Access Control Administrator': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'f58310d9-a9f6-439a-9e8d-f62e7b41a168' + ) + 'User Access Administrator': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9' + ) +} + +var formattedRoleAssignments = [ + for (roleAssignment, index) in (roleAssignments ?? []): union(roleAssignment, { + roleDefinitionId: builtInRoleNames[?roleAssignment.roleDefinitionIdOrName] ?? (contains( + roleAssignment.roleDefinitionIdOrName, + '/providers/Microsoft.Authorization/roleDefinitions/' + ) + ? roleAssignment.roleDefinitionIdOrName + : subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleAssignment.roleDefinitionIdOrName)) + }) +] + +var enableReferencedModulesTelemetry = false + +resource cognitiveService 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = { + name: name +} + +@batchSize(1) +resource cognitiveService_deployments 'Microsoft.CognitiveServices/accounts/deployments@2025-04-01-preview' = [ + for (deployment, index) in (deployments ?? []): { + parent: cognitiveService + name: deployment.?name ?? '${name}-deployments' + properties: { + model: deployment.model + raiPolicyName: deployment.?raiPolicyName + versionUpgradeOption: deployment.?versionUpgradeOption + } + sku: deployment.?sku ?? { + name: sku + capacity: sku.?capacity + tier: sku.?tier + size: sku.?size + family: sku.?family + } + } +] + +resource cognitiveService_lock 'Microsoft.Authorization/locks@2020-05-01' = if (!empty(lock ?? {}) && lock.?kind != 'None') { + name: lock.?name ?? 'lock-${name}' + properties: { + level: lock.?kind ?? '' + notes: lock.?kind == 'CanNotDelete' + ? 'Cannot delete resource or child resources.' + : 'Cannot delete or modify the resource or child resources.' + } + scope: cognitiveService +} + +resource cognitiveService_diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = [ + for (diagnosticSetting, index) in (diagnosticSettings ?? []): { + name: diagnosticSetting.?name ?? '${name}-diagnosticSettings' + properties: { + storageAccountId: diagnosticSetting.?storageAccountResourceId + workspaceId: diagnosticSetting.?workspaceResourceId + eventHubAuthorizationRuleId: diagnosticSetting.?eventHubAuthorizationRuleResourceId + eventHubName: diagnosticSetting.?eventHubName + metrics: [ + for group in (diagnosticSetting.?metricCategories ?? [{ category: 'AllMetrics' }]): { + category: group.category + enabled: group.?enabled ?? true + timeGrain: null + } + ] + logs: [ + for group in (diagnosticSetting.?logCategoriesAndGroups ?? [{ categoryGroup: 'allLogs' }]): { + categoryGroup: group.?categoryGroup + category: group.?category + enabled: group.?enabled ?? true + } + ] + marketplacePartnerId: diagnosticSetting.?marketplacePartnerResourceId + logAnalyticsDestinationType: diagnosticSetting.?logAnalyticsDestinationType + } + scope: cognitiveService + } +] + +module cognitiveService_privateEndpoints 'br/public:avm/res/network/private-endpoint:0.11.0' = [ + for (privateEndpoint, index) in (privateEndpoints ?? []): { + name: '${uniqueString(deployment().name, location)}-cognitiveService-PrivateEndpoint-${index}' + scope: resourceGroup( + split(privateEndpoint.?resourceGroupResourceId ?? resourceGroup().id, '/')[2], + split(privateEndpoint.?resourceGroupResourceId ?? resourceGroup().id, '/')[4] + ) + params: { + name: privateEndpoint.?name ?? 'pep-${last(split(cognitiveService.id, '/'))}-${privateEndpoint.?service ?? 'account'}-${index}' + privateLinkServiceConnections: privateEndpoint.?isManualConnection != true + ? [ + { + name: privateEndpoint.?privateLinkServiceConnectionName ?? '${last(split(cognitiveService.id, '/'))}-${privateEndpoint.?service ?? 'account'}-${index}' + properties: { + privateLinkServiceId: cognitiveService.id + groupIds: [ + privateEndpoint.?service ?? 'account' + ] + } + } + ] + : null + manualPrivateLinkServiceConnections: privateEndpoint.?isManualConnection == true + ? [ + { + name: privateEndpoint.?privateLinkServiceConnectionName ?? '${last(split(cognitiveService.id, '/'))}-${privateEndpoint.?service ?? 'account'}-${index}' + properties: { + privateLinkServiceId: cognitiveService.id + groupIds: [ + privateEndpoint.?service ?? 'account' + ] + requestMessage: privateEndpoint.?manualConnectionRequestMessage ?? 'Manual approval required.' + } + } + ] + : null + subnetResourceId: privateEndpoint.subnetResourceId + enableTelemetry: enableReferencedModulesTelemetry + location: privateEndpoint.?location ?? reference( + split(privateEndpoint.subnetResourceId, '/subnets/')[0], + '2020-06-01', + 'Full' + ).location + lock: privateEndpoint.?lock ?? lock + privateDnsZoneGroup: privateEndpoint.?privateDnsZoneGroup + roleAssignments: privateEndpoint.?roleAssignments + tags: privateEndpoint.?tags ?? tags + customDnsConfigs: privateEndpoint.?customDnsConfigs + ipConfigurations: privateEndpoint.?ipConfigurations + applicationSecurityGroupResourceIds: privateEndpoint.?applicationSecurityGroupResourceIds + customNetworkInterfaceName: privateEndpoint.?customNetworkInterfaceName + } + } +] + +resource cognitiveService_roleAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [ + for (roleAssignment, index) in (formattedRoleAssignments ?? []): { + name: roleAssignment.?name ?? guid(cognitiveService.id, roleAssignment.principalId, roleAssignment.roleDefinitionId) + properties: { + roleDefinitionId: roleAssignment.roleDefinitionId + principalId: roleAssignment.principalId + description: roleAssignment.?description + principalType: roleAssignment.?principalType + condition: roleAssignment.?condition + conditionVersion: !empty(roleAssignment.?condition) ? (roleAssignment.?conditionVersion ?? '2.0') : null // Must only be set if condtion is set + delegatedManagedIdentityResourceId: roleAssignment.?delegatedManagedIdentityResourceId + } + scope: cognitiveService + } +] + +module secretsExport './keyVaultExport.bicep' = if (secretsExportConfiguration != null) { + name: '${uniqueString(deployment().name, location)}-secrets-kv' + scope: resourceGroup( + split(secretsExportConfiguration.?keyVaultResourceId!, '/')[2], + split(secretsExportConfiguration.?keyVaultResourceId!, '/')[4] + ) + params: { + keyVaultName: last(split(secretsExportConfiguration.?keyVaultResourceId!, '/')) + secretsToSet: union( + [], + contains(secretsExportConfiguration!, 'accessKey1Name') + ? [ + { + name: secretsExportConfiguration!.?accessKey1Name + value: cognitiveService.listKeys().key1 + } + ] + : [], + contains(secretsExportConfiguration!, 'accessKey2Name') + ? [ + { + name: secretsExportConfiguration!.?accessKey2Name + value: cognitiveService.listKeys().key2 + } + ] + : [] + ) + } +} + +module aiProject 'project.bicep' = if(!empty(projectName) || !empty(azureExistingAIProjectResourceId)) { + name: take('${name}-ai-project-${projectName}-deployment', 64) + params: { + name: projectName + desc: projectDescription + aiServicesName: cognitiveService.name + location: location + tags: tags + azureExistingAIProjectResourceId: azureExistingAIProjectResourceId + } +} + +import { secretsOutputType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('A hashtable of references to the secrets exported to the provided Key Vault. The key of each reference is each secret\'s name.') +output exportedSecrets secretsOutputType = (secretsExportConfiguration != null) + ? toObject(secretsExport.outputs.secretsSet, secret => last(split(secret.secretResourceId, '/')), secret => secret) + : {} + +@description('The private endpoints of the congitive services account.') +output privateEndpoints privateEndpointOutputType[] = [ + for (pe, index) in (privateEndpoints ?? []): { + name: cognitiveService_privateEndpoints[index].outputs.name + resourceId: cognitiveService_privateEndpoints[index].outputs.resourceId + groupId: cognitiveService_privateEndpoints[index].outputs.?groupId! + customDnsConfigs: cognitiveService_privateEndpoints[index].outputs.customDnsConfigs + networkInterfaceResourceIds: cognitiveService_privateEndpoints[index].outputs.networkInterfaceResourceIds + } +] + +import { aiProjectOutputType } from 'project.bicep' +output aiProjectInfo aiProjectOutputType = aiProject.outputs.aiProjectInfo + +// ================ // +// Definitions // +// ================ // + +@export() +@description('The type for the private endpoint output.') +type privateEndpointOutputType = { + @description('The name of the private endpoint.') + name: string + + @description('The resource ID of the private endpoint.') + resourceId: string + + @description('The group Id for the private endpoint Group.') + groupId: string? + + @description('The custom DNS configurations of the private endpoint.') + customDnsConfigs: { + @description('FQDN that resolves to private endpoint IP address.') + fqdn: string? + + @description('A list of private IP addresses of the private endpoint.') + ipAddresses: string[] + }[] + + @description('The IDs of the network interfaces associated with the private endpoint.') + networkInterfaceResourceIds: string[] +} + +@export() +@description('The type for a cognitive services account deployment.') +type deploymentType = { + @description('Optional. Specify the name of cognitive service account deployment.') + name: string? + + @description('Required. Properties of Cognitive Services account deployment model.') + model: { + @description('Required. The name of Cognitive Services account deployment model.') + name: string + + @description('Required. The format of Cognitive Services account deployment model.') + format: string + + @description('Required. The version of Cognitive Services account deployment model.') + version: string + } + + @description('Optional. The resource model definition representing SKU.') + sku: { + @description('Required. The name of the resource model definition representing SKU.') + name: string + + @description('Optional. The capacity of the resource model definition representing SKU.') + capacity: int? + + @description('Optional. The tier of the resource model definition representing SKU.') + tier: string? + + @description('Optional. The size of the resource model definition representing SKU.') + size: string? + + @description('Optional. The family of the resource model definition representing SKU.') + family: string? + }? + + @description('Optional. The name of RAI policy.') + raiPolicyName: string? + + @description('Optional. The version upgrade option.') + versionUpgradeOption: string? +} + +@export() +@description('The type for a cognitive services account endpoint.') +type endpointType = { + @description('Type of the endpoint.') + name: string? + @description('The endpoint URI.') + endpoint: string? +} + +@export() +@description('The type of the secrets exported to the provided Key Vault.') +type secretsExportConfigurationType = { + @description('Required. The key vault name where to store the keys and connection strings generated by the modules.') + keyVaultResourceId: string + + @description('Optional. The name for the accessKey1 secret to create.') + accessKey1Name: string? + + @description('Optional. The name for the accessKey2 secret to create.') + accessKey2Name: string? +} diff --git a/infra/modules/account/modules/keyVaultExport.bicep b/infra/modules/account/modules/keyVaultExport.bicep new file mode 100644 index 000000000..a54cc5576 --- /dev/null +++ b/infra/modules/account/modules/keyVaultExport.bicep @@ -0,0 +1,43 @@ +// ============== // +// Parameters // +// ============== // + +@description('Required. The name of the Key Vault to set the ecrets in.') +param keyVaultName string + +import { secretToSetType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('Required. The secrets to set in the Key Vault.') +param secretsToSet secretToSetType[] + +// ============= // +// Resources // +// ============= // + +resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { + name: keyVaultName +} + +resource secrets 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = [ + for secret in secretsToSet: { + name: secret.name + parent: keyVault + properties: { + value: secret.value + } + } +] + +// =========== // +// Outputs // +// =========== // + +import { secretSetOutputType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('The references to the secrets exported to the provided Key Vault.') +output secretsSet secretSetOutputType[] = [ + #disable-next-line outputs-should-not-contain-secrets // Only returning the references, not a secret value + for index in range(0, length(secretsToSet ?? [])): { + secretResourceId: secrets[index].id + secretUri: secrets[index].properties.secretUri + secretUriWithVersion: secrets[index].properties.secretUriWithVersion + } +] diff --git a/infra/modules/account/modules/project.bicep b/infra/modules/account/modules/project.bicep new file mode 100644 index 000000000..8ca346546 --- /dev/null +++ b/infra/modules/account/modules/project.bicep @@ -0,0 +1,61 @@ +@description('Required. Name of the AI Services project.') +param name string + +@description('Required. The location of the Project resource.') +param location string = resourceGroup().location + +@description('Optional. The description of the AI Foundry project to create. Defaults to the project name.') +param desc string = name + +@description('Required. Name of the existing Cognitive Services resource to create the AI Foundry project in.') +param aiServicesName string + +@description('Optional. Tags to be applied to the resources.') +param tags object = {} + +@description('Optional. Use this parameter to use an existing AI project resource ID from different resource group') +param azureExistingAIProjectResourceId string = '' + +// // Extract components from existing AI Project Resource ID if provided +var useExistingProject = !empty(azureExistingAIProjectResourceId) +var existingProjName = useExistingProject ? last(split(azureExistingAIProjectResourceId, '/')) : '' +var existingProjEndpoint = useExistingProject ? format('https://{0}.services.ai.azure.com/api/projects/{1}', aiServicesName, existingProjName) : '' +// Reference to cognitive service in current resource group for new projects +resource cogServiceReference 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = { + name: aiServicesName +} + +// Create new AI project only if not reusing existing one +resource aiProject 'Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview' = if(!useExistingProject) { + parent: cogServiceReference + name: name + tags: tags + location: location + identity: { + type: 'SystemAssigned' + } + properties: { + description: desc + displayName: name + } +} + +@description('AI Project metadata including name, resource ID, and API endpoint.') +output aiProjectInfo aiProjectOutputType = { + name: useExistingProject ? existingProjName : aiProject.name + resourceId: useExistingProject ? azureExistingAIProjectResourceId : aiProject.id + apiEndpoint: useExistingProject ? existingProjEndpoint : aiProject.properties.endpoints['AI Foundry API'] +} + +@export() +@description('Output type representing AI project information.') +type aiProjectOutputType = { + @description('Required. Name of the AI project.') + name: string + + @description('Required. Resource ID of the AI project.') + resourceId: string + + @description('Required. API endpoint for the AI project.') + apiEndpoint: string +} diff --git a/infra/modules/role.bicep b/infra/modules/role.bicep new file mode 100644 index 000000000..f700f092f --- /dev/null +++ b/infra/modules/role.bicep @@ -0,0 +1,51 @@ +@description('The name of the role assignment resource. Typically generated using `guid()` for uniqueness.') +param name string + +@description('The object ID of the principal (user, group, or service principal) to whom the role will be assigned.') +param principalId string + +@description('The name of the existing Azure Cognitive Services account.') +param aiServiceName string + +resource cognitiveServiceExisting 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = { + name: aiServiceName +} + +resource aiUser 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { + name: '53ca6127-db72-4b80-b1b0-d745d6d5456d' +} + +resource aiDeveloper 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { + name: '64702f94-c441-49e6-a78b-ef80e0188fee' +} + +resource cognitiveServiceOpenAIUser 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { + name: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' +} + +resource aiUserAccessFoundry 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(name, 'aiUserAccessFoundry') + scope: cognitiveServiceExisting + properties: { + roleDefinitionId: aiUser.id + principalId: principalId + } +} + +resource aiDeveloperAccessFoundry 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(name, 'aiDeveloperAccessFoundry') + scope: cognitiveServiceExisting + properties: { + roleDefinitionId: aiDeveloper.id + principalId: principalId + } +} + +resource cognitiveServiceOpenAIUserAccessFoundry 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(name, 'cognitiveServiceOpenAIUserAccessFoundry') + scope: cognitiveServiceExisting + properties: { + roleDefinitionId: cognitiveServiceOpenAIUser.id + principalId: principalId + } +} diff --git a/src/backend/context/cosmos_memory_kernel.py b/src/backend/context/cosmos_memory_kernel.py index 64f96d4f1..02e732706 100644 --- a/src/backend/context/cosmos_memory_kernel.py +++ b/src/backend/context/cosmos_memory_kernel.py @@ -268,7 +268,7 @@ async def get_plan(self, plan_id: str) -> Optional[Plan]: async def get_all_plans(self) -> List[Plan]: """Retrieve all plans.""" - query = "SELECT * FROM c WHERE c.user_id=@user_id AND c.data_type=@data_type ORDER BY c._ts DESC OFFSET 0 LIMIT 10" + query = "SELECT * FROM c WHERE c.user_id=@user_id AND c.data_type=@data_type ORDER BY c._ts DESC" parameters = [ {"name": "@data_type", "value": "plan"}, {"name": "@user_id", "value": self.user_id}, diff --git a/src/backend/kernel_agents/group_chat_manager.py b/src/backend/kernel_agents/group_chat_manager.py index 69abae8c5..19215c34c 100644 --- a/src/backend/kernel_agents/group_chat_manager.py +++ b/src/backend/kernel_agents/group_chat_manager.py @@ -5,6 +5,7 @@ from context.cosmos_memory_kernel import CosmosMemoryContext from event_utils import track_event_if_configured from kernel_agents.agent_base import BaseAgent +from utils_date import format_date_for_user from models.messages_kernel import (ActionRequest, AgentMessage, AgentType, HumanFeedback, HumanFeedbackStatus, InputTask, Plan, Step, StepStatus) @@ -222,7 +223,9 @@ class Step(BaseDataModel): received_human_feedback_on_step = "" # Provide generic context to the model - general_information = f"Today's date is {datetime.now().date()}." + current_date = datetime.now().strftime("%Y-%m-%d") + formatted_date = format_date_for_user(current_date) + general_information = f"Today's date is {formatted_date}." # Get the general background information provided by the user in regards to the overall plan (not the steps) to add as context. plan = await self._memory_store.get_plan_by_session( diff --git a/src/backend/kernel_tools/hr_tools.py b/src/backend/kernel_tools/hr_tools.py index 7eb74c4f4..9951c0a1a 100644 --- a/src/backend/kernel_tools/hr_tools.py +++ b/src/backend/kernel_tools/hr_tools.py @@ -5,6 +5,7 @@ from models.messages_kernel import AgentType import json from typing import get_type_hints +from utils_date import format_date_for_user class HrTools: @@ -15,12 +16,15 @@ class HrTools: @staticmethod @kernel_function(description="Schedule an orientation session for a new employee.") async def schedule_orientation_session(employee_name: str, date: str) -> str: + formatted_date = format_date_for_user(date) + return ( f"##### Orientation Session Scheduled\n" f"**Employee Name:** {employee_name}\n" - f"**Date:** {date}\n\n" + f"**Date:** {formatted_date}\n\n" f"Your orientation session has been successfully scheduled. " f"Please mark your calendar and be prepared for an informative session.\n" + f"AGENT SUMMARY: I scheduled the orientation session for {employee_name} on {formatted_date}, as part of her onboarding process.\n" f"{HrTools.formatting_instructions}" ) diff --git a/src/backend/kernel_tools/product_tools.py b/src/backend/kernel_tools/product_tools.py index 5a30dee34..b5c119b76 100644 --- a/src/backend/kernel_tools/product_tools.py +++ b/src/backend/kernel_tools/product_tools.py @@ -9,6 +9,7 @@ from models.messages_kernel import AgentType import json from typing import get_type_hints +from utils_date import format_date_for_user class ProductTools: @@ -23,10 +24,11 @@ class ProductTools: async def add_mobile_extras_pack(new_extras_pack_name: str, start_date: str) -> str: """Add an extras pack/new product to the mobile plan for the customer. For example, adding a roaming plan to their service. The arguments should include the new_extras_pack_name and the start_date as strings. You must provide the exact plan name, as found using the get_product_info() function.""" formatting_instructions = "Instructions: returning the output of this function call verbatim to the user in markdown. Then write AGENT SUMMARY: and then include a summary of what you did." + formatted_date = format_date_for_user(start_date) analysis = ( f"# Request to Add Extras Pack to Mobile Plan\n" f"## New Plan:\n{new_extras_pack_name}\n" - f"## Start Date:\n{start_date}\n\n" + f"## Start Date:\n{formatted_date}\n\n" f"These changes have been completed and should be reflected in your app in 5-10 minutes." f"\n\n{formatting_instructions}" ) @@ -81,7 +83,8 @@ async def get_billing_date() -> str: now = datetime.now() start_of_month = datetime(now.year, now.month, 1) start_of_month_string = start_of_month.strftime("%Y-%m-%d") - return f"## Billing Date\nYour most recent billing date was **{start_of_month_string}**." + formatted_date = format_date_for_user(start_of_month_string) + return f"## Billing Date\nYour most recent billing date was **{formatted_date}**." @staticmethod @kernel_function( @@ -130,7 +133,8 @@ async def update_product_price(product_name: str, price: float) -> str: @kernel_function(description="Schedule a product launch event on a specific date.") async def schedule_product_launch(product_name: str, launch_date: str) -> str: """Schedule a product launch on a specific date.""" - message = f"## Product Launch Scheduled\nProduct **'{product_name}'** launch scheduled on **{launch_date}**." + formatted_date = format_date_for_user(launch_date) + message = f"## Product Launch Scheduled\nProduct **'{product_name}'** launch scheduled on **{formatted_date}**." return message diff --git a/src/backend/utils_date.py b/src/backend/utils_date.py new file mode 100644 index 000000000..d346e3cd0 --- /dev/null +++ b/src/backend/utils_date.py @@ -0,0 +1,24 @@ +import locale +from datetime import datetime +import logging +from typing import Optional + + +def format_date_for_user(date_str: str, user_locale: Optional[str] = None) -> str: + """ + Format date based on user's desktop locale preference. + + Args: + date_str (str): Date in ISO format (YYYY-MM-DD). + user_locale (str, optional): User's locale string, e.g., 'en_US', 'en_GB'. + + Returns: + str: Formatted date respecting locale or raw date if formatting fails. + """ + try: + date_obj = datetime.strptime(date_str, "%Y-%m-%d") + locale.setlocale(locale.LC_TIME, user_locale or '') + return date_obj.strftime("%B %d, %Y") + except Exception as e: + logging.warning(f"Date formatting failed for '{date_str}': {e}") + return date_str diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index db1c59f45..b711faa9c 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -4422,6 +4422,19 @@ "node": ">= 8" } }, + "node_modules/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", + "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.", + "license": "ISC" + }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, "node_modules/css-selector-parser": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-3.1.2.tgz", diff --git a/src/frontend/package.json b/src/frontend/package.json index 64e4c2c11..f45a785c2 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -67,4 +67,4 @@ "vite": "^5.4.19", "vitest": "^1.6.1" } -} \ No newline at end of file +} diff --git a/src/frontend/src/assets/WebWarning.svg b/src/frontend/src/assets/WebWarning.svg new file mode 100644 index 000000000..2dd158577 --- /dev/null +++ b/src/frontend/src/assets/WebWarning.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/frontend/src/components/NotFound/ContentNotFound.tsx b/src/frontend/src/components/NotFound/ContentNotFound.tsx new file mode 100644 index 000000000..dd17639b2 --- /dev/null +++ b/src/frontend/src/components/NotFound/ContentNotFound.tsx @@ -0,0 +1,87 @@ +import React from "react"; +import { + Button, + Image, + Text, + Title2, + makeStyles, + tokens, +} from "@fluentui/react-components"; +import NotFound from "../../assets/WebWarning.svg"; + +type ContentNotFoundProps = { + imageSrc?: string; + title?: string; + subtitle?: string; + primaryButtonText?: string; + onPrimaryButtonClick?: () => void; + secondaryButtonText?: string; + onSecondaryButtonClick?: () => void; +}; + +const DEFAULT_IMAGE = NotFound; +const DEFAULT_TITLE = ""; + +const useStyles = makeStyles({ + root: { + minHeight: "80vh", + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + textAlign: "center", + gap: tokens.spacingVerticalL, + padding: tokens.spacingVerticalXXL, + }, + image: { + width: "80px", + height: "80px", + objectFit: "contain", + }, + buttonGroup: { + display: "flex", + gap: tokens.spacingHorizontalM, + justifyContent: "center", + marginTop: tokens.spacingVerticalM, + }, +}); + +const ContentNotFound: React.FC = ({ + imageSrc = DEFAULT_IMAGE, + title = DEFAULT_TITLE, + subtitle, + primaryButtonText, + onPrimaryButtonClick, + secondaryButtonText, + onSecondaryButtonClick, +}) => { + const styles = useStyles(); + + return ( +
+ Content Not Found + {title} + {subtitle && ( + + {subtitle} + + )} + {(primaryButtonText || secondaryButtonText) && ( +
+ {primaryButtonText && ( + + )} + {secondaryButtonText && ( + + )} +
+ )} +
+ ); +}; + +export default ContentNotFound; diff --git a/src/frontend/src/components/content/PlanChat.tsx b/src/frontend/src/components/content/PlanChat.tsx index 62cf4dc8e..ef9f4fa8a 100644 --- a/src/frontend/src/components/content/PlanChat.tsx +++ b/src/frontend/src/components/content/PlanChat.tsx @@ -19,6 +19,7 @@ import "../../styles/Chat.css"; import "../../styles/prism-material-oceanic.css"; import { TaskService } from "@/services/TaskService"; import InlineToaster from "../toast/InlineToaster"; +import ContentNotFound from "../NotFound/ContentNotFound"; const PlanChat: React.FC = ({ planData, @@ -62,8 +63,6 @@ const PlanChat: React.FC = ({ } }, [input]); // or [inputValue, submittingChatDisableInput] - - const scrollToBottom = () => { messagesContainerRef.current?.scrollTo({ top: messagesContainerRef.current.scrollHeight, @@ -72,7 +71,10 @@ const PlanChat: React.FC = ({ setShowScrollButton(false); }; - if (!planData) return ; + if (!planData) + return ( + + ); return (
@@ -126,10 +128,13 @@ const PlanChat: React.FC = ({ style={{ height: 28, width: 28 }} icon={} /> -
- } appearance="filled" size="extra-small"> + } + appearance="filled" + size="extra-small" + > Sample data for demonstration purposes only.
@@ -151,13 +156,12 @@ const PlanChat: React.FC = ({ style={{ bottom: inputHeight, position: "absolute", // ensure this or your class handles it - right: 16, // optional, for right alignment + right: 16, // optional, for right alignment zIndex: 5, }} > Back to bottom - )}
@@ -167,7 +171,7 @@ const PlanChat: React.FC = ({ onChange={setInput} onEnter={() => OnChatSubmit(input)} disabledChat={ - planData.enableChat ? submittingChatDisableInput : true + planData?.enableChat ? submittingChatDisableInput : true } placeholder="Add more info to this task..." > @@ -175,7 +179,9 @@ const PlanChat: React.FC = ({ appearance="transparent" onClick={() => OnChatSubmit(input)} icon={} - disabled={planData.enableChat ? submittingChatDisableInput : true} + disabled={ + planData?.enableChat ? submittingChatDisableInput : true + } />
diff --git a/src/frontend/src/components/content/PlanPanelLeft.tsx b/src/frontend/src/components/content/PlanPanelLeft.tsx index 5e0d9f7e3..8f14d823c 100644 --- a/src/frontend/src/components/content/PlanPanelLeft.tsx +++ b/src/frontend/src/components/content/PlanPanelLeft.tsx @@ -29,7 +29,7 @@ import PanelFooter from "@/coral/components/Panels/PanelFooter"; import PanelUserCard from "../../coral/components/Panels/UserCard"; import { getUserInfoGlobal } from "@/api/config"; -const PlanPanelLeft: React.FC = ({ reloadTasks }) => { +const PlanPanelLeft: React.FC = ({ reloadTasks,restReload }) => { const { dispatchToast } = useToastController("toast"); const navigate = useNavigate(); const { planId } = useParams<{ planId: string }>(); @@ -42,7 +42,7 @@ const PlanPanelLeft: React.FC = ({ reloadTasks }) => { const [userInfo, setUserInfo] = useState( getUserInfoGlobal() ); - // Fetch plans + const loadPlansData = useCallback(async (forceRefresh = false) => { try { setPlansLoading(true); @@ -59,6 +59,15 @@ const PlanPanelLeft: React.FC = ({ reloadTasks }) => { } }, []); + useEffect(() => { + if (reloadTasks) { + loadPlansData(); + restReload?.(); + } + }, [reloadTasks, loadPlansData, restReload]); + // Fetch plans + + useEffect(() => { loadPlansData(); }, [loadPlansData]); diff --git a/src/frontend/src/components/content/TaskDetails.tsx b/src/frontend/src/components/content/TaskDetails.tsx index 142bec13a..efd16ecec 100644 --- a/src/frontend/src/components/content/TaskDetails.tsx +++ b/src/frontend/src/components/content/TaskDetails.tsx @@ -38,6 +38,16 @@ const TaskDetails: React.FC = ({ ); const agents = planData?.agents || []; + React.useEffect(() => { + // Initialize steps and counts from planData + setSteps(planData.steps || []); + setCompletedCount(planData?.plan.completed || 0); + setTotal(planData?.plan.total_steps || 1); + setProgress( + (planData?.plan.completed || 0) / (planData?.plan.total_steps || 1) + ); + }, [planData]); + const renderStatusIcon = (status: string) => { switch (status) { case "completed": @@ -59,17 +69,6 @@ const TaskDetails: React.FC = ({ ...step, human_approval_status: "accepted" as HumanFeedbackStatus, }; - - // Create a new array with the updated step - const updatedSteps = steps.map((s) => - s.id === step.id ? updatedStep : s - ); - - // Update local state to reflect changes immediately - - setSteps(updatedSteps); - setCompletedCount(completedCount + 1); // Increment completed count - setProgress((completedCount + 1) / total); // Update progress // Then call the main approval function // This could be your existing OnApproveStep function that handles API calls, etc. await OnApproveStep(updatedStep, total, completedCount + 1, true); @@ -88,15 +87,6 @@ const TaskDetails: React.FC = ({ human_approval_status: "rejected" as HumanFeedbackStatus, }; - // Create a new array with the updated step - const updatedSteps = steps.map((s) => - s.id === step.id ? updatedStep : s - ); - - // Update local state to reflect changes immediately - setSteps(updatedSteps); - setCompletedCount(completedCount + 1); // Increment completed count - setProgress((completedCount + 1) / total); // Update progress // Then call the main rejection function // This could be your existing OnRejectStep function that handles API calls, etc. await OnApproveStep(updatedStep, total, completedCount + 1, false); @@ -159,7 +149,7 @@ const TaskDetails: React.FC = ({
{step.human_approval_status !== "accepted" && step.human_approval_status !== "rejected" && ( - <> + <>