diff --git a/samples/json-remediation-agent/Deployment/main.bicep b/samples/json-remediation-agent/Deployment/main.bicep new file mode 100644 index 0000000..9ea587d --- /dev/null +++ b/samples/json-remediation-agent/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 +// +// JSON Remediation Agent - Azure Infrastructure as Code +// Deploys Logic Apps Standard with Azure OpenAI for autonomous JSON repair and validation +// 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/json-remediation-agent/samples/json-remediation-agent/Deployment/workflows.zip) +var workflowsZipUrl = 'https://raw.githubusercontent.com/modularity/logicapps-labs/json-remediation-agent/samples/json-remediation-agent/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/json-remediation-agent/Deployment/sample-arm.json b/samples/json-remediation-agent/Deployment/sample-arm.json new file mode 100644 index 0000000..e72327d --- /dev/null +++ b/samples/json-remediation-agent/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": "5488634814907059008" + } + }, + "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/json-remediation-agent/samples/json-remediation-agent/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/json-remediation-agent/Deployment/workflows.zip b/samples/json-remediation-agent/Deployment/workflows.zip new file mode 100644 index 0000000..aa40c56 Binary files /dev/null and b/samples/json-remediation-agent/Deployment/workflows.zip differ diff --git a/samples/json-remediation-agent/LogicApps/JsonRemediationAgent/workflow.json b/samples/json-remediation-agent/LogicApps/JsonRemediationAgent/workflow.json new file mode 100644 index 0000000..b8c41f0 --- /dev/null +++ b/samples/json-remediation-agent/LogicApps/JsonRemediationAgent/workflow.json @@ -0,0 +1,194 @@ +{ + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "actions": { + "Incident_Request_Summary": { + "type": "Compose", + "inputs": { + "caseId": "@triggerBody()?['caseId']", + "requestDate": "@utcNow()" + }, + "runAfter": {} + }, + "Json_Remediation_Agent": { + "type": "Agent", + "inputs": { + "parameters": { + "deploymentId": "gpt-4o-mini", + "messages": [ + { + "role": "system", + "content": "You are an autonomous data quality agent.\n\nSTEPS:\n1. Call Load_incident_json to get malformed JSON\n2. Call Repair_json to apply transformations\n3. Call Validate_schema to verify compliance\n4. Call Format_response to mark ready\n\nOUTPUT (exact format):\nCASE ID: [caseId]\nSTATUS: [SUCCESS/FAILED]\nCHANGES APPLIED:\n- [describe specific change with before/after values]\n- [e.g., \"Added missing field 'customerId' with value 'UNKNOWN'\"]\n- [e.g., \"Converted priority from string '2' to integer 2\"]\n- [e.g., \"Renamed field 'cust_id' to 'customerId'\"]\n- [e.g., \"Replaced null details with {description: 'No details provided'}\"]\nVALIDATION: [PASSED/FAILED]\nREPAIRED JSON:\n[show final repaired JSON]\n\nIMPORTANT: Be specific about what changed. Compare raw vs repaired JSON from tool results. After Format_response succeeds, STOP." + }, + { + "role": "user", + "content": "Process incident case: @{outputs('Incident_Request_Summary')}" + } + ], + "agentModelType": "AzureOpenAI", + "agentModelSettings": { + "deploymentModelProperties": { + "name": "gpt-4o-mini", + "format": "OpenAI", + "version": "2024-07-18" + } + } + }, + "modelConfigurations": { + "model1": { + "referenceName": "agent" + } + } + }, + "limit": { + "count": 100 + }, + "tools": { + "Load_incident_json": { + "actions": { + "Load_Incident_Data": { + "type": "Compose", + "inputs": "@if(equals(agentParameters('caseId'), 'incident-missing-field'), '{\"incidentId\":\"INC-9001\",\"priority\":3,\"details\":{\"description\":\"System outage\"}}', if(equals(agentParameters('caseId'), 'incident-wrong-type'), '{\"incidentId\":\"INC-9002\",\"customerId\":\"CUST-456\",\"priority\":\"2\",\"details\":{\"description\":\"Database slowdown\"}}', if(equals(agentParameters('caseId'), 'incident-field-standardization'), '{\"id\":\"INC-9004\",\"cust_id\":\"CUST-101\",\"prio\":3,\"details\":{\"desc\":\"Login errors\"}}', if(equals(agentParameters('caseId'), 'incident-partial-object'), '{\"incidentId\":\"INC-9005\",\"customerId\":\"CUST-202\",\"priority\":2,\"details\":null}', '{}'))))" + } + }, + "description": "Load malformed incident JSON by caseId from data store", + "agentParameterSchema": { + "type": "object", + "properties": { + "caseId": { + "type": "string", + "description": "The case ID of the incident to load (e.g., 'incident-missing-field')" + } + }, + "required": ["caseId"] + } + }, + "Repair_json": { + "actions": { + "Call_JsonRepairEngine": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "JsonRepairEngine" + } + }, + "body": { + "payload": "@agentParameters('rawPayload')", + "caseId": "@agentParameters('caseId')" + } + } + } + }, + "description": "Apply deterministic transformations to repair malformed JSON (add missing fields, fix types, rename fields, normalize values)", + "agentParameterSchema": { + "type": "object", + "properties": { + "caseId": { + "type": "string", + "description": "Case ID for reference" + }, + "rawPayload": { + "type": "string", + "description": "The raw incident JSON as a string to repair" + } + }, + "required": ["caseId", "rawPayload"] + } + }, + "Validate_schema": { + "actions": { + "Call_SchemaValidator": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SchemaValidator" + } + }, + "body": { + "payload": "@agentParameters('repairedPayload')", + "schemaPath": "data/schema/incident.schema.json" + } + } + } + }, + "description": "Validate repaired JSON against incident schema (checks required fields, types, and structure)", + "agentParameterSchema": { + "type": "object", + "properties": { + "repairedPayload": { + "type": "string", + "description": "The repaired incident JSON as a string to validate" + } + }, + "required": ["repairedPayload"] + } + }, + "Format_response": { + "actions": { + "Format_Output": { + "type": "Compose", + "inputs": { + "success": true, + "caseId": "@agentParameters('caseId')", + "cleanedPayload": "@agentParameters('cleanedPayload')", + "message": "Cleaned JSON ready for ingestion (ephemeral demo - no persistence)" + } + } + }, + "description": "Mark cleaned JSON ready for ingestion (ephemeral storage for demo purposes)", + "agentParameterSchema": { + "type": "object", + "properties": { + "caseId": { + "type": "string", + "description": "Case ID for reference" + }, + "cleanedPayload": { + "type": "string", + "description": "The repaired and validated JSON payload as a JSON string" + } + }, + "required": ["caseId", "cleanedPayload"] + } + } + }, + "runAfter": { + "Incident_Request_Summary": ["Succeeded"] + } + }, + "Response": { + "type": "Response", + "kind": "Http", + "inputs": { + "statusCode": 200, + "body": "@outputs('Json_Remediation_Agent')?['lastAssistantMessage']" + }, + "runAfter": { + "Json_Remediation_Agent": ["Succeeded"] + } + } + }, + "contentVersion": "1.0.0.0", + "outputs": {}, + "triggers": { + "manual": { + "type": "Request", + "kind": "Http", + "inputs": { + "schema": { + "type": "object", + "properties": { + "caseId": { + "type": "string" + } + }, + "required": ["caseId"] + } + } + } + } + }, + "kind": "Agentic" +} diff --git a/samples/json-remediation-agent/LogicApps/JsonRepairEngine/workflow.json b/samples/json-remediation-agent/LogicApps/JsonRepairEngine/workflow.json new file mode 100644 index 0000000..f9ca53e --- /dev/null +++ b/samples/json-remediation-agent/LogicApps/JsonRepairEngine/workflow.json @@ -0,0 +1,104 @@ +{ + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "actions": { + "Parse_Payload": { + "type": "Compose", + "inputs": "@json(triggerBody()?['payload'])", + "runAfter": {} + }, + "Set_CustomerId": { + "type": "Compose", + "inputs": "@coalesce(outputs('Parse_Payload')?['customerId'], outputs('Parse_Payload')?['cust_id'], 'UNKNOWN')", + "runAfter": { + "Parse_Payload": ["Succeeded"] + } + }, + "Set_IncidentId": { + "type": "Compose", + "inputs": "@coalesce(outputs('Parse_Payload')?['incidentId'], outputs('Parse_Payload')?['id'])", + "runAfter": { + "Parse_Payload": ["Succeeded"] + } + }, + "Set_Priority": { + "type": "Compose", + "inputs": "@if(equals(string(outputs('Parse_Payload')?['priority']), string(outputs('Parse_Payload')?['priority'])), if(greater(length(coalesce(string(outputs('Parse_Payload')?['priority']), '')), 0), int(string(outputs('Parse_Payload')?['priority'])), coalesce(int(outputs('Parse_Payload')?['prio']), 3)), 3)", + "runAfter": { + "Parse_Payload": ["Succeeded"] + } + }, + "Set_Details": { + "type": "Compose", + "inputs": "@if(or(equals(outputs('Parse_Payload')?['details'], null), empty(outputs('Parse_Payload')?['details'])), json('{\"description\":\"No details provided\"}'), if(contains(coalesce(outputs('Parse_Payload')?['details'], json('{}')), 'desc'), json(concat('{\"description\":\"', string(outputs('Parse_Payload')?['details']?['desc']), '\"}')), outputs('Parse_Payload')?['details']))", + "runAfter": { + "Parse_Payload": ["Succeeded"] + } + }, + "Compose_Repaired": { + "type": "Compose", + "inputs": { + "incidentId": "@outputs('Set_IncidentId')", + "customerId": "@outputs('Set_CustomerId')", + "priority": "@outputs('Set_Priority')", + "details": "@outputs('Set_Details')" + }, + "runAfter": { + "Set_CustomerId": ["Succeeded"], + "Set_IncidentId": ["Succeeded"], + "Set_Priority": ["Succeeded"], + "Set_Details": ["Succeeded"] + } + }, + "Compose_Changes": { + "type": "Compose", + "inputs": [ + "normalized customerId", + "normalized priority", + "normalized details" + ], + "runAfter": { + "Compose_Repaired": ["Succeeded"] + } + }, + "Response": { + "type": "Response", + "kind": "http", + "inputs": { + "statusCode": 200, + "body": { + "repaired": "@outputs('Compose_Repaired')", + "changes": "@outputs('Compose_Changes')" + } + }, + "runAfter": { + "Compose_Changes": [ + "Succeeded" + ] + } + } + }, + "contentVersion": "1.0.0.0", + "outputs": {}, + "triggers": { + "manual": { + "type": "Request", + "kind": "Http", + "inputs": { + "schema": { + "type": "object", + "properties": { + "payload": { + "type": "object" + }, + "caseId": { + "type": "string" + } + } + } + } + } + } + }, + "kind": "Stateful" +} diff --git a/samples/json-remediation-agent/LogicApps/SchemaValidator/workflow.json b/samples/json-remediation-agent/LogicApps/SchemaValidator/workflow.json new file mode 100644 index 0000000..946d3a2 --- /dev/null +++ b/samples/json-remediation-agent/LogicApps/SchemaValidator/workflow.json @@ -0,0 +1,83 @@ +{ + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "actions": { + "Parse_Payload": { + "type": "Compose", + "inputs": "@json(triggerBody()?['payload'])", + "runAfter": {} + }, + "Check_Required_Fields": { + "type": "Compose", + "inputs": { + "hasIncidentId": "@not(empty(coalesce(outputs('Parse_Payload')?['incidentId'], '')))", + "hasPriority": "@not(equals(outputs('Parse_Payload')?['priority'], null))", + "hasCustomerId": "@not(empty(coalesce(outputs('Parse_Payload')?['customerId'], '')))", + "hasDetails": "@not(equals(outputs('Parse_Payload')?['details'], null))", + "hasDescription": "@not(empty(coalesce(outputs('Parse_Payload')?['details']?['description'], '')))" + }, + "runAfter": { + "Parse_Payload": ["Succeeded"] + } + }, + "Determine_IsValid": { + "type": "Compose", + "inputs": "@and(and(and(outputs('Check_Required_Fields').hasIncidentId, outputs('Check_Required_Fields').hasPriority), and(outputs('Check_Required_Fields').hasCustomerId, outputs('Check_Required_Fields').hasDetails)), outputs('Check_Required_Fields').hasDescription)", + "runAfter": { + "Check_Required_Fields": ["Succeeded"] + } + }, + "Build_Violations": { + "type": "Compose", + "inputs": [ + "@if(outputs('Check_Required_Fields').hasIncidentId, null, 'Missing required field: incidentId')", + "@if(outputs('Check_Required_Fields').hasPriority, null, 'Missing required field: priority')", + "@if(outputs('Check_Required_Fields').hasCustomerId, null, 'Missing required field: customerId')", + "@if(outputs('Check_Required_Fields').hasDetails, null, 'Missing required field: details')", + "@if(outputs('Check_Required_Fields').hasDescription, null, 'Missing required field: details.description')" + ], + "runAfter": { + "Determine_IsValid": ["Succeeded"] + } + }, + "Response": { + "type": "Response", + "kind": "http", + "inputs": { + "statusCode": 200, + "body": { + "isValid": "@outputs('Determine_IsValid')", + "violations": "@outputs('Build_Violations')" + } + }, + "runAfter": { + "Build_Violations": [ + "Succeeded" + ] + } + } + }, + "contentVersion": "1.0.0.0", + "outputs": {}, + "triggers": { + "manual": { + "type": "Request", + "kind": "Http", + "inputs": { + "schema": { + "type": "object", + "properties": { + "payload": { + "type": "object" + }, + "schemaPath": { + "type": "string" + } + } + } + } + } + } + }, + "kind": "Stateful" +} diff --git a/samples/json-remediation-agent/LogicApps/connections.json b/samples/json-remediation-agent/LogicApps/connections.json new file mode 100644 index 0000000..806d743 --- /dev/null +++ b/samples/json-remediation-agent/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/json-remediation-agent/LogicApps/data/incidents/incident-field-standardization.json b/samples/json-remediation-agent/LogicApps/data/incidents/incident-field-standardization.json new file mode 100644 index 0000000..6b61b07 --- /dev/null +++ b/samples/json-remediation-agent/LogicApps/data/incidents/incident-field-standardization.json @@ -0,0 +1 @@ +{ "id":"INC-9004","prio":"Critical","cust_id":"C-4455","details":{"desc":"CPU saturation"} } diff --git a/samples/json-remediation-agent/LogicApps/data/incidents/incident-missing-field.json b/samples/json-remediation-agent/LogicApps/data/incidents/incident-missing-field.json new file mode 100644 index 0000000..cec2f68 --- /dev/null +++ b/samples/json-remediation-agent/LogicApps/data/incidents/incident-missing-field.json @@ -0,0 +1 @@ +{ "incidentId":"INC-9001","priority":"High","details":{"description":"System outage"} } diff --git a/samples/json-remediation-agent/LogicApps/data/incidents/incident-partial-object.json b/samples/json-remediation-agent/LogicApps/data/incidents/incident-partial-object.json new file mode 100644 index 0000000..16718e4 --- /dev/null +++ b/samples/json-remediation-agent/LogicApps/data/incidents/incident-partial-object.json @@ -0,0 +1 @@ +{ "incidentId":"INC-9005","priority":"Low","details":null } diff --git a/samples/json-remediation-agent/LogicApps/data/incidents/incident-trailing-comma.json b/samples/json-remediation-agent/LogicApps/data/incidents/incident-trailing-comma.json new file mode 100644 index 0000000..d8795f9 --- /dev/null +++ b/samples/json-remediation-agent/LogicApps/data/incidents/incident-trailing-comma.json @@ -0,0 +1 @@ +{ "incidentId":"INC-9003","priority":"Medium","details":{"description":"Malformed entry here"}, } diff --git a/samples/json-remediation-agent/LogicApps/data/incidents/incident-wrong-type.json b/samples/json-remediation-agent/LogicApps/data/incidents/incident-wrong-type.json new file mode 100644 index 0000000..50d0cab --- /dev/null +++ b/samples/json-remediation-agent/LogicApps/data/incidents/incident-wrong-type.json @@ -0,0 +1 @@ +{ "incidentId":"INC-9002","priority":"3","customerId":"12345","details":{"durationMinutes":"42"} } diff --git a/samples/json-remediation-agent/LogicApps/data/schema/incident.schema.json b/samples/json-remediation-agent/LogicApps/data/schema/incident.schema.json new file mode 100644 index 0000000..e1d1ba7 --- /dev/null +++ b/samples/json-remediation-agent/LogicApps/data/schema/incident.schema.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Incident", + "type": "object", + "required": ["incidentId", "priority", "customerId", "details"], + "properties": { + "incidentId": { + "type": "string" + }, + "priority": { + "type": ["string", "number"] + }, + "customerId": { + "type": "string" + }, + "details": { + "type": "object", + "required": ["description"], + "properties": { + "description": { + "type": "string" + } + } + } + }, + "additionalProperties": true +} diff --git a/samples/json-remediation-agent/LogicApps/host.json b/samples/json-remediation-agent/LogicApps/host.json new file mode 100644 index 0000000..89ab59e --- /dev/null +++ b/samples/json-remediation-agent/LogicApps/host.json @@ -0,0 +1,7 @@ +{ + "version": "2.0", + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle.Workflows", + "version": "[1.*, 2.0.0)" + } +} diff --git a/samples/json-remediation-agent/LogicApps/local.settings.json b/samples/json-remediation-agent/LogicApps/local.settings.json new file mode 100644 index 0000000..45c4c85 --- /dev/null +++ b/samples/json-remediation-agent/LogicApps/local.settings.json @@ -0,0 +1,12 @@ +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "FUNCTIONS_WORKER_RUNTIME": "node", + "INCIDENTS_PATH": "data/incidents", + "OUTBOX_PATH": "data/outbox", + "SCHEMA_PATH": "data/schema", + "agent_openAIEndpoint": "", + "agent_ResourceID": "" + } +} diff --git a/samples/json-remediation-agent/README.md b/samples/json-remediation-agent/README.md new file mode 100644 index 0000000..07f24d0 --- /dev/null +++ b/samples/json-remediation-agent/README.md @@ -0,0 +1,339 @@ +# JSON Remediation Agent + +An autonomous operations agent that repairs malformed JSON records, applies deterministic schema corrections, validates structure, and outputs clean, ingestion-ready payloads—entirely deterministic with no external service dependencies. + +**[Watch Demo Video](https://youtu.be/uciB7jtDpyk)** | **[Agent Workflow Blog](https://techcommunity.microsoft.com/blog/integrationsonazureblog/%F0%9F%A4%96-agent-loop-demos-%F0%9F%A4%96/4414770)** + +--- + +## 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%2Fjson-remediation-agent%2Fsamples%2Fjson-remediation-agent%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-jsonremediation`) +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 autonomous agent workflows with built-in test data for immediate testing + +
+ +
+What gets deployed + +| Resource | Purpose | +|----------|---------| +| Logic App Standard | Hosts autonomous 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, Australia East + +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 = "jsonops") | +|----------|-----------------------------------| +| Resource Group | `rg-jsonops` | +| Logic App | `jsonopsxyz123-logicapp` | +| Azure OpenAI | `jsonops-openai` | +| Storage Account | `jsonopsxyz123` | + +
+ +--- + +## Explore + +After deployment, test the agent with different malformed JSON scenarios to see how it autonomously repairs and validates data. + +### Run a test + +1. Open [Azure Portal](https://portal.azure.com) > your resource group > Logic App > **Workflows** > **JsonRemediationAgent** > [**Run history**](https://learn.microsoft.com/azure/logic-apps/monitor-logic-apps#review-runs-history) + +2. Click **Run** > **Run with payload** + +3. Paste one of the test payloads below and select **Run**. Wait a few seconds for the workflow to finish; on the **Output** tab you'll see `Status Code: 200` and the agent's repair summary in the response body. + +4. (Optional) Select to open the monitoring view if you want to inspect the workflow execution in detail. The monitoring view shows action inputs/outputs and agent tool calls in the **Agent log** tab (e.g., **LoadIncidentJson**, **RepairJson**). + +**Test these scenarios to see different repair strategies:** + +
+Scenario 1: Missing required field + +Missing `customerId` field: + +```json +{"caseId":"incident-missing-field"} +``` + +**Expected result:** Agent outputs: +``` +CASE ID: incident-missing-field +STATUS: SUCCESS +CHANGES APPLIED: +- Added missing field 'customerId' with value 'UNKNOWN' +VALIDATION: PASSED +REPAIRED JSON: +{"incidentId":"INC-9001","customerId":"UNKNOWN","priority":3,"details":{"description":"System outage"}} +``` + +
+ +
+Scenario 2: Type mismatches + +Priority as string number, nested field with wrong type: + +```json +{"caseId":"incident-wrong-type"} +``` + +**Expected result:** Agent outputs: +``` +CASE ID: incident-wrong-type +STATUS: SUCCESS +CHANGES APPLIED: +- Converted priority from string "2" to integer 2 +VALIDATION: PASSED +``` + +
+ +
+Scenario 3: Field standardization + +Non-canonical field names (`id`, `prio`, `cust_id`, `desc`): + +```json +{"caseId":"incident-field-standardization"} +``` + +**Expected result:** Agent outputs: +``` +CASE ID: incident-field-standardization +STATUS: SUCCESS +CHANGES APPLIED: +- Normalized field 'cust_id' to 'customerId' +- Normalized field 'prio' to 'priority' +- Normalized field 'details.desc' to 'details.description' +VALIDATION: PASSED +``` + +
+ +
+Scenario 4: Null object handling + +Null details object: + +```json +{"caseId":"incident-partial-object"} +``` + +**Expected result:** Agent outputs: +``` +CASE ID: incident-partial-object +STATUS: SUCCESS +CHANGES APPLIED: +- Replaced null details with {description: 'No details provided'} +VALIDATION: PASSED +``` + +
+ +**Tips:** +- Review **Agent log** tab to see which tools the agent called +- Check **Metadata** tab 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 includes preconfigured test data in the `LogicApps/data/` folder that deploys with the workflow package to your Logic App instance. This data is **ephemeral and resets on every deployment**—it is not intended for production use. Implementations should replace these demo components with persistent connectors and services. + +### Replace demo components + +| Component | Demo Implementation | Production Options | +|-----------|----------------------|-------------------| +| Incident Data Source | Embedded JSON files in workflow logic (no persistent `/data/incidents/` folder) | Azure Blob Storage (trigger on new blob), Cosmos DB (query by status), Event Grid (subscribe to events), Service Bus (process queue messages) | +| Schema Management | Embedded schema reference | Azure App Configuration, Blob Storage with versioning, Schema Registry with multi-tenant support | +| Output Destination | Response-only (no persistence) | Azure Blob Storage (partitioned writes), Azure Queue (downstream processing), Event Hub (real-time analytics), Cosmos DB (indexed storage) | +| Validation | Built-in JavaScript checks | Azure Data Factory validation, JSON Schema validators, custom validation APIs | + +### 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** +- Install [Azure Logic Apps (Standard) extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurelogicapps) +- In VS Code, select the Azure icon on the Activity Bar +- Under **Resources**, expand your subscription and find your deployed Logic App +- Expand **Workflows**, right-click a workflow, and select **Open Designer** to view/edit +- [Learn more about editing workflows in VS Code](https://learn.microsoft.com/azure/logic-apps/manage-logic-apps-visual-studio-code) + +--- + +## Workflows + +Three workflows process malformed JSON using autonomous AI decision-making: + +
+Workflow details + +### JsonRemediationAgent + +Orchestrates JSON remediation using an AI agent. The agent evaluates malformed data, autonomously selecting and sequencing repair tools. + +**Agent Tools:** +- **LoadIncidentJson** - Reads malformed JSON from app content folder with error handling +- **RepairJson** - Applies deterministic transformations (add fields, fix types, rename, normalize) +- **ValidateSchema** - Validates against incident.schema.json +- **FormatResponse** - Formats completion summary for the response + +**Process Flow:** + +```mermaid +flowchart TD + A[HTTP Trigger] + A --> B[Incident Request Summary] + + B --> C[Agent Loop Action] + + C --> D[Load Incident Json] + C --> E[Repair Json] + C --> F[Validate Schema] + C --> G[Format Response] + + D --> C + E --> C + F --> C + G --> C + + C --> H[HTTP Response] +``` + +### JsonRepairEngine (Nested) + +Applies deterministic transformations to malformed JSON: +- Adds required fields with defaults (e.g., `customerId: "UNKNOWN"`) +- Fixes type mismatches (string numbers → integers) +- Renames non-canonical fields (`prio` → `priority`, `cust_id` → `customerId`) +- Initializes null objects with default structures +- Returns repaired JSON and array of changes made + +### SchemaValidator (Nested) + +Validates JSON against schema definition: +- Checks all required fields exist (`incidentId`, `priority`, `customerId`, `details`) +- Validates field types (string, number, object) +- Verifies nested object requirements (`details.description`) +- Returns `isValid` boolean and array of violations + +
+ +
+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 JsonRemediationAgent 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). | +| **JSON parse failed** | LoadIncidentJson handles broken JSON automatically. If syntax is too corrupted, check violations array in response. | +| **Schema validation fails** | Check `violations[]` in response body. Common: missing fields, type mismatches, null objects. Enhance RepairJson transforms. | +| **Outbox files not found** | `/data/outbox/` is ephemeral (overwritten on redeploy). For production, use Azure Blob Storage. | +| **Agent loop timeout** | Increase limit count in AgentLoop action or simplify repair logic. Check Agent log for stuck tools. | +| **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 embedded in the workflow package to simplify exploration: +- **Incident files**: 5 malformed JSON files in `LogicApps/data/incidents/` +- **Schema definition**: `LogicApps/data/schema/incident.schema.json` +- **Outbox**: Simulated write to `LogicApps/data/outbox/` (ephemeral) + +**Data folder location**: `LogicApps/data/` is packaged with workflows.zip and deployed to the Logic App's application content folder. This folder is **overwritten on every deployment** and should not be used for durable storage in production. + +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/json-remediation-agent/json-remediation-agent.code-workspace b/samples/json-remediation-agent/json-remediation-agent.code-workspace new file mode 100644 index 0000000..84acaf5 --- /dev/null +++ b/samples/json-remediation-agent/json-remediation-agent.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/product-return-agent-sample/Deployment/main.bicep b/samples/product-return-agent-sample/Deployment/main.bicep new file mode 100644 index 0000000..d489101 --- /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 0000000..66e9756 --- /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 0000000..86b4fa1 Binary files /dev/null and b/samples/product-return-agent-sample/Deployment/workflows.zip differ diff --git a/samples/product-return-agent-sample/LogicApps/.funcignore b/samples/product-return-agent-sample/LogicApps/.funcignore new file mode 100644 index 0000000..5235706 --- /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 0000000..090e597 --- /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 0000000..25b6b94 --- /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 0000000..1ef9cca --- /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 0000000..871c3a8 --- /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 0000000..b5cbe8a --- /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 0000000..b9be6a8 --- /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 0000000..806d743 --- /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 0000000..fa69cd6 --- /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 0000000..0967ef4 --- /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 0000000..1de672a --- /dev/null +++ b/samples/product-return-agent-sample/README.md @@ -0,0 +1,313 @@ +# 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. + +**[Watch Demo Video](https://youtu.be/ilSGRUpjMdU)** + +--- + +## 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 0000000..84acaf5 --- /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 d2a7825..f081594 100644 --- a/samples/readme.md +++ b/samples/readme.md @@ -6,6 +6,9 @@ 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. +- [**Transaction Repair Agent**](./transaction-repair-agent/): Conversational AI agent that helps operations teams diagnose and repair failed work orders through natural chat interaction. Built with Azure Logic Apps Standard and Azure OpenAI, featuring guided workflows, approval management, and ITSM audit logging. +- [**JSON Remediation Agent**](./json-remediation-agent/): Autonomous operations agent that repairs malformed JSON records, applies deterministic schema corrections, validates structure, and outputs clean, ingestion-ready payloads with no external service dependencies. ## How to contribute diff --git a/samples/shared/README.md b/samples/shared/README.md new file mode 100644 index 0000000..45d1576 --- /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 0000000..1b8e5dd --- /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 0000000..2a4a2cf --- /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 0000000..fb9a60b --- /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 0000000..e501a93 --- /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 0000000..1ed0ed8 --- /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 0000000..449f211 --- /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 0000000..b39c290 --- /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 0000000..73ef27a --- /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 +// +// Logic Apps Agent Sample - Azure Infrastructure as Code +// Deploys Logic Apps Standard with Azure OpenAI for autonomous agent workflows +// 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 diff --git a/samples/transaction-repair-agent/Deployment/main.bicep b/samples/transaction-repair-agent/Deployment/main.bicep new file mode 100644 index 0000000..cab0011 --- /dev/null +++ b/samples/transaction-repair-agent/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 +// +// Transaction Repair Agent - Azure Infrastructure as Code +// Deploys Logic Apps Standard with Azure OpenAI for conversational operations agent +// 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 actual GitHub URL) +var workflowsZipUrl = 'https://raw.githubusercontent.com/modularity/logicapps-labs/transaction-repair-agent/samples/transaction-repair-agent/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/transaction-repair-agent/Deployment/sample-arm.json b/samples/transaction-repair-agent/Deployment/sample-arm.json new file mode 100644 index 0000000..c17d9e2 --- /dev/null +++ b/samples/transaction-repair-agent/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": "5815924213456264593" + } + }, + "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/transaction-repair-agent/samples/transaction-repair-agent/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/transaction-repair-agent/Deployment/workflows.zip b/samples/transaction-repair-agent/Deployment/workflows.zip new file mode 100644 index 0000000..4379654 Binary files /dev/null and b/samples/transaction-repair-agent/Deployment/workflows.zip differ diff --git a/samples/transaction-repair-agent/LogicApps/GetApproval/workflow.json b/samples/transaction-repair-agent/LogicApps/GetApproval/workflow.json new file mode 100644 index 0000000..f18db26 --- /dev/null +++ b/samples/transaction-repair-agent/LogicApps/GetApproval/workflow.json @@ -0,0 +1,55 @@ +{ + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "contentVersion": "1.0.0.0", + "actions": { + "Initialize_Approvals": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "ApprovalResponses", + "type": "object", + "value": { + "WO-12345": "APPROVED", + "WO-67890": "DENIED", + "WO-11111": "APPROVED" + } + } + ] + }, + "runAfter": {} + }, + "Response": { + "type": "Response", + "kind": "Http", + "inputs": { + "statusCode": 200, + "body": "@variables('ApprovalResponses')[triggerBody()?['trackingId']]" + }, + "runAfter": { + "Initialize_Approvals": ["Succeeded"] + } + } + }, + "contentVersion": "1.0.0.0", + "outputs": {}, + "triggers": { + "manual": { + "type": "Request", + "kind": "Http", + "inputs": { + "schema": { + "type": "object", + "properties": { + "trackingId": { + "type": "string" + } + } + } + } + } + } + }, + "kind": "Stateful" +} diff --git a/samples/transaction-repair-agent/LogicApps/GetFixPlan/workflow.json b/samples/transaction-repair-agent/LogicApps/GetFixPlan/workflow.json new file mode 100644 index 0000000..065bc11 --- /dev/null +++ b/samples/transaction-repair-agent/LogicApps/GetFixPlan/workflow.json @@ -0,0 +1,61 @@ +{ + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "contentVersion": "1.0.0.0", + "actions": { + "Initialize_Catalog": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "RepairCatalog", + "type": "object", + "value": { + "schema_validation_error": { + "fixSteps": ["Convert amount field from string to number", "Validate against order schema", "Resubmit to processing queue"] + }, + "timeout_config": { + "fixSteps": ["Increase API timeout from 30s to 300s", "Update retry policy", "Redeploy configuration"] + }, + "missing_required_field": { + "fixSteps": ["Add missing required field with default value", "Apply field mapping transformation"] + } + } + } + ] + }, + "runAfter": {} + }, + "Response": { + "type": "Response", + "kind": "Http", + "inputs": { + "statusCode": 200, + "body": "@variables('RepairCatalog')[triggerBody()?['failureType']]" + }, + "runAfter": { + "Initialize_Catalog": ["Succeeded"] + } + } + }, + "contentVersion": "1.0.0.0", + "outputs": {}, + "triggers": { + "manual": { + "type": "Request", + "kind": "Http", + "inputs": { + "schema": { + "type": "object", + "properties": { + "failureType": { + "type": "string" + } + } + } + } + } + } + }, + "kind": "Stateful" +} diff --git a/samples/transaction-repair-agent/LogicApps/GetWorkOrder/workflow.json b/samples/transaction-repair-agent/LogicApps/GetWorkOrder/workflow.json new file mode 100644 index 0000000..52494a5 --- /dev/null +++ b/samples/transaction-repair-agent/LogicApps/GetWorkOrder/workflow.json @@ -0,0 +1,83 @@ +{ + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "contentVersion": "1.0.0.0", + "actions": { + "Find_WorkOrder": { + "type": "Query", + "inputs": { + "from": "@variables('WorkOrderQueue')", + "where": "@equals(item()?['trackingId'], triggerBody()?['trackingId'])" + }, + "runAfter": { + "Initialize_Queue": [ + "Succeeded" + ] + } + }, + "Response": { + "type": "Response", + "kind": "Http", + "inputs": { + "statusCode": 200, + "body": "@first(body('Find_WorkOrder'))" + }, + "runAfter": { + "Find_WorkOrder": [ + "Succeeded" + ] + } + }, + "Initialize_Queue": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "WorkOrderQueue", + "type": "array", + "value": [ + { + "trackingId": "WO-12345", + "failureType": "schema_validation_error", + "payload": {"orderId": "ORD-9876", "amount": "250.00", "status": "failed"}, + "timestamp": "2025-12-15T10:30:00Z" + }, + { + "trackingId": "WO-67890", + "failureType": "timeout_config", + "payload": {"apiEndpoint": "/api/process", "timeout": 30, "error": "timeout_exceeded"}, + "timestamp": "2025-12-15T11:15:00Z" + }, + { + "trackingId": "WO-11111", + "failureType": "missing_required_field", + "payload": {"customerId": "CUST-555", "productId": "PROD-123"}, + "timestamp": "2025-12-15T09:45:00Z" + } + ] + } + ] + } + } + }, + "contentVersion": "1.0.0.0", + "outputs": {}, + "triggers": { + "manual": { + "type": "Request", + "kind": "Http", + "inputs": { + "schema": { + "type": "object", + "properties": { + "trackingId": { + "type": "string" + } + } + } + } + } + } + }, + "kind": "Stateful" +} diff --git a/samples/transaction-repair-agent/LogicApps/LogToITSM/workflow.json b/samples/transaction-repair-agent/LogicApps/LogToITSM/workflow.json new file mode 100644 index 0000000..a6276e1 --- /dev/null +++ b/samples/transaction-repair-agent/LogicApps/LogToITSM/workflow.json @@ -0,0 +1,42 @@ +{ + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "contentVersion": "1.0.0.0", + "actions": { + "Response": { + "type": "Response", + "kind": "Http", + "inputs": { + "statusCode": 200, + "body": { + "incidentId": "@concat('INC', string(rand(100000, 999999)))", + "trackingId": "@triggerBody()?['trackingId']", + "timestamp": "@utcNow()" + } + } + } + }, + "contentVersion": "1.0.0.0", + "outputs": {}, + "triggers": { + "manual": { + "type": "Request", + "kind": "Http", + "inputs": { + "schema": { + "type": "object", + "properties": { + "trackingId": { + "type": "string" + }, + "action": { + "type": "string" + } + } + } + } + } + } + }, + "kind": "Stateful" +} diff --git a/samples/transaction-repair-agent/LogicApps/ResubmitWorkOrder/workflow.json b/samples/transaction-repair-agent/LogicApps/ResubmitWorkOrder/workflow.json new file mode 100644 index 0000000..a50dddf --- /dev/null +++ b/samples/transaction-repair-agent/LogicApps/ResubmitWorkOrder/workflow.json @@ -0,0 +1,42 @@ +{ + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "contentVersion": "1.0.0.0", + "actions": { + "Response": { + "type": "Response", + "kind": "Http", + "inputs": { + "statusCode": 200, + "body": { + "status": "resubmitted", + "trackingId": "@triggerBody()?['trackingId']", + "timestamp": "@utcNow()" + } + } + } + }, + "contentVersion": "1.0.0.0", + "outputs": {}, + "triggers": { + "manual": { + "type": "Request", + "kind": "Http", + "inputs": { + "schema": { + "type": "object", + "properties": { + "trackingId": { + "type": "string" + }, + "payload": { + "type": "object" + } + } + } + } + } + } + }, + "kind": "Stateful" +} diff --git a/samples/transaction-repair-agent/LogicApps/TransactionRepairAgent/workflow.json b/samples/transaction-repair-agent/LogicApps/TransactionRepairAgent/workflow.json new file mode 100644 index 0000000..158ee23 --- /dev/null +++ b/samples/transaction-repair-agent/LogicApps/TransactionRepairAgent/workflow.json @@ -0,0 +1,208 @@ +{ + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "actions": { + "Transaction_Repair_Agent": { + "type": "Agent", + "inputs": { + "parameters": { + "deploymentId": "gpt-4o-mini", + "messages": [ + { + "role": "system", + "content": "You're a friendly transaction repair assistant helping operations teams fix failed work orders. When a user provides a work order tracking ID, follow these steps:\n\n1) Retrieve the failed work order details\n2) Analyze the failure and propose a repair plan\n3) Check if the repair requires approval\n4) If approved, resubmit the corrected work order. If denied, skip resubmission\n5) ALWAYS log the action (approved/denied) to ITSM for audit trail\n\nBe conversational and explain what you're doing at each step. Call ONE tool per turn. After logging to ITSM, confirm completion and ask if they need help with another work order." + } + ], + "agentModelType": "AzureOpenAI", + "agentModelSettings": { + "deploymentModelProperties": { + "name": "gpt-4o-mini", + "format": "OpenAI", + "version": "2024-07-18" + }, + "agentHistoryReductionSettings": { + "agentHistoryReductionType": "maximumTokenCountReduction", + "maximumTokenCount": 128000 + } + } + }, + "modelConfigurations": { + "model1": { + "referenceName": "agent" + } + } + }, + "tools": { + "Get_failed_workorder": { + "actions": { + "Call_GetWorkOrder": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "GetWorkOrder" + } + }, + "body": { + "trackingId": "@agentParameters('trackingId')" + } + } + } + }, + "description": "Get work order details by tracking ID", + "agentParameterSchema": { + "type": "object", + "properties": { + "trackingId": { + "type": "string", + "description": "Work order tracking ID (e.g., WO-12345)" + } + }, + "required": ["trackingId"] + } + }, + "Propose_fix": { + "actions": { + "Call_GetFixPlan": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "GetFixPlan" + } + }, + "body": { + "failureType": "@agentParameters('failureType')" + } + } + } + }, + "description": "Get repair plan for the failure type", + "agentParameterSchema": { + "type": "object", + "properties": { + "failureType": { + "type": "string", + "description": "Type of failure (e.g., 'Schema validation error', 'Timeout error')" + } + }, + "required": ["failureType"] + } + }, + "Get_approval": { + "actions": { + "Call_GetApproval": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "GetApproval" + } + }, + "body": { + "trackingId": "@agentParameters('trackingId')" + } + } + } + }, + "description": "Check if repair is approved for this work order", + "agentParameterSchema": { + "type": "object", + "properties": { + "trackingId": { + "type": "string", + "description": "Work order tracking ID" + } + }, + "required": ["trackingId"] + } + }, + "Resubmit_workorder": { + "actions": { + "Call_ResubmitWorkOrder": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "ResubmitWorkOrder" + } + }, + "body": { + "trackingId": "@agentParameters('trackingId')", + "payload": "@json(agentParameters('payload'))" + } + } + } + }, + "description": "Resubmit the corrected work order", + "agentParameterSchema": { + "type": "object", + "properties": { + "trackingId": { + "type": "string", + "description": "Work order tracking ID" + }, + "payload": { + "type": "string", + "description": "Corrected JSON payload as string" + } + }, + "required": ["trackingId", "payload"] + } + }, + "Log_to_itsm": { + "actions": { + "Call_LogToITSM": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "LogToITSM" + } + }, + "body": { + "trackingId": "@agentParameters('trackingId')", + "action": "@agentParameters('action')" + } + } + } + }, + "description": "Log repair action to ITSM system", + "agentParameterSchema": { + "type": "object", + "properties": { + "trackingId": { + "type": "string", + "description": "Work order tracking ID" + }, + "action": { + "type": "string", + "description": "Action taken (e.g., 'Resubmitted' or 'Denied')" + } + }, + "required": ["trackingId", "action"] + } + } + }, + "runAfter": { + "When_a_new_chat_session_starts": [ + "Succeeded" + ] + } + } + }, + "contentVersion": "1.0.0.0", + "outputs": {}, + "triggers": { + "When_a_new_chat_session_starts": { + "type": "Request", + "kind": "Agent", + "inputs": { + "agentName": "TransactionRepairAgent", + "agentDescription": "Analyzes failed work orders and orchestrates repair with approval workflow" + } + } + } + }, + "kind": "Agent" +} diff --git a/samples/transaction-repair-agent/LogicApps/connections.json b/samples/transaction-repair-agent/LogicApps/connections.json new file mode 100644 index 0000000..806d743 --- /dev/null +++ b/samples/transaction-repair-agent/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/transaction-repair-agent/LogicApps/data/approvals/approve-resubmit.json b/samples/transaction-repair-agent/LogicApps/data/approvals/approve-resubmit.json new file mode 100644 index 0000000..f15a170 --- /dev/null +++ b/samples/transaction-repair-agent/LogicApps/data/approvals/approve-resubmit.json @@ -0,0 +1,21 @@ +{ + "WO-12345": { + "approved": true, + "approver": "ops-manager", + "timestamp": "2025-12-15T10:30:00Z", + "notes": "Schema fix is safe - proceed with resubmit" + }, + "WO-67890": { + "approved": false, + "approver": "ops-manager", + "timestamp": "2025-12-15T10:35:00Z", + "notes": "Timeout issue requires infrastructure review before resubmit", + "reason": "requires-manual-review" + }, + "WO-11111": { + "approved": true, + "approver": "ops-supervisor", + "timestamp": "2025-12-15T10:40:00Z", + "notes": "Default shipping address can be applied - proceed" + } +} diff --git a/samples/transaction-repair-agent/LogicApps/data/itsm/incidents-example.json b/samples/transaction-repair-agent/LogicApps/data/itsm/incidents-example.json new file mode 100644 index 0000000..d1cf3ac --- /dev/null +++ b/samples/transaction-repair-agent/LogicApps/data/itsm/incidents-example.json @@ -0,0 +1,10 @@ +[ + { + "incidentId": "INC000123", + "trackingId": "WO-12345", + "activity": "Diagnosed schema-validation-failed, proposed fix, received approval, resubmitted successfully", + "operator": "ops-agent", + "timestamp": "2025-12-15T10:30:15Z", + "status": "logged" + } +] diff --git a/samples/transaction-repair-agent/LogicApps/data/queues/failed-workorders.json b/samples/transaction-repair-agent/LogicApps/data/queues/failed-workorders.json new file mode 100644 index 0000000..7696d47 --- /dev/null +++ b/samples/transaction-repair-agent/LogicApps/data/queues/failed-workorders.json @@ -0,0 +1,41 @@ +[ + { + "trackingId": "WO-12345", + "failureReason": "schema-validation-failed", + "failureDetails": "Field 'amount' has type string but should be number", + "payload": { + "orderId": "ORD-9001", + "customerId": "CUST-456", + "amount": "invalid", + "timestamp": "2025-12-15T09:00:00Z" + }, + "queueName": "order-processing", + "retries": 3 + }, + { + "trackingId": "WO-67890", + "failureReason": "timeout-retry-exhausted", + "failureDetails": "Downstream service did not respond within 30s after 5 retries", + "payload": { + "orderId": "ORD-9002", + "customerId": "CUST-789", + "amount": 150.00, + "timestamp": "2025-12-15T09:15:00Z" + }, + "queueName": "order-processing", + "retries": 5 + }, + { + "trackingId": "WO-11111", + "failureReason": "transform-patch-required", + "failureDetails": "Missing required field 'shippingAddress' in payload", + "payload": { + "orderId": "ORD-9003", + "customerId": "CUST-101", + "amount": 200.00, + "timestamp": "2025-12-15T09:30:00Z" + }, + "queueName": "order-processing", + "retries": 0 + } +] diff --git a/samples/transaction-repair-agent/LogicApps/data/repair-catalog.json b/samples/transaction-repair-agent/LogicApps/data/repair-catalog.json new file mode 100644 index 0000000..e94403c --- /dev/null +++ b/samples/transaction-repair-agent/LogicApps/data/repair-catalog.json @@ -0,0 +1,35 @@ +{ + "schema-validation-failed": { + "steps": [ + "Convert amount field from string to number", + "Validate against order schema", + "Resubmit to processing queue" + ], + "estimatedDuration": "2 minutes", + "risk": "low", + "category": "data-quality", + "documentation": "https://docs.example.com/schema-validation" + }, + "timeout-retry-exhausted": { + "steps": [ + "Increase timeout threshold to 60s", + "Reset retry counter", + "Resubmit with priority flag" + ], + "estimatedDuration": "5 minutes", + "risk": "medium", + "category": "connectivity", + "documentation": "https://docs.example.com/timeout-handling" + }, + "transform-patch-required": { + "steps": [ + "Add missing required field with default value", + "Apply field mapping transformation", + "Resubmit to processing queue" + ], + "estimatedDuration": "3 minutes", + "risk": "low", + "category": "transformation", + "documentation": "https://docs.example.com/field-mapping" + } +} diff --git a/samples/transaction-repair-agent/LogicApps/host.json b/samples/transaction-repair-agent/LogicApps/host.json new file mode 100644 index 0000000..5814d25 --- /dev/null +++ b/samples/transaction-repair-agent/LogicApps/host.json @@ -0,0 +1,15 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle.Workflows", + "version": "[1.*, 2.0.0)" + } +} diff --git a/samples/transaction-repair-agent/LogicApps/local.settings.json b/samples/transaction-repair-agent/LogicApps/local.settings.json new file mode 100644 index 0000000..45aac4b --- /dev/null +++ b/samples/transaction-repair-agent/LogicApps/local.settings.json @@ -0,0 +1,12 @@ +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "agent_openAIEndpoint": "", + "WORKFLOWS_LOCATION_NAME": "", + "WORKFLOWS_RESOURCE_GROUP_NAME": "", + "WORKFLOWS_SUBSCRIPTION_ID": "", + "agent_ResourceID": "", + "FUNCTIONS_WORKER_RUNTIME": "dotnet" + } +} diff --git a/samples/transaction-repair-agent/README.md b/samples/transaction-repair-agent/README.md new file mode 100644 index 0000000..675721e --- /dev/null +++ b/samples/transaction-repair-agent/README.md @@ -0,0 +1,381 @@ +# Transaction Repair Agent + +A conversational AI agent that helps operations teams diagnose and repair failed work orders through natural chat interaction. Built with Azure Logic Apps Standard and Azure OpenAI, the agent guides users through work order analysis, proposes fixes, manages approvals, resubmits transactions, and logs all activities to ITSM for audit compliance. + +**[Watch Demo Video](https://youtu.be/-V4n9VMcN0k)** | **[Agent Workflow Blog](https://techcommunity.microsoft.com/blog/integrationsonazureblog/%F0%9F%A4%96-agent-loop-demos-%F0%9F%A4%96/4414770)** + +--- + +## 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%2Ftransaction-repair-agent%2Fsamples%2Ftransaction-repair-agent%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-transactionrepair`) +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 conversational agent workflows with built-in test data + +
+ +
+What gets deployed + +| Resource | Purpose | +|----------|----------| +| Logic App Standard | Hosts conversational 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, Australia East + +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 = "txrepair") | +|----------|------------------------------------| +| Resource Group | `rg-txrepair` | +| Logic App | `txrepairxyz123-logicapp` | +| Azure OpenAI | `txrepair-openai` | +| Storage Account | `txrepairxyz123` | + +
+ +--- + +## Explore + +After deployment, test the conversational agent to see how it guides you through work order repair workflows + + + +### Run a test + +1. Open [Azure Portal](https://portal.azure.com) > your resource group > Logic App > **Workflows** > **TransactionRepairAgent** +2. Click **Overview** > **Chat** button to [start a conversation with the agent](https://learn.microsoft.com/azure/logic-apps/create-conversational-agent-workflows#run-and-test-your-conversational-agent) +3. Type or paste one of the test prompts below +4. Watch the agent retrieve work order details, propose fixes, check approvals, and log to ITSM +5. (Optional) Check [**Run history**](https://learn.microsoft.com/azure/logic-apps/monitor-logic-apps#review-runs-history) to see nested workflow calls + +**Test these scenarios to see different decision paths:** + +
+Test scenario 1: Schema validation failure (approved) + +Chat prompt: +``` +Process work order WO-12345 +``` + +**Expected behavior:** +- Agent retrieves work order (schema_validation_error, $250 amount) +- Proposes fix: Convert amount from string to number, validate schema +- Checks approval → **APPROVED** +- Resubmits corrected work order +- Logs to ITSM (generates incident ID like INC416577) +- Confirms completion and offers help with another work order + +**Run history shows:** 5 nested workflow calls (GetWorkOrder → GetFixPlan → GetApproval → ResubmitWorkOrder → LogToITSM) + +
+ +
+Test scenario 2: Timeout configuration (denied) + +Chat prompt: +``` +Let's start by retrieving the details of the failed work order WO-67890 +``` + +**Expected behavior:** +- Agent retrieves work order (timeout_config, 30s timeout) +- Proposes fix: Increase timeout to 300s, add retry policy +- Checks approval → **DENIED** +- Skips resubmission (correctly follows denial) +- Logs to ITSM automatically for audit trail +- Confirms completion + +**Run history shows:** 4 nested workflow calls (GetWorkOrder → GetFixPlan → GetApproval → LogToITSM, skips ResubmitWorkOrder) + +
+ +
+Test scenario 3: Missing required field (approved) + +Chat prompt: +``` +Help me fix work order WO-11111 +``` + +**Expected behavior:** +- Agent retrieves work order (missing_required_field, customerId/productId) +- Proposes fix: Add missing field, apply field mapping +- Checks approval → **APPROVED** +- Resubmits corrected work order +- Logs to ITSM (generates incident ID like INC217995) +- Confirms completion and offers help with another work order + +**Run history shows:** 5 nested workflow calls (GetWorkOrder → GetFixPlan → GetApproval → ResubmitWorkOrder → LogToITSM) + +
+ +**Tips:** + +- Agent is conversational and explains each step +- Agent calls **one tool per turn** (retrieves info, proposes fix, checks approval, etc.) +- Review **Run history** to see nested workflow execution +- Check run details for token usage in **Metadata** tab +- Conversations complete in 15-30 seconds (multiple turns) +- [Learn more about conversational agents](https://learn.microsoft.com/azure/logic-apps/create-conversational-agent-workflows) + +--- + +## Extend + +This sample uses **nested workflows with mock data** to eliminate external dependencies. Here's how to extend it for production use: + +**Note:** The Chat interface is for testing only. For production scenarios with external users, configure proper authentication. [Learn more about securing conversational agent workflows](https://learn.microsoft.com/azure/logic-apps/create-conversational-agent-workflows?tabs=standard#trigger-or-run-the-workflow). + +### Replace demo services + +| Component | Demo Implementation | Production Options | +|-----------|----------------------|-------------------| +| Work Order Queue | **GetWorkOrder** nested workflow with mock array (3 work orders) | Azure Service Bus, Cosmos DB, SQL Database, Event Grid, REST APIs | +| Repair Catalog | **GetFixPlan** nested workflow with mock catalog (3 failure types) | Azure App Configuration, Cosmos DB rules engine, AI recommendations, knowledge base | +| Approval System | **GetApproval** nested workflow with mock responses (3 approvals) | Teams Adaptive Cards, Power Automate approvals, ServiceNow, custom approval API | +| Work Order Submission | **ResubmitWorkOrder** nested workflow (simulated) | Azure Service Bus send, REST API POST, ERP integration, queue writers | +| ITSM Logging | **LogToITSM** nested workflow (generates incident IDs) | ServiceNow REST API, Azure Monitor, Application Insights, Jira, custom webhook | + +### 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** +- Install [Azure Logic Apps (Standard) extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurelogicapps) +- In VS Code, select the Azure icon on the Activity Bar +- Under **Resources**, expand your subscription and find your deployed Logic App +- Expand **Workflows**, right-click a workflow, and select **Open Designer** to view/edit +- [Learn more about editing workflows in VS Code](https://learn.microsoft.com/azure/logic-apps/manage-logic-apps-visual-studio-code) + +### Integrate with external systems + +Replace nested workflow mock data with real connectors: + +**For GetWorkOrder (Work Order Queue):** +- Add [Azure Service Bus connector](https://learn.microsoft.com/azure/connectors/connectors-create-api-servicebus) to receive messages from queue +- Or use [Cosmos DB connector](https://learn.microsoft.com/azure/connectors/connectors-create-api-cosmos-db) to query work order database + +**For GetApproval (Approval System):** +- Add [Microsoft Teams connector](https://learn.microsoft.com/azure/connectors/connectors-create-api-teams) with Adaptive Cards for human approval +- Or use [HTTP connector](https://learn.microsoft.com/azure/connectors/connectors-native-http) to call custom approval API + +**For LogToITSM (ITSM Logging):** +- Add [ServiceNow connector](https://learn.microsoft.com/azure/connectors/connectors-create-api-servicenow) to create incident records +- Or use [HTTP connector](https://learn.microsoft.com/azure/connectors/connectors-native-http) to call custom ITSM webhook + +**For ResubmitWorkOrder (Work Order Submission):** +- Add [Azure Service Bus connector](https://learn.microsoft.com/azure/connectors/connectors-create-api-servicebus) to send corrected work order to queue +- Or use [HTTP connector](https://learn.microsoft.com/azure/connectors/connectors-native-http) to POST to processing API + +--- + +## Workflows + +Six workflows process work order repairs using conversational AI interaction: + +
+Workflow details + +### TransactionRepairAgent + +Conversational agent that guides users through work order repair workflows. The agent analyzes failed work orders, proposes fixes, manages approvals, resubmits transactions, and logs all activities to ITSM for audit compliance. + +**Agent Tools (Nested Workflows):** +- **Get_failed_workorder** - Retrieves failed work order details by tracking ID (calls GetWorkOrder workflow) +- **Propose_fix** - Analyzes failure type and proposes repair steps (calls GetFixPlan workflow) +- **Get_approval** - Checks if repair requires approval and retrieves decision (calls GetApproval workflow) +- **Resubmit_workorder** - Resubmits corrected work order to processing queue (calls ResubmitWorkOrder workflow) +- **Log_to_itsm** - Generates ITSM incident for audit trail (calls LogToITSM workflow) + +**Process Flow:** + +```mermaid +flowchart TD + A[Chat Trigger
When new chat session starts] + A --> B[Transaction Repair Agent
Conversational AI] + + B --> C{Agent Multi-turn
Conversation} + + C --> D[Get Failed Workorder
nested workflow] + C --> E[Propose Fix
nested workflow] + C --> F[Get Approval
nested workflow] + C --> G[Resubmit Workorder
nested workflow] + C --> H[Log To ITSM
nested workflow] + + D --> C + E --> C + F --> C + G --> C + H --> C + + C --> I[Response to user] +``` + +### GetWorkOrder + +Retrieves failed work order details by tracking ID. Uses Query action to find matching work order from mock data array. + +**Mock Data:** 3 work orders (WO-12345, WO-67890, WO-11111) with different failure types + +### GetFixPlan + +Analyzes failure type and returns repair steps from catalog. Looks up fix plan based on failure reason. + +**Mock Data:** Repair catalog with 3 failure types (schema_validation_error, timeout_config, missing_required_field) + +### GetApproval + +Checks approval status for repair request. Returns approval decision based on tracking ID. + +**Mock Data:** 3 approval responses (WO-12345: APPROVED, WO-67890: DENIED, WO-11111: APPROVED) + +### ResubmitWorkOrder + +Resubmits corrected work order to processing queue. Returns confirmation with tracking ID and timestamp. + +**Mock Data:** Simulated resubmission (returns status "resubmitted") + +### LogToITSM + +Generates ITSM incident record for audit trail. Returns incident ID with tracking information. + +**Mock Data:** Generates random incident IDs (INC######) + +
+ +
+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 conversational AI agent in TransactionRepairAgent 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 → save → rename back > save. [Details](https://learn.microsoft.com/answers/questions/2046895). | +| **No Chat button** | Verify TransactionRepairAgent workflow `kind` is `"Agent"` and trigger `kind` is `"Agent"`. | +| **Chat not responding** | Wait 1-2 min, refresh. Check run history for errors. Verify OpenAI model is active. | +| **Nested workflow error** | Verify all nested workflows have Response actions at workflow level with statusCode 200. | + +
+ +
+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 nested workflows with mock data to simplify exploration: +- **Work orders:** GetWorkOrder workflow (3 mock work orders) +- **Repair catalog:** GetFixPlan workflow (3 failure types) +- **Approval system:** GetApproval workflow (predetermined decisions) +- **ITSM logging:** LogToITSM workflow (generated incident IDs) + +**Note:** The Chat interface is for testing only. For production use with external users, configure proper authentication. [Learn more](https://learn.microsoft.com/azure/logic-apps/create-conversational-agent-workflows?tabs=standard#trigger-or-run-the-workflow). + +For production integration options, see [Extend](#extend). + +
+ +
+Architecture decisions + +**Why nested workflows?** +- Demonstrates agent tool pattern with callable workflows +- Each tool is independently testable +- Clear separation of concerns (retrieval, analysis, actions) +- Easy to extend with real connectors +- Reusable workflows across multiple agents + +**Why conversational agent (not autonomous)?** +- Human-in-the-loop for work order decisions +- Operations teams prefer guided workflows +- Natural language interaction for diagnostics +- Multi-turn conversation pattern +- First conversational agent sample in repository + +**Why deterministic mock data?** +- Reproducible test results +- No external dependencies to configure +- Clear demonstration of approval workflows +- Easy to extend to Teams/ServiceNow/etc. + +
+ +
+Resources + +**Agent workflows:** [Create conversational agents](https://learn.microsoft.com/azure/logic-apps/create-conversational-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) + +