From 80797453e9ca0c20e4714d3fe1a87a5af1cfa554 Mon Sep 17 00:00:00 2001 From: Peter Hauge Date: Wed, 21 Jan 2026 15:17:57 -0800 Subject: [PATCH] Adding & updating files needed for the Product Return Agent sample --- .../Deployment/main.bicep | 99 +++ .../Deployment/sample-arm.json | 723 ++++++++++++++++++ .../Deployment/workflows.zip | Bin 0 -> 7660 bytes .../LogicApps/.funcignore | 7 + .../LogicApps/.gitignore | 7 + .../LogicApps/CalculateRefund/workflow.json | 64 ++ .../LogicApps/GetOrderHistory/workflow.json | 53 ++ .../ProductReturnAgent/workflow.json | 341 +++++++++ .../LogicApps/README.md | 74 ++ .../LogicApps/cloud.settings.json | 12 + .../LogicApps/connections.json | 14 + .../LogicApps/host.json | 15 + .../LogicApps/parameters.json | 1 + samples/product-return-agent-sample/README.md | 311 ++++++++ ...product-return-agent-sample.code-workspace | 27 + samples/readme.md | 1 + samples/shared/README.md | 203 +++++ .../shared/modules/deployment-script.bicep | 89 +++ samples/shared/modules/logicapp.bicep | 122 +++ samples/shared/modules/openai-rbac.bicep | 26 + samples/shared/modules/openai.bicep | 40 + samples/shared/modules/storage-rbac.bicep | 45 ++ samples/shared/modules/storage.bicep | 28 + samples/shared/scripts/BundleAssets.ps1 | 279 +++++++ samples/shared/templates/main.bicep.template | 99 +++ 25 files changed, 2680 insertions(+) create mode 100644 samples/product-return-agent-sample/Deployment/main.bicep create mode 100644 samples/product-return-agent-sample/Deployment/sample-arm.json create mode 100644 samples/product-return-agent-sample/Deployment/workflows.zip create mode 100644 samples/product-return-agent-sample/LogicApps/.funcignore create mode 100644 samples/product-return-agent-sample/LogicApps/.gitignore create mode 100644 samples/product-return-agent-sample/LogicApps/CalculateRefund/workflow.json create mode 100644 samples/product-return-agent-sample/LogicApps/GetOrderHistory/workflow.json create mode 100644 samples/product-return-agent-sample/LogicApps/ProductReturnAgent/workflow.json create mode 100644 samples/product-return-agent-sample/LogicApps/README.md create mode 100644 samples/product-return-agent-sample/LogicApps/cloud.settings.json create mode 100644 samples/product-return-agent-sample/LogicApps/connections.json create mode 100644 samples/product-return-agent-sample/LogicApps/host.json create mode 100644 samples/product-return-agent-sample/LogicApps/parameters.json create mode 100644 samples/product-return-agent-sample/README.md create mode 100644 samples/product-return-agent-sample/ai-product-return-agent-sample.code-workspace create mode 100644 samples/shared/README.md create mode 100644 samples/shared/modules/deployment-script.bicep create mode 100644 samples/shared/modules/logicapp.bicep create mode 100644 samples/shared/modules/openai-rbac.bicep create mode 100644 samples/shared/modules/openai.bicep create mode 100644 samples/shared/modules/storage-rbac.bicep create mode 100644 samples/shared/modules/storage.bicep create mode 100644 samples/shared/scripts/BundleAssets.ps1 create mode 100644 samples/shared/templates/main.bicep.template diff --git a/samples/product-return-agent-sample/Deployment/main.bicep b/samples/product-return-agent-sample/Deployment/main.bicep new file mode 100644 index 00000000..d4891016 --- /dev/null +++ b/samples/product-return-agent-sample/Deployment/main.bicep @@ -0,0 +1,99 @@ +// Auto-generated from shared/templates/main.bicep.template +// To customize: edit this file directly or delete to regenerate from template +// +// AI Product Return Agent - Azure Infrastructure as Code +// Deploys Logic Apps Standard with Azure OpenAI for autonomous product return decisions +// Uses managed identity exclusively (no secrets/connection strings) + +targetScope = 'resourceGroup' + +@description('Base name used for the resources that will be deployed (alphanumerics and hyphens only)') +@minLength(3) +@maxLength(60) +param BaseName string + +// uniqueSuffix for when we need unique values +var uniqueSuffix = uniqueString(resourceGroup().id) + +// URL to workflows.zip (replaced by BundleAssets.ps1 with https://raw.githubusercontent.com/modularity/logicapps-labs/product-return-sample/samples/product-return-agent-sample/Deployment/workflows.zip) +var workflowsZipUrl = 'https://raw.githubusercontent.com/modularity/logicapps-labs/product-return-sample/samples/product-return-agent-sample/Deployment/workflows.zip' + +// User-Assigned Managed Identity for Logic App → Storage authentication +resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: '${take(BaseName, 60)}-managedidentity' + location: resourceGroup().location +} + +// Storage Account for workflow runtime +module storage '../../shared/modules/storage.bicep' = { + name: '${take(BaseName, 43)}-storage-deployment' + params: { + storageAccountName: toLower(take(replace('${take(BaseName, 16)}${uniqueSuffix}', '-', ''), 24)) + location: resourceGroup().location + } +} + +// Azure OpenAI with gpt-4o-mini model +module openai '../../shared/modules/openai.bicep' = { + name: '${take(BaseName, 44)}-openai-deployment' + params: { + openAIName: '${take(BaseName, 54)}-openai' + location: resourceGroup().location + } +} + +// Logic Apps Standard with dual managed identities +module logicApp '../../shared/modules/logicapp.bicep' = { + name: '${take(BaseName, 42)}-logicapp-deployment' + params: { + logicAppName: '${take(BaseName, 22)}${uniqueSuffix}' + location: resourceGroup().location + storageAccountName: storage.outputs.storageAccountName + openAIEndpoint: openai.outputs.endpoint + openAIResourceId: openai.outputs.resourceId + managedIdentityId: userAssignedIdentity.id + } +} + +// RBAC: Logic App → Storage (Blob, Queue, Table Contributor roles) +module storageRbac '../../shared/modules/storage-rbac.bicep' = { + name: '${take(BaseName, 38)}-storage-rbac-deployment' + params: { + storageAccountName: storage.outputs.storageAccountName + logicAppPrincipalId: userAssignedIdentity.properties.principalId + } + dependsOn: [ + logicApp + ] +} + +// RBAC: Logic App → Azure OpenAI (Cognitive Services User role) +module openaiRbac '../../shared/modules/openai-rbac.bicep' = { + name: '${take(BaseName, 39)}-openai-rbac-deployment' + params: { + openAIName: openai.outputs.name + logicAppPrincipalId: logicApp.outputs.systemAssignedPrincipalId + } +} + +// Deploy workflows using deployment script with RBAC +module workflowDeployment '../../shared/modules/deployment-script.bicep' = { + name: '${take(BaseName, 42)}-workflow-deployment' + params: { + deploymentScriptName: '${BaseName}-deploy-workflows' + location: resourceGroup().location + userAssignedIdentityId: userAssignedIdentity.id + deploymentIdentityPrincipalId: userAssignedIdentity.properties.principalId + logicAppName: logicApp.outputs.name + resourceGroupName: resourceGroup().name + workflowsZipUrl: workflowsZipUrl + } + dependsOn: [ + storageRbac + openaiRbac + ] +} + +// Outputs +output logicAppName string = logicApp.outputs.name +output openAIEndpoint string = openai.outputs.endpoint diff --git a/samples/product-return-agent-sample/Deployment/sample-arm.json b/samples/product-return-agent-sample/Deployment/sample-arm.json new file mode 100644 index 00000000..66e97567 --- /dev/null +++ b/samples/product-return-agent-sample/Deployment/sample-arm.json @@ -0,0 +1,723 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "7356318468140071735" + } + }, + "parameters": { + "BaseName": { + "type": "string", + "minLength": 3, + "maxLength": 60, + "metadata": { + "description": "Base name used for the resources that will be deployed (alphanumerics and hyphens only)" + } + } + }, + "variables": { + "uniqueSuffix": "[uniqueString(resourceGroup().id)]", + "workflowsZipUrl": "https://raw.githubusercontent.com/modularity/logicapps-labs/product-return-sample/samples/product-return-agent-sample/Deployment/workflows.zip" + }, + "resources": [ + { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2023-01-31", + "name": "[format('{0}-managedidentity', take(parameters('BaseName'), 60))]", + "location": "[resourceGroup().location]" + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-storage-deployment', take(parameters('BaseName'), 43))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[toLower(take(replace(format('{0}{1}', take(parameters('BaseName'), 16), variables('uniqueSuffix')), '-', ''), 24))]" + }, + "location": { + "value": "[resourceGroup().location]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "6666359930464991611" + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "metadata": { + "description": "Storage account name" + } + }, + "location": { + "type": "string", + "metadata": { + "description": "Location for the storage account" + } + } + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2023-05-01", + "name": "[parameters('storageAccountName')]", + "location": "[parameters('location')]", + "sku": { + "name": "Standard_LRS" + }, + "kind": "StorageV2", + "properties": { + "supportsHttpsTrafficOnly": true, + "minimumTlsVersion": "TLS1_2", + "allowBlobPublicAccess": false, + "allowSharedKeyAccess": false + } + } + ], + "outputs": { + "storageAccountName": { + "type": "string", + "value": "[parameters('storageAccountName')]" + }, + "storageAccountId": { + "type": "string", + "value": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]" + }, + "blobServiceUri": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2023-05-01').primaryEndpoints.blob]" + }, + "queueServiceUri": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2023-05-01').primaryEndpoints.queue]" + }, + "tableServiceUri": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2023-05-01').primaryEndpoints.table]" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-openai-deployment', take(parameters('BaseName'), 44))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "openAIName": { + "value": "[format('{0}-openai', take(parameters('BaseName'), 54))]" + }, + "location": { + "value": "[resourceGroup().location]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "5370603553016721570" + } + }, + "parameters": { + "openAIName": { + "type": "string", + "metadata": { + "description": "Azure OpenAI account name" + } + }, + "location": { + "type": "string", + "metadata": { + "description": "Location for Azure OpenAI" + } + } + }, + "resources": [ + { + "type": "Microsoft.CognitiveServices/accounts", + "apiVersion": "2024-10-01", + "name": "[parameters('openAIName')]", + "location": "[parameters('location')]", + "kind": "OpenAI", + "sku": { + "name": "S0" + }, + "properties": { + "customSubDomainName": "[parameters('openAIName')]", + "publicNetworkAccess": "Enabled" + } + }, + { + "type": "Microsoft.CognitiveServices/accounts/deployments", + "apiVersion": "2024-10-01", + "name": "[format('{0}/{1}', parameters('openAIName'), 'gpt-4o-mini')]", + "sku": { + "name": "GlobalStandard", + "capacity": 50 + }, + "properties": { + "model": { + "format": "OpenAI", + "name": "gpt-4o-mini", + "version": "2024-07-18" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName'))]" + ] + } + ], + "outputs": { + "name": { + "type": "string", + "value": "[parameters('openAIName')]" + }, + "endpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName')), '2024-10-01').endpoint]" + }, + "resourceId": { + "type": "string", + "value": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName'))]" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-logicapp-deployment', take(parameters('BaseName'), 42))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "logicAppName": { + "value": "[format('{0}{1}', take(parameters('BaseName'), 22), variables('uniqueSuffix'))]" + }, + "location": { + "value": "[resourceGroup().location]" + }, + "storageAccountName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-storage-deployment', take(parameters('BaseName'), 43))), '2022-09-01').outputs.storageAccountName.value]" + }, + "openAIEndpoint": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-openai-deployment', take(parameters('BaseName'), 44))), '2022-09-01').outputs.endpoint.value]" + }, + "openAIResourceId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-openai-deployment', take(parameters('BaseName'), 44))), '2022-09-01').outputs.resourceId.value]" + }, + "managedIdentityId": { + "value": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}-managedidentity', take(parameters('BaseName'), 60)))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "2259432651138452624" + } + }, + "parameters": { + "logicAppName": { + "type": "string", + "metadata": { + "description": "Logic App name" + } + }, + "location": { + "type": "string", + "metadata": { + "description": "Location for Logic App" + } + }, + "storageAccountName": { + "type": "string", + "metadata": { + "description": "Storage account name" + } + }, + "openAIEndpoint": { + "type": "string", + "metadata": { + "description": "OpenAI endpoint" + } + }, + "openAIResourceId": { + "type": "string", + "metadata": { + "description": "OpenAI resource ID" + } + }, + "managedIdentityId": { + "type": "string", + "metadata": { + "description": "User-assigned managed identity resource ID for storage authentication" + } + } + }, + "resources": [ + { + "type": "Microsoft.Web/serverfarms", + "apiVersion": "2023-12-01", + "name": "[format('{0}-plan', parameters('logicAppName'))]", + "location": "[parameters('location')]", + "sku": { + "name": "WS1", + "tier": "WorkflowStandard" + }, + "kind": "elastic", + "properties": { + "maximumElasticWorkerCount": 20 + } + }, + { + "type": "Microsoft.Web/sites", + "apiVersion": "2023-12-01", + "name": "[format('{0}-logicapp', parameters('logicAppName'))]", + "location": "[parameters('location')]", + "kind": "functionapp,workflowapp", + "identity": { + "type": "SystemAssigned, UserAssigned", + "userAssignedIdentities": { + "[format('{0}', parameters('managedIdentityId'))]": {} + } + }, + "properties": { + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', format('{0}-plan', parameters('logicAppName')))]", + "siteConfig": { + "netFrameworkVersion": "v8.0", + "functionsRuntimeScaleMonitoringEnabled": true, + "appSettings": [ + { + "name": "FUNCTIONS_EXTENSION_VERSION", + "value": "~4" + }, + { + "name": "FUNCTIONS_WORKER_RUNTIME", + "value": "dotnet" + }, + { + "name": "AzureWebJobsStorage__managedIdentityResourceId", + "value": "[parameters('managedIdentityId')]" + }, + { + "name": "AzureWebJobsStorage__credential", + "value": "managedIdentity" + }, + { + "name": "AzureWebJobsStorage__blobServiceUri", + "value": "[format('https://{0}.blob.{1}', parameters('storageAccountName'), environment().suffixes.storage)]" + }, + { + "name": "AzureWebJobsStorage__queueServiceUri", + "value": "[format('https://{0}.queue.{1}', parameters('storageAccountName'), environment().suffixes.storage)]" + }, + { + "name": "AzureWebJobsStorage__tableServiceUri", + "value": "[format('https://{0}.table.{1}', parameters('storageAccountName'), environment().suffixes.storage)]" + }, + { + "name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE", + "value": "false" + }, + { + "name": "AzureFunctionsJobHost__extensionBundle__id", + "value": "Microsoft.Azure.Functions.ExtensionBundle.Workflows" + }, + { + "name": "AzureFunctionsJobHost__extensionBundle__version", + "value": "[1.*, 2.0.0)" + }, + { + "name": "APP_KIND", + "value": "workflowApp" + }, + { + "name": "WORKFLOWS_SUBSCRIPTION_ID", + "value": "[subscription().subscriptionId]" + }, + { + "name": "WORKFLOWS_LOCATION_NAME", + "value": "[parameters('location')]" + }, + { + "name": "WORKFLOWS_RESOURCE_GROUP_NAME", + "value": "[resourceGroup().name]" + }, + { + "name": "agent_openAIEndpoint", + "value": "[parameters('openAIEndpoint')]" + }, + { + "name": "agent_ResourceID", + "value": "[parameters('openAIResourceId')]" + } + ] + }, + "httpsOnly": true + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms', format('{0}-plan', parameters('logicAppName')))]" + ] + } + ], + "outputs": { + "name": { + "type": "string", + "value": "[format('{0}-logicapp', parameters('logicAppName'))]" + }, + "systemAssignedPrincipalId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Web/sites', format('{0}-logicapp', parameters('logicAppName'))), '2023-12-01', 'full').identity.principalId]" + }, + "quickTestUrl": { + "type": "string", + "value": "[format('https://{0}/api/ProductReturnAgent/triggers/When_a_HTTP_request_is_received/invoke?api-version=2022-05-01&sp=%2Ftriggers%2FWhen_a_HTTP_request_is_received%2Frun&sv=1.0&sig=', reference(resourceId('Microsoft.Web/sites', format('{0}-logicapp', parameters('logicAppName'))), '2023-12-01').defaultHostName)]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('{0}-openai-deployment', take(parameters('BaseName'), 44)))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-storage-deployment', take(parameters('BaseName'), 43)))]", + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}-managedidentity', take(parameters('BaseName'), 60)))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-storage-rbac-deployment', take(parameters('BaseName'), 38))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-storage-deployment', take(parameters('BaseName'), 43))), '2022-09-01').outputs.storageAccountName.value]" + }, + "logicAppPrincipalId": { + "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}-managedidentity', take(parameters('BaseName'), 60))), '2023-01-31').principalId]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "13372547588259048703" + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "metadata": { + "description": "Storage account name" + } + }, + "logicAppPrincipalId": { + "type": "string", + "metadata": { + "description": "Principal ID of the Logic App managed identity" + } + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('storageAccountName'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), parameters('logicAppPrincipalId'), 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b')]", + "principalId": "[parameters('logicAppPrincipalId')]", + "principalType": "ServicePrincipal" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('storageAccountName'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), parameters('logicAppPrincipalId'), '974c5e8b-45b9-4653-ba55-5f855dd0fb88')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')]", + "principalId": "[parameters('logicAppPrincipalId')]", + "principalType": "ServicePrincipal" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('storageAccountName'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), parameters('logicAppPrincipalId'), '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')]", + "principalId": "[parameters('logicAppPrincipalId')]", + "principalType": "ServicePrincipal" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('{0}-logicapp-deployment', take(parameters('BaseName'), 42)))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-storage-deployment', take(parameters('BaseName'), 43)))]", + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}-managedidentity', take(parameters('BaseName'), 60)))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-openai-rbac-deployment', take(parameters('BaseName'), 39))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "openAIName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-openai-deployment', take(parameters('BaseName'), 44))), '2022-09-01').outputs.name.value]" + }, + "logicAppPrincipalId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-logicapp-deployment', take(parameters('BaseName'), 42))), '2022-09-01').outputs.systemAssignedPrincipalId.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.34.44.8038", + "templateHash": "3822718305786172483" + } + }, + "parameters": { + "openAIName": { + "type": "string", + "metadata": { + "description": "OpenAI account name" + } + }, + "logicAppPrincipalId": { + "type": "string", + "metadata": { + "description": "Logic App managed identity principal ID" + } + } + }, + "variables": { + "cognitiveServicesOpenAIUserRoleId": "5e0bd9bd-7b93-4f28-af87-19fc36ad61bd" + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('openAIName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName')), parameters('logicAppPrincipalId'), variables('cognitiveServicesOpenAIUserRoleId'))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('cognitiveServicesOpenAIUserRoleId'))]", + "principalId": "[parameters('logicAppPrincipalId')]", + "principalType": "ServicePrincipal" + } + } + ], + "outputs": { + "roleAssignmentId": { + "type": "string", + "value": "[extensionResourceId(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName')), 'Microsoft.Authorization/roleAssignments', guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName')), parameters('logicAppPrincipalId'), variables('cognitiveServicesOpenAIUserRoleId')))]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('{0}-logicapp-deployment', take(parameters('BaseName'), 42)))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-openai-deployment', take(parameters('BaseName'), 44)))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-workflow-deployment', take(parameters('BaseName'), 42))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "deploymentScriptName": { + "value": "[format('{0}-deploy-workflows', parameters('BaseName'))]" + }, + "location": { + "value": "[resourceGroup().location]" + }, + "userAssignedIdentityId": { + "value": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}-managedidentity', take(parameters('BaseName'), 60)))]" + }, + "deploymentIdentityPrincipalId": { + "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}-managedidentity', take(parameters('BaseName'), 60))), '2023-01-31').principalId]" + }, + "logicAppName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-logicapp-deployment', take(parameters('BaseName'), 42))), '2022-09-01').outputs.name.value]" + }, + "resourceGroupName": { + "value": "[resourceGroup().name]" + }, + "workflowsZipUrl": { + "value": "[variables('workflowsZipUrl')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "6479226689635161201" + } + }, + "parameters": { + "location": { + "type": "string", + "metadata": { + "description": "Location for the deployment script resource" + } + }, + "deploymentScriptName": { + "type": "string", + "metadata": { + "description": "Name for the deployment script resource" + } + }, + "userAssignedIdentityId": { + "type": "string", + "metadata": { + "description": "User-assigned managed identity ID for deployment" + } + }, + "deploymentIdentityPrincipalId": { + "type": "string", + "metadata": { + "description": "Principal ID of the user-assigned managed identity used for deployment" + } + }, + "logicAppName": { + "type": "string", + "metadata": { + "description": "Name of the Logic App to deploy to" + } + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "Resource group name" + } + }, + "workflowsZipUrl": { + "type": "string", + "metadata": { + "description": "URL to the workflows.zip file" + } + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(resourceGroup().id, parameters('deploymentIdentityPrincipalId'), 'de139f84-1756-47ae-9be6-808fbbe84772')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'de139f84-1756-47ae-9be6-808fbbe84772')]", + "principalId": "[parameters('deploymentIdentityPrincipalId')]", + "principalType": "ServicePrincipal" + } + }, + { + "type": "Microsoft.Resources/deploymentScripts", + "apiVersion": "2023-08-01", + "name": "[parameters('deploymentScriptName')]", + "location": "[parameters('location')]", + "kind": "AzureCLI", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[format('{0}', parameters('userAssignedIdentityId'))]": {} + } + }, + "properties": { + "azCliVersion": "2.59.0", + "retentionInterval": "PT1H", + "timeout": "PT30M", + "cleanupPreference": "OnSuccess", + "environmentVariables": [ + { + "name": "LOGIC_APP_NAME", + "value": "[parameters('logicAppName')]" + }, + { + "name": "RESOURCE_GROUP", + "value": "[parameters('resourceGroupName')]" + }, + { + "name": "WORKFLOWS_ZIP_URL", + "value": "[parameters('workflowsZipUrl')]" + } + ], + "scriptContent": " #!/bin/bash\r\n set -e\r\n\r\n echo \"Downloading workflows.zip...\"\r\n wget -O workflows.zip \"$WORKFLOWS_ZIP_URL\"\r\n\r\n echo \"Deploying workflows to Logic App: $LOGIC_APP_NAME\"\r\n az functionapp deployment source config-zip \\\r\n --resource-group \"$RESOURCE_GROUP\" \\\r\n --name \"$LOGIC_APP_NAME\" \\\r\n --src workflows.zip\r\n\r\n echo \"Waiting 60 seconds for workflow registration and RBAC propagation...\"\r\n sleep 60\r\n\r\n echo \"Deployment completed successfully\"\r\n " + }, + "dependsOn": [ + "[resourceId('Microsoft.Authorization/roleAssignments', guid(resourceGroup().id, parameters('deploymentIdentityPrincipalId'), 'de139f84-1756-47ae-9be6-808fbbe84772'))]" + ] + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('{0}-logicapp-deployment', take(parameters('BaseName'), 42)))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-openai-rbac-deployment', take(parameters('BaseName'), 39)))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-storage-rbac-deployment', take(parameters('BaseName'), 38)))]", + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}-managedidentity', take(parameters('BaseName'), 60)))]" + ] + } + ], + "outputs": { + "logicAppName": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-logicapp-deployment', take(parameters('BaseName'), 42))), '2022-09-01').outputs.name.value]" + }, + "openAIEndpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-openai-deployment', take(parameters('BaseName'), 44))), '2022-09-01').outputs.endpoint.value]" + } + } +} \ No newline at end of file diff --git a/samples/product-return-agent-sample/Deployment/workflows.zip b/samples/product-return-agent-sample/Deployment/workflows.zip new file mode 100644 index 0000000000000000000000000000000000000000..86b4fa14ccc412768c5402a67cb2b6c0b46236ee GIT binary patch literal 7660 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 literal 0 HcmV?d00001 diff --git a/samples/product-return-agent-sample/LogicApps/.funcignore b/samples/product-return-agent-sample/LogicApps/.funcignore new file mode 100644 index 00000000..5235706e --- /dev/null +++ b/samples/product-return-agent-sample/LogicApps/.funcignore @@ -0,0 +1,7 @@ +local.settings.json +.vscode/ +__azurite* +__blobstorage__* +__queuestorage__* +node_modules/ +*.zip diff --git a/samples/product-return-agent-sample/LogicApps/.gitignore b/samples/product-return-agent-sample/LogicApps/.gitignore new file mode 100644 index 00000000..090e597d --- /dev/null +++ b/samples/product-return-agent-sample/LogicApps/.gitignore @@ -0,0 +1,7 @@ +.vscode/ +local.settings.json +__azurite* +__blobstorage__* +__queuestorage__* +node_modules/ +*.zip diff --git a/samples/product-return-agent-sample/LogicApps/CalculateRefund/workflow.json b/samples/product-return-agent-sample/LogicApps/CalculateRefund/workflow.json new file mode 100644 index 00000000..25b6b943 --- /dev/null +++ b/samples/product-return-agent-sample/LogicApps/CalculateRefund/workflow.json @@ -0,0 +1,64 @@ +{ + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "actions": { + "Calculate_Refund_Amount": { + "type": "Compose", + "inputs": { + "orderTotal": "@triggerBody()?['orderTotal']", + "reason": "@triggerBody()?['reason']", + "category": "@triggerBody()?['category']", + "condition": "@triggerBody()?['condition']", + "refundAmount": "@if(equals(triggerBody()?['reason'], 'defective'), triggerBody()?['orderTotal'], if(and(equals(triggerBody()?['category'], 'electronics'), equals(triggerBody()?['condition'], 'opened')), mul(triggerBody()?['orderTotal'], 0.8), if(equals(triggerBody()?['reason'], 'changed_mind'), sub(triggerBody()?['orderTotal'], 10), triggerBody()?['orderTotal'])))", + "fees": { + "restockingFee": "@if(and(equals(triggerBody()?['category'], 'electronics'), equals(triggerBody()?['condition'], 'opened')), mul(triggerBody()?['orderTotal'], 0.2), 0)", + "shippingFee": "@if(equals(triggerBody()?['reason'], 'changed_mind'), 10, 0)" + }, + "explanation": "@if(equals(triggerBody()?['reason'], 'defective'), 'Full refund for defective item', if(and(equals(triggerBody()?['category'], 'electronics'), equals(triggerBody()?['condition'], 'opened')), 'Refund minus 20% restocking fee for opened electronics', if(equals(triggerBody()?['reason'], 'changed_mind'), 'Refund minus $10 shipping fee', 'Full refund')))" + }, + "runAfter": {} + }, + "Response": { + "type": "Response", + "kind": "http", + "inputs": { + "statusCode": 200, + "body": "@outputs('Calculate_Refund_Amount')" + }, + "runAfter": { + "Calculate_Refund_Amount": [ + "SUCCEEDED" + ] + } + } + }, + "triggers": { + "manual": { + "type": "Request", + "kind": "Http", + "inputs": { + "schema": { + "type": "object", + "properties": { + "orderTotal": { + "type": "number" + }, + "reason": { + "type": "string" + }, + "category": { + "type": "string" + }, + "condition": { + "type": "string" + } + } + } + } + } + }, + "contentVersion": "1.0.0.0", + "outputs": {} + }, + "kind": "Stateful" +} diff --git a/samples/product-return-agent-sample/LogicApps/GetOrderHistory/workflow.json b/samples/product-return-agent-sample/LogicApps/GetOrderHistory/workflow.json new file mode 100644 index 00000000..1ef9cca9 --- /dev/null +++ b/samples/product-return-agent-sample/LogicApps/GetOrderHistory/workflow.json @@ -0,0 +1,53 @@ +{ + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "actions": { + "Order_Details": { + "type": "Compose", + "inputs": { + "orderId": "@triggerBody()?['orderId']", + "customerId": "@if(equals(triggerBody()?['orderId'], 'ORD001'), 'CUST001', if(equals(triggerBody()?['orderId'], 'ORD002'), 'CUST002', if(equals(triggerBody()?['orderId'], 'ORD003'), 'CUST003', if(equals(triggerBody()?['orderId'], 'ORD004'), 'CUST001', 'UNKNOWN'))))", + "product": "@if(equals(triggerBody()?['orderId'], 'ORD001'), 'Coffee Maker Pro', if(equals(triggerBody()?['orderId'], 'ORD002'), 'Premium Coffee Beans', if(equals(triggerBody()?['orderId'], 'ORD003'), 'Espresso Machine Deluxe', if(equals(triggerBody()?['orderId'], 'ORD004'), 'Coffee Grinder', 'Unknown Product'))))", + "category": "@if(equals(triggerBody()?['orderId'], 'ORD001'), 'electronics', if(equals(triggerBody()?['orderId'], 'ORD002'), 'perishable', if(equals(triggerBody()?['orderId'], 'ORD003'), 'electronics', if(equals(triggerBody()?['orderId'], 'ORD004'), 'electronics', 'other'))))", + "price": "@if(equals(triggerBody()?['orderId'], 'ORD001'), 150, if(equals(triggerBody()?['orderId'], 'ORD002'), 89, if(equals(triggerBody()?['orderId'], 'ORD003'), 450, if(equals(triggerBody()?['orderId'], 'ORD004'), 120, 0))))", + "purchaseDate": "@if(equals(triggerBody()?['orderId'], 'ORD001'), '2024-11-19', if(equals(triggerBody()?['orderId'], 'ORD002'), '2024-11-24', if(equals(triggerBody()?['orderId'], 'ORD003'), '2024-10-30', if(equals(triggerBody()?['orderId'], 'ORD004'), '2024-11-14', 'unknown'))))", + "daysOld": "@if(equals(triggerBody()?['orderId'], 'ORD001'), 15, if(equals(triggerBody()?['orderId'], 'ORD002'), 10, if(equals(triggerBody()?['orderId'], 'ORD003'), 35, if(equals(triggerBody()?['orderId'], 'ORD004'), 20, 0))))", + "condition": "@if(equals(triggerBody()?['orderId'], 'ORD001'), 'unopened', if(equals(triggerBody()?['orderId'], 'ORD002'), 'opened', if(equals(triggerBody()?['orderId'], 'ORD003'), 'unopened', if(equals(triggerBody()?['orderId'], 'ORD004'), 'opened', 'unknown'))))" + }, + "runAfter": {} + }, + "Response": { + "type": "Response", + "kind": "http", + "inputs": { + "statusCode": 200, + "body": "@outputs('Order_Details')" + }, + "runAfter": { + "Order_Details": [ + "SUCCEEDED" + ] + } + } + }, + "triggers": { + "manual": { + "type": "Request", + "kind": "Http", + "inputs": { + "schema": { + "type": "object", + "properties": { + "orderId": { + "type": "string" + } + } + } + } + } + }, + "contentVersion": "1.0.0.0", + "outputs": {} + }, + "kind": "Stateful" +} diff --git a/samples/product-return-agent-sample/LogicApps/ProductReturnAgent/workflow.json b/samples/product-return-agent-sample/LogicApps/ProductReturnAgent/workflow.json new file mode 100644 index 00000000..871c3a80 --- /dev/null +++ b/samples/product-return-agent-sample/LogicApps/ProductReturnAgent/workflow.json @@ -0,0 +1,341 @@ +{ + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "actions": { + "Product_Return_Agent": { + "type": "Agent", + "inputs": { + "parameters": { + "deploymentId": "gpt-4o-mini", + "messages": [ + { + "role": "system", + "content": "You are a product return agent.\n\n1. GATHER DATA: Call Get_order_details, Get_customer_status, Get_return_history, Get_return_policy, and Analyze_product_image\n\n2. MAKE DECISION: Read Get_return_policy and apply rules in order. Stop at first match.\n\n3. TAKE ACTION based on decision:\n - ESCALATE → Call Escalate_to_human\n - REJECT → Call Notify_customer(decision='REJECTED', refundAmount=0)\n - APPROVE → Call Calculate_refund, then Notify_customer(decision='APPROVED', refundAmount=result)\n\n4. OUTPUT (exact format):\nORDER ID: [id]\nDECISION: [APPROVED/REJECTED/ESCALATED]\nREFUND AMOUNT: $[amount]\nREASON: [brief explanation]\n\nIMPORTANT: Use Calculate_refund result. Don't calculate manually." + }, + { + "role": "user", + "content": "This is the return request - @{outputs('Return_Request_Summary')}" + } + ], + "agentModelType": "AzureOpenAI", + "agentModelSettings": { + "deploymentModelProperties": { + "name": "gpt-4o-mini", + "format": "OpenAI", + "version": "2024-07-18" + } + } + }, + "modelConfigurations": { + "model1": { + "referenceName": "agent" + } + } + }, + "limit": { + "count": 100 + }, + "tools": { + "Get_return_policy": { + "actions": { + "Return_Policy": { + "type": "Compose", + "inputs": "PRODUCT RETURN POLICY\n\nAPPLY RULES IN THIS ORDER (stop when a rule matches):\n\n1. ESCALATE TO HUMAN (highest priority):\n If ALL THREE conditions are true:\n - Customer status = 'Premium' (not 'Standard')\n - Return history flagged = true\n - Order price > $400\n Then: Escalate to human agent. Do not calculate refund.\n\n2. AUTO-REJECT:\n If ANY of these is true:\n - Order not found (customerId='UNKNOWN' or product='Unknown Product' or price=0)\n - Order is over 60 days old\n - Product category is 'perishable' AND condition is 'opened'\n Then: Reject the return. Refund = $0.\n\n3. APPROVE:\n If none of the above rules matched:\n - Call Calculate_refund to determine refund amount\n - Approve the return with the calculated refund" + } + }, + "description": "Get complete return policy with decision rules" + }, + "Get_order_details": { + "actions": { + "Call_GetOrderHistory_Workflow": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "GetOrderHistory" + } + }, + "body": { + "orderId": "@agentParameters('orderId')" + } + } + } + }, + "description": "Get order details including product, price, and purchase date", + "agentParameterSchema": { + "type": "object", + "properties": { + "orderId": { + "type": "string", + "description": "Order ID" + } + }, + "required": [ + "orderId" + ] + } + }, + "Get_customer_status": { + "actions": { + "Customer_Status": { + "type": "Compose", + "inputs": { + "customerId": "@agentParameters('customerId')", + "status": "@if(equals(agentParameters('customerId'), 'CUST003'), 'Premium', 'Standard')", + "returnWindow": "@if(equals(agentParameters('customerId'), 'CUST003'), 60, 30)" + } + } + }, + "description": "Get customer status (Premium or Standard) and return window", + "agentParameterSchema": { + "type": "object", + "properties": { + "customerId": { + "type": "string", + "description": "Customer ID" + } + }, + "required": [ + "customerId" + ] + } + }, + "Calculate_refund": { + "actions": { + "Call_CalculateRefund_Workflow": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "CalculateRefund" + } + }, + "body": { + "orderTotal": "@agentParameters('orderTotal')", + "reason": "@agentParameters('reason')", + "category": "@agentParameters('category')", + "condition": "@agentParameters('condition')" + } + } + } + }, + "description": "Calculate refund amount based on order total, reason, and product condition", + "agentParameterSchema": { + "type": "object", + "properties": { + "orderTotal": { + "type": "number", + "description": "Original order total" + }, + "reason": { + "type": "string", + "description": "Return reason: defective, changed_mind, wrong_item" + }, + "category": { + "type": "string", + "description": "Product category: electronics, perishable, other" + }, + "condition": { + "type": "string", + "description": "Product condition: opened or unopened" + } + }, + "required": [ + "orderTotal", + "reason", + "category", + "condition" + ] + } + }, + "Escalate_to_human": { + "actions": { + "Escalation_Response": { + "type": "Compose", + "inputs": { + "status": "ESCALATED", + "message": "This return request requires human review. A specialist will contact you within 24 hours.", + "reason": "@agentParameters('escalationReason')" + } + } + }, + "description": "Escalate complex cases to human review", + "agentParameterSchema": { + "type": "object", + "properties": { + "escalationReason": { + "type": "string", + "description": "Reason for escalation" + } + }, + "required": [ + "escalationReason" + ] + } + }, + "Analyze_product_image": { + "actions": { + "Image_Analysis_Result": { + "type": "Compose", + "inputs": { + "imageUrl": "@agentParameters('imageData')", + "damageLevel": "@if(contains(agentParameters('imageData'), 'ORD001'), 'minor', if(contains(agentParameters('imageData'), 'ORD002'), 'severe', if(contains(agentParameters('imageData'), 'ORD003'), 'none', if(contains(agentParameters('imageData'), 'ORD004'), 'minor', if(contains(agentParameters('imageData'), 'ORD005'), 'moderate', 'unknown')))))", + "confidence": "@if(contains(agentParameters('imageData'), 'ORD001'), 0.85, if(contains(agentParameters('imageData'), 'ORD002'), 0.95, if(contains(agentParameters('imageData'), 'ORD003'), 0.90, if(contains(agentParameters('imageData'), 'ORD004'), 0.87, if(contains(agentParameters('imageData'), 'ORD005'), 0.88, 0.0)))))", + "findings": "@if(contains(agentParameters('imageData'), 'ORD001'), createArray('Surface scratches'), if(contains(agentParameters('imageData'), 'ORD002'), createArray('Package torn', 'Contents exposed'), if(contains(agentParameters('imageData'), 'ORD003'), createArray('No visible damage'), if(contains(agentParameters('imageData'), 'ORD004'), createArray('Item has been used', 'Minor wear'), if(contains(agentParameters('imageData'), 'ORD005'), createArray('Packaging compromised'), createArray('Unable to analyze'))))))" + } + } + }, + "description": "Analyze product image to detect damage and condition", + "agentParameterSchema": { + "type": "object", + "properties": { + "imageData": { + "type": "string", + "description": "Product image URL or data" + } + }, + "required": [ + "imageData" + ] + } + }, + "Get_return_history": { + "actions": { + "Return_History_Result": { + "type": "Compose", + "inputs": { + "customerId": "@agentParameters('customerId')", + "returnCount": "@if(equals(agentParameters('customerId'), 'CUST001'), 1, if(equals(agentParameters('customerId'), 'CUST002'), 2, if(equals(agentParameters('customerId'), 'CUST003'), 5, if(equals(agentParameters('customerId'), 'CUST004'), 0, 0))))", + "flagged": "@if(equals(agentParameters('customerId'), 'CUST003'), true, false)", + "reason": "@if(equals(agentParameters('customerId'), 'CUST003'), 'Excessive returns in 6 months', '')" + } + } + }, + "description": "Get customer return history and fraud flags", + "agentParameterSchema": { + "type": "object", + "properties": { + "customerId": { + "type": "string", + "description": "Customer ID" + } + }, + "required": [ + "customerId" + ] + } + }, + "Notify_customer": { + "actions": { + "Notification_Result": { + "type": "Compose", + "inputs": { + "notificationSent": true, + "channel": "email", + "timestamp": "@utcNow()", + "messageId": "@guid()", + "customerId": "@agentParameters('customerId')", + "decision": "@agentParameters('decision')", + "refundAmount": "@coalesce(agentParameters('refundAmount'), 0)", + "message": "@coalesce(agentParameters('message'), '')" + } + } + }, + "description": "Send notification to customer about return decision", + "agentParameterSchema": { + "type": "object", + "properties": { + "customerId": { + "type": "string", + "description": "Customer ID" + }, + "decision": { + "type": "string", + "description": "Return decision: APPROVED, REJECTED, or ESCALATED" + }, + "refundAmount": { + "type": "number", + "description": "Refund amount (optional, defaults to 0 if not provided)" + }, + "message": { + "type": "string", + "description": "Notification message to customer" + } + }, + "required": [ + "customerId", + "decision" + ] + } + } + }, + "runAfter": { + "Return_Request_Summary": [ + "SUCCEEDED" + ] + } + }, + "Response": { + "type": "Response", + "kind": "Http", + "inputs": { + "statusCode": 200, + "body": "@outputs('Product_Return_Agent')?['lastAssistantMessage']" + }, + "runAfter": { + "Product_Return_Agent": [ + "SUCCEEDED" + ] + } + }, + "Return_Request_Summary": { + "type": "Compose", + "inputs": { + "orderId": "@triggerBody()?['orderId']", + "customerId": "@triggerBody()?['customerId']", + "reason": "@triggerBody()?['reason']", + "description": "@triggerBody()?['description']", + "imageData": "@triggerBody()?['imageData']", + "requestDate": "@utcNow()" + }, + "runAfter": {} + } + }, + "triggers": { + "manual": { + "type": "Request", + "kind": "Http", + "inputs": { + "schema": { + "type": "object", + "properties": { + "orderId": { + "type": "string" + }, + "customerId": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "description": { + "type": "string" + }, + "imageData": { + "type": "string" + } + }, + "required": [ + "orderId", + "customerId", + "reason" + ] + } + } + } + }, + "contentVersion": "1.0.0.0", + "outputs": {} + }, + "kind": "Agentic" +} diff --git a/samples/product-return-agent-sample/LogicApps/README.md b/samples/product-return-agent-sample/LogicApps/README.md new file mode 100644 index 00000000..b5cbe8a6 --- /dev/null +++ b/samples/product-return-agent-sample/LogicApps/README.md @@ -0,0 +1,74 @@ +# LogicApps + +This folder contains the Logic Apps Standard workflows for the AI Product Return Agent sample. + +## Workflows + +- **ProductReturnAgent** - Main agent workflow that orchestrates return approvals using AI +- **GetOrderHistory** - Returns mock order data for testing +- **CalculateRefund** - Calculates refund amounts based on return policies + +## Local Development + +To develop and test these workflows locally: + +1. Install [Azure Logic Apps (Standard) VS Code extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurelogicapps) +2. Install [Azure Functions Core Tools v4](https://learn.microsoft.com/azure/azure-functions/functions-run-local) +3. Install [Azurite](https://learn.microsoft.com/azure/storage/common/storage-use-azurite) for local storage emulation + +4. Update `cloud.settings.json` with your Azure OpenAI details: + ```json + { + "WORKFLOWS_SUBSCRIPTION_ID": "your-subscription-id", + "WORKFLOWS_LOCATION_NAME": "eastus2", + "WORKFLOWS_RESOURCE_GROUP_NAME": "your-resource-group", + "agent_openAIEndpoint": "https://your-openai.openai.azure.com/", + "agent_ResourceID": "/subscriptions/.../your-openai-resource" + } + ``` + +5. Start Azurite for local storage: + ```powershell + azurite --silent --location ./__azurite__ --debug ./__azurite__/debug.log + ``` + +6. Press F5 in VS Code to start the Logic App runtime + +7. Test workflows using the local endpoints displayed in the terminal + +## Deployment + +These workflows are automatically deployed when using the 1-click deployment. For manual deployment: + +1. Navigate to the parent folder and run: + ```powershell + cd ../1ClickDeploy + .\BundleAssets.ps1 + ``` + +2. This creates `workflows.zip` which can be deployed to Azure Logic Apps Standard using: + ```bash + az functionapp deployment source config-zip \ + --resource-group \ + --name \ + --src workflows.zip + ``` + +## Files + +- `connections.json` - Defines the Azure OpenAI connection using Managed Identity +- `host.json` - Logic Apps runtime configuration +- `parameters.json` - Workflow parameters (empty for this sample) +- `cloud.settings.json` - Cloud environment settings (for local development reference) +- `.funcignore` / `.gitignore` - Excludes development artifacts from deployment + +## Agent Configuration + +The ProductReturnAgent workflow uses Azure OpenAI GPT-4o-mini model with these settings: + +- **System prompt:** Instructs agent to call one tool per turn in sequence +- **Tools:** 5 tools for policy, orders, customer status, refund calculation, and escalation +- **Model:** gpt-4o-mini (version 2024-07-18) +- **Authentication:** Managed Identity (no API keys required) + +See the [main README](../README.md) for more details on testing and extending the workflows. diff --git a/samples/product-return-agent-sample/LogicApps/cloud.settings.json b/samples/product-return-agent-sample/LogicApps/cloud.settings.json new file mode 100644 index 00000000..b9be6a8b --- /dev/null +++ b/samples/product-return-agent-sample/LogicApps/cloud.settings.json @@ -0,0 +1,12 @@ +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "FUNCTIONS_WORKER_RUNTIME": "dotnet", + "WORKFLOWS_SUBSCRIPTION_ID": "your-subscription-id", + "WORKFLOWS_LOCATION_NAME": "eastus2", + "WORKFLOWS_RESOURCE_GROUP_NAME": "your-resource-group", + "agent_openAIEndpoint": "https://your-openai-endpoint.openai.azure.com/", + "agent_ResourceID": "/subscriptions/your-subscription-id/resourceGroups/your-resource-group/providers/Microsoft.CognitiveServices/accounts/your-openai-account" + } +} diff --git a/samples/product-return-agent-sample/LogicApps/connections.json b/samples/product-return-agent-sample/LogicApps/connections.json new file mode 100644 index 00000000..806d743b --- /dev/null +++ b/samples/product-return-agent-sample/LogicApps/connections.json @@ -0,0 +1,14 @@ +{ + "agentConnections": { + "agent": { + "displayName": "Azure OpenAI Connection", + "authentication": { + "type": "ManagedServiceIdentity" + }, + "endpoint": "@appsetting('agent_openAIEndpoint')", + "resourceId": "@appsetting('agent_ResourceID')", + "type": "model" + } + }, + "managedApiConnections": {} +} diff --git a/samples/product-return-agent-sample/LogicApps/host.json b/samples/product-return-agent-sample/LogicApps/host.json new file mode 100644 index 00000000..fa69cd6f --- /dev/null +++ b/samples/product-return-agent-sample/LogicApps/host.json @@ -0,0 +1,15 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "maxTelemetryItemsPerSecond": 20 + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle.Workflows", + "version": "[1.*, 2.0.0)" + } +} diff --git a/samples/product-return-agent-sample/LogicApps/parameters.json b/samples/product-return-agent-sample/LogicApps/parameters.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/samples/product-return-agent-sample/LogicApps/parameters.json @@ -0,0 +1 @@ +{} diff --git a/samples/product-return-agent-sample/README.md b/samples/product-return-agent-sample/README.md new file mode 100644 index 00000000..c1d84a7e --- /dev/null +++ b/samples/product-return-agent-sample/README.md @@ -0,0 +1,311 @@ +# AI Product Return Agent + +An AI-powered product return system that automates the evaluation of return requests using Azure Logic Apps Standard and Azure OpenAI. The agent autonomously analyzes return requests, validates policies, checks order details, evaluates customer status, calculates refunds, and makes approval decisions or escalates complex cases to human reviewers. + +--- + +## Deploy + +**Prerequisites:** +- Azure subscription with contributor access +- Region supporting Azure OpenAI (GPT-4o-mini) and Logic Apps Standard - see [region selection](#region-selection) + +**Deploy to your Azure subscription:** + +[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fmodularity%2Flogicapps-labs%2Fproduct-return-sample%2Fsamples%2Fproduct-return-agent-sample%2FDeployment%2Fsample-arm.json) + +
+What happens when you deploy + +1. Opens Azure Portal and prompts for subscription, [resource group](https://learn.microsoft.com/azure/azure-resource-manager/management/manage-resource-groups-portal) (create new recommended: `rg-productreturn`) +2. Provisions Azure resources (Logic App, OpenAI, Storage, App Service Plan, Managed Identity) +3. Configures [RBAC (Role-Based Access Control)](https://learn.microsoft.com/azure/role-based-access-control/overview) permissions for passwordless authentication +4. Deploys AI agent workflows with built-in test scenarios + +
+ +
+What gets deployed + +| Resource | Purpose | +|----------|----------| +| Logic App Standard | Hosts AI agent workflows | +| Azure OpenAI | GPT-4o-mini model for agent reasoning | +| Storage Account | Workflow state and run history | +| App Service Plan | Compute resources | +| Managed Identity | Passwordless authentication | + +See [Deployment automation](#learn-more) and [Sample data approach](#learn-more) for technical details. + +
+ +
+Region selection + +Recommended regions: East US 2, West Europe, Sweden Central, North Central US + +See regional availability: +- [Azure OpenAI models](https://learn.microsoft.com/azure/ai-services/openai/concepts/models#model-summary-table-and-region-availability) +- [Logic Apps Standard](https://azure.microsoft.com/explore/global-infrastructure/products-by-region/) + +
+ +
+Resource naming + +Resources use `{projectName}` for subscription-scoped resources and `{projectName}{uniqueId}` for globally-unique resources: + +| Resource | Example (projectName = "productreturn") | +|----------|----------------------------------| +| Resource Group | `rg-productreturn` | +| Logic App | `productreturnxyz123-logicapp` | +| Azure OpenAI | `productreturn-openai` | +| Storage Account | `productreturnxyz123` | + +
+ +--- + +## Explore + +After deployment, test the agent with different return scenarios to see how it autonomously makes decisions. + +### Run a test + +1. Open [Azure Portal](https://portal.azure.com) > your resource group > Logic App > **Workflows** > **ProductReturnAgent** > [**Run history**](https://learn.microsoft.com/azure/logic-apps/monitor-logic-apps#review-runs-history) +2. Click **Run** > [**Run with payload**](https://learn.microsoft.com/azure/logic-apps/test-logic-apps-track-results#run-with-payload) +3. Paste one of the test payloads below > **Run** (returns success) +4. Click **Refresh** > click the **Identifier** to open monitoring view +5. In **Agent log** tab, review which tools the agent called +6. In workflow, click **Product Return Agent** action > **Outputs** tab > verify `decision` and `refundAmount` fields + +**Test these scenarios to see different decision paths:** + +
+Test scenario 1: Defective item - Auto-approval + +Coffee maker reported as defective, within return window: + +```json +{"orderId":"ORD001","customerId":"CUST001","reason":"defective","description":"Coffee maker stopped working after 10 days","imageData":"https://example.com/images/ORD001-product.jpg"} +``` + +**Expected result:** `decision` = `"APPROVED"` with `refundAmount` = `150` in Product Return Agent action outputs, full refund for defective item + +
+ +
+Test scenario 2: Opened perishable - Auto-rejection + +Opened coffee beans, perishable item: + +```json +{"orderId":"ORD002","customerId":"CUST002","reason":"changed_mind","description":"Don't like the flavor","imageData":"https://example.com/images/ORD002-product.jpg"} +``` + +**Expected result:** `decision` = `"REJECTED"` with `refundAmount` = `0` in Product Return Agent action outputs, perishable items cannot be returned once opened + +
+ +
+Test scenario 3: Order not found - Auto-rejection + +Order doesn't exist in system: + +```json +{"orderId":"ORD005","customerId":"CUST004","reason":"changed_mind","description":"Want to return this item","imageData":"https://example.com/images/ORD005-product.jpg"} +``` + +**Expected result:** `decision` = `"REJECTED"` with `refundAmount` = `0` in Product Return Agent action outputs, order not found + +
+ +
+Test scenario 4: High-value fraud risk - Escalation + +Premium customer with excessive return history, expensive item: + +```json +{"orderId":"ORD003","customerId":"CUST003","reason":"changed_mind","description":"Decided to get a different model","imageData":"https://example.com/images/ORD003-product.jpg"} +``` + +**Expected result:** `decision` = `"ESCALATED"` with `refundAmount` = `0` in Product Return Agent action outputs. Agent log shows "Escalate_to_human" tool call for manual review. + +
+ +
+Test scenario 5: Opened electronics - Approved with fee + +Opened electronics with restocking fee: + +```json +{"orderId":"ORD004","customerId":"CUST001","reason":"changed_mind","description":"Found a better price elsewhere","imageData":"https://example.com/images/ORD004-opened.jpg"} +``` + +**Expected result:** `decision` = `"APPROVED"` with `refundAmount` = `96` in Product Return Agent action outputs (20% restocking fee applied to $120 order) + +
+ +**Tips:** +- Review **Agent log** tab to see which tools the agent called +- Check **Metadata** tab (under Product Return Agent action) for token usage statistics +- Runs complete in 5-15 seconds +- [Learn more about reviewing agent execution](https://learn.microsoft.com/azure/logic-apps/create-autonomous-agent-workflows#review-tool-execution-data) + +--- + +## Extend + +This sample uses built-in test data to eliminate external dependencies. Here's how to extend it for production use: + +### Replace demo services + +| Component | Demo Implementation | Production Options | +|-----------|----------------------|-------------------| +| Order Database | Static mock data (5 orders) | SQL Database, Cosmos DB, E-commerce API, ERP systems | +| Customer Management | Hardcoded Premium status | CRM systems (Dynamics 365, Salesforce), Customer database | +| Image Analysis | Pattern-matching mock | Azure AI Vision, Custom Vision for damage detection | +| Refund Processing | Calculation only | Payment gateways (Stripe, PayPal), ERP systems | +| Human Escalation | Compose action response | Microsoft Teams Adaptive Cards, ServiceNow, Jira | +| Notifications | Template responses | Office 365 Outlook, SendGrid, Azure Communication Services | + +### Customize workflows + +**Option 1: Edit in Azure Portal** +- Navigate to your Logic App > Workflows > select workflow > **Edit** +- Use the visual designer to modify workflow logic +- [Learn more about editing workflows in Azure Portal](https://learn.microsoft.com/azure/logic-apps/create-single-tenant-workflows-azure-portal) + +**Option 2: Edit in VS Code** +- Follow setup instructions in [`LogicApps/README.md`](LogicApps/README.md) +- Edit workflow JSON files locally +- Deploy changes using Azure Logic Apps VS Code extension + +--- + +## Workflows + +Three workflows process product return requests using autonomous AI decision-making: + +
+Workflow details + +### ProductReturnAgent + +Orchestrates return approval using an AI agent. The agent evaluates requests against business rules, autonomously selecting and sequencing tools. + +**Agent Tools:** +- **Get_return_policy** - Retrieves return policy rules and conditions +- **Get_order_details** - Fetches order information including product, price, purchase date +- **Analyze_product_image** - Analyzes product photos to detect damage and assess condition +- **Get_return_history** - Checks customer return patterns and fraud flags +- **Get_customer_status** - Checks if customer is Premium (60-day window) or Standard (30-day window) +- **Calculate_refund** - Computes refund amount based on reason, category, and condition +- **Notify_customer** - Sends email notification with return decision and refund details +- **Escalate_to_human** - Routes complex cases to human review + +**Process Flow:** + +```mermaid +flowchart TD + A[HTTP Trigger
manual] --> B[Return Request Summary] + B --> C[Product Return Agent
AI Orchestrator] + + C --> D{Agent Loop
Iterations} + + D -->|Tool 1| E[Get Return Policy] + D -->|Tool 2| F[Get Order Details] + D -->|Tool 3| G[Get Customer Status] + D -->|Tool 4| H[Calculate Refund] + D -->|Tool 5| I[Escalate to Human] + + E --> D + F --> D + G --> D + H --> D + I --> J[Manual Review] + + D --> K[Response] + J --> K +``` + +### GetOrderHistory + +Retrieves simulated order data including product details, purchase date, and condition. In production, this would integrate with e-commerce or ERP systems. + +### CalculateRefund + +Evaluates refund amounts based on return reason, product category, and condition. Returns calculated refund following business rules for restocking fees and shipping charges. + +
+ +
+Required Connections + +This sample uses Azure OpenAI with Managed Identity authentication for passwordless access. + +| Connection Name | Connector Name | Connector Type | Purpose | +|-----------------|----------------|----------------|---------| +| Azure OpenAI Connection | Azure OpenAI | Agent | Powers the AI agent decision-making in ProductReturnAgent workflow | + +**Authentication:** System-Assigned Managed Identity with `Cognitive Services OpenAI User` role assigned to Azure OpenAI resource during deployment. + +
+ +--- + +## Learn more + +
+Troubleshooting + +| Issue | Solution | +|-------|----------| +| **CustomDomainInUse** | Use different project name. [Purge deleted resources](https://learn.microsoft.com/azure/ai-services/recover-purge-resources) if needed. | +| **InsufficientQuota** | Try different [region](#region-selection) or [request quota increase](https://learn.microsoft.com/azure/ai-services/openai/how-to/quota). | +| **Deployment timeout** | Allow 15 min. [View Activity Log](https://learn.microsoft.com/azure/azure-monitor/essentials/activity-log). Redeploy: resource group > Deployments > select > Redeploy. | +| **Unauthorized** | Wait 2-3 min for RBAC propagation. [Verify role assignments](https://learn.microsoft.com/azure/logic-apps/authenticate-with-managed-identity?tabs=standard). | +| **ajaxExtended call failed** | Designer: rename trigger "manual" → "manual2" > save > rename back > save. [Details](https://learn.microsoft.com/answers/questions/2046895). | +| **Run stuck** | Wait 1-2 min, refresh. Check run history for errors. Verify model is active. | + +
+ +
+Deployment automation + +The Deploy to Azure button uses a two-stage process: + +**Build** (manual via [`BundleAssets.ps1`](../shared/scripts/BundleAssets.ps1)): +- Compiles [Bicep modules](../shared/modules/) → [`sample-arm.json`](Deployment/sample-arm.json) +- Bundles [workflow definitions](LogicApps/) → [`workflows.zip`](Deployment/workflows.zip) + +**Deploy** (automated): +- [ARM (Azure Resource Manager)](https://learn.microsoft.com/azure/azure-resource-manager/templates/overview) template provisions Azure resources +- Embedded deployment script configures RBAC and deploys workflows + +[Learn about ARM deployment scripts](https://learn.microsoft.com/azure/azure-resource-manager/bicep/deployment-script-bicep) + +
+ +
+Sample data approach + +This sample uses built-in test data to simplify exploration: +- **Order database:** `Compose` actions with 5 mock orders +- **Customer status:** Hardcoded Premium detection +- **Image analysis:** Pattern-matching mock +- **Refund calculation:** Formula-based logic +- **Human escalation:** Conditional logic (no Teams integration) + +For production integration options, see [Extend](#extend). + +
+ +
+Resources + +**Agent workflows:** [Create autonomous agents](https://learn.microsoft.com/azure/logic-apps/create-autonomous-agent-workflows) | [Best practices](https://learn.microsoft.com/azure/logic-apps/create-autonomous-agent-workflows#best-practices-for-agents-and-tools) + +**Azure OpenAI:** [System messages](https://learn.microsoft.com/azure/ai-services/openai/concepts/system-message) | [Managed Identity](https://learn.microsoft.com/azure/logic-apps/authenticate-with-managed-identity) + +
diff --git a/samples/product-return-agent-sample/ai-product-return-agent-sample.code-workspace b/samples/product-return-agent-sample/ai-product-return-agent-sample.code-workspace new file mode 100644 index 00000000..84acaf5f --- /dev/null +++ b/samples/product-return-agent-sample/ai-product-return-agent-sample.code-workspace @@ -0,0 +1,27 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "files.exclude": { + "**/.git": true, + "**/__azurite*": true, + "**/__blobstorage__*": true, + "**/__queuestorage__*": true, + "**/node_modules": true + }, + "azureLogicAppsStandard.deploySubpath": "LogicApps", + "azureLogicAppsStandard.projectLanguage": "JavaScript", + "debug.internalConsoleOptions": "neverOpen" + }, + "extensions": { + "recommendations": [ + "ms-azuretools.vscode-azurelogicapps", + "ms-azuretools.vscode-azurefunctions", + "ms-vscode.azure-account", + "Azurite.azurite" + ] + } +} diff --git a/samples/readme.md b/samples/readme.md index d2a78252..cc356dc6 100644 --- a/samples/readme.md +++ b/samples/readme.md @@ -6,6 +6,7 @@ You can use this folder to share Logic Apps sample. - [**Sample Logic Apps Workspace**](./sample-logicapp-workspace/): This is a simple request response project, just to exemplify the required structure. - [**AI Loan Agent**](./ai-loan-agent-sample/): AI-powered loan approval system that automates the evaluation of vehicle loan applications using Azure Logic Apps Standard and Azure OpenAI. +- [**AI Product Return Agent**](./product-return-agent-sample/): AI-powered product return system that automates the evaluation of return requests using Azure Logic Apps Standard and Azure OpenAI. Features autonomous decision-making with policy validation, refund calculations, and human escalation. ## How to contribute diff --git a/samples/shared/README.md b/samples/shared/README.md new file mode 100644 index 00000000..45d15760 --- /dev/null +++ b/samples/shared/README.md @@ -0,0 +1,203 @@ +# Shared Infrastructure for Logic Apps Samples + +Reusable Bicep modules, templates, and scripts that provide consistent infrastructure across all Logic Apps samples. + +## Quick Start + +```powershell +# 1. Create folder structure +samples/your-sample-name/ +├── Deployment/ # Generated assets (empty initially) +└── LogicApps/ # Your workflows go here + +# 2. Generate deployment files +.\samples\shared\scripts\BundleAssets.ps1 -Sample "your-sample-name" + +# 3. Deploy infrastructure +az group create --name rg-test --location westeurope +az deployment group create ` + --resource-group rg-test ` + --template-file samples/your-sample-name/Deployment/main.bicep ` + --parameters BaseName=test123 + +# 4. Upload local workflows (deployment-script 404 is expected!) +az webapp deploy ` + --name test123-logicapp ` + --resource-group rg-test ` + --src-path samples/your-sample-name/Deployment/workflows.zip ` + --type zip + +# 5. Test your workflows in Azure Portal +``` + +## Directory Structure + +``` +shared/ +├── modules/ # 6 reusable Bicep modules +│ ├── storage.bicep +│ ├── openai.bicep +│ ├── logicapp.bicep +│ ├── storage-rbac.bicep +│ ├── openai-rbac.bicep +│ └── deployment-script.bicep +├── scripts/ +│ └── BundleAssets.ps1 # Generates deployment artifacts +└── templates/ + └── main.bicep.template # Infrastructure template +``` + +## Bicep Modules + +Six reusable modules provide complete Logic Apps infrastructure: + +| Module | Purpose | +|--------|---------| +| **storage.bicep** | Storage Account for Logic App runtime (managed identity only, HTTPS/TLS 1.2, no shared keys) | +| **openai.bicep** | Azure OpenAI S0 with gpt-4o-mini model (GlobalStandard, 150K tokens) | +| **logicapp.bicep** | Logic App Standard with App Service Plan (WS1 SKU, system + user-assigned identities) | +| **storage-rbac.bicep** | Storage Blob/Queue/Table Data Contributor roles for Logic App | +| **openai-rbac.bicep** | Cognitive Services OpenAI User role for Logic App | +| **deployment-script.bicep** | Downloads and deploys workflows.zip using deployment script | + +## BundleAssets.ps1 Script + +Generates all deployment artifacts for a sample: + +```powershell +.\samples\shared\scripts\BundleAssets.ps1 -Sample "your-sample-name" +``` + +**Generates:** +1. `main.bicep` - From template (only if doesn't exist, preserves customizations) +2. `sample-arm.json` - Compiled ARM template +3. `workflows.zip` - Bundled LogicApps folder + +**How it works:** +- Uses hardcoded `Azure/logicapps-labs` main branch for upstream URLs +- Never overwrites existing `main.bicep` (delete to regenerate) +- Replaces `{{WORKFLOWS_ZIP_URL}}` template placeholder with upstream URL +- Requires Bicep CLI and PowerShell 5.1+ + +**Why upstream URLs?** The generated `workflowsZipUrl` parameter points to the upstream main branch so the "Deploy to Azure" button works for end users. The deployment-script module downloads and deploys workflows.zip from this URL automatically. For local testing before merge, use `az webapp deploy` to upload your local zip file instead. + +## Creating a New Sample + +### 1. Create Folder Structure + +``` +samples/your-sample-name/ +├── Deployment/ # Empty (generated by script) +└── LogicApps/ # Your workflows + ├── host.json + ├── connections.json + └── YourWorkflow/ + └── workflow.json +``` + +### 2. Generate Deployment Files + +```powershell +.\samples\shared\scripts\BundleAssets.ps1 -Sample "your-sample-name" +``` + +Creates `Deployment/main.bicep` with: +- 6 shared module references +- `workflowsZipUrl` parameter pointing to upstream main (for "Deploy to Azure" button) +- Standard BaseName and location parameters + +The upstream URL enables the deployment-script module to automatically download and deploy workflows.zip when users click "Deploy to Azure". + +### 3. Customize (Optional) + +Edit `Deployment/main.bicep` to add sample-specific resources or configurations. The script won't overwrite your changes. + +### 4. Test Locally + +```powershell +# Create resource group (use supported region) +az group create --name rg-test --location westeurope + +# Deploy infrastructure +az deployment group create ` + --resource-group rg-test ` + --template-file samples/your-sample-name/Deployment/main.bicep ` + --parameters BaseName=test123 + +# Upload local workflows (deployment-script 404 is normal!) +az webapp deploy ` + --name test123-logicapp ` + --resource-group rg-test ` + --src-path samples/your-sample-name/Deployment/workflows.zip ` + --type zip + +# Test workflows in Azure Portal +``` + +### 5. Commit After Testing + +```powershell +git add samples/your-sample-name/ +git commit -m "Add your-sample-name sample" +git push +``` + +## Local Testing Details + +### Prerequisites + +- Azure CLI and Bicep CLI installed +- Azure subscription with: + - Azure OpenAI access ([request here](https://aka.ms/oai/access)) + - Logic Apps Standard quota +- Supported regions: **eastus2**, **westeurope**, **australiaeast** + +### Expected Deployment Behavior + +**Infrastructure deployment:** +- ✅ Storage Account deploys successfully +- ✅ Azure OpenAI deploys successfully +- ✅ Logic App deploys successfully +- ✅ RBAC roles assigned successfully +- ❌ **deployment-script fails with 404** - This is **expected**! workflows.zip isn't on main yet. + +**Solution:** Use `az webapp deploy` to upload your local workflows.zip directly. + +### Validation Checklist + +After deployment: +- ✅ All resources visible in Azure Portal +- ✅ Logic App has system + user-assigned managed identities +- ✅ Storage uses managed identity only (no keys) +- ✅ Workflows deployed and visible in Logic App +- ✅ Workflows execute successfully +- ✅ OpenAI connections work + +### Troubleshooting + +| Issue | Solution | +|-------|----------| +| **Deployment-script 404** | Normal for local testing - use `az webapp deploy` instead | +| **Logic Apps quota error** | Try different region: eastus2, westeurope, or australiaeast | +| **OpenAI access denied** | Request access at https://aka.ms/oai/access | +| **Region not supported** | Use eastus2, westeurope, or australiaeast only | + +### Cleanup + +```powershell +az group delete --name rg-test --yes --no-wait +``` + +## Benefits + +✅ **Consistency** - All samples use identical infrastructure patterns +✅ **Security** - Managed identity only, RBAC-based, no shared keys +✅ **Simplicity** - One script, three files, ready to deploy +✅ **Testable** - Local testing workflow with `az webapp deploy` +✅ **Maintainable** - Fix modules once, all samples benefit + +## Examples + +See existing samples: +- [product-return-agent-sample](../product-return-agent-sample/) +- [ai-loan-agent-sample](../ai-loan-agent-sample/) diff --git a/samples/shared/modules/deployment-script.bicep b/samples/shared/modules/deployment-script.bicep new file mode 100644 index 00000000..1b8e5dd1 --- /dev/null +++ b/samples/shared/modules/deployment-script.bicep @@ -0,0 +1,89 @@ +// Deployment Script Module - Deploys workflows.zip to Logic App +// Includes RBAC assignment for deployment identity + +@description('Location for the deployment script resource') +param location string + +@description('Name for the deployment script resource') +param deploymentScriptName string + +@description('User-assigned managed identity ID for deployment') +param userAssignedIdentityId string + +@description('Principal ID of the user-assigned managed identity used for deployment') +param deploymentIdentityPrincipalId string + +@description('Name of the Logic App to deploy to') +param logicAppName string + +@description('Resource group name') +param resourceGroupName string + +@description('URL to the workflows.zip file') +param workflowsZipUrl string + +// Grant Website Contributor role at resource group level to deployment identity +// This allows the deployment script to deploy code to the Logic App and read the App Service Plan +resource websiteContributorRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(resourceGroup().id, deploymentIdentityPrincipalId, 'de139f84-1756-47ae-9be6-808fbbe84772') + scope: resourceGroup() + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'de139f84-1756-47ae-9be6-808fbbe84772') // Website Contributor + principalId: deploymentIdentityPrincipalId + principalType: 'ServicePrincipal' + } +} + +// Deploy workflows.zip to Logic App using Azure CLI +resource workflowDeploymentScript 'Microsoft.Resources/deploymentScripts@2023-08-01' = { + name: deploymentScriptName + location: location + kind: 'AzureCLI' + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${userAssignedIdentityId}': {} + } + } + properties: { + azCliVersion: '2.59.0' + retentionInterval: 'PT1H' + timeout: 'PT30M' + cleanupPreference: 'OnSuccess' + environmentVariables: [ + { + name: 'LOGIC_APP_NAME' + value: logicAppName + } + { + name: 'RESOURCE_GROUP' + value: resourceGroupName + } + { + name: 'WORKFLOWS_ZIP_URL' + value: workflowsZipUrl + } + ] + scriptContent: ''' + #!/bin/bash + set -e + + echo "Downloading workflows.zip..." + wget -O workflows.zip "$WORKFLOWS_ZIP_URL" + + echo "Deploying workflows to Logic App: $LOGIC_APP_NAME" + az functionapp deployment source config-zip \ + --resource-group "$RESOURCE_GROUP" \ + --name "$LOGIC_APP_NAME" \ + --src workflows.zip + + echo "Waiting 60 seconds for workflow registration and RBAC propagation..." + sleep 60 + + echo "Deployment completed successfully" + ''' + } + dependsOn: [ + websiteContributorRoleAssignment + ] +} diff --git a/samples/shared/modules/logicapp.bicep b/samples/shared/modules/logicapp.bicep new file mode 100644 index 00000000..2a4a2cf8 --- /dev/null +++ b/samples/shared/modules/logicapp.bicep @@ -0,0 +1,122 @@ +// Logic App Standard Module + +@description('Logic App name') +param logicAppName string + +@description('Location for Logic App') +param location string + +@description('Storage account name') +param storageAccountName string + +@description('OpenAI endpoint') +param openAIEndpoint string + +@description('OpenAI resource ID') +param openAIResourceId string + +@description('User-assigned managed identity resource ID for storage authentication') +param managedIdentityId string + +resource appServicePlan 'Microsoft.Web/serverfarms@2023-12-01' = { + name: '${logicAppName}-plan' + location: location + sku: { + name: 'WS1' + tier: 'WorkflowStandard' + } + kind: 'elastic' + properties: { + maximumElasticWorkerCount: 20 + } +} + +resource logicApp 'Microsoft.Web/sites@2023-12-01' = { + name: '${logicAppName}-logicapp' + location: location + kind: 'functionapp,workflowapp' + identity: { + type: 'SystemAssigned, UserAssigned' + userAssignedIdentities: { + '${managedIdentityId}': {} + } + } + properties: { + serverFarmId: appServicePlan.id + siteConfig: { + netFrameworkVersion: 'v8.0' + functionsRuntimeScaleMonitoringEnabled: true + appSettings: [ + { + name: 'FUNCTIONS_EXTENSION_VERSION' + value: '~4' + } + { + name: 'FUNCTIONS_WORKER_RUNTIME' + value: 'dotnet' + } + { + name: 'AzureWebJobsStorage__managedIdentityResourceId' + value: managedIdentityId + } + { + name: 'AzureWebJobsStorage__credential' + value: 'managedIdentity' + } + { + name: 'AzureWebJobsStorage__blobServiceUri' + value: 'https://${storageAccountName}.blob.${environment().suffixes.storage}' + } + { + name: 'AzureWebJobsStorage__queueServiceUri' + value: 'https://${storageAccountName}.queue.${environment().suffixes.storage}' + } + { + name: 'AzureWebJobsStorage__tableServiceUri' + value: 'https://${storageAccountName}.table.${environment().suffixes.storage}' + } + { + name: 'WEBSITES_ENABLE_APP_SERVICE_STORAGE' + value: 'false' + } + { + name: 'AzureFunctionsJobHost__extensionBundle__id' + value: 'Microsoft.Azure.Functions.ExtensionBundle.Workflows' + } + { + name: 'AzureFunctionsJobHost__extensionBundle__version' + value: '[1.*, 2.0.0)' + } + { + name: 'APP_KIND' + value: 'workflowApp' + } + { + name: 'WORKFLOWS_SUBSCRIPTION_ID' + value: subscription().subscriptionId + } + { + name: 'WORKFLOWS_LOCATION_NAME' + value: location + } + { + name: 'WORKFLOWS_RESOURCE_GROUP_NAME' + value: resourceGroup().name + } + { + name: 'agent_openAIEndpoint' + value: openAIEndpoint + } + { + name: 'agent_ResourceID' + value: openAIResourceId + } + ] + } + httpsOnly: true + } +} + +output name string = logicApp.name +output systemAssignedPrincipalId string = logicApp.identity.principalId +output quickTestUrl string = 'https://${logicApp.properties.defaultHostName}/api/ProductReturnAgent/triggers/When_a_HTTP_request_is_received/invoke?api-version=2022-05-01&sp=%2Ftriggers%2FWhen_a_HTTP_request_is_received%2Frun&sv=1.0&sig=' diff --git a/samples/shared/modules/openai-rbac.bicep b/samples/shared/modules/openai-rbac.bicep new file mode 100644 index 00000000..fb9a60ba --- /dev/null +++ b/samples/shared/modules/openai-rbac.bicep @@ -0,0 +1,26 @@ +// OpenAI RBAC Module - Grants Logic App access to OpenAI + +@description('OpenAI account name') +param openAIName string + +@description('Logic App managed identity principal ID') +param logicAppPrincipalId string + +resource openAI 'Microsoft.CognitiveServices/accounts@2023-05-01' existing = { + name: openAIName +} + +// Cognitive Services OpenAI User role +var cognitiveServicesOpenAIUserRoleId = '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' + +resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(openAI.id, logicAppPrincipalId, cognitiveServicesOpenAIUserRoleId) + scope: openAI + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', cognitiveServicesOpenAIUserRoleId) + principalId: logicAppPrincipalId + principalType: 'ServicePrincipal' + } +} + +output roleAssignmentId string = roleAssignment.id diff --git a/samples/shared/modules/openai.bicep b/samples/shared/modules/openai.bicep new file mode 100644 index 00000000..e501a938 --- /dev/null +++ b/samples/shared/modules/openai.bicep @@ -0,0 +1,40 @@ +// Azure OpenAI Module + +@description('Azure OpenAI account name') +param openAIName string + +@description('Location for Azure OpenAI') +param location string + +resource openAI 'Microsoft.CognitiveServices/accounts@2024-10-01' = { + name: openAIName + location: location + kind: 'OpenAI' + sku: { + name: 'S0' + } + properties: { + customSubDomainName: openAIName + publicNetworkAccess: 'Enabled' + } +} + +resource deployment 'Microsoft.CognitiveServices/accounts/deployments@2024-10-01' = { + parent: openAI + name: 'gpt-4o-mini' + sku: { + name: 'GlobalStandard' + capacity: 50 + } + properties: { + model: { + format: 'OpenAI' + name: 'gpt-4o-mini' + version: '2024-07-18' + } + } +} + +output name string = openAI.name +output endpoint string = openAI.properties.endpoint +output resourceId string = openAI.id diff --git a/samples/shared/modules/storage-rbac.bicep b/samples/shared/modules/storage-rbac.bicep new file mode 100644 index 00000000..1ed0ed81 --- /dev/null +++ b/samples/shared/modules/storage-rbac.bicep @@ -0,0 +1,45 @@ +// Storage RBAC Module - Assigns required roles to Logic App managed identity + +@description('Storage account name') +param storageAccountName string + +@description('Principal ID of the Logic App managed identity') +param logicAppPrincipalId string + +// Storage account reference +resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' existing = { + name: storageAccountName +} + +// Role assignment: Storage Blob Data Owner +resource storageBlobDataOwnerAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(storageAccount.id, logicAppPrincipalId, 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b') + scope: storageAccount + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b') + principalId: logicAppPrincipalId + principalType: 'ServicePrincipal' + } +} + +// Role assignment: Storage Queue Data Contributor +resource storageQueueDataContributorAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(storageAccount.id, logicAppPrincipalId, '974c5e8b-45b9-4653-ba55-5f855dd0fb88') + scope: storageAccount + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88') + principalId: logicAppPrincipalId + principalType: 'ServicePrincipal' + } +} + +// Role assignment: Storage Table Data Contributor +resource storageTableDataContributorAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(storageAccount.id, logicAppPrincipalId, '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3') + scope: storageAccount + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3') + principalId: logicAppPrincipalId + principalType: 'ServicePrincipal' + } +} diff --git a/samples/shared/modules/storage.bicep b/samples/shared/modules/storage.bicep new file mode 100644 index 00000000..449f2114 --- /dev/null +++ b/samples/shared/modules/storage.bicep @@ -0,0 +1,28 @@ +// Storage Account Module - For Logic App runtime only + +@description('Storage account name') +param storageAccountName string + +@description('Location for the storage account') +param location string + +resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = { + name: storageAccountName + location: location + sku: { + name: 'Standard_LRS' + } + kind: 'StorageV2' + properties: { + supportsHttpsTrafficOnly: true + minimumTlsVersion: 'TLS1_2' + allowBlobPublicAccess: false + allowSharedKeyAccess: false // Enforce managed identity only - no connection strings or keys + } +} + +output storageAccountName string = storageAccount.name +output storageAccountId string = storageAccount.id +output blobServiceUri string = storageAccount.properties.primaryEndpoints.blob +output queueServiceUri string = storageAccount.properties.primaryEndpoints.queue +output tableServiceUri string = storageAccount.properties.primaryEndpoints.table diff --git a/samples/shared/scripts/BundleAssets.ps1 b/samples/shared/scripts/BundleAssets.ps1 new file mode 100644 index 00000000..b39c2902 --- /dev/null +++ b/samples/shared/scripts/BundleAssets.ps1 @@ -0,0 +1,279 @@ +#!/usr/bin/env powershell +<# +.SYNOPSIS + Create ARM template and bundle LogicApps folder for 1-click deployment. + +.DESCRIPTION + This script prepares all necessary assets for 1-click deployment by performing two key tasks: + + 1. Build ARM Template: Compiles the Bicep infrastructure file into an ARM template using the Bicep CLI. + 2. Bundle Workflows: Creates a deployment-ready workflows.zip containing all Logic App workflows. + + Automatically excludes development artifacts: + - Version control (.git) + - Editor settings (.vscode) + - Dependencies (node_modules) + - Local storage (__azurite*, __blobstorage__*, __queuestorage__*) + - Existing zip files + +.PARAMETER Sample + Required. Name of the sample folder (e.g., "product-return-agent-sample"). + All paths are built from this parameter. + +.EXAMPLE + # From anywhere in the repository: + .\samples\shared\scripts\BundleAssets.ps1 -Sample "product-return-agent-sample" + +.EXAMPLE + # From samples folder: + .\shared\scripts\BundleAssets.ps1 -Sample "ai-loan-agent-sample" + +.NOTES + Requirements: + - Bicep CLI must be installed + - PowerShell 5.1 or later + + Expected folder structure: + samples/your-sample/ + ├── Deployment/ + │ ├── main.bicep + │ ├── sample-arm.json # Generated + │ └── workflows.zip # Generated + └── LogicApps/ +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$Sample +) + +$ErrorActionPreference = "Stop" + +# ============================================================================ +# CONSTANTS +# ============================================================================ + +# Hardcoded upstream repository for URL generation +$upstreamRepo = "Azure/logicapps-labs" +$upstreamBranch = "main" + +# ============================================================================ +# HELPER FUNCTIONS +# ============================================================================ + +Function New-MainBicepFromTemplate { + <# + .SYNOPSIS + Generates main.bicep from template with placeholder replacement + #> + param( + [Parameter(Mandatory = $true)] + [string]$TemplatePath, + + [Parameter(Mandatory = $true)] + [string]$OutputPath, + + [Parameter(Mandatory = $true)] + [string]$WorkflowsZipUrl + ) + + if (-not (Test-Path $TemplatePath)) { + throw "Template file not found: $TemplatePath" + } + + $template = Get-Content $TemplatePath -Raw + $content = $template -replace '\{\{WORKFLOWS_ZIP_URL\}\}', $WorkflowsZipUrl + + $outputDir = Split-Path $OutputPath + if (-not (Test-Path $outputDir)) { + New-Item -ItemType Directory -Path $outputDir -Force | Out-Null + } + + Set-Content -Path $OutputPath -Value $content -NoNewline +} + +# ============================================================================ +# BUILD PATHS FROM SAMPLE NAME +# ============================================================================ + +# Find repository root (contains samples/ folder) +$scriptDir = $PSScriptRoot +$repoRoot = $scriptDir +while ($repoRoot -and -not (Test-Path (Join-Path $repoRoot "samples"))) { + $repoRoot = Split-Path $repoRoot -Parent +} + +if (-not $repoRoot) { + Write-Host "✗ Could not find repository root (looking for samples/ folder)" -ForegroundColor Red + exit 1 +} + +# Build all paths from sample name +$sampleFolder = Join-Path $repoRoot "samples\$Sample" +$deploymentFolder = Join-Path $sampleFolder "Deployment" +$logicAppsFolder = Join-Path $sampleFolder "LogicApps" +$bicepPath = Join-Path $deploymentFolder "main.bicep" +$armTemplatePath = Join-Path $deploymentFolder "sample-arm.json" +$zipPath = Join-Path $deploymentFolder "workflows.zip" + +# Get sample display name (convert folder name to title case) +$sampleDisplayName = ($Sample -replace '-sample$', '' -replace '-', ' ' | + ForEach-Object { (Get-Culture).TextInfo.ToTitleCase($_) }) + +Write-Host "`n=== Bundling Assets for $sampleDisplayName ===" -ForegroundColor Cyan +Write-Host "Sample Folder: $sampleFolder" -ForegroundColor Gray +Write-Host "Deployment Folder: $deploymentFolder" -ForegroundColor Gray +Write-Host "LogicApps Folder: $logicAppsFolder" -ForegroundColor Gray + +# Validate paths +if (-not (Test-Path $sampleFolder)) { + Write-Host "✗ Sample folder not found: $sampleFolder" -ForegroundColor Red + exit 1 +} + +if (-not (Test-Path $logicAppsFolder)) { + Write-Host "✗ LogicApps folder not found: $logicAppsFolder" -ForegroundColor Red + exit 1 +} + +# Ensure deployment directory exists +if (-not (Test-Path $deploymentFolder)) { + New-Item -Path $deploymentFolder -ItemType Directory -Force | Out-Null + Write-Host "✓ Created Deployment folder" -ForegroundColor Green +} + +# ============================================================================ +# TEMPLATE GENERATION: Create main.bicep if it doesn't exist +# ============================================================================ + +if (-not (Test-Path $bicepPath)) { + Write-Host "`nGenerating main.bicep from template..." -ForegroundColor Cyan + + # Use hardcoded upstream repo for URL generation + $workflowsUrl = "https://raw.githubusercontent.com/$upstreamRepo/$upstreamBranch/samples/$Sample/Deployment/workflows.zip" + Write-Host " Using: $upstreamRepo / $upstreamBranch" -ForegroundColor Gray + + # Use template from shared + $templatePath = Join-Path $repoRoot "samples\shared\templates\main.bicep.template" + + if (-not (Test-Path $templatePath)) { + Write-Host "✗ Template file not found: $templatePath" -ForegroundColor Red + Write-Host " Expected at: samples/shared/templates/main.bicep.template" -ForegroundColor Yellow + exit 1 + } + + try { + New-MainBicepFromTemplate -TemplatePath $templatePath -OutputPath $bicepPath -WorkflowsZipUrl $workflowsUrl + Write-Host " ✓ Created: main.bicep" -ForegroundColor Green + Write-Host " Location: $bicepPath" -ForegroundColor Gray + } catch { + Write-Host "✗ Failed to generate main.bicep: $($_.Exception.Message)" -ForegroundColor Red + exit 1 + } +} else { + Write-Host "`nUsing existing main.bicep (not overwriting)" -ForegroundColor Cyan + Write-Host " Location: $bicepPath" -ForegroundColor Gray +} + +# ============================================================================ +# BUILD BICEP TO ARM TEMPLATE +# ============================================================================ + +Write-Host "`nBuilding ARM template from Bicep..." -ForegroundColor Cyan + +# Check for Bicep CLI +$bicepAvailable = $null -ne (Get-Command bicep -ErrorAction SilentlyContinue) + +if (-not $bicepAvailable) { + Write-Host "✗ Bicep CLI not found. Please install it first." -ForegroundColor Red + Write-Host "Install: https://learn.microsoft.com/azure/azure-resource-manager/bicep/install" -ForegroundColor Yellow + exit 1 +} + +try { + bicep build $bicepPath --outfile $armTemplatePath + + if (Test-Path $armTemplatePath) { + $armSize = (Get-Item $armTemplatePath).Length / 1KB + Write-Host "✓ Successfully created sample-arm.json ($("{0:N2}" -f $armSize) KB)" -ForegroundColor Green + } else { + throw "ARM template file was not created" + } +} catch { + Write-Host "✗ Failed to build ARM template: $($_.Exception.Message)" -ForegroundColor Red + exit 1 +} + +# ============================================================================ +# BUNDLE WORKFLOWS ZIP +# ============================================================================ + +Write-Host "`nBundling workflows.zip..." -ForegroundColor Cyan + +# Remove existing zip if present +if (Test-Path $zipPath) { + Remove-Item $zipPath -Force + Write-Host "✓ Removed existing workflows.zip" -ForegroundColor Green +} + +# Get all items except those we want to exclude +$itemsToZip = Get-ChildItem -Path $logicAppsFolder | Where-Object { + $_.Name -notin @('.git', '.vscode', 'node_modules') -and + $_.Name -notlike '__azurite*' -and + $_.Name -notlike '__blobstorage__*' -and + $_.Name -notlike '__queuestorage__*' -and + $_.Extension -ne '.zip' +} + +Write-Host "`nIncluding files:" +$itemsToZip | ForEach-Object { Write-Host " - $($_.Name)" -ForegroundColor Gray } + +# Create zip +Push-Location $logicAppsFolder +try { + Compress-Archive -Path $itemsToZip.Name -DestinationPath $zipPath -Force +} catch { + Pop-Location + Write-Host "`n✗ Failed to create workflows.zip: $($_.Exception.Message)" -ForegroundColor Red + exit 1 +} +Pop-Location + +if (Test-Path $zipPath) { + $zipSize = (Get-Item $zipPath).Length / 1MB + Write-Host "`n✓ Successfully created workflows.zip ($("{0:N2}" -f $zipSize) MB)" -ForegroundColor Green + Write-Host "Location: $zipPath" -ForegroundColor Cyan +} else { + Write-Host "`n✗ Failed to create workflows.zip" -ForegroundColor Red + exit 1 +} + +# ============================================================================ +# DEPLOY TO AZURE BUTTON +# ============================================================================ + +Write-Host "`n=== Deploy to Azure Button ===" -ForegroundColor Cyan + +# Construct the URL to sample-arm.json using hardcoded upstream repo +$armUrl = "https://raw.githubusercontent.com/$upstreamRepo/$upstreamBranch/samples/$Sample/Deployment/sample-arm.json" + +# URL encode for Azure Portal +$encodedUrl = [System.Uri]::EscapeDataString($armUrl) +$portalUrl = "https://portal.azure.com/#create/Microsoft.Template/uri/$encodedUrl" +$badgeUrl = "https://aka.ms/deploytoazurebutton" + +Write-Host "Repository: $upstreamRepo" -ForegroundColor Gray +Write-Host "Branch: $upstreamBranch" -ForegroundColor Gray +Write-Host "ARM URL: $armUrl" -ForegroundColor Gray +Write-Host "`nAdd this to your README.md:" -ForegroundColor Cyan +Write-Host "[![Deploy to Azure]($badgeUrl)]($portalUrl)" -ForegroundColor Green + +# ============================================================================ +# SUMMARY +# ============================================================================ + +Write-Host "`n=== Bundling Complete ===" -ForegroundColor Cyan +Write-Host "Sample: $sampleDisplayName" -ForegroundColor Gray +Write-Host "ARM Template: $armTemplatePath" -ForegroundColor Gray +Write-Host "Workflows Zip: $zipPath" -ForegroundColor Gray diff --git a/samples/shared/templates/main.bicep.template b/samples/shared/templates/main.bicep.template new file mode 100644 index 00000000..16718b9b --- /dev/null +++ b/samples/shared/templates/main.bicep.template @@ -0,0 +1,99 @@ +// Auto-generated from shared/templates/main.bicep.template +// To customize: edit this file directly or delete to regenerate from template +// +// AI Product Return Agent - Azure Infrastructure as Code +// Deploys Logic Apps Standard with Azure OpenAI for autonomous product return decisions +// Uses managed identity exclusively (no secrets/connection strings) + +targetScope = 'resourceGroup' + +@description('Base name used for the resources that will be deployed (alphanumerics and hyphens only)') +@minLength(3) +@maxLength(60) +param BaseName string + +// uniqueSuffix for when we need unique values +var uniqueSuffix = uniqueString(resourceGroup().id) + +// URL to workflows.zip (replaced by BundleAssets.ps1 with {{WORKFLOWS_ZIP_URL}}) +var workflowsZipUrl = '{{WORKFLOWS_ZIP_URL}}' + +// User-Assigned Managed Identity for Logic App → Storage authentication +resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: '${take(BaseName, 60)}-managedidentity' + location: resourceGroup().location +} + +// Storage Account for workflow runtime +module storage '../../shared/modules/storage.bicep' = { + name: '${take(BaseName, 43)}-storage-deployment' + params: { + storageAccountName: toLower(take(replace('${take(BaseName, 16)}${uniqueSuffix}', '-', ''), 24)) + location: resourceGroup().location + } +} + +// Azure OpenAI with gpt-4o-mini model +module openai '../../shared/modules/openai.bicep' = { + name: '${take(BaseName, 44)}-openai-deployment' + params: { + openAIName: '${take(BaseName, 54)}-openai' + location: resourceGroup().location + } +} + +// Logic Apps Standard with dual managed identities +module logicApp '../../shared/modules/logicapp.bicep' = { + name: '${take(BaseName, 42)}-logicapp-deployment' + params: { + logicAppName: '${take(BaseName, 22)}${uniqueSuffix}' + location: resourceGroup().location + storageAccountName: storage.outputs.storageAccountName + openAIEndpoint: openai.outputs.endpoint + openAIResourceId: openai.outputs.resourceId + managedIdentityId: userAssignedIdentity.id + } +} + +// RBAC: Logic App → Storage (Blob, Queue, Table Contributor roles) +module storageRbac '../../shared/modules/storage-rbac.bicep' = { + name: '${take(BaseName, 38)}-storage-rbac-deployment' + params: { + storageAccountName: storage.outputs.storageAccountName + logicAppPrincipalId: userAssignedIdentity.properties.principalId + } + dependsOn: [ + logicApp + ] +} + +// RBAC: Logic App → Azure OpenAI (Cognitive Services User role) +module openaiRbac '../../shared/modules/openai-rbac.bicep' = { + name: '${take(BaseName, 39)}-openai-rbac-deployment' + params: { + openAIName: openai.outputs.name + logicAppPrincipalId: logicApp.outputs.systemAssignedPrincipalId + } +} + +// Deploy workflows using deployment script with RBAC +module workflowDeployment '../../shared/modules/deployment-script.bicep' = { + name: '${take(BaseName, 42)}-workflow-deployment' + params: { + deploymentScriptName: '${BaseName}-deploy-workflows' + location: resourceGroup().location + userAssignedIdentityId: userAssignedIdentity.id + deploymentIdentityPrincipalId: userAssignedIdentity.properties.principalId + logicAppName: logicApp.outputs.name + resourceGroupName: resourceGroup().name + workflowsZipUrl: workflowsZipUrl + } + dependsOn: [ + storageRbac + openaiRbac + ] +} + +// Outputs +output logicAppName string = logicApp.outputs.name +output openAIEndpoint string = openai.outputs.endpoint