mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Adding azure functions support
This commit is contained in:
@@ -113,14 +113,14 @@
|
||||
<PackageVersion Include="Microsoft.Agents.ObjectModel.PowerFx" Version="2026.1.2.3" />
|
||||
<PackageVersion Include="Microsoft.PowerFx.Interpreter" Version="1.5.0-build.20251008-1002" />
|
||||
<!-- Durable Task -->
|
||||
<PackageVersion Include="Microsoft.DurableTask.Client" Version="1.18.0" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Client.AzureManaged" Version="1.18.0" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Worker" Version="1.18.0" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Worker.AzureManaged" Version="1.18.0" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Client" Version="1.19.1" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Client.AzureManaged" Version="1.19.0" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Worker" Version="1.19.0" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Worker.AzureManaged" Version="1.19.0" />
|
||||
<!-- Azure Functions -->
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker" Version="2.50.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="2.50.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" Version="1.11.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" Version="1.13.1" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" Version="1.0.1" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.3.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="2.1.0" />
|
||||
|
||||
@@ -52,6 +52,13 @@
|
||||
<Project Path="samples/Durable/Workflow/ConsoleApps/02_ConcurrentWorkflow/02_ConcurrentWorkflow.csproj" />
|
||||
<Project Path="samples/Durable/Workflow/ConsoleApps/03_ConditionalEdges/03_ConditionalEdges.csproj" />
|
||||
<Project Path="samples/Durable/Workflow/ConsoleApps/04_WorkflowAndAgents/04_WorkflowAndAgents.csproj" />
|
||||
<Project Path="samples/Durable/Workflow/ConsoleApps/05_NestedWorkflows/05_NestedWorkflows.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/Durable/Workflows/AzureFunctions/">
|
||||
<Project Path="samples/Durable/Workflow/AzureFunctions/01_SequentialWorkflow/01_SequentialWorkflow.csproj" />
|
||||
<Project Path="samples/Durable/Workflow/AzureFunctions/02_ConcurrentWorkflow/02_ConcurrentWorkflow.csproj" />
|
||||
<Project Path="samples/Durable/Workflow/AzureFunctions/03_ConditionalEdges/03_ConditionalEdges.csproj" />
|
||||
<Project Path="samples/Durable/Workflow/AzureFunctions/04_NestedWorkflows/04_NestedWorkflows.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/">
|
||||
<File Path="samples/GettingStarted/README.md" />
|
||||
@@ -403,7 +410,6 @@
|
||||
<Project Path="src/Microsoft.Agents.AI.Abstractions/Microsoft.Agents.AI.Abstractions.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.AGUI/Microsoft.Agents.AI.AGUI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Anthropic/Microsoft.Agents.AI.Anthropic.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.GitHub.Copilot/Microsoft.Agents.AI.GitHub.Copilot.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.AzureAI.Persistent/Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.AzureAI/Microsoft.Agents.AI.AzureAI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.CopilotStudio/Microsoft.Agents.AI.CopilotStudio.csproj" />
|
||||
@@ -411,6 +417,7 @@
|
||||
<Project Path="src/Microsoft.Agents.AI.Declarative/Microsoft.Agents.AI.Declarative.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.DevUI/Microsoft.Agents.AI.DevUI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.DurableTask/Microsoft.Agents.AI.DurableTask.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.GitHub.Copilot/Microsoft.Agents.AI.GitHub.Copilot.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A/Microsoft.Agents.AI.Hosting.A2A.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
|
||||
@@ -422,8 +429,8 @@
|
||||
<Project Path="src/Microsoft.Agents.AI.Purview/Microsoft.Agents.AI.Purview.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative.AzureAI/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Workflows/Microsoft.Agents.AI.Workflows.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Workflows.Generators/Microsoft.Agents.AI.Workflows.Generators.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Workflows/Microsoft.Agents.AI.Workflows.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Tests/" />
|
||||
@@ -433,8 +440,8 @@
|
||||
<Project Path="tests/AzureAI.IntegrationTests/AzureAI.IntegrationTests.csproj" />
|
||||
<Project Path="tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj" />
|
||||
<Project Path="tests/CopilotStudio.IntegrationTests/CopilotStudio.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Microsoft.Agents.AI.DurableTask.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Mem0.IntegrationTests/Microsoft.Agents.AI.Mem0.IntegrationTests.csproj" />
|
||||
@@ -448,13 +455,13 @@
|
||||
<Project Path="tests/Microsoft.Agents.AI.Abstractions.UnitTests/Microsoft.Agents.AI.Abstractions.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.AGUI.UnitTests/Microsoft.Agents.AI.AGUI.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Anthropic.UnitTests/Microsoft.Agents.AI.Anthropic.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.AzureAI.UnitTests/Microsoft.Agents.AI.AzureAI.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/Microsoft.Agents.AI.CosmosNoSql.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Declarative.UnitTests/Microsoft.Agents.AI.Declarative.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.DevUI.UnitTests/Microsoft.Agents.AI.DevUI.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.DurableTask.UnitTests/Microsoft.Agents.AI.DurableTask.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Microsoft.Agents.AI.Hosting.A2A.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests.csproj" />
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
|
||||
<OutputType>Exe</OutputType>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- The Functions build tools don't like namespaces that start with a number -->
|
||||
<AssemblyName>SequentialWorkflowFunctionApp</AssemblyName>
|
||||
<RootNamespace>SequentialWorkflowFunctionApp</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Azure Functions packages -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
|
||||
<!--
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,23 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Azure.Functions.Worker;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace SequentialWorkflowFunctionApp;
|
||||
|
||||
public class Function
|
||||
{
|
||||
private readonly ILogger<Function> _logger;
|
||||
|
||||
public Function(ILogger<Function> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[Function("Function")]
|
||||
public IActionResult Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequest req)
|
||||
{
|
||||
_logger.LogInformation("C# HTTP trigger function processed a request.");
|
||||
return new OkObjectResult("Welcome to Azure Functions!");
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
|
||||
namespace SequentialWorkflow;
|
||||
|
||||
/// <summary>
|
||||
/// Parses an Order ID from a string input and returns an Order object populated.
|
||||
/// </summary>
|
||||
internal sealed class OrderLookup() : Executor<string, Order>("OrderLookup")
|
||||
{
|
||||
public override async ValueTask<Order> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Populate Order information from OrderId.
|
||||
return new Order(message, 100.0m);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enriches an Order object with additional information.
|
||||
/// </summary>
|
||||
internal sealed class OrderEnrich() : Executor<Order, Order>("EnrichOrder")
|
||||
{
|
||||
public override async ValueTask<Order> HandleAsync(Order message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (message.Customer is null)
|
||||
{
|
||||
// populate customer information for the order from database.
|
||||
message.Customer = new Customer(1, "Jerry");
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class PaymentProcessor() : Executor<Order, Order>("ProcessPayment")
|
||||
{
|
||||
public override async ValueTask<Order> HandleAsync(Order message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
message.PaymentReferenceNumber = Guid.NewGuid().ToString()[^4..];
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class Order
|
||||
{
|
||||
public Order(string id, decimal amount)
|
||||
{
|
||||
this.Id = id;
|
||||
this.Amount = amount;
|
||||
}
|
||||
public string Id { get; }
|
||||
public decimal Amount { get; }
|
||||
public Customer? Customer { get; set; }
|
||||
public string? PaymentReferenceNumber { get; set; }
|
||||
}
|
||||
|
||||
public sealed record Customer(int Id, string Name);
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Azure.Functions.Worker.Builder;
|
||||
using Microsoft.Agents.AI.Hosting.AzureFunctions;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using SequentialWorkflow;
|
||||
|
||||
OrderLookup orderLookupExecutor = new();
|
||||
OrderEnrich orderEnricherExeecutor = new();
|
||||
PaymentProcessor paymentProcessorExecutor = new();
|
||||
|
||||
Workflow fulfillOrder = new WorkflowBuilder(orderLookupExecutor)
|
||||
.WithName("FulfillOrder")
|
||||
.WithDescription("Looks up an order by ID and run payment processing")
|
||||
.AddEdge(orderLookupExecutor, orderEnricherExeecutor)
|
||||
.AddEdge(orderEnricherExeecutor, paymentProcessorExecutor)
|
||||
.Build();
|
||||
|
||||
// Configure the function app to host the AI agent.
|
||||
// This will automatically generate HTTP API endpoints for the agent.
|
||||
using IHost app = FunctionsApplication
|
||||
.CreateBuilder(args)
|
||||
.ConfigureFunctionsWebApplication()
|
||||
.ConfigureDurableOptions(durableOption =>
|
||||
{
|
||||
// Add a workflow.
|
||||
durableOption.Workflows.AddWorkflow(fulfillOrder);
|
||||
})
|
||||
.Build();
|
||||
app.Run();
|
||||
@@ -0,0 +1,8 @@
|
||||
# Default endpoint address for local testing
|
||||
@authority=http://localhost:7071
|
||||
|
||||
### Prompt the agent
|
||||
POST {{authority}}/api/workflows/FulfillOrder/run
|
||||
Content-Type: text/plain
|
||||
|
||||
987
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": "2.0",
|
||||
"logging": {
|
||||
"logLevel": {
|
||||
"Microsoft.Agents.AI.DurableTask": "Information",
|
||||
"Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
|
||||
"DurableTask": "Information",
|
||||
"Microsoft.DurableTask": "Information"
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"durableTask": {
|
||||
"hubName": "default",
|
||||
"storageProvider": {
|
||||
"type": "AzureManaged",
|
||||
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
|
||||
<OutputType>Exe</OutputType>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- The Functions build tools don't like namespaces that start with a number -->
|
||||
<AssemblyName>SequentialWorkflowFunctionApp</AssemblyName>
|
||||
<RootNamespace>SequentialWorkflowFunctionApp</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Azure Functions packages -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
|
||||
<!--
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
|
||||
namespace WorkflowConcurrency;
|
||||
|
||||
/// <summary>
|
||||
/// Parses and validates the incoming question before sending to AI agents.
|
||||
/// </summary>
|
||||
internal sealed class ParseQuestionExecutor() : Executor<string, string>("ParseQuestion")
|
||||
{
|
||||
public override ValueTask<string> HandleAsync(
|
||||
string message,
|
||||
IWorkflowContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.ForegroundColor = ConsoleColor.Magenta;
|
||||
Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
|
||||
Console.WriteLine("│ [ParseQuestion] Preparing question for AI agents...");
|
||||
|
||||
string formattedQuestion = message.Trim();
|
||||
if (!formattedQuestion.EndsWith('?'))
|
||||
{
|
||||
formattedQuestion += "?";
|
||||
}
|
||||
|
||||
Console.WriteLine($"│ [ParseQuestion] Question: \"{formattedQuestion}\"");
|
||||
Console.WriteLine("│ [ParseQuestion] → Sending to Physicist and Chemist in PARALLEL...");
|
||||
Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
|
||||
Console.ResetColor();
|
||||
|
||||
return ValueTask.FromResult(formattedQuestion);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggregates responses from all AI agents into a comprehensive answer.
|
||||
/// This is the Fan-in point where parallel results are collected.
|
||||
/// </summary>
|
||||
internal sealed class AggregatorExecutor() : Executor<string[], string>("Aggregator")
|
||||
{
|
||||
public override ValueTask<string> HandleAsync(
|
||||
string[] message,
|
||||
IWorkflowContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
|
||||
Console.WriteLine($"│ [Aggregator] 📋 Received {message.Length} AI agent responses");
|
||||
Console.WriteLine("│ [Aggregator] Combining into comprehensive answer...");
|
||||
Console.WriteLine("│ [Aggregator] ✓ Aggregation complete!");
|
||||
Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
|
||||
Console.ResetColor();
|
||||
|
||||
string aggregatedResult = "═══════════════════════════════════════════════════════════════\n" +
|
||||
" AI EXPERT PANEL RESPONSES\n" +
|
||||
"═══════════════════════════════════════════════════════════════\n\n";
|
||||
|
||||
for (int i = 0; i < message.Length; i++)
|
||||
{
|
||||
string expertLabel = i == 0 ? "⚛️ PHYSICIST" : "🧪 CHEMIST";
|
||||
aggregatedResult += $"{expertLabel}:\n{message[i]}\n\n";
|
||||
}
|
||||
|
||||
aggregatedResult += "═══════════════════════════════════════════════════════════════\n" +
|
||||
$"Summary: Received perspectives from {message.Length} AI experts.\n" +
|
||||
"═══════════════════════════════════════════════════════════════";
|
||||
|
||||
return ValueTask.FromResult(aggregatedResult);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Azure;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hosting.AzureFunctions;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Azure.Functions.Worker.Builder;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using OpenAI.Chat;
|
||||
using WorkflowConcurrency;
|
||||
|
||||
// Configuration
|
||||
string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING")
|
||||
?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";
|
||||
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT")
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT is not set.");
|
||||
string? azureOpenAiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
|
||||
|
||||
// Create Azure OpenAI client
|
||||
AzureOpenAIClient openAiClient = !string.IsNullOrEmpty(azureOpenAiKey)
|
||||
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
|
||||
ChatClient chatClient = openAiClient.GetChatClient(deploymentName);
|
||||
|
||||
// Define the 4 executors for the workflow
|
||||
ParseQuestionExecutor parseQuestion = new();
|
||||
AIAgent physicist = chatClient.AsAIAgent("You are a physics expert. Be concise (2-3 sentences).", "Physicist");
|
||||
AIAgent chemist = chatClient.AsAIAgent("You are a chemistry expert. Be concise (2-3 sentences).", "Chemist");
|
||||
AggregatorExecutor aggregator = new();
|
||||
|
||||
// Build workflow: ParseQuestion -> [Physicist, Chemist] (parallel) -> Aggregator
|
||||
Workflow workflow = new WorkflowBuilder(parseQuestion)
|
||||
.WithName("ExpertReview")
|
||||
.AddFanOutEdge(parseQuestion, [physicist, chemist])
|
||||
.AddFanInEdge([physicist, chemist], aggregator)
|
||||
.Build();
|
||||
|
||||
// Configure the function app to host the AI agent.
|
||||
// This will automatically generate HTTP API endpoints for the agent.
|
||||
using IHost app = FunctionsApplication
|
||||
.CreateBuilder(args)
|
||||
.ConfigureFunctionsWebApplication()
|
||||
.ConfigureDurableOptions(durableOption =>
|
||||
{
|
||||
// Add a workflow.
|
||||
durableOption.Workflows.AddWorkflow(workflow);
|
||||
})
|
||||
.Build();
|
||||
app.Run();
|
||||
@@ -0,0 +1,8 @@
|
||||
# Default endpoint address for local testing
|
||||
@authority=http://localhost:7071
|
||||
|
||||
### Prompt the agent
|
||||
POST {{authority}}/api/workflows/ExpertReview/run
|
||||
Content-Type: text/plain
|
||||
|
||||
What is saturation?
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": "2.0",
|
||||
"logging": {
|
||||
"logLevel": {
|
||||
"Microsoft.Agents.AI.DurableTask": "Information",
|
||||
"Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
|
||||
"DurableTask": "Information",
|
||||
"Microsoft.DurableTask": "Information"
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"durableTask": {
|
||||
"hubName": "default",
|
||||
"storageProvider": {
|
||||
"type": "AzureManaged",
|
||||
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
|
||||
<OutputType>Exe</OutputType>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- The Functions build tools don't like namespaces that start with a number -->
|
||||
<AssemblyName>ConditionalEdgesFunctionApp</AssemblyName>
|
||||
<RootNamespace>ConditionalEdgesFunctionApp</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Azure Functions packages -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
|
||||
<!--
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,118 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
|
||||
namespace ConditionalEdgesFunctionApp;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an order with customer and payment details.
|
||||
/// </summary>
|
||||
internal sealed class Order
|
||||
{
|
||||
public Order(string id, decimal amount)
|
||||
{
|
||||
this.Id = id;
|
||||
this.Amount = amount;
|
||||
}
|
||||
|
||||
public string Id { get; }
|
||||
public decimal Amount { get; }
|
||||
public Customer? Customer { get; set; }
|
||||
public string? PaymentReferenceNumber { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a customer associated with an order.
|
||||
/// </summary>
|
||||
public sealed record Customer(int Id, string Name, bool IsBlocked);
|
||||
|
||||
/// <summary>
|
||||
/// Parses the order ID and retrieves order details.
|
||||
/// </summary>
|
||||
internal sealed class OrderIdParser() : Executor<string, Order>("OrderIdParser")
|
||||
{
|
||||
public override ValueTask<Order> HandleAsync(
|
||||
string message,
|
||||
IWorkflowContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine($"[OrderIdParser] Parsing order ID: {message}");
|
||||
Order order = new(message, 100.0m);
|
||||
return ValueTask.FromResult(order);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enriches the order with customer information.
|
||||
/// Orders with IDs containing 'B' are associated with blocked customers.
|
||||
/// </summary>
|
||||
internal sealed class OrderEnrich() : Executor<Order, Order>("EnrichOrder")
|
||||
{
|
||||
public override ValueTask<Order> HandleAsync(
|
||||
Order message,
|
||||
IWorkflowContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
message.Customer = GetCustomerForOrder(message.Id);
|
||||
Console.WriteLine($"[EnrichOrder] Customer: {message.Customer.Name}, IsBlocked: {message.Customer.IsBlocked}");
|
||||
return ValueTask.FromResult(message);
|
||||
}
|
||||
|
||||
private static Customer GetCustomerForOrder(string orderId)
|
||||
{
|
||||
if (orderId.Contains('B'))
|
||||
{
|
||||
return new Customer(101, "George", true);
|
||||
}
|
||||
|
||||
return new Customer(201, "Jerry", false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes payment for valid (non-blocked) orders.
|
||||
/// </summary>
|
||||
internal sealed class PaymentProcessor() : Executor<Order, Order>("PaymentProcessor")
|
||||
{
|
||||
public override ValueTask<Order> HandleAsync(
|
||||
Order message,
|
||||
IWorkflowContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
message.PaymentReferenceNumber = Guid.NewGuid().ToString()[..4];
|
||||
Console.WriteLine($"[PaymentProcessor] Payment processed for order {message.Id}. Reference: {message.PaymentReferenceNumber}");
|
||||
return ValueTask.FromResult(message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notifies the fraud team when a blocked customer places an order.
|
||||
/// </summary>
|
||||
internal sealed class NotifyFraud() : Executor<Order, string>("NotifyFraud")
|
||||
{
|
||||
public override ValueTask<string> HandleAsync(
|
||||
Order message,
|
||||
IWorkflowContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
string result = $"Order {message.Id} flagged as fraudulent for customer {message.Customer?.Name}.";
|
||||
Console.WriteLine($"[NotifyFraud] {result}");
|
||||
return ValueTask.FromResult(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines condition functions for routing orders based on customer status.
|
||||
/// </summary>
|
||||
internal static class OrderRouteConditions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a condition that evaluates to true when the customer is blocked.
|
||||
/// </summary>
|
||||
internal static Func<Order?, bool> WhenBlocked() => order => order?.Customer?.IsBlocked == true;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a condition that evaluates to true when the customer is not blocked.
|
||||
/// </summary>
|
||||
internal static Func<Order?, bool> WhenNotBlocked() => order => order?.Customer?.IsBlocked == false;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates conditional edges in a workflow hosted as an Azure Function.
|
||||
// Orders are routed to different executors based on customer status:
|
||||
// - Blocked customers → NotifyFraud
|
||||
// - Valid customers → PaymentProcessor
|
||||
|
||||
using ConditionalEdgesFunctionApp;
|
||||
using Microsoft.Agents.AI.Hosting.AzureFunctions;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Azure.Functions.Worker.Builder;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
// Create executor instances
|
||||
OrderIdParser orderParser = new();
|
||||
OrderEnrich orderEnrich = new();
|
||||
PaymentProcessor paymentProcessor = new();
|
||||
NotifyFraud notifyFraud = new();
|
||||
|
||||
// Build workflow with conditional edges.
|
||||
// The condition functions evaluate the Order output from OrderEnrich
|
||||
// to determine whether to route to NotifyFraud or PaymentProcessor.
|
||||
Workflow auditOrder = new WorkflowBuilder(orderParser)
|
||||
.WithName("AuditOrder")
|
||||
.WithDescription("Audits an order and routes based on customer status")
|
||||
.AddEdge(orderParser, orderEnrich)
|
||||
.AddEdge(orderEnrich, notifyFraud, condition: OrderRouteConditions.WhenBlocked())
|
||||
.AddEdge(orderEnrich, paymentProcessor, condition: OrderRouteConditions.WhenNotBlocked())
|
||||
.Build();
|
||||
|
||||
// Configure the function app to host the workflow.
|
||||
// This will automatically generate HTTP API endpoints for the workflow.
|
||||
using IHost app = FunctionsApplication
|
||||
.CreateBuilder(args)
|
||||
.ConfigureFunctionsWebApplication()
|
||||
.ConfigureDurableOptions(durableOption =>
|
||||
{
|
||||
// Add the workflow.
|
||||
durableOption.Workflows.AddWorkflow(auditOrder);
|
||||
})
|
||||
.Build();
|
||||
app.Run();
|
||||
@@ -0,0 +1,84 @@
|
||||
# Conditional Edges Workflow - Azure Functions Sample
|
||||
|
||||
This sample demonstrates how to build a workflow with **conditional edges** hosted as an Azure Function. Orders are routed to different executors based on customer status.
|
||||
|
||||
## Key Concepts Demonstrated
|
||||
|
||||
- Building workflows with **conditional edges** using `AddEdge` with a `condition` parameter
|
||||
- Hosting a conditional workflow as an Azure Function using `ConfigureDurableOptions`
|
||||
- Defining reusable condition functions for routing logic
|
||||
- Branching workflow execution based on data-driven decisions
|
||||
|
||||
## Overview
|
||||
|
||||
The workflow implements an order audit that routes orders differently based on whether the customer is blocked (flagged for fraud):
|
||||
|
||||
```
|
||||
OrderIdParser --> OrderEnrich --[IsBlocked]--> NotifyFraud
|
||||
|
|
||||
+--[NotBlocked]--> PaymentProcessor
|
||||
```
|
||||
|
||||
| Executor | Description |
|
||||
|----------|-------------|
|
||||
| OrderIdParser | Parses the order ID and retrieves order details |
|
||||
| OrderEnrich | Enriches the order with customer information |
|
||||
| PaymentProcessor | Processes payment for valid orders |
|
||||
| NotifyFraud | Notifies the fraud team for blocked customers |
|
||||
|
||||
## How Conditional Edges Work
|
||||
|
||||
Conditional edges allow you to specify a condition function that determines whether the edge should be traversed:
|
||||
|
||||
```csharp
|
||||
builder
|
||||
.AddEdge(orderParser, orderEnrich)
|
||||
.AddEdge(orderEnrich, notifyFraud, condition: OrderRouteConditions.WhenBlocked())
|
||||
.AddEdge(orderEnrich, paymentProcessor, condition: OrderRouteConditions.WhenNotBlocked());
|
||||
```
|
||||
|
||||
The condition functions receive the output of the source executor and return a boolean:
|
||||
|
||||
```csharp
|
||||
internal static class OrderRouteConditions
|
||||
{
|
||||
internal static Func<Order?, bool> WhenBlocked() =>
|
||||
order => order?.Customer?.IsBlocked == true;
|
||||
|
||||
internal static Func<Order?, bool> WhenNotBlocked() =>
|
||||
order => order?.Customer?.IsBlocked == false;
|
||||
}
|
||||
```
|
||||
|
||||
### Routing Logic
|
||||
|
||||
- Order IDs containing the letter **'B'** are associated with blocked customers → routed to `NotifyFraud`
|
||||
- All other order IDs are associated with valid customers → routed to `PaymentProcessor`
|
||||
|
||||
## Environment Setup
|
||||
|
||||
This sample requires:
|
||||
|
||||
- [.NET 10 SDK](https://dotnet.microsoft.com/download)
|
||||
- [Azure Functions Core Tools v4](https://learn.microsoft.com/azure/azure-functions/functions-run-local)
|
||||
- [Durable Task Scheduler](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-azure-managed-storage) running locally (default: `http://localhost:8080`)
|
||||
- [Azurite](https://learn.microsoft.com/azure/storage/common/storage-use-azurite) for local Azure Storage emulation
|
||||
|
||||
## Running the Sample
|
||||
|
||||
```bash
|
||||
cd dotnet/samples/Durable/Workflow/AzureFunctions/03_ConditionalEdges
|
||||
func start
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
**Valid order (routes to PaymentProcessor):**
|
||||
```bash
|
||||
curl -X POST http://localhost:7071/api/workflows/AuditOrder/run -H "Content-Type: text/plain" -d "12345"
|
||||
```
|
||||
|
||||
**Blocked order (routes to NotifyFraud):**
|
||||
```bash
|
||||
curl -X POST http://localhost:7071/api/workflows/AuditOrder/run -H "Content-Type: text/plain" -d "12345B"
|
||||
```
|
||||
@@ -0,0 +1,14 @@
|
||||
# Default endpoint address for local testing
|
||||
@authority=http://localhost:7071
|
||||
|
||||
### Valid order (routes to PaymentProcessor)
|
||||
POST {{authority}}/api/workflows/AuditOrder/run
|
||||
Content-Type: text/plain
|
||||
|
||||
12345
|
||||
|
||||
### Blocked order (routes to NotifyFraud) - Order IDs containing 'B' are flagged
|
||||
POST {{authority}}/api/workflows/AuditOrder/run
|
||||
Content-Type: text/plain
|
||||
|
||||
12345B
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": "2.0",
|
||||
"logging": {
|
||||
"logLevel": {
|
||||
"Microsoft.Agents.AI.DurableTask": "Information",
|
||||
"Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
|
||||
"DurableTask": "Information",
|
||||
"Microsoft.DurableTask": "Information"
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"durableTask": {
|
||||
"hubName": "default",
|
||||
"storageProvider": {
|
||||
"type": "AzureManaged",
|
||||
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
|
||||
<OutputType>Exe</OutputType>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- The Functions build tools don't like namespaces that start with a number -->
|
||||
<AssemblyName>NestedWorkflowsFunctionApp</AssemblyName>
|
||||
<RootNamespace>NestedWorkflowsFunctionApp</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Azure Functions packages -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
|
||||
<!--
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,220 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
|
||||
namespace NestedWorkflowsFunctionApp;
|
||||
|
||||
// ============================================
|
||||
// Order Processing Models
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// Represents an order being processed through the workflow.
|
||||
/// </summary>
|
||||
internal sealed class OrderInfo
|
||||
{
|
||||
public required string OrderId { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
public string? PaymentTransactionId { get; set; }
|
||||
public string? InventoryReservationId { get; set; }
|
||||
public string? TrackingNumber { get; set; }
|
||||
public string? Carrier { get; set; }
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Main Workflow Executors
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// Entry point executor that receives the order ID and creates an OrderInfo object.
|
||||
/// </summary>
|
||||
internal sealed class OrderReceived() : Executor<string, OrderInfo>("OrderReceived")
|
||||
{
|
||||
public override ValueTask<OrderInfo> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine($"[OrderReceived] Processing order '{message}'");
|
||||
|
||||
OrderInfo order = new()
|
||||
{
|
||||
OrderId = message,
|
||||
Amount = 99.99m
|
||||
};
|
||||
|
||||
return ValueTask.FromResult(order);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Final executor that outputs the completed order summary.
|
||||
/// </summary>
|
||||
internal sealed class OrderCompleted() : Executor<OrderInfo, string>("OrderCompleted")
|
||||
{
|
||||
public override ValueTask<string> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine($"[OrderCompleted] Order '{message.OrderId}' processed. Payment: {message.PaymentTransactionId}, Inventory: {message.InventoryReservationId}, Shipping: {message.Carrier} - {message.TrackingNumber}");
|
||||
|
||||
return ValueTask.FromResult($"Order {message.OrderId} completed. Tracking: {message.TrackingNumber}");
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Payment Sub-Workflow Executors
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// Validates payment information for an order.
|
||||
/// </summary>
|
||||
internal sealed class ValidatePayment() : Executor<OrderInfo, OrderInfo>("ValidatePayment")
|
||||
{
|
||||
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine($"[Payment/ValidatePayment] Validating payment for order '{message.OrderId}'");
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
|
||||
Console.WriteLine($"[Payment/ValidatePayment] Payment validated for ${message.Amount}");
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Fraud Check Sub-Sub-Workflow Executors (Level 2 nesting)
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes transaction patterns for potential fraud.
|
||||
/// </summary>
|
||||
internal sealed class AnalyzePatterns() : Executor<OrderInfo, OrderInfo>("AnalyzePatterns")
|
||||
{
|
||||
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine($"[Payment/FraudCheck/AnalyzePatterns] Analyzing patterns for order '{message.OrderId}'");
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
|
||||
Console.WriteLine("[Payment/FraudCheck/AnalyzePatterns] Pattern analysis complete");
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates a risk score for the transaction.
|
||||
/// </summary>
|
||||
internal sealed class CalculateRiskScore() : Executor<OrderInfo, OrderInfo>("CalculateRiskScore")
|
||||
{
|
||||
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine($"[Payment/FraudCheck/CalculateRiskScore] Calculating risk score for order '{message.OrderId}'");
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
|
||||
int riskScore = new Random().Next(1, 100);
|
||||
|
||||
Console.WriteLine($"[Payment/FraudCheck/CalculateRiskScore] Risk score: {riskScore}/100 (Low risk)");
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Charges the payment for an order.
|
||||
/// </summary>
|
||||
internal sealed class ChargePayment() : Executor<OrderInfo, OrderInfo>("ChargePayment")
|
||||
{
|
||||
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine($"[Payment/ChargePayment] Charging ${message.Amount} for order '{message.OrderId}'");
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
|
||||
|
||||
message.PaymentTransactionId = $"TXN-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
|
||||
|
||||
Console.WriteLine($"[Payment/ChargePayment] Payment processed: {message.PaymentTransactionId}");
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Inventory Sub-Workflow Executors
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// Checks inventory availability for an order.
|
||||
/// </summary>
|
||||
internal sealed class CheckInventory() : Executor<OrderInfo, OrderInfo>("CheckInventory")
|
||||
{
|
||||
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine($"[Inventory/CheckInventory] Checking inventory for order '{message.OrderId}'");
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
|
||||
Console.WriteLine("[Inventory/CheckInventory] Items available in stock");
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reserves inventory for an order.
|
||||
/// </summary>
|
||||
internal sealed class ReserveInventory() : Executor<OrderInfo, OrderInfo>("ReserveInventory")
|
||||
{
|
||||
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine($"[Inventory/ReserveInventory] Reserving items for order '{message.OrderId}'");
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
|
||||
|
||||
message.InventoryReservationId = $"RES-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
|
||||
|
||||
Console.WriteLine($"[Inventory/ReserveInventory] Reserved: {message.InventoryReservationId}");
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Shipping Sub-Workflow Executors
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// Selects a shipping carrier for an order.
|
||||
/// </summary>
|
||||
internal sealed class SelectCarrier() : Executor<OrderInfo, OrderInfo>("SelectCarrier")
|
||||
{
|
||||
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine($"[Shipping/SelectCarrier] Selecting carrier for order '{message.OrderId}'");
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
|
||||
message.Carrier = message.Amount > 50 ? "Express" : "Standard";
|
||||
|
||||
Console.WriteLine($"[Shipping/SelectCarrier] Selected carrier: {message.Carrier}");
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates shipment and generates tracking number.
|
||||
/// </summary>
|
||||
internal sealed class CreateShipment() : Executor<OrderInfo, OrderInfo>("CreateShipment")
|
||||
{
|
||||
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine($"[Shipping/CreateShipment] Creating shipment for order '{message.OrderId}'");
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
|
||||
|
||||
message.TrackingNumber = $"TRACK-{Guid.NewGuid().ToString("N")[..10].ToUpperInvariant()}";
|
||||
|
||||
Console.WriteLine($"[Shipping/CreateShipment] Shipment created: {message.TrackingNumber}");
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates nested workflows (sub-workflows) hosted as an Azure Function.
|
||||
// One workflow can be used as an executor within another workflow, enabling modular,
|
||||
// reusable workflow components with independent checkpointing and replay.
|
||||
//
|
||||
// Workflow structure:
|
||||
//
|
||||
// ┌─────────────────────────────────────────────────────────────────────────┐
|
||||
// │ OrderProcessing (Main Workflow) │
|
||||
// │ │
|
||||
// │ OrderReceived │
|
||||
// │ │ │
|
||||
// │ ▼ │
|
||||
// │ ┌─────────────────────────────────────────────────────────┐ │
|
||||
// │ │ Payment (Sub-Workflow) │ │
|
||||
// │ │ │ │
|
||||
// │ │ ValidatePayment │ │
|
||||
// │ │ │ │ │
|
||||
// │ │ ▼ │ │
|
||||
// │ │ ┌─────────────────────────────────────────┐ │ │
|
||||
// │ │ │ FraudCheck (Sub-Sub-Workflow) │ │ │
|
||||
// │ │ │ │ │ │
|
||||
// │ │ │ AnalyzePatterns ──► CalculateRiskScore │ │ │
|
||||
// │ │ └─────────────────────────────────────────┘ │ │
|
||||
// │ │ │ │ │
|
||||
// │ │ ▼ │ │
|
||||
// │ │ ChargePayment │ │
|
||||
// │ └─────────────────────────────────────────────────────────┘ │
|
||||
// │ │ │
|
||||
// │ ▼ │
|
||||
// │ ┌─────────────────────────────────────────────────────────┐ │
|
||||
// │ │ Inventory (Sub-Workflow) │ │
|
||||
// │ │ │ │
|
||||
// │ │ CheckInventory ──► ReserveInventory │ │
|
||||
// │ └─────────────────────────────────────────────────────────┘ │
|
||||
// │ │ │
|
||||
// │ ▼ │
|
||||
// │ ┌─────────────────────────────────────────────────────────┐ │
|
||||
// │ │ Shipping (Sub-Workflow) │ │
|
||||
// │ │ │ │
|
||||
// │ │ SelectCarrier ──► CreateShipment │ │
|
||||
// │ └─────────────────────────────────────────────────────────┘ │
|
||||
// │ │ │
|
||||
// │ ▼ │
|
||||
// │ OrderCompleted │
|
||||
// └─────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
using Microsoft.Agents.AI.Hosting.AzureFunctions;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Azure.Functions.Worker.Builder;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NestedWorkflowsFunctionApp;
|
||||
|
||||
// Create executor instances for the Fraud Check sub-sub-workflow (Level 2 nesting)
|
||||
AnalyzePatterns analyzePatterns = new();
|
||||
CalculateRiskScore calculateRiskScore = new();
|
||||
|
||||
Workflow fraudCheckWorkflow = new WorkflowBuilder(analyzePatterns)
|
||||
.WithName("SubFraudCheck")
|
||||
.WithDescription("Analyzes transaction patterns and calculates risk score")
|
||||
.AddEdge(analyzePatterns, calculateRiskScore)
|
||||
.Build();
|
||||
|
||||
// Create executor instances for the Payment Processing sub-workflow
|
||||
ValidatePayment validatePayment = new();
|
||||
ExecutorBinding fraudCheckExecutor = fraudCheckWorkflow.BindAsExecutor("FraudCheck");
|
||||
ChargePayment chargePayment = new();
|
||||
|
||||
Workflow paymentWorkflow = new WorkflowBuilder(validatePayment)
|
||||
.WithName("SubPaymentProcessing")
|
||||
.WithDescription("Validates and processes payment for an order")
|
||||
.AddEdge(validatePayment, fraudCheckExecutor)
|
||||
.AddEdge(fraudCheckExecutor, chargePayment)
|
||||
.Build();
|
||||
|
||||
// Create executor instances for the Inventory Management sub-workflow
|
||||
CheckInventory checkInventory = new();
|
||||
ReserveInventory reserveInventory = new();
|
||||
|
||||
Workflow inventoryWorkflow = new WorkflowBuilder(checkInventory)
|
||||
.WithName("SubInventoryManagement")
|
||||
.WithDescription("Checks availability and reserves inventory")
|
||||
.AddEdge(checkInventory, reserveInventory)
|
||||
.Build();
|
||||
|
||||
// Create executor instances for the Shipping Arrangement sub-workflow
|
||||
SelectCarrier selectCarrier = new();
|
||||
CreateShipment createShipment = new();
|
||||
|
||||
Workflow shippingWorkflow = new WorkflowBuilder(selectCarrier)
|
||||
.WithName("SubShippingArrangement")
|
||||
.WithDescription("Selects carrier and creates shipment")
|
||||
.AddEdge(selectCarrier, createShipment)
|
||||
.Build();
|
||||
|
||||
// Build the main Order Processing workflow using sub-workflows as executors
|
||||
ExecutorBinding paymentExecutor = paymentWorkflow.BindAsExecutor("Payment");
|
||||
ExecutorBinding inventoryExecutor = inventoryWorkflow.BindAsExecutor("Inventory");
|
||||
ExecutorBinding shippingExecutor = shippingWorkflow.BindAsExecutor("Shipping");
|
||||
|
||||
OrderReceived orderReceived = new();
|
||||
OrderCompleted orderCompleted = new();
|
||||
|
||||
Workflow orderProcessingWorkflow = new WorkflowBuilder(orderReceived)
|
||||
.WithName("OrderProcessing")
|
||||
.WithDescription("Processes an order through payment, inventory, and shipping")
|
||||
.AddEdge(orderReceived, paymentExecutor)
|
||||
.AddEdge(paymentExecutor, inventoryExecutor)
|
||||
.AddEdge(inventoryExecutor, shippingExecutor)
|
||||
.AddEdge(shippingExecutor, orderCompleted)
|
||||
.Build();
|
||||
|
||||
// Configure the function app to host the workflow.
|
||||
// Sub-workflows are discovered and registered automatically.
|
||||
using IHost app = FunctionsApplication
|
||||
.CreateBuilder(args)
|
||||
.ConfigureFunctionsWebApplication()
|
||||
.ConfigureDurableOptions(durableOption =>
|
||||
durableOption.Workflows.AddWorkflow(orderProcessingWorkflow))
|
||||
.Build();
|
||||
app.Run();
|
||||
@@ -0,0 +1,112 @@
|
||||
# Nested Workflows - Azure Functions Sample
|
||||
|
||||
This sample demonstrates how to build **nested workflows** (sub-workflows) hosted as an Azure Function. One workflow can be used as an executor within another workflow, enabling modular, reusable workflow components.
|
||||
|
||||
## Key Concepts Demonstrated
|
||||
|
||||
- Building workflows with **sub-workflow executors** using `BindAsExecutor`
|
||||
- **Multi-level nesting** — a sub-workflow contains its own sub-workflow (FraudCheck inside Payment)
|
||||
- Hosting nested workflows as an Azure Function using `ConfigureDurableOptions`
|
||||
- Automatic discovery and registration of sub-workflows
|
||||
|
||||
## Overview
|
||||
|
||||
The workflow implements an order processing pipeline where each stage is a separate sub-workflow:
|
||||
|
||||
```
|
||||
OrderReceived
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────┐
|
||||
│ Payment (Sub-Workflow) │
|
||||
│ ValidatePayment │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────────────┐ │
|
||||
│ │ FraudCheck (Sub-Sub) │ │
|
||||
│ │ AnalyzePatterns │ │
|
||||
│ │ ▼ │ │
|
||||
│ │ CalculateRiskScore │ │
|
||||
│ └────────────────────────────┘ │
|
||||
│ ▼ │
|
||||
│ ChargePayment │
|
||||
└─────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────┐
|
||||
│ Inventory (Sub-Workflow) │
|
||||
│ CheckInventory ──► Reserve │
|
||||
└─────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────┐
|
||||
│ Shipping (Sub-Workflow) │
|
||||
│ SelectCarrier ──► CreateShip. │
|
||||
└─────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
OrderCompleted
|
||||
```
|
||||
|
||||
| Executor | Sub-Workflow | Description |
|
||||
|----------|-------------|-------------|
|
||||
| OrderReceived | Main | Receives order ID and creates OrderInfo |
|
||||
| ValidatePayment | Payment | Validates payment information |
|
||||
| AnalyzePatterns | FraudCheck (nested in Payment) | Analyzes transaction patterns |
|
||||
| CalculateRiskScore | FraudCheck (nested in Payment) | Calculates fraud risk score |
|
||||
| ChargePayment | Payment | Charges the payment |
|
||||
| CheckInventory | Inventory | Checks item availability |
|
||||
| ReserveInventory | Inventory | Reserves inventory items |
|
||||
| SelectCarrier | Shipping | Selects shipping carrier |
|
||||
| CreateShipment | Shipping | Creates shipment with tracking |
|
||||
| OrderCompleted | Main | Outputs completed order summary |
|
||||
|
||||
## How Nested Workflows Work
|
||||
|
||||
Sub-workflows are created by binding a workflow as an executor:
|
||||
|
||||
```csharp
|
||||
// Build a sub-workflow
|
||||
Workflow paymentWorkflow = new WorkflowBuilder(validatePayment)
|
||||
.WithName("SubPaymentProcessing")
|
||||
.AddEdge(validatePayment, fraudCheckExecutor)
|
||||
.AddEdge(fraudCheckExecutor, chargePayment)
|
||||
.Build();
|
||||
|
||||
// Bind it as an executor in the parent workflow
|
||||
ExecutorBinding paymentExecutor = paymentWorkflow.BindAsExecutor("Payment");
|
||||
|
||||
// Use it like any other executor
|
||||
Workflow mainWorkflow = new WorkflowBuilder(orderReceived)
|
||||
.AddEdge(orderReceived, paymentExecutor)
|
||||
.Build();
|
||||
```
|
||||
|
||||
Each sub-workflow runs as a separate orchestration instance, providing:
|
||||
- **Modularity** — workflows can be composed from reusable sub-workflows
|
||||
- **Independent checkpointing** — each sub-workflow has its own replay history
|
||||
- **Hierarchical visualization** — view parent-child relationships in the DTS dashboard
|
||||
- **Failure isolation** — sub-workflow failures don't corrupt parent state
|
||||
|
||||
## Environment Setup
|
||||
|
||||
This sample requires:
|
||||
|
||||
- [.NET 10 SDK](https://dotnet.microsoft.com/download)
|
||||
- [Azure Functions Core Tools v4](https://learn.microsoft.com/azure/azure-functions/functions-run-local)
|
||||
- [Durable Task Scheduler](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-azure-managed-storage) running locally (default: `http://localhost:8080`)
|
||||
- [Azurite](https://learn.microsoft.com/azure/storage/common/storage-use-azurite) for local Azure Storage emulation
|
||||
|
||||
## Running the Sample
|
||||
|
||||
```bash
|
||||
cd dotnet/samples/Durable/Workflow/AzureFunctions/04_NestedWorkflows
|
||||
func start
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:7071/api/workflows/OrderProcessing/run -H "Content-Type: text/plain" -d "ORD-2026-001"
|
||||
```
|
||||
|
||||
Open the DTS dashboard at `http://localhost:8080` to see the parent-child orchestration hierarchy in the Timeline view.
|
||||
@@ -0,0 +1,14 @@
|
||||
# Default endpoint address for local testing
|
||||
@authority=http://localhost:7071
|
||||
|
||||
### Process an order through nested workflows (Payment → Inventory → Shipping)
|
||||
POST {{authority}}/api/workflows/OrderProcessing/run
|
||||
Content-Type: text/plain
|
||||
|
||||
ORD-2026-001
|
||||
|
||||
### Process another order
|
||||
POST {{authority}}/api/workflows/OrderProcessing/run
|
||||
Content-Type: text/plain
|
||||
|
||||
ORD-2026-002
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": "2.0",
|
||||
"logging": {
|
||||
"logLevel": {
|
||||
"Microsoft.Agents.AI.DurableTask": "Information",
|
||||
"Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
|
||||
"DurableTask": "Information",
|
||||
"Microsoft.DurableTask": "Information"
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"durableTask": {
|
||||
"hubName": "default",
|
||||
"storageProvider": {
|
||||
"type": "AzureManaged",
|
||||
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<OutputType>Exe</OutputType>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>NestedWorkflows</AssemblyName>
|
||||
<RootNamespace>NestedWorkflows</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.DurableTask.Client.AzureManaged" />
|
||||
<PackageReference Include="Microsoft.DurableTask.Worker.AzureManaged" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
|
||||
<!--
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.DurableTask" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Workflows" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.DurableTask\Microsoft.Agents.AI.DurableTask.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,250 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
|
||||
namespace NestedWorkflows;
|
||||
|
||||
// ============================================
|
||||
// Order Processing Models
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// Represents an order being processed through the workflow.
|
||||
/// </summary>
|
||||
internal sealed class OrderInfo
|
||||
{
|
||||
public required string OrderId { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
public string? PaymentTransactionId { get; set; }
|
||||
public string? InventoryReservationId { get; set; }
|
||||
public string? TrackingNumber { get; set; }
|
||||
public string? Carrier { get; set; }
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Main Workflow Executors
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// Entry point executor that receives the order ID and creates an OrderInfo object.
|
||||
/// </summary>
|
||||
internal sealed class OrderReceived() : Executor<string, OrderInfo>("OrderReceived")
|
||||
{
|
||||
public override ValueTask<OrderInfo> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.WriteLine($"[OrderReceived] Processing order '{message}'");
|
||||
Console.ResetColor();
|
||||
|
||||
OrderInfo order = new()
|
||||
{
|
||||
OrderId = message,
|
||||
Amount = 99.99m
|
||||
};
|
||||
|
||||
return ValueTask.FromResult(order);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Final executor that outputs the completed order summary.
|
||||
/// </summary>
|
||||
internal sealed class OrderCompleted() : Executor<OrderInfo, string>("OrderCompleted")
|
||||
{
|
||||
public override ValueTask<string> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
|
||||
Console.WriteLine($"│ [OrderCompleted] Order '{message.OrderId}' successfully processed!");
|
||||
Console.WriteLine($"│ Payment: {message.PaymentTransactionId}");
|
||||
Console.WriteLine($"│ Inventory: {message.InventoryReservationId}");
|
||||
Console.WriteLine($"│ Shipping: {message.Carrier} - {message.TrackingNumber}");
|
||||
Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
|
||||
Console.ResetColor();
|
||||
|
||||
return ValueTask.FromResult($"Order {message.OrderId} completed. Tracking: {message.TrackingNumber}");
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Payment Sub-Workflow Executors
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// Validates payment information for an order.
|
||||
/// </summary>
|
||||
internal sealed class ValidatePayment() : Executor<OrderInfo, OrderInfo>("ValidatePayment")
|
||||
{
|
||||
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($" [Payment/ValidatePayment] Validating payment for order '{message.OrderId}'...");
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
|
||||
Console.WriteLine($" [Payment/ValidatePayment] Payment validated for ${message.Amount}");
|
||||
Console.ResetColor();
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Fraud Check Sub-Sub-Workflow Executors (Level 2 nesting)
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes transaction patterns for potential fraud.
|
||||
/// </summary>
|
||||
internal sealed class AnalyzePatterns() : Executor<OrderInfo, OrderInfo>("AnalyzePatterns")
|
||||
{
|
||||
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.DarkYellow;
|
||||
Console.WriteLine($" [Payment/FraudCheck/AnalyzePatterns] Analyzing patterns for order '{message.OrderId}'...");
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
|
||||
Console.WriteLine(" [Payment/FraudCheck/AnalyzePatterns] ✓ Pattern analysis complete");
|
||||
Console.ResetColor();
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates a risk score for the transaction.
|
||||
/// </summary>
|
||||
internal sealed class CalculateRiskScore() : Executor<OrderInfo, OrderInfo>("CalculateRiskScore")
|
||||
{
|
||||
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.DarkYellow;
|
||||
Console.WriteLine($" [Payment/FraudCheck/CalculateRiskScore] Calculating risk score for order '{message.OrderId}'...");
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
|
||||
int riskScore = new Random().Next(1, 100);
|
||||
|
||||
Console.WriteLine($" [Payment/FraudCheck/CalculateRiskScore] ✓ Risk score: {riskScore}/100 (Low risk)");
|
||||
Console.ResetColor();
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Charges the payment for an order.
|
||||
/// </summary>
|
||||
internal sealed class ChargePayment() : Executor<OrderInfo, OrderInfo>("ChargePayment")
|
||||
{
|
||||
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($" [Payment/ChargePayment] Charging ${message.Amount} for order '{message.OrderId}'...");
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
|
||||
|
||||
message.PaymentTransactionId = $"TXN-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
|
||||
|
||||
Console.WriteLine($" [Payment/ChargePayment] ✓ Payment processed: {message.PaymentTransactionId}");
|
||||
Console.ResetColor();
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Inventory Sub-Workflow Executors
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// Checks inventory availability for an order.
|
||||
/// </summary>
|
||||
internal sealed class CheckInventory() : Executor<OrderInfo, OrderInfo>("CheckInventory")
|
||||
{
|
||||
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.ForegroundColor = ConsoleColor.Magenta;
|
||||
Console.WriteLine($" [Inventory/CheckInventory] Checking inventory for order '{message.OrderId}'...");
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
|
||||
Console.WriteLine(" [Inventory/CheckInventory] ✓ Items available in stock");
|
||||
Console.ResetColor();
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reserves inventory for an order.
|
||||
/// </summary>
|
||||
internal sealed class ReserveInventory() : Executor<OrderInfo, OrderInfo>("ReserveInventory")
|
||||
{
|
||||
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Magenta;
|
||||
Console.WriteLine($" [Inventory/ReserveInventory] Reserving items for order '{message.OrderId}'...");
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
|
||||
|
||||
message.InventoryReservationId = $"RES-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
|
||||
|
||||
Console.WriteLine($" [Inventory/ReserveInventory] ✓ Reserved: {message.InventoryReservationId}");
|
||||
Console.ResetColor();
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Shipping Sub-Workflow Executors
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// Selects a shipping carrier for an order.
|
||||
/// </summary>
|
||||
internal sealed class SelectCarrier() : Executor<OrderInfo, OrderInfo>("SelectCarrier")
|
||||
{
|
||||
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.ForegroundColor = ConsoleColor.Blue;
|
||||
Console.WriteLine($" [Shipping/SelectCarrier] Selecting carrier for order '{message.OrderId}'...");
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
|
||||
message.Carrier = message.Amount > 50 ? "Express" : "Standard";
|
||||
|
||||
Console.WriteLine($" [Shipping/SelectCarrier] ✓ Selected carrier: {message.Carrier}");
|
||||
Console.ResetColor();
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates shipment and generates tracking number.
|
||||
/// </summary>
|
||||
internal sealed class CreateShipment() : Executor<OrderInfo, OrderInfo>("CreateShipment")
|
||||
{
|
||||
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Blue;
|
||||
Console.WriteLine($" [Shipping/CreateShipment] Creating shipment for order '{message.OrderId}'...");
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
|
||||
|
||||
message.TrackingNumber = $"TRACK-{Guid.NewGuid().ToString("N")[..10].ToUpperInvariant()}";
|
||||
|
||||
Console.WriteLine($" [Shipping/CreateShipment] ✓ Shipment created: {message.TrackingNumber}");
|
||||
Console.ResetColor();
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates nested workflows (sub-workflows) where one workflow
|
||||
// can be used as an executor within another workflow. This enables modular,
|
||||
// reusable workflow components with independent checkpointing and replay.
|
||||
//
|
||||
// Workflow structure:
|
||||
//
|
||||
// ┌─────────────────────────────────────────────────────────────────────────┐
|
||||
// │ OrderProcessing (Main Workflow) │
|
||||
// │ │
|
||||
// │ OrderReceived │
|
||||
// │ │ │
|
||||
// │ ▼ │
|
||||
// │ ┌─────────────────────────────────────────────────────────┐ │
|
||||
// │ │ Payment (Sub-Workflow) │ │
|
||||
// │ │ │ │
|
||||
// │ │ ValidatePayment │ │
|
||||
// │ │ │ │ │
|
||||
// │ │ ▼ │ │
|
||||
// │ │ ┌─────────────────────────────────────────┐ │ │
|
||||
// │ │ │ FraudCheck (Sub-Sub-Workflow) │ │ │
|
||||
// │ │ │ │ │ │
|
||||
// │ │ │ AnalyzePatterns ──► CalculateRiskScore │ │ │
|
||||
// │ │ └─────────────────────────────────────────┘ │ │
|
||||
// │ │ │ │ │
|
||||
// │ │ ▼ │ │
|
||||
// │ │ ChargePayment │ │
|
||||
// │ └─────────────────────────────────────────────────────────┘ │
|
||||
// │ │ │
|
||||
// │ ▼ │
|
||||
// │ ┌─────────────────────────────────────────────────────────┐ │
|
||||
// │ │ Inventory (Sub-Workflow) │ │
|
||||
// │ │ │ │
|
||||
// │ │ CheckInventory ──► ReserveInventory │ │
|
||||
// │ └─────────────────────────────────────────────────────────┘ │
|
||||
// │ │ │
|
||||
// │ ▼ │
|
||||
// │ ┌─────────────────────────────────────────────────────────┐ │
|
||||
// │ │ Shipping (Sub-Workflow) │ │
|
||||
// │ │ │ │
|
||||
// │ │ SelectCarrier ──► CreateShipment │ │
|
||||
// │ └─────────────────────────────────────────────────────────┘ │
|
||||
// │ │ │
|
||||
// │ ▼ │
|
||||
// │ OrderCompleted │
|
||||
// └─────────────────────────────────────────────────────────────────────────┘
|
||||
//
|
||||
// Each sub-workflow runs as a separate orchestration instance, providing:
|
||||
// - Modular, reusable workflow components
|
||||
// - Independent checkpointing and replay
|
||||
// - Hierarchical visualization in the DTS dashboard
|
||||
// - Failure isolation between parent and child workflows
|
||||
|
||||
using Microsoft.Agents.AI.DurableTask;
|
||||
using Microsoft.Agents.AI.DurableTask.Workflows;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.DurableTask.Client.AzureManaged;
|
||||
using Microsoft.DurableTask.Worker.AzureManaged;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NestedWorkflows;
|
||||
|
||||
string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING")
|
||||
?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";
|
||||
|
||||
// ============================================
|
||||
// Step 1: Build the Fraud Check sub-sub-workflow (Level 2 nesting)
|
||||
// ============================================
|
||||
AnalyzePatterns analyzePatterns = new();
|
||||
CalculateRiskScore calculateRiskScore = new();
|
||||
|
||||
Workflow fraudCheckWorkflow = new WorkflowBuilder(analyzePatterns)
|
||||
.WithName("SubFraudCheck")
|
||||
.WithDescription("Analyzes transaction patterns and calculates risk score")
|
||||
.AddEdge(analyzePatterns, calculateRiskScore)
|
||||
.Build();
|
||||
|
||||
// ============================================
|
||||
// Step 2: Build the Payment Processing sub-workflow (with nested FraudCheck)
|
||||
// ============================================
|
||||
ValidatePayment validatePayment = new();
|
||||
ExecutorBinding fraudCheckExecutor = fraudCheckWorkflow.BindAsExecutor("FraudCheck");
|
||||
ChargePayment chargePayment = new();
|
||||
|
||||
Workflow paymentWorkflow = new WorkflowBuilder(validatePayment)
|
||||
.WithName("SubPaymentProcessing")
|
||||
.WithDescription("Validates and processes payment for an order")
|
||||
.AddEdge(validatePayment, fraudCheckExecutor)
|
||||
.AddEdge(fraudCheckExecutor, chargePayment)
|
||||
.Build();
|
||||
|
||||
// ============================================
|
||||
// Step 3: Build the Inventory Management sub-workflow
|
||||
// ============================================
|
||||
CheckInventory checkInventory = new();
|
||||
ReserveInventory reserveInventory = new();
|
||||
|
||||
Workflow inventoryWorkflow = new WorkflowBuilder(checkInventory)
|
||||
.WithName("SubInventoryManagement")
|
||||
.WithDescription("Checks availability and reserves inventory")
|
||||
.AddEdge(checkInventory, reserveInventory)
|
||||
.Build();
|
||||
|
||||
// ============================================
|
||||
// Step 4: Build the Shipping Arrangement sub-workflow
|
||||
// ============================================
|
||||
SelectCarrier selectCarrier = new();
|
||||
CreateShipment createShipment = new();
|
||||
|
||||
Workflow shippingWorkflow = new WorkflowBuilder(selectCarrier)
|
||||
.WithName("SubShippingArrangement")
|
||||
.WithDescription("Selects carrier and creates shipment")
|
||||
.AddEdge(selectCarrier, createShipment)
|
||||
.Build();
|
||||
|
||||
// ============================================
|
||||
// Step 5: Build the Main Order Processing workflow using sub-workflows
|
||||
// ============================================
|
||||
ExecutorBinding paymentExecutor = paymentWorkflow.BindAsExecutor("Payment");
|
||||
ExecutorBinding inventoryExecutor = inventoryWorkflow.BindAsExecutor("Inventory");
|
||||
ExecutorBinding shippingExecutor = shippingWorkflow.BindAsExecutor("Shipping");
|
||||
|
||||
OrderReceived orderReceived = new();
|
||||
OrderCompleted orderCompleted = new();
|
||||
|
||||
// Main workflow: OrderReceived -> Payment -> Inventory -> Shipping -> OrderCompleted
|
||||
Workflow orderProcessingWorkflow = new WorkflowBuilder(orderReceived)
|
||||
.WithName("OrderProcessing")
|
||||
.WithDescription("Processes an order through payment, inventory, and shipping")
|
||||
.AddEdge(orderReceived, paymentExecutor)
|
||||
.AddEdge(paymentExecutor, inventoryExecutor)
|
||||
.AddEdge(inventoryExecutor, shippingExecutor)
|
||||
.AddEdge(shippingExecutor, orderCompleted)
|
||||
.Build();
|
||||
|
||||
// ============================================
|
||||
// Step 6: Configure and start the host
|
||||
// ============================================
|
||||
IHost host = Host.CreateDefaultBuilder(args)
|
||||
.ConfigureLogging(logging => logging.SetMinimumLevel(LogLevel.Warning))
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
// Register only the main workflow - sub-workflows are discovered automatically!
|
||||
services.ConfigureDurableWorkflows(
|
||||
workflowOptions => workflowOptions.AddWorkflow(orderProcessingWorkflow),
|
||||
workerBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString),
|
||||
clientBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString));
|
||||
})
|
||||
.Build();
|
||||
|
||||
await host.StartAsync();
|
||||
|
||||
IWorkflowClient workflowClient = host.Services.GetRequiredService<IWorkflowClient>();
|
||||
|
||||
Console.WriteLine("╔══════════════════════════════════════════════════════════════════╗");
|
||||
Console.WriteLine("║ Nested Workflows Sample ║");
|
||||
Console.WriteLine("╠══════════════════════════════════════════════════════════════════╣");
|
||||
Console.WriteLine("║ Main Workflow: OrderProcessing ║");
|
||||
Console.WriteLine("║ ├── Payment (sub-workflow) ║");
|
||||
Console.WriteLine("║ │ ├── ValidatePayment ║");
|
||||
Console.WriteLine("║ │ ├── FraudCheck (sub-sub-workflow) ← Level 2 nesting! ║");
|
||||
Console.WriteLine("║ │ │ ├── AnalyzePatterns ║");
|
||||
Console.WriteLine("║ │ │ └── CalculateRiskScore ║");
|
||||
Console.WriteLine("║ │ └── ChargePayment ║");
|
||||
Console.WriteLine("║ ├── Inventory (sub-workflow) ║");
|
||||
Console.WriteLine("║ │ ├── CheckInventory ║");
|
||||
Console.WriteLine("║ │ └── ReserveInventory ║");
|
||||
Console.WriteLine("║ └── Shipping (sub-workflow) ║");
|
||||
Console.WriteLine("║ ├── SelectCarrier ║");
|
||||
Console.WriteLine("║ └── CreateShipment ║");
|
||||
Console.WriteLine("╚══════════════════════════════════════════════════════════════════╝");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Open the DTS dashboard at http://localhost:8080 to see the");
|
||||
Console.WriteLine("parent-child orchestration hierarchy in the Timeline view!");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Enter an order ID (or 'exit'):");
|
||||
|
||||
while (true)
|
||||
{
|
||||
Console.Write("> ");
|
||||
string? input = Console.ReadLine();
|
||||
if (string.IsNullOrWhiteSpace(input) || input.Equals("exit", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await StartNewWorkflowAsync(input, orderProcessingWorkflow, workflowClient);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error: {ex.Message}");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
await host.StopAsync();
|
||||
|
||||
// Start a new workflow and wait for completion
|
||||
static async Task StartNewWorkflowAsync(string orderId, Workflow workflow, IWorkflowClient client)
|
||||
{
|
||||
Console.WriteLine($"\nStarting order processing for '{orderId}'...");
|
||||
|
||||
IAwaitableWorkflowRun run = (IAwaitableWorkflowRun)await client.RunAsync(workflow, orderId);
|
||||
Console.WriteLine($"Run ID: {run.RunId}");
|
||||
Console.WriteLine("Check the DTS dashboard Timeline tab to see sub-orchestrations!");
|
||||
Console.WriteLine();
|
||||
|
||||
try
|
||||
{
|
||||
Console.WriteLine("Waiting for workflow to complete...");
|
||||
string? result = await run.WaitForCompletionAsync<string>();
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine($"✓ Order completed: {result}");
|
||||
Console.ResetColor();
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine($"✗ Failed: {ex.Message}");
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
# Nested Workflows Sample
|
||||
|
||||
This sample demonstrates how to use **nested workflows** (sub-workflows) where one workflow can be used as an executor within another workflow. This enables modular, reusable workflow components with independent checkpointing and replay.
|
||||
|
||||
## Key Concepts Demonstrated
|
||||
|
||||
- Using `Workflow.BindAsExecutor()` to embed a workflow as an executor in another workflow
|
||||
- Multi-level nesting (sub-workflow within a sub-workflow)
|
||||
- Automatic discovery of sub-workflows when registering the main workflow
|
||||
- Independent orchestration instances for each sub-workflow
|
||||
|
||||
## Overview
|
||||
|
||||
The sample implements an order processing workflow with three sub-workflows and one sub-sub-workflow:
|
||||
|
||||
```
|
||||
OrderProcessing (Main Workflow)
|
||||
├── OrderReceived
|
||||
├── Payment (sub-workflow)
|
||||
│ ├── ValidatePayment
|
||||
│ ├── FraudCheck (sub-sub-workflow) ← Level 2 nesting!
|
||||
│ │ ├── AnalyzePatterns
|
||||
│ │ └── CalculateRiskScore
|
||||
│ └── ChargePayment
|
||||
├── Inventory (sub-workflow)
|
||||
│ ├── CheckInventory
|
||||
│ └── ReserveInventory
|
||||
├── Shipping (sub-workflow)
|
||||
│ ├── SelectCarrier
|
||||
│ └── CreateShipment
|
||||
└── OrderCompleted
|
||||
```
|
||||
|
||||
## How Nested Workflows Work
|
||||
|
||||
Sub-workflows are created using `BindAsExecutor()`, which converts a `Workflow` into an `ExecutorBinding` that can be used in another workflow's graph:
|
||||
|
||||
```csharp
|
||||
// Build a sub-workflow
|
||||
Workflow paymentWorkflow = new WorkflowBuilder(validatePayment)
|
||||
.WithName("SubPaymentProcessing")
|
||||
.AddEdge(validatePayment, fraudCheckExecutor)
|
||||
.AddEdge(fraudCheckExecutor, chargePayment)
|
||||
.Build();
|
||||
|
||||
// Bind as executor for use in the parent workflow
|
||||
ExecutorBinding paymentExecutor = paymentWorkflow.BindAsExecutor("Payment");
|
||||
|
||||
// Use in the main workflow
|
||||
Workflow mainWorkflow = new WorkflowBuilder(orderReceived)
|
||||
.AddEdge(orderReceived, paymentExecutor)
|
||||
.Build();
|
||||
```
|
||||
|
||||
Each sub-workflow runs as a separate orchestration instance in the Durable Task Scheduler, visible in the DTS dashboard Timeline view.
|
||||
|
||||
## Environment Setup
|
||||
|
||||
See the [README.md](../README.md) file in the parent directory for information on configuring the environment, including how to install and run the Durable Task Scheduler.
|
||||
|
||||
## Running the Sample
|
||||
|
||||
```bash
|
||||
cd dotnet/samples/Durable/Workflow/ConsoleApps/05_NestedWorkflows
|
||||
dotnet run --framework net10.0
|
||||
```
|
||||
|
||||
### Sample Output
|
||||
|
||||
```text
|
||||
╔══════════════════════════════════════════════════════════════════╗
|
||||
║ Nested Workflows Sample ║
|
||||
╠══════════════════════════════════════════════════════════════════╣
|
||||
║ Main Workflow: OrderProcessing ║
|
||||
║ ├── Payment (sub-workflow) ║
|
||||
║ │ ├── ValidatePayment ║
|
||||
║ │ ├── FraudCheck (sub-sub-workflow) ← Level 2 nesting! ║
|
||||
║ │ │ ├── AnalyzePatterns ║
|
||||
║ │ │ └── CalculateRiskScore ║
|
||||
║ │ └── ChargePayment ║
|
||||
║ ├── Inventory (sub-workflow) ║
|
||||
║ │ ├── CheckInventory ║
|
||||
║ │ └── ReserveInventory ║
|
||||
║ └── Shipping (sub-workflow) ║
|
||||
║ ├── SelectCarrier ║
|
||||
║ └── CreateShipment ║
|
||||
╚══════════════════════════════════════════════════════════════════╝
|
||||
|
||||
Enter an order ID (or 'exit'):
|
||||
> ORD-001
|
||||
Starting order processing for 'ORD-001'...
|
||||
Run ID: abc123...
|
||||
Waiting for workflow to complete...
|
||||
✓ Order completed: Order ORD-001 completed. Tracking: TRACK-1A2B3C4D5E
|
||||
```
|
||||
@@ -197,24 +197,33 @@ public static class ServiceCollectionExtensions
|
||||
|
||||
// Configure Durable Task Worker - capture sharedOptions reference in closure.
|
||||
// The options object is populated by all Configure* calls before the worker starts.
|
||||
services.AddDurableTaskWorker(builder =>
|
||||
|
||||
if (workerBuilder is not null)
|
||||
{
|
||||
workerBuilder?.Invoke(builder);
|
||||
|
||||
builder.AddTasks(registry => RegisterTasksFromOptions(registry, sharedOptions));
|
||||
});
|
||||
services.AddDurableTaskWorker(builder =>
|
||||
{
|
||||
workerBuilder?.Invoke(builder);
|
||||
|
||||
builder.AddTasks(registry => RegisterTasksFromOptions(registry, sharedOptions));
|
||||
});
|
||||
}
|
||||
// Configure Durable Task Client
|
||||
// Only register a client if explicitly configured. For Azure Functions,
|
||||
// the Functions extension provides the DurableTaskClient automatically via bindings.
|
||||
if (clientBuilder is not null)
|
||||
{
|
||||
services.AddDurableTaskClient(clientBuilder);
|
||||
|
||||
// These services depend on DurableTaskClient from DI, so only register them
|
||||
// when we're registering a client. For Azure Functions, the client comes from
|
||||
// bindings, not DI, so these won't work there.
|
||||
services.TryAddSingleton<DurableWorkflowClient>();
|
||||
services.TryAddSingleton<IWorkflowClient>(sp => sp.GetRequiredService<DurableWorkflowClient>());
|
||||
services.TryAddSingleton<IDurableAgentClient, DefaultDurableAgentClient>();
|
||||
}
|
||||
|
||||
// Register workflow and agent services
|
||||
services.TryAddSingleton<DurableWorkflowClient>();
|
||||
services.TryAddSingleton<IWorkflowClient>(sp => sp.GetRequiredService<DurableWorkflowClient>());
|
||||
// Register workflow and agent services that don't depend on DurableTaskClient
|
||||
services.TryAddSingleton<DataConverter, DurableDataConverter>();
|
||||
services.TryAddSingleton<IDurableAgentClient, DefaultDurableAgentClient>();
|
||||
|
||||
// Register agent factories resolver - returns factories from the shared options
|
||||
services.TryAddSingleton(
|
||||
@@ -257,7 +266,11 @@ public static class ServiceCollectionExtensions
|
||||
ExecutorBinding binding = activity.Binding;
|
||||
registry.AddActivityFunc<string, string>(
|
||||
activity.ActivityName,
|
||||
(context, input) => DurableActivityExecutor.ExecuteAsync(binding, input));
|
||||
(context, input) =>
|
||||
{
|
||||
// to do:a
|
||||
return DurableActivityExecutor.ExecuteAsync(binding, input);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,19 +14,20 @@ using Microsoft.Extensions.Logging;
|
||||
namespace Microsoft.Agents.AI.DurableTask.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Dispatches workflow executors to either activities or AI agents.
|
||||
/// Dispatches workflow executors to activities, AI agents, or sub-workflow orchestrations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Called during the dispatch phase of each superstep by
|
||||
/// <c>DurableWorkflowRunner.DispatchExecutorsInParallelAsync</c>. For each executor that has
|
||||
/// pending input, this dispatcher determines whether the executor is an AI agent (stateful,
|
||||
/// backed by Durable Entities) or a regular activity, and invokes the appropriate Durable Task API.
|
||||
/// backed by Durable Entities), a sub-workflow (child orchestration), or a regular activity,
|
||||
/// and invokes the appropriate Durable Task API.
|
||||
/// The serialised string result is returned to the runner for the routing phase.
|
||||
/// </remarks>
|
||||
internal static class DurableExecutorDispatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// Dispatches an executor based on its type (activity or AI agent).
|
||||
/// Dispatches an executor based on its type (activity, AI agent, or sub-workflow).
|
||||
/// </summary>
|
||||
/// <param name="context">The task orchestration context.</param>
|
||||
/// <param name="executorInfo">Information about the executor to dispatch.</param>
|
||||
@@ -46,6 +47,11 @@ internal static class DurableExecutorDispatcher
|
||||
return await ExecuteAgentAsync(context, executorInfo, logger, envelope.Message).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
if (executorInfo.IsSubworkflowExecutor)
|
||||
{
|
||||
return await ExecuteSubWorkflowAsync(context, executorInfo, envelope.Message).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
return await ExecuteActivityAsync(context, executorInfo, envelope.Message, envelope.InputTypeName).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
@@ -96,4 +102,29 @@ internal static class DurableExecutorDispatcher
|
||||
|
||||
return response.Text;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a sub-workflow as a child orchestration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The input is passed as a raw JSON object (not a string) to avoid double-encoding.
|
||||
/// </remarks>
|
||||
private static async Task<string> ExecuteSubWorkflowAsync(
|
||||
TaskOrchestrationContext context,
|
||||
WorkflowExecutorInfo executorInfo,
|
||||
string input)
|
||||
{
|
||||
string orchestrationName = WorkflowNamingHelper.ToOrchestrationFunctionName(executorInfo.SubWorkflow!.Name!);
|
||||
|
||||
// Parse the input JSON to pass as an object, preventing double-encoding.
|
||||
// The sub-workflow orchestrator receives DurableWorkflowInput<object> where Input
|
||||
// should be the deserialized object (not a JSON string).
|
||||
using JsonDocument doc = JsonDocument.Parse(input);
|
||||
object inputObj = doc.RootElement.Clone();
|
||||
DurableWorkflowInput<object> workflowInput = new() { Input = inputObj };
|
||||
|
||||
return await context.CallSubOrchestratorAsync<string>(
|
||||
orchestrationName,
|
||||
workflowInput).ConfigureAwait(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace Microsoft.Agents.AI.DurableTask.Workflows;
|
||||
/// <summary>
|
||||
/// Runs workflow orchestrations using message-driven superstep execution with Durable Task.
|
||||
/// </summary>
|
||||
internal sealed class DurableWorkflowRunner
|
||||
public class DurableWorkflowRunner
|
||||
{
|
||||
private const int MaxSupersteps = 100;
|
||||
|
||||
@@ -75,7 +75,7 @@ internal sealed class DurableWorkflowRunner
|
||||
/// Initializes a new instance of the <see cref="DurableWorkflowRunner"/> class.
|
||||
/// </summary>
|
||||
/// <param name="durableOptions">The durable options containing workflow configurations.</param>
|
||||
internal DurableWorkflowRunner(DurableOptions durableOptions)
|
||||
public DurableWorkflowRunner(DurableOptions durableOptions)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(durableOptions);
|
||||
|
||||
|
||||
@@ -102,6 +102,43 @@ internal sealed class BuiltInFunctionExecutor : IFunctionExecutor
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.FunctionDefinition.EntryPoint == BuiltInFunctions.RunWorkflowOrechstrtationHttpFunctionEntryPoint)
|
||||
{
|
||||
if (httpRequestData == null)
|
||||
{
|
||||
throw new InvalidOperationException($"HTTP request data binding is missing for the invocation {context.InvocationId}.");
|
||||
}
|
||||
context.GetInvocationResult().Value = await BuiltInFunctions.RunWorkflowOrechstrtationHttpTriggerAsync(
|
||||
httpRequestData,
|
||||
durableTaskClient,
|
||||
context);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.FunctionDefinition.EntryPoint == BuiltInFunctions.InvokeWorkflowOrchestrationFunctionEntryPoint)
|
||||
{
|
||||
// The orchestration trigger binding provides an encoded orchestration request string.
|
||||
// We use the same string binding that entities use (encodedEntityRequest is reused for orchestrations).
|
||||
if (encodedEntityRequest is null)
|
||||
{
|
||||
throw new InvalidOperationException($"Orchestration trigger binding is missing for the invocation {context.InvocationId}.");
|
||||
}
|
||||
|
||||
// Execute the orchestration using the static method in BuiltInFunctions
|
||||
context.GetInvocationResult().Value = BuiltInFunctions.InvokeWorkflowOrchestration(
|
||||
encodedEntityRequest,
|
||||
context);
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.FunctionDefinition.EntryPoint == BuiltInFunctions.InvokeWorkflowActivityFunctionEntryPoint)
|
||||
{
|
||||
// to do
|
||||
context.GetInvocationResult().Value = await BuiltInFunctions.InvokeWorkflowActivityAsync(encodedEntityRequest!, durableTaskClient, context);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Unsupported function entry point '{context.FunctionDefinition.EntryPoint}' for invocation {context.InvocationId}.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,16 @@
|
||||
using System.Net;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Agents.AI.DurableTask;
|
||||
using Microsoft.Agents.AI.DurableTask.Workflows;
|
||||
using Microsoft.Azure.Functions.Worker;
|
||||
using Microsoft.Azure.Functions.Worker.Extensions.Mcp;
|
||||
using Microsoft.Azure.Functions.Worker.Http;
|
||||
using Microsoft.DurableTask;
|
||||
using Microsoft.DurableTask.Client;
|
||||
using Microsoft.DurableTask.Worker.Grpc;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
|
||||
|
||||
@@ -22,6 +25,120 @@ internal static class BuiltInFunctions
|
||||
internal static readonly string RunAgentEntityFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(InvokeAgentAsync)}";
|
||||
internal static readonly string RunAgentMcpToolFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(RunMcpToolAsync)}";
|
||||
|
||||
internal static readonly string RunWorkflowOrechstrtationHttpFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(RunWorkflowOrechstrtationHttpTriggerAsync)}";
|
||||
internal static readonly string InvokeWorkflowOrchestrationFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(InvokeWorkflowOrchestration)}";
|
||||
|
||||
internal static readonly string InvokeWorkflowActivityFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(InvokeWorkflowActivityAsync)}";
|
||||
internal static readonly string RunWorkflowMcpToolFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(RunWorkflowMcpToolAsync)}";
|
||||
|
||||
#pragma warning disable IL3000 // Avoid accessing Assembly file path when publishing as a single file - Azure Functions does not use single-file publishing
|
||||
internal static readonly string ScriptFile = Path.GetFileName(typeof(BuiltInFunctions).Assembly.Location);
|
||||
#pragma warning restore IL3000
|
||||
|
||||
/// <summary>
|
||||
/// Invokes a workflow orchestration using the encoded orchestration request.
|
||||
/// </summary>
|
||||
/// <param name="encodedOrchestratorRequest">The base64-encoded protobuf payload for the orchestration.</param>
|
||||
/// <param name="functionContext">The function context.</param>
|
||||
/// <returns>The encoded orchestration response.</returns>
|
||||
internal static string InvokeWorkflowOrchestration(
|
||||
string encodedOrchestratorRequest,
|
||||
FunctionContext functionContext)
|
||||
{
|
||||
DurableOptions durableOptions = functionContext.InstanceServices.GetRequiredService<DurableOptions>();
|
||||
|
||||
return GrpcOrchestrationRunner.LoadAndRun<DurableWorkflowInput<object>, string>(
|
||||
encodedOrchestratorRequest,
|
||||
(orchestrationContext, input) => RunWorkflowOrchestrationCoreAsync(orchestrationContext, input, durableOptions)!,
|
||||
functionContext.InstanceServices);
|
||||
}
|
||||
|
||||
private static async Task<string> RunWorkflowOrchestrationCoreAsync(
|
||||
TaskOrchestrationContext orchestrationContext,
|
||||
DurableWorkflowInput<object>? input,
|
||||
DurableOptions durableOptions)
|
||||
{
|
||||
ILogger logger = orchestrationContext.CreateReplaySafeLogger("DurableWorkflow");
|
||||
DurableWorkflowRunner runner = new(durableOptions);
|
||||
|
||||
// ConfigureAwait(true) is required in orchestration code for deterministic replay.
|
||||
return await runner.RunWorkflowOrchestrationAsync(
|
||||
orchestrationContext,
|
||||
input ?? new DurableWorkflowInput<object> { Input = string.Empty },
|
||||
logger).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes a workflow orchestration in response to an HTTP request.
|
||||
/// </summary>
|
||||
public static async Task<HttpResponseData> RunWorkflowOrechstrtationHttpTriggerAsync(
|
||||
[HttpTrigger] HttpRequestData req,
|
||||
[DurableClient] DurableTaskClient client,
|
||||
FunctionContext context)
|
||||
{
|
||||
var workflowName = context.FunctionDefinition.Name.Replace(HttpPrefix, string.Empty);
|
||||
var orchestrationFunctionName = WorkflowNamingHelper.ToOrchestrationFunctionName(workflowName);
|
||||
var inputMessage = await req.ReadAsStringAsync();
|
||||
|
||||
DurableWorkflowInput<string> orchestrtionInput = new() { Input = inputMessage! };
|
||||
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(orchestrationFunctionName, orchestrtionInput);
|
||||
|
||||
HttpResponseData response = req.CreateResponse(HttpStatusCode.Accepted);
|
||||
await response.WriteStringAsync($"InvokeWorkflowOrechstrtationAsync is invoked for {workflowName}. Orchestration instanceId: {instanceId}");
|
||||
return response;
|
||||
}
|
||||
|
||||
public static Task<string> InvokeWorkflowActivityAsync(
|
||||
[ActivityTrigger] string input,
|
||||
[DurableClient] DurableTaskClient durableTaskClient,
|
||||
FunctionContext functionContext)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(input);
|
||||
ArgumentNullException.ThrowIfNull(durableTaskClient);
|
||||
ArgumentNullException.ThrowIfNull(functionContext);
|
||||
|
||||
string activityFunctionName = functionContext.FunctionDefinition.Name;
|
||||
string executorName = WorkflowNamingHelper.ToWorkflowName(activityFunctionName);
|
||||
|
||||
DurableOptions durableOptions = functionContext.InstanceServices.GetRequiredService<DurableOptions>();
|
||||
if (!durableOptions.Workflows.Executors.TryGetExecutor(executorName, out ExecutorRegistration? registration))
|
||||
{
|
||||
throw new InvalidOperationException($"Executor '{executorName}' not found in workflow options.");
|
||||
}
|
||||
|
||||
return DurableActivityExecutor.ExecuteAsync(registration.Binding, input, functionContext.CancellationToken);
|
||||
}
|
||||
|
||||
public static async Task<string?> RunWorkflowMcpToolAsync(
|
||||
[McpToolTrigger("BuiltInWorkflowMcpTool")] ToolInvocationContext context,
|
||||
[DurableClient] DurableTaskClient client,
|
||||
FunctionContext functionContext)
|
||||
{
|
||||
if (context.Arguments is null)
|
||||
{
|
||||
throw new ArgumentException("MCP Tool invocation is missing required arguments.");
|
||||
}
|
||||
|
||||
if (!context.Arguments.TryGetValue("input", out object? inputObj) || inputObj is not string input)
|
||||
{
|
||||
throw new ArgumentException("MCP Tool invocation is missing required 'input' argument of type string.");
|
||||
}
|
||||
|
||||
// Extract workflow name from the MCP tool name (format: mcptool-workflow-{workflowName})
|
||||
string workflowName = context.Name;
|
||||
string orchestrationFunctionName = WorkflowNamingHelper.ToOrchestrationFunctionName(workflowName);
|
||||
|
||||
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(orchestrationFunctionName, input);
|
||||
|
||||
// Wait for the orchestration to complete and return the result
|
||||
OrchestrationMetadata? metadata = await client.WaitForInstanceCompletionAsync(
|
||||
instanceId,
|
||||
getInputsAndOutputs: true,
|
||||
cancellation: functionContext.CancellationToken);
|
||||
|
||||
return metadata?.ReadOutputAs<string>();
|
||||
}
|
||||
|
||||
// Exposed as an entity trigger via AgentFunctionsProvider
|
||||
public static Task<string> InvokeAgentAsync(
|
||||
[DurableClient] DurableTaskClient client,
|
||||
|
||||
+161
-1
@@ -1,11 +1,18 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.DurableTask;
|
||||
using Microsoft.Agents.AI.DurableTask.Workflows;
|
||||
using Microsoft.Agents.AI.Hosting.AzureFunctions.Workflows;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Azure.Functions.Worker;
|
||||
using Microsoft.Azure.Functions.Worker.Builder;
|
||||
using Microsoft.Azure.Functions.Worker.Core.FunctionMetadata;
|
||||
using Microsoft.DurableTask;
|
||||
using Microsoft.DurableTask.Worker;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
|
||||
|
||||
@@ -14,6 +21,153 @@ namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
|
||||
/// </summary>
|
||||
public static class FunctionsApplicationBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures durable agents and workflows in a unified way.
|
||||
/// </summary>
|
||||
/// <param name="builder">The Functions application builder.</param>
|
||||
/// <param name="configure">A delegate to configure the durable options.</param>
|
||||
/// <returns>The Functions application builder for method chaining.</returns>
|
||||
/// <remarks>
|
||||
/// This method provides a unified configuration point for both durable agents and workflows.
|
||||
/// It automatically generates HTTP API endpoints for agents and workflows, and configures
|
||||
/// the necessary middleware and services for durable execution.
|
||||
/// </remarks>
|
||||
public static FunctionsApplicationBuilder ConfigureDurableOptions(
|
||||
this FunctionsApplicationBuilder builder,
|
||||
Action<DurableOptions> configure)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
ArgumentNullException.ThrowIfNull(configure);
|
||||
|
||||
DurableOptions options = new();
|
||||
configure(options);
|
||||
|
||||
builder.Services.AddSingleton(options);
|
||||
|
||||
if (options.Workflows.Workflows.Count > 0)
|
||||
{
|
||||
ConfigureWorkflowOrchestrations(builder, options.Workflows);
|
||||
// Do things to enable workflow as orchestrator functions.
|
||||
// Register the Workflow metadata transformer.
|
||||
builder.ConfigureDurableWorkflows(durableWorkflwoOptions =>
|
||||
{
|
||||
// what
|
||||
});
|
||||
|
||||
builder.Services.AddSingleton<IFunctionMetadataTransformer, DurableWorkflowFunctionMetadataTransformer>();
|
||||
|
||||
builder.UseWhen<BuiltInFunctionExecutionMiddleware>(static context =>
|
||||
string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentHttpFunctionEntryPoint, StringComparison.Ordinal) ||
|
||||
string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentMcpToolFunctionEntryPoint, StringComparison.Ordinal) ||
|
||||
string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentEntityFunctionEntryPoint, StringComparison.Ordinal)
|
||||
|| string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunWorkflowOrechstrtationHttpFunctionEntryPoint, StringComparison.Ordinal)
|
||||
|
||||
|| string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunWorkflowOrechstrtationHttpFunctionEntryPoint, StringComparison.Ordinal)
|
||||
|| string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.InvokeWorkflowOrchestrationFunctionEntryPoint, StringComparison.Ordinal)
|
||||
|| string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.InvokeWorkflowActivityFunctionEntryPoint, StringComparison.Ordinal)
|
||||
);
|
||||
builder.Services.AddSingleton<BuiltInFunctionExecutor>();
|
||||
|
||||
//builder.UseWhen<BuiltInFunctionExecutionMiddleware>(static context =>
|
||||
// string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunWorkflowOrechstrtationHttpFunctionEntryPoint, StringComparison.Ordinal)
|
||||
// || string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.InvokeWorkflowOrchestrationFunctionEntryPoint, StringComparison.Ordinal)
|
||||
// || string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.InvokeWorkflowActivityFunctionEntryPoint, StringComparison.Ordinal)
|
||||
// || string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentHttpFunctionEntryPoint, StringComparison.Ordinal)
|
||||
// );
|
||||
//builder.Services.AddSingleton<BuiltInFunctionExecutor>();
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static void ConfigureWorkflowOrchestrations(FunctionsApplicationBuilder builder, DurableWorkflowOptions workflows)
|
||||
{
|
||||
// Discover sub-workflows recursively and add them to the workflows dictionary
|
||||
// so they are registered as separate orchestrations alongside the main workflows.
|
||||
DiscoverSubWorkflows(workflows);
|
||||
|
||||
builder.ConfigureDurableWorker().AddTasks(tasks =>
|
||||
{
|
||||
foreach (string workflowName in workflows.Workflows.Select(kp => kp.Key))
|
||||
{
|
||||
string orchestrationFunctionName = WorkflowNamingHelper.ToOrchestrationFunctionName(workflowName);
|
||||
|
||||
tasks.AddOrchestratorFunc<DurableWorkflowInput<object>, string>(
|
||||
orchestrationFunctionName,
|
||||
async (orchestrationContext, orchInput) =>
|
||||
{
|
||||
FunctionContext functionContext = orchestrationContext.GetFunctionContext()
|
||||
?? throw new InvalidOperationException("FunctionContext is not available in the orchestration context.");
|
||||
|
||||
DurableWorkflowRunner runner = functionContext.InstanceServices.GetRequiredService<DurableWorkflowRunner>();
|
||||
ILogger logger = orchestrationContext.CreateReplaySafeLogger(orchestrationFunctionName);
|
||||
DurableWorkflowInput<object> workflowInput = orchInput;
|
||||
|
||||
return await runner.RunWorkflowOrchestrationAsync(orchestrationContext, workflowInput, logger).ConfigureAwait(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void DiscoverSubWorkflows(DurableWorkflowOptions workflows)
|
||||
{
|
||||
HashSet<string> visited = new(workflows.Workflows.Keys);
|
||||
Queue<Workflow> queue = new(workflows.Workflows.Values);
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
Workflow workflow = queue.Dequeue();
|
||||
|
||||
foreach (ExecutorBinding binding in workflow.ReflectExecutors().Values)
|
||||
{
|
||||
if (binding is SubworkflowBinding subworkflowBinding)
|
||||
{
|
||||
Workflow subWorkflow = subworkflowBinding.WorkflowInstance;
|
||||
if (subWorkflow.Name is not null && visited.Add(subWorkflow.Name))
|
||||
{
|
||||
workflows.AddWorkflow(subWorkflow);
|
||||
queue.Enqueue(subWorkflow);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
internal static FunctionsApplicationBuilder RegisterWorkflowServices(this FunctionsApplicationBuilder builder)
|
||||
{
|
||||
// Register FunctionsWorkflowRunner as a singleton
|
||||
// builder.Services.TryAddSingleton<FunctionsWorkflowRunner>();
|
||||
|
||||
// Also register it as DurableWorkflowRunner so orchestrations can resolve it by base type
|
||||
//builder.Services.TryAddSingleton<DurableWorkflowRunner>(sp => sp.GetRequiredService<FunctionsWorkflowRunner>());
|
||||
|
||||
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IFunctionMetadataTransformer, DurableWorkflowFunctionMetadataTransformer>());
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures durable workflow services for the application and allows customization of durable workflow options.
|
||||
/// </summary>
|
||||
/// <remarks>This method registers the services required for durable workflows using
|
||||
/// Microsoft.DurableTask.Workflows. Call this method during application startup to enable durable workflows in your
|
||||
/// Azure Functions app.</remarks>
|
||||
/// <param name="builder">The application builder used to configure services and middleware for the Azure Functions app.</param>
|
||||
/// <param name="configure">A delegate that is used to configure the durable workflow options. Cannot be null.</param>
|
||||
/// <returns>The same <see cref="FunctionsApplicationBuilder"/> instance that this method was called on, to support method
|
||||
/// chaining.</returns>
|
||||
public static FunctionsApplicationBuilder ConfigureDurableWorkflows(this FunctionsApplicationBuilder builder, Action<DurableWorkflowOptions> configure)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(configure);
|
||||
|
||||
//RegisterWorkflowServices(builder);
|
||||
//builder.Services.AddSingleton<IFunctionMetadataTransformer, DurableWorkflowFunctionMetadataTransformer>();
|
||||
|
||||
// The main durable workflows services registration is done in Microsoft.DurableTask.Workflows.
|
||||
builder.Services.ConfigureDurableWorkflows(configure);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the application to use durable agents with a builder pattern.
|
||||
/// </summary>
|
||||
@@ -38,7 +192,13 @@ public static class FunctionsApplicationBuilderExtensions
|
||||
builder.UseWhen<BuiltInFunctionExecutionMiddleware>(static context =>
|
||||
string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentHttpFunctionEntryPoint, StringComparison.Ordinal) ||
|
||||
string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentMcpToolFunctionEntryPoint, StringComparison.Ordinal) ||
|
||||
string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentEntityFunctionEntryPoint, StringComparison.Ordinal));
|
||||
string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentEntityFunctionEntryPoint, StringComparison.Ordinal)
|
||||
|| string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunWorkflowOrechstrtationHttpFunctionEntryPoint, StringComparison.Ordinal)
|
||||
|
||||
|| string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunWorkflowOrechstrtationHttpFunctionEntryPoint, StringComparison.Ordinal)
|
||||
|| string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.InvokeWorkflowOrchestrationFunctionEntryPoint, StringComparison.Ordinal)
|
||||
|| string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.InvokeWorkflowActivityFunctionEntryPoint, StringComparison.Ordinal)
|
||||
);
|
||||
builder.Services.AddSingleton<BuiltInFunctionExecutor>();
|
||||
|
||||
return builder;
|
||||
|
||||
+4
-1
@@ -4,7 +4,10 @@
|
||||
<TargetFrameworks>$(TargetFrameworksCore)</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<!-- CA2007: This rule should generally be suppressed in Durable Task libraries. Also, this is not library code. -->
|
||||
<NoWarn>$(NoWarn);CA2007</NoWarn>
|
||||
<!-- AD0001: Suppress analyzer crashes from DurableTask analyzers (known bug with certain code patterns) -->
|
||||
<NoWarn>$(NoWarn);CA2007;AD0001</NoWarn>
|
||||
<!-- Ensure analyzer exceptions don't fail the build -->
|
||||
<ReportAnalyzerExceptionsAsErrors>false</ReportAnalyzerExceptionsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.DurableTask;
|
||||
using Microsoft.Agents.AI.DurableTask.Workflows;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Azure.Functions.Worker.Core.FunctionMetadata;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.AzureFunctions.Workflows;
|
||||
|
||||
internal sealed class DurableWorkflowFunctionMetadataTransformer : IFunctionMetadataTransformer
|
||||
{
|
||||
private static readonly HashSet<string> _registeredFunctionNames = new();
|
||||
private readonly ILogger<DurableWorkflowFunctionMetadataTransformer> _logger;
|
||||
private readonly DurableWorkflowOptions _options;
|
||||
|
||||
public DurableWorkflowFunctionMetadataTransformer(ILogger<DurableWorkflowFunctionMetadataTransformer> logger, DurableOptions durableOptions)
|
||||
{
|
||||
this._logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
ArgumentNullException.ThrowIfNull(durableOptions);
|
||||
this._options = durableOptions.Workflows;
|
||||
}
|
||||
|
||||
public string Name => nameof(DurableWorkflowFunctionMetadataTransformer);
|
||||
|
||||
public void Transform(IList<IFunctionMetadata> original)
|
||||
{
|
||||
this._logger.LogTransformStart(original.Count);
|
||||
|
||||
foreach (var workflow in this._options.Workflows)
|
||||
{
|
||||
this._logger.LogAddingWorkflowFunction(workflow.Key);
|
||||
|
||||
// Currently due to how durable executor is registered, we are not able to bind TaskOrechestrationContext parameter properly
|
||||
// because the InputBinding for TOC happens inside the DurableExecutor (rathen than in an input converter).
|
||||
// So for now, we are going to use single orchestration function for all workflows.
|
||||
//original.Add(CreateOrchestrationTrigger(workflow.Key));
|
||||
|
||||
// We also want to create an HTTP trigger for this orchestration so users can start it via HTTP.
|
||||
this._logger.LogAddingHttpTrigger(workflow.Key);
|
||||
original.Add(CreateHttpTrigger(workflow.Key, $"workflows/{workflow.Key}/run"));
|
||||
|
||||
// Check if MCP tool trigger is enabled for this workflow
|
||||
if (DurableWorkflowOptionsExtensions.TryGetWorkflowOptions(workflow.Key, out FunctionsWorkflowOptions? workflowOptions) &&
|
||||
workflowOptions?.McpToolTrigger.IsEnabled == true)
|
||||
{
|
||||
this._logger.LogAddingMcpToolTrigger(workflow.Key);
|
||||
original.Add(CreateMcpToolTrigger(workflow.Key, workflow.Value.Description));
|
||||
}
|
||||
|
||||
// Create activity/entity functions for each executor in the workflow based on their type
|
||||
// Extract executor IDs from edges and start executor
|
||||
HashSet<string> executorIds = new() { workflow.Value.StartExecutorId };
|
||||
|
||||
var reflectedEdges = workflow.Value.ReflectEdges();
|
||||
foreach (var (sourceId, edgeSet) in reflectedEdges)
|
||||
{
|
||||
executorIds.Add(sourceId);
|
||||
foreach (var edge in edgeSet)
|
||||
{
|
||||
foreach (var sinkId in edge.Connection.SinkIds)
|
||||
{
|
||||
executorIds.Add(sinkId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Dictionary<string, ExecutorBinding> executorBindings = workflow.Value.ReflectExecutors();
|
||||
|
||||
foreach (string executorId in executorIds)
|
||||
{
|
||||
if (executorBindings.TryGetValue(executorId, out ExecutorBinding? executorBinding))
|
||||
{
|
||||
// Sub-workflow bindings are registered as separate orchestrations, not as activities
|
||||
if (executorBinding is SubworkflowBinding)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string executorName = WorkflowNamingHelper.GetExecutorName(executorId);
|
||||
string functionName = WorkflowNamingHelper.ToOrchestrationFunctionName(executorName);
|
||||
|
||||
// Skip if this function has already been registered by another workflow
|
||||
if (!_registeredFunctionNames.Add(functionName))
|
||||
{
|
||||
this._logger.LogSkippingDuplicateFunction(functionName, workflow.Key);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the executor type is an agent-related type
|
||||
if (executorBinding is AIAgentBinding)
|
||||
{
|
||||
this._logger.LogAddingAgentEntityFunction(executorId, executorBinding.ExecutorType.FullName ?? executorBinding.ExecutorType.Name, workflow.Key);
|
||||
original.Add(CreateEntityTrigger(executorName));
|
||||
}
|
||||
else
|
||||
{
|
||||
this._logger.LogAddingActivityFunction(executorId, executorBinding.ExecutorType.FullName ?? executorBinding.ExecutorType.Name, workflow.Key);
|
||||
original.Add(CreateActivityTrigger(functionName));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._logger.LogTransformFinished(original.Count);
|
||||
}
|
||||
|
||||
private static DefaultFunctionMetadata CreateHttpTrigger(string name, string route)
|
||||
{
|
||||
return new DefaultFunctionMetadata()
|
||||
{
|
||||
Name = $"{BuiltInFunctions.HttpPrefix}{name}",
|
||||
Language = "dotnet-isolated",
|
||||
RawBindings =
|
||||
[
|
||||
$"{{\"name\":\"req\",\"type\":\"httpTrigger\",\"direction\":\"In\",\"authLevel\":\"function\",\"methods\": [\"post\"],\"route\":\"{route}\"}}",
|
||||
"{\"name\":\"$return\",\"type\":\"http\",\"direction\":\"Out\"}",
|
||||
"{\"name\":\"client\",\"type\":\"durableClient\",\"direction\":\"In\"}"
|
||||
],
|
||||
EntryPoint = BuiltInFunctions.RunWorkflowOrechstrtationHttpFunctionEntryPoint,
|
||||
ScriptFile = BuiltInFunctions.ScriptFile
|
||||
};
|
||||
}
|
||||
|
||||
//private static DefaultFunctionMetadata CreateOrchestrationTrigger(string name)
|
||||
//{
|
||||
// return new DefaultFunctionMetadata()
|
||||
// {
|
||||
// Name = AgentSessionId.ToEntityName(name),
|
||||
// Language = "dotnet-isolated",
|
||||
// RawBindings =
|
||||
// [
|
||||
// // """{"name":"context","type":"orchestrationTrigger","direction":"In"}""",
|
||||
// """{"name":"taskOrchestrationContext","type":"orchestrationTrigger","direction":"In"}""",
|
||||
|
||||
// ],
|
||||
// EntryPoint = BuiltInFunctions.RunWorkflowOrechstrtationFunctionEntryPoint,
|
||||
// ScriptFile = BuiltInFunctions.ScriptFile,
|
||||
// };
|
||||
//}
|
||||
|
||||
//private static DefaultFunctionMetadata CreateOrchestrationFunction(string functionName)
|
||||
//{
|
||||
// throw new NotImplementedException();
|
||||
//}
|
||||
|
||||
private static DefaultFunctionMetadata CreateActivityTrigger(string functionName)
|
||||
{
|
||||
return new DefaultFunctionMetadata()
|
||||
{
|
||||
Name = functionName,
|
||||
Language = "dotnet-isolated",
|
||||
RawBindings =
|
||||
[
|
||||
"""{"name":"input","type":"activityTrigger","direction":"In","dataType":"String"}""",
|
||||
"""{"name":"durableTaskClient","type":"durableClient","direction":"In"}"""
|
||||
],
|
||||
EntryPoint = BuiltInFunctions.InvokeWorkflowActivityFunctionEntryPoint,
|
||||
ScriptFile = BuiltInFunctions.ScriptFile,
|
||||
};
|
||||
}
|
||||
|
||||
private static DefaultFunctionMetadata CreateEntityTrigger(string functionName)
|
||||
{
|
||||
return new DefaultFunctionMetadata()
|
||||
{
|
||||
Name = AgentSessionId.ToEntityName(functionName),
|
||||
Language = "dotnet-isolated",
|
||||
RawBindings =
|
||||
[
|
||||
"""{"name":"encodedEntityRequest","type":"entityTrigger","direction":"In"}""",
|
||||
"""{"name":"client","type":"durableClient","direction":"In"}"""
|
||||
],
|
||||
EntryPoint = BuiltInFunctions.RunAgentEntityFunctionEntryPoint,
|
||||
ScriptFile = BuiltInFunctions.ScriptFile,
|
||||
};
|
||||
}
|
||||
|
||||
private static DefaultFunctionMetadata CreateMcpToolTrigger(string workflowName, string? description)
|
||||
{
|
||||
return new DefaultFunctionMetadata
|
||||
{
|
||||
Name = $"{BuiltInFunctions.McpToolPrefix}{workflowName}",
|
||||
Language = "dotnet-isolated",
|
||||
RawBindings =
|
||||
[
|
||||
$$"""{"name":"context","type":"mcpToolTrigger","direction":"In","toolName":"{{workflowName}}","description":"{{description ?? $"Run the {workflowName} workflow"}}","toolProperties":"[{\"propertyName\":\"input\",\"propertyType\":\"string\",\"description\":\"The input to the workflow.\",\"isRequired\":true,\"isArray\":false}]"}""",
|
||||
"""{"name":"input","type":"mcpToolProperty","direction":"In","propertyName":"input","description":"The input to the workflow","isRequired":true,"dataType":"String","propertyType":"string"}""",
|
||||
"""{"name":"client","type":"durableClient","direction":"In"}"""
|
||||
],
|
||||
EntryPoint = BuiltInFunctions.RunWorkflowMcpToolFunctionEntryPoint,
|
||||
ScriptFile = BuiltInFunctions.ScriptFile,
|
||||
};
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.AzureFunctions.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Logging messages for <see cref="DurableWorkflowFunctionMetadataTransformer"/>.
|
||||
/// </summary>
|
||||
[ExcludeFromCodeCoverage]
|
||||
internal static partial class DurableWorkflowFunctionMetadataTransformerLogs
|
||||
{
|
||||
[LoggerMessage(
|
||||
EventId = 200,
|
||||
Level = LogLevel.Information,
|
||||
Message = "Transforming function metadata to add durable workflow functions. Initial function count: {FunctionCount}")]
|
||||
public static partial void LogTransformStart(this ILogger logger, int functionCount);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 201,
|
||||
Level = LogLevel.Information,
|
||||
Message = "Adding durable workflow function for workflow: {WorkflowName}")]
|
||||
public static partial void LogAddingWorkflowFunction(this ILogger logger, string workflowName);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 202,
|
||||
Level = LogLevel.Information,
|
||||
Message = "Adding HTTP trigger function for workflow: {WorkflowName}")]
|
||||
public static partial void LogAddingHttpTrigger(this ILogger logger, string workflowName);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 203,
|
||||
Level = LogLevel.Information,
|
||||
Message = "Adding activity function for executor: {ExecutorId} (Type: {ExecutorType}) in workflow: {WorkflowName}")]
|
||||
public static partial void LogAddingActivityFunction(this ILogger logger, string executorId, string executorType, string workflowName);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 204,
|
||||
Level = LogLevel.Information,
|
||||
Message = "Adding agent entity function for executor: {ExecutorId} (Type: {ExecutorType}) in workflow: {WorkflowName}")]
|
||||
public static partial void LogAddingAgentEntityFunction(this ILogger logger, string executorId, string executorType, string workflowName);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 205,
|
||||
Level = LogLevel.Information,
|
||||
Message = "Adding MCP tool trigger function for workflow: {WorkflowName}")]
|
||||
public static partial void LogAddingMcpToolTrigger(this ILogger logger, string workflowName);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 206,
|
||||
Level = LogLevel.Information,
|
||||
Message = "Transform finished. Updated function count: {FunctionCount}")]
|
||||
public static partial void LogTransformFinished(this ILogger logger, int functionCount);
|
||||
|
||||
[LoggerMessage(
|
||||
EventId = 207,
|
||||
Level = LogLevel.Debug,
|
||||
Message = "Skipping duplicate function registration: {FunctionName} (already registered by another workflow) in workflow: {WorkflowName}")]
|
||||
public static partial void LogSkippingDuplicateFunction(this ILogger logger, string functionName, string workflowName);
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.DurableTask.Workflows;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.AzureFunctions.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for registering and configuring workflows in the context of the Azure Functions hosting environment.
|
||||
/// </summary>
|
||||
public static class DurableWorkflowOptionsExtensions
|
||||
{
|
||||
// Registry of workflow options.
|
||||
private static readonly Dictionary<string, FunctionsWorkflowOptions> s_workflowOptions = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a workflow to the specified <see cref="DurableWorkflowOptions"/> instance and optionally configures
|
||||
/// workflow-specific options.
|
||||
/// </summary>
|
||||
/// <param name="options">The <see cref="DurableWorkflowOptions"/> instance to which the workflow will be added.</param>
|
||||
/// <param name="workflow">The workflow to add. The workflow's Name property must not be null or empty.</param>
|
||||
/// <param name="configure">An optional delegate to configure workflow-specific options. If null, default options are used.</param>
|
||||
/// <returns>The updated <see cref="DurableWorkflowOptions"/> instance containing the added workflow.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="options"/> or <paramref name="workflow"/> is null.</exception>
|
||||
/// <exception cref="ArgumentException">Thrown when the workflow does not have a valid name.</exception>
|
||||
public static DurableWorkflowOptions AddWorkflow(
|
||||
this DurableWorkflowOptions options,
|
||||
Workflow workflow,
|
||||
Action<FunctionsWorkflowOptions>? configure)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
ArgumentNullException.ThrowIfNull(workflow);
|
||||
|
||||
if (string.IsNullOrEmpty(workflow.Name))
|
||||
{
|
||||
throw new ArgumentException("Workflow must have a valid Name property.", nameof(workflow));
|
||||
}
|
||||
|
||||
// Initialize with default behavior (MCP trigger disabled)
|
||||
FunctionsWorkflowOptions workflowOptions = new();
|
||||
configure?.Invoke(workflowOptions);
|
||||
|
||||
options.AddWorkflow(workflow);
|
||||
s_workflowOptions[workflow.Name] = workflowOptions;
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a workflow to the specified <see cref="DurableWorkflowOptions"/> instance and configures
|
||||
/// trigger support for MCP tool invocations.
|
||||
/// </summary>
|
||||
/// <param name="options">The <see cref="DurableWorkflowOptions"/> instance to which the workflow will be added.</param>
|
||||
/// <param name="workflow">The workflow to add. The workflow's Name property must not be null or empty.</param>
|
||||
/// <param name="enableMcpToolTrigger">true to enable an MCP tool trigger for the workflow; otherwise, false.</param>
|
||||
/// <returns>The updated <see cref="DurableWorkflowOptions"/> instance with the specified workflow and trigger configuration applied.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="options"/> or <paramref name="workflow"/> is null.</exception>
|
||||
/// <exception cref="ArgumentException">Thrown when the workflow does not have a valid name.</exception>
|
||||
public static DurableWorkflowOptions AddWorkflow(
|
||||
this DurableWorkflowOptions options,
|
||||
Workflow workflow,
|
||||
bool enableMcpToolTrigger)
|
||||
{
|
||||
return AddWorkflow(options, workflow, workflowOptions => workflowOptions.McpToolTrigger.IsEnabled = enableMcpToolTrigger);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the <see cref="FunctionsWorkflowOptions"/> for a workflow by name.
|
||||
/// </summary>
|
||||
/// <param name="workflowName">The name of the workflow.</param>
|
||||
/// <param name="workflowOptions">When this method returns, contains the workflow options if found; otherwise, null.</param>
|
||||
/// <returns><c>true</c> if the workflow options were found; otherwise, <c>false</c>.</returns>
|
||||
internal static bool TryGetWorkflowOptions(string workflowName, out FunctionsWorkflowOptions? workflowOptions)
|
||||
{
|
||||
return s_workflowOptions.TryGetValue(workflowName, out workflowOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the workflow options used for dependency injection (read-only copy).
|
||||
/// </summary>
|
||||
internal static IReadOnlyDictionary<string, FunctionsWorkflowOptions> GetWorkflowOptionsSnapshot()
|
||||
{
|
||||
return new Dictionary<string, FunctionsWorkflowOptions>(s_workflowOptions, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.AzureFunctions.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Provides configuration options for enabling and customizing function triggers for a workflow.
|
||||
/// </summary>
|
||||
public sealed class FunctionsWorkflowOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the options used to configure the MCP tool trigger behavior.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// By default, MCP tool trigger is disabled for workflows.
|
||||
/// </remarks>
|
||||
public McpToolTriggerOptions McpToolTrigger { get; set; } = new(false);
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Net;
|
||||
using Microsoft.Agents.AI.DurableTask;
|
||||
using Microsoft.Agents.AI.DurableTask.Workflows;
|
||||
using Microsoft.Azure.Functions.Worker;
|
||||
using Microsoft.Azure.Functions.Worker.Http;
|
||||
using Microsoft.DurableTask.Client;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.AzureFunctions.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Provides functionality to invoke and manage workflow orchestrations in response to HTTP requests within an Azure
|
||||
/// Functions environment.
|
||||
/// </summary>
|
||||
public sealed class FunctionsWorkflowRunner : DurableWorkflowRunner
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the FunctionsWorkflowRunner class using the specified DurableOptions.
|
||||
/// </summary>
|
||||
/// <param name="durableOptions">The DurableOptions that configure the behavior of the workflow runner. This parameter cannot be null.</param>
|
||||
public FunctionsWorkflowRunner(DurableOptions durableOptions) : base(durableOptions)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes a workflow orchestration in response to an HTTP request.
|
||||
/// </summary>
|
||||
public static async Task<HttpResponseData> RunWorkflowOrechstrtationHttpTriggerAsync(
|
||||
[HttpTrigger] HttpRequestData req,
|
||||
[DurableClient] DurableTaskClient client,
|
||||
FunctionContext context)
|
||||
{
|
||||
var functionName = context.FunctionDefinition.Name;
|
||||
var workflowName = functionName.Replace("-http", string.Empty);
|
||||
var orchestrationFunctionName = WorkflowNamingHelper.ToOrchestrationFunctionName(workflowName);
|
||||
var inputMessage = await req.ReadAsStringAsync();
|
||||
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(orchestrationFunctionName, inputMessage);
|
||||
|
||||
HttpResponseData response = req.CreateResponse(HttpStatusCode.Accepted);
|
||||
await response.WriteStringAsync($"InvokeWorkflowOrechstrtationAsync is invoked for {workflowName}. Orchestration instanceId: {instanceId}");
|
||||
return response;
|
||||
}
|
||||
|
||||
internal async Task<string> ExecuteActivityAsync(string activityFunctionName, string input, DurableTaskClient durableTaskClient, FunctionContext functionContext)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user