From ce1812165aae678235acea8c49b993479bd97126 Mon Sep 17 00:00:00 2001 From: greenie-msft Date: Wed, 9 Jul 2025 14:55:31 -0700 Subject: [PATCH 1/7] Add agents prompt chaining example --- .../PromptChaining/AgentChainingSample.csproj | 31 ++ .../PromptChaining/AgentChainingSample.sln | 33 ++ .../PromptChaining/Client/Client.csproj | 11 +- .../Agents/PromptChaining/Client/Program.cs | 355 ++++++++++---- .../dotnet/Agents/PromptChaining/README.md | 192 +++++++- .../Worker/Activities/ContentActivities.cs | 36 +- .../ContentCreationOrchestration.cs | 8 +- .../Agents/PromptChaining/Worker/Program.cs | 43 +- .../Worker/Services/AgentServices.cs | 428 +++++++++++++++++ .../Worker/Services/IAgentService.cs | 436 ++++++++++++++++++ .../PromptChaining/Worker/Worker.csproj | 13 +- 11 files changed, 1454 insertions(+), 132 deletions(-) create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/AgentChainingSample.csproj create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/AgentChainingSample.sln create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/AgentServices.cs create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/IAgentService.cs diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/AgentChainingSample.csproj b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/AgentChainingSample.csproj new file mode 100644 index 0000000..0f41321 --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/AgentChainingSample.csproj @@ -0,0 +1,31 @@ + + + + Exe + net8.0 + enable + enable + + true + 1.0.0 + AgentChainingSample + Sample demonstrating agent prompt chaining with Durable Task + Copyright © 2025 + Microsoft + AgentChainingSample + + + + + + + + + + + + + + + + diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/AgentChainingSample.sln b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/AgentChainingSample.sln new file mode 100644 index 0000000..888097e --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/AgentChainingSample.sln @@ -0,0 +1,33 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client", "Client\Client.csproj", "{62F7AE2C-3F09-46F7-BF2E-837A32AB4F5A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Worker", "Worker\Worker.csproj", "{5E211685-5D13-499A-9226-C319D9C3D057}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared", "Shared\Shared.csproj", "{FD60FF3D-89E1-4294-9F2D-F9F2DEF36EEB}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {62F7AE2C-3F09-46F7-BF2E-837A32AB4F5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {62F7AE2C-3F09-46F7-BF2E-837A32AB4F5A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {62F7AE2C-3F09-46F7-BF2E-837A32AB4F5A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {62F7AE2C-3F09-46F7-BF2E-837A32AB4F5A}.Release|Any CPU.Build.0 = Release|Any CPU + {5E211685-5D13-499A-9226-C319D9C3D057}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5E211685-5D13-499A-9226-C319D9C3D057}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5E211685-5D13-499A-9226-C319D9C3D057}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5E211685-5D13-499A-9226-C319D9C3D057}.Release|Any CPU.Build.0 = Release|Any CPU + {FD60FF3D-89E1-4294-9F2D-F9F2DEF36EEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FD60FF3D-89E1-4294-9F2D-F9F2DEF36EEB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FD60FF3D-89E1-4294-9F2D-F9F2DEF36EEB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FD60FF3D-89E1-4294-9F2D-F9F2DEF36EEB}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/Client.csproj b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/Client.csproj index f8e179b..d1b3ffc 100644 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/Client.csproj +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/Client.csproj @@ -1,5 +1,5 @@ - - + + Exe @@ -8,6 +8,7 @@ enable AgentChainingSample.Client AgentChainingSample.Client + 9d1ef95e-1a0b-440c-96b9-87fad3f1091c @@ -19,8 +20,4 @@ - - - - - + \ No newline at end of file diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/Program.cs b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/Program.cs index 01c9009..24964ab 100644 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/Program.cs +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/Program.cs @@ -1,34 +1,37 @@ using Azure.Identity; +using Microsoft.AspNetCore.Mvc; using Microsoft.DurableTask; using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Client.AzureManaged; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using AgentChainingSample.Shared.Models; +using AgentChainingSample.Client.Models; using System.Text.Json; -using System.Threading.Tasks; -using System.Diagnostics; -// Configure configuration -var configuration = new ConfigurationBuilder() - .AddEnvironmentVariables() - .Build(); +var builder = WebApplication.CreateBuilder(args); -// Configure logging -using ILoggerFactory loggerFactory = LoggerFactory.Create(builder => -{ - builder.AddConsole(); - builder.SetMinimumLevel(LogLevel.Information); -}); - -ILogger logger = loggerFactory.CreateLogger(); -logger.LogInformation("Starting Agent Chaining Sample - Content Creation Client"); +// Add services to the container +builder.Services.AddEndpointsApiExplorer(); // Get connection string from configuration with fallback to default local emulator connection -string connectionString = configuration["DTS_CONNECTION_STRING"] ?? +string connectionString = builder.Configuration["ENDPOINT"] ?? + builder.Configuration["DTS_CONNECTION_STRING"] ?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"; +// If we have the endpoint but not a full connection string, construct it +if (connectionString.StartsWith("Endpoint=") && !connectionString.Contains("TaskHub=")) +{ + string taskHub = builder.Configuration["TASKHUB"] ?? builder.Configuration["TASKHUB_NAME"] ?? "default"; + string clientId = builder.Configuration["AZURE_MANAGED_IDENTITY_CLIENT_ID"] ?? ""; + + if (!string.IsNullOrEmpty(clientId)) + { + connectionString = $"{connectionString};Authentication=ManagedIdentity;ClientID={clientId};TaskHub={taskHub}"; + } + else + { + connectionString = $"{connectionString};TaskHub={taskHub}"; + } +} + // Determine if we're connecting to the local emulator bool isLocalEmulator = connectionString.Contains("localhost"); @@ -36,123 +39,271 @@ if (isLocalEmulator) { AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); - logger.LogInformation("Using local emulator"); + builder.Services.AddLogging(logging => logging.AddConsole().SetMinimumLevel(LogLevel.Information)); + builder.Services.AddDurableTaskClient(options => + { + options.UseDurableTaskScheduler(connectionString); + }); } else { - logger.LogInformation("Using Azure endpoint with DefaultAzure authentication"); + builder.Services.AddLogging(logging => logging.AddConsole().SetMinimumLevel(LogLevel.Information)); + builder.Services.AddDurableTaskClient(options => + { + options.UseDurableTaskScheduler(connectionString); + }); } -logger.LogInformation("Connection string: {ConnectionString}", connectionString); +var app = builder.Build(); +var logger = app.Services.GetRequiredService>(); + +logger.LogInformation("Starting Agent Chaining Sample - Content Creation Client"); +// Log the connection string with sensitive info redacted +string managedIdentityClientId = builder.Configuration["AZURE_MANAGED_IDENTITY_CLIENT_ID"] ?? ""; +string logConnectionString = !string.IsNullOrEmpty(managedIdentityClientId) ? + connectionString.Replace(managedIdentityClientId, "[REDACTED]") : + connectionString; +logger.LogInformation("Connection string: {ConnectionString}", logConnectionString); +logger.LogInformation("TaskHub: {TaskHub}", builder.Configuration["TASKHUB"] ?? builder.Configuration["TASKHUB_NAME"] ?? "default"); +logger.LogInformation("Environment Variables:"); +logger.LogInformation(" TASKHUB: {Value}", builder.Configuration["TASKHUB"]); +logger.LogInformation(" TASKHUB_NAME: {Value}", builder.Configuration["TASKHUB_NAME"]); +logger.LogInformation(" ENDPOINT: {Value}", builder.Configuration["ENDPOINT"]); +logger.LogInformation(" DTS_CONNECTION_STRING: {Value}", builder.Configuration["DTS_CONNECTION_STRING"]); +logger.LogInformation(" DTS_URL: {Value}", builder.Configuration["DTS_URL"]); logger.LogInformation("This sample implements a news article generator workflow with multiple specialized agents"); -// Create the client using DI service provider -ServiceCollection services = new ServiceCollection(); -services.AddLogging(builder => builder.AddConsole()); +// Configure the HTTP request pipeline +app.UseHttpsRedirection(); -// Register the client, which can be used to start orchestrations -services.AddDurableTaskClient(options => +// Define routes +app.MapGet("/", () => { - options.UseDurableTaskScheduler(connectionString); + return "Agent Chaining Content Generator API - Use /api/content to create content"; }); -ServiceProvider serviceProvider = services.BuildServiceProvider(); -DurableTaskClient client = serviceProvider.GetRequiredService(); +// Add a health check endpoint +app.MapGet("/health", () => Results.Ok("Healthy")); -try +// Get status of an orchestration +app.MapGet("/api/content/{instanceId}", async (string instanceId, [FromServices] DurableTaskClient client) => { - // Ask for the news topic - Console.WriteLine("\nEnter a news topic to research and generate an article (or type 'exit' to quit):"); - string? topic = Console.ReadLine(); - - while (!string.IsNullOrWhiteSpace(topic) && topic.ToLower() != "exit") + try { - // Create the request - ContentCreationRequest request = new ContentCreationRequest + var metadata = await client.GetInstanceAsync(instanceId, getInputsAndOutputs: true); + if (metadata == null) { - Topic = topic, - RequestId = Guid.NewGuid().ToString() - }; + return Results.NotFound($"No orchestration found with ID: {instanceId}"); + } + + // If the orchestration is complete, return the result + if (metadata.RuntimeStatus == OrchestrationRuntimeStatus.Completed && metadata.ReadOutputAs() is ContentWorkflowResult result) + { + return Results.Ok(result); + } + + // Otherwise return status + return Results.Ok(new + { + InstanceId = metadata.InstanceId, + CreatedAt = metadata.CreatedAt, + LastUpdatedAt = metadata.LastUpdatedAt, + Status = metadata.RuntimeStatus.ToString() + }); + } + catch (Exception ex) + { + logger.LogError(ex, "Error getting orchestration status"); + return Results.Problem("Error retrieving orchestration status"); + } +}); + +// Create a new content generation request +app.MapPost("/api/content", async ([FromBody] ContentCreationRequest request, [FromServices] DurableTaskClient client) => +{ + try + { + if (string.IsNullOrEmpty(request.Topic)) + { + return Results.BadRequest("Topic is required"); + } + + // Set request ID if not provided + request.RequestId ??= Guid.NewGuid().ToString(); // Start the orchestration string instanceId = await client.ScheduleNewOrchestrationInstanceAsync( "ContentCreationOrchestration", request); - logger.LogInformation("Started orchestration with ID: {InstanceId}", instanceId); + logger.LogInformation("Started orchestration with ID: {InstanceId} for topic: {Topic}", instanceId, request.Topic); - // Wait for the orchestration to complete with timeout - logger.LogInformation("Waiting for orchestration to complete..."); - using var timeoutCts = new CancellationTokenSource(TimeSpan.FromMinutes(5)); - OrchestrationMetadata metadata = await client.WaitForInstanceCompletionAsync( - instanceId, - getInputsAndOutputs: true, - timeoutCts.Token); + return Results.Accepted($"/api/content/{instanceId}", new + { + InstanceId = instanceId, + Topic = request.Topic, + Status = "Accepted", + StatusQueryGetUri = $"/api/content/{instanceId}" + }); + } + catch (Exception ex) + { + logger.LogError(ex, "Error starting orchestration"); + return Results.Problem("Error starting content generation process"); + } +}); + +// Get all active orchestrations +app.MapGet("/api/content", async ([FromServices] DurableTaskClient client) => +{ + try + { + var query = new OrchestrationQuery + { + PageSize = 100, + // Get only running and pending orchestrations using the correct property + // Check latest API documentation for property name + FetchInputsAndOutputs = false + }; + + var resultList = new List(); + await foreach (var instance in client.GetAllInstancesAsync(query)) + { + resultList.Add(new + { + InstanceId = instance.InstanceId, + CreatedAt = instance.CreatedAt, + LastUpdatedAt = instance.LastUpdatedAt, + Status = instance.RuntimeStatus.ToString() + }); + } - if (metadata.RuntimeStatus == OrchestrationRuntimeStatus.Completed) + return Results.Ok(resultList); + } + catch (Exception ex) + { + logger.LogError(ex, "Error listing orchestrations"); + return Results.Problem("Error retrieving orchestrations"); + } +}); + +// Get the final HTML document for viewing +app.MapGet("/api/content/{instanceId}/document", async (string instanceId, [FromServices] DurableTaskClient client) => +{ + try + { + var metadata = await client.GetInstanceAsync(instanceId, getInputsAndOutputs: true); + if (metadata == null) { - ContentWorkflowResult? result = metadata.ReadOutputAs(); - - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine("\n===== NEWS ARTICLE GENERATION COMPLETED ====="); - Console.ResetColor(); - - Console.WriteLine($"\nTopic: {result?.Topic}"); - - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("\n----- RESEARCH SUMMARY -----"); - Console.ResetColor(); - Console.WriteLine(result?.ResearchData.Summary); - - Console.WriteLine("\nSources Found:"); - foreach (var source in result?.ResearchData.Sources ?? new List()) + return Results.NotFound($"No orchestration found with ID: {instanceId}"); + } + + if (metadata.RuntimeStatus == OrchestrationRuntimeStatus.Completed && metadata.ReadOutputAs() is ContentWorkflowResult result) + { + if (!string.IsNullOrEmpty(result.FinalArticle)) + { + return Results.Content(result.FinalArticle, "text/html", System.Text.Encoding.UTF8); + } + else { - Console.WriteLine($"- {source.Title}: {source.Url}"); + return Results.NotFound("Final article content not available"); } - - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("\n----- ARTICLE CONTENT -----"); - Console.ResetColor(); - Console.WriteLine(result?.ArticleContent); - - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("\n----- GENERATED IMAGES -----"); - Console.ResetColor(); - foreach (var image in result?.GeneratedImages ?? new List()) + } + else + { + return Results.BadRequest($"Orchestration is not completed. Current status: {metadata.RuntimeStatus}"); + } + } + catch (Exception ex) + { + logger.LogError(ex, "Error retrieving document"); + return Results.Problem("Error retrieving document"); + } +}); + +// Download the final HTML document as a file +app.MapGet("/api/content/{instanceId}/download", async (string instanceId, [FromServices] DurableTaskClient client) => +{ + try + { + var metadata = await client.GetInstanceAsync(instanceId, getInputsAndOutputs: true); + if (metadata == null) + { + return Results.NotFound($"No orchestration found with ID: {instanceId}"); + } + + if (metadata.RuntimeStatus == OrchestrationRuntimeStatus.Completed && metadata.ReadOutputAs() is ContentWorkflowResult result) + { + if (!string.IsNullOrEmpty(result.FinalArticle)) { - Console.WriteLine($"- {image.Description}"); - Console.WriteLine($" Caption: {image.Caption}"); - Console.WriteLine($" DALL-E Prompt: {image.Prompt}"); - Console.WriteLine(); + var contentBytes = System.Text.Encoding.UTF8.GetBytes(result.FinalArticle); + var fileName = $"article-{instanceId}-{DateTime.UtcNow:yyyyMMddHHmmss}.html"; + + return Results.File(contentBytes, "text/html", fileName); } - - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("\n----- COMPLETE ARTICLE WITH IMAGES -----"); - Console.ResetColor(); - - if (!string.IsNullOrEmpty(result?.ArticleBlobUrl)) + else { - Console.WriteLine($"Article is available online at: {result?.ArticleBlobUrl}"); - Console.WriteLine($"Local HTML file saved at: {result?.ArticleFilePath}"); - Console.WriteLine(); + return Results.NotFound("Final article content not available"); } - - Console.WriteLine(result?.FinalArticle); - - // Ask for another topic - Console.WriteLine("\nEnter another news topic to research (or type 'exit' to quit):"); - topic = Console.ReadLine(); } else { - Console.WriteLine($"Orchestration ended with status: {metadata.RuntimeStatus}"); - break; + return Results.BadRequest($"Orchestration is not completed. Current status: {metadata.RuntimeStatus}"); + } + } + catch (Exception ex) + { + logger.LogError(ex, "Error downloading document"); + return Results.Problem("Error downloading document"); + } +}); + +// Wait for a specific result (polling endpoint) +app.MapGet("/api/content/{instanceId}/wait", async (string instanceId, int timeoutSeconds, [FromServices] DurableTaskClient client) => +{ + try + { + timeoutSeconds = Math.Min(timeoutSeconds, 60); // Cap at 60 seconds max + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)); + + OrchestrationMetadata metadata = await client.WaitForInstanceCompletionAsync( + instanceId, + getInputsAndOutputs: true, + timeoutCts.Token); + + if (metadata.RuntimeStatus == OrchestrationRuntimeStatus.Completed) + { + ContentWorkflowResult? result = metadata.ReadOutputAs(); + return Results.Ok(result); + } + else + { + return Results.Ok(new + { + InstanceId = metadata.InstanceId, + Status = metadata.RuntimeStatus.ToString(), + Message = "Orchestration not yet complete" + }); } } + catch (OperationCanceledException) + { + return Results.Accepted($"/api/content/{instanceId}", new { Message = "Operation timed out, but still processing" }); + } + catch (Exception ex) + { + logger.LogError(ex, "Error waiting for orchestration"); + return Results.Problem("Error waiting for content generation to complete"); + } +}); + +// Start the app +try +{ + logger.LogInformation("Starting web host on port 5000"); + app.Run("http://0.0.0.0:5000"); } catch (Exception ex) { - logger.LogError(ex, "Error in client application"); -} - -logger.LogInformation("Client application stopped"); + logger.LogError(ex, "Error starting client application"); +} \ No newline at end of file diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/README.md b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/README.md index 647b9e9..2717be4 100644 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/README.md +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/README.md @@ -38,7 +38,11 @@ The workflow is orchestrated using the Durable Task SDK, which handles the workf ```bash # Required: Azure AI Projects endpoint - export AGENT_CONNECTION_STRING="https://your-ai-project-endpoint.services.ai.azure.com/api/projects/your-project-id" + export AGENT_CONNECTION_STRING="https://{region}.aiprojects.azure.com/api/projects/{resourceGroup}/{projectName}" + + # Note: The AGENT_CONNECTION_STRING must be in URL format as shown above. + # The format should match: https://{region}.aiprojects.azure.com/api/projects/{resourceGroup}/{projectName} + # Example: https://eastus.aiprojects.azure.com/api/projects/my-resource-group/my-ai-project # Optional: OpenAI model name (defaults to "gpt-4") export OPENAI_DEPLOYMENT_NAME="gpt-4-turbo" @@ -66,13 +70,195 @@ The workflow is orchestrated using the Durable Task SDK, which handles the workf 4. **Generate an Article** - Follow the prompts in the client console to enter a news topic. The application will: + **Option 1: Using the HTTP API** + + The client exposes an HTTP API endpoint at http://localhost:5000. You can use tools like curl or any HTTP client to interact with it: + + ```bash + # Make a request to generate an article + curl -X POST http://localhost:5000/api/articles \ + -H "Content-Type: application/json" \ + -d '{"topic": "renewable energy innovations"}' + + # Check the status of an article generation + curl http://localhost:5000/api/articles/{instanceId} + ``` + + There is also a test.http file that can be used with the VS Code REST Client extension. + + **Option 2: Using the Console Interface** + + Alternatively, follow the prompts in the client console to enter a news topic. The application will: - Research the topic - Generate article content - Create supporting images - Save an HTML file with the complete article When finished, the client will show the path to your generated HTML file (typically in a temp directory like `/var/folders/.../T/article-generator/` on macOS). + +## Deploy to Azure + +This sample includes everything needed to deploy to Azure using the Azure Developer CLI (azd). The deployment will automatically provision all required Azure resources and deploy your application. + +### Prerequisites + +Before deploying to Azure, ensure you have: + +- **Azure CLI**: [Download and install](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli) +- **Azure Developer CLI (azd)**: [Download and install](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) +- **Azure Subscription**: You'll need an active Azure subscription +- **Docker**: Required for building container images + +### Step-by-Step Deployment + +1. **Login to Azure** + ```bash + # Login with your Azure account + az login + + # Set your subscription (if you have multiple) + az account set --subscription "your-subscription-id" + ``` + +2. **Initialize the Azure Developer CLI** + ```bash + # Initialize azd in the project directory + azd init + + # This will detect the existing azure.yaml configuration + ``` + +3. **Deploy to Azure** + ```bash + # Deploy the entire application with one command + azd up + ``` + + The `azd up` command will: + - **Provision Infrastructure**: Create all required Azure resources + - **Build Images**: Build Docker containers for the client and worker + - **Deploy Services**: Deploy to Azure Container Apps + - **Configure Networking**: Set up ingress and internal communication + - **Set Environment Variables**: Configure all necessary settings + +### What Gets Deployed + +The deployment creates the following Azure resources: + +| Resource | Type | Purpose | +|----------|------|---------| +| **Resource Group** | `Microsoft.Resources/resourceGroups` | Contains all project resources | +| **Container Apps Environment** | `Microsoft.App/managedEnvironments` | Runtime environment for containers | +| **Container Registry** | `Microsoft.ContainerRegistry/registries` | Stores Docker images | +| **Client App** | `Microsoft.App/containerApps` | Web API frontend (publicly accessible) | +| **Worker App** | `Microsoft.App/containerApps` | Background worker (internal only) | +| **Durable Task Scheduler** | `Microsoft.DurableTask/schedulers` | Orchestration engine | +| **AI Project** | `Microsoft.CognitiveServices/accounts` | Azure AI services | +| **OpenAI Deployments** | `Microsoft.CognitiveServices/accounts/deployments` | GPT-4o-mini and DALL-E 3 models | +| **Log Analytics** | `Microsoft.OperationalInsights/workspaces` | Application monitoring and logs | +| **Managed Identity** | `Microsoft.ManagedIdentity/userAssignedIdentities` | Secure authentication | + +### Post-Deployment + +After successful deployment, you'll see output similar to: + +```bash +SUCCESS: Your application was deployed to Azure in 4 minutes. + +You can view the resources created under the resource group rg- in Azure Portal: +https://portal.azure.com/#@/resource/subscriptions/.../resourceGroups/rg-/overview + +Services: + client https://ca--client..azurecontainerapps.io/ + worker (internal only) +``` + +### Using Your Deployed Application + +1. **Test the API** + + Use the provided client URL to interact with your deployed application: + + ```bash + # Replace with your actual deployment URL + curl -X POST /api/content \ + -H "Content-Type: application/json" \ + -d '{"topic": "Latest developments in AI technology"}' + ``` + +2. **View Generated Articles** + + When an orchestration completes, the response will include an `articleEndpoint` property: + + ```json + { + "instanceId": "abc123...", + "articleEndpoint": "https://ca-xyz-client.region.azurecontainerapps.io/api/content/abc123.../document", + "status": "Completed" + } + ``` + + Open the `articleEndpoint` URL in your browser to view the generated HTML article. + +3. **Monitor Your Application** + + - **Azure Portal**: View resources and metrics + - **Container Apps Logs**: Monitor application logs and performance + - **Log Analytics**: Query detailed telemetry and traces + +### Managing Your Deployment + +**Update your application:** +```bash +# Deploy code changes +azd deploy + +# Update infrastructure and deploy +azd up +``` + +**View deployment status:** +```bash +# Show current deployment information +azd show + +# View environment variables +azd env get-values +``` + +**Clean up resources:** +```bash +# Remove all Azure resources (be careful!) +azd down +``` + +### Troubleshooting Deployment + +**Common Issues:** + +1. **Insufficient permissions**: Ensure your Azure account has `Contributor` or `Owner` role +2. **Resource naming conflicts**: Try a different environment name with `azd env set AZURE_ENV_NAME ` +3. **Deployment timeout**: Container builds can take time; wait for completion +4. **Region availability**: Some Azure AI services may not be available in all regions + +**View detailed logs:** +```bash +# View container app logs +az containerapp logs show --name --resource-group + +# View deployment history +azd deploy --debug +``` + +### Cost Considerations + +The deployed resources will incur costs based on usage: +- **Container Apps**: Pay-per-use scaling model +- **Azure OpenAI**: Token-based pricing for GPT and DALL-E +- **Container Registry**: Storage costs for images +- **Log Analytics**: Data ingestion and retention costs + +Consider setting up [Azure Cost Management](https://learn.microsoft.com/en-us/azure/cost-management-billing/) alerts to monitor spending. ## How It Works The application uses a durable orchestration workflow with four key steps: @@ -111,4 +297,4 @@ Monitor your workflow executions in the Durable Task Scheduler dashboard: - [Durable Task SDK for .NET](https://github.com/microsoft/durabletask-dotnet) - [Azure AI Projects Documentation](https://learn.microsoft.com/azure/ai-services/ai-project/overview) -- [Azure OpenAI Service](https://learn.microsoft.com/azure/ai-services/openai/) +- [Azure OpenAI Service](https://learn.microsoft.com/azure/ai-services/openai/) \ No newline at end of file diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Activities/ContentActivities.cs b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Activities/ContentActivities.cs index 0640214..a543a20 100644 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Activities/ContentActivities.cs +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Activities/ContentActivities.cs @@ -1,4 +1,4 @@ -using AgentChainingSample.Shared.Models; +using AgentChainingSample.Worker.Models; using AgentChainingSample.Services; using Microsoft.DurableTask; using Microsoft.Extensions.Configuration; @@ -31,6 +31,11 @@ public class ArticleResult /// The URL to the article in blob storage (kept for compatibility, always empty) /// public string BlobUrl { get; set; } = string.Empty; + + /// + /// The URL endpoint to view the article online + /// + public string ArticleEndpoint { get; set; } = string.Empty; } /// @@ -132,12 +137,12 @@ public override async Task> RunAsync(TaskActivityContext co /// [DurableTask] public class AssembleFinalArticleActivity(ILogger logger, IConfiguration configuration) - : TaskActivity<(string ArticleContent, List Images), ArticleResult> + : TaskActivity<(string ArticleContent, List Images, string InstanceId), ArticleResult> { // Use system temp directory by default, or the configured directory if specified private readonly string _outputDirectory = configuration["OutputDirectory"] ?? Path.GetTempPath(); - public override async Task RunAsync(TaskActivityContext context, (string ArticleContent, List Images) input) + public override async Task RunAsync(TaskActivityContext context, (string ArticleContent, List Images, string InstanceId) input) { logger.LogInformation("Assembling final article with images in HTML format"); @@ -268,6 +273,9 @@ public override async Task RunAsync(TaskActivityContext context, logger.LogInformation("HTML article saved to file: {FilePath}", localFilePath); + // Construct the article endpoint URL + string articleEndpoint = ConstructArticleEndpoint(input.InstanceId); + logger.LogInformation( "Successfully assembled final article in HTML format of {Length} characters with {ImageCount} images", finalHtml.Length, input.Images.Count); @@ -276,7 +284,8 @@ public override async Task RunAsync(TaskActivityContext context, { HtmlContent = finalHtml, FilePath = localFilePath, - BlobUrl = string.Empty // No blob URL since we're not uploading + BlobUrl = string.Empty, // No blob URL since we're not uploading + ArticleEndpoint = articleEndpoint }; } catch (Exception ex) @@ -286,6 +295,23 @@ public override async Task RunAsync(TaskActivityContext context, } } + /// + /// Constructs the article endpoint URL dynamically for both local and container app environments + /// + private string ConstructArticleEndpoint(string instanceId) + { + // Try to get base URL from environment variables (for container apps) + string? baseUrl = Environment.GetEnvironmentVariable("CLIENT_BASE_URL"); + + // Fallback to local development URL + if (string.IsNullOrEmpty(baseUrl)) + { + baseUrl = "http://localhost:5000"; + } + + return $"{baseUrl}/api/content/{instanceId}/document"; + } + /// /// Extracts paragraphs from HTML content /// @@ -333,4 +359,4 @@ private string SanitizeForFileName(string input) return result; } -} +} \ No newline at end of file diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Orchestrations/ContentCreationOrchestration.cs b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Orchestrations/ContentCreationOrchestration.cs index 6e133c1..f584ead 100644 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Orchestrations/ContentCreationOrchestration.cs +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Orchestrations/ContentCreationOrchestration.cs @@ -1,5 +1,5 @@ using AgentChainingSample.Activities; -using AgentChainingSample.Shared.Models; +using AgentChainingSample.Worker.Models; using Microsoft.DurableTask; using Microsoft.Extensions.Logging; @@ -42,10 +42,11 @@ public override async Task RunAsync(TaskOrchestrationCont // 4. Assemble the final article with content and images and save to file in the project's tmp directory var articleResult = await context.CallActivityAsync( nameof(AssembleFinalArticleActivity), - (articleContent, generatedImages)); + (articleContent, generatedImages, context.InstanceId)); logger.LogInformation("Final article assembled. Length: {Length} characters", articleResult.HtmlContent.Length); logger.LogInformation("Article saved to file: {FilePath}", articleResult.FilePath); + logger.LogInformation("Article endpoint: {Endpoint}", articleResult.ArticleEndpoint); // 5. Return the complete workflow result return new ContentWorkflowResult @@ -57,7 +58,8 @@ public override async Task RunAsync(TaskOrchestrationCont FinalArticle = articleResult.HtmlContent, ArticleFilePath = articleResult.FilePath, ArticleBlobUrl = articleResult.BlobUrl, + ArticleEndpoint = articleResult.ArticleEndpoint, CompletedTimestamp = DateTime.UtcNow }; } -} +} \ No newline at end of file diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Program.cs b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Program.cs index 787dac5..1ed9c5d 100644 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Program.cs +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Program.cs @@ -9,10 +9,11 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using AgentChainingSample.Services; +// Force rebuild timestamp: 2025-01-06 10:26 using AgentChainingSample.Activities; using AgentChainingSample.Orchestrations; -using AgentChainingSample.Shared.Models; +using AgentChainingSample.Services; +using AgentChainingSample.Worker.Models; // Configure the host builder HostApplicationBuilder builder = Host.CreateApplicationBuilder(); @@ -44,9 +45,26 @@ // No need to manually register them here // Get connection string from configuration with fallback to default local emulator connection -string connectionString = builder.Configuration["DTS_CONNECTION_STRING"] ?? +string connectionString = builder.Configuration["ENDPOINT"] ?? + builder.Configuration["DTS_CONNECTION_STRING"] ?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"; +// If we have the endpoint but not a full connection string, construct it +if (connectionString.StartsWith("Endpoint=") && !connectionString.Contains("TaskHub=")) +{ + string taskHub = builder.Configuration["TASKHUB"] ?? builder.Configuration["TASKHUB_NAME"] ?? "default"; + string clientId = builder.Configuration["AZURE_MANAGED_IDENTITY_CLIENT_ID"] ?? ""; + + if (!string.IsNullOrEmpty(clientId)) + { + connectionString = $"{connectionString};Authentication=ManagedIdentity;ClientID={clientId};TaskHub={taskHub}"; + } + else + { + connectionString = $"{connectionString};TaskHub={taskHub}"; + } +} + // Configure services // Register tasks with DI builder.Services.AddDurableTaskWorker(builder => @@ -64,8 +82,23 @@ // Get a proper logger from the service provider var logger = host.Services.GetRequiredService>(); -logger.LogInformation("Connection string: {ConnectionString}", connectionString); +// Log the constructed connection string with sensitive info redacted +string managedIdentityClientId = builder.Configuration["AZURE_MANAGED_IDENTITY_CLIENT_ID"] ?? ""; +string logConnectionString = !string.IsNullOrEmpty(managedIdentityClientId) ? + connectionString.Replace(managedIdentityClientId, "[REDACTED]") : + connectionString; +logger.LogInformation("Connection string: {ConnectionString}", logConnectionString); +logger.LogInformation("TaskHub: {TaskHub}", builder.Configuration["TASKHUB"] ?? builder.Configuration["TASKHUB_NAME"] ?? "default"); logger.LogInformation("This worker implements a news article generator workflow with multiple specialized agents"); + +// Log OpenAI configuration +logger.LogInformation("Azure OpenAI Endpoint: {Endpoint}", builder.Configuration["AZURE_OPENAI_ENDPOINT"] ?? "Not set"); +logger.LogInformation("Azure OpenAI Deployment: {Deployment}", builder.Configuration["OPENAI_DEPLOYMENT_NAME"] ?? "gpt-4 (default)"); +logger.LogInformation("DALL-E Endpoint: {DalleEndpoint}", !string.IsNullOrEmpty(builder.Configuration["DALLE_ENDPOINT"]) ? + "Configured" : "Not set - will use placeholder images"); +logger.LogInformation("Agent Connection String: {AgentConnectionString}", !string.IsNullOrEmpty(builder.Configuration["AGENT_CONNECTION_STRING"]) ? + "Configured" : "Not set - required for agent functionality"); + logger.LogInformation("Starting Agent Chaining Sample Worker"); // Start the host @@ -87,4 +120,4 @@ } // Stop the host -await host.StopAsync(); +await host.StopAsync(); \ No newline at end of file diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/AgentServices.cs b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/AgentServices.cs new file mode 100644 index 0000000..301ae97 --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/AgentServices.cs @@ -0,0 +1,428 @@ +using Microsoft.Extensions.Logging; +using AgentChainingSample.Shared.Models; +using Azure.Identity; +using Azure.Core; + +namespace AgentChainingSample.Services; + +/// +/// Research agent service for news article research +/// +public class ResearchAgentService : BaseAgentService +{ + private const string AgentName = "ResearchAgent"; + private const string EndpointEnvVar = "AGENT_CONNECTION_STRING"; + private readonly string _systemPrompt = @"You are an expert research agent specializing in news topics. +Your task is to analyze topics, identify key facts, and organize information in a journalistic format. +Focus on accuracy, thoroughness, and objectivity when researching any topic."; + + private bool _initialized = false; + + public ResearchAgentService(ILogger logger) + : base(Environment.GetEnvironmentVariable(EndpointEnvVar) ?? + throw new InvalidOperationException($"Environment variable '{EndpointEnvVar}' not set"), + logger) + { + } + + /// + /// Initializes the agent if needed + /// + private async Task InitializeAsync() + { + if (!_initialized) + { + await EnsureAgentExistsAsync(AgentName, _systemPrompt); + _initialized = true; + } + } + + /// + /// Gets the research agent to gather information about a topic using web search + /// + /// The news topic to research + /// Research data in JSON format + public async Task ResearchTopicAsync(string topic) + { + await InitializeAsync(); + + string prompt = $@"Research the following news topic: '{topic}'. + +Imagine you are a professional researcher for a news organization. Your task is to gather and organize +comprehensive information about this topic that would be useful for writing a news article. + +Follow these steps: +1. Consider what background information would be helpful +2. Think about latest developments or news on this topic +3. Consider different perspectives or viewpoints +4. Identify key facts, statistics, or potential quotes +5. Consider expert opinions or analyses that might be available + +Organize your findings into: +- Key facts and details about the topic +- Potential news sources that would cover this topic +- A concise summary of what's important about this topic +- Possible angles for a news article + +Respond in JSON format with the following structure: +{{ + ""facts"": [""fact1"", ""fact2"", ""fact3"", ...], + ""sources"": [ + {{ + ""url"": ""https://example-source.com/article"", + ""title"": ""Example Article Title"", + ""description"": ""Brief description of what this source might contain"" + }}, + ... + ], + ""summary"": ""A concise summary of key findings about this topic"", + ""articleAngles"": [""angle1"", ""angle2"", ...] +}}"; + + Logger.LogInformation($"Requesting research for topic: {topic}"); + return await GetResponseAsync(prompt); + } +} + +/// +/// Content generation agent service for news articles +/// +public class ContentGenerationAgentService : BaseAgentService +{ + private const string AgentName = "ContentGenerationAgent"; + private const string EndpointEnvVar = "AGENT_CONNECTION_STRING"; + private readonly string _systemPrompt = @"You are an expert news article writer with knowledge of journalistic standards. +Write professional articles with compelling headlines, strong leads, and proper sourcing. +Follow AP style guidelines, inverted pyramid structure, and maintain journalistic integrity."; + + private bool _initialized = false; + + public ContentGenerationAgentService(ILogger logger) + : base(Environment.GetEnvironmentVariable(EndpointEnvVar) ?? + throw new InvalidOperationException($"Environment variable '{EndpointEnvVar}' not set"), + logger) + { + } + + /// + /// Initializes the agent if needed + /// + private async Task InitializeAsync() + { + if (!_initialized) + { + await EnsureAgentExistsAsync(AgentName, _systemPrompt); + _initialized = true; + } + } + + /// + /// Gets the content generation agent to create a news article from research data + /// + /// The news topic + /// Research data in JSON format + /// Complete news article + public async Task CreateArticleAsync(string topic, string researchJson) + { + await InitializeAsync(); + + string prompt = $@"Write a professional news article about '{topic}' using the following research data: + +{researchJson} + +Imagine you are a professional journalist writing for a respected news outlet. Apply your knowledge +of AP style guidelines and journalistic best practices when writing this article. + +Follow these guidelines: +1. Create a compelling headline that captures reader attention +2. Start with a strong lead paragraph that captures the 5 Ws (who, what, when, where, why) +3. Include key facts from the research in order of importance +4. Cite sources appropriately within the text using journalistic attribution standards +5. Organize the article with logical structure following inverted pyramid style +6. Write in a balanced, objective tone adhering to journalistic standards +7. Include quotes if available in the research with proper attribution +8. End with a conclusion that summarizes or provides perspective + +The article should be approximately 400-600 words in length. +Format the article with appropriate HTML tags (

,

, etc.) following journalistic standards."; + + Logger.LogInformation($"Requesting article creation for topic: {topic}"); + return await GetResponseAsync(prompt); + } +} + +///

