diff --git a/infra/core/agents/botservice.bicep b/infra/core/agents/botservice.bicep new file mode 100644 index 0000000..ea0ed46 --- /dev/null +++ b/infra/core/agents/botservice.bicep @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// +// Creates an Azure Bot Service (with a Microsoft Teams channel) for an +// activity-protocol digital worker. The bot's MSA App ID is the blueprint client +// ID, and the messaging endpoint points at the agent's activity protocol route so +// inbound Teams messages reach the deployed agent. + +param botName string +param displayName string +param msaAppId string +param endpoint string +param botServiceSku string = 'F0' + +resource botService 'Microsoft.BotService/botServices@2022-09-15' = { + name: botName + kind: 'azurebot' + location: 'global' + sku: { + name: botServiceSku + } + properties: { + displayName: displayName + endpoint: endpoint + msaAppId: msaAppId + msaAppTenantId: tenant().tenantId + msaAppType: 'SingleTenant' + } +} + +resource botServiceMsTeamsChannel 'Microsoft.BotService/botServices/channels@2021-03-01' = { + parent: botService + location: 'global' + name: 'MsTeamsChannel' + properties: { + channelName: 'MsTeamsChannel' + } +} diff --git a/infra/core/agents/deployment-script-umi.bicep b/infra/core/agents/deployment-script-umi.bicep new file mode 100644 index 0000000..b4f8208 --- /dev/null +++ b/infra/core/agents/deployment-script-umi.bicep @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// +// Creates a user-assigned managed identity used to run the digital-worker +// deployment script, and grants it the roles required to create a Managed Agent +// Identity Blueprint (MAIB) in the Foundry project via a data-plane call. + +targetScope = 'resourceGroup' + +@description('Name of the User Assigned Managed Identity to create') +param identityName string = 'foundry-deployment-script-umi' + +resource umi 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: identityName + location: resourceGroup().location +} + +resource contributorAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(resourceGroup().id, umi.id, 'Contributor') + scope: resourceGroup() + properties: { + roleDefinitionId: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'b24988ac-6180-42a0-ab88-20f7382dd24c' + ) + principalId: umi.properties.principalId + principalType: 'ServicePrincipal' + } +} + +var cognitiveServicesUserRoleDefinitionId = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a97b65f3-24c7-4388-baec-2e87135dc908') + +resource cogServicesUserRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(resourceGroup().id, umi.id, cognitiveServicesUserRoleDefinitionId) + scope: resourceGroup() + properties: { + roleDefinitionId: cognitiveServicesUserRoleDefinitionId + principalId: umi.properties.principalId + principalType: 'ServicePrincipal' + } +} + +output uamiClientId string = umi.properties.clientId +output uamiPrincipalId string = umi.properties.principalId +output uamiResourceId string = umi.id diff --git a/infra/core/agents/maib-creation-script.bicep b/infra/core/agents/maib-creation-script.bicep new file mode 100644 index 0000000..ac1041f --- /dev/null +++ b/infra/core/agents/maib-creation-script.bicep @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// +// Creates a Managed Agent Identity Blueprint (MAIB) in the Foundry project. The +// blueprint backs an activity-protocol digital worker. Because blueprint creation +// is a data-plane operation, it runs via an AzurePowerShell deployment script +// executed as the supplied user-assigned managed identity. + +targetScope = 'resourceGroup' + +@description('User-assigned managed identity resource ID that the script will run as') +param uamiResourceId string + +@description('Azure AI Project (Foundry) endpoint URL') +param azureAIProjectEndpoint string + +@description('Managed agent identity blueprint name for the Azure AI Project') +param maibName string + +resource psScript 'Microsoft.Resources/deploymentScripts@2023-08-01' = { + name: 'create-agent-script' + location: resourceGroup().location + kind: 'AzurePowerShell' + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${uamiResourceId}': {} + } + } + properties: { + azPowerShellVersion: '11.5' + timeout: 'PT15M' + retentionInterval: 'P1D' + arguments: '-AzureAIProjectEndpoint "${azureAIProjectEndpoint}" -MAIBName "${maibName}"' + environmentVariables: [ + { + name: 'RESOURCE_GROUP_NAME' + value: resourceGroup().name + } + ] + scriptContent: ''' + param( + [Parameter(Mandatory = $true)] + [string] $AzureAIProjectEndpoint, + [Parameter(Mandatory = $true)] + [string] $MAIBName + ) + + $ErrorActionPreference = "Stop" + + $maibUrl = "$($AzureAIProjectEndpoint)/managedagentidentityblueprints/$($MAIBName)?api-version=2025-11-15-preview" + + Write-Host "Connecting with managed identity..." + Connect-AzAccount -Identity + + Write-Host "Getting access token for https://ai.azure.com ..." + $tokenResponse = Get-AzAccessToken -ResourceUrl "https://ai.azure.com" + $aiAzureToken = $tokenResponse.Token | ConvertFrom-SecureString -AsPlainText + Write-Host "Token length: $($aiAzureToken.Length)" + + $headers = @{ + "Content-Type" = "application/json" + "Accept" = "application/json" + "Authorization" = "Bearer $aiAzureToken" + } + + Write-Host "Creating managed agent identity blueprint at: $maibUrl" + + $response = Invoke-RestMethod -Uri $maibUrl ` + -Method Put ` + -Headers $headers ` + -ErrorAction Stop + + Write-Host "" + Write-Host "Response:" + $response | ConvertTo-Json -Depth 100 | Write-Host + + $blueprintClientId = $response.agentIdentityBlueprint.clientId + + $DeploymentScriptOutputs = @{ + blueprintClientId = $blueprintClientId + } +''' + } +} + +output blueprintClientId string = psScript.properties.outputs.blueprintClientId diff --git a/infra/main.bicep b/infra/main.bicep index a4e03cb..b4dc573 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -113,6 +113,12 @@ param existingApplicationInsightsResourceId string = '' @description('Optional. Name of an existing Application Insights connection on the Foundry project. If provided, no new App Insights or connection will be created.') param existingAppInsightsConnectionName string = '' +@description('Enable creation of an activity-protocol digital worker: a Managed Agent Identity Blueprint (MAIB) and an Azure Bot Service with a Teams channel. Defaults to false so non-activity agents are unaffected.') +param enableDigitalWorker bool = false + +@description('Foundry agent name. Used to name the digital worker blueprint and bot, and to build the bot messaging endpoint. Required when enableDigitalWorker is true.') +param agentName string = '' + // Tags that should be applied to all resources. // // Note that 'azd-service-name' tags should be applied separately to service host resources. @@ -205,6 +211,42 @@ module acrForExistingProject 'core/host/acr.bicep' = if (shouldCreateAcrForExist } } +// Digital worker (activity protocol): MAIB + Bot Service. Created only when +// enableDigitalWorker is true and a new Foundry project is being provisioned. +var createDigitalWorker = enableDigitalWorker && !useExistingAiProject && !empty(agentName) +var digitalWorkerMaibName = '${agentName}-maib' +var digitalWorkerBotName = '${agentName}-bot' +var digitalWorkerFoundryEndpoint = useExistingAiProject ? '' : (createDigitalWorker ? aiProject.outputs.FOUNDRY_PROJECT_ENDPOINT : '') + +// 1. UMI used to run the MAIB creation deployment script (data-plane op). +module digitalWorkerUmi 'core/agents/deployment-script-umi.bicep' = if (createDigitalWorker) { + scope: rg + name: 'digital-worker-umi' +} + +// 2. Create the Managed Agent Identity Blueprint in the Foundry project. +module digitalWorkerMaib 'core/agents/maib-creation-script.bicep' = if (createDigitalWorker) { + scope: rg + name: 'digital-worker-maib' + params: { + uamiResourceId: digitalWorkerUmi!.outputs.uamiResourceId + azureAIProjectEndpoint: digitalWorkerFoundryEndpoint + maibName: digitalWorkerMaibName + } +} + +// 3. Create the Azure Bot Service (Teams channel) targeting the agent's activity endpoint. +module digitalWorkerBot 'core/agents/botservice.bicep' = if (createDigitalWorker) { + scope: rg + name: 'digital-worker-bot' + params: { + botName: digitalWorkerBotName + displayName: '${agentName} Bot' + msaAppId: digitalWorkerMaib!.outputs.blueprintClientId + endpoint: '${digitalWorkerFoundryEndpoint}/agents/${agentName}/endpoint/protocols/activityProtocol?api-version=2025-05-15-preview' + } +} + // Resources output AZURE_RESOURCE_GROUP string = resourceGroupName output AZURE_AI_ACCOUNT_ID string = useExistingAiProject ? existingAiProject.outputs.accountId : aiProject.outputs.accountId @@ -246,3 +288,8 @@ output AZURE_STORAGE_ACCOUNT_NAME string = useExistingAiProject ? existingAiProj // Connections output AI_PROJECT_CONNECTION_IDS_JSON string = useExistingAiProject ? string(existingAiProject.outputs.connectionIds) : string(aiProject.outputs.connectionIds) + +// Digital worker (activity protocol) +output MAIB_NAME string = createDigitalWorker ? digitalWorkerMaibName : '' +output AGENT_IDENTITY_BLUEPRINT_ID string = createDigitalWorker ? digitalWorkerMaib!.outputs.blueprintClientId : '' +output AGENT_NAME string = agentName diff --git a/infra/main.parameters.json b/infra/main.parameters.json index 694d32c..5b17f3f 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -73,6 +73,12 @@ }, "existingAppInsightsConnectionName": { "value": "${APPLICATIONINSIGHTS_CONNECTION_NAME=}" + }, + "enableDigitalWorker": { + "value": "${ENABLE_DIGITAL_WORKER=false}" + }, + "agentName": { + "value": "${AGENT_NAME=}" } } }