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/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/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 01c9009..ef3a6c4 100644
--- a/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/Program.cs
+++ b/samples/durable-task-sdks/dotnet/Agents/PromptChaining/Client/Program.cs
@@ -1,33 +1,18 @@
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"] ??
- "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";
+// Get connection string from configuration
+string connectionString = BuildConnectionString(builder.Configuration);
// Determine if we're connecting to the local emulator
bool isLocalEmulator = connectionString.Contains("localhost");
@@ -36,123 +21,302 @@
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