+/// Image generation agent service for news articles with direct DALL-E integration +/// +public class ImageGenerationAgentService : BaseAgentService +{ + private const string AgentName = "ImageGenerationAgent"; + private const string EndpointEnvVar = "AGENT_CONNECTION_STRING"; + private readonly string _systemPrompt = @"You are an expert image specialist for news articles. +Create detailed descriptions of images that would complement news stories. +Focus on photorealistic, journalistically appropriate imagery and compositions. +Provide descriptive captions that enhance understanding of the article content."; + + private bool _initialized = false; + private readonly HttpClient? _httpClient; + private readonly string? _dallEEndpoint; + private readonly bool _dalleEnabled; + private readonly DefaultAzureCredential? _credential; + + public ImageGenerationAgentService(ILogger logger) + : base(Environment.GetEnvironmentVariable(EndpointEnvVar) ?? + throw new InvalidOperationException($"Environment variable '{EndpointEnvVar}' not set"), + logger) + { + // Check if DALL-E endpoint is set + string? dalleEndpoint = Environment.GetEnvironmentVariable("DALLE_ENDPOINT"); + + // Only enable DALL-E if endpoint is provided + _dalleEnabled = !string.IsNullOrEmpty(dalleEndpoint); + + if (_dalleEnabled) + { + Logger.LogInformation("DALL-E image generation is enabled with Microsoft Entra ID authentication"); + + // Use the DALL-E endpoint environment variable directly without modification + _dallEEndpoint = dalleEndpoint; + + // Create HTTP client + _httpClient = new HttpClient(); + + // Create DefaultAzureCredential for authentication + _credential = new DefaultAzureCredential(); + } + else + { + Logger.LogWarning("DALL-E image generation is disabled. Set DALLE_ENDPOINT and DALLE_API_KEY environment variables to enable it."); + _dallEEndpoint = null; + _httpClient = null; + } + } + + /// + /// Initializes the agent if needed + /// + private async Task InitializeAsync() + { + if (!_initialized) + { + await EnsureAgentExistsAsync(AgentName, _systemPrompt); + _initialized = true; + } + } + + /// + /// Gets image descriptions from the agent and then generates actual images using DALL-E + /// + /// The news topic + /// The article text + /// Generated image details in JSON format + public async Task GenerateImagesAsync(string topic, string articleText) + { + await InitializeAsync(); + + // Step 1: Get image descriptions from the agent + string prompt = $@"Create descriptions for two compelling images to accompany a news article about '{topic}'. +Here's the article text to help you understand the content: + +{articleText} + +For each image, create an extremely detailed prompt that would work well with DALL-E image generation. +Make the prompts detailed, specific, and visually descriptive to generate photorealistic journalistic images. + +For each image: + +1. First, identify key visual concepts from the article that would make compelling images: + - Main subject or focus of the article + - Key scene or setting described + - Visual representation of important concepts + +2. Then, create a detailed prompt for DALL-E generation: + - Be extremely specific about subject matter + - Include details about composition, perspective, lighting + - Specify photorealistic style appropriate for news + - Include details about setting, mood, and context + +3. Write a descriptive caption that would accompany the image in the article + +Respond in JSON format with the following structure: +[ + {{ + ""description"": ""Brief description of what this image represents"", + ""prompt"": ""Detailed DALL-E prompt to generate a photorealistic news image"", + ""caption"": ""Caption for this image as it would appear in the article"" + }}, + {{ + ""description"": ""Brief description of what this second image represents"", + ""prompt"": ""Detailed DALL-E prompt to generate a photorealistic news image"", + ""caption"": ""Caption for this second image as it would appear in the article"" + }} +]"; + + Logger.LogInformation("Requesting image descriptions from agent"); + string descriptionsJson = await GetResponseAsync(prompt); + string cleanDescriptionsJson = CleanJsonResponse(descriptionsJson); + + try + { + // Step 2: Parse the descriptions and generate actual images with DALL-E + var imageDescriptions = System.Text.Json.JsonSerializer.Deserialize>( + cleanDescriptionsJson, + new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + + if (imageDescriptions == null || !imageDescriptions.Any()) + { + Logger.LogWarning("No valid image descriptions received from agent"); + return "[]"; + } + + Logger.LogInformation($"Received {imageDescriptions.Count} image descriptions"); + + // Generate images for each description + var generatedImages = new List(); + + if (_dalleEnabled) + { + Logger.LogInformation("Generating images with DALL-E"); + + foreach (var description in imageDescriptions) + { + try + { + // Call DALL-E API to generate the image + var imageUrl = await GenerateImageWithDallE(description.Prompt); + + generatedImages.Add(new GeneratedImage + { + Description = description.Description, + Prompt = description.Prompt, + ImageUrl = imageUrl, + Caption = description.Caption + }); + + Logger.LogInformation($"Successfully generated image: {description.Description}"); + } + catch (Exception ex) + { + Logger.LogError(ex, $"Error generating image for prompt: {description.Prompt}"); + AddPlaceholderImage(generatedImages, description); + } + } + } + else + { + // DALL-E is not configured, use placeholders instead + Logger.LogWarning("DALL-E is not configured. Using placeholder images instead."); + + foreach (var description in imageDescriptions) + { + AddPlaceholderImage(generatedImages, description); + } + } + + // Return the generated images as JSON + return System.Text.Json.JsonSerializer.Serialize(generatedImages); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error processing image descriptions"); + return "[]"; + } + } + + /// + /// Adds a placeholder image to the list of generated images + /// + private void AddPlaceholderImage(List images, ImageDescription description) + { + images.Add(new GeneratedImage + { + Description = description.Description, + Prompt = description.Prompt, + ImageUrl = "https://via.placeholder.com/800x600?text=Image+Generation+Sample", + Caption = description.Caption + }); + + Logger.LogInformation($"Added placeholder image for: {description.Description}"); + } + + /// + /// Calls the DALL-E API to generate an image from a prompt using Microsoft Entra ID authentication + /// + /// The image generation prompt + /// URL of the generated image + private async Task GenerateImageWithDallE(string prompt) + { + if (!_dalleEnabled || _httpClient == null || _dallEEndpoint == null || _credential == null) + { + throw new InvalidOperationException("DALL-E is not enabled. Check environment variable DALLE_ENDPOINT."); + } + + // Create the request body for DALL-E API + var requestBody = new + { + prompt = prompt, + n = 1, + size = "1024x1024", + response_format = "url", + quality = "standard" + }; + + var content = new StringContent( + System.Text.Json.JsonSerializer.Serialize(requestBody), + System.Text.Encoding.UTF8, + "application/json"); + + Logger.LogInformation($"Calling DALL-E API with prompt: {prompt}"); + + // Get an access token from Azure using DefaultAzureCredential + var tokenRequestContext = new Azure.Core.TokenRequestContext( + scopes: new[] { "https://cognitiveservices.azure.com/.default" }); + var accessToken = await _credential.GetTokenAsync(tokenRequestContext); + + // Add the bearer token to the request header + _httpClient.DefaultRequestHeaders.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken.Token); + + // Make the API call + var response = await _httpClient.PostAsync(_dallEEndpoint, content); + + // Check if the request was successful + if (!response.IsSuccessStatusCode) + { + var errorContent = await response.Content.ReadAsStringAsync(); + Logger.LogError($"DALL-E API error: {response.StatusCode}, {errorContent}"); + throw new Exception($"DALL-E API returned status code {response.StatusCode}"); + } + + // Parse the response + var responseContent = await response.Content.ReadAsStringAsync(); + var responseJson = System.Text.Json.JsonDocument.Parse(responseContent); + + // Extract the image URL + var imageUrl = responseJson.RootElement + .GetProperty("data")[0] + .GetProperty("url") + .GetString(); + + if (string.IsNullOrEmpty(imageUrl)) + { + throw new Exception("No image URL found in DALL-E response"); + } + + Logger.LogInformation("Successfully generated image with DALL-E"); + return imageUrl; + } +} + +/// +/// Model for image description +/// +public class ImageDescription +{ + public string Description { get; set; } = string.Empty; + public string Prompt { get; set; } = string.Empty; + public string Caption { get; set; } = string.Empty; +} diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/IAgentService.cs b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/IAgentService.cs new file mode 100644 index 0000000..4de34ad --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/IAgentService.cs @@ -0,0 +1,436 @@ +using System.Text.Json; +using Azure; +using Azure.AI.Projects; +using Azure.AI.Agents.Persistent; +using Azure.Core; +using Azure.Identity; +using Microsoft.Extensions.Logging; +using System.Text; + +namespace AgentChainingSample.Services; + +/// +/// Interface for agent services +/// +public interface IAgentService +{ + /// + /// Gets the agent ID used by this service + /// + string AgentId { get; set; } + + /// + /// Gets the endpoint URI for this agent service + /// + string Endpoint { get; } + + /// + /// Ensures the agent exists, creating it if necessary + /// + Task EnsureAgentExistsAsync(string agentName, string systemPrompt); + + /// + /// Gets a response from the agent + /// + Task GetResponseAsync(string prompt); + + /// + /// Cleans JSON response from markdown formatting + /// + string CleanJsonResponse(string response); +} + +/// +/// Base implementation for agent services +/// +public abstract class BaseAgentService : IAgentService +{ + protected readonly JsonSerializerOptions JsonOptions; + protected readonly ILogger Logger; + protected readonly AIProjectClient ProjectClient; + protected PersistentAgentsClient AgentsClient; + protected readonly TokenCredential Credential; + + // Retry configuration + private const int MaxRetryAttempts = 3; + private const int InitialRetryDelayMs = 1000; // Start with a 1 second delay + + // Deployment name from the instructions + protected const string DeploymentName = "gpt-4o-mini"; + + public string AgentId { get; set; } + public string Endpoint { get; } + + protected BaseAgentService(string endpointUrl, ILogger logger) + { + AgentId = string.Empty; // Will be set during initialization + Endpoint = endpointUrl ?? throw new ArgumentNullException(nameof(endpointUrl)); + Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + // Create credential for authentication + Credential = new DefaultAzureCredential(); + + Logger.LogInformation($"Initializing Azure AI Projects client with endpoint: {Endpoint}"); + + // Create the AIProjectClient using the endpoint and credential + ProjectClient = new AIProjectClient(new Uri(Endpoint), Credential); + + // Get the PersistentAgentsClient for agent operations + AgentsClient = ProjectClient.GetPersistentAgentsClient(); + + Logger.LogInformation("Azure AI Projects client and Persistent Agents client initialized successfully"); + + JsonOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true, + PropertyNameCaseInsensitive = true, + AllowTrailingCommas = true + }; + } + + + + /// + /// Ensures the agent exists, creating it if necessary + /// + public async Task EnsureAgentExistsAsync(string agentName, string systemPrompt) + { + Logger.LogInformation($"Setting up agent: {agentName}"); + Logger.LogInformation($"Using Azure AI Projects endpoint: {Endpoint}"); + + try + { + // Check if an agent with this name already exists + PersistentAgent? existingAgent = null; + string existingAgentId = string.Empty; + + try + { + Logger.LogInformation($"Attempting to retrieve agents from Azure AI Projects endpoint: {Endpoint}"); + var agents = AgentsClient.Administration.GetAgents(); + + // Try to find an existing agent with the same name + foreach (var agent in agents) + { + if (agent.Name == agentName) + { + existingAgent = agent; + existingAgentId = agent.Id; + Logger.LogInformation($"Found existing agent: {agentName} with ID: {existingAgentId}"); + break; + } + } + } + catch (RequestFailedException rfEx) + { + Logger.LogWarning($"Error getting agents: {rfEx.Message}. Status: {rfEx.Status}"); + throw; // Re-throw the exception to surface it + } + + string agentId; + + if (existingAgent == null) + { + try + { + Logger.LogInformation($"Creating new agent: {agentName}"); + Logger.LogInformation($"Using Azure AI Projects endpoint: {Endpoint}"); + Logger.LogInformation($"Using model deployment: {DeploymentName}"); + + // Create a new agent + var agentResponse = await AgentsClient.Administration.CreateAgentAsync( + DeploymentName, + agentName, + systemPrompt, + $"News Article Generator agent created on {DateTime.UtcNow:yyyy-MM-dd}"); + + agentId = agentResponse.Value.Id; + + Logger.LogInformation($"Created new agent: {agentName} with ID: {agentId}"); + } + catch (Exception ex) + { + Logger.LogWarning($"Error creating agent: {ex.Message}"); + throw; // Re-throw the exception to surface it + } + } + else + { + agentId = existingAgentId; + + try + { + // Update the agent's system prompt if it exists + var updatedAgent = await AgentsClient.Administration.UpdateAgentAsync( + agentId, + systemPrompt, + $"News Article Generator agent updated on {DateTime.UtcNow:yyyy-MM-dd}"); + Logger.LogInformation($"Updated existing agent: {agentName} with ID: {agentId}"); + } + catch (Exception ex) + { + Logger.LogWarning($"Error updating agent: {ex.Message}. Using existing agent ID anyway."); + } + } + + // Set the agent ID field + AgentId = agentId; + return agentId; + } + catch (Exception ex) + { + Logger.LogWarning($"Unexpected error in EnsureAgentExistsAsync: {ex.Message}"); + throw; // Re-throw the exception to surface it + } + } + + /// + /// Validates and normalizes JSON responses from agents + /// + /// The JSON response from an agent + /// Validated JSON string + public string CleanJsonResponse(string response) + { + if (string.IsNullOrEmpty(response)) + { + Logger.LogWarning("[JSON-PARSER] Response was null or empty"); + return "{}"; + } + + Logger.LogInformation($"[JSON-PARSER] Processing response ({response.Length} chars)"); + + // Trim any whitespace + response = response.Trim(); + + // Simple case: Check if response is already valid JSON + try + { + using (JsonDocument.Parse(response)) + { + Logger.LogInformation("[JSON-PARSER] Response is valid JSON"); + return response; + } + } + catch (JsonException) + { + Logger.LogInformation("[JSON-PARSER] Initial JSON validation failed, attempting to extract JSON"); + } + + // Handle markdown code blocks if present + if (response.Contains("```")) + { + // Find start and end of code block + int codeBlockStart = response.IndexOf("```"); + int codeBlockEnd = response.LastIndexOf("```"); + + if (codeBlockStart != codeBlockEnd) // Make sure we found both opening and closing markers + { + // Extract content between code blocks + int startIndex = response.IndexOf('\n', codeBlockStart) + 1; + int endIndex = codeBlockEnd; + + // Make sure we have valid start and end indices + if (startIndex > 0 && endIndex > startIndex) + { + string codeContent = response.Substring(startIndex, endIndex - startIndex).Trim(); + Logger.LogInformation("[JSON-PARSER] Extracted content from code block"); + + // Remove any language specifier like ```json + if (codeContent.StartsWith("json", StringComparison.OrdinalIgnoreCase)) + { + codeContent = codeContent.Substring(4).Trim(); + } + + response = codeContent; + } + } + } + + // Check if response is wrapped in backticks + if (response.StartsWith("`") && response.EndsWith("`")) + { + response = response.Substring(1, response.Length - 2).Trim(); + Logger.LogInformation("[JSON-PARSER] Removed backticks"); + } + + // Final validation + try + { + using (JsonDocument.Parse(response)) + { + Logger.LogInformation("[JSON-PARSER] Successfully validated JSON"); + return response; + } + } + catch (JsonException ex) + { + Logger.LogError($"[JSON-PARSER] Failed to parse JSON: {ex.Message}"); + return "{}"; // Return empty JSON object as fallback + } + } + + public async Task GetResponseAsync(string prompt) + { + + int retryCount = 0; + int retryDelay = InitialRetryDelayMs; + bool shouldRetry; + + do + { + shouldRetry = false; + + try + { + // Check if agent ID is set + if (string.IsNullOrEmpty(AgentId)) + { + throw new InvalidOperationException($"Agent ID is not set. Call EnsureAgentExistsAsync first."); + } + + Logger.LogInformation($"Getting response from agent {AgentId}"); + + // Create a thread + var threadResponse = await AgentsClient.Threads.CreateThreadAsync(); + string threadId = threadResponse.Value.Id; + Logger.LogInformation($"Created thread, thread ID: {threadId}"); + + // Create message content + var messageContent = new List + { + new MessageInputTextBlock(prompt) + }; + + // Send the prompt to the thread + var messageResponse = await AgentsClient.Messages.CreateMessageAsync( + threadId: threadId, + role: MessageRole.User, + content: prompt); + + var threadMessage = messageResponse.Value; + Logger.LogInformation($"Created message, message ID: {threadMessage.Id}"); + + // Create a run with the agent using the agent ID + var runResponse = await AgentsClient.Runs.CreateRunAsync(threadId, AgentId); + + var run = runResponse.Value; + Logger.LogInformation($"Created run, run ID: {run.Id}"); + + // Poll the run until it's completed + do + { + await Task.Delay(TimeSpan.FromMilliseconds(500)); + var getRunResponse = await AgentsClient.Runs.GetRunAsync(threadId, run.Id); + run = getRunResponse.Value; + } + while (run.Status == RunStatus.Queued + || run.Status == RunStatus.InProgress + || run.Status == RunStatus.RequiresAction); + + Logger.LogInformation($"Run completed with status: {run.Status}"); + + if (run.Status == RunStatus.Failed) + { + // Try to extract error message + string errorMessage = run.LastError?.Message ?? string.Empty; + + // Check if the error is due to rate limiting + if (errorMessage.Contains("Rate limit") && retryCount < MaxRetryAttempts) + { + shouldRetry = true; + retryDelay = await HandleRetry(++retryCount, retryDelay, errorMessage); + continue; + } + + throw new Exception($"Run failed: {errorMessage}"); + } + + // Get messages from the assistant thread + var messagesResponse = AgentsClient.Messages.GetMessagesAsync( + threadId: threadId, + order: ListSortOrder.Descending); // Get newest first + + List assistantMessages = new List(); + + await foreach (var message in messagesResponse) + { + if (message.Role == "assistant") + { + assistantMessages.Add(message); + } + } + + if (assistantMessages.Count == 0) + { + Logger.LogWarning("No assistant messages found in the response"); + return string.Empty; + } + + // Get the most recent message from the assistant (first in list with Descending order) + var latestMessage = assistantMessages.First(); + + // Extract all text content items from the message + StringBuilder contentBuilder = new StringBuilder(); + + foreach (var contentItem in latestMessage.ContentItems) + { + if (contentItem is MessageTextContent textContent) + { + contentBuilder.Append(textContent.Text); + } + } + + string responseContent = contentBuilder.ToString(); + + Logger.LogInformation($"Retrieved response from agent {AgentId}, content length: {responseContent.Length} characters"); + + return responseContent; + } + catch (RequestFailedException ex) when ( + (ex.Status == 429 || // 429 Too Many Requests + ex.Status == 503) && // 503 Service Unavailable + retryCount < MaxRetryAttempts) + { + shouldRetry = true; + retryDelay = await HandleRetry(++retryCount, retryDelay, ex.Message); + } + catch (Exception ex) + { + // Check if the exception message contains indication of rate limit + if (ex.Message.Contains("Rate limit") && retryCount < MaxRetryAttempts) + { + shouldRetry = true; + retryDelay = await HandleRetry(++retryCount, retryDelay, ex.Message); + } + else + { + Logger.LogError($"Error calling agent {AgentId}: {ex.Message}"); + throw; + } + } + } while (shouldRetry); + + // This should not be reached unless all retry attempts fail + throw new Exception($"Failed to get a response from agent {AgentId} after {MaxRetryAttempts} attempts"); + } + + private async Task HandleRetry(int retryCount, int retryDelay, string errorMessage) + { + // Calculate exponential backoff with jitter + int maxJitterMs = retryDelay / 4; + Random random = new Random(); + int jitter = random.Next(-maxJitterMs, maxJitterMs); + int actualDelay = retryDelay + jitter; + + Logger.LogInformation($"Rate limit hit for agent {AgentId}. Retrying in {actualDelay}ms (attempt {retryCount} of {MaxRetryAttempts}). Error: {errorMessage}"); + + // Wait for the calculated delay + await Task.Delay(actualDelay); + + // Double the delay for the next retry (exponential backoff) + return retryDelay * 2; + } + + +} diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Worker.csproj b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Worker.csproj index 345ca81..34a4832 100644 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Worker.csproj +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Worker.csproj @@ -1,4 +1,4 @@ - + @@ -8,13 +8,16 @@ enable AgentChainingSample.Worker AgentChainingSample.Worker + f186b044-82dd-4212-96df-113caf6ef3f9 + + false + agent-chaining-worker - @@ -29,8 +32,4 @@ $(BaseIntermediateOutputPath)Generated - - - - - + \ No newline at end of file From ddfc5955df6c61079546455127de2697dfea162c Mon Sep 17 00:00:00 2001 From: greenie-msft Date: Thu, 10 Jul 2025 15:40:56 -0700 Subject: [PATCH 2/7] remove hard coded deployment name --- .../PromptChaining/Worker/Services/AgentServices.cs | 9 +++++++-- .../PromptChaining/Worker/Services/IAgentService.cs | 10 +++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/AgentServices.cs b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/AgentServices.cs index 301ae97..f1816b2 100644 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/AgentServices.cs +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/AgentServices.cs @@ -182,9 +182,12 @@ public ImageGenerationAgentService(ILogger logger) if (_dalleEnabled) { - Logger.LogInformation("DALL-E image generation is enabled with Microsoft Entra ID authentication"); + Logger.LogInformation($"DALL-E image generation is enabled with Microsoft Entra ID authentication"); + Logger.LogInformation($"Using DALL-E endpoint: {dalleEndpoint}"); // Use the DALL-E endpoint environment variable directly without modification + // Note: The DALLE_ENDPOINT should be the complete URL including deployment name and API version + // Example format: https://your-resource.openai.azure.com/openai/deployments/your-deployment-name/images/generations?api-version=2023-12-01-preview _dallEEndpoint = dalleEndpoint; // Create HTTP client @@ -375,6 +378,7 @@ private async Task GenerateImageWithDallE(string prompt) System.Text.Encoding.UTF8, "application/json"); + Logger.LogInformation($"Using DALL-E endpoint directly: {_dallEEndpoint}"); Logger.LogInformation($"Calling DALL-E API with prompt: {prompt}"); // Get an access token from Azure using DefaultAzureCredential @@ -386,7 +390,8 @@ private async Task GenerateImageWithDallE(string prompt) _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken.Token); - // Make the API call + // Make the API call - use the full endpoint URL directly from the environment variable + // The DALLE_ENDPOINT should already contain the complete URL including deployment name and API version var response = await _httpClient.PostAsync(_dallEEndpoint, content); // Check if the request was successful diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/IAgentService.cs b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/IAgentService.cs index 4de34ad..71cabc7 100644 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/IAgentService.cs +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/IAgentService.cs @@ -55,8 +55,8 @@ public abstract class BaseAgentService : IAgentService private const int MaxRetryAttempts = 3; private const int InitialRetryDelayMs = 1000; // Start with a 1 second delay - // Deployment name from the instructions - protected const string DeploymentName = "gpt-4o-mini"; + // Deployment name from environment variable or fallback to default + protected readonly string DeploymentName; public string AgentId { get; set; } public string Endpoint { get; } @@ -67,6 +67,10 @@ protected BaseAgentService(string endpointUrl, ILogger logger) Endpoint = endpointUrl ?? throw new ArgumentNullException(nameof(endpointUrl)); Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + // Get deployment name from environment variable or use fallback + DeploymentName = Environment.GetEnvironmentVariable("OPENAI_DEPLOYMENT_NAME") ?? "gpt-4"; + Logger.LogInformation($"Using OpenAI deployment: {DeploymentName}"); + // Create credential for authentication Credential = new DefaultAzureCredential(); @@ -136,7 +140,7 @@ public async Task EnsureAgentExistsAsync(string agentName, string system { Logger.LogInformation($"Creating new agent: {agentName}"); Logger.LogInformation($"Using Azure AI Projects endpoint: {Endpoint}"); - Logger.LogInformation($"Using model deployment: {DeploymentName}"); + Logger.LogInformation($"Using model deployment from env var OPENAI_DEPLOYMENT_NAME: {DeploymentName}"); // Create a new agent var agentResponse = await AgentsClient.Administration.CreateAgentAsync( From 75003c615412a12e4d599a2effe545adf971ce17 Mon Sep 17 00:00:00 2001 From: greenie-msft Date: Tue, 15 Jul 2025 13:53:31 -0700 Subject: [PATCH 3/7] Address PR feedback --- .../PromptChaining/AgentChainingSample.csproj | 31 -- .../PromptChaining/AgentChainingSample.sln | 33 -- .../PromptChaining/AgentChainingSample.slnx | 2 +- .../ContentCreationOrchestration.cs | 7 + .../Worker/Services/AgentServices.cs | 434 +----------------- .../Services/ContentGenerationAgentService.cs | 2 +- .../Worker/Services/IAgentService.cs | 11 +- .../Services/ImageGenerationAgentService.cs | 3 +- .../Worker/Services/ResearchAgentService.cs | 2 +- 9 files changed, 24 insertions(+), 501 deletions(-) delete mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/AgentChainingSample.csproj delete mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/AgentChainingSample.sln diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/AgentChainingSample.csproj b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/AgentChainingSample.csproj deleted file mode 100644 index 0f41321..0000000 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/AgentChainingSample.csproj +++ /dev/null @@ -1,31 +0,0 @@ - - - - Exe - net8.0 - enable - enable - - true - 1.0.0 - AgentChainingSample - Sample demonstrating agent prompt chaining with Durable Task - Copyright © 2025 - Microsoft - AgentChainingSample - - - - - - - - - - - - - - - - diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/AgentChainingSample.sln b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/AgentChainingSample.sln deleted file mode 100644 index 888097e..0000000 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/AgentChainingSample.sln +++ /dev/null @@ -1,33 +0,0 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client", "Client\Client.csproj", "{62F7AE2C-3F09-46F7-BF2E-837A32AB4F5A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Worker", "Worker\Worker.csproj", "{5E211685-5D13-499A-9226-C319D9C3D057}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared", "Shared\Shared.csproj", "{FD60FF3D-89E1-4294-9F2D-F9F2DEF36EEB}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {62F7AE2C-3F09-46F7-BF2E-837A32AB4F5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {62F7AE2C-3F09-46F7-BF2E-837A32AB4F5A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {62F7AE2C-3F09-46F7-BF2E-837A32AB4F5A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {62F7AE2C-3F09-46F7-BF2E-837A32AB4F5A}.Release|Any CPU.Build.0 = Release|Any CPU - {5E211685-5D13-499A-9226-C319D9C3D057}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5E211685-5D13-499A-9226-C319D9C3D057}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5E211685-5D13-499A-9226-C319D9C3D057}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5E211685-5D13-499A-9226-C319D9C3D057}.Release|Any CPU.Build.0 = Release|Any CPU - {FD60FF3D-89E1-4294-9F2D-F9F2DEF36EEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FD60FF3D-89E1-4294-9F2D-F9F2DEF36EEB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FD60FF3D-89E1-4294-9F2D-F9F2DEF36EEB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FD60FF3D-89E1-4294-9F2D-F9F2DEF36EEB}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/AgentChainingSample.slnx b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/AgentChainingSample.slnx index 675298e..9eec6e2 100644 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/AgentChainingSample.slnx +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/AgentChainingSample.slnx @@ -2,4 +2,4 @@ - + \ No newline at end of file diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Orchestrations/ContentCreationOrchestration.cs b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Orchestrations/ContentCreationOrchestration.cs index f584ead..7349f4c 100644 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Orchestrations/ContentCreationOrchestration.cs +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Orchestrations/ContentCreationOrchestration.cs @@ -42,11 +42,18 @@ public override async Task RunAsync(TaskOrchestrationCont // 4. Assemble the final article with content and images and save to file in the project's tmp directory var articleResult = await context.CallActivityAsync( nameof(AssembleFinalArticleActivity), +<<<<<<< HEAD (articleContent, generatedImages, context.InstanceId)); logger.LogInformation("Final article assembled. Length: {Length} characters", articleResult.HtmlContent.Length); logger.LogInformation("Article saved to file: {FilePath}", articleResult.FilePath); logger.LogInformation("Article endpoint: {Endpoint}", articleResult.ArticleEndpoint); +======= + (articleContent, generatedImages)); + + logger.LogInformation("Final article assembled. Length: {Length} characters", articleResult.HtmlContent.Length); + logger.LogInformation("Article saved to file: {FilePath}", articleResult.FilePath); +>>>>>>> 21471c1 (Address PR feedback) // 5. Return the complete workflow result return new ContentWorkflowResult diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/AgentServices.cs b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/AgentServices.cs index f1816b2..a34c27f 100644 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/AgentServices.cs +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/AgentServices.cs @@ -1,433 +1,9 @@ using Microsoft.Extensions.Logging; -using AgentChainingSample.Shared.Models; -using Azure.Identity; -using Azure.Core; namespace AgentChainingSample.Services; -/// -/// Research agent service for news article research -/// -public class ResearchAgentService : BaseAgentService -{ - private const string AgentName = "ResearchAgent"; - private const string EndpointEnvVar = "AGENT_CONNECTION_STRING"; - private readonly string _systemPrompt = @"You are an expert research agent specializing in news topics. -Your task is to analyze topics, identify key facts, and organize information in a journalistic format. -Focus on accuracy, thoroughness, and objectivity when researching any topic."; - - private bool _initialized = false; - - public ResearchAgentService(ILogger logger) - : base(Environment.GetEnvironmentVariable(EndpointEnvVar) ?? - throw new InvalidOperationException($"Environment variable '{EndpointEnvVar}' not set"), - logger) - { - } - - /// - /// Initializes the agent if needed - /// - private async Task InitializeAsync() - { - if (!_initialized) - { - await EnsureAgentExistsAsync(AgentName, _systemPrompt); - _initialized = true; - } - } - - /// - /// Gets the research agent to gather information about a topic using web search - /// - /// The news topic to research - /// Research data in JSON format - public async Task ResearchTopicAsync(string topic) - { - await InitializeAsync(); - - string prompt = $@"Research the following news topic: '{topic}'. - -Imagine you are a professional researcher for a news organization. Your task is to gather and organize -comprehensive information about this topic that would be useful for writing a news article. - -Follow these steps: -1. Consider what background information would be helpful -2. Think about latest developments or news on this topic -3. Consider different perspectives or viewpoints -4. Identify key facts, statistics, or potential quotes -5. Consider expert opinions or analyses that might be available - -Organize your findings into: -- Key facts and details about the topic -- Potential news sources that would cover this topic -- A concise summary of what's important about this topic -- Possible angles for a news article - -Respond in JSON format with the following structure: -{{ - ""facts"": [""fact1"", ""fact2"", ""fact3"", ...], - ""sources"": [ - {{ - ""url"": ""https://example-source.com/article"", - ""title"": ""Example Article Title"", - ""description"": ""Brief description of what this source might contain"" - }}, - ... - ], - ""summary"": ""A concise summary of key findings about this topic"", - ""articleAngles"": [""angle1"", ""angle2"", ...] -}}"; - - Logger.LogInformation($"Requesting research for topic: {topic}"); - return await GetResponseAsync(prompt); - } -} - -/// -/// Content generation agent service for news articles -/// -public class ContentGenerationAgentService : BaseAgentService -{ - private const string AgentName = "ContentGenerationAgent"; - private const string EndpointEnvVar = "AGENT_CONNECTION_STRING"; - private readonly string _systemPrompt = @"You are an expert news article writer with knowledge of journalistic standards. -Write professional articles with compelling headlines, strong leads, and proper sourcing. -Follow AP style guidelines, inverted pyramid structure, and maintain journalistic integrity."; - - private bool _initialized = false; - - public ContentGenerationAgentService(ILogger logger) - : base(Environment.GetEnvironmentVariable(EndpointEnvVar) ?? - throw new InvalidOperationException($"Environment variable '{EndpointEnvVar}' not set"), - logger) - { - } - - /// - /// Initializes the agent if needed - /// - private async Task InitializeAsync() - { - if (!_initialized) - { - await EnsureAgentExistsAsync(AgentName, _systemPrompt); - _initialized = true; - } - } - - /// - /// Gets the content generation agent to create a news article from research data - /// - /// The news topic - /// Research data in JSON format - /// Complete news article - public async Task CreateArticleAsync(string topic, string researchJson) - { - await InitializeAsync(); - - string prompt = $@"Write a professional news article about '{topic}' using the following research data: - -{researchJson} - -Imagine you are a professional journalist writing for a respected news outlet. Apply your knowledge -of AP style guidelines and journalistic best practices when writing this article. - -Follow these guidelines: -1. Create a compelling headline that captures reader attention -2. Start with a strong lead paragraph that captures the 5 Ws (who, what, when, where, why) -3. Include key facts from the research in order of importance -4. Cite sources appropriately within the text using journalistic attribution standards -5. Organize the article with logical structure following inverted pyramid style -6. Write in a balanced, objective tone adhering to journalistic standards -7. Include quotes if available in the research with proper attribution -8. End with a conclusion that summarizes or provides perspective - -The article should be approximately 400-600 words in length. -Format the article with appropriate HTML tags (

,

, etc.) following journalistic standards."; - - Logger.LogInformation($"Requesting article creation for topic: {topic}"); - return await GetResponseAsync(prompt); - } -} - -///

