This commit is contained in:
Shyju Krishnankutty
2026-02-12 16:28:24 -08:00
Unverified
parent 310d1b8e10
commit b5e4553f05
12 changed files with 437 additions and 141 deletions
+1
View File
@@ -59,6 +59,7 @@
<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" />
<Project Path="samples/Durable/Workflow/AzureFunctions/05_WorkflowAndAgents/05_WorkflowAndAgents.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/">
<File Path="samples/GettingStarted/README.md" />
@@ -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>WorkflowAndAgentsFunctionApp</AssemblyName>
<RootNamespace>WorkflowAndAgentsFunctionApp</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,54 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows;
namespace WorkflowAndAgentsFunctionApp;
/// <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($"[ParseQuestion] Preparing question: \"{message}\"");
string formattedQuestion = message.Trim();
if (!formattedQuestion.EndsWith('?'))
{
formattedQuestion += "?";
}
return ValueTask.FromResult(formattedQuestion);
}
}
/// <summary>
/// Aggregates responses from multiple AI agents into a unified response.
/// </summary>
internal sealed class ResponseAggregatorExecutor() : Executor<string[], string>("ResponseAggregator")
{
public override ValueTask<string> HandleAsync(
string[] message,
IWorkflowContext context,
CancellationToken cancellationToken = default)
{
Console.WriteLine($"[Aggregator] Received {message.Length} AI agent responses, combining...");
string aggregatedResult = "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 += $"Summary: Received perspectives from {message.Length} AI experts.";
return ValueTask.FromResult(aggregatedResult);
}
}
@@ -0,0 +1,99 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates the THREE ways to configure durable agents and workflows
// in an Azure Functions app:
//
// 1. ConfigureDurableAgents() - For standalone agents only
// 2. ConfigureDurableWorkflows() - For workflows only
// 3. ConfigureDurableOptions() - For both agents AND workflows
//
// KEY: All methods can be called MULTIPLE times - configurations are ADDITIVE.
//
// Workflow structure:
//
// PhysicsExpertReview: ParseQuestion ──► Physicist (AI Agent)
//
// ExpertTeamReview: ParseQuestion ──┬──► Physicist (AI Agent) ──┬──► Aggregator
// └──► Chemist (AI Agent) ──┘
//
// ChemistryExpertReview: ParseQuestion ──► Chemist (AI Agent)
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 WorkflowAndAgentsFunctionApp;
// Configuration
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);
// Create AI agents
AIAgent physicist = chatClient.AsAIAgent("You are a physics expert. Explain concepts clearly in 2-3 sentences.", "Physicist");
AIAgent chemist = chatClient.AsAIAgent("You are a chemistry expert. Explain concepts clearly in 2-3 sentences.", "Chemist");
AIAgent biologist = chatClient.AsAIAgent("You are a biology expert. Explain concepts clearly in 2-3 sentences.", "Biologist");
// Create custom executors
ParseQuestionExecutor questionParser = new();
ResponseAggregatorExecutor responseAggregator = new();
// Workflow 1: Single-agent workflow (Physics)
Workflow physicsWorkflow = new WorkflowBuilder(questionParser)
.WithName("PhysicsExpertReview")
.AddEdge(questionParser, physicist)
.Build();
// Workflow 2: Multi-agent workflow with fan-out/fan-in (Expert Team)
Workflow expertTeamWorkflow = new WorkflowBuilder(questionParser)
.WithName("ExpertTeamReview")
.AddFanOutEdge(questionParser, [physicist, chemist])
.AddFanInEdge([physicist, chemist], responseAggregator)
.Build();
// Workflow 3: Single-agent workflow (Chemistry)
Workflow chemistryWorkflow = new WorkflowBuilder(questionParser)
.WithName("ChemistryExpertReview")
.AddEdge(questionParser, chemist)
.Build();
// Configure the function app using all 3 methods to demonstrate additive configuration.
// Each method can be called one or more times - configurations accumulate.
using IHost app = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
// METHOD 1: ConfigureDurableAgents - for standalone agents only.
// Registers the biologist agent as a standalone agent (not part of any workflow).
.ConfigureDurableAgents(options => options.AddAIAgent(biologist))
// METHOD 2: ConfigureDurableWorkflows - for workflows only.
// Registers the physics workflow. Agents referenced in the workflow (Physicist) are auto-discovered.
.ConfigureDurableWorkflows(options => options.AddWorkflow(physicsWorkflow))
// METHOD 3: ConfigureDurableOptions - for both agents AND workflows.
// Registers the chemist agent explicitly and the expert team workflow together.
.ConfigureDurableOptions(options =>
{
options.Agents.AddAIAgent(chemist);
options.Workflows.AddWorkflow(expertTeamWorkflow);
})
// Second call to ConfigureDurableOptions (additive - adds to existing config).
.ConfigureDurableOptions(options => options.Workflows.AddWorkflow(chemistryWorkflow))
.Build();
app.Run();
@@ -0,0 +1,73 @@
# Workflow and Agents - Azure Functions Sample
This sample demonstrates how to combine **custom executors with AI agents** (backed by Azure OpenAI) in workflows hosted as Azure Functions. It shows single-agent workflows, multi-agent fan-out/fan-in workflows, and how multiple workflows can be registered together.
## Key Concepts Demonstrated
- Using **AI agents** (Azure OpenAI) as workflow executors alongside custom executors
- **Fan-out/fan-in** pattern with multiple AI agents running in parallel
- Registering **multiple workflows** in a single Azure Functions app via `ConfigureDurableOptions`
## Overview
Three workflows are registered, each demonstrating different patterns:
```
PhysicsExpertReview: ParseQuestion ──► Physicist (AI Agent)
ExpertTeamReview: ParseQuestion ──┬──► Physicist (AI Agent) ──┬──► Aggregator
└──► Chemist (AI Agent) ──┘
ChemistryExpertReview: ParseQuestion ──► Chemist (AI Agent)
```
| Executor | Type | Description |
|----------|------|-------------|
| ParseQuestion | Custom Executor | Validates and formats the incoming question |
| Physicist | AI Agent | Physics expert backed by Azure OpenAI |
| Chemist | AI Agent | Chemistry expert backed by Azure OpenAI |
| ResponseAggregator | Custom Executor | Combines responses from multiple AI agents |
## 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
- An [Azure OpenAI](https://learn.microsoft.com/azure/ai-services/openai/) deployment
### Configuration
Set the following environment variables in `local.settings.json`:
| Variable | Description |
|----------|-------------|
| `AZURE_OPENAI_ENDPOINT` | Your Azure OpenAI endpoint URL |
| `AZURE_OPENAI_DEPLOYMENT` | The model deployment name (e.g., `gpt-4o`) |
| `AZURE_OPENAI_KEY` | *(Optional)* API key. If not set, uses `AzureCliCredential` |
## Running the Sample
```bash
cd dotnet/samples/Durable/Workflow/AzureFunctions/05_WorkflowAndAgents
func start
```
### Testing
**Single-agent workflow (Physics):**
```bash
curl -X POST http://localhost:7071/api/workflows/PhysicsExpertReview/run -H "Content-Type: text/plain" -d "What is the relationship between energy and mass?"
```
**Multi-agent workflow (Expert Team):**
```bash
curl -X POST http://localhost:7071/api/workflows/ExpertTeamReview/run -H "Content-Type: text/plain" -d "How does radiation affect living cells?"
```
**Single-agent workflow (Chemistry):**
```bash
curl -X POST http://localhost:7071/api/workflows/ChemistryExpertReview/run -H "Content-Type: text/plain" -d "What happens during combustion?"
```
@@ -0,0 +1,20 @@
# Default endpoint address for local testing
@authority=http://localhost:7071
### Single-agent workflow: Physics expert
POST {{authority}}/api/workflows/PhysicsExpertReview/run
Content-Type: text/plain
What is the relationship between energy and mass?
### Multi-agent workflow: Expert team (Physicist + Chemist in parallel)
POST {{authority}}/api/workflows/ExpertTeamReview/run
Content-Type: text/plain
How does radiation affect living cells?
### Single-agent workflow: Chemistry expert
POST {{authority}}/api/workflows/ChemistryExpertReview/run
Content-Type: text/plain
What happens during combustion?
@@ -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"
}
}
}
}
@@ -235,19 +235,18 @@ public static class ServiceCollectionExtensions
private static void RegisterTasksFromOptions(DurableTaskRegistry registry, DurableOptions durableOptions)
{
// Build registrations for all workflows including sub-workflows
// Build registrations for all workflows including discovered sub-workflows
List<WorkflowRegistrationInfo> registrations = [];
HashSet<string> registeredActivities = [];
HashSet<string> registeredOrchestrations = [];
foreach (Workflow workflow in durableOptions.Workflows.Workflows.Values.ToList())
foreach (Workflow workflow in durableOptions.Workflows.Workflows.Values)
{
BuildWorkflowRegistrationRecursive(
workflow,
durableOptions.Workflows,
registrations,
registeredActivities,
registeredOrchestrations);
string orchestrationName = WorkflowNamingHelper.ToOrchestrationFunctionName(workflow.Name!);
if (registeredOrchestrations.Add(orchestrationName))
{
registrations.Add(BuildWorkflowRegistration(workflow, registeredActivities));
}
}
IReadOnlyDictionary<string, Func<IServiceProvider, AIAgent>> agentFactories =
@@ -281,39 +280,6 @@ public static class ServiceCollectionExtensions
}
}
private static void BuildWorkflowRegistrationRecursive(
Workflow workflow,
DurableWorkflowOptions workflowOptions,
List<WorkflowRegistrationInfo> registrations,
HashSet<string> registeredActivities,
HashSet<string> registeredOrchestrations)
{
string orchestrationName = WorkflowNamingHelper.ToOrchestrationFunctionName(workflow.Name!);
if (!registeredOrchestrations.Add(orchestrationName))
{
return;
}
registrations.Add(BuildWorkflowRegistration(workflow, registeredActivities));
// Process subworkflows recursively to register them as separate orchestrations
foreach (SubworkflowBinding subworkflowBinding in workflow.ReflectExecutors()
.Select(e => e.Value)
.OfType<SubworkflowBinding>())
{
Workflow subWorkflow = subworkflowBinding.WorkflowInstance;
workflowOptions.AddWorkflow(subWorkflow);
BuildWorkflowRegistrationRecursive(
subWorkflow,
workflowOptions,
registrations,
registeredActivities,
registeredOrchestrations);
}
}
private static WorkflowRegistrationInfo BuildWorkflowRegistration(
Workflow workflow,
HashSet<string> registeredActivities)
@@ -74,6 +74,7 @@ public sealed class DurableWorkflowOptions
/// <summary>
/// Registers all executors from a workflow, including AI agents if agent options are available.
/// Any sub-workflow bindings are automatically discovered and added recursively.
/// </summary>
private void RegisterWorkflowExecutors(Workflow workflow)
{
@@ -85,6 +86,16 @@ public sealed class DurableWorkflowOptions
this.Executors.Register(executorName, executorId, workflow);
TryRegisterAgent(binding, agentOptions);
// Automatically discover and register sub-workflows
if (binding is SubworkflowBinding subworkflowBinding)
{
Workflow subWorkflow = subworkflowBinding.WorkflowInstance;
if (subWorkflow.Name is not null && !this._workflows.ContainsKey(subWorkflow.Name))
{
this.AddWorkflow(subWorkflow);
}
}
}
}
@@ -21,6 +21,11 @@ internal sealed class DurableAgentFunctionMetadataTransformer : IFunctionMetadat
private static readonly string s_builtInFunctionsScriptFile = Path.GetFileName(typeof(BuiltInFunctions).Assembly.Location);
#pragma warning restore IL3000
/// <summary>
/// Tracks all registered function names to prevent duplicates across multiple metadata transformers.
/// </summary>
internal static readonly HashSet<string> RegisteredFunctionNames = new(StringComparer.OrdinalIgnoreCase);
public DurableAgentFunctionMetadataTransformer(
IReadOnlyDictionary<string, Func<IServiceProvider, AIAgent>> agents,
ILogger<DurableAgentFunctionMetadataTransformer> logger,
@@ -42,6 +47,13 @@ internal sealed class DurableAgentFunctionMetadataTransformer : IFunctionMetadat
foreach (KeyValuePair<string, Func<IServiceProvider, AIAgent>> kvp in this._agents)
{
string agentName = kvp.Key;
string entityFunctionName = AgentSessionId.ToEntityName(agentName);
// Skip if this entity function has already been registered by another transformer
if (!RegisteredFunctionNames.Add(entityFunctionName))
{
continue;
}
this._logger.LogRegisteringTriggerForAgent(agentName, "entity");
@@ -3,7 +3,6 @@
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;
@@ -21,6 +20,16 @@ namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
/// </summary>
public static class FunctionsApplicationBuilderExtensions
{
/// <summary>
/// Tracks workflow orchestration names that have already been registered to prevent duplicates.
/// </summary>
private static readonly HashSet<string> s_registeredOrchestrations = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Tracks whether middleware and shared services have been registered.
/// </summary>
private static bool s_middlewareRegistered;
/// <summary>
/// Configures durable agents and workflows in a unified way.
/// </summary>
@@ -31,6 +40,7 @@ public static class FunctionsApplicationBuilderExtensions
/// 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.
/// Multiple calls to this method are supported and configurations are composed additively.
/// </remarks>
public static FunctionsApplicationBuilder ConfigureDurableOptions(
this FunctionsApplicationBuilder builder,
@@ -39,56 +49,60 @@ public static class FunctionsApplicationBuilderExtensions
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(configure);
DurableOptions options = new();
configure(options);
// Delegate to the shared DurableOptions registration in Microsoft.Agents.AI.DurableTask.
// This ensures a single shared DurableOptions instance across all Configure* calls.
builder.Services.ConfigureDurableOptions(options => configure(options));
builder.Services.AddSingleton(options);
// Read the shared options to check if workflows were added
DurableOptions sharedOptions = GetOrCreateSharedOptions(builder.Services);
if (options.Workflows.Workflows.Count > 0)
if (sharedOptions.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
});
ConfigureWorkflowOrchestrations(builder, sharedOptions.Workflows);
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>();
builder.Services.TryAddEnumerable(
ServiceDescriptor.Singleton<IFunctionMetadataTransformer, DurableWorkflowFunctionMetadataTransformer>());
}
EnsureMiddlewareRegistered(builder);
return builder;
}
private static void ConfigureWorkflowOrchestrations(FunctionsApplicationBuilder builder, DurableWorkflowOptions workflows)
/// <summary>
/// Gets or creates a shared <see cref="DurableOptions"/> instance from the service collection.
/// </summary>
private static DurableOptions GetOrCreateSharedOptions(IServiceCollection services)
{
// Discover sub-workflows recursively and add them to the workflows dictionary
// so they are registered as separate orchestrations alongside the main workflows.
DiscoverSubWorkflows(workflows);
ServiceDescriptor? existingDescriptor = services.FirstOrDefault(
d => d.ServiceType == typeof(DurableOptions) && d.ImplementationInstance is not null);
if (existingDescriptor?.ImplementationInstance is DurableOptions existing)
{
return existing;
}
DurableOptions options = new();
services.AddSingleton(options);
return options;
}
private static void ConfigureWorkflowOrchestrations(FunctionsApplicationBuilder builder, DurableWorkflowOptions workflowOptions)
{
// Collect only workflows that haven't been registered yet
List<string> newWorkflowNames = workflowOptions.Workflows
.Select(kp => kp.Key)
.Where(name => s_registeredOrchestrations.Add(name))
.ToList();
if (newWorkflowNames.Count == 0)
{
return;
}
builder.ConfigureDurableWorker().AddTasks(tasks =>
{
foreach (string workflowName in workflows.Workflows.Select(kp => kp.Key))
foreach (string workflowName in newWorkflowNames)
{
string orchestrationFunctionName = WorkflowNamingHelper.ToOrchestrationFunctionName(workflowName);
@@ -109,48 +123,19 @@ public static class FunctionsApplicationBuilderExtensions
});
}
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.
/// Configures durable workflow services for the Azure Functions application.
/// </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>
/// <remarks>
/// <para>
/// This method registers the services required for durable workflows in an Azure Functions app,
/// including orchestration triggers, HTTP triggers, and activity/entity triggers for executors.
/// </para>
/// <para>
/// Multiple calls to this method are supported and configurations are composed additively.
/// Agents referenced in workflows are automatically discovered and registered.
/// </para>
/// </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
@@ -159,13 +144,7 @@ public static class FunctionsApplicationBuilderExtensions
{
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;
return builder.ConfigureDurableOptions(options => configure(options.Workflows));
}
/// <summary>
@@ -188,19 +167,31 @@ public static class FunctionsApplicationBuilderExtensions
builder.Services.AddSingleton<IFunctionMetadataTransformer, DurableAgentFunctionMetadataTransformer>();
// Handling of built-in function execution for Agent HTTP, MCP tool, or Entity invocations.
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>();
EnsureMiddlewareRegistered(builder);
return builder;
}
/// <summary>
/// Registers the built-in function execution middleware and executor exactly once.
/// </summary>
private static void EnsureMiddlewareRegistered(FunctionsApplicationBuilder builder)
{
if (s_middlewareRegistered)
{
return;
}
s_middlewareRegistered = true;
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.InvokeWorkflowOrchestrationFunctionEntryPoint, StringComparison.Ordinal) ||
string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.InvokeWorkflowActivityFunctionEntryPoint, StringComparison.Ordinal)
);
builder.Services.TryAddSingleton<BuiltInFunctionExecutor>();
}
}
@@ -10,7 +10,6 @@ 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;
@@ -29,6 +28,14 @@ internal sealed class DurableWorkflowFunctionMetadataTransformer : IFunctionMeta
foreach (var workflow in this._options.Workflows)
{
string httpFunctionName = $"{BuiltInFunctions.HttpPrefix}{workflow.Key}";
// Skip if this workflow's HTTP trigger has already been registered by another transformer
if (!DurableAgentFunctionMetadataTransformer.RegisteredFunctionNames.Add(httpFunctionName))
{
continue;
}
this._logger.LogAddingWorkflowFunction(workflow.Key);
// Currently due to how durable executor is registered, we are not able to bind TaskOrechestrationContext parameter properly
@@ -80,8 +87,8 @@ internal sealed class DurableWorkflowFunctionMetadataTransformer : IFunctionMeta
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))
// Skip if this function has already been registered by another workflow or transformer
if (!DurableAgentFunctionMetadataTransformer.RegisteredFunctionNames.Add(functionName))
{
this._logger.LogSkippingDuplicateFunction(functionName, workflow.Key);
continue;