mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
WIP
This commit is contained in:
@@ -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" />
|
||||
|
||||
+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>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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+12
@@ -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");
|
||||
|
||||
|
||||
+88
-97
@@ -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
-3
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user