-/// Image generation agent service for news articles with direct DALL-E integration -/// -public class ImageGenerationAgentService : BaseAgentService -{ - private const string AgentName = "ImageGenerationAgent"; - private const string EndpointEnvVar = "AGENT_CONNECTION_STRING"; - private readonly string _systemPrompt = @"You are an expert image specialist for news articles. -Create detailed descriptions of images that would complement news stories. -Focus on photorealistic, journalistically appropriate imagery and compositions. -Provide descriptive captions that enhance understanding of the article content."; - - private bool _initialized = false; - private readonly HttpClient? _httpClient; - private readonly string? _dallEEndpoint; - private readonly bool _dalleEnabled; - private readonly DefaultAzureCredential? _credential; - - public ImageGenerationAgentService(ILogger logger) - : base(Environment.GetEnvironmentVariable(EndpointEnvVar) ?? - throw new InvalidOperationException($"Environment variable '{EndpointEnvVar}' not set"), - logger) - { - // Check if DALL-E endpoint is set - string? dalleEndpoint = Environment.GetEnvironmentVariable("DALLE_ENDPOINT"); - - // Only enable DALL-E if endpoint is provided - _dalleEnabled = !string.IsNullOrEmpty(dalleEndpoint); - - if (_dalleEnabled) - { - Logger.LogInformation($"DALL-E image generation is enabled with Microsoft Entra ID authentication"); - Logger.LogInformation($"Using DALL-E endpoint: {dalleEndpoint}"); - - // Use the DALL-E endpoint environment variable directly without modification - // Note: The DALLE_ENDPOINT should be the complete URL including deployment name and API version - // Example format: https://your-resource.openai.azure.com/openai/deployments/your-deployment-name/images/generations?api-version=2023-12-01-preview - _dallEEndpoint = dalleEndpoint; - - // Create HTTP client - _httpClient = new HttpClient(); - - // Create DefaultAzureCredential for authentication - _credential = new DefaultAzureCredential(); - } - else - { - Logger.LogWarning("DALL-E image generation is disabled. Set DALLE_ENDPOINT and DALLE_API_KEY environment variables to enable it."); - _dallEEndpoint = null; - _httpClient = null; - } - } - - /// - /// Initializes the agent if needed - /// - private async Task InitializeAsync() - { - if (!_initialized) - { - await EnsureAgentExistsAsync(AgentName, _systemPrompt); - _initialized = true; - } - } - - /// - /// Gets image descriptions from the agent and then generates actual images using DALL-E - /// - /// The news topic - /// The article text - /// Generated image details in JSON format - public async Task GenerateImagesAsync(string topic, string articleText) - { - await InitializeAsync(); - - // Step 1: Get image descriptions from the agent - string prompt = $@"Create descriptions for two compelling images to accompany a news article about '{topic}'. -Here's the article text to help you understand the content: - -{articleText} - -For each image, create an extremely detailed prompt that would work well with DALL-E image generation. -Make the prompts detailed, specific, and visually descriptive to generate photorealistic journalistic images. - -For each image: - -1. First, identify key visual concepts from the article that would make compelling images: - - Main subject or focus of the article - - Key scene or setting described - - Visual representation of important concepts - -2. Then, create a detailed prompt for DALL-E generation: - - Be extremely specific about subject matter - - Include details about composition, perspective, lighting - - Specify photorealistic style appropriate for news - - Include details about setting, mood, and context - -3. Write a descriptive caption that would accompany the image in the article - -Respond in JSON format with the following structure: -[ - {{ - ""description"": ""Brief description of what this image represents"", - ""prompt"": ""Detailed DALL-E prompt to generate a photorealistic news image"", - ""caption"": ""Caption for this image as it would appear in the article"" - }}, - {{ - ""description"": ""Brief description of what this second image represents"", - ""prompt"": ""Detailed DALL-E prompt to generate a photorealistic news image"", - ""caption"": ""Caption for this second image as it would appear in the article"" - }} -]"; - - Logger.LogInformation("Requesting image descriptions from agent"); - string descriptionsJson = await GetResponseAsync(prompt); - string cleanDescriptionsJson = CleanJsonResponse(descriptionsJson); - - try - { - // Step 2: Parse the descriptions and generate actual images with DALL-E - var imageDescriptions = System.Text.Json.JsonSerializer.Deserialize>( - cleanDescriptionsJson, - new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - - if (imageDescriptions == null || !imageDescriptions.Any()) - { - Logger.LogWarning("No valid image descriptions received from agent"); - return "[]"; - } - - Logger.LogInformation($"Received {imageDescriptions.Count} image descriptions"); - - // Generate images for each description - var generatedImages = new List(); - - if (_dalleEnabled) - { - Logger.LogInformation("Generating images with DALL-E"); - - foreach (var description in imageDescriptions) - { - try - { - // Call DALL-E API to generate the image - var imageUrl = await GenerateImageWithDallE(description.Prompt); - - generatedImages.Add(new GeneratedImage - { - Description = description.Description, - Prompt = description.Prompt, - ImageUrl = imageUrl, - Caption = description.Caption - }); - - Logger.LogInformation($"Successfully generated image: {description.Description}"); - } - catch (Exception ex) - { - Logger.LogError(ex, $"Error generating image for prompt: {description.Prompt}"); - AddPlaceholderImage(generatedImages, description); - } - } - } - else - { - // DALL-E is not configured, use placeholders instead - Logger.LogWarning("DALL-E is not configured. Using placeholder images instead."); - - foreach (var description in imageDescriptions) - { - AddPlaceholderImage(generatedImages, description); - } - } - - // Return the generated images as JSON - return System.Text.Json.JsonSerializer.Serialize(generatedImages); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error processing image descriptions"); - return "[]"; - } - } - - /// - /// Adds a placeholder image to the list of generated images - /// - private void AddPlaceholderImage(List images, ImageDescription description) - { - images.Add(new GeneratedImage - { - Description = description.Description, - Prompt = description.Prompt, - ImageUrl = "https://via.placeholder.com/800x600?text=Image+Generation+Sample", - Caption = description.Caption - }); - - Logger.LogInformation($"Added placeholder image for: {description.Description}"); - } - - /// - /// Calls the DALL-E API to generate an image from a prompt using Microsoft Entra ID authentication - /// - /// The image generation prompt - /// URL of the generated image - private async Task GenerateImageWithDallE(string prompt) - { - if (!_dalleEnabled || _httpClient == null || _dallEEndpoint == null || _credential == null) - { - throw new InvalidOperationException("DALL-E is not enabled. Check environment variable DALLE_ENDPOINT."); - } - - // Create the request body for DALL-E API - var requestBody = new - { - prompt = prompt, - n = 1, - size = "1024x1024", - response_format = "url", - quality = "standard" - }; - - var content = new StringContent( - System.Text.Json.JsonSerializer.Serialize(requestBody), - System.Text.Encoding.UTF8, - "application/json"); - - Logger.LogInformation($"Using DALL-E endpoint directly: {_dallEEndpoint}"); - Logger.LogInformation($"Calling DALL-E API with prompt: {prompt}"); - - // Get an access token from Azure using DefaultAzureCredential - var tokenRequestContext = new Azure.Core.TokenRequestContext( - scopes: new[] { "https://cognitiveservices.azure.com/.default" }); - var accessToken = await _credential.GetTokenAsync(tokenRequestContext); - - // Add the bearer token to the request header - _httpClient.DefaultRequestHeaders.Authorization = - new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken.Token); - - // Make the API call - use the full endpoint URL directly from the environment variable - // The DALLE_ENDPOINT should already contain the complete URL including deployment name and API version - var response = await _httpClient.PostAsync(_dallEEndpoint, content); - - // Check if the request was successful - if (!response.IsSuccessStatusCode) - { - var errorContent = await response.Content.ReadAsStringAsync(); - Logger.LogError($"DALL-E API error: {response.StatusCode}, {errorContent}"); - throw new Exception($"DALL-E API returned status code {response.StatusCode}"); - } - - // Parse the response - var responseContent = await response.Content.ReadAsStringAsync(); - var responseJson = System.Text.Json.JsonDocument.Parse(responseContent); - - // Extract the image URL - var imageUrl = responseJson.RootElement - .GetProperty("data")[0] - .GetProperty("url") - .GetString(); - - if (string.IsNullOrEmpty(imageUrl)) - { - throw new Exception("No image URL found in DALL-E response"); - } - - Logger.LogInformation("Successfully generated image with DALL-E"); - return imageUrl; - } -} - -/// -/// Model for image description -/// -public class ImageDescription -{ - public string Description { get; set; } = string.Empty; - public string Prompt { get; set; } = string.Empty; - public string Caption { get; set; } = string.Empty; -} +// Note: Implementation classes have been moved to separate files: +// - ResearchAgentService.cs +// - ContentGenerationAgentService.cs +// - ImageGenerationAgentService.cs +// - Models/DalleModels.cs diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/ContentGenerationAgentService.cs b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/ContentGenerationAgentService.cs index b03f914..6ebf723 100644 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/ContentGenerationAgentService.cs +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/ContentGenerationAgentService.cs @@ -55,4 +55,4 @@ The article should be approximately 400-600 words in length. Logger.LogInformation($"Requesting article creation for topic: {topic}"); return await GetResponseAsync(prompt); } -} +} \ No newline at end of file diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/IAgentService.cs b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/IAgentService.cs index 71cabc7..b1a8e56 100644 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/IAgentService.cs +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/IAgentService.cs @@ -4,6 +4,7 @@ using Azure.AI.Agents.Persistent; using Azure.Core; using Azure.Identity; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using System.Text; @@ -50,25 +51,27 @@ public abstract class BaseAgentService : IAgentService protected readonly AIProjectClient ProjectClient; protected PersistentAgentsClient AgentsClient; protected readonly TokenCredential Credential; + protected readonly IConfiguration Configuration; // Retry configuration private const int MaxRetryAttempts = 3; private const int InitialRetryDelayMs = 1000; // Start with a 1 second delay - // Deployment name from environment variable or fallback to default + // Deployment name from configuration or fallback to default protected readonly string DeploymentName; public string AgentId { get; set; } public string Endpoint { get; } - protected BaseAgentService(string endpointUrl, ILogger logger) + protected BaseAgentService(string endpointUrl, ILogger logger, IConfiguration configuration) { AgentId = string.Empty; // Will be set during initialization Endpoint = endpointUrl ?? throw new ArgumentNullException(nameof(endpointUrl)); Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); - // Get deployment name from environment variable or use fallback - DeploymentName = Environment.GetEnvironmentVariable("OPENAI_DEPLOYMENT_NAME") ?? "gpt-4"; + // Get deployment name from configuration or use fallback + DeploymentName = Configuration["OPENAI_DEPLOYMENT_NAME"] ?? "gpt-4"; Logger.LogInformation($"Using OpenAI deployment: {DeploymentName}"); // Create credential for authentication diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/ImageGenerationAgentService.cs b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/ImageGenerationAgentService.cs index 4d2f41b..80d7fa6 100644 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/ImageGenerationAgentService.cs +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/ImageGenerationAgentService.cs @@ -1,6 +1,7 @@ + using Microsoft.Extensions.Logging; using Microsoft.Extensions.Configuration; -using AgentChainingSample.Shared.Models; +using AgentChainingSample.Worker.Models; using Azure.Identity; using Azure.Core; using System.Collections.Generic; diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/ResearchAgentService.cs b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/ResearchAgentService.cs index ee57827..c69e05f 100644 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/ResearchAgentService.cs +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/ResearchAgentService.cs @@ -67,4 +67,4 @@ 5. Consider expert opinions or analyses that might be available Logger.LogInformation($"Requesting research for topic: {topic}"); return await GetResponseAsync(prompt); } -} +} \ No newline at end of file From 5b1571623e9336aa01622fc3a76eaa7b8c23fa50 Mon Sep 17 00:00:00 2001 From: greenie-msft Date: Wed, 23 Jul 2025 10:05:32 -0700 Subject: [PATCH 4/7] Address more PR feedback --- .../Worker/Services/AgentServices.cs | 9 - .../Worker/Services/BaseAgentService.cs | 20 +- .../Worker/Services/IAgentService.cs | 443 ------------------ 3 files changed, 17 insertions(+), 455 deletions(-) delete mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/AgentServices.cs delete mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/IAgentService.cs diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/AgentServices.cs b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/AgentServices.cs deleted file mode 100644 index a34c27f..0000000 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/AgentServices.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.Extensions.Logging; - -namespace AgentChainingSample.Services; - -// Note: Implementation classes have been moved to separate files: -// - ResearchAgentService.cs -// - ContentGenerationAgentService.cs -// - ImageGenerationAgentService.cs -// - Models/DalleModels.cs diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/BaseAgentService.cs b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/BaseAgentService.cs index a9a0856..01f9918 100644 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/BaseAgentService.cs +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/BaseAgentService.cs @@ -50,8 +50,22 @@ protected BaseAgentService(string endpointUrl, ILogger logger, DeploymentName = Configuration["OPENAI_DEPLOYMENT_NAME"] ?? "gpt-4"; Logger.LogInformation($"Using OpenAI deployment: {DeploymentName}"); - // Create credential for authentication - Credential = new DefaultAzureCredential(); + // Create credential for authentication with specific client ID if available + var clientId = Configuration["AGENT_CONNECTION_STRING__clientId"] ?? Configuration["AZURE_CLIENT_ID"]; + if (!string.IsNullOrEmpty(clientId)) + { + Logger.LogInformation($"Using managed identity with client ID: {clientId}"); + var defaultCredentialOptions = new DefaultAzureCredentialOptions + { + ManagedIdentityClientId = clientId + }; + Credential = new DefaultAzureCredential(defaultCredentialOptions); + } + else + { + Logger.LogInformation("Using default Azure credential without specific client ID"); + Credential = new DefaultAzureCredential(); + } Logger.LogInformation($"Initializing Azure AI Projects client with endpoint: {Endpoint}"); @@ -464,4 +478,4 @@ private async Task HandleRetry(int retryCount, int retryDelay, string error // Double the delay for the next retry (exponential backoff) return retryDelay * 2; } -} +} \ No newline at end of file diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/IAgentService.cs b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/IAgentService.cs deleted file mode 100644 index b1a8e56..0000000 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Services/IAgentService.cs +++ /dev/null @@ -1,443 +0,0 @@ -using System.Text.Json; -using Azure; -using Azure.AI.Projects; -using Azure.AI.Agents.Persistent; -using Azure.Core; -using Azure.Identity; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using System.Text; - -namespace AgentChainingSample.Services; - -/// -/// Interface for agent services -/// -public interface IAgentService -{ - /// - /// Gets the agent ID used by this service - /// - string AgentId { get; set; } - - /// - /// Gets the endpoint URI for this agent service - /// - string Endpoint { get; } - - /// - /// Ensures the agent exists, creating it if necessary - /// - Task EnsureAgentExistsAsync(string agentName, string systemPrompt); - - /// - /// Gets a response from the agent - /// - Task GetResponseAsync(string prompt); - - /// - /// Cleans JSON response from markdown formatting - /// - string CleanJsonResponse(string response); -} - -/// -/// Base implementation for agent services -/// -public abstract class BaseAgentService : IAgentService -{ - protected readonly JsonSerializerOptions JsonOptions; - protected readonly ILogger Logger; - protected readonly AIProjectClient ProjectClient; - protected PersistentAgentsClient AgentsClient; - protected readonly TokenCredential Credential; - protected readonly IConfiguration Configuration; - - // Retry configuration - private const int MaxRetryAttempts = 3; - private const int InitialRetryDelayMs = 1000; // Start with a 1 second delay - - // Deployment name from configuration or fallback to default - protected readonly string DeploymentName; - - public string AgentId { get; set; } - public string Endpoint { get; } - - protected BaseAgentService(string endpointUrl, ILogger logger, IConfiguration configuration) - { - AgentId = string.Empty; // Will be set during initialization - Endpoint = endpointUrl ?? throw new ArgumentNullException(nameof(endpointUrl)); - Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); - - // Get deployment name from configuration or use fallback - DeploymentName = Configuration["OPENAI_DEPLOYMENT_NAME"] ?? "gpt-4"; - Logger.LogInformation($"Using OpenAI deployment: {DeploymentName}"); - - // Create credential for authentication - Credential = new DefaultAzureCredential(); - - Logger.LogInformation($"Initializing Azure AI Projects client with endpoint: {Endpoint}"); - - // Create the AIProjectClient using the endpoint and credential - ProjectClient = new AIProjectClient(new Uri(Endpoint), Credential); - - // Get the PersistentAgentsClient for agent operations - AgentsClient = ProjectClient.GetPersistentAgentsClient(); - - Logger.LogInformation("Azure AI Projects client and Persistent Agents client initialized successfully"); - - JsonOptions = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - PropertyNameCaseInsensitive = true, - AllowTrailingCommas = true - }; - } - - - - /// - /// Ensures the agent exists, creating it if necessary - /// - public async Task EnsureAgentExistsAsync(string agentName, string systemPrompt) - { - Logger.LogInformation($"Setting up agent: {agentName}"); - Logger.LogInformation($"Using Azure AI Projects endpoint: {Endpoint}"); - - try - { - // Check if an agent with this name already exists - PersistentAgent? existingAgent = null; - string existingAgentId = string.Empty; - - try - { - Logger.LogInformation($"Attempting to retrieve agents from Azure AI Projects endpoint: {Endpoint}"); - var agents = AgentsClient.Administration.GetAgents(); - - // Try to find an existing agent with the same name - foreach (var agent in agents) - { - if (agent.Name == agentName) - { - existingAgent = agent; - existingAgentId = agent.Id; - Logger.LogInformation($"Found existing agent: {agentName} with ID: {existingAgentId}"); - break; - } - } - } - catch (RequestFailedException rfEx) - { - Logger.LogWarning($"Error getting agents: {rfEx.Message}. Status: {rfEx.Status}"); - throw; // Re-throw the exception to surface it - } - - string agentId; - - if (existingAgent == null) - { - try - { - Logger.LogInformation($"Creating new agent: {agentName}"); - Logger.LogInformation($"Using Azure AI Projects endpoint: {Endpoint}"); - Logger.LogInformation($"Using model deployment from env var OPENAI_DEPLOYMENT_NAME: {DeploymentName}"); - - // Create a new agent - var agentResponse = await AgentsClient.Administration.CreateAgentAsync( - DeploymentName, - agentName, - systemPrompt, - $"News Article Generator agent created on {DateTime.UtcNow:yyyy-MM-dd}"); - - agentId = agentResponse.Value.Id; - - Logger.LogInformation($"Created new agent: {agentName} with ID: {agentId}"); - } - catch (Exception ex) - { - Logger.LogWarning($"Error creating agent: {ex.Message}"); - throw; // Re-throw the exception to surface it - } - } - else - { - agentId = existingAgentId; - - try - { - // Update the agent's system prompt if it exists - var updatedAgent = await AgentsClient.Administration.UpdateAgentAsync( - agentId, - systemPrompt, - $"News Article Generator agent updated on {DateTime.UtcNow:yyyy-MM-dd}"); - Logger.LogInformation($"Updated existing agent: {agentName} with ID: {agentId}"); - } - catch (Exception ex) - { - Logger.LogWarning($"Error updating agent: {ex.Message}. Using existing agent ID anyway."); - } - } - - // Set the agent ID field - AgentId = agentId; - return agentId; - } - catch (Exception ex) - { - Logger.LogWarning($"Unexpected error in EnsureAgentExistsAsync: {ex.Message}"); - throw; // Re-throw the exception to surface it - } - } - - /// - /// Validates and normalizes JSON responses from agents - /// - /// The JSON response from an agent - /// Validated JSON string - public string CleanJsonResponse(string response) - { - if (string.IsNullOrEmpty(response)) - { - Logger.LogWarning("[JSON-PARSER] Response was null or empty"); - return "{}"; - } - - Logger.LogInformation($"[JSON-PARSER] Processing response ({response.Length} chars)"); - - // Trim any whitespace - response = response.Trim(); - - // Simple case: Check if response is already valid JSON - try - { - using (JsonDocument.Parse(response)) - { - Logger.LogInformation("[JSON-PARSER] Response is valid JSON"); - return response; - } - } - catch (JsonException) - { - Logger.LogInformation("[JSON-PARSER] Initial JSON validation failed, attempting to extract JSON"); - } - - // Handle markdown code blocks if present - if (response.Contains("```")) - { - // Find start and end of code block - int codeBlockStart = response.IndexOf("```"); - int codeBlockEnd = response.LastIndexOf("```"); - - if (codeBlockStart != codeBlockEnd) // Make sure we found both opening and closing markers - { - // Extract content between code blocks - int startIndex = response.IndexOf('\n', codeBlockStart) + 1; - int endIndex = codeBlockEnd; - - // Make sure we have valid start and end indices - if (startIndex > 0 && endIndex > startIndex) - { - string codeContent = response.Substring(startIndex, endIndex - startIndex).Trim(); - Logger.LogInformation("[JSON-PARSER] Extracted content from code block"); - - // Remove any language specifier like ```json - if (codeContent.StartsWith("json", StringComparison.OrdinalIgnoreCase)) - { - codeContent = codeContent.Substring(4).Trim(); - } - - response = codeContent; - } - } - } - - // Check if response is wrapped in backticks - if (response.StartsWith("`") && response.EndsWith("`")) - { - response = response.Substring(1, response.Length - 2).Trim(); - Logger.LogInformation("[JSON-PARSER] Removed backticks"); - } - - // Final validation - try - { - using (JsonDocument.Parse(response)) - { - Logger.LogInformation("[JSON-PARSER] Successfully validated JSON"); - return response; - } - } - catch (JsonException ex) - { - Logger.LogError($"[JSON-PARSER] Failed to parse JSON: {ex.Message}"); - return "{}"; // Return empty JSON object as fallback - } - } - - public async Task GetResponseAsync(string prompt) - { - - int retryCount = 0; - int retryDelay = InitialRetryDelayMs; - bool shouldRetry; - - do - { - shouldRetry = false; - - try - { - // Check if agent ID is set - if (string.IsNullOrEmpty(AgentId)) - { - throw new InvalidOperationException($"Agent ID is not set. Call EnsureAgentExistsAsync first."); - } - - Logger.LogInformation($"Getting response from agent {AgentId}"); - - // Create a thread - var threadResponse = await AgentsClient.Threads.CreateThreadAsync(); - string threadId = threadResponse.Value.Id; - Logger.LogInformation($"Created thread, thread ID: {threadId}"); - - // Create message content - var messageContent = new List - { - new MessageInputTextBlock(prompt) - }; - - // Send the prompt to the thread - var messageResponse = await AgentsClient.Messages.CreateMessageAsync( - threadId: threadId, - role: MessageRole.User, - content: prompt); - - var threadMessage = messageResponse.Value; - Logger.LogInformation($"Created message, message ID: {threadMessage.Id}"); - - // Create a run with the agent using the agent ID - var runResponse = await AgentsClient.Runs.CreateRunAsync(threadId, AgentId); - - var run = runResponse.Value; - Logger.LogInformation($"Created run, run ID: {run.Id}"); - - // Poll the run until it's completed - do - { - await Task.Delay(TimeSpan.FromMilliseconds(500)); - var getRunResponse = await AgentsClient.Runs.GetRunAsync(threadId, run.Id); - run = getRunResponse.Value; - } - while (run.Status == RunStatus.Queued - || run.Status == RunStatus.InProgress - || run.Status == RunStatus.RequiresAction); - - Logger.LogInformation($"Run completed with status: {run.Status}"); - - if (run.Status == RunStatus.Failed) - { - // Try to extract error message - string errorMessage = run.LastError?.Message ?? string.Empty; - - // Check if the error is due to rate limiting - if (errorMessage.Contains("Rate limit") && retryCount < MaxRetryAttempts) - { - shouldRetry = true; - retryDelay = await HandleRetry(++retryCount, retryDelay, errorMessage); - continue; - } - - throw new Exception($"Run failed: {errorMessage}"); - } - - // Get messages from the assistant thread - var messagesResponse = AgentsClient.Messages.GetMessagesAsync( - threadId: threadId, - order: ListSortOrder.Descending); // Get newest first - - List assistantMessages = new List(); - - await foreach (var message in messagesResponse) - { - if (message.Role == "assistant") - { - assistantMessages.Add(message); - } - } - - if (assistantMessages.Count == 0) - { - Logger.LogWarning("No assistant messages found in the response"); - return string.Empty; - } - - // Get the most recent message from the assistant (first in list with Descending order) - var latestMessage = assistantMessages.First(); - - // Extract all text content items from the message - StringBuilder contentBuilder = new StringBuilder(); - - foreach (var contentItem in latestMessage.ContentItems) - { - if (contentItem is MessageTextContent textContent) - { - contentBuilder.Append(textContent.Text); - } - } - - string responseContent = contentBuilder.ToString(); - - Logger.LogInformation($"Retrieved response from agent {AgentId}, content length: {responseContent.Length} characters"); - - return responseContent; - } - catch (RequestFailedException ex) when ( - (ex.Status == 429 || // 429 Too Many Requests - ex.Status == 503) && // 503 Service Unavailable - retryCount < MaxRetryAttempts) - { - shouldRetry = true; - retryDelay = await HandleRetry(++retryCount, retryDelay, ex.Message); - } - catch (Exception ex) - { - // Check if the exception message contains indication of rate limit - if (ex.Message.Contains("Rate limit") && retryCount < MaxRetryAttempts) - { - shouldRetry = true; - retryDelay = await HandleRetry(++retryCount, retryDelay, ex.Message); - } - else - { - Logger.LogError($"Error calling agent {AgentId}: {ex.Message}"); - throw; - } - } - } while (shouldRetry); - - // This should not be reached unless all retry attempts fail - throw new Exception($"Failed to get a response from agent {AgentId} after {MaxRetryAttempts} attempts"); - } - - private async Task HandleRetry(int retryCount, int retryDelay, string errorMessage) - { - // Calculate exponential backoff with jitter - int maxJitterMs = retryDelay / 4; - Random random = new Random(); - int jitter = random.Next(-maxJitterMs, maxJitterMs); - int actualDelay = retryDelay + jitter; - - Logger.LogInformation($"Rate limit hit for agent {AgentId}. Retrying in {actualDelay}ms (attempt {retryCount} of {MaxRetryAttempts}). Error: {errorMessage}"); - - // Wait for the calculated delay - await Task.Delay(actualDelay); - - // Double the delay for the next retry (exponential backoff) - return retryDelay * 2; - } - - -} From 183a7182455ca5c1c6f31e623985921e0dce19fa Mon Sep 17 00:00:00 2001 From: greenie-msft Date: Tue, 5 Aug 2025 14:32:16 -0700 Subject: [PATCH 5/7] Added Bicep --- .../Agents/PromptChaining/Client/Dockerfile | 40 + .../Models/ContentModels.cs | 7 +- .../Agents/PromptChaining/Client/Program.cs | 122 + .../Agents/PromptChaining/Client/README.md | 38 + .../Agents/PromptChaining/Client/test.http | 57 + .../PromptChaining/Directory.Packages.props | 4 + .../PromptChaining/Shared/Shared.csproj | 16 - .../Agents/PromptChaining/Worker/Dockerfile | 40 + .../Worker/Models/ContentModels.cs | 202 + .../ContentCreationOrchestration.cs | 8 + .../dotnet/Agents/PromptChaining/azure.yaml | 32 + .../PromptChaining/infra/abbreviations.json | 139 + ...ost-capability-host-role-assignments.bicep | 106 + .../infra/agent/standard-ai-hub.bicep | 0 .../standard-ai-project-capability-host.bicep | 43 + ...standard-ai-project-role-assignments.bicep | 269 ++ .../infra/agent/standard-ai-project.bicep | 132 + .../agent/standard-dependent-resources.bicep | 284 ++ .../infra/app/ai-role-assignments.bicep | 86 + .../Agents/PromptChaining/infra/app/app.bicep | 186 + .../Agents/PromptChaining/infra/app/dts.bicep | 29 + .../infra/app/openai-access.bicep | 0 .../infra/app/registry-access.bicep | 27 + .../infra/app/storage-Access.bicep | 30 + .../infra/app/user-assigned-identity.bicep | 17 + .../infra/app/user-registry-access.bicep | 26 + .../PromptChaining/infra/core/ai/ai-hub.bicep | 0 .../infra/core/ai/ai-project.bicep | 100 + .../infra/core/ai/ai-role-assignments.bicep | 26 + .../PromptChaining/infra/core/ai/hub.bicep | 0 .../infra/core/ai/standard-ai-project.bicep | 83 + .../core/host/appservice-appsettings.bicep | 3 + .../infra/core/host/appservice-plan.bicep | 22 + .../infra/core/host/container-app.bicep | 150 + .../host/container-apps-environment.bicep | 45 + .../infra/core/host/container-apps.bicep | 80 + .../infra/core/host/functions-app.bicep | 75 + .../identity/user-assigned-identity.bicep | 14 + .../core/monitor/appinsights-access.bicep | 20 + .../core/monitor/application-insights.bicep | 22 + .../infra/core/monitor/loganalytics.bicep | 21 + .../infra/core/monitor/monitoring.bicep | 31 + .../infra/core/networking/vnet.bicep | 57 + .../infra/core/security/registry-access.bicep | 25 + .../infra/core/security/role.bicep | 21 + .../infra/core/storage/storage-account.bicep | 48 + .../Agents/PromptChaining/infra/main.bicep | 429 ++ .../Agents/PromptChaining/infra/main.json | 3949 +++++++++++++++++ .../PromptChaining/infra/main.parameters.json | 15 + .../main.update-existing.parameters.json | 18 + .../scripts/docker-push-with-retry.sh | 38 + 51 files changed, 7215 insertions(+), 17 deletions(-) create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/Dockerfile rename samples/durable-task-sdks/dotnet/Agents/PromptChaining/{Shared => Client}/Models/ContentModels.cs (96%) create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/README.md create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/test.http delete mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/Shared/Shared.csproj create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Dockerfile create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Models/ContentModels.cs create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/azure.yaml create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/abbreviations.json create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/agent/post-capability-host-role-assignments.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/agent/standard-ai-hub.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/agent/standard-ai-project-capability-host.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/agent/standard-ai-project-role-assignments.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/agent/standard-ai-project.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/agent/standard-dependent-resources.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/ai-role-assignments.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/app.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/dts.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/openai-access.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/registry-access.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/storage-Access.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/user-assigned-identity.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/user-registry-access.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/ai/ai-hub.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/ai/ai-project.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/ai/ai-role-assignments.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/ai/hub.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/ai/standard-ai-project.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/host/appservice-appsettings.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/host/appservice-plan.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/host/container-app.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/host/container-apps-environment.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/host/container-apps.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/host/functions-app.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/identity/user-assigned-identity.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/monitor/appinsights-access.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/monitor/application-insights.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/monitor/loganalytics.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/monitor/monitoring.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/networking/vnet.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/security/registry-access.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/security/role.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/storage/storage-account.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/main.bicep create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/main.json create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/main.parameters.json create mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/main.update-existing.parameters.json create mode 100755 samples/durable-task-sdks/dotnet/Agents/PromptChaining/scripts/docker-push-with-retry.sh diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/Dockerfile b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/Dockerfile new file mode 100644 index 0000000..d5f90cd --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/Dockerfile @@ -0,0 +1,40 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /app + +# Copy csproj and restore as distinct layers +COPY ["Client.csproj", "./"] +COPY ["*.config", "./"] +# Create NuGet.config if it doesn't exist in the build context +RUN if [ ! -f "NuGet.config" ] && [ ! -f "nuget.config" ]; then echo '\n\n \n \n \n \n' > NuGet.config; fi + +# Copy any props files and create Directory.Packages.props if needed +COPY ["*.props", "./"] +# Create Directory.Packages.props if it doesn't exist +RUN if [ ! -f "Directory.Packages.props" ]; then echo '\n \n true\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n' > Directory.Packages.props; fi + +RUN dotnet restore "Client.csproj" + +# Copy source code and models +COPY ["Program.cs", "./"] +COPY ["Models/", "Models/"] + +# Build the application +RUN dotnet build -c Release + +# Publish the application +RUN dotnet publish -c Release -o /app/publish + +# Build runtime image +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final +WORKDIR /app +COPY --from=build /app/publish . + +# Expose port 5000 explicitly for web API access +EXPOSE 5000 + +# Configure web server to listen on port 5000 +ENV ASPNETCORE_URLS=http://+:5000 +ENV ASPNETCORE_ENVIRONMENT=Production + +# Set the entrypoint +ENTRYPOINT ["dotnet", "AgentChainingSample.Client.dll"] diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Shared/Models/ContentModels.cs b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/Models/ContentModels.cs similarity index 96% rename from samples/durable-task-sdks/dotnet/Agents/PromptChaining/Shared/Models/ContentModels.cs rename to samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/Models/ContentModels.cs index da2ecec..3cd84ea 100644 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Shared/Models/ContentModels.cs +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/Models/ContentModels.cs @@ -1,7 +1,7 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace AgentChainingSample.Shared.Models; +namespace AgentChainingSample.Client.Models; /// /// Request to initiate the news article generation workflow @@ -64,6 +64,11 @@ public class ContentWorkflowResult /// public string ArticleBlobUrl { get; set; } = string.Empty; + /// + /// The URL endpoint to view the article online + /// + public string ArticleEndpoint { get; set; } = string.Empty; + /// /// Workflow completion timestamp /// diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/Program.cs b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/Program.cs index 24964ab..3cbe8a3 100644 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/Program.cs +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/Program.cs @@ -1,4 +1,126 @@ +using System; +using System.Threading; +using System.Threading.Tasks; using Azure.Identity; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Worker; +using Microsoft.DurableTask.Worker.AzureManaged; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +// Force rebuild timestamp: 2025-01-06 10:26 +using AgentChainingSample.Activities; +using AgentChainingSample.Orchestrations; +using AgentChainingSample.Services; +using AgentChainingSample.Worker.Models; + +// Configure the host builder +HostApplicationBuilder builder = Host.CreateApplicationBuilder(); + +// Configure logging +builder.Logging.AddConsole(); +builder.Logging.SetMinimumLevel(LogLevel.Information); + +// Add configuration to services +builder.Services.AddSingleton(builder.Configuration); + +// Add HttpClient factory for proper management of HTTP connections +builder.Services.AddHttpClient(); + +// Register named HttpClient for DALL-E with appropriate timeouts +builder.Services.AddHttpClient("DallEClient", client => +{ + client.Timeout = TimeSpan.FromSeconds(120); // Increase timeout for image generation +}); + +// Register services with DI as singletons +// These services perform initialization work that doesn't need to be repeated for each activity +// Thread safety for initialization is handled by the BaseAgentService implementation +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +// Activities with [DurableTask] attribute are auto-registered via AddAllGeneratedTasks() +// No need to manually register them here + +// Get connection string from configuration with fallback to default local emulator connection +string connectionString = builder.Configuration["ENDPOINT"] ?? + builder.Configuration["DTS_CONNECTION_STRING"] ?? + "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"; + +// If we have the endpoint but not a full connection string, construct it +if (connectionString.StartsWith("Endpoint=") && !connectionString.Contains("TaskHub=")) +{ + string taskHub = builder.Configuration["TASKHUB"] ?? builder.Configuration["TASKHUB_NAME"] ?? "default"; + string clientId = builder.Configuration["AZURE_MANAGED_IDENTITY_CLIENT_ID"] ?? ""; + + if (!string.IsNullOrEmpty(clientId)) + { + connectionString = $"{connectionString};Authentication=ManagedIdentity;ClientID={clientId};TaskHub={taskHub}"; + } + else + { + connectionString = $"{connectionString};TaskHub={taskHub}"; + } +} + +// Configure services +// Register tasks with DI +builder.Services.AddDurableTaskWorker(builder => +{ + builder.AddTasks(registry => + { + // Auto-register all tasks marked with [DurableTask] attribute + registry.AddAllGeneratedTasks(); + }); + builder.UseDurableTaskScheduler(connectionString); +}); + +// Build the host +IHost host = builder.Build(); + +// Get a proper logger from the service provider +var logger = host.Services.GetRequiredService>(); +// Log the constructed connection string with sensitive info redacted +string managedIdentityClientId = builder.Configuration["AZURE_MANAGED_IDENTITY_CLIENT_ID"] ?? ""; +string logConnectionString = !string.IsNullOrEmpty(managedIdentityClientId) ? + connectionString.Replace(managedIdentityClientId, "[REDACTED]") : + connectionString; +logger.LogInformation("Connection string: {ConnectionString}", logConnectionString); +logger.LogInformation("TaskHub: {TaskHub}", builder.Configuration["TASKHUB"] ?? builder.Configuration["TASKHUB_NAME"] ?? "default"); +logger.LogInformation("This worker implements a news article generator workflow with multiple specialized agents"); + +// Log OpenAI configuration +logger.LogInformation("Azure OpenAI Endpoint: {Endpoint}", builder.Configuration["AZURE_OPENAI_ENDPOINT"] ?? "Not set"); +logger.LogInformation("Azure OpenAI Deployment: {Deployment}", builder.Configuration["OPENAI_DEPLOYMENT_NAME"] ?? "gpt-4 (default)"); +logger.LogInformation("DALL-E Endpoint: {DalleEndpoint}", !string.IsNullOrEmpty(builder.Configuration["DALLE_ENDPOINT"]) ? + "Configured" : "Not set - will use placeholder images"); +logger.LogInformation("Agent Connection String: {AgentConnectionString}", !string.IsNullOrEmpty(builder.Configuration["AGENT_CONNECTION_STRING"]) ? + "Configured" : "Not set - required for agent functionality"); + +logger.LogInformation("Starting Agent Chaining Sample Worker"); + +// Start the host +await host.StartAsync(); + +logger.LogInformation("Worker started and waiting for tasks..."); + +// Wait indefinitely in environments without interactive console, +// or until a key is pressed in interactive environments +if (Environment.UserInteractive && !Console.IsInputRedirected) +{ + logger.LogInformation("Press any key to stop..."); + Console.ReadKey(); +} +else +{ + // In non-interactive environments (like containers), wait indefinitely + await Task.Delay(Timeout.InfiniteTimeSpan); +} + +// Stop the host +await host.StopAsync();using Azure.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.DurableTask; using Microsoft.DurableTask.Client; diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/README.md b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/README.md new file mode 100644 index 0000000..3f15ca8 --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/README.md @@ -0,0 +1,38 @@ +# Testing the Agent Chaining API + +This directory contains a `test.http` file that can be used to test the API endpoints using VS Code REST Client extension. + +## Prerequisites + +1. Install the [REST Client extension](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) for VS Code. + +## Using the test.http file + +1. Open the `test.http` file in VS Code +2. You'll see several HTTP requests defined in the file +3. Click the "Send Request" link that appears above each request to execute it +4. The response will be displayed in a new tab + +## Available Endpoints + +- `GET /health` - Health check endpoint +- `POST /api/content` - Create a new content generation request +- `GET /api/content` - List all active content generation requests +- `GET /api/content/{instanceId}` - Get status of a specific request +- `GET /api/content/{instanceId}/wait` - Wait for completion of a specific request with timeout + +## Sample Workflow + +1. Send a POST request to `/api/content` with a topic +2. Copy the instance ID from the response +3. Use that ID to check status with `GET /api/content/{instanceId}` +4. When the status is "Completed", you can retrieve the full result + +## Running Locally + +```bash +cd Client +dotnet run +``` + +The API will be available at http://localhost:5000. diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/test.http b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/test.http new file mode 100644 index 0000000..7eba04b --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/test.http @@ -0,0 +1,57 @@ +# HTTP REST Client test file for Agent Chaining Sample +# Update the baseUrl and instanceId variables below with your actual deployment values + +@baseUrl = https://your-app-name.region.azurecontainerapps.io +@instanceId = your-instance-id-here + +### Health check +GET {{baseUrl}}/health + +### Get root page +GET {{baseUrl}}/ + +### Create a new content generation request +POST {{baseUrl}}/api/content +Content-Type: application/json + +{ + "topic": "The Rise of Artificial Intelligence in Healthcare", + "requestId": "{{$guid}}" +} + +### Get all active orchestrations +GET {{baseUrl}}/api/content + +### Get status of a specific orchestration +# Replace the instanceId with a real ID from a previous request +# The response will now include an "ArticleEndpoint" property with a direct URL to view the article +GET {{baseUrl}}/api/content/{{instanceId}} + +### View the generated document in browser (NEW!) +# Replace the instanceId with a real ID from a completed request +GET {{baseUrl}}/api/content/{{instanceId}}/document + +### Download the generated document as HTML file (NEW!) +# Replace the instanceId with a real ID from a completed request +GET {{baseUrl}}/api/content/{{instanceId}}/download + +### Wait for orchestration to complete (with timeout) +# Replace the instanceId with a real ID from a previous request +GET {{baseUrl}}/api/content/{{instanceId}}/wait?timeoutSeconds=60 + +### Create another content generation request +POST {{baseUrl}}/api/content +Content-Type: application/json + +{ + "topic": "Future of Sustainable Energy", + "requestId": "{{$guid}}" +} + +### Create content about current technology trends +POST {{baseUrl}}/api/content +Content-Type: application/json + +{ + "topic": "Latest Trends in Quantum Computing" +} diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Directory.Packages.props b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Directory.Packages.props index c23729c..68130f0 100644 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Directory.Packages.props +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Directory.Packages.props @@ -30,5 +30,9 @@ + + + + diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Shared/Shared.csproj b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Shared/Shared.csproj deleted file mode 100644 index 3e57fa1..0000000 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Shared/Shared.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - - - net8.0 - enable - enable - AgentChainingSample.Shared - AgentChainingSample.Shared - - - - - - - diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Dockerfile b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Dockerfile new file mode 100644 index 0000000..b7d4d62 --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Dockerfile @@ -0,0 +1,40 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /app + +# Copy csproj files and restore as distinct layers +COPY ["Worker.csproj", "./"] +COPY ["*.config", "./"] +# Create NuGet.config if it doesn't exist in the build context +RUN if [ ! -f "NuGet.config" ] && [ ! -f "nuget.config" ]; then echo '\n\n \n \n \n \n' > NuGet.config; fi + +# Copy any props files and create Directory.Packages.props if needed +COPY ["*.props", "./"] +# Create Directory.Packages.props if it doesn't exist +RUN if [ ! -f "Directory.Packages.props" ]; then echo '\n \n true\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n' > Directory.Packages.props; fi + +# Restore packages +RUN dotnet restore "Worker.csproj" + +# Copy source code +COPY ["Program.cs", "./"] +COPY ["Activities/", "Activities/"] +COPY ["Orchestrations/", "Orchestrations/"] +COPY ["Services/", "Services/"] +COPY ["Models/", "Models/"] + +# Build the application +RUN dotnet build "Worker.csproj" -c Release + +# Publish the application +RUN dotnet publish "Worker.csproj" -c Release -o /app/publish + +# Build runtime image +FROM mcr.microsoft.com/dotnet/runtime:8.0 AS final +WORKDIR /app +COPY --from=build /app/publish . + +# Expose port 8080 explicitly +EXPOSE 8080 + +# Set the entrypoint +ENTRYPOINT ["dotnet", "AgentChainingSample.Worker.dll"] diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Models/ContentModels.cs b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Models/ContentModels.cs new file mode 100644 index 0000000..7d2e288 --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Models/ContentModels.cs @@ -0,0 +1,202 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace AgentChainingSample.Worker.Models; + +/// +/// Request to initiate the news article generation workflow +/// +public class ContentCreationRequest +{ + /// + /// News topic to research and write about + /// + public string Topic { get; set; } = string.Empty; + + /// + /// Optional client request ID for tracking + /// + public string? RequestId { get; set; } + + /// + /// Timestamp when the request was received + /// + public DateTime RequestTimestamp { get; set; } = DateTime.UtcNow; +} + +/// +/// News article workflow result containing all agent outputs +/// +public class ContentWorkflowResult +{ + /// + /// The original topic + /// + public string Topic { get; set; } = string.Empty; + + /// + /// Research data from the Research Agent with Web Search + /// + public ResearchData ResearchData { get; set; } = new(); + + /// + /// Article content from the Content Generation Agent with Knowledge Files + /// + public string ArticleContent { get; set; } = string.Empty; + + /// + /// Image details from the Image Generation Agent with DALL-E + /// + public List GeneratedImages { get; set; } = new(); + + /// + /// Final article HTML content with images and proper formatting + /// + public string FinalArticle { get; set; } = string.Empty; + + /// + /// Local file path where the HTML article is saved + /// + public string ArticleFilePath { get; set; } = string.Empty; + + /// + /// URL to the article in blob storage (kept for compatibility, always empty) + /// + public string ArticleBlobUrl { get; set; } = string.Empty; + + /// + /// The URL endpoint to view the article online + /// + public string ArticleEndpoint { get; set; } = string.Empty; + + /// + /// Workflow completion timestamp + /// + public DateTime CompletedTimestamp { get; set; } = DateTime.UtcNow; +} + +/// +/// Research data from web search +/// +public class ResearchData +{ + /// + /// Key facts discovered during research + /// + [JsonPropertyName("facts")] + public List Facts { get; set; } = new(); + + /// + /// Relevant sources found during research + /// + [JsonPropertyName("sources")] + public List Sources { get; set; } = new(); + + /// + /// Summary of research findings + /// + [JsonPropertyName("summary")] + public string Summary { get; set; } = string.Empty; + + /// + /// Suggested article angles + /// + [JsonPropertyName("articleAngles")] + public List ArticleAngles { get; set; } = new(); + + /// + /// Parses research data from JSON + /// + public static ResearchData FromJson(string json) + { + try + { + var result = JsonSerializer.Deserialize(json, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + + return result ?? new ResearchData(); + } + catch + { + // Return empty research data if parsing fails + return new ResearchData(); + } + } +} + +/// +/// Source information from research +/// +public class ResearchSource +{ + /// + /// URL of the source + /// + [JsonPropertyName("url")] + public string Url { get; set; } = string.Empty; + + /// + /// Title of the source + /// + [JsonPropertyName("title")] + public string Title { get; set; } = string.Empty; + + /// + /// Brief description of the source content + /// + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; +} + +/// +/// Image generated by DALL-E +/// +public class GeneratedImage +{ + /// + /// Description of the image + /// + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + /// + /// Prompt used to generate the image + /// + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; + + /// + /// URL or Base64 representation of the image + /// + [JsonPropertyName("imageUrl")] + public string ImageUrl { get; set; } = string.Empty; + + /// + /// Caption for the image + /// + [JsonPropertyName("caption")] + public string Caption { get; set; } = string.Empty; + + /// + /// Parses generated image from JSON + /// + public static List FromJson(string json) + { + try + { + var result = JsonSerializer.Deserialize>(json, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + + return result ?? new List(); + } + catch + { + // Return empty list if parsing fails + return new List(); + } + } +} diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Orchestrations/ContentCreationOrchestration.cs b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Orchestrations/ContentCreationOrchestration.cs index 7349f4c..a622324 100644 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Orchestrations/ContentCreationOrchestration.cs +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Orchestrations/ContentCreationOrchestration.cs @@ -42,6 +42,7 @@ public override async Task RunAsync(TaskOrchestrationCont // 4. Assemble the final article with content and images and save to file in the project's tmp directory var articleResult = await context.CallActivityAsync( nameof(AssembleFinalArticleActivity), +<<<<<<< HEAD <<<<<<< HEAD (articleContent, generatedImages, context.InstanceId)); @@ -54,6 +55,13 @@ public override async Task RunAsync(TaskOrchestrationCont logger.LogInformation("Final article assembled. Length: {Length} characters", articleResult.HtmlContent.Length); logger.LogInformation("Article saved to file: {FilePath}", articleResult.FilePath); >>>>>>> 21471c1 (Address PR feedback) +======= + (articleContent, generatedImages, context.InstanceId)); + + logger.LogInformation("Final article assembled. Length: {Length} characters", articleResult.HtmlContent.Length); + logger.LogInformation("Article saved to file: {FilePath}", articleResult.FilePath); + logger.LogInformation("Article endpoint: {Endpoint}", articleResult.ArticleEndpoint); +>>>>>>> c34d9d0 (Added Bicep) // 5. Return the complete workflow result return new ContentWorkflowResult diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/azure.yaml b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/azure.yaml new file mode 100644 index 0000000..c0cc762 --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/azure.yaml @@ -0,0 +1,32 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +# This is an example starter azure.yaml file containing several example services in comments below. +# Make changes as needed to describe your application setup. +# To learn more about the azure.yaml file, visit https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/azd-schema + +# Name of the application. +metadata: + template: agent-prompt-chaining-aca-v2 +name: agent-prompt-chaining-aca +services: + client: + project: ./Client + language: csharp + host: containerapp + apiVersion: 2025-01-01 + docker: + path: ./Dockerfile + bindings: + - port: 5000 + protocol: http + transport: auto + targetPort: 5000 + external: true + worker: + project: ./Worker + language: csharp + host: containerapp + apiVersion: 2025-01-01 + docker: + path: ./Dockerfile + diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/abbreviations.json b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/abbreviations.json new file mode 100644 index 0000000..1f9a112 --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/abbreviations.json @@ -0,0 +1,139 @@ +{ + "analysisServicesServers": "as", + "apiManagementService": "apim-", + "appConfigurationStores": "appcs-", + "appManagedEnvironments": "cae-", + "appContainerApps": "ca-", + "authorizationPolicyDefinitions": "policy-", + "automationAutomationAccounts": "aa-", + "blueprintBlueprints": "bp-", + "blueprintBlueprintsArtifacts": "bpa-", + "cacheRedis": "redis-", + "cdnProfiles": "cdnp-", + "cdnProfilesEndpoints": "cdne-", + "cognitiveServicesAccounts": "cog-", + "cognitiveServicesFormRecognizer": "cog-fr-", + "cognitiveServicesTextAnalytics": "cog-ta-", + "cognitiveServicesSpeech": "cog-sp-", + "computeAvailabilitySets": "avail-", + "computeCloudServices": "cld-", + "computeDiskEncryptionSets": "des", + "computeDisks": "disk", + "computeDisksOs": "osdisk", + "computeGalleries": "gal", + "computeSnapshots": "snap-", + "computeVirtualMachines": "vm", + "computeVirtualMachineScaleSets": "vmss-", + "containerInstanceContainerGroups": "ci", + "containerRegistryRegistries": "cr", + "containerServiceManagedClusters": "aks-", + "databricksWorkspaces": "dbw-", + "dataFactoryFactories": "adf-", + "dataLakeAnalyticsAccounts": "dla", + "dataLakeStoreAccounts": "dls", + "dataMigrationServices": "dms-", + "dBforMySQLServers": "mysql-", + "dBforPostgreSQLServers": "psql-", + "devicesIotHubs": "iot-", + "devicesProvisioningServices": "provs-", + "devicesProvisioningServicesCertificates": "pcert-", + "documentDBDatabaseAccounts": "cosmos-", + "eventGridDomains": "evgd-", + "eventGridDomainsTopics": "evgt-", + "eventGridEventSubscriptions": "evgs-", + "eventHubNamespaces": "evhns-", + "eventHubNamespacesEventHubs": "evh-", + "hdInsightClustersHadoop": "hadoop-", + "hdInsightClustersHbase": "hbase-", + "hdInsightClustersKafka": "kafka-", + "hdInsightClustersMl": "mls-", + "hdInsightClustersSpark": "spark-", + "hdInsightClustersStorm": "storm-", + "hybridComputeMachines": "arcs-", + "insightsActionGroups": "ag-", + "insightsComponents": "appi-", + "keyVaultVaults": "kv-", + "kubernetesConnectedClusters": "arck", + "kustoClusters": "dec", + "kustoClustersDatabases": "dedb", + "loadTesting": "lt-", + "logicIntegrationAccounts": "ia-", + "logicWorkflows": "logic-", + "machineLearningServicesWorkspaces": "mlw-", + "managedIdentityUserAssignedIdentities": "id-", + "managementManagementGroups": "mg-", + "migrateAssessmentProjects": "migr-", + "networkApplicationGateways": "agw-", + "networkApplicationSecurityGroups": "asg-", + "networkAzureFirewalls": "afw-", + "networkBastionHosts": "bas-", + "networkConnections": "con-", + "networkDnsZones": "dnsz-", + "networkExpressRouteCircuits": "erc-", + "networkFirewallPolicies": "afwp-", + "networkFirewallPoliciesWebApplication": "waf", + "networkFirewallPoliciesRuleGroups": "wafrg", + "networkFrontDoors": "fd-", + "networkFrontdoorWebApplicationFirewallPolicies": "fdfp-", + "networkLoadBalancersExternal": "lbe-", + "networkLoadBalancersInternal": "lbi-", + "networkLoadBalancersInboundNatRules": "rule-", + "networkLocalNetworkGateways": "lgw-", + "networkNatGateways": "ng-", + "networkNetworkInterfaces": "nic-", + "networkNetworkSecurityGroups": "nsg-", + "networkNetworkSecurityGroupsSecurityRules": "nsgsr-", + "networkNetworkWatchers": "nw-", + "networkPrivateDnsZones": "pdnsz-", + "networkPrivateLinkServices": "pl-", + "networkPublicIPAddresses": "pip-", + "networkPublicIPPrefixes": "ippre-", + "networkRouteFilters": "rf-", + "networkRouteTables": "rt-", + "networkRouteTablesRoutes": "udr-", + "networkTrafficManagerProfiles": "traf-", + "networkVirtualNetworkGateways": "vgw-", + "networkVirtualNetworks": "vnet-", + "networkVirtualNetworksSubnets": "snet-", + "networkVirtualNetworksVirtualNetworkPeerings": "peer-", + "networkVirtualWans": "vwan-", + "networkVpnGateways": "vpng-", + "networkVpnGatewaysVpnConnections": "vcn-", + "networkVpnGatewaysVpnSites": "vst-", + "notificationHubsNamespaces": "ntfns-", + "notificationHubsNamespacesNotificationHubs": "ntf-", + "operationalInsightsWorkspaces": "log-", + "portalDashboards": "dash-", + "powerBIDedicatedCapacities": "pbi-", + "purviewAccounts": "pview-", + "recoveryServicesVaults": "rsv-", + "resourcesResourceGroups": "rg-", + "searchSearchServices": "srch-", + "serviceBusNamespaces": "sb-", + "serviceBusNamespacesQueues": "sbq-", + "serviceBusNamespacesTopics": "sbt-", + "serviceEndPointPolicies": "se-", + "serviceFabricClusters": "sf-", + "signalRServiceSignalR": "sigr", + "sqlManagedInstances": "sqlmi-", + "sqlServers": "sql-", + "sqlServersDataWarehouse": "sqldw-", + "sqlServersDatabases": "sqldb-", + "sqlServersDatabasesStretch": "sqlstrdb-", + "storageStorageAccounts": "st", + "storageStorageAccountsVm": "stvm", + "storSimpleManagers": "ssimp", + "streamAnalyticsCluster": "asa-", + "synapseWorkspaces": "syn", + "synapseWorkspacesAnalyticsWorkspaces": "synw", + "synapseWorkspacesSqlPoolsDedicated": "syndp", + "synapseWorkspacesSqlPoolsSpark": "synsp", + "timeSeriesInsightsEnvironments": "tsi-", + "webServerFarms": "plan-", + "webSitesAppService": "app-", + "webSitesAppServiceEnvironment": "ase-", + "webSitesFunctions": "func-", + "webStaticSites": "stapp-", + "dts": "dts-", + "taskhub": "taskhub-" +} diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/agent/post-capability-host-role-assignments.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/agent/post-capability-host-role-assignments.bicep new file mode 100644 index 0000000..f202255 --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/agent/post-capability-host-role-assignments.bicep @@ -0,0 +1,106 @@ +// These must be created post-capability host addition because otherwise +// the containers will not yet exist. + +param aiProjectPrincipalId string +param aiProjectPrincipalType string = 'ServicePrincipal' // Workaround for https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments-template#new-service-principal +param aiProjectWorkspaceId string + +param aiStorageAccountName string +param cosmosDbAccountName string + +// Assignments for Storage Account containers +// ------------------------------------------------------------------ + +resource storage 'Microsoft.Storage/storageAccounts@2022-05-01' existing = { + name: aiStorageAccountName +} + +// Assign AI Project Storage Blob Data Owner Role for the dependent resource storage account. +// Limits ownership to containers specific to the Project Workspace. + +var storageBlobDataOwnerRoleDefinitionId = 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b' +var conditionStr = '((!(ActionMatches{\'Microsoft.Storage/storageAccounts/blobServices/containers/blobs/tags/read\'}) AND !(ActionMatches{\'Microsoft.Storage/storageAccounts/blobServices/containers/blobs/filter/action\'}) AND !(ActionMatches{\'Microsoft.Storage/storageAccounts/blobServices/containers/blobs/tags/write\'}) ) OR (@Resource[Microsoft.Storage/storageAccounts/blobServices/containers:name] StringStartsWithIgnoreCase \'${aiProjectWorkspaceId}\' AND @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:name] StringLikeIgnoreCase \'*-azureml-agent\'))' + +resource storageBlobDataOwnerAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: storage + name: guid(storage.id, aiProjectPrincipalId, storageBlobDataOwnerRoleDefinitionId, aiProjectWorkspaceId) + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', storageBlobDataOwnerRoleDefinitionId) + principalId: aiProjectPrincipalId + principalType: aiProjectPrincipalType + conditionVersion: '2.0' + condition: conditionStr + } +} + +// Assignments for CosmosDB containers +// ------------------------------------------------------------------ + +var userThreadName = '${aiProjectWorkspaceId}-thread-message-store' +var systemThreadName = '${aiProjectWorkspaceId}-system-thread-message-store' +var entityStoreName = '${aiProjectWorkspaceId}-agent-entity-store' + +resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2024-12-01-preview' existing = { + name: cosmosDbAccountName +} + +// Reference existing database +resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2024-12-01-preview' existing = { + parent: cosmosAccount + name: 'enterprise_memory' +} + +resource containerUserMessageStore 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2024-12-01-preview' existing = { + parent: database + name: userThreadName +} + +resource containerSystemMessageStore 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2024-12-01-preview' existing = { + parent: database + name: systemThreadName +} + +resource containerEntityStore 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2024-12-01-preview' existing = { + parent: database + name: entityStoreName +} + +var roleDefinitionId = resourceId( + 'Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', + cosmosDbAccountName, + '00000000-0000-0000-0000-000000000002' +) + +var scopeSystemContainer = '${cosmosAccount.id}/dbs/enterprise_memory/colls/${systemThreadName}' +var scopeUserContainer = '${cosmosAccount.id}/dbs/enterprise_memory/colls/${userThreadName}' +var scopeEntityContainer = '${cosmosAccount.id}/dbs/enterprise_memory/colls/${entityStoreName}' + +resource containerRoleAssignmentUserContainer 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2022-05-15' = { + parent: cosmosAccount + name: guid(aiProjectWorkspaceId, containerUserMessageStore.id, roleDefinitionId, aiProjectPrincipalId) + properties: { + principalId: aiProjectPrincipalId + roleDefinitionId: roleDefinitionId + scope: scopeUserContainer + } +} + +resource containerRoleAssignmentSystemContainer 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2022-05-15' = { + parent: cosmosAccount + name: guid(aiProjectWorkspaceId, containerSystemMessageStore.id, roleDefinitionId, aiProjectPrincipalId) + properties: { + principalId: aiProjectPrincipalId + roleDefinitionId: roleDefinitionId + scope: scopeSystemContainer + } +} + +resource containerRoleAssignmentEntityContainer 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2022-05-15' = { + parent: cosmosAccount + name: guid(aiProjectWorkspaceId, containerEntityStore.id, roleDefinitionId, aiProjectPrincipalId) + properties: { + principalId: aiProjectPrincipalId + roleDefinitionId: roleDefinitionId + scope: scopeEntityContainer + } +} diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/agent/standard-ai-hub.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/agent/standard-ai-hub.bicep new file mode 100644 index 0000000..e69de29 diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/agent/standard-ai-project-capability-host.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/agent/standard-ai-project-capability-host.bicep new file mode 100644 index 0000000..0bb95b8 --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/agent/standard-ai-project-capability-host.bicep @@ -0,0 +1,43 @@ +param cosmosDbConnection string +param azureStorageConnection string +param aiSearchConnection string +param projectName string +param aiServicesAccountName string +param projectCapHost string +param accountCapHost string + +var threadConnections = ['${cosmosDbConnection}'] +var storageConnections = ['${azureStorageConnection}'] +var vectorStoreConnections = ['${aiSearchConnection}'] + + +resource account 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = { + name: aiServicesAccountName +} + +resource project 'Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview' existing = { + name: projectName + parent: account +} + +resource accountCapabilityHost 'Microsoft.CognitiveServices/accounts/capabilityHosts@2025-04-01-preview' = { + name: accountCapHost + parent: account + properties: { + capabilityHostKind: 'Agents' + } +} + +resource projectCapabilityHost 'Microsoft.CognitiveServices/accounts/projects/capabilityHosts@2025-04-01-preview' = { + name: projectCapHost + parent: project + properties: { + capabilityHostKind: 'Agents' + vectorStoreConnections: vectorStoreConnections + storageConnections: storageConnections + threadStorageConnections: threadConnections + } + dependsOn: [ + accountCapabilityHost + ] +} diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/agent/standard-ai-project-role-assignments.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/agent/standard-ai-project-role-assignments.bicep new file mode 100644 index 0000000..9b558a2 --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/agent/standard-ai-project-role-assignments.bicep @@ -0,0 +1,269 @@ +param aiProjectPrincipalId string +param aiProjectPrincipalType string = 'ServicePrincipal' // Workaround for https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments-template#new-service-principal +param userPrincipalId string = '' +param allowUserIdentityPrincipal bool = false // Flag to enable user identity role assignments + +param aiServicesName string +param aiSearchName string +param aiCosmosDbName string +param aiStorageAccountName string + +param integrationStorageAccountName string + +// Parameters for function app managed identity +param functionAppManagedIdentityPrincipalId string = '' +param allowFunctionAppIdentityPrincipal bool = true // Flag to enable function app identity role assignments + +// Parameters for DALL-E deployment +param dalleAiServicesId string = '' + +// Assignments for AI Services +// ------------------------------------------------------------------ + +resource aiServices 'Microsoft.CognitiveServices/accounts@2024-06-01-preview' existing = { + name: aiServicesName +} + +// Assign AI Project the Cognitive Services Contributor Role on the AI Services resource + +var cognitiveServicesContributorRoleDefinitionId = '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68' + +resource cognitiveServicesContributorAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01'= { + scope: aiServices + name: guid(aiServices.id, cognitiveServicesContributorRoleDefinitionId, aiProjectPrincipalId) + properties: { + principalId: aiProjectPrincipalId + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', cognitiveServicesContributorRoleDefinitionId) + principalType: aiProjectPrincipalType + } +} + +// Assign AI Project the Cognitive Services OpenAI User Role on the AI Services resource + +var cognitiveServicesOpenAIUserRoleDefinitionId = '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' + +resource cognitiveServicesOpenAIUserRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: aiServices + name: guid(aiProjectPrincipalId, cognitiveServicesOpenAIUserRoleDefinitionId, aiServices.id) + properties: { + principalId: aiProjectPrincipalId + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', cognitiveServicesOpenAIUserRoleDefinitionId) + principalType: aiProjectPrincipalType + } +} + +// Assign AI Project the Cognitive Services User Role on the AI Services resource + +var cognitiveServicesUserRoleDefinitionId = 'a97b65f3-24c7-4388-baec-2e87135dc908' + +resource cognitiveServicesUserRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: aiServices + name: guid(aiProjectPrincipalId, cognitiveServicesUserRoleDefinitionId, aiServices.id) + properties: { + principalId: aiProjectPrincipalId + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', cognitiveServicesUserRoleDefinitionId) + principalType: aiProjectPrincipalType + } +} + +// Assign AI Project the Cognitive Services Contributor Role on the User Identity Principal on the AI Services resource + +resource cognitiveServicesContributorAssignmentUser 'Microsoft.Authorization/roleAssignments@2022-04-01'= if (allowUserIdentityPrincipal && !empty(userPrincipalId)) { + scope: aiServices + name: guid(aiServices.id, cognitiveServicesContributorRoleDefinitionId, userPrincipalId) + properties: { + principalId: userPrincipalId + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', cognitiveServicesContributorRoleDefinitionId) + principalType: 'User' + } +} + +// Assign AI Project the Cognitive Services OpenAI User Role on the User Identity Principal on the AI Services resource + +resource cognitiveServicesOpenAIUserRoleAssignmentUser 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (allowUserIdentityPrincipal && !empty(userPrincipalId)){ + scope: aiServices + name: guid(userPrincipalId, cognitiveServicesOpenAIUserRoleDefinitionId, aiServices.id) + properties: { + principalId: userPrincipalId + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', cognitiveServicesOpenAIUserRoleDefinitionId) + principalType: 'User' + } +} + +// Assign AI Project the Cognitive Services User Role on the User Identity Principal on the AI Services resource + +resource cognitiveServicesUserRoleAssignmentUser 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (allowUserIdentityPrincipal && !empty(userPrincipalId)){ + scope: aiServices + name: guid(userPrincipalId, cognitiveServicesUserRoleDefinitionId, aiServices.id) + properties: { + principalId: userPrincipalId + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', cognitiveServicesUserRoleDefinitionId) + principalType: 'User' + } +} + +// Assignments for AI Search Service +// ------------------------------------------------------------------ + +resource aiSearchService 'Microsoft.Search/searchServices@2024-06-01-preview' existing = { + name: aiSearchName +} + +// Assign AI Project the Search Index Data Contributor Role on the AI Search Service resource + +var searchIndexDataContributorRoleDefinitionId = '8ebe5a00-799e-43f5-93ac-243d3dce84a7' + +resource searchIndexDataContributorAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: aiSearchService + name: guid(aiProjectPrincipalId, searchIndexDataContributorRoleDefinitionId, aiSearchService.id) + properties: { + principalId: aiProjectPrincipalId + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', searchIndexDataContributorRoleDefinitionId) + principalType: aiProjectPrincipalType + } +} + +// Assign AI Project the Search Index Data Contributor Role on the AI Search Service resource + +var searchServiceContributorRoleDefinitionId = '7ca78c08-252a-4471-8644-bb5ff32d4ba0' + +resource searchServiceContributorRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: aiSearchService + name: guid(aiProjectPrincipalId, searchServiceContributorRoleDefinitionId, aiSearchService.id) + properties: { + principalId: aiProjectPrincipalId + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', searchServiceContributorRoleDefinitionId) + principalType: aiProjectPrincipalType + } +} + +// Assignments for Storage Account +// ------------------------------------------------------------------ + +resource aiStorageAccount 'Microsoft.Storage/storageAccounts@2024-01-01' existing = { + name: aiStorageAccountName +} + +// Assign AI Project the Storage Blob Data Contributor Role on the Storage Account resource + +var storageBlobDataContributorRoleDefinitionId = 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' + +resource storageBlobDataContributorRoleAssignmentProject 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: aiStorageAccount + name: guid(aiProjectPrincipalId, storageBlobDataContributorRoleDefinitionId, aiStorageAccount.id) + properties: { + principalId: aiProjectPrincipalId + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', storageBlobDataContributorRoleDefinitionId) + principalType: aiProjectPrincipalType + } +} + +// Assignments for Cosmos DB +// ------------------------------------------------------------------ + +resource cosmosDbAccount 'Microsoft.DocumentDB/databaseAccounts@2024-12-01-preview' existing = { + name: aiCosmosDbName +} + +// Assign AI Project the Cosmos DB Operator Role on the Cosmos DB Account resource + +var cosmosDbOperatorRoleDefinitionId = '230815da-be43-4aae-9cb4-875f7bd000aa' + +resource cosmosDbOperatorRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: cosmosDbAccount + name: guid(aiProjectPrincipalId, cosmosDbOperatorRoleDefinitionId, cosmosDbAccount.id) + properties: { + principalId: aiProjectPrincipalId + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', cosmosDbOperatorRoleDefinitionId) + principalType: aiProjectPrincipalType + } +} + +// Assignments for Storage Account +// ------------------------------------------------------------------ + +resource integrationStorageAccount 'Microsoft.Storage/storageAccounts@2024-01-01' existing = { + name: integrationStorageAccountName +} + +// Assign AI Project Storage Queue Data Contributor Role on the integration Storage Account resource +// between the agent and azure function + +var storageQueueDataContributorRoleDefinitionId = '974c5e8b-45b9-4653-ba55-5f855dd0fb88' + +resource storageQueueDataContributorRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(integrationStorageAccount.id, aiProjectPrincipalId, storageQueueDataContributorRoleDefinitionId) + scope: integrationStorageAccount + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', storageQueueDataContributorRoleDefinitionId) + principalId: aiProjectPrincipalId + principalType: aiProjectPrincipalType + } +} + +// assignments for User Identity Principal + +resource storageQueueDataContributorRoleAssignmentUser 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (allowUserIdentityPrincipal && !empty(userPrincipalId)) { + name: guid(integrationStorageAccount.id, userPrincipalId, storageQueueDataContributorRoleDefinitionId) + scope: integrationStorageAccount + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', storageQueueDataContributorRoleDefinitionId) + principalId: userPrincipalId + principalType: 'User' + } +} + +// Assign Function App Managed Identity the Cognitive Services Contributor Role on the AI Services resource + +resource cognitiveServicesContributorAssignmentFunctionApp 'Microsoft.Authorization/roleAssignments@2022-04-01'= if (allowFunctionAppIdentityPrincipal && !empty(functionAppManagedIdentityPrincipalId)) { + scope: aiServices + name: guid(aiServices.id, cognitiveServicesContributorRoleDefinitionId, functionAppManagedIdentityPrincipalId) + properties: { + principalId: functionAppManagedIdentityPrincipalId + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', cognitiveServicesContributorRoleDefinitionId) + principalType: 'ServicePrincipal' + } +} + +// Assign Function App Managed Identity the Cognitive Services OpenAI User Role on the AI Services resource + +resource cognitiveServicesOpenAIUserRoleAssignmentFunctionApp 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (allowFunctionAppIdentityPrincipal && !empty(functionAppManagedIdentityPrincipalId)) { + scope: aiServices + name: guid(functionAppManagedIdentityPrincipalId, cognitiveServicesOpenAIUserRoleDefinitionId, aiServices.id) + properties: { + principalId: functionAppManagedIdentityPrincipalId + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', cognitiveServicesOpenAIUserRoleDefinitionId) + principalType: 'ServicePrincipal' + } +} + +// Assign Function App Managed Identity the Cognitive Services User Role on the AI Services resource + +resource cognitiveServicesUserRoleAssignmentFunctionApp 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (allowFunctionAppIdentityPrincipal && !empty(functionAppManagedIdentityPrincipalId)) { + scope: aiServices + name: guid(functionAppManagedIdentityPrincipalId, cognitiveServicesUserRoleDefinitionId, aiServices.id) + properties: { + principalId: functionAppManagedIdentityPrincipalId + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', cognitiveServicesUserRoleDefinitionId) + principalType: 'ServicePrincipal' + } +} + +// DALL-E Role Assignments +// ------------------------------------------------------------------ + +// Reference to the Image Generation AI Services resource +resource imageGenAiServices 'Microsoft.CognitiveServices/accounts@2024-06-01-preview' existing = { + name: last(split(dalleAiServicesId, '/')) +} + +// Assign Function App Managed Identity the Cognitive Services OpenAI User Role on the Image Generation AI Services resource +resource imageGenOpenAIUserRoleAssignmentFunctionApp 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (allowFunctionAppIdentityPrincipal && !empty(functionAppManagedIdentityPrincipalId)) { + scope: imageGenAiServices + name: guid(functionAppManagedIdentityPrincipalId, cognitiveServicesOpenAIUserRoleDefinitionId, dalleAiServicesId) + properties: { + principalId: functionAppManagedIdentityPrincipalId + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', cognitiveServicesOpenAIUserRoleDefinitionId) + principalType: 'ServicePrincipal' + } +} diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/agent/standard-ai-project.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/agent/standard-ai-project.bicep new file mode 100644 index 0000000..c17a3bf --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/agent/standard-ai-project.bicep @@ -0,0 +1,132 @@ +// Creates an Azure AI resource with proxied endpoints for the Azure AI services provider + +@description('Azure region of the deployment') +param location string + +@description('Tags to add to the resources') +param tags object + +@description('AI Services Foundry account under which the project will be created') +param aiServicesAccountName string + +@description('AI Project name') +param aiProjectName string + +@description('AI Project display name') +param aiProjectFriendlyName string = aiProjectName + +@description('AI Project description') +param aiProjectDescription string + +@description('Cosmos DB Account for agent thread storage') +param cosmosDbAccountName string +param cosmosDbAccountSubscriptionId string +param cosmosDbAccountResourceGroupName string + +@description('Storage Account for agent artifacts') +param storageAccountName string +param storageAccountSubscriptionId string +param storageAccountResourceGroupName string + +@description('AI Search Service for vector store and search') +param aiSearchName string +param aiSearchSubscriptionId string +param aiSearchResourceGroupName string + +resource cosmosDbAccount 'Microsoft.DocumentDB/databaseAccounts@2024-11-15' existing = { + name: cosmosDbAccountName + scope: resourceGroup(cosmosDbAccountSubscriptionId, cosmosDbAccountResourceGroupName) +} + +resource storageAccount 'Microsoft.Storage/storageAccounts@2024-01-01' existing = { + name: storageAccountName + scope: resourceGroup(storageAccountSubscriptionId, storageAccountResourceGroupName) +} + +resource aiSearchService 'Microsoft.Search/searchServices@2024-06-01-preview' existing = { + name: aiSearchName + scope: resourceGroup(aiSearchSubscriptionId, aiSearchResourceGroupName) +} + +resource aiServicesAccount 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = { + name: aiServicesAccountName +} + +resource aiProject 'Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview' = { + parent: aiServicesAccount + name: aiProjectName + location: location + tags: tags + identity: { + type: 'SystemAssigned' + } + properties: { + description: aiProjectDescription + displayName: aiProjectFriendlyName + } + + resource project_connection_cosmosdb_account 'connections@2025-04-01-preview' = { + name: cosmosDbAccountName + properties: { + category: 'CosmosDB' + target: cosmosDbAccount.properties.documentEndpoint + authType: 'AAD' + metadata: { + ApiType: 'Azure' + ResourceId: cosmosDbAccount.id + location: cosmosDbAccount.location + } + } + } + + resource project_connection_azure_storage 'connections@2025-04-01-preview' = { + name: storageAccountName + properties: { + category: 'AzureStorageAccount' + target: storageAccount.properties.primaryEndpoints.blob + authType: 'AAD' + metadata: { + ApiType: 'Azure' + ResourceId: storageAccount.id + location: storageAccount.location + } + } + } + + resource project_connection_azureai_search 'connections@2025-04-01-preview' = { + name: aiSearchName + properties: { + category: 'CognitiveSearch' + target: 'https://${aiSearchName}.search.windows.net' + authType: 'AAD' + metadata: { + ApiType: 'Azure' + ResourceId: aiSearchService.id + location: aiSearchService.location + } + } + } +} + +// Outputs + +output aiProjectName string = aiProject.name +output aiProjectResourceId string = aiProject.id +output aiProjectPrincipalId string = aiProject.identity.principalId + +output aiSearchConnection string = aiSearchName +output azureStorageConnection string = storageAccountName +output cosmosDbConnection string = cosmosDbAccountName + +// This is used for storage naming conventions and is needed to help +// create the right fine-grained role assignments. The naming +// convention also uses dashes injected into the value, so we're +// handling that here. +// This will likely change or be made available via a different property. +#disable-next-line BCP053 +var internalId = aiProject.properties.internalId +output projectWorkspaceId string = '${substring(internalId, 0, 8)}-${substring(internalId, 8, 4)}-${substring(internalId, 12, 4)}-${substring(internalId, 16, 4)}-${substring(internalId, 20, 12)}' + +// This endpoint is also built by convention at this time but will +// hopefully be available as a different property at some point. +output projectEndpoint string = 'https://${aiServicesAccountName}.services.ai.azure.com/api/projects/${aiProjectName}' diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/agent/standard-dependent-resources.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/agent/standard-dependent-resources.bicep new file mode 100644 index 0000000..7e5ee38 --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/agent/standard-dependent-resources.bicep @@ -0,0 +1,284 @@ +// Creates Azure dependent resources for Azure AI studio + +@description('Azure region of the deployment') +param location string = resourceGroup().location + +@description('Tags to add to the resources') +param tags object = {} + +@description('AI services name') +param aiServicesName string + +@description('The name of the AI Search resource') +param aiSearchName string + +@description('The name of the Cosmos DB account') +param cosmosDbName string + +@description('Name of the storage account') +param storageName string + +@description('Model name for deployment') +param modelName string + +@description('Model format for deployment') +param modelFormat string + +@description('Model version for deployment') +param modelVersion string + +@description('Model deployment SKU name') +param modelSkuName string + +@description('Model deployment capacity') +param modelCapacity int + +@description('DALL-E model name for deployment') +param dalleModelName string = 'dall-e-3' + +@description('DALL-E model version for deployment') +param dalleModelVersion string = '3.0' + +@description('DALL-E model deployment SKU name') +param dalleModelSkuName string = 'Standard' + +@description('DALL-E model deployment capacity') +param dalleModelCapacity int = 1 + +@description('Model/AI Resource deployment location') +param modelLocation string + +// Create a separate Azure OpenAI resource for image generation in East US +// Always create this resource since image models often need specific regions +resource imageGenAiServices 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' = { + name: '${aiServicesName}-imagegen' + location: 'eastus' // Try East US for DALL-E availability + sku: { + name: 'S0' + } + kind: 'OpenAI' + identity: { + type: 'SystemAssigned' + } + properties: { + customSubDomainName: toLower('${aiServicesName}-imagegen') + networkAcls: { + defaultAction: 'Allow' + virtualNetworkRules: [] + ipRules: [] + } + publicNetworkAccess: 'Enabled' + disableLocalAuth: false + } +} + +@description('The AI Service Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created.') +param aiServiceAccountResourceId string + +@description('The AI Search Service full ARM Resource ID. This is an optional field, and if not provided, the resource will be created.') +param aiSearchServiceResourceId string + +@description('The AI Storage Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created.') +param aiStorageAccountResourceId string + +@description('The AI Cosmos DB Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created.') +param aiCosmosDbAccountResourceId string + +var aiServiceExists = aiServiceAccountResourceId != '' +var acsExists = aiSearchServiceResourceId != '' +var aiStorageExists = aiStorageAccountResourceId != '' +var cosmosExists = aiCosmosDbAccountResourceId != '' + +// Create an AI Service account and model deployment if it doesn't already exist + +var aiServiceParts = split(aiServiceAccountResourceId, '/') + +resource existingAIServiceAccount 'Microsoft.CognitiveServices/accounts@2023-05-01' existing = if (aiServiceExists) { + name: aiServiceParts[8] + scope: resourceGroup(aiServiceParts[2], aiServiceParts[4]) +} + +resource aiServices 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' = if(!aiServiceExists) { + name: aiServicesName + location: modelLocation + sku: { + name: 'S0' + } + kind: 'AIServices' + identity: { + type: 'SystemAssigned' + } + properties: { + allowProjectManagement: true + customSubDomainName: toLower('${(aiServicesName)}') + networkAcls: { + defaultAction: 'Allow' + virtualNetworkRules: [] + ipRules: [] + } + publicNetworkAccess: 'Enabled' + // API-key based auth is not supported for the Agent service + disableLocalAuth: false + } +} +resource modelDeployment 'Microsoft.CognitiveServices/accounts/deployments@2025-04-01-preview'= if(!aiServiceExists){ + parent: aiServices + name: modelName + sku : { + capacity: modelCapacity + name: modelSkuName + } + properties: { + model:{ + name: modelName + format: modelFormat + version: modelVersion + } + } +} + +// DALL-E 3 deployment for image generation in East US +resource dalleDeployment 'Microsoft.CognitiveServices/accounts/deployments@2025-04-01-preview' = { + parent: imageGenAiServices + name: dalleModelName + sku: { + capacity: dalleModelCapacity + name: dalleModelSkuName + } + properties: { + model: { + name: dalleModelName + format: 'OpenAI' + version: dalleModelVersion + } + } +} + +// Create an AI Search Service if it doesn't already exist + +var acsParts = split(aiSearchServiceResourceId, '/') + +resource existingSearchService 'Microsoft.Search/searchServices@2023-11-01' existing = if (acsExists) { + name: acsParts[8] + scope: resourceGroup(acsParts[2], acsParts[4]) +} +resource aiSearch 'Microsoft.Search/searchServices@2024-06-01-preview' = if(!acsExists) { + name: aiSearchName + location: location + tags: tags + identity: { + type: 'SystemAssigned' + } + properties: { + disableLocalAuth: false + authOptions: { aadOrApiKey: { aadAuthFailureMode: 'http401WithBearerChallenge'}} + encryptionWithCmk: { + enforcement: 'Unspecified' + } + hostingMode: 'default' + partitionCount: 1 + publicNetworkAccess: 'enabled' + replicaCount: 1 + semanticSearch: 'disabled' + } + sku: { + name: 'standard' + } +} + +// Create a Storage account if it doesn't already exist + +var aiStorageParts = split(aiStorageAccountResourceId, '/') + +resource existingAIStorageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' existing = if (aiStorageExists) { + name: aiStorageParts[8] + scope: resourceGroup(aiStorageParts[2], aiStorageParts[4]) +} + +param sku string = 'Standard_LRS' + +resource storage 'Microsoft.Storage/storageAccounts@2023-05-01' = if(!aiStorageExists) { + name: storageName + location: location + kind: 'StorageV2' + sku: { + name: sku + } + properties: { + minimumTlsVersion: 'TLS1_2' + allowBlobPublicAccess: false + publicNetworkAccess: 'Enabled' + networkAcls: { + bypass: 'AzureServices' + defaultAction: 'Allow' + virtualNetworkRules: [] + } + allowSharedKeyAccess: false + // Do not set requireInfrastructureEncryption as it's a read-only property + } +} + +// Create a Cosmos DB Account if it doesn't already exist + +var cosmosAccountParts = split(aiCosmosDbAccountResourceId, '/') + +resource existingCosmosDbAccount 'Microsoft.DocumentDB/databaseAccounts@2024-11-15' existing = if (cosmosExists) { + name: cosmosAccountParts[8] + scope: resourceGroup(cosmosAccountParts[2], cosmosAccountParts[4]) +} + +var canaryRegions = ['eastus2euap', 'centraluseuap'] +var cosmosDbRegion = contains(canaryRegions, location) ? 'eastus2' : location +resource cosmosDbAccount 'Microsoft.DocumentDB/databaseAccounts@2024-11-15' = if(!cosmosExists) { + name: cosmosDbName + location: cosmosDbRegion + kind: 'GlobalDocumentDB' + properties: { + consistencyPolicy: { + defaultConsistencyLevel: 'Session' + } + disableLocalAuth: true + enableAutomaticFailover: false + enableMultipleWriteLocations: false + enableFreeTier: false + locations: [ + { + locationName: location + failoverPriority: 0 + isZoneRedundant: false + } + ] + databaseAccountOfferType: 'Standard' + } +} + +// Outputs + +output aiServicesName string = aiServiceExists ? existingAIServiceAccount.name : aiServices.name +output aiservicesID string = aiServiceExists ? existingAIServiceAccount.id : aiServices.id +#disable-next-line BCP318 +output aiservicesTarget string = aiServiceExists ? existingAIServiceAccount.properties.endpoint : aiServices.properties.endpoint +output aiServiceAccountResourceGroupName string = aiServiceExists ? aiServiceParts[4] : resourceGroup().name +output aiServiceAccountSubscriptionId string = aiServiceExists ? aiServiceParts[2] : subscription().subscriptionId + +// DALL-E 3 image generation endpoint +#disable-next-line BCP318 +output dalleEndpoint string = '${imageGenAiServices.properties.endpoint}openai/deployments/${dalleModelName}/images/generations?api-version=2023-12-01-preview' + +// Image generation AI Services resource ID for role assignments +output dalleAiServicesId string = imageGenAiServices.id + +output aiSearchName string = acsExists ? existingSearchService.name : aiSearch.name +output aisearchID string = acsExists ? existingSearchService.id : aiSearch.id +output aiSearchServiceResourceGroupName string = acsExists ? acsParts[4] : resourceGroup().name +output aiSearchServiceSubscriptionId string = acsExists ? acsParts[2] : subscription().subscriptionId + +output storageAccountName string = aiStorageExists ? existingAIStorageAccount.name : storage.name +output storageId string = aiStorageExists ? existingAIStorageAccount.id : storage.id +output storageAccountResourceGroupName string = aiStorageExists ? aiStorageParts[4] : resourceGroup().name +output storageAccountSubscriptionId string = aiStorageExists ? aiStorageParts[2] : subscription().subscriptionId + +output cosmosDbAccountName string = cosmosExists ? existingCosmosDbAccount.name : cosmosDbAccount.name +output cosmosDbAccountId string = cosmosExists ? existingCosmosDbAccount.id : cosmosDbAccount.id +output cosmosDbAccountResourceGroupName string = cosmosExists ? cosmosAccountParts[4] : resourceGroup().name +output cosmosDbAccountSubscriptionId string = cosmosExists ? cosmosAccountParts[2] : subscription().subscriptionId diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/ai-role-assignments.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/ai-role-assignments.bicep new file mode 100644 index 0000000..a82a118 --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/ai-role-assignments.bicep @@ -0,0 +1,86 @@ +// ai-role-assignments.bicep +// Assigns the necessary roles for AI resources + +@description('Principal ID to assign roles to') +param principalId string + +@description('Name of the OpenAI resource') +param openAiResourceName string = '' + +@description('Name of the AI Hub') +param aiHubName string = '' + +@description('Name of the AI Project') +param aiProjectName string = '' + +// If OpenAI resource name is provided, assign roles +resource openAi 'Microsoft.CognitiveServices/accounts@2023-05-01' existing = if (!empty(openAiResourceName)) { + name: openAiResourceName +} + +// Assign Cognitive Services OpenAI User role +resource openAiUserRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(openAiResourceName)) { + name: guid(openAi.id, principalId, '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd') + scope: openAi + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd') // Cognitive Services OpenAI User + principalType: 'ServicePrincipal' + } +} + +// Assign Cognitive Services Contributor role +resource cognitiveServicesContributorRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(openAiResourceName)) { + name: guid(openAi.id, principalId, '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68') + scope: openAi + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68') // Cognitive Services Contributor + principalType: 'ServicePrincipal' + } +} + +// Add AI Hub roles if hub name is provided +resource aiHub 'Microsoft.MachineLearningServices/workspaces@2024-01-01-preview' existing = if (!empty(aiHubName)) { + name: aiHubName +} + +resource aiHubContributorRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(aiHubName)) { + name: guid(aiHub.id, principalId, 'b24988ac-6180-42a0-ab88-20f7382dd24c') + scope: aiHub + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') // Contributor + principalType: 'ServicePrincipal' + } +} + +// Add AI Project roles if project name is provided +resource aiProject 'Microsoft.MachineLearningServices/workspaces@2023-08-01-preview' existing = if (!empty(aiProjectName)) { + name: aiProjectName +} + +resource aiProjectContributorRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(aiProjectName)) { + name: guid(aiProject.id, principalId, 'b24988ac-6180-42a0-ab88-20f7382dd24c') + scope: aiProject + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') // Contributor + principalType: 'ServicePrincipal' + } +} + +// Add Reader role - required for the managed identity to use AI Project services +resource aiProjectReaderRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(aiProjectName)) { + name: guid(aiProject.id, principalId, 'acdd72a7-3385-48ef-bd42-f606fba81ae7') + scope: aiProject + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7') // Reader role + principalType: 'ServicePrincipal' + } +} + +output openAiUserRoleAssignmentId string = !empty(openAiResourceName) ? openAiUserRoleAssignment.id : '' +output cognitiveServicesContributorRoleAssignmentId string = !empty(openAiResourceName) ? cognitiveServicesContributorRoleAssignment.id : '' +output aiProjectReaderRoleAssignmentId string = !empty(aiProjectName) ? aiProjectReaderRoleAssignment.id : '' diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/app.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/app.bicep new file mode 100644 index 0000000..1823f10 --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/app.bicep @@ -0,0 +1,186 @@ +param appName string +param location string = resourceGroup().location +param tags object = {} + +param identityName string +param containerAppsEnvironmentName string +param containerRegistryName string +param serviceName string = 'aca' +param dtsEndpoint string +param taskHubName string +// Legacy parameters (kept for backward compatibility) +param agentConnectionString string = '' +param openAiEndpoint string = '' +param openAiDeploymentName string = 'gpt-4o-mini' +param openAiApiKey string = '' +// New parameters using direct naming convention +param AGENT_CONNECTION_STRING string = '' +param OPENAI_DEPLOYMENT_NAME string = 'gpt-4o-mini' +param AGENT_CONNECTION_STRING__clientId string = '' +param DALLE_ENDPOINT string = '' +param clientBaseUrl string = '' + +type managedIdentity = { + resourceId: string + clientId: string +} + +@description('Unique identifier for user-assigned managed identity.') +param userAssignedManagedIdentity managedIdentity + +// Define different container configurations based on service type +var serviceConfig = serviceName == 'client' ? { + enableIngress: true + external: true + targetPort: 5000 + containerImage: 'mcr.microsoft.com/dotnet/aspnet:8.0' + probes: [ + { + type: 'Startup' + httpGet: { + path: '/health' + port: 5000 + scheme: 'HTTP' + } + initialDelaySeconds: 5 + periodSeconds: 10 + failureThreshold: 3 + timeoutSeconds: 5 + } + { + type: 'Liveness' + httpGet: { + path: '/health' + port: 5000 + scheme: 'HTTP' + } + initialDelaySeconds: 10 + periodSeconds: 30 + failureThreshold: 3 + timeoutSeconds: 5 + } + ] +} : { + enableIngress: false // Worker doesn't need ingress + external: false + targetPort: 0 // Not needed for worker + containerImage: 'mcr.microsoft.com/dotnet/runtime:8.0' + probes: [] // No probes for worker +} + +module containerAppsApp '../core/host/container-app.bicep' = { + name: 'container-apps-${serviceName}' + params: { + name: appName + containerAppsEnvironmentName: containerAppsEnvironmentName + containerRegistryName: containerRegistryName + location: location + tags: union(tags, { 'azd-service-name': serviceName }) + enableIngress: serviceConfig.enableIngress + external: serviceConfig.external + targetPort: serviceConfig.targetPort + containerImage: serviceConfig.containerImage + identityName: identityName + minReplicas: 1 + maxReplicas: 10 + environmentVariables: serviceName == 'worker' ? [ + { + name: 'AZURE_MANAGED_IDENTITY_CLIENT_ID' + secretRef: 'azure-managed-identity-client-id' + } + { + name: 'AZURE_CLIENT_ID' + value: userAssignedManagedIdentity.clientId + } + { + name: 'ENDPOINT' + value: 'Endpoint=${dtsEndpoint};Authentication=ManagedIdentity;ClientID=${userAssignedManagedIdentity.clientId}' + } + { + name: 'TASKHUB' + value: taskHubName + } + { + name: 'AGENT_CONNECTION_STRING' + value: !empty(AGENT_CONNECTION_STRING) ? AGENT_CONNECTION_STRING : !empty(agentConnectionString) ? agentConnectionString : '' + } + { + name: 'OPENAI_DEPLOYMENT_NAME' + value: !empty(OPENAI_DEPLOYMENT_NAME) ? OPENAI_DEPLOYMENT_NAME : !empty(openAiDeploymentName) ? openAiDeploymentName : 'gpt-4o-mini' + } + { + name: 'AGENT_CONNECTION_STRING__clientId' + value: !empty(AGENT_CONNECTION_STRING__clientId) ? AGENT_CONNECTION_STRING__clientId : userAssignedManagedIdentity.clientId + } + { + name: 'AZURE_OPENAI_ENDPOINT' + value: !empty(openAiEndpoint) ? openAiEndpoint : '' + } + { + name: 'OPENAI_API_KEY' + value: !empty(openAiApiKey) ? openAiApiKey : '' + } + { + name: 'DALLE_ENDPOINT' + value: DALLE_ENDPOINT + } + { + name: 'CLIENT_BASE_URL' + value: clientBaseUrl + } + { + name: 'CLIENT_BASE_URL' + value: clientBaseUrl + } + ] : [ + { + name: 'AZURE_MANAGED_IDENTITY_CLIENT_ID' + secretRef: 'azure-managed-identity-client-id' + } + { + name: 'AZURE_CLIENT_ID' + value: userAssignedManagedIdentity.clientId + } + { + name: 'ENDPOINT' + value: 'Endpoint=${dtsEndpoint};Authentication=ManagedIdentity;ClientID=${userAssignedManagedIdentity.clientId}' + } + { + name: 'TASKHUB' + value: taskHubName + } + { + name: 'AGENT_CONNECTION_STRING' + value: !empty(AGENT_CONNECTION_STRING) ? AGENT_CONNECTION_STRING : !empty(agentConnectionString) ? agentConnectionString : '' + } + { + name: 'OPENAI_DEPLOYMENT_NAME' + value: !empty(OPENAI_DEPLOYMENT_NAME) ? OPENAI_DEPLOYMENT_NAME : !empty(openAiDeploymentName) ? openAiDeploymentName : 'gpt-4o-mini' + } + { + name: 'AGENT_CONNECTION_STRING__clientId' + value: !empty(AGENT_CONNECTION_STRING__clientId) ? AGENT_CONNECTION_STRING__clientId : userAssignedManagedIdentity.clientId + } + ] + secrets: [ + { + name: 'azure-managed-identity-client-id' + value: userAssignedManagedIdentity.clientId + } + ] + enableCustomScaleRule: false // Disable custom scale rule for initial deployment + scaleRuleName: 'dtsscaler-orchestration' + scaleRuleType: 'azure-durabletask-scheduler' + scaleRuleMetadata: { + endpoint: dtsEndpoint + maxConcurrentWorkItemsCount: '1' + taskhubName: taskHubName + workItemType: 'Orchestration' + } + scaleRuleIdentity: userAssignedManagedIdentity.resourceId + probes: serviceConfig.probes + } +} + +output endpoint string = containerAppsApp.outputs.containerAppFqdn +output envName string = appName diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/dts.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/dts.bicep new file mode 100644 index 0000000..c435832 --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/dts.bicep @@ -0,0 +1,29 @@ +param ipAllowlist array +param location string +param tags object = {} +param name string +param taskhubname string +param skuName string +param skuCapacity int + +resource dts 'Microsoft.DurableTask/schedulers@2024-10-01-preview' = { + location: location + tags: tags + name: name + properties: { + ipAllowlist: ipAllowlist + sku: { + name: skuName + capacity: skuCapacity + } + } +} + +resource taskhub 'Microsoft.DurableTask/schedulers/taskhubs@2024-10-01-preview' = { + parent: dts + name: taskhubname +} + +output dts_NAME string = dts.name +output dts_URL string = dts.properties.endpoint +output TASKHUB_NAME string = taskhub.name diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/openai-access.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/openai-access.bicep new file mode 100644 index 0000000..e69de29 diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/registry-access.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/registry-access.bicep new file mode 100644 index 0000000..2acd04e --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/registry-access.bicep @@ -0,0 +1,27 @@ +@description('The name of the Container Registry') +param containerRegistryName string + +@description('The Principal ID of the identity that needs access to the registry') +param principalID string + +@description('The type of the principal (User, ServicePrincipal, etc.)') +param principalType string = 'ServicePrincipal' + +// AcrPull role definition ID: 7f951dda-4ed3-4680-a7ca-43fe172d538d +var acrPullRoleDefinitionId = '7f951dda-4ed3-4680-a7ca-43fe172d538d' + +resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-11-01-preview' existing = { + name: containerRegistryName +} + +resource acrPullRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(containerRegistry.id, principalID, acrPullRoleDefinitionId) + scope: containerRegistry + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', acrPullRoleDefinitionId) + principalId: principalID + principalType: principalType + } +} + +output roleAssignmentId string = acrPullRoleAssignment.id diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/storage-Access.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/storage-Access.bicep new file mode 100644 index 0000000..2422be4 --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/storage-Access.bicep @@ -0,0 +1,30 @@ +// storage-Access.bicep +// Assigns role to a principal on a storage account + +@description('The name of the storage account') +param storageAccountName string + +@description('The principal ID to assign the role to') +param principalID string + +@description('The role definition ID to assign') +param roleDefinitionID string + +@description('The type of the principal (User, Group, ServicePrincipal)') +param principalType string + +resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' existing = { + name: storageAccountName +} + +resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(storageAccount.id, principalID, roleDefinitionID) + scope: storageAccount + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionID) + principalId: principalID + principalType: principalType + } +} + +output roleAssignmentId string = roleAssignment.id diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/user-assigned-identity.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/user-assigned-identity.bicep new file mode 100644 index 0000000..0583ab8 --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/user-assigned-identity.bicep @@ -0,0 +1,17 @@ +metadata description = 'Creates a Microsoft Entra user-assigned identity.' + +param name string +param location string = resourceGroup().location +param tags object = {} + +resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: name + location: location + tags: tags +} + +output name string = identity.name +output resourceId string = identity.id +output principalId string = identity.properties.principalId +output clientId string = identity.properties.clientId +output tenantId string = identity.properties.tenantId diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/user-registry-access.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/user-registry-access.bicep new file mode 100644 index 0000000..1ab4dfd --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/user-registry-access.bicep @@ -0,0 +1,26 @@ +@description('The name of the Container Registry') +param containerRegistryName string + +@description('The user principal ID that needs push access to the registry') +param userPrincipalID string + +@description('The type of the principal (usually User)') +param principalType string = 'User' + +// AcrPush role definition ID: 8311e382-0749-4cb8-b61a-304f252e45ec +var acrPushRoleDefinitionId = '8311e382-0749-4cb8-b61a-304f252e45ec' + +resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-11-01-preview' existing = { + name: containerRegistryName +} + +// Only deploy if skip parameter is false +resource acrPushRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(containerRegistry.id, userPrincipalID, acrPushRoleDefinitionId) + scope: containerRegistry + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', acrPushRoleDefinitionId) + principalId: userPrincipalID + principalType: principalType + } +} diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/ai/ai-hub.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/ai/ai-hub.bicep new file mode 100644 index 0000000..e69de29 diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/ai/ai-project.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/ai/ai-project.bicep new file mode 100644 index 0000000..a926943 --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/ai/ai-project.bicep @@ -0,0 +1,100 @@ +// This file defines an Azure AI Project resource and capability host for agents + +@description('Name of the AI Project') +param name string + +@description('Azure region for the project') +param location string = resourceGroup().location + +@description('Project SKU') +param sku object = { + name: 'Free' + tier: 'Free' +} + +@description('Tags for the resource') +param tags object = {} + +@description('The Azure OpenAI resource ID to connect to this project') +param openAiResourceId string + +@description('The Azure OpenAI resource name for connections') +param openAiName string + +@description('The Hub resource ID to associate with this project') +param hubResourceId string + +@description('If a Managed Service Identity for this AI Project should be created') +param managedIdentity bool = true + +// For creating proper connection strings +var subscriptionId = subscription().subscriptionId +var resourceGroupName = resourceGroup().name +// Original semicolon format - kept for reference +var standardConnectionString = '${location}.api.azureml.ms;${subscriptionId};${resourceGroupName};${name}' +// URL format connection string that matches the SDK requirements +// Using standard Azure AI Project endpoint format +var projectConnectionString = 'https://${location}.aiprojects.azure.com/api/projects/${resourceGroupName}/${name}' + +var capabilityHostName = 'agent-host' + +// Define the necessary connections for the capability host +var storageConnections = ['${name}/workspaceblobstore'] +var aiServiceConnections = ['${openAiName}-connection'] +var vectorStoreConnections = ['${name}/default-vectorstore'] // Required non-empty value + +// Create the AI Project with the proper hub association and capability host +resource aiProject 'Microsoft.MachineLearningServices/workspaces@2023-08-01-preview' = { + name: name + location: location + tags: union(tags, { + ProjectConnectionString: projectConnectionString + StandardConnectionString: standardConnectionString + OpenAiResourceId: openAiResourceId + }) + identity: { + type: managedIdentity ? 'SystemAssigned' : 'None' + } + sku: sku + kind: 'project' + properties: { + friendlyName: 'Agent Chaining AI Project' + description: 'AI Project for the agent chaining sample' + hubResourceId: hubResourceId + publicNetworkAccess: 'Enabled' + // Do not reference storage account directly for projects + // Storage is managed by the Hub + } + + // Create the capability host for agents - follows the pattern from Travel Plan Orchestrator example + resource capabilityHost 'capabilityHosts@2024-10-01-preview' = { + name: '${name}-${capabilityHostName}' + properties: { + capabilityHostKind: 'Agents' + aiServicesConnections: aiServiceConnections + vectorStoreConnections: vectorStoreConnections // Using non-empty vector store connection + storageConnections: storageConnections + } + } +} + +// End of aiProject resource + +// Add role assignment for the AI Project to access storage +resource storageRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (managedIdentity) { + name: guid(resourceGroup().id, aiProject.id, 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') + scope: resourceGroup() + properties: { + principalId: aiProject.identity.principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') // Storage Blob Data Contributor + principalType: 'ServicePrincipal' + } +} + +// Output the project ID and information +output id string = aiProject.id +output name string = aiProject.name +output projectConnectionString string = projectConnectionString +output principalId string = managedIdentity ? aiProject.identity.principalId : '' +output projectResourceId string = aiProject.id +output capabilityHostName string = '${name}-${capabilityHostName}' diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/ai/ai-role-assignments.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/ai/ai-role-assignments.bicep new file mode 100644 index 0000000..565ecbe --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/ai/ai-role-assignments.bicep @@ -0,0 +1,26 @@ +// ai-role-assignments.bicep - Role assignments for AI services + +@description('The principal ID to assign roles to') +param principalId string + +@description('The ID of the Azure AI Project') +param aiProjectId string + +@description('The type of the principal (ServicePrincipal, User, Group, etc)') +param principalType string = 'ServicePrincipal' + +// AI Project Contributor role definition ID +var aiProjectContributorRoleId = '1affc506-2bb4-4bbd-86a8-804c7c9d7a99' // Azure AI Project Contributor + +// Create role assignment for AI Project Contributor +resource aiProjectContributorRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(aiProjectId, principalId, aiProjectContributorRoleId) + scope: resourceGroup() + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', aiProjectContributorRoleId) + principalId: principalId + principalType: principalType + } +} + +output aiProjectRoleAssignmentId string = aiProjectContributorRoleAssignment.id diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/ai/hub.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/ai/hub.bicep new file mode 100644 index 0000000..e69de29 diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/ai/standard-ai-project.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/ai/standard-ai-project.bicep new file mode 100644 index 0000000..b2b594b --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/ai/standard-ai-project.bicep @@ -0,0 +1,83 @@ +// standard-ai-project.bicep +// This file defines an Azure AI Project resource and capability host for agents +// Based on the Travel Plan Orchestrator example + +@description('Azure region of the deployment') +param location string + +@description('Tags to add to the resources') +param tags object = {} + +@description('AI Project name') +param aiProjectName string + +@description('AI Project display name') +param aiProjectFriendlyName string = aiProjectName + +@description('AI Project description') +param aiProjectDescription string = 'AI Project for agent chaining functionality' + +@description('Resource ID of the AI Hub resource') +param aiHubId string + +@description('Name for capabilityHost') +param capabilityHostName string = 'agent-host' + +@description('Name for OpenAI connection') +param aoaiConnectionName string = 'openai-connection' + +// For constructing endpoint +var subscriptionId = subscription().subscriptionId +var resourceGroupName = resourceGroup().name +// Original semicolon format - kept for reference/compatibility +var standardConnectionString = '${location}.api.azureml.ms;${subscriptionId};${resourceGroupName};${aiProjectName}' +// URL format connection string that matches the SDK requirements +// Using standard Azure AI Project endpoint format +var projectConnectionString = 'https://${location}.aiprojects.azure.com/api/projects/${resourceGroupName}/${aiProjectName}' + +// Define storage connections exactly as in the example +var storageConnections = ['${aiProjectName}/workspaceblobstore'] + +// Define a dummy vector store connection as required by the API +var vectorStoreConnections = ['dummy-vectorstore'] + +// Define AI services connections - must match the connection name from the hub +// This is critical - must match exactly the connection name defined in the AI Hub +var aiServiceConnections = [aoaiConnectionName] + +resource aiProject 'Microsoft.MachineLearningServices/workspaces@2023-08-01-preview' = { + name: aiProjectName + location: location + tags: union(tags, { + ProjectConnectionString: projectConnectionString + StandardConnectionString: standardConnectionString + }) + identity: { + type: 'SystemAssigned' + } + properties: { + // organization + friendlyName: aiProjectFriendlyName + description: aiProjectDescription + + // dependent resources + hubResourceId: aiHubId + } + kind: 'project' + + // Resource definition for the capability host + resource capabilityHost 'capabilityHosts@2024-10-01-preview' = { + name: '${aiProjectName}-${capabilityHostName}' + properties: { + capabilityHostKind: 'Agents' + aiServicesConnections: aiServiceConnections + vectorStoreConnections: vectorStoreConnections + storageConnections: storageConnections + } + } +} + +output aiProjectName string = aiProject.name +output aiProjectResourceId string = aiProject.id +output aiProjectPrincipalId string = aiProject.identity.principalId +output projectConnectionString string = projectConnectionString diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/host/appservice-appsettings.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/host/appservice-appsettings.bicep new file mode 100644 index 0000000..cfb405c --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/host/appservice-appsettings.bicep @@ -0,0 +1,3 @@ +param appSettings array = [] + +output appSettings array = appSettings diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/host/appservice-plan.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/host/appservice-plan.bicep new file mode 100644 index 0000000..46ea1fb --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/host/appservice-plan.bicep @@ -0,0 +1,22 @@ +param name string +param location string +param tags object = {} +param sku object = { + name: 'EP1' + tier: 'ElasticPremium' +} + +module appServicePlan 'br/public:avm/res/web/serverfarm:0.4.1' = { + name: 'serverfarmDeployment' + params: { + name: name + location: location + tags: tags + skuName: sku.name + kind: 'linux' + zoneRedundant: false + } +} + +output id string = appServicePlan.outputs.resourceId +output name string = appServicePlan.outputs.name diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/host/container-app.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/host/container-app.bicep new file mode 100644 index 0000000..9682994 --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/host/container-app.bicep @@ -0,0 +1,150 @@ +// container-app.bicep - Creates a Container App resource in a Container Apps Environment + +@description('The name of the container app') +param name string + +@description('The location for the resources') +param location string = resourceGroup().location + +@description('Tags for the resources') +param tags object = {} + +@description('The name of the container apps environment') +param containerAppsEnvironmentName string + +@description('The ID of the container registry') +param containerRegistryName string = '' + +@description('The name of the user-assigned managed identity') +param identityName string = '' + +@description('The container image to deploy') +param containerImage string + +@description('Target port for the container') +param targetPort int = 80 + +@description('Environment variables for the container') +param environmentVariables array = [] + +@description('CPU resources for the container') +param containerCpu string = '0.5' + +@description('Memory resources for the container') +param containerMemory string = '1.0Gi' + +@description('Minimum number of replicas') +param minReplicas int = 1 + +@description('Maximum number of replicas') +param maxReplicas int = 10 + +@description('Enable ingress for the container app') +param enableIngress bool = true + +@description('Additional secrets to be set on the container app') +param secrets array = [] + +@description('Enable custom scale rules') +param enableCustomScaleRule bool = false + +@description('Scale rule name') +param scaleRuleName string = '' + +@description('Scale rule type') +param scaleRuleType string = '' + +@description('Scale rule metadata') +param scaleRuleMetadata object = {} + +@description('Scale rule identity') +param scaleRuleIdentity string = '' + +@description('Make the container app visible externally') +param external bool = true + +@description('Probes to configure for the container app') +param probes array = [] + +var hasIdentity = !empty(identityName) +var hasRegistry = !empty(containerRegistryName) + +resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-11-02-preview' existing = { + name: containerAppsEnvironmentName +} + +resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = if (hasIdentity) { + name: identityName +} + +resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-11-01-preview' existing = if (hasRegistry) { + name: containerRegistryName +} + +resource containerApp 'Microsoft.App/containerApps@2023-11-02-preview' = { + name: name + location: location + tags: tags + identity: hasIdentity ? { + type: 'UserAssigned' + userAssignedIdentities: { + '${identity.id}': {} + } + } : null + properties: { + managedEnvironmentId: containerAppsEnvironment.id + configuration: { + ingress: enableIngress ? { + external: external + targetPort: targetPort + transport: 'auto' + allowInsecure: false + } : null + registries: hasRegistry ? [ + { + #disable-next-line BCP318 + server: containerRegistry.properties.loginServer + identity: hasIdentity ? identity.id : null + } + ] : [] + secrets: secrets + activeRevisionsMode: 'Single' + } + template: { + containers: [ + { + name: name + image: containerImage + env: environmentVariables + resources: { + cpu: json(containerCpu) + memory: containerMemory + } + probes: !empty(probes) ? probes : [] + } + ] + scale: { + minReplicas: minReplicas + maxReplicas: maxReplicas + rules: enableCustomScaleRule ? [ + { + name: scaleRuleName + custom: { + type: scaleRuleType + metadata: scaleRuleMetadata + auth: !empty(scaleRuleIdentity) ? [ + { + secretRef: 'scale-rule-auth' + triggerParameter: 'userAssignedIdentity' + } + ] : [] + } + } + ] : [] + } + } + } +} + +output containerAppId string = containerApp.id +output containerAppFqdn string = enableIngress && external ? containerApp.properties.configuration.ingress.fqdn : '' diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/host/container-apps-environment.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/host/container-apps-environment.bicep new file mode 100644 index 0000000..2eb8e4e --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/host/container-apps-environment.bicep @@ -0,0 +1,45 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +@description('Log Analytics workspace resource ID') +param logAnalyticsWorkspaceId string + +@description('Virtual network resource ID') +param vnetId string = '' + +@description('Subnet name for infrastructure components') +param infrastructureSubnetName string = '' + +@description('Existing subnet ID for infrastructure resources. If not specified, a delegated subnet will be created.') +param infrastructureSubnetId string = '' + +@description('Whether to create the Container Apps Environment in an internal or external network. Default is external.') +param internal bool = false + +// Use existing subnet if provided, otherwise reference it from the VNET +var subnetId = !empty(infrastructureSubnetId) ? infrastructureSubnetId : !empty(vnetId) && !empty(infrastructureSubnetName) ? '${vnetId}/subnets/${infrastructureSubnetName}' : '' + +resource environment 'Microsoft.App/managedEnvironments@2023-05-01' = { + name: name + location: location + tags: tags + properties: { + appLogsConfiguration: { + destination: 'log-analytics' + logAnalyticsConfiguration: { + customerId: reference(logAnalyticsWorkspaceId, '2021-06-01').customerId + sharedKey: listKeys(logAnalyticsWorkspaceId, '2021-06-01').primarySharedKey + } + } + vnetConfiguration: !empty(subnetId) ? { + infrastructureSubnetId: subnetId + internal: internal + } : null + } +} + +output id string = environment.id +output name string = environment.name +output defaultDomain string = environment.properties.defaultDomain +output staticIp string = environment.properties.staticIp diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/host/container-apps.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/host/container-apps.bicep new file mode 100644 index 0000000..e4bbf89 --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/host/container-apps.bicep @@ -0,0 +1,80 @@ +// container-apps.bicep - Create container apps environment and container registry + +@description('The name prefix for resources') +param name string = 'app' + +@description('The name of the container apps environment') +param containerAppsEnvironmentName string + +@description('The name of the container registry') +param containerRegistryName string + +@description('The location of the container apps environment') +param location string = resourceGroup().location + +@description('Tags for the resources') +param tags object = {} + +@description('Whether the container apps environment should be internal') +param internal bool = false + +@description('Resource ID of the virtual network subnet to use for the container apps environment') +param infrastructureSubnetId string = '' + +@description('Virtual network resource ID') +param vnetId string = '' + +@description('Subnet name for infrastructure components') +param infrastructureSubnetName string = '' + +// Create a Log Analytics workspace for the Container Apps environment +resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { + name: '${name}-logs' + location: location + tags: tags + properties: { + sku: { + name: 'PerGB2018' + } + retentionInDays: 30 + features: { + enableLogAccessUsingOnlyResourcePermissions: true + } + workspaceCapping: { + dailyQuotaGb: 1 + } + } +} + +// Deploy the Container Apps environment using the module +module containerAppsEnvironment '../host/container-apps-environment.bicep' = { + name: 'container-apps-environment-deploy' + params: { + name: containerAppsEnvironmentName + location: location + tags: tags + logAnalyticsWorkspaceId: logAnalyticsWorkspace.id + infrastructureSubnetId: infrastructureSubnetId + vnetId: vnetId + infrastructureSubnetName: infrastructureSubnetName + internal: internal + } +} + +resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-11-01-preview' = { + name: containerRegistryName + location: location + tags: tags + sku: { + name: 'Basic' + } + properties: { + adminUserEnabled: false + } +} + +// Output necessary properties +output environmentName string = containerAppsEnvironment.outputs.name +output environmentDefaultDomain string = containerAppsEnvironment.outputs.defaultDomain +output registryName string = containerRegistry.name +output registryLoginServer string = containerRegistry.properties.loginServer diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/host/functions-app.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/host/functions-app.bicep new file mode 100644 index 0000000..3e8a28f --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/host/functions-app.bicep @@ -0,0 +1,75 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +// Reference Properties +param applicationInsightsName string = '' +param appServicePlanId string +param storageAccountName string +param virtualNetworkSubnetId string = '' + +@allowed(['SystemAssigned', 'UserAssigned']) +param identityType string + +@description('User assigned identity name') +param identityId string + +param kind string = 'functionapp,linux' + +// Microsoft.Web/sites/config +param appSettings object = {} +param allowedOrigins array = [] + +var linuxFxVersion string = 'DOTNET-ISOLATED|8.0' + +resource storageAccount 'Microsoft.Storage/storageAccounts@2024-01-01' existing = { + name: storageAccountName +} + +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (!empty(applicationInsightsName)) { + name: applicationInsightsName +} + +module functions 'br/public:avm/res/web/site:0.16.0' = { + name: 'siteDeployment' + params: { + name: name + kind: kind + location: location + tags: tags + serverFarmResourceId: appServicePlanId + managedIdentities: { + userAssignedResourceIds: [identityId] + } + siteConfig: { + netFrameworkVersion: 'v8.0' + linuxFxVersion: linuxFxVersion + cors: { + allowedOrigins: union([ 'https://portal.azure.com', 'https://ms.portal.azure.com' ], allowedOrigins) + } + alwaysOn: true + use32BitWorkerProcess: false + ftpsState: 'FtpsOnly' + } + virtualNetworkSubnetId: !empty(virtualNetworkSubnetId) ? virtualNetworkSubnetId : null + configs: [ + { + name: 'appsettings' + properties: union(appSettings, + { + FUNCTIONS_EXTENSION_VERSION: '~4' + FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' + AzureWebJobsStorage__accountName: storageAccount.name + AzureWebJobsStorage__credential : 'managedidentity' + APPLICATIONINSIGHTS_CONNECTION_STRING: applicationInsights.properties.ConnectionString + WEBSITE_USE_PLACEHOLDER_DOTNETISOLATED: '1' + }) + } + ] + } +} + +output name string = functions.outputs.name +output uri string = 'https://${functions.outputs.defaultHostname}' +output identityPrincipalId string = identityType == 'SystemAssigned' ? functions.outputs.systemAssignedMIPrincipalId : '' +output id string = functions.outputs.resourceId diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/identity/user-assigned-identity.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/identity/user-assigned-identity.bicep new file mode 100644 index 0000000..8255649 --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/identity/user-assigned-identity.bicep @@ -0,0 +1,14 @@ +param identityName string +param location string +param tags object = {} + +resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2025-01-31-preview' = { + name: identityName + location: location + tags: tags +} + +output identityId string = userAssignedIdentity.id +output identityName string = userAssignedIdentity.name +output identityPrincipalId string = userAssignedIdentity.properties.principalId +output identityClientId string = userAssignedIdentity.properties.clientId diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/monitor/appinsights-access.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/monitor/appinsights-access.bicep new file mode 100644 index 0000000..1073303 --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/monitor/appinsights-access.bicep @@ -0,0 +1,20 @@ +param principalID string +param roleDefinitionID string +param appInsightsName string + +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = { + name: appInsightsName +} + +// Allow access from API to app insights using a managed identity and least priv role +resource appInsightsRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { + name: guid(applicationInsights.id, principalID, roleDefinitionID) + scope: applicationInsights + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionID) + principalId: principalID + principalType: 'ServicePrincipal' // Workaround for https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments-template#new-service-principal + } +} + +output ROLE_ASSIGNMENT_NAME string = appInsightsRoleAssignment.name diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/monitor/application-insights.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/monitor/application-insights.bicep new file mode 100644 index 0000000..f6d9ee5 --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/monitor/application-insights.bicep @@ -0,0 +1,22 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +param logAnalyticsWorkspaceId string +param disableLocalAuth bool = false + +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { + name: name + location: location + tags: tags + kind: 'web' + properties: { + Application_Type: 'web' + WorkspaceResourceId: logAnalyticsWorkspaceId + DisableLocalAuth: disableLocalAuth + } +} + +output connectionString string = applicationInsights.properties.ConnectionString +output instrumentationKey string = applicationInsights.properties.InstrumentationKey +output name string = applicationInsights.name diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/monitor/loganalytics.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/monitor/loganalytics.bicep new file mode 100644 index 0000000..770544c --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/monitor/loganalytics.bicep @@ -0,0 +1,21 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = { + name: name + location: location + tags: tags + properties: any({ + retentionInDays: 30 + features: { + searchVersion: 1 + } + sku: { + name: 'PerGB2018' + } + }) +} + +output id string = logAnalytics.id +output name string = logAnalytics.name diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/monitor/monitoring.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/monitor/monitoring.bicep new file mode 100644 index 0000000..c6d21bf --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/monitor/monitoring.bicep @@ -0,0 +1,31 @@ +param logAnalyticsName string +param applicationInsightsName string +param location string = resourceGroup().location +param tags object = {} +param disableLocalAuth bool = false + +module logAnalytics 'loganalytics.bicep' = { + name: 'loganalytics' + params: { + name: logAnalyticsName + location: location + tags: tags + } +} + +module applicationInsights 'application-insights.bicep' = { + name: 'applicationinsights' + params: { + name: applicationInsightsName + location: location + tags: tags + logAnalyticsWorkspaceId: logAnalytics.outputs.id + disableLocalAuth: disableLocalAuth + } +} + +output applicationInsightsConnectionString string = applicationInsights.outputs.connectionString +output applicationInsightsInstrumentationKey string = applicationInsights.outputs.instrumentationKey +output applicationInsightsName string = applicationInsights.outputs.name +output logAnalyticsWorkspaceId string = logAnalytics.outputs.id +output logAnalyticsWorkspaceName string = logAnalytics.outputs.name diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/networking/vnet.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/networking/vnet.bicep new file mode 100644 index 0000000..c26d44f --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/networking/vnet.bicep @@ -0,0 +1,57 @@ +// vnet.bicep - Create a virtual network with subnets + +@description('Name of the virtual network') +param name string + +@description('Location for the virtual network') +param location string = resourceGroup().location + +@description('Tags for the resources') +param tags object = {} + +@description('Address prefix for the virtual network') +param vnetAddressPrefix string = '10.0.0.0/16' + +@description('Address prefix for the infrastructure subnet') +param infrastructureSubnetPrefix string = '10.0.0.0/23' + +@description('Address prefix for the app subnet') +param appSubnetPrefix string = '10.0.2.0/23' + +var infrastructureSubnetName = 'infrastructure-subnet' +var appSubnetName = 'app-subnet' + +resource vnet 'Microsoft.Network/virtualNetworks@2023-05-01' = { + name: name + location: location + tags: tags + properties: { + addressSpace: { + addressPrefixes: [ + vnetAddressPrefix + ] + } + subnets: [ + { + name: infrastructureSubnetName + properties: { + addressPrefix: infrastructureSubnetPrefix + // Don't pre-delegate the subnet - Container Apps will handle this + } + } + { + name: appSubnetName + properties: { + addressPrefix: appSubnetPrefix + } + } + ] + } +} + +output vnetId string = vnet.id +output vnetName string = vnet.name +output infrastructureSubnetName string = infrastructureSubnetName +output infrastructureSubnetId string = resourceId('Microsoft.Network/virtualNetworks/subnets', name, infrastructureSubnetName) +output appSubnetName string = appSubnetName +output appSubnetId string = resourceId('Microsoft.Network/virtualNetworks/subnets', name, appSubnetName) diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/security/registry-access.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/security/registry-access.bicep new file mode 100644 index 0000000..819681f --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/security/registry-access.bicep @@ -0,0 +1,25 @@ +// registry-access.bicep - Grant ACR Pull access to a principal + +@description('The name of the container registry') +param containerRegistryName string + +@description('The principal ID to assign ACR Pull role to') +param principalId string + +var acrPullRoleDefinitionId = '7f951dda-4ed3-4680-a7ca-43fe172d538d' // AcrPull role + +resource registry 'Microsoft.ContainerRegistry/registries@2023-11-01-preview' existing = { + name: containerRegistryName +} + +resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(registry.id, principalId, acrPullRoleDefinitionId) + scope: registry + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', acrPullRoleDefinitionId) + principalType: 'ServicePrincipal' + } +} + +output roleAssignmentId string = roleAssignment.id diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/security/role.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/security/role.bicep new file mode 100644 index 0000000..e7658a6 --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/security/role.bicep @@ -0,0 +1,21 @@ +// role.bicep - Assign a role to a principal on a resource + +@description('The principal ID to assign the role to') +param principalId string + +@description('The role definition ID to assign') +param roleDefinitionId string + +@description('The type of the principal (User, Group, ServicePrincipal)') +param principalType string = 'ServicePrincipal' + +resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(resourceGroup().id, principalId, roleDefinitionId) + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionId) + principalId: principalId + principalType: principalType + } +} + +output roleAssignmentId string = roleAssignment.id diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/storage/storage-account.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/storage/storage-account.bicep new file mode 100644 index 0000000..85e651d --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/core/storage/storage-account.bicep @@ -0,0 +1,48 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +param allowBlobPublicAccess bool = false +param containers array = [] +param kind string = 'StorageV2' +param minimumTlsVersion string = 'TLS1_2' +param sku object = { name: 'Standard_LRS' } +param networkAcls object = { + bypass: 'AzureServices' + defaultAction: 'Allow' +} + +resource storage 'Microsoft.Storage/storageAccounts@2023-05-01' = { + name: name + location: location + tags: tags + kind: kind + sku: sku + properties: { + minimumTlsVersion: minimumTlsVersion + allowBlobPublicAccess: allowBlobPublicAccess + allowSharedKeyAccess: false + networkAcls: networkAcls + } + + resource blobServices 'blobServices' = if (!empty(containers)) { + name: 'default' + resource container 'containers' = [for container in containers: { + name: container.name + properties: { + publicAccess: container.?publicAccess ?? 'None' + } + }] + } + + resource queueServices 'queueServices' = { + name: 'default' + resource queue 'queues' = [for queueName in ['input', 'output']: { + name: queueName + }] + } +} + +output name string = storage.name +output primaryEndpoints object = storage.properties.primaryEndpoints +output id string = storage.id diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/main.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/main.bicep new file mode 100644 index 0000000..1668509 --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/main.bicep @@ -0,0 +1,429 @@ +targetScope = 'subscription' + +// The main bicep module to provision Azure resources. +// For a more complete walkthrough to understand how this file works with azd, +// see https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/make-azd-compatible?pivots=azd-create + +// For a more complete walkthrough to understand how this file works with azd, +// see https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/make-azd-compatible?pivots=azd-create + +@minLength(1) +@maxLength(64) +@description('Name of the the environment which is used to generate a short unique hash used in all resources.') +param environmentName string + +@minLength(1) +@description('Primary location for all resources') +param location string + +@description('Id of the user identity to be used for testing and debugging. This is not required in production. Leave empty if not needed.') +param principalId string = deployer().objectId + +@description('Name for the AI project resources.') +param aiProjectName string = 'project-demo' + +@description('Friendly name for your Azure AI resource') +param aiProjectFriendlyName string = 'Agents Project resource' + +@description('Description of your Azure AI resource displayed in AI studio') +param aiProjectDescription string = 'This is an example AI Project resource for use in Azure AI Studio.' + +@description('Name of the Azure AI Search account') +param aiSearchName string = 'agent-ai-search' + +@description('Name for capabilityHost.') +param accountCapabilityHostName string = 'caphostacc' + +@description('Name for capabilityHost.') +param projectCapabilityHostName string = 'caphostproj' + +@description('Name of the Azure AI Services account') +param aiServicesName string = 'agent-ai-services' + +@description('Model name for deployment') +param modelName string = 'gpt-4.1-mini' + +@description('Model format for deployment') +param modelFormat string = 'OpenAI' + +@description('Model version for deployment') +param modelVersion string = '2025-04-14' + +@description('Model deployment SKU name') +param modelSkuName string = 'GlobalStandard' + +@description('Model deployment capacity') +param modelCapacity int = 50 + +@description('Name of the Cosmos DB account for agent thread storage') +param cosmosDbName string = 'agent-ai-cosmos' + +@description('The AI Service Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created.') +param aiServiceAccountResourceId string = '' + +@description('The Ai Search Service full ARM Resource ID. This is an optional field, and if not provided, the resource will be created.') +param aiSearchServiceResourceId string = '' + +@description('The Ai Storage Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created.') +param aiStorageAccountResourceId string = '' + +@description('The Cosmos DB Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created.') +param aiCosmosDbAccountResourceId string = '' + +var projectName = toLower('${aiProjectName}') +param vnetEnabled bool + +param containerAppsEnvName string = '' +param containerAppsAppName string = '' +param containerRegistryName string = '' +param dtsLocation string = 'centralus' +param dtsSkuName string = 'Dedicated' +param dtsCapacity int = 1 +param dtsName string = '' +param taskHubName string = '' +var deploymentStorageContainerName = 'app-package-${take(workerServiceName, 32)}-${take(toLower(uniqueString(workerServiceName, resourceToken)), 7)}' +param storageAccountName string = '' +param clientsServiceName string = 'client' +param workerServiceName string = 'worker' +// Create a short, unique suffix, that will be unique to each resource group +var uniqueSuffix = toLower(uniqueString(subscription().id, environmentName, location)) + +// Optional parameters to override the default azd resource naming conventions. +// Add the following to main.parameters.json to provide values: +// "resourceGroupName": { +// "value": "myGroupName" +// } +param resourceGroupName string = '' + +var abbrs = loadJsonContent('./abbreviations.json') + +// tags that should be applied to all resources. +var tags = { + // Tag all resources with the environment name. + 'azd-env-name': environmentName +} + +// Generate a unique token to be used in naming resources. +// Remove linter suppression after using. +#disable-next-line no-unused-vars +var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) + +// Organize resources in a resource group +resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: !empty(resourceGroupName) ? resourceGroupName : '${abbrs.resourcesResourceGroups}${environmentName}' + location: location + tags: tags +} + +// Add resources to be provisioned below. +// A full example that leverages azd bicep modules can be seen in the todo-python-mongo template: +// https://github.com/Azure-Samples/todo-python-mongo/tree/main/infra + +// Create a user assigned identity +module identity './app/user-assigned-identity.bicep' = { + name: 'identity' + scope: rg + params: { + name: 'dts-ca-identity' + } +} + +module identityAssignDTS './core/security/role.bicep' = { + name: 'identityAssignDTS' + scope: rg + params: { + principalId: identity.outputs.principalId + roleDefinitionId: '0ad04412-c4d5-4796-b79c-f76d14c8d402' + principalType: 'ServicePrincipal' + } +} + +module identityAssignDTSDash './core/security/role.bicep' = { + name: 'identityAssignDTSDash' + scope: rg + params: { + principalId: principalId + roleDefinitionId: '0ad04412-c4d5-4796-b79c-f76d14c8d402' + principalType: 'User' + } +} + +// Create virtual network with subnets for Container Apps +module vnet './core/networking/vnet.bicep' = { + name: 'vnet' + scope: rg + params: { + name: '${abbrs.networkVirtualNetworks}${resourceToken}' + location: location + tags: tags + } +} + +// Container apps env and registry +module containerAppsEnv './core/host/container-apps.bicep' = { + name: 'container-apps' + scope: rg + params: { + name: 'app' + containerAppsEnvironmentName: !empty(containerAppsEnvName) ? containerAppsEnvName : '${abbrs.appManagedEnvironments}${resourceToken}' + containerRegistryName: !empty(containerRegistryName) ? containerRegistryName : '${abbrs.containerRegistryRegistries}${resourceToken}' + location: location + tags: tags + internal: false + vnetId: vnet.outputs.vnetId + infrastructureSubnetName: 'infrastructure-subnet' + infrastructureSubnetId: vnet.outputs.infrastructureSubnetId + } +} + +module dts './app/dts.bicep' = { + scope: rg + name: 'dtsResource' + params: { + name: !empty(dtsName) ? dtsName : '${abbrs.dts}${resourceToken}' + taskhubname: !empty(taskHubName) ? taskHubName : '${abbrs.taskhub}${resourceToken}' + location: dtsLocation + tags: tags + ipAllowlist: [ + '0.0.0.0/0' + ] + skuName: dtsSkuName + skuCapacity: dtsCapacity + } +} + + +// Client registry access must be deployed before client container app +module clientRegistryAccess 'app/registry-access.bicep' = { + name: 'client-registry-access' + scope: rg + params: { + containerRegistryName: containerAppsEnv.outputs.registryName + principalID: identity.outputs.principalId + principalType: 'ServicePrincipal' + } +} + +// Container app +module client 'app/app.bicep' = { + name: clientsServiceName + scope: rg + params: { + appName: !empty(containerAppsAppName) ? '${containerAppsAppName}-client' : '${abbrs.appContainerApps}${resourceToken}-client' + containerAppsEnvironmentName: containerAppsEnv.outputs.environmentName + containerRegistryName: containerAppsEnv.outputs.registryName + userAssignedManagedIdentity: { + resourceId: identity.outputs.resourceId + clientId: identity.outputs.clientId + } + location: location + tags: tags + serviceName: 'client' + identityName: identity.outputs.name + dtsEndpoint: dts.outputs.dts_URL + taskHubName: dts.outputs.TASKHUB_NAME + AGENT_CONNECTION_STRING: aiProject.outputs.projectEndpoint + OPENAI_DEPLOYMENT_NAME: modelName + AGENT_CONNECTION_STRING__clientId: identity.outputs.clientId + clientBaseUrl: '' // Not used by client + } + dependsOn: [ + clientRegistryAccess // Make sure registry access is set up before deploying the app + ] +} + +// Give the worker access to ACR +module workerRegistryAccess 'app/registry-access.bicep' = { + name: 'worker-registry-access' + scope: rg + params: { + containerRegistryName: containerAppsEnv.outputs.registryName + principalID: identity.outputs.principalId + principalType: 'ServicePrincipal' + } +} + + // Container app +module worker 'app/app.bicep' = { + name: workerServiceName + scope: rg + params: { + appName: !empty(containerAppsAppName) ? '${containerAppsAppName}-worker' : '${abbrs.appContainerApps}${resourceToken}-worker' + containerAppsEnvironmentName: containerAppsEnv.outputs.environmentName + containerRegistryName: containerAppsEnv.outputs.registryName + userAssignedManagedIdentity: { + resourceId: identity.outputs.resourceId + clientId: identity.outputs.clientId + } + location: location + tags: tags + serviceName: 'worker' + identityName: identity.outputs.name + dtsEndpoint: dts.outputs.dts_URL + taskHubName: dts.outputs.TASKHUB_NAME + // Use the connection string in URL format for direct client usage + AGENT_CONNECTION_STRING: aiProject.outputs.projectEndpoint + OPENAI_DEPLOYMENT_NAME: modelName + AGENT_CONNECTION_STRING__clientId: identity.outputs.clientId + DALLE_ENDPOINT: aiDependencies.outputs.dalleEndpoint + clientBaseUrl: 'https://${client.outputs.endpoint}' + } +} + +// Dependent resources for the Azure Machine Learning workspace +module aiDependencies './agent/standard-dependent-resources.bicep' = { + name: 'dependencies${projectName}${uniqueSuffix}deployment' + scope: rg + params: { + location: location + storageName: 'stai${uniqueSuffix}' + aiServicesName: '${aiServicesName}${uniqueSuffix}' + aiSearchName: '${aiSearchName}${uniqueSuffix}' + cosmosDbName: '${cosmosDbName}${uniqueSuffix}' + tags: tags + + // Model deployment parameters + modelName: modelName + modelFormat: modelFormat + modelVersion: modelVersion + modelSkuName: modelSkuName + modelCapacity: modelCapacity + modelLocation: location + + aiServiceAccountResourceId: aiServiceAccountResourceId + aiSearchServiceResourceId: aiSearchServiceResourceId + aiStorageAccountResourceId: aiStorageAccountResourceId + aiCosmosDbAccountResourceId: aiCosmosDbAccountResourceId + } +} + +module aiProject './agent/standard-ai-project.bicep' = { + name: '${projectName}${uniqueSuffix}deployment' + scope: rg + params: { + // workspace organization + aiServicesAccountName: aiDependencies.outputs.aiServicesName + aiProjectName: '${projectName}${uniqueSuffix}' + aiProjectFriendlyName: aiProjectFriendlyName + aiProjectDescription: aiProjectDescription + location: location + tags: tags + + // dependent resources + aiSearchName: aiDependencies.outputs.aiSearchName + aiSearchSubscriptionId: aiDependencies.outputs.aiSearchServiceSubscriptionId + aiSearchResourceGroupName: aiDependencies.outputs.aiSearchServiceResourceGroupName + storageAccountName: aiDependencies.outputs.storageAccountName + storageAccountSubscriptionId: aiDependencies.outputs.storageAccountSubscriptionId + storageAccountResourceGroupName: aiDependencies.outputs.storageAccountResourceGroupName + cosmosDbAccountName: aiDependencies.outputs.cosmosDbAccountName + cosmosDbAccountSubscriptionId: aiDependencies.outputs.cosmosDbAccountSubscriptionId + cosmosDbAccountResourceGroupName: aiDependencies.outputs.cosmosDbAccountResourceGroupName + } +} + +module projectRoleAssignments './agent/standard-ai-project-role-assignments.bicep' = { + name: 'aiprojectroleassignments${projectName}${uniqueSuffix}deployment' + scope: rg + params: { + aiProjectPrincipalId: aiProject.outputs.aiProjectPrincipalId + userPrincipalId: principalId + allowUserIdentityPrincipal: true // Enable user identity role assignments + aiServicesName: aiDependencies.outputs.aiServicesName + aiSearchName: aiDependencies.outputs.aiSearchName + aiCosmosDbName: aiDependencies.outputs.cosmosDbAccountName + integrationStorageAccountName: storage.outputs.name + aiStorageAccountName: aiDependencies.outputs.storageAccountName + functionAppManagedIdentityPrincipalId: identity.outputs.principalId + allowFunctionAppIdentityPrincipal: true // Enable function app identity role assignments + dalleAiServicesId: aiDependencies.outputs.dalleAiServicesId + } +} + +module aiProjectCapabilityHost './agent/standard-ai-project-capability-host.bicep' = { + name: 'capabilityhost${projectName}${uniqueSuffix}deployment' + scope: rg + params: { + aiServicesAccountName: aiDependencies.outputs.aiServicesName + projectName: aiProject.outputs.aiProjectName + aiSearchConnection: aiProject.outputs.aiSearchConnection + azureStorageConnection: aiProject.outputs.azureStorageConnection + cosmosDbConnection: aiProject.outputs.cosmosDbConnection + + accountCapHost: '${accountCapabilityHostName}${uniqueSuffix}' + projectCapHost: '${projectCapabilityHostName}${uniqueSuffix}' + } + dependsOn: [ projectRoleAssignments ] +} + +module postCapabilityHostCreationRoleAssignments './agent/post-capability-host-role-assignments.bicep' = { + name: 'postcaphostra${projectName}${uniqueSuffix}deployment' + scope: rg + params: { + aiProjectPrincipalId: aiProject.outputs.aiProjectPrincipalId + aiProjectWorkspaceId: aiProject.outputs.projectWorkspaceId + aiStorageAccountName: aiDependencies.outputs.storageAccountName + cosmosDbAccountName: aiDependencies.outputs.cosmosDbAccountName + } + dependsOn: [ aiProjectCapabilityHost ] +} + +// Backing storage for Azure functions backend API +module storage 'br/public:avm/res/storage/storage-account:0.8.3' = { + name: 'storage' + scope: rg + params: { + name: !empty(storageAccountName) ? storageAccountName : '${abbrs.storageStorageAccounts}${resourceToken}' + allowBlobPublicAccess: false + allowSharedKeyAccess: false // Disable local authentication methods as per policy + dnsEndpointType: 'Standard' + publicNetworkAccess: vnetEnabled ? 'Disabled' : 'Enabled' + // Explicitly disable the property that can't be updated + requireInfrastructureEncryption: false + // When vNet is enabled, restrict access but allow Azure services and specifically grant access to the AI Agent service + networkAcls: vnetEnabled ? { + defaultAction: 'Deny' + bypass: 'AzureServices' // Allow Azure services including AI Agent service + resourceAccessRules: [ + { + tenantId: tenant().tenantId + resourceId: aiDependencies.outputs.aiservicesID // Grant explicit access to AI Agent service + } + ] + } : { + defaultAction: 'Allow' + bypass: 'AzureServices' + } + blobServices: { + containers: [{name: deploymentStorageContainerName}] + } + queueServices: { + queues: [ + { name: 'input' } + { name: 'output' } + ] + } + minimumTlsVersion: 'TLS1_2' // Enforcing TLS 1.2 for better security + location: location + tags: tags + } +} + + +output AZURE_LOCATION string = location +output AZURE_TENANT_ID string = tenant().tenantId +// Container outputs +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerAppsEnv.outputs.registryLoginServer +output AZURE_CONTAINER_REGISTRY_NAME string = containerAppsEnv.outputs.registryName + +// Application outputs +output AZURE_CONTAINER_APP_ENDPOINT string = client.outputs.endpoint +output AZURE_CONTAINER_ENVIRONMENT_NAME string = client.outputs.envName +output DTS_URL string = dts.outputs.dts_URL +output TASKHUB_NAME string = dts.outputs.TASKHUB_NAME + +// AI Project outputs +output AI_PROJECT_NAME string = aiProject.outputs.aiProjectName + +// Identity outputs +output AZURE_USER_ASSIGNED_IDENTITY_NAME string = identity.outputs.name diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/main.json b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/main.json new file mode 100644 index 0000000..7a21100 --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/main.json @@ -0,0 +1,3949 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "5767556487397985243" + } + }, + "parameters": { + "environmentName": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "metadata": { + "description": "Name of the the environment which is used to generate a short unique hash used in all resources." + } + }, + "location": { + "type": "string", + "minLength": 1, + "metadata": { + "description": "Primary location for all resources" + } + }, + "principalId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Id of the user or app to assign application roles" + } + }, + "containerAppsEnvName": { + "type": "string", + "defaultValue": "" + }, + "containerAppsAppName": { + "type": "string", + "defaultValue": "" + }, + "containerRegistryName": { + "type": "string", + "defaultValue": "" + }, + "dtsLocation": { + "type": "string", + "defaultValue": "centralus" + }, + "dtsSkuName": { + "type": "string", + "defaultValue": "Dedicated" + }, + "dtsCapacity": { + "type": "int", + "defaultValue": 1 + }, + "dtsName": { + "type": "string", + "defaultValue": "" + }, + "taskHubName": { + "type": "string", + "defaultValue": "" + }, + "clientsServiceName": { + "type": "string", + "defaultValue": "client" + }, + "workerServiceName": { + "type": "string", + "defaultValue": "worker" + }, + "resourceGroupName": { + "type": "string", + "defaultValue": "" + }, + "name": { + "type": "string", + "defaultValue": "aihub", + "metadata": { + "description": "Base name for AI resources" + } + }, + "projectName": { + "type": "string", + "defaultValue": "aiproj", + "metadata": { + "description": "Name for AI project" + } + }, + "aiHubFriendlyName": { + "type": "string", + "defaultValue": "Agent Chaining Hub", + "metadata": { + "description": "AI Hub friendly name" + } + }, + "aiHubDescription": { + "type": "string", + "defaultValue": "Hub for AI agent capabilities", + "metadata": { + "description": "AI Hub description" + } + }, + "aiProjectFriendlyName": { + "type": "string", + "defaultValue": "Agent Chaining Project", + "metadata": { + "description": "AI Project friendly name" + } + }, + "aiProjectDescription": { + "type": "string", + "defaultValue": "Project for AI agent capabilities", + "metadata": { + "description": "AI Project description" + } + }, + "capabilityHostName": { + "type": "string", + "defaultValue": "agent-host", + "metadata": { + "description": "Capability host name" + } + }, + "modelName": { + "type": "string", + "defaultValue": "gpt-4o-mini" + }, + "modelFormat": { + "type": "string", + "defaultValue": "OpenAI" + }, + "modelVersion": { + "type": "string", + "defaultValue": "2024-07-18" + }, + "modelSkuName": { + "type": "string", + "defaultValue": "Standard" + }, + "modelCapacity": { + "type": "int", + "defaultValue": 20 + }, + "modelLocation": { + "type": "string", + "defaultValue": "[parameters('location')]" + } + }, + "variables": { + "$fxv#0": { + "analysisServicesServers": "as", + "apiManagementService": "apim-", + "appConfigurationStores": "appcs-", + "appManagedEnvironments": "cae-", + "appContainerApps": "ca-", + "authorizationPolicyDefinitions": "policy-", + "automationAutomationAccounts": "aa-", + "blueprintBlueprints": "bp-", + "blueprintBlueprintsArtifacts": "bpa-", + "cacheRedis": "redis-", + "cdnProfiles": "cdnp-", + "cdnProfilesEndpoints": "cdne-", + "cognitiveServicesAccounts": "cog-", + "cognitiveServicesFormRecognizer": "cog-fr-", + "cognitiveServicesTextAnalytics": "cog-ta-", + "cognitiveServicesSpeech": "cog-sp-", + "computeAvailabilitySets": "avail-", + "computeCloudServices": "cld-", + "computeDiskEncryptionSets": "des", + "computeDisks": "disk", + "computeDisksOs": "osdisk", + "computeGalleries": "gal", + "computeSnapshots": "snap-", + "computeVirtualMachines": "vm", + "computeVirtualMachineScaleSets": "vmss-", + "containerInstanceContainerGroups": "ci", + "containerRegistryRegistries": "cr", + "containerServiceManagedClusters": "aks-", + "databricksWorkspaces": "dbw-", + "dataFactoryFactories": "adf-", + "dataLakeAnalyticsAccounts": "dla", + "dataLakeStoreAccounts": "dls", + "dataMigrationServices": "dms-", + "dBforMySQLServers": "mysql-", + "dBforPostgreSQLServers": "psql-", + "devicesIotHubs": "iot-", + "devicesProvisioningServices": "provs-", + "devicesProvisioningServicesCertificates": "pcert-", + "documentDBDatabaseAccounts": "cosmos-", + "eventGridDomains": "evgd-", + "eventGridDomainsTopics": "evgt-", + "eventGridEventSubscriptions": "evgs-", + "eventHubNamespaces": "evhns-", + "eventHubNamespacesEventHubs": "evh-", + "hdInsightClustersHadoop": "hadoop-", + "hdInsightClustersHbase": "hbase-", + "hdInsightClustersKafka": "kafka-", + "hdInsightClustersMl": "mls-", + "hdInsightClustersSpark": "spark-", + "hdInsightClustersStorm": "storm-", + "hybridComputeMachines": "arcs-", + "insightsActionGroups": "ag-", + "insightsComponents": "appi-", + "keyVaultVaults": "kv-", + "kubernetesConnectedClusters": "arck", + "kustoClusters": "dec", + "kustoClustersDatabases": "dedb", + "loadTesting": "lt-", + "logicIntegrationAccounts": "ia-", + "logicWorkflows": "logic-", + "machineLearningServicesWorkspaces": "mlw-", + "managedIdentityUserAssignedIdentities": "id-", + "managementManagementGroups": "mg-", + "migrateAssessmentProjects": "migr-", + "networkApplicationGateways": "agw-", + "networkApplicationSecurityGroups": "asg-", + "networkAzureFirewalls": "afw-", + "networkBastionHosts": "bas-", + "networkConnections": "con-", + "networkDnsZones": "dnsz-", + "networkExpressRouteCircuits": "erc-", + "networkFirewallPolicies": "afwp-", + "networkFirewallPoliciesWebApplication": "waf", + "networkFirewallPoliciesRuleGroups": "wafrg", + "networkFrontDoors": "fd-", + "networkFrontdoorWebApplicationFirewallPolicies": "fdfp-", + "networkLoadBalancersExternal": "lbe-", + "networkLoadBalancersInternal": "lbi-", + "networkLoadBalancersInboundNatRules": "rule-", + "networkLocalNetworkGateways": "lgw-", + "networkNatGateways": "ng-", + "networkNetworkInterfaces": "nic-", + "networkNetworkSecurityGroups": "nsg-", + "networkNetworkSecurityGroupsSecurityRules": "nsgsr-", + "networkNetworkWatchers": "nw-", + "networkPrivateDnsZones": "pdnsz-", + "networkPrivateLinkServices": "pl-", + "networkPublicIPAddresses": "pip-", + "networkPublicIPPrefixes": "ippre-", + "networkRouteFilters": "rf-", + "networkRouteTables": "rt-", + "networkRouteTablesRoutes": "udr-", + "networkTrafficManagerProfiles": "traf-", + "networkVirtualNetworkGateways": "vgw-", + "networkVirtualNetworks": "vnet-", + "networkVirtualNetworksSubnets": "snet-", + "networkVirtualNetworksVirtualNetworkPeerings": "peer-", + "networkVirtualWans": "vwan-", + "networkVpnGateways": "vpng-", + "networkVpnGatewaysVpnConnections": "vcn-", + "networkVpnGatewaysVpnSites": "vst-", + "notificationHubsNamespaces": "ntfns-", + "notificationHubsNamespacesNotificationHubs": "ntf-", + "operationalInsightsWorkspaces": "log-", + "portalDashboards": "dash-", + "powerBIDedicatedCapacities": "pbi-", + "purviewAccounts": "pview-", + "recoveryServicesVaults": "rsv-", + "resourcesResourceGroups": "rg-", + "searchSearchServices": "srch-", + "serviceBusNamespaces": "sb-", + "serviceBusNamespacesQueues": "sbq-", + "serviceBusNamespacesTopics": "sbt-", + "serviceEndPointPolicies": "se-", + "serviceFabricClusters": "sf-", + "signalRServiceSignalR": "sigr", + "sqlManagedInstances": "sqlmi-", + "sqlServers": "sql-", + "sqlServersDataWarehouse": "sqldw-", + "sqlServersDatabases": "sqldb-", + "sqlServersDatabasesStretch": "sqlstrdb-", + "storageStorageAccounts": "st", + "storageStorageAccountsVm": "stvm", + "storSimpleManagers": "ssimp", + "streamAnalyticsCluster": "asa-", + "synapseWorkspaces": "syn", + "synapseWorkspacesAnalyticsWorkspaces": "synw", + "synapseWorkspacesSqlPoolsDedicated": "syndp", + "synapseWorkspacesSqlPoolsSpark": "synsp", + "timeSeriesInsightsEnvironments": "tsi-", + "webServerFarms": "plan-", + "webSitesAppService": "app-", + "webSitesAppServiceEnvironment": "ase-", + "webSitesFunctions": "func-", + "webStaticSites": "stapp-", + "dts": "dts-", + "taskhub": "taskhub-" + }, + "abbrs": "[variables('$fxv#0')]", + "tags": { + "azd-env-name": "[parameters('environmentName')]" + }, + "resourceToken": "[toLower(uniqueString(subscription().id, parameters('environmentName'), parameters('location')))]", + "uniqueSuffix": "[toLower(uniqueString(subscription().id, parameters('environmentName'), parameters('location')))]", + "storageBlobDataContributorRole": "ba92f5b4-2d11-453d-a403-e96b0029c9fe", + "storageQueueDataContributorRoleDefinitionId": "974c5e8b-45b9-4653-ba55-5f855dd0fb88", + "storageTableDataContributorRoleDefinitionId": "0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3" + }, + "resources": [ + { + "type": "Microsoft.Resources/resourceGroups", + "apiVersion": "2021-04-01", + "name": "[if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))]", + "location": "[parameters('location')]", + "tags": "[variables('tags')]" + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "identity", + "resourceGroup": "[if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "dts-ca-identity" + } + }, + "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": "5039160413996154793" + }, + "description": "Creates a Microsoft Entra user-assigned identity." + }, + "parameters": { + "name": { + "type": "string" + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "tags": { + "type": "object", + "defaultValue": {} + } + }, + "resources": [ + { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2023-01-31", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]" + } + ], + "outputs": { + "name": { + "type": "string", + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "value": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('name'))]" + }, + "principalId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('name')), '2023-01-31').principalId]" + }, + "clientId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('name')), '2023-01-31').clientId]" + }, + "tenantId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('name')), '2023-01-31').tenantId]" + } + } + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName'))))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "identityAssignDTS", + "resourceGroup": "[if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "principalId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'identity'), '2022-09-01').outputs.principalId.value]" + }, + "roleDefinitionId": { + "value": "0ad04412-c4d5-4796-b79c-f76d14c8d402" + }, + "principalType": { + "value": "ServicePrincipal" + } + }, + "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": "4856410466654776593" + } + }, + "parameters": { + "principalId": { + "type": "string", + "metadata": { + "description": "The principal ID to assign the role to" + } + }, + "roleDefinitionId": { + "type": "string", + "metadata": { + "description": "The role definition ID to assign" + } + }, + "principalType": { + "type": "string", + "defaultValue": "ServicePrincipal", + "metadata": { + "description": "The type of the principal (User, Group, ServicePrincipal)" + } + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(resourceGroup().id, parameters('principalId'), parameters('roleDefinitionId'))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', parameters('roleDefinitionId'))]", + "principalId": "[parameters('principalId')]", + "principalType": "[parameters('principalType')]" + } + } + ], + "outputs": { + "roleAssignmentId": { + "type": "string", + "value": "[resourceId('Microsoft.Authorization/roleAssignments', guid(resourceGroup().id, parameters('principalId'), parameters('roleDefinitionId')))]" + } + } + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'identity')]", + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName'))))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "identityAssignDTSDash", + "resourceGroup": "[if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "principalId": { + "value": "[parameters('principalId')]" + }, + "roleDefinitionId": { + "value": "0ad04412-c4d5-4796-b79c-f76d14c8d402" + }, + "principalType": { + "value": "User" + } + }, + "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": "4856410466654776593" + } + }, + "parameters": { + "principalId": { + "type": "string", + "metadata": { + "description": "The principal ID to assign the role to" + } + }, + "roleDefinitionId": { + "type": "string", + "metadata": { + "description": "The role definition ID to assign" + } + }, + "principalType": { + "type": "string", + "defaultValue": "ServicePrincipal", + "metadata": { + "description": "The type of the principal (User, Group, ServicePrincipal)" + } + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(resourceGroup().id, parameters('principalId'), parameters('roleDefinitionId'))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', parameters('roleDefinitionId'))]", + "principalId": "[parameters('principalId')]", + "principalType": "[parameters('principalType')]" + } + } + ], + "outputs": { + "roleAssignmentId": { + "type": "string", + "value": "[resourceId('Microsoft.Authorization/roleAssignments', guid(resourceGroup().id, parameters('principalId'), parameters('roleDefinitionId')))]" + } + } + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName'))))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "vnet", + "resourceGroup": "[if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[format('{0}{1}', variables('abbrs').networkVirtualNetworks, variables('resourceToken'))]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[variables('tags')]" + } + }, + "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": "13244175646240278370" + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Name of the virtual network" + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Location for the virtual network" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags for the resources" + } + }, + "vnetAddressPrefix": { + "type": "string", + "defaultValue": "10.0.0.0/16", + "metadata": { + "description": "Address prefix for the virtual network" + } + }, + "infrastructureSubnetPrefix": { + "type": "string", + "defaultValue": "10.0.0.0/23", + "metadata": { + "description": "Address prefix for the infrastructure subnet" + } + }, + "appSubnetPrefix": { + "type": "string", + "defaultValue": "10.0.2.0/23", + "metadata": { + "description": "Address prefix for the app subnet" + } + } + }, + "variables": { + "infrastructureSubnetName": "infrastructure-subnet", + "appSubnetName": "app-subnet" + }, + "resources": [ + { + "type": "Microsoft.Network/virtualNetworks", + "apiVersion": "2023-05-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "addressSpace": { + "addressPrefixes": [ + "[parameters('vnetAddressPrefix')]" + ] + }, + "subnets": [ + { + "name": "[variables('infrastructureSubnetName')]", + "properties": { + "addressPrefix": "[parameters('infrastructureSubnetPrefix')]" + } + }, + { + "name": "[variables('appSubnetName')]", + "properties": { + "addressPrefix": "[parameters('appSubnetPrefix')]" + } + } + ] + } + } + ], + "outputs": { + "vnetId": { + "type": "string", + "value": "[resourceId('Microsoft.Network/virtualNetworks', parameters('name'))]" + }, + "vnetName": { + "type": "string", + "value": "[parameters('name')]" + }, + "infrastructureSubnetName": { + "type": "string", + "value": "[variables('infrastructureSubnetName')]" + }, + "infrastructureSubnetId": { + "type": "string", + "value": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('name'), variables('infrastructureSubnetName'))]" + }, + "appSubnetName": { + "type": "string", + "value": "[variables('appSubnetName')]" + }, + "appSubnetId": { + "type": "string", + "value": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('name'), variables('appSubnetName'))]" + } + } + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName'))))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "container-apps", + "resourceGroup": "[if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "app" + }, + "containerAppsEnvironmentName": "[if(not(empty(parameters('containerAppsEnvName'))), createObject('value', parameters('containerAppsEnvName')), createObject('value', format('{0}{1}', variables('abbrs').appManagedEnvironments, variables('resourceToken'))))]", + "containerRegistryName": "[if(not(empty(parameters('containerRegistryName'))), createObject('value', parameters('containerRegistryName')), createObject('value', format('{0}{1}', variables('abbrs').containerRegistryRegistries, variables('resourceToken'))))]", + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "internal": { + "value": false + }, + "vnetId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'vnet'), '2022-09-01').outputs.vnetId.value]" + }, + "infrastructureSubnetName": { + "value": "infrastructure-subnet" + }, + "infrastructureSubnetId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'vnet'), '2022-09-01').outputs.infrastructureSubnetId.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": "5726479277483441007" + } + }, + "parameters": { + "name": { + "type": "string", + "defaultValue": "app", + "metadata": { + "description": "The name prefix for resources" + } + }, + "containerAppsEnvironmentName": { + "type": "string", + "metadata": { + "description": "The name of the container apps environment" + } + }, + "containerRegistryName": { + "type": "string", + "metadata": { + "description": "The name of the container registry" + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "The location of the container apps environment" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags for the resources" + } + }, + "internal": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Whether the container apps environment should be internal" + } + }, + "infrastructureSubnetId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Resource ID of the virtual network subnet to use for the container apps environment" + } + }, + "vnetId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Virtual network resource ID" + } + }, + "infrastructureSubnetName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Subnet name for infrastructure components" + } + } + }, + "resources": [ + { + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2022-10-01", + "name": "[format('{0}-logs', parameters('name'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "sku": { + "name": "PerGB2018" + }, + "retentionInDays": 30, + "features": { + "enableLogAccessUsingOnlyResourcePermissions": true + }, + "workspaceCapping": { + "dailyQuotaGb": 1 + } + } + }, + { + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2023-11-01-preview", + "name": "[parameters('containerRegistryName')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "sku": { + "name": "Basic" + }, + "properties": { + "adminUserEnabled": false + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "container-apps-environment-deploy", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[parameters('containerAppsEnvironmentName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "logAnalyticsWorkspaceId": { + "value": "[resourceId('Microsoft.OperationalInsights/workspaces', format('{0}-logs', parameters('name')))]" + }, + "infrastructureSubnetId": { + "value": "[parameters('infrastructureSubnetId')]" + }, + "vnetId": { + "value": "[parameters('vnetId')]" + }, + "infrastructureSubnetName": { + "value": "[parameters('infrastructureSubnetName')]" + }, + "internal": { + "value": "[parameters('internal')]" + } + }, + "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": "9322202461622995362" + } + }, + "parameters": { + "name": { + "type": "string" + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "tags": { + "type": "object", + "defaultValue": {} + }, + "logAnalyticsWorkspaceId": { + "type": "string", + "metadata": { + "description": "Log Analytics workspace resource ID" + } + }, + "vnetId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Virtual network resource ID" + } + }, + "infrastructureSubnetName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Subnet name for infrastructure components" + } + }, + "infrastructureSubnetId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Existing subnet ID for infrastructure resources. If not specified, a delegated subnet will be created." + } + }, + "internal": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Whether to create the Container Apps Environment in an internal or external network. Default is external." + } + } + }, + "variables": { + "subnetId": "[if(not(empty(parameters('infrastructureSubnetId'))), parameters('infrastructureSubnetId'), if(and(not(empty(parameters('vnetId'))), not(empty(parameters('infrastructureSubnetName')))), format('{0}/subnets/{1}', parameters('vnetId'), parameters('infrastructureSubnetName')), ''))]" + }, + "resources": [ + { + "type": "Microsoft.App/managedEnvironments", + "apiVersion": "2023-05-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "appLogsConfiguration": { + "destination": "log-analytics", + "logAnalyticsConfiguration": { + "customerId": "[reference(parameters('logAnalyticsWorkspaceId'), '2021-06-01').customerId]", + "sharedKey": "[listKeys(parameters('logAnalyticsWorkspaceId'), '2021-06-01').primarySharedKey]" + } + }, + "vnetConfiguration": "[if(not(empty(variables('subnetId'))), createObject('infrastructureSubnetId', variables('subnetId'), 'internal', parameters('internal')), null())]" + } + } + ], + "outputs": { + "id": { + "type": "string", + "value": "[resourceId('Microsoft.App/managedEnvironments', parameters('name'))]" + }, + "name": { + "type": "string", + "value": "[parameters('name')]" + }, + "defaultDomain": { + "type": "string", + "value": "[reference(resourceId('Microsoft.App/managedEnvironments', parameters('name')), '2023-05-01').defaultDomain]" + }, + "staticIp": { + "type": "string", + "value": "[reference(resourceId('Microsoft.App/managedEnvironments', parameters('name')), '2023-05-01').staticIp]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.OperationalInsights/workspaces', format('{0}-logs', parameters('name')))]" + ] + } + ], + "outputs": { + "environmentName": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'container-apps-environment-deploy'), '2022-09-01').outputs.name.value]" + }, + "environmentDefaultDomain": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'container-apps-environment-deploy'), '2022-09-01').outputs.defaultDomain.value]" + }, + "registryName": { + "type": "string", + "value": "[parameters('containerRegistryName')]" + }, + "registryLoginServer": { + "type": "string", + "value": "[reference(resourceId('Microsoft.ContainerRegistry/registries', parameters('containerRegistryName')), '2023-11-01-preview').loginServer]" + } + } + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName'))))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'vnet')]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "dtsResource", + "resourceGroup": "[if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": "[if(not(empty(parameters('dtsName'))), createObject('value', parameters('dtsName')), createObject('value', format('{0}{1}', variables('abbrs').dts, variables('resourceToken'))))]", + "taskhubname": "[if(not(empty(parameters('taskHubName'))), createObject('value', parameters('taskHubName')), createObject('value', format('{0}{1}', variables('abbrs').taskhub, variables('resourceToken'))))]", + "location": { + "value": "[parameters('dtsLocation')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "ipAllowlist": { + "value": [ + "0.0.0.0/0" + ] + }, + "skuName": { + "value": "[parameters('dtsSkuName')]" + }, + "skuCapacity": { + "value": "[parameters('dtsCapacity')]" + } + }, + "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": "13524563139135649995" + } + }, + "parameters": { + "ipAllowlist": { + "type": "array" + }, + "location": { + "type": "string" + }, + "tags": { + "type": "object", + "defaultValue": {} + }, + "name": { + "type": "string" + }, + "taskhubname": { + "type": "string" + }, + "skuName": { + "type": "string" + }, + "skuCapacity": { + "type": "int" + } + }, + "resources": [ + { + "type": "Microsoft.DurableTask/schedulers", + "apiVersion": "2024-10-01-preview", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "ipAllowlist": "[parameters('ipAllowlist')]", + "sku": { + "name": "[parameters('skuName')]", + "capacity": "[parameters('skuCapacity')]" + } + } + }, + { + "type": "Microsoft.DurableTask/schedulers/taskHubs", + "apiVersion": "2024-10-01-preview", + "name": "[format('{0}/{1}', parameters('name'), parameters('taskhubname'))]", + "dependsOn": [ + "[resourceId('Microsoft.DurableTask/schedulers', parameters('name'))]" + ] + } + ], + "outputs": { + "dts_NAME": { + "type": "string", + "value": "[parameters('name')]" + }, + "dts_URL": { + "type": "string", + "value": "[reference(resourceId('Microsoft.DurableTask/schedulers', parameters('name')), '2024-10-01-preview').endpoint]" + }, + "TASKHUB_NAME": { + "type": "string", + "value": "[parameters('taskhubname')]" + } + } + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName'))))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[parameters('clientsServiceName')]", + "resourceGroup": "[if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "appName": "[if(not(empty(parameters('containerAppsAppName'))), createObject('value', format('{0}-client', parameters('containerAppsAppName'))), createObject('value', format('{0}{1}-client', variables('abbrs').appContainerApps, variables('resourceToken'))))]", + "containerAppsEnvironmentName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'container-apps'), '2022-09-01').outputs.environmentName.value]" + }, + "containerRegistryName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'container-apps'), '2022-09-01').outputs.registryName.value]" + }, + "userAssignedManagedIdentity": { + "value": { + "resourceId": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'identity'), '2022-09-01').outputs.resourceId.value]", + "clientId": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'identity'), '2022-09-01').outputs.clientId.value]" + } + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "serviceName": { + "value": "client" + }, + "exists": { + "value": false + }, + "identityName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'identity'), '2022-09-01').outputs.name.value]" + }, + "dtsEndpoint": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'dtsResource'), '2022-09-01').outputs.dts_URL.value]" + }, + "taskHubName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'dtsResource'), '2022-09-01').outputs.TASKHUB_NAME.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "6029474384670576685" + } + }, + "definitions": { + "managedIdentity": { + "type": "object", + "properties": { + "resourceId": { + "type": "string" + }, + "clientId": { + "type": "string" + } + } + } + }, + "parameters": { + "appName": { + "type": "string" + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "tags": { + "type": "object", + "defaultValue": {} + }, + "identityName": { + "type": "string" + }, + "containerAppsEnvironmentName": { + "type": "string" + }, + "containerRegistryName": { + "type": "string" + }, + "serviceName": { + "type": "string", + "defaultValue": "aca" + }, + "exists": { + "type": "bool" + }, + "dtsEndpoint": { + "type": "string" + }, + "taskHubName": { + "type": "string" + }, + "agentConnectionString": { + "type": "string", + "defaultValue": "" + }, + "userAssignedManagedIdentity": { + "$ref": "#/definitions/managedIdentity", + "metadata": { + "description": "Unique identifier for user-assigned managed identity." + } + } + }, + "resources": { + "containerAppsApp": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('container-apps-{0}', parameters('serviceName'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[parameters('appName')]" + }, + "containerAppsEnvironmentName": { + "value": "[parameters('containerAppsEnvironmentName')]" + }, + "containerRegistryName": { + "value": "[parameters('containerRegistryName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[union(parameters('tags'), createObject('azd-service-name', parameters('serviceName')))]" + }, + "enableIngress": { + "value": true + }, + "external": { + "value": true + }, + "containerImage": { + "value": "mcr.microsoft.com/dotnet/aspnet:8.0" + }, + "identityName": { + "value": "[parameters('identityName')]" + }, + "minReplicas": { + "value": 1 + }, + "maxReplicas": { + "value": 10 + }, + "environmentVariables": { + "value": [ + { + "name": "AZURE_MANAGED_IDENTITY_CLIENT_ID", + "secretRef": "azure-managed-identity-client-id" + }, + { + "name": "ENDPOINT", + "value": "[format('Endpoint={0};Authentication=ManagedIdentity;ClientID={1}', parameters('dtsEndpoint'), parameters('userAssignedManagedIdentity').clientId)]" + }, + { + "name": "TASKHUB", + "value": "[parameters('taskHubName')]" + }, + { + "name": "AGENT_CONNECTION_STRING", + "value": "[if(not(empty(parameters('agentConnectionString'))), parameters('agentConnectionString'), '')]" + } + ] + }, + "secrets": { + "value": [ + { + "name": "azure-managed-identity-client-id", + "value": "[parameters('userAssignedManagedIdentity').clientId]" + } + ] + }, + "enableCustomScaleRule": { + "value": false + }, + "scaleRuleName": { + "value": "dtsscaler-orchestration" + }, + "scaleRuleType": { + "value": "azure-durabletask-scheduler" + }, + "scaleRuleMetadata": { + "value": { + "endpoint": "[parameters('dtsEndpoint')]", + "maxConcurrentWorkItemsCount": "1", + "taskhubName": "[parameters('taskHubName')]", + "workItemType": "Orchestration" + } + }, + "scaleRuleIdentity": { + "value": "[parameters('userAssignedManagedIdentity').resourceId]" + } + }, + "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": "17598589017450875416" + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the container app" + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "The location for the resources" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags for the resources" + } + }, + "containerAppsEnvironmentName": { + "type": "string", + "metadata": { + "description": "The name of the container apps environment" + } + }, + "containerRegistryName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The ID of the container registry" + } + }, + "identityName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The name of the user-assigned managed identity" + } + }, + "containerImage": { + "type": "string", + "metadata": { + "description": "The container image to deploy" + } + }, + "targetPort": { + "type": "int", + "defaultValue": 80, + "metadata": { + "description": "Target port for the container" + } + }, + "environmentVariables": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Environment variables for the container" + } + }, + "containerCpu": { + "type": "string", + "defaultValue": "0.5", + "metadata": { + "description": "CPU resources for the container" + } + }, + "containerMemory": { + "type": "string", + "defaultValue": "1.0Gi", + "metadata": { + "description": "Memory resources for the container" + } + }, + "minReplicas": { + "type": "int", + "defaultValue": 1, + "metadata": { + "description": "Minimum number of replicas" + } + }, + "maxReplicas": { + "type": "int", + "defaultValue": 10, + "metadata": { + "description": "Maximum number of replicas" + } + }, + "enableIngress": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Enable ingress for the container app" + } + }, + "secrets": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Additional secrets to be set on the container app" + } + }, + "enableCustomScaleRule": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Enable custom scale rules" + } + }, + "scaleRuleName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Scale rule name" + } + }, + "scaleRuleType": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Scale rule type" + } + }, + "scaleRuleMetadata": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Scale rule metadata" + } + }, + "scaleRuleIdentity": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Scale rule identity" + } + }, + "external": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Make the container app visible externally" + } + } + }, + "variables": { + "hasIdentity": "[not(empty(parameters('identityName')))]", + "hasRegistry": "[not(empty(parameters('containerRegistryName')))]" + }, + "resources": [ + { + "type": "Microsoft.App/containerApps", + "apiVersion": "2023-11-02-preview", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "identity": "[if(variables('hasIdentity'), createObject('type', 'UserAssigned', 'userAssignedIdentities', createObject(format('{0}', resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('identityName'))), createObject())), null())]", + "properties": { + "managedEnvironmentId": "[resourceId('Microsoft.App/managedEnvironments', parameters('containerAppsEnvironmentName'))]", + "configuration": { + "ingress": "[if(parameters('enableIngress'), createObject('external', parameters('external'), 'targetPort', parameters('targetPort'), 'transport', 'auto', 'allowInsecure', false()), null())]", + "registries": "[if(variables('hasRegistry'), createArray(createObject('server', reference(resourceId('Microsoft.ContainerRegistry/registries', parameters('containerRegistryName')), '2023-11-01-preview').loginServer, 'identity', if(variables('hasIdentity'), resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('identityName')), null()))), createArray())]", + "secrets": "[parameters('secrets')]" + }, + "template": { + "containers": [ + { + "name": "[parameters('name')]", + "image": "[parameters('containerImage')]", + "env": "[parameters('environmentVariables')]", + "resources": { + "cpu": "[json(parameters('containerCpu'))]", + "memory": "[parameters('containerMemory')]" + } + } + ], + "scale": { + "minReplicas": "[parameters('minReplicas')]", + "maxReplicas": "[parameters('maxReplicas')]", + "rules": "[if(parameters('enableCustomScaleRule'), createArray(createObject('name', parameters('scaleRuleName'), 'custom', createObject('type', parameters('scaleRuleType'), 'metadata', parameters('scaleRuleMetadata'), 'auth', if(not(empty(parameters('scaleRuleIdentity'))), createArray(createObject('secretRef', 'scale-rule-auth', 'triggerParameter', 'userAssignedIdentity')), createArray())))), createArray())]" + } + } + } + } + ], + "outputs": { + "containerAppId": { + "type": "string", + "value": "[resourceId('Microsoft.App/containerApps', parameters('name'))]" + }, + "containerAppFqdn": { + "type": "string", + "value": "[if(and(parameters('enableIngress'), parameters('external')), reference(resourceId('Microsoft.App/containerApps', parameters('name')), '2023-11-02-preview').configuration.ingress.fqdn, '')]" + } + } + } + } + } + }, + "outputs": { + "endpoint": { + "type": "string", + "value": "[reference('containerAppsApp').outputs.containerAppFqdn.value]" + }, + "envName": { + "type": "string", + "value": "[parameters('appName')]" + } + } + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'container-apps')]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'dtsResource')]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'identity')]", + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName'))))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "client-registry-access", + "resourceGroup": "[if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "containerRegistryName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'container-apps'), '2022-09-01').outputs.registryName.value]" + }, + "principalID": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'identity'), '2022-09-01').outputs.principalId.value]" + }, + "principalType": { + "value": "ServicePrincipal" + } + }, + "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": "15121309236660803763" + } + }, + "parameters": { + "containerRegistryName": { + "type": "string", + "metadata": { + "description": "The name of the Container Registry" + } + }, + "principalID": { + "type": "string", + "metadata": { + "description": "The Principal ID of the identity that needs access to the registry" + } + }, + "principalType": { + "type": "string", + "defaultValue": "ServicePrincipal", + "metadata": { + "description": "The type of the principal (User, ServicePrincipal, etc.)" + } + } + }, + "variables": { + "acrPullRoleDefinitionId": "7f951dda-4ed3-4680-a7ca-43fe172d538d" + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.ContainerRegistry/registries/{0}', parameters('containerRegistryName'))]", + "name": "[guid(resourceId('Microsoft.ContainerRegistry/registries', parameters('containerRegistryName')), parameters('principalID'), variables('acrPullRoleDefinitionId'))]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', variables('acrPullRoleDefinitionId'))]", + "principalId": "[parameters('principalID')]", + "principalType": "[parameters('principalType')]" + } + } + ], + "outputs": { + "roleAssignmentId": { + "type": "string", + "value": "[extensionResourceId(resourceId('Microsoft.ContainerRegistry/registries', parameters('containerRegistryName')), 'Microsoft.Authorization/roleAssignments', guid(resourceId('Microsoft.ContainerRegistry/registries', parameters('containerRegistryName')), parameters('principalID'), variables('acrPullRoleDefinitionId')))]" + } + } + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'container-apps')]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'identity')]", + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName'))))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[parameters('workerServiceName')]", + "resourceGroup": "[if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "appName": "[if(not(empty(parameters('containerAppsAppName'))), createObject('value', format('{0}-worker', parameters('containerAppsAppName'))), createObject('value', format('{0}{1}-worker', variables('abbrs').appContainerApps, variables('resourceToken'))))]", + "containerAppsEnvironmentName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'container-apps'), '2022-09-01').outputs.environmentName.value]" + }, + "containerRegistryName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'container-apps'), '2022-09-01').outputs.registryName.value]" + }, + "userAssignedManagedIdentity": { + "value": { + "resourceId": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'identity'), '2022-09-01').outputs.resourceId.value]", + "clientId": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'identity'), '2022-09-01').outputs.clientId.value]" + } + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "serviceName": { + "value": "worker" + }, + "exists": { + "value": false + }, + "identityName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'identity'), '2022-09-01').outputs.name.value]" + }, + "dtsEndpoint": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'dtsResource'), '2022-09-01').outputs.dts_URL.value]" + }, + "taskHubName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'dtsResource'), '2022-09-01').outputs.TASKHUB_NAME.value]" + }, + "agentConnectionString": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('{0}-{1}-deployment', parameters('projectName'), variables('resourceToken'))), '2022-09-01').outputs.projectConnectionString.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "6029474384670576685" + } + }, + "definitions": { + "managedIdentity": { + "type": "object", + "properties": { + "resourceId": { + "type": "string" + }, + "clientId": { + "type": "string" + } + } + } + }, + "parameters": { + "appName": { + "type": "string" + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "tags": { + "type": "object", + "defaultValue": {} + }, + "identityName": { + "type": "string" + }, + "containerAppsEnvironmentName": { + "type": "string" + }, + "containerRegistryName": { + "type": "string" + }, + "serviceName": { + "type": "string", + "defaultValue": "aca" + }, + "exists": { + "type": "bool" + }, + "dtsEndpoint": { + "type": "string" + }, + "taskHubName": { + "type": "string" + }, + "agentConnectionString": { + "type": "string", + "defaultValue": "" + }, + "userAssignedManagedIdentity": { + "$ref": "#/definitions/managedIdentity", + "metadata": { + "description": "Unique identifier for user-assigned managed identity." + } + } + }, + "resources": { + "containerAppsApp": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('container-apps-{0}', parameters('serviceName'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[parameters('appName')]" + }, + "containerAppsEnvironmentName": { + "value": "[parameters('containerAppsEnvironmentName')]" + }, + "containerRegistryName": { + "value": "[parameters('containerRegistryName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[union(parameters('tags'), createObject('azd-service-name', parameters('serviceName')))]" + }, + "enableIngress": { + "value": true + }, + "external": { + "value": true + }, + "containerImage": { + "value": "mcr.microsoft.com/dotnet/aspnet:8.0" + }, + "identityName": { + "value": "[parameters('identityName')]" + }, + "minReplicas": { + "value": 1 + }, + "maxReplicas": { + "value": 10 + }, + "environmentVariables": { + "value": [ + { + "name": "AZURE_MANAGED_IDENTITY_CLIENT_ID", + "secretRef": "azure-managed-identity-client-id" + }, + { + "name": "ENDPOINT", + "value": "[format('Endpoint={0};Authentication=ManagedIdentity;ClientID={1}', parameters('dtsEndpoint'), parameters('userAssignedManagedIdentity').clientId)]" + }, + { + "name": "TASKHUB", + "value": "[parameters('taskHubName')]" + }, + { + "name": "AGENT_CONNECTION_STRING", + "value": "[if(not(empty(parameters('agentConnectionString'))), parameters('agentConnectionString'), '')]" + } + ] + }, + "secrets": { + "value": [ + { + "name": "azure-managed-identity-client-id", + "value": "[parameters('userAssignedManagedIdentity').clientId]" + } + ] + }, + "enableCustomScaleRule": { + "value": false + }, + "scaleRuleName": { + "value": "dtsscaler-orchestration" + }, + "scaleRuleType": { + "value": "azure-durabletask-scheduler" + }, + "scaleRuleMetadata": { + "value": { + "endpoint": "[parameters('dtsEndpoint')]", + "maxConcurrentWorkItemsCount": "1", + "taskhubName": "[parameters('taskHubName')]", + "workItemType": "Orchestration" + } + }, + "scaleRuleIdentity": { + "value": "[parameters('userAssignedManagedIdentity').resourceId]" + } + }, + "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": "17598589017450875416" + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the container app" + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "The location for the resources" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags for the resources" + } + }, + "containerAppsEnvironmentName": { + "type": "string", + "metadata": { + "description": "The name of the container apps environment" + } + }, + "containerRegistryName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The ID of the container registry" + } + }, + "identityName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The name of the user-assigned managed identity" + } + }, + "containerImage": { + "type": "string", + "metadata": { + "description": "The container image to deploy" + } + }, + "targetPort": { + "type": "int", + "defaultValue": 80, + "metadata": { + "description": "Target port for the container" + } + }, + "environmentVariables": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Environment variables for the container" + } + }, + "containerCpu": { + "type": "string", + "defaultValue": "0.5", + "metadata": { + "description": "CPU resources for the container" + } + }, + "containerMemory": { + "type": "string", + "defaultValue": "1.0Gi", + "metadata": { + "description": "Memory resources for the container" + } + }, + "minReplicas": { + "type": "int", + "defaultValue": 1, + "metadata": { + "description": "Minimum number of replicas" + } + }, + "maxReplicas": { + "type": "int", + "defaultValue": 10, + "metadata": { + "description": "Maximum number of replicas" + } + }, + "enableIngress": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Enable ingress for the container app" + } + }, + "secrets": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Additional secrets to be set on the container app" + } + }, + "enableCustomScaleRule": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Enable custom scale rules" + } + }, + "scaleRuleName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Scale rule name" + } + }, + "scaleRuleType": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Scale rule type" + } + }, + "scaleRuleMetadata": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Scale rule metadata" + } + }, + "scaleRuleIdentity": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Scale rule identity" + } + }, + "external": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Make the container app visible externally" + } + } + }, + "variables": { + "hasIdentity": "[not(empty(parameters('identityName')))]", + "hasRegistry": "[not(empty(parameters('containerRegistryName')))]" + }, + "resources": [ + { + "type": "Microsoft.App/containerApps", + "apiVersion": "2023-11-02-preview", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "identity": "[if(variables('hasIdentity'), createObject('type', 'UserAssigned', 'userAssignedIdentities', createObject(format('{0}', resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('identityName'))), createObject())), null())]", + "properties": { + "managedEnvironmentId": "[resourceId('Microsoft.App/managedEnvironments', parameters('containerAppsEnvironmentName'))]", + "configuration": { + "ingress": "[if(parameters('enableIngress'), createObject('external', parameters('external'), 'targetPort', parameters('targetPort'), 'transport', 'auto', 'allowInsecure', false()), null())]", + "registries": "[if(variables('hasRegistry'), createArray(createObject('server', reference(resourceId('Microsoft.ContainerRegistry/registries', parameters('containerRegistryName')), '2023-11-01-preview').loginServer, 'identity', if(variables('hasIdentity'), resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('identityName')), null()))), createArray())]", + "secrets": "[parameters('secrets')]" + }, + "template": { + "containers": [ + { + "name": "[parameters('name')]", + "image": "[parameters('containerImage')]", + "env": "[parameters('environmentVariables')]", + "resources": { + "cpu": "[json(parameters('containerCpu'))]", + "memory": "[parameters('containerMemory')]" + } + } + ], + "scale": { + "minReplicas": "[parameters('minReplicas')]", + "maxReplicas": "[parameters('maxReplicas')]", + "rules": "[if(parameters('enableCustomScaleRule'), createArray(createObject('name', parameters('scaleRuleName'), 'custom', createObject('type', parameters('scaleRuleType'), 'metadata', parameters('scaleRuleMetadata'), 'auth', if(not(empty(parameters('scaleRuleIdentity'))), createArray(createObject('secretRef', 'scale-rule-auth', 'triggerParameter', 'userAssignedIdentity')), createArray())))), createArray())]" + } + } + } + } + ], + "outputs": { + "containerAppId": { + "type": "string", + "value": "[resourceId('Microsoft.App/containerApps', parameters('name'))]" + }, + "containerAppFqdn": { + "type": "string", + "value": "[if(and(parameters('enableIngress'), parameters('external')), reference(resourceId('Microsoft.App/containerApps', parameters('name')), '2023-11-02-preview').configuration.ingress.fqdn, '')]" + } + } + } + } + } + }, + "outputs": { + "endpoint": { + "type": "string", + "value": "[reference('containerAppsApp').outputs.containerAppFqdn.value]" + }, + "envName": { + "type": "string", + "value": "[parameters('appName')]" + } + } + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('{0}-{1}-deployment', parameters('projectName'), variables('resourceToken')))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'container-apps')]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'dtsResource')]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'identity')]", + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName'))))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "worker-ai-roles", + "resourceGroup": "[if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "principalId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'identity'), '2022-09-01').outputs.principalId.value]" + }, + "openAiResourceName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('dependencies-{0}-{1}', parameters('name'), variables('resourceToken'))), '2022-09-01').outputs.aiServicesName.value]" + }, + "aiHubName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('{0}-{1}', parameters('name'), variables('resourceToken'))), '2022-09-01').outputs.aiHubName.value]" + }, + "aiProjectName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('{0}-{1}-deployment', parameters('projectName'), variables('resourceToken'))), '2022-09-01').outputs.aiProjectName.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": "6757808878387663145" + } + }, + "parameters": { + "principalId": { + "type": "string", + "metadata": { + "description": "Principal ID to assign roles to" + } + }, + "openAiResourceName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the OpenAI resource" + } + }, + "aiHubName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the AI Hub" + } + }, + "aiProjectName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the AI Project" + } + } + }, + "resources": [ + { + "condition": "[not(empty(parameters('openAiResourceName')))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('openAiResourceName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAiResourceName')), parameters('principalId'), '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')]", + "properties": { + "principalId": "[parameters('principalId')]", + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[not(empty(parameters('openAiResourceName')))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('openAiResourceName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAiResourceName')), parameters('principalId'), '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68')]", + "properties": { + "principalId": "[parameters('principalId')]", + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68')]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[not(empty(parameters('aiHubName')))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.MachineLearningServices/workspaces/{0}', parameters('aiHubName'))]", + "name": "[guid(resourceId('Microsoft.MachineLearningServices/workspaces', parameters('aiHubName')), parameters('principalId'), 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "properties": { + "principalId": "[parameters('principalId')]", + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[not(empty(parameters('aiProjectName')))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.MachineLearningServices/workspaces/{0}', parameters('aiProjectName'))]", + "name": "[guid(resourceId('Microsoft.MachineLearningServices/workspaces', parameters('aiProjectName')), parameters('principalId'), 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "properties": { + "principalId": "[parameters('principalId')]", + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "principalType": "ServicePrincipal" + } + }, + { + "condition": "[not(empty(parameters('aiProjectName')))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.MachineLearningServices/workspaces/{0}', parameters('aiProjectName'))]", + "name": "[guid(resourceId('Microsoft.MachineLearningServices/workspaces', parameters('aiProjectName')), parameters('principalId'), 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "properties": { + "principalId": "[parameters('principalId')]", + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "principalType": "ServicePrincipal" + } + } + ], + "outputs": { + "openAiUserRoleAssignmentId": { + "type": "string", + "value": "[if(not(empty(parameters('openAiResourceName'))), extensionResourceId(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAiResourceName')), 'Microsoft.Authorization/roleAssignments', guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAiResourceName')), parameters('principalId'), '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')), '')]" + }, + "cognitiveServicesContributorRoleAssignmentId": { + "type": "string", + "value": "[if(not(empty(parameters('openAiResourceName'))), extensionResourceId(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAiResourceName')), 'Microsoft.Authorization/roleAssignments', guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('openAiResourceName')), parameters('principalId'), '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68')), '')]" + }, + "aiProjectReaderRoleAssignmentId": { + "type": "string", + "value": "[if(not(empty(parameters('aiProjectName'))), extensionResourceId(resourceId('Microsoft.MachineLearningServices/workspaces', parameters('aiProjectName')), 'Microsoft.Authorization/roleAssignments', guid(resourceId('Microsoft.MachineLearningServices/workspaces', parameters('aiProjectName')), parameters('principalId'), 'acdd72a7-3385-48ef-bd42-f606fba81ae7')), '')]" + } + } + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('dependencies-{0}-{1}', parameters('name'), variables('resourceToken')))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('{0}-{1}', parameters('name'), variables('resourceToken')))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('{0}-{1}-deployment', parameters('projectName'), variables('resourceToken')))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'identity')]", + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName'))))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "aiStorage", + "resourceGroup": "[if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[format('st{0}', variables('resourceToken'))]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[variables('tags')]" + } + }, + "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": "12086832046631103271" + } + }, + "parameters": { + "name": { + "type": "string" + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "tags": { + "type": "object", + "defaultValue": {} + }, + "allowBlobPublicAccess": { + "type": "bool", + "defaultValue": false + }, + "containers": { + "type": "array", + "defaultValue": [] + }, + "kind": { + "type": "string", + "defaultValue": "StorageV2" + }, + "minimumTlsVersion": { + "type": "string", + "defaultValue": "TLS1_2" + }, + "sku": { + "type": "object", + "defaultValue": { + "name": "Standard_LRS" + } + }, + "networkAcls": { + "type": "object", + "defaultValue": { + "bypass": "AzureServices", + "defaultAction": "Allow" + } + } + }, + "resources": [ + { + "copy": { + "name": "storage::blobServices::container", + "count": "[length(parameters('containers'))]" + }, + "condition": "[not(empty(parameters('containers')))]", + "type": "Microsoft.Storage/storageAccounts/blobServices/containers", + "apiVersion": "2023-05-01", + "name": "[format('{0}/{1}/{2}', parameters('name'), 'default', parameters('containers')[copyIndex()].name)]", + "properties": { + "publicAccess": "[coalesce(tryGet(parameters('containers')[copyIndex()], 'publicAccess'), 'None')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts/blobServices', parameters('name'), 'default')]" + ] + }, + { + "copy": { + "name": "storage::queueServices::queue", + "count": "[length(createArray('input', 'output'))]" + }, + "type": "Microsoft.Storage/storageAccounts/queueServices/queues", + "apiVersion": "2023-05-01", + "name": "[format('{0}/{1}/{2}', parameters('name'), 'default', createArray('input', 'output')[copyIndex()])]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts/queueServices', parameters('name'), 'default')]" + ] + }, + { + "condition": "[not(empty(parameters('containers')))]", + "type": "Microsoft.Storage/storageAccounts/blobServices", + "apiVersion": "2023-05-01", + "name": "[format('{0}/{1}', parameters('name'), 'default')]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', parameters('name'))]" + ] + }, + { + "type": "Microsoft.Storage/storageAccounts/queueServices", + "apiVersion": "2023-05-01", + "name": "[format('{0}/{1}', parameters('name'), 'default')]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', parameters('name'))]" + ] + }, + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2023-05-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "kind": "[parameters('kind')]", + "sku": "[parameters('sku')]", + "properties": { + "minimumTlsVersion": "[parameters('minimumTlsVersion')]", + "allowBlobPublicAccess": "[parameters('allowBlobPublicAccess')]", + "allowSharedKeyAccess": false, + "networkAcls": "[parameters('networkAcls')]" + } + } + ], + "outputs": { + "name": { + "type": "string", + "value": "[parameters('name')]" + }, + "primaryEndpoints": { + "type": "object", + "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', parameters('name')), '2023-05-01').primaryEndpoints]" + }, + "id": { + "type": "string", + "value": "[resourceId('Microsoft.Storage/storageAccounts', parameters('name'))]" + } + } + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName'))))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "api-identity", + "resourceGroup": "[if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[format('api-identity-{0}', variables('resourceToken'))]" + } + }, + "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": "5039160413996154793" + }, + "description": "Creates a Microsoft Entra user-assigned identity." + }, + "parameters": { + "name": { + "type": "string" + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "tags": { + "type": "object", + "defaultValue": {} + } + }, + "resources": [ + { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2023-01-31", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]" + } + ], + "outputs": { + "name": { + "type": "string", + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "value": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('name'))]" + }, + "principalId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('name')), '2023-01-31').principalId]" + }, + "clientId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('name')), '2023-01-31').clientId]" + }, + "tenantId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('name')), '2023-01-31').tenantId]" + } + } + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName'))))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('dependencies-{0}-{1}', parameters('name'), variables('resourceToken'))]", + "resourceGroup": "[if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "storageName": { + "value": "[format('st{0}', variables('resourceToken'))]" + }, + "keyvaultName": { + "value": "[format('kv-{0}{1}', parameters('name'), variables('resourceToken'))]" + }, + "aiServicesName": { + "value": "[format('ai{0}', variables('resourceToken'))]" + }, + "aiSearchName": { + "value": "[format('search{0}', variables('resourceToken'))]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "modelName": { + "value": "[parameters('modelName')]" + }, + "modelFormat": { + "value": "[parameters('modelFormat')]" + }, + "modelVersion": { + "value": "[parameters('modelVersion')]" + }, + "modelSkuName": { + "value": "[parameters('modelSkuName')]" + }, + "modelCapacity": { + "value": "[parameters('modelCapacity')]" + }, + "modelLocation": { + "value": "[parameters('modelLocation')]" + }, + "aiStorageAccountResourceId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'aiStorage'), '2022-09-01').outputs.id.value]" + }, + "aiServiceAccountResourceId": { + "value": "" + }, + "aiSearchServiceResourceId": { + "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": "13415245204975770646" + } + }, + "parameters": { + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Azure region of the deployment" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags to add to the resources" + } + }, + "aiServicesName": { + "type": "string", + "metadata": { + "description": "AI services name" + } + }, + "keyvaultName": { + "type": "string", + "metadata": { + "description": "The name of the Key Vault" + } + }, + "aiSearchName": { + "type": "string", + "metadata": { + "description": "The name of the AI Search resource" + } + }, + "storageName": { + "type": "string", + "metadata": { + "description": "Name of the storage account" + } + }, + "modelName": { + "type": "string", + "metadata": { + "description": "Model name for deployment" + } + }, + "modelFormat": { + "type": "string", + "metadata": { + "description": "Model format for deployment" + } + }, + "modelVersion": { + "type": "string", + "metadata": { + "description": "Model version for deployment" + } + }, + "modelSkuName": { + "type": "string", + "metadata": { + "description": "Model deployment SKU name" + } + }, + "modelCapacity": { + "type": "int", + "metadata": { + "description": "Model deployment capacity" + } + }, + "modelLocation": { + "type": "string", + "metadata": { + "description": "Model/AI Resource deployment location" + } + }, + "aiServiceAccountResourceId": { + "type": "string", + "metadata": { + "description": "The AI Service Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created." + } + }, + "aiSearchServiceResourceId": { + "type": "string", + "metadata": { + "description": "The AI Search Service full ARM Resource ID. This is an optional field, and if not provided, the resource will be created." + } + }, + "aiStorageAccountResourceId": { + "type": "string", + "metadata": { + "description": "The AI Storage Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created." + } + }, + "sku": { + "type": "string", + "defaultValue": "Standard_LRS" + } + }, + "variables": { + "aiServiceExists": "[not(equals(parameters('aiServiceAccountResourceId'), ''))]", + "acsExists": "[not(equals(parameters('aiSearchServiceResourceId'), ''))]", + "aiStorageExists": "[not(equals(parameters('aiStorageAccountResourceId'), ''))]", + "aiServiceParts": "[split(parameters('aiServiceAccountResourceId'), '/')]", + "acsParts": "[split(parameters('aiSearchServiceResourceId'), '/')]", + "aiStorageParts": "[split(parameters('aiStorageAccountResourceId'), '/')]" + }, + "resources": [ + { + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2024-12-01-preview", + "name": "[parameters('keyvaultName')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "createMode": "default", + "enabledForDeployment": false, + "enabledForDiskEncryption": false, + "enabledForTemplateDeployment": false, + "enableSoftDelete": true, + "enableRbacAuthorization": true, + "networkAcls": { + "bypass": "AzureServices", + "defaultAction": "Deny" + }, + "sku": { + "family": "A", + "name": "standard" + }, + "softDeleteRetentionInDays": 7, + "tenantId": "[subscription().tenantId]" + } + }, + { + "condition": "[not(variables('aiServiceExists'))]", + "type": "Microsoft.CognitiveServices/accounts", + "apiVersion": "2024-06-01-preview", + "name": "[parameters('aiServicesName')]", + "location": "[parameters('modelLocation')]", + "sku": { + "name": "S0" + }, + "kind": "AIServices", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "customSubDomainName": "[toLower(format('{0}', parameters('aiServicesName')))]", + "publicNetworkAccess": "Enabled" + } + }, + { + "condition": "[not(variables('aiServiceExists'))]", + "type": "Microsoft.CognitiveServices/accounts/deployments", + "apiVersion": "2024-06-01-preview", + "name": "[format('{0}/{1}', parameters('aiServicesName'), parameters('modelName'))]", + "sku": { + "capacity": "[parameters('modelCapacity')]", + "name": "[parameters('modelSkuName')]" + }, + "properties": { + "model": { + "name": "[parameters('modelName')]", + "format": "[parameters('modelFormat')]", + "version": "[parameters('modelVersion')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName'))]" + ] + }, + { + "condition": "[not(variables('acsExists'))]", + "type": "Microsoft.Search/searchServices", + "apiVersion": "2024-06-01-preview", + "name": "[parameters('aiSearchName')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "disableLocalAuth": false, + "authOptions": { + "aadOrApiKey": { + "aadAuthFailureMode": "http401WithBearerChallenge" + } + }, + "encryptionWithCmk": { + "enforcement": "Unspecified" + }, + "hostingMode": "default", + "partitionCount": 1, + "publicNetworkAccess": "enabled", + "replicaCount": 1, + "semanticSearch": "disabled" + }, + "sku": { + "name": "standard" + } + }, + { + "condition": "[not(variables('aiStorageExists'))]", + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2022-05-01", + "name": "[parameters('storageName')]", + "location": "[parameters('location')]", + "kind": "StorageV2", + "sku": { + "name": "[parameters('sku')]" + }, + "properties": { + "minimumTlsVersion": "TLS1_2", + "allowBlobPublicAccess": false, + "publicNetworkAccess": "Enabled", + "networkAcls": { + "bypass": "AzureServices", + "defaultAction": "Allow", + "virtualNetworkRules": [] + }, + "allowSharedKeyAccess": false + } + } + ], + "outputs": { + "aiServicesName": { + "type": "string", + "value": "[if(variables('aiServiceExists'), variables('aiServiceParts')[8], parameters('aiServicesName'))]" + }, + "aiservicesID": { + "type": "string", + "value": "[if(variables('aiServiceExists'), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('aiServiceParts')[2], variables('aiServiceParts')[4]), 'Microsoft.CognitiveServices/accounts', variables('aiServiceParts')[8]), resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName')))]" + }, + "aiservicesTarget": { + "type": "string", + "value": "[if(variables('aiServiceExists'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('aiServiceParts')[2], variables('aiServiceParts')[4]), 'Microsoft.CognitiveServices/accounts', variables('aiServiceParts')[8]), '2023-05-01').endpoint, reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName')), '2024-06-01-preview').endpoint)]" + }, + "aiServiceAccountResourceGroupName": { + "type": "string", + "value": "[if(variables('aiServiceExists'), variables('aiServiceParts')[4], resourceGroup().name)]" + }, + "aiServiceAccountSubscriptionId": { + "type": "string", + "value": "[if(variables('aiServiceExists'), variables('aiServiceParts')[2], subscription().subscriptionId)]" + }, + "aiSearchName": { + "type": "string", + "value": "[if(variables('acsExists'), variables('acsParts')[8], parameters('aiSearchName'))]" + }, + "aisearchID": { + "type": "string", + "value": "[if(variables('acsExists'), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('acsParts')[2], variables('acsParts')[4]), 'Microsoft.Search/searchServices', variables('acsParts')[8]), resourceId('Microsoft.Search/searchServices', parameters('aiSearchName')))]" + }, + "aiSearchServiceResourceGroupName": { + "type": "string", + "value": "[if(variables('acsExists'), variables('acsParts')[4], resourceGroup().name)]" + }, + "aiSearchServiceSubscriptionId": { + "type": "string", + "value": "[if(variables('acsExists'), variables('acsParts')[2], subscription().subscriptionId)]" + }, + "storageAccountName": { + "type": "string", + "value": "[if(variables('aiStorageExists'), variables('aiStorageParts')[8], parameters('storageName'))]" + }, + "storageId": { + "type": "string", + "value": "[if(variables('aiStorageExists'), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('aiStorageParts')[2], variables('aiStorageParts')[4]), 'Microsoft.Storage/storageAccounts', variables('aiStorageParts')[8]), resourceId('Microsoft.Storage/storageAccounts', parameters('storageName')))]" + }, + "storageAccountResourceGroupName": { + "type": "string", + "value": "[if(variables('aiStorageExists'), variables('aiStorageParts')[4], resourceGroup().name)]" + }, + "storageAccountSubscriptionId": { + "type": "string", + "value": "[if(variables('aiStorageExists'), variables('aiStorageParts')[2], subscription().subscriptionId)]" + }, + "keyvaultId": { + "type": "string", + "value": "[resourceId('Microsoft.KeyVault/vaults', parameters('keyvaultName'))]" + }, + "keyvaultUri": { + "type": "string", + "value": "[reference(resourceId('Microsoft.KeyVault/vaults', parameters('keyvaultName')), '2024-12-01-preview').vaultUri]" + } + } + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName'))))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'aiStorage')]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-{1}', parameters('name'), variables('resourceToken'))]", + "resourceGroup": "[if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "aiHubName": { + "value": "[format('{0}{1}', parameters('name'), variables('uniqueSuffix'))]" + }, + "aiHubFriendlyName": { + "value": "[parameters('aiHubFriendlyName')]" + }, + "aiHubDescription": { + "value": "[parameters('aiHubDescription')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "capabilityHostName": { + "value": "[format('{0}{1}{2}', parameters('name'), variables('uniqueSuffix'), parameters('capabilityHostName'))]" + }, + "aiSearchName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('dependencies-{0}-{1}', parameters('name'), variables('resourceToken'))), '2022-09-01').outputs.aiSearchName.value]" + }, + "aiSearchId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('dependencies-{0}-{1}', parameters('name'), variables('resourceToken'))), '2022-09-01').outputs.aisearchID.value]" + }, + "aiServicesName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('dependencies-{0}-{1}', parameters('name'), variables('resourceToken'))), '2022-09-01').outputs.aiServicesName.value]" + }, + "aiServicesId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('dependencies-{0}-{1}', parameters('name'), variables('resourceToken'))), '2022-09-01').outputs.aiservicesID.value]" + }, + "aiServicesTarget": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('dependencies-{0}-{1}', parameters('name'), variables('resourceToken'))), '2022-09-01').outputs.aiservicesTarget.value]" + }, + "keyVaultId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('dependencies-{0}-{1}', parameters('name'), variables('resourceToken'))), '2022-09-01').outputs.keyvaultId.value]" + }, + "storageAccountId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('dependencies-{0}-{1}', parameters('name'), variables('resourceToken'))), '2022-09-01').outputs.storageId.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": "7733613507821473412" + } + }, + "parameters": { + "location": { + "type": "string", + "metadata": { + "description": "Azure region of the deployment" + } + }, + "tags": { + "type": "object", + "metadata": { + "description": "Tags to add to the resources" + } + }, + "aiHubName": { + "type": "string", + "metadata": { + "description": "AI hub name" + } + }, + "aiHubFriendlyName": { + "type": "string", + "defaultValue": "[parameters('aiHubName')]", + "metadata": { + "description": "AI hub display name" + } + }, + "aiHubDescription": { + "type": "string", + "metadata": { + "description": "AI hub description" + } + }, + "keyVaultId": { + "type": "string", + "metadata": { + "description": "Resource ID of the key vault resource for storing connection strings" + } + }, + "storageAccountId": { + "type": "string", + "metadata": { + "description": "Resource ID of the storage account resource for storing experimentation outputs" + } + }, + "aiServicesId": { + "type": "string", + "metadata": { + "description": "Resource ID of the AI Services resource" + } + }, + "aiServicesTarget": { + "type": "string", + "metadata": { + "description": "Resource ID of the AI Services endpoint" + } + }, + "aiServicesName": { + "type": "string", + "metadata": { + "description": "Name AI Services resource" + } + }, + "aiSearchName": { + "type": "string", + "metadata": { + "description": "Name AI Search resource" + } + }, + "aiSearchId": { + "type": "string", + "metadata": { + "description": "Resource ID of the AI Search resource" + } + }, + "capabilityHostName": { + "type": "string", + "defaultValue": "caphost1", + "metadata": { + "description": "Name for capabilityHost." + } + } + }, + "variables": { + "acsConnectionName": "[format('{0}-connection-AISearch', parameters('aiHubName'))]", + "aoaiConnection": "[format('{0}-connection-AIServices_aoai', parameters('aiHubName'))]" + }, + "resources": [ + { + "type": "Microsoft.MachineLearningServices/workspaces/connections", + "apiVersion": "2024-07-01-preview", + "name": "[format('{0}/{1}', parameters('aiHubName'), format('{0}-connection-AIServices', parameters('aiHubName')))]", + "properties": { + "category": "AIServices", + "target": "[parameters('aiServicesTarget')]", + "authType": "AAD", + "isSharedToAll": true, + "metadata": { + "ApiType": "Azure", + "ResourceId": "[parameters('aiServicesId')]", + "location": "[reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName')), '2023-05-01', 'full').location]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.MachineLearningServices/workspaces', parameters('aiHubName'))]" + ] + }, + { + "type": "Microsoft.MachineLearningServices/workspaces/connections", + "apiVersion": "2024-07-01-preview", + "name": "[format('{0}/{1}', parameters('aiHubName'), variables('acsConnectionName'))]", + "properties": { + "category": "CognitiveSearch", + "target": "[format('https://{0}.search.windows.net', parameters('aiSearchName'))]", + "authType": "AAD", + "isSharedToAll": true, + "metadata": { + "ApiType": "Azure", + "ResourceId": "[parameters('aiSearchId')]", + "location": "[reference(resourceId('Microsoft.Search/searchServices', parameters('aiSearchName')), '2023-11-01', 'full').location]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.MachineLearningServices/workspaces', parameters('aiHubName'))]" + ] + }, + { + "type": "Microsoft.MachineLearningServices/workspaces/capabilityHosts", + "apiVersion": "2024-10-01-preview", + "name": "[format('{0}/{1}', parameters('aiHubName'), format('{0}-{1}', parameters('aiHubName'), parameters('capabilityHostName')))]", + "properties": { + "capabilityHostKind": "Agents" + }, + "dependsOn": [ + "[resourceId('Microsoft.MachineLearningServices/workspaces', parameters('aiHubName'))]" + ] + }, + { + "type": "Microsoft.MachineLearningServices/workspaces", + "apiVersion": "2024-07-01-preview", + "name": "[parameters('aiHubName')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "friendlyName": "[parameters('aiHubFriendlyName')]", + "description": "[parameters('aiHubDescription')]", + "keyVault": "[parameters('keyVaultId')]", + "storageAccount": "[parameters('storageAccountId')]" + }, + "kind": "hub" + } + ], + "outputs": { + "aiHubID": { + "type": "string", + "value": "[resourceId('Microsoft.MachineLearningServices/workspaces', parameters('aiHubName'))]" + }, + "aiHubName": { + "type": "string", + "value": "[parameters('aiHubName')]" + }, + "aoaiConnectionName": { + "type": "string", + "value": "[variables('aoaiConnection')]" + }, + "acsConnectionName": { + "type": "string", + "value": "[variables('acsConnectionName')]" + } + } + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('dependencies-{0}-{1}', parameters('name'), variables('resourceToken')))]", + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName'))))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-{1}-deployment', parameters('projectName'), variables('resourceToken'))]", + "resourceGroup": "[if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "aiProjectName": { + "value": "[format('{0}{1}', parameters('projectName'), variables('uniqueSuffix'))]" + }, + "aiProjectFriendlyName": { + "value": "[parameters('aiProjectFriendlyName')]" + }, + "aiProjectDescription": { + "value": "[parameters('aiProjectDescription')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "capabilityHostName": { + "value": "[format('{0}{1}{2}', parameters('projectName'), variables('uniqueSuffix'), parameters('capabilityHostName'))]" + }, + "aiHubId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('{0}-{1}', parameters('name'), variables('resourceToken'))), '2022-09-01').outputs.aiHubID.value]" + }, + "acsConnectionName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('{0}-{1}', parameters('name'), variables('resourceToken'))), '2022-09-01').outputs.acsConnectionName.value]" + }, + "aoaiConnectionName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('{0}-{1}', parameters('name'), variables('resourceToken'))), '2022-09-01').outputs.aoaiConnectionName.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": "13093580484595830078" + } + }, + "parameters": { + "location": { + "type": "string", + "metadata": { + "description": "Azure region of the deployment" + } + }, + "tags": { + "type": "object", + "metadata": { + "description": "Tags to add to the resources" + } + }, + "aiProjectName": { + "type": "string", + "metadata": { + "description": "AI Project name" + } + }, + "aiProjectFriendlyName": { + "type": "string", + "defaultValue": "[parameters('aiProjectName')]", + "metadata": { + "description": "AI Project display name" + } + }, + "aiProjectDescription": { + "type": "string", + "metadata": { + "description": "AI Project description" + } + }, + "aiHubId": { + "type": "string", + "metadata": { + "description": "Resource ID of the AI Hub resource" + } + }, + "capabilityHostName": { + "type": "string", + "defaultValue": "caphost1", + "metadata": { + "description": "Name for capabilityHost." + } + }, + "acsConnectionName": { + "type": "string", + "metadata": { + "description": "Name for ACS connection." + } + }, + "aoaiConnectionName": { + "type": "string", + "metadata": { + "description": "Name for ACS connection." + } + } + }, + "variables": { + "subscriptionId": "[subscription().subscriptionId]", + "resourceGroupName": "[resourceGroup().name]", + "standardConnectionString": "[format('{0}.api.azureml.ms;{1};{2};{3}', parameters('location'), variables('subscriptionId'), variables('resourceGroupName'), parameters('aiProjectName'))]", + "projectConnectionString": "[format('https://{0}.aiprojects.azure.com/api/projects/{1}/{2}', parameters('location'), variables('resourceGroupName'), parameters('aiProjectName'))]", + "storageConnections": [ + "[format('{0}/workspaceblobstore', parameters('aiProjectName'))]" + ], + "aiSearchConnection": [ + "[format('{0}', parameters('acsConnectionName'))]" + ], + "aiServiceConnections": [ + "[format('{0}', parameters('aoaiConnectionName'))]" + ] + }, + "resources": [ + { + "type": "Microsoft.MachineLearningServices/workspaces/capabilityHosts", + "apiVersion": "2024-10-01-preview", + "name": "[format('{0}/{1}', parameters('aiProjectName'), format('{0}-{1}', parameters('aiProjectName'), parameters('capabilityHostName')))]", + "properties": { + "capabilityHostKind": "Agents", + "aiServicesConnections": "[variables('aiServiceConnections')]", + "vectorStoreConnections": "[variables('aiSearchConnection')]", + "storageConnections": "[variables('storageConnections')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.MachineLearningServices/workspaces', parameters('aiProjectName'))]" + ] + }, + { + "type": "Microsoft.MachineLearningServices/workspaces", + "apiVersion": "2023-08-01-preview", + "name": "[parameters('aiProjectName')]", + "location": "[parameters('location')]", + "tags": "[union(parameters('tags'), createObject('ProjectConnectionString', variables('projectConnectionString'), 'StandardConnectionString', variables('standardConnectionString')))]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "friendlyName": "[parameters('aiProjectFriendlyName')]", + "description": "[parameters('aiProjectDescription')]", + "hubResourceId": "[parameters('aiHubId')]" + }, + "kind": "project" + } + ], + "outputs": { + "aiProjectName": { + "type": "string", + "value": "[parameters('aiProjectName')]" + }, + "aiProjectResourceId": { + "type": "string", + "value": "[resourceId('Microsoft.MachineLearningServices/workspaces', parameters('aiProjectName'))]" + }, + "aiProjectPrincipalId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.MachineLearningServices/workspaces', parameters('aiProjectName')), '2023-08-01-preview', 'full').identity.principalId]" + }, + "aiProjectWorkspaceId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.MachineLearningServices/workspaces', parameters('aiProjectName')), '2023-08-01-preview').workspaceId]" + }, + "projectConnectionString": { + "type": "string", + "value": "[variables('projectConnectionString')]" + } + } + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('{0}-{1}', parameters('name'), variables('resourceToken')))]", + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName'))))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('aiserviceroleassignments{0}-{1}', parameters('projectName'), variables('resourceToken'))]", + "resourceGroup": "[if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "aiServicesName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('dependencies-{0}-{1}', parameters('name'), variables('resourceToken'))), '2022-09-01').outputs.aiServicesName.value]" + }, + "aiProjectPrincipalId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('{0}-{1}-deployment', parameters('projectName'), variables('resourceToken'))), '2022-09-01').outputs.aiProjectPrincipalId.value]" + }, + "aiProjectId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('{0}-{1}-deployment', parameters('projectName'), variables('resourceToken'))), '2022-09-01').outputs.aiProjectResourceId.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": "3327108401608568038" + } + }, + "parameters": { + "aiServicesName": { + "type": "string" + }, + "aiProjectPrincipalId": { + "type": "string" + }, + "aiProjectId": { + "type": "string" + } + }, + "variables": { + "cognitiveServicesOpenAIUserRoleId": "5e0bd9bd-7b93-4f28-af87-19fc36ad61bd", + "cognitiveServiceServicesUserRoleId": "a97b65f3-24c7-4388-baec-2e87135dc908" + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('aiServicesName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName')), resourceId('Microsoft.Authorization/roleDefinitions', '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68'), parameters('aiProjectId'))]", + "properties": { + "principalId": "[parameters('aiProjectPrincipalId')]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68')]", + "principalType": "ServicePrincipal" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('aiServicesName'))]", + "name": "[guid(parameters('aiProjectId'), resourceId('Microsoft.Authorization/roleDefinitions', variables('cognitiveServicesOpenAIUserRoleId')), resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName')))]", + "properties": { + "principalId": "[parameters('aiProjectPrincipalId')]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', variables('cognitiveServicesOpenAIUserRoleId'))]", + "principalType": "ServicePrincipal" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('aiServicesName'))]", + "name": "[guid(parameters('aiProjectId'), resourceId('Microsoft.Authorization/roleDefinitions', variables('cognitiveServiceServicesUserRoleId')), resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName')))]", + "properties": { + "principalId": "[parameters('aiProjectPrincipalId')]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', variables('cognitiveServiceServicesUserRoleId'))]", + "principalType": "ServicePrincipal" + } + } + ] + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('dependencies-{0}-{1}', parameters('name'), variables('resourceToken')))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('{0}-{1}-deployment', parameters('projectName'), variables('resourceToken')))]", + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName'))))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('aisearchroleassignments{0}-{1}', parameters('projectName'), variables('resourceToken'))]", + "resourceGroup": "[if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "aiSearchName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('dependencies-{0}-{1}', parameters('name'), variables('resourceToken'))), '2022-09-01').outputs.aiSearchName.value]" + }, + "aiProjectPrincipalId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('{0}-{1}-deployment', parameters('projectName'), variables('resourceToken'))), '2022-09-01').outputs.aiProjectPrincipalId.value]" + }, + "aiProjectId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('{0}-{1}-deployment', parameters('projectName'), variables('resourceToken'))), '2022-09-01').outputs.aiProjectResourceId.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": "6371455345142990163" + } + }, + "parameters": { + "aiSearchName": { + "type": "string" + }, + "aiProjectPrincipalId": { + "type": "string" + }, + "aiProjectId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Search/searchServices/{0}', parameters('aiSearchName'))]", + "name": "[guid(parameters('aiProjectId'), resourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7'), resourceId('Microsoft.Search/searchServices', parameters('aiSearchName')))]", + "properties": { + "principalId": "[parameters('aiProjectPrincipalId')]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7')]", + "principalType": "ServicePrincipal" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Search/searchServices/{0}', parameters('aiSearchName'))]", + "name": "[guid(parameters('aiProjectId'), resourceId('Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0'), resourceId('Microsoft.Search/searchServices', parameters('aiSearchName')))]", + "properties": { + "principalId": "[parameters('aiProjectPrincipalId')]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0')]", + "principalType": "ServicePrincipal" + } + } + ] + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('dependencies-{0}-{1}', parameters('name'), variables('resourceToken')))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('{0}-{1}-deployment', parameters('projectName'), variables('resourceToken')))]", + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName'))))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('storageRoleAssignmentApi-{0}', variables('resourceToken'))]", + "resourceGroup": "[if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'aiStorage'), '2022-09-01').outputs.name.value]" + }, + "roleDefinitionID": { + "value": "[variables('storageBlobDataContributorRole')]" + }, + "principalID": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'api-identity'), '2022-09-01').outputs.principalId.value]" + }, + "principalType": { + "value": "ServicePrincipal" + } + }, + "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": "5825364076825170648" + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "metadata": { + "description": "The name of the storage account" + } + }, + "principalID": { + "type": "string", + "metadata": { + "description": "The principal ID to assign the role to" + } + }, + "roleDefinitionID": { + "type": "string", + "metadata": { + "description": "The role definition ID to assign" + } + }, + "principalType": { + "type": "string", + "metadata": { + "description": "The type of the principal (User, Group, ServicePrincipal)" + } + } + }, + "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('principalID'), parameters('roleDefinitionID'))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', parameters('roleDefinitionID'))]", + "principalId": "[parameters('principalID')]", + "principalType": "[parameters('principalType')]" + } + } + ], + "outputs": { + "roleAssignmentId": { + "type": "string", + "value": "[extensionResourceId(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), 'Microsoft.Authorization/roleAssignments', guid(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), parameters('principalID'), parameters('roleDefinitionID')))]" + } + } + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'api-identity')]", + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName'))))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'aiStorage')]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('storageRoleAssignmentUser-{0}', variables('resourceToken'))]", + "resourceGroup": "[if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'aiStorage'), '2022-09-01').outputs.name.value]" + }, + "roleDefinitionID": { + "value": "[variables('storageBlobDataContributorRole')]" + }, + "principalID": { + "value": "[parameters('principalId')]" + }, + "principalType": { + "value": "User" + } + }, + "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": "5825364076825170648" + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "metadata": { + "description": "The name of the storage account" + } + }, + "principalID": { + "type": "string", + "metadata": { + "description": "The principal ID to assign the role to" + } + }, + "roleDefinitionID": { + "type": "string", + "metadata": { + "description": "The role definition ID to assign" + } + }, + "principalType": { + "type": "string", + "metadata": { + "description": "The type of the principal (User, Group, ServicePrincipal)" + } + } + }, + "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('principalID'), parameters('roleDefinitionID'))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', parameters('roleDefinitionID'))]", + "principalId": "[parameters('principalID')]", + "principalType": "[parameters('principalType')]" + } + } + ], + "outputs": { + "roleAssignmentId": { + "type": "string", + "value": "[extensionResourceId(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), 'Microsoft.Authorization/roleAssignments', guid(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), parameters('principalID'), parameters('roleDefinitionID')))]" + } + } + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName'))))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'aiStorage')]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('storageQueueDataContributorRoleAssignmentprocessor-{0}', variables('resourceToken'))]", + "resourceGroup": "[if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'aiStorage'), '2022-09-01').outputs.name.value]" + }, + "roleDefinitionID": { + "value": "[variables('storageQueueDataContributorRoleDefinitionId')]" + }, + "principalID": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'api-identity'), '2022-09-01').outputs.principalId.value]" + }, + "principalType": { + "value": "ServicePrincipal" + } + }, + "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": "5825364076825170648" + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "metadata": { + "description": "The name of the storage account" + } + }, + "principalID": { + "type": "string", + "metadata": { + "description": "The principal ID to assign the role to" + } + }, + "roleDefinitionID": { + "type": "string", + "metadata": { + "description": "The role definition ID to assign" + } + }, + "principalType": { + "type": "string", + "metadata": { + "description": "The type of the principal (User, Group, ServicePrincipal)" + } + } + }, + "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('principalID'), parameters('roleDefinitionID'))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', parameters('roleDefinitionID'))]", + "principalId": "[parameters('principalID')]", + "principalType": "[parameters('principalType')]" + } + } + ], + "outputs": { + "roleAssignmentId": { + "type": "string", + "value": "[extensionResourceId(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), 'Microsoft.Authorization/roleAssignments', guid(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), parameters('principalID'), parameters('roleDefinitionID')))]" + } + } + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'api-identity')]", + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName'))))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'aiStorage')]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('storageQueueDataContributorRoleAssignmentAIProject-{0}', variables('resourceToken'))]", + "resourceGroup": "[if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'aiStorage'), '2022-09-01').outputs.name.value]" + }, + "roleDefinitionID": { + "value": "[variables('storageQueueDataContributorRoleDefinitionId')]" + }, + "principalID": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('{0}-{1}-deployment', parameters('projectName'), variables('resourceToken'))), '2022-09-01').outputs.aiProjectPrincipalId.value]" + }, + "principalType": { + "value": "ServicePrincipal" + } + }, + "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": "5825364076825170648" + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "metadata": { + "description": "The name of the storage account" + } + }, + "principalID": { + "type": "string", + "metadata": { + "description": "The principal ID to assign the role to" + } + }, + "roleDefinitionID": { + "type": "string", + "metadata": { + "description": "The role definition ID to assign" + } + }, + "principalType": { + "type": "string", + "metadata": { + "description": "The type of the principal (User, Group, ServicePrincipal)" + } + } + }, + "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('principalID'), parameters('roleDefinitionID'))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', parameters('roleDefinitionID'))]", + "principalId": "[parameters('principalID')]", + "principalType": "[parameters('principalType')]" + } + } + ], + "outputs": { + "roleAssignmentId": { + "type": "string", + "value": "[extensionResourceId(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), 'Microsoft.Authorization/roleAssignments', guid(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), parameters('principalID'), parameters('roleDefinitionID')))]" + } + } + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('{0}-{1}-deployment', parameters('projectName'), variables('resourceToken')))]", + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName'))))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'aiStorage')]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('storageQueueDataContributorRole-{0}', variables('resourceToken'))]", + "resourceGroup": "[if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'aiStorage'), '2022-09-01').outputs.name.value]" + }, + "roleDefinitionID": { + "value": "[variables('storageQueueDataContributorRoleDefinitionId')]" + }, + "principalID": { + "value": "[parameters('principalId')]" + }, + "principalType": { + "value": "User" + } + }, + "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": "5825364076825170648" + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "metadata": { + "description": "The name of the storage account" + } + }, + "principalID": { + "type": "string", + "metadata": { + "description": "The principal ID to assign the role to" + } + }, + "roleDefinitionID": { + "type": "string", + "metadata": { + "description": "The role definition ID to assign" + } + }, + "principalType": { + "type": "string", + "metadata": { + "description": "The type of the principal (User, Group, ServicePrincipal)" + } + } + }, + "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('principalID'), parameters('roleDefinitionID'))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', parameters('roleDefinitionID'))]", + "principalId": "[parameters('principalID')]", + "principalType": "[parameters('principalType')]" + } + } + ], + "outputs": { + "roleAssignmentId": { + "type": "string", + "value": "[extensionResourceId(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), 'Microsoft.Authorization/roleAssignments', guid(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), parameters('principalID'), parameters('roleDefinitionID')))]" + } + } + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName'))))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'aiStorage')]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('storageTableDataContributorRole-{0}', variables('resourceToken'))]", + "resourceGroup": "[if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'aiStorage'), '2022-09-01').outputs.name.value]" + }, + "roleDefinitionID": { + "value": "[variables('storageTableDataContributorRoleDefinitionId')]" + }, + "principalID": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'api-identity'), '2022-09-01').outputs.principalId.value]" + }, + "principalType": { + "value": "ServicePrincipal" + } + }, + "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": "5825364076825170648" + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "metadata": { + "description": "The name of the storage account" + } + }, + "principalID": { + "type": "string", + "metadata": { + "description": "The principal ID to assign the role to" + } + }, + "roleDefinitionID": { + "type": "string", + "metadata": { + "description": "The role definition ID to assign" + } + }, + "principalType": { + "type": "string", + "metadata": { + "description": "The type of the principal (User, Group, ServicePrincipal)" + } + } + }, + "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('principalID'), parameters('roleDefinitionID'))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', parameters('roleDefinitionID'))]", + "principalId": "[parameters('principalID')]", + "principalType": "[parameters('principalType')]" + } + } + ], + "outputs": { + "roleAssignmentId": { + "type": "string", + "value": "[extensionResourceId(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), 'Microsoft.Authorization/roleAssignments', guid(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), parameters('principalID'), parameters('roleDefinitionID')))]" + } + } + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'api-identity')]", + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName'))))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'aiStorage')]" + ] + } + ], + "outputs": { + "AZURE_LOCATION": { + "type": "string", + "value": "[parameters('location')]" + }, + "AZURE_TENANT_ID": { + "type": "string", + "value": "[tenant().tenantId]" + }, + "AZURE_CONTAINER_REGISTRY_ENDPOINT": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'container-apps'), '2022-09-01').outputs.registryLoginServer.value]" + }, + "AZURE_CONTAINER_REGISTRY_NAME": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'container-apps'), '2022-09-01').outputs.registryName.value]" + }, + "AZURE_CONTAINER_APP_ENDPOINT": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', parameters('clientsServiceName')), '2022-09-01').outputs.endpoint.value]" + }, + "AZURE_CONTAINER_ENVIRONMENT_NAME": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', parameters('clientsServiceName')), '2022-09-01').outputs.envName.value]" + }, + "DTS_URL": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'dtsResource'), '2022-09-01').outputs.dts_URL.value]" + }, + "TASKHUB_NAME": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'dtsResource'), '2022-09-01').outputs.TASKHUB_NAME.value]" + }, + "AGENT_CONNECTION_STRING": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('{0}-{1}-deployment', parameters('projectName'), variables('resourceToken'))), '2022-09-01').outputs.projectConnectionString.value]" + }, + "AI_PROJECT_NAME": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('{0}-{1}-deployment', parameters('projectName'), variables('resourceToken'))), '2022-09-01').outputs.aiProjectName.value]" + }, + "AI_HUB_NAME": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', format('{0}-{1}', parameters('name'), variables('resourceToken'))), '2022-09-01').outputs.aiHubName.value]" + }, + "AZURE_USER_ASSIGNED_IDENTITY_NAME": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, if(not(empty(parameters('resourceGroupName'))), parameters('resourceGroupName'), format('{0}{1}', variables('abbrs').resourcesResourceGroups, parameters('environmentName')))), 'Microsoft.Resources/deployments', 'identity'), '2022-09-01').outputs.name.value]" + } + } +} \ No newline at end of file diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/main.parameters.json b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/main.parameters.json new file mode 100644 index 0000000..c8d3453 --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/main.parameters.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "environmentName": { + "value": "${AZURE_ENV_NAME}" + }, + "location": { + "value": "${AZURE_LOCATION}" + }, + "principalId": { + "value": "${AZURE_PRINCIPAL_ID}" + } + } +} diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/main.update-existing.parameters.json b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/main.update-existing.parameters.json new file mode 100644 index 0000000..72b501f --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/main.update-existing.parameters.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "environmentName": { + "value": "${AZURE_ENV_NAME}" + }, + "location": { + "value": "${AZURE_LOCATION}" + }, + "principalId": { + "value": "${AZURE_PRINCIPAL_ID}" + }, + "assignAcrPushRole": { + "value": false + } + } +} diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/scripts/docker-push-with-retry.sh b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/scripts/docker-push-with-retry.sh new file mode 100755 index 0000000..ff80dd6 --- /dev/null +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/scripts/docker-push-with-retry.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# Script to push Docker images with automatic retries +# Usage: ./docker-push-with-retry.sh + +MAX_RETRIES=5 +RETRY_DELAY=10 +IMAGE_NAME=$1 + +if [ -z "$IMAGE_NAME" ]; then + echo "Error: Image name is required" + echo "Usage: $0 " + exit 1 +fi + +echo "Pushing image $IMAGE_NAME with up to $MAX_RETRIES retries..." + +for ((i=1; i<=MAX_RETRIES; i++)); do + echo "Attempt $i of $MAX_RETRIES..." + + # Try pushing the image + docker push $IMAGE_NAME + + # Check if push was successful + if [ $? -eq 0 ]; then + echo "Successfully pushed $IMAGE_NAME on attempt $i" + exit 0 + fi + + echo "Push failed. Waiting $RETRY_DELAY seconds before retrying..." + sleep $RETRY_DELAY + + # Increase delay for next attempt (exponential backoff) + RETRY_DELAY=$((RETRY_DELAY * 2)) +done + +echo "Failed to push $IMAGE_NAME after $MAX_RETRIES attempts" +exit 1 From 57148fbf3cb5ccc75bbe3b26967fa4bfb17d7385 Mon Sep 17 00:00:00 2001 From: greenie-msft Date: Wed, 6 Aug 2025 14:25:08 -0700 Subject: [PATCH 6/7] Fix merge conflicts --- .../Agents/PromptChaining/Client/Program.cs | 122 ------------------ .../Agents/PromptChaining/Client/README.md | 38 ------ .../ContentCreationOrchestration.cs | 15 --- .../main.update-existing.parameters.json | 18 --- .../scripts/docker-push-with-retry.sh | 38 ------ 5 files changed, 231 deletions(-) delete mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/README.md delete mode 100644 samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/main.update-existing.parameters.json delete mode 100755 samples/durable-task-sdks/dotnet/Agents/PromptChaining/scripts/docker-push-with-retry.sh diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/Program.cs b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/Program.cs index 3cbe8a3..24964ab 100644 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/Program.cs +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/Program.cs @@ -1,126 +1,4 @@ -using System; -using System.Threading; -using System.Threading.Tasks; using Azure.Identity; -using Microsoft.DurableTask; -using Microsoft.DurableTask.Worker; -using Microsoft.DurableTask.Worker.AzureManaged; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -// Force rebuild timestamp: 2025-01-06 10:26 -using AgentChainingSample.Activities; -using AgentChainingSample.Orchestrations; -using AgentChainingSample.Services; -using AgentChainingSample.Worker.Models; - -// Configure the host builder -HostApplicationBuilder builder = Host.CreateApplicationBuilder(); - -// Configure logging -builder.Logging.AddConsole(); -builder.Logging.SetMinimumLevel(LogLevel.Information); - -// Add configuration to services -builder.Services.AddSingleton(builder.Configuration); - -// Add HttpClient factory for proper management of HTTP connections -builder.Services.AddHttpClient(); - -// Register named HttpClient for DALL-E with appropriate timeouts -builder.Services.AddHttpClient("DallEClient", client => -{ - client.Timeout = TimeSpan.FromSeconds(120); // Increase timeout for image generation -}); - -// Register services with DI as singletons -// These services perform initialization work that doesn't need to be repeated for each activity -// Thread safety for initialization is handled by the BaseAgentService implementation -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); - -// Activities with [DurableTask] attribute are auto-registered via AddAllGeneratedTasks() -// No need to manually register them here - -// Get connection string from configuration with fallback to default local emulator connection -string connectionString = builder.Configuration["ENDPOINT"] ?? - builder.Configuration["DTS_CONNECTION_STRING"] ?? - "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"; - -// If we have the endpoint but not a full connection string, construct it -if (connectionString.StartsWith("Endpoint=") && !connectionString.Contains("TaskHub=")) -{ - string taskHub = builder.Configuration["TASKHUB"] ?? builder.Configuration["TASKHUB_NAME"] ?? "default"; - string clientId = builder.Configuration["AZURE_MANAGED_IDENTITY_CLIENT_ID"] ?? ""; - - if (!string.IsNullOrEmpty(clientId)) - { - connectionString = $"{connectionString};Authentication=ManagedIdentity;ClientID={clientId};TaskHub={taskHub}"; - } - else - { - connectionString = $"{connectionString};TaskHub={taskHub}"; - } -} - -// Configure services -// Register tasks with DI -builder.Services.AddDurableTaskWorker(builder => -{ - builder.AddTasks(registry => - { - // Auto-register all tasks marked with [DurableTask] attribute - registry.AddAllGeneratedTasks(); - }); - builder.UseDurableTaskScheduler(connectionString); -}); - -// Build the host -IHost host = builder.Build(); - -// Get a proper logger from the service provider -var logger = host.Services.GetRequiredService>(); -// Log the constructed connection string with sensitive info redacted -string managedIdentityClientId = builder.Configuration["AZURE_MANAGED_IDENTITY_CLIENT_ID"] ?? ""; -string logConnectionString = !string.IsNullOrEmpty(managedIdentityClientId) ? - connectionString.Replace(managedIdentityClientId, "[REDACTED]") : - connectionString; -logger.LogInformation("Connection string: {ConnectionString}", logConnectionString); -logger.LogInformation("TaskHub: {TaskHub}", builder.Configuration["TASKHUB"] ?? builder.Configuration["TASKHUB_NAME"] ?? "default"); -logger.LogInformation("This worker implements a news article generator workflow with multiple specialized agents"); - -// Log OpenAI configuration -logger.LogInformation("Azure OpenAI Endpoint: {Endpoint}", builder.Configuration["AZURE_OPENAI_ENDPOINT"] ?? "Not set"); -logger.LogInformation("Azure OpenAI Deployment: {Deployment}", builder.Configuration["OPENAI_DEPLOYMENT_NAME"] ?? "gpt-4 (default)"); -logger.LogInformation("DALL-E Endpoint: {DalleEndpoint}", !string.IsNullOrEmpty(builder.Configuration["DALLE_ENDPOINT"]) ? - "Configured" : "Not set - will use placeholder images"); -logger.LogInformation("Agent Connection String: {AgentConnectionString}", !string.IsNullOrEmpty(builder.Configuration["AGENT_CONNECTION_STRING"]) ? - "Configured" : "Not set - required for agent functionality"); - -logger.LogInformation("Starting Agent Chaining Sample Worker"); - -// Start the host -await host.StartAsync(); - -logger.LogInformation("Worker started and waiting for tasks..."); - -// Wait indefinitely in environments without interactive console, -// or until a key is pressed in interactive environments -if (Environment.UserInteractive && !Console.IsInputRedirected) -{ - logger.LogInformation("Press any key to stop..."); - Console.ReadKey(); -} -else -{ - // In non-interactive environments (like containers), wait indefinitely - await Task.Delay(Timeout.InfiniteTimeSpan); -} - -// Stop the host -await host.StopAsync();using Azure.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.DurableTask; using Microsoft.DurableTask.Client; diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/README.md b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/README.md deleted file mode 100644 index 3f15ca8..0000000 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Testing the Agent Chaining API - -This directory contains a `test.http` file that can be used to test the API endpoints using VS Code REST Client extension. - -## Prerequisites - -1. Install the [REST Client extension](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) for VS Code. - -## Using the test.http file - -1. Open the `test.http` file in VS Code -2. You'll see several HTTP requests defined in the file -3. Click the "Send Request" link that appears above each request to execute it -4. The response will be displayed in a new tab - -## Available Endpoints - -- `GET /health` - Health check endpoint -- `POST /api/content` - Create a new content generation request -- `GET /api/content` - List all active content generation requests -- `GET /api/content/{instanceId}` - Get status of a specific request -- `GET /api/content/{instanceId}/wait` - Wait for completion of a specific request with timeout - -## Sample Workflow - -1. Send a POST request to `/api/content` with a topic -2. Copy the instance ID from the response -3. Use that ID to check status with `GET /api/content/{instanceId}` -4. When the status is "Completed", you can retrieve the full result - -## Running Locally - -```bash -cd Client -dotnet run -``` - -The API will be available at http://localhost:5000. diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Orchestrations/ContentCreationOrchestration.cs b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Orchestrations/ContentCreationOrchestration.cs index a622324..f584ead 100644 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Orchestrations/ContentCreationOrchestration.cs +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Orchestrations/ContentCreationOrchestration.cs @@ -42,26 +42,11 @@ public override async Task RunAsync(TaskOrchestrationCont // 4. Assemble the final article with content and images and save to file in the project's tmp directory var articleResult = await context.CallActivityAsync( nameof(AssembleFinalArticleActivity), -<<<<<<< HEAD -<<<<<<< HEAD (articleContent, generatedImages, context.InstanceId)); logger.LogInformation("Final article assembled. Length: {Length} characters", articleResult.HtmlContent.Length); logger.LogInformation("Article saved to file: {FilePath}", articleResult.FilePath); logger.LogInformation("Article endpoint: {Endpoint}", articleResult.ArticleEndpoint); -======= - (articleContent, generatedImages)); - - logger.LogInformation("Final article assembled. Length: {Length} characters", articleResult.HtmlContent.Length); - logger.LogInformation("Article saved to file: {FilePath}", articleResult.FilePath); ->>>>>>> 21471c1 (Address PR feedback) -======= - (articleContent, generatedImages, context.InstanceId)); - - logger.LogInformation("Final article assembled. Length: {Length} characters", articleResult.HtmlContent.Length); - logger.LogInformation("Article saved to file: {FilePath}", articleResult.FilePath); - logger.LogInformation("Article endpoint: {Endpoint}", articleResult.ArticleEndpoint); ->>>>>>> c34d9d0 (Added Bicep) // 5. Return the complete workflow result return new ContentWorkflowResult diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/main.update-existing.parameters.json b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/main.update-existing.parameters.json deleted file mode 100644 index 72b501f..0000000 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/main.update-existing.parameters.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "environmentName": { - "value": "${AZURE_ENV_NAME}" - }, - "location": { - "value": "${AZURE_LOCATION}" - }, - "principalId": { - "value": "${AZURE_PRINCIPAL_ID}" - }, - "assignAcrPushRole": { - "value": false - } - } -} diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/scripts/docker-push-with-retry.sh b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/scripts/docker-push-with-retry.sh deleted file mode 100755 index ff80dd6..0000000 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/scripts/docker-push-with-retry.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash - -# Script to push Docker images with automatic retries -# Usage: ./docker-push-with-retry.sh - -MAX_RETRIES=5 -RETRY_DELAY=10 -IMAGE_NAME=$1 - -if [ -z "$IMAGE_NAME" ]; then - echo "Error: Image name is required" - echo "Usage: $0 " - exit 1 -fi - -echo "Pushing image $IMAGE_NAME with up to $MAX_RETRIES retries..." - -for ((i=1; i<=MAX_RETRIES; i++)); do - echo "Attempt $i of $MAX_RETRIES..." - - # Try pushing the image - docker push $IMAGE_NAME - - # Check if push was successful - if [ $? -eq 0 ]; then - echo "Successfully pushed $IMAGE_NAME on attempt $i" - exit 0 - fi - - echo "Push failed. Waiting $RETRY_DELAY seconds before retrying..." - sleep $RETRY_DELAY - - # Increase delay for next attempt (exponential backoff) - RETRY_DELAY=$((RETRY_DELAY * 2)) -done - -echo "Failed to push $IMAGE_NAME after $MAX_RETRIES attempts" -exit 1 From 31ccc05b31b7e871c6c65e00b3967d101d32974d Mon Sep 17 00:00:00 2001 From: greenie-msft Date: Wed, 1 Oct 2025 14:08:42 -0700 Subject: [PATCH 7/7] Address feedback and update bicep --- .../Agents/PromptChaining/Client/Program.cs | 53 +++++++++++------- .../Agents/PromptChaining/Worker/Program.cs | 55 ++++++++++++------- .../agent/standard-dependent-resources.bicep | 7 +-- .../Agents/PromptChaining/infra/app/app.bicep | 5 -- 4 files changed, 70 insertions(+), 50 deletions(-) diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/Program.cs b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/Program.cs index 24964ab..ef3a6c4 100644 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/Program.cs +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/Program.cs @@ -11,26 +11,8 @@ // Add services to the container builder.Services.AddEndpointsApiExplorer(); -// Get connection string from configuration with fallback to default local emulator connection -string connectionString = builder.Configuration["ENDPOINT"] ?? - builder.Configuration["DTS_CONNECTION_STRING"] ?? - "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"; - -// If we have the endpoint but not a full connection string, construct it -if (connectionString.StartsWith("Endpoint=") && !connectionString.Contains("TaskHub=")) -{ - string taskHub = builder.Configuration["TASKHUB"] ?? builder.Configuration["TASKHUB_NAME"] ?? "default"; - string clientId = builder.Configuration["AZURE_MANAGED_IDENTITY_CLIENT_ID"] ?? ""; - - if (!string.IsNullOrEmpty(clientId)) - { - connectionString = $"{connectionString};Authentication=ManagedIdentity;ClientID={clientId};TaskHub={taskHub}"; - } - else - { - connectionString = $"{connectionString};TaskHub={taskHub}"; - } -} +// Get connection string from configuration +string connectionString = BuildConnectionString(builder.Configuration); // Determine if we're connecting to the local emulator bool isLocalEmulator = connectionString.Contains("localhost"); @@ -306,4 +288,35 @@ catch (Exception ex) { logger.LogError(ex, "Error starting client application"); +} + +/// +/// Builds a connection string for the Durable Task Scheduler from configuration values. +/// +/// The configuration object containing connection settings. +/// A properly formatted connection string. +static string BuildConnectionString(IConfiguration configuration) +{ + // Get connection string from configuration with fallback to default local emulator connection + string connectionString = configuration["ENDPOINT"] ?? + configuration["DTS_CONNECTION_STRING"] ?? + "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"; + + // If we have the endpoint but not a full connection string, construct it + if (connectionString.StartsWith("Endpoint=") && !connectionString.Contains("TaskHub=")) + { + string taskHub = configuration["TASKHUB"] ?? configuration["TASKHUB_NAME"] ?? "default"; + string clientId = configuration["AZURE_MANAGED_IDENTITY_CLIENT_ID"] ?? ""; + + if (!string.IsNullOrEmpty(clientId)) + { + connectionString = $"{connectionString};Authentication=ManagedIdentity;ClientID={clientId};TaskHub={taskHub}"; + } + else + { + connectionString = $"{connectionString};TaskHub={taskHub}"; + } + } + + return connectionString; } \ No newline at end of file diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Program.cs b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Program.cs index 1ed9c5d..6f675da 100644 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Program.cs +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Worker/Program.cs @@ -44,26 +44,8 @@ // Activities with [DurableTask] attribute are auto-registered via AddAllGeneratedTasks() // No need to manually register them here -// Get connection string from configuration with fallback to default local emulator connection -string connectionString = builder.Configuration["ENDPOINT"] ?? - builder.Configuration["DTS_CONNECTION_STRING"] ?? - "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"; - -// If we have the endpoint but not a full connection string, construct it -if (connectionString.StartsWith("Endpoint=") && !connectionString.Contains("TaskHub=")) -{ - string taskHub = builder.Configuration["TASKHUB"] ?? builder.Configuration["TASKHUB_NAME"] ?? "default"; - string clientId = builder.Configuration["AZURE_MANAGED_IDENTITY_CLIENT_ID"] ?? ""; - - if (!string.IsNullOrEmpty(clientId)) - { - connectionString = $"{connectionString};Authentication=ManagedIdentity;ClientID={clientId};TaskHub={taskHub}"; - } - else - { - connectionString = $"{connectionString};TaskHub={taskHub}"; - } -} +// Get connection string from configuration +string connectionString = BuildConnectionString(builder.Configuration); // Configure services // Register tasks with DI @@ -120,4 +102,35 @@ } // Stop the host -await host.StopAsync(); \ No newline at end of file +await host.StopAsync(); + +/// +/// Builds a connection string for the Durable Task Scheduler from configuration values. +/// +/// The configuration object containing connection settings. +/// A properly formatted connection string. +static string BuildConnectionString(IConfiguration configuration) +{ + // Get connection string from configuration with fallback to default local emulator connection + string connectionString = configuration["ENDPOINT"] ?? + configuration["DTS_CONNECTION_STRING"] ?? + "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"; + + // If we have the endpoint but not a full connection string, construct it + if (connectionString.StartsWith("Endpoint=") && !connectionString.Contains("TaskHub=")) + { + string taskHub = configuration["TASKHUB"] ?? configuration["TASKHUB_NAME"] ?? "default"; + string clientId = configuration["AZURE_MANAGED_IDENTITY_CLIENT_ID"] ?? ""; + + if (!string.IsNullOrEmpty(clientId)) + { + connectionString = $"{connectionString};Authentication=ManagedIdentity;ClientID={clientId};TaskHub={taskHub}"; + } + else + { + connectionString = $"{connectionString};TaskHub={taskHub}"; + } + } + + return connectionString; +} \ No newline at end of file diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/agent/standard-dependent-resources.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/agent/standard-dependent-resources.bicep index 7e5ee38..bc29153 100644 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/agent/standard-dependent-resources.bicep +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/agent/standard-dependent-resources.bicep @@ -68,7 +68,7 @@ resource imageGenAiServices 'Microsoft.CognitiveServices/accounts@2025-04-01-pre ipRules: [] } publicNetworkAccess: 'Enabled' - disableLocalAuth: false + disableLocalAuth: true } } @@ -118,7 +118,7 @@ resource aiServices 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' = } publicNetworkAccess: 'Enabled' // API-key based auth is not supported for the Agent service - disableLocalAuth: false + disableLocalAuth: true } } resource modelDeployment 'Microsoft.CognitiveServices/accounts/deployments@2025-04-01-preview'= if(!aiServiceExists){ @@ -170,8 +170,7 @@ resource aiSearch 'Microsoft.Search/searchServices@2024-06-01-preview' = if(!acs type: 'SystemAssigned' } properties: { - disableLocalAuth: false - authOptions: { aadOrApiKey: { aadAuthFailureMode: 'http401WithBearerChallenge'}} + disableLocalAuth: true encryptionWithCmk: { enforcement: 'Unspecified' } diff --git a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/app.bicep b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/app.bicep index 1823f10..de86262 100644 --- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/app.bicep +++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/infra/app/app.bicep @@ -12,7 +12,6 @@ param taskHubName string param agentConnectionString string = '' param openAiEndpoint string = '' param openAiDeploymentName string = 'gpt-4o-mini' -param openAiApiKey string = '' // New parameters using direct naming convention param AGENT_CONNECTION_STRING string = '' param OPENAI_DEPLOYMENT_NAME string = 'gpt-4o-mini' @@ -116,10 +115,6 @@ module containerAppsApp '../core/host/container-app.bicep' = { name: 'AZURE_OPENAI_ENDPOINT' value: !empty(openAiEndpoint) ? openAiEndpoint : '' } - { - name: 'OPENAI_API_KEY' - value: !empty(openAiApiKey) ? openAiApiKey : '' - } { name: 'DALLE_ENDPOINT' value: DALLE_ENDPOINT