-
Notifications
You must be signed in to change notification settings - Fork 1.9k
.NET: [Feature Branch] Add Human In the Loop support for durable workflows #4358
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 6 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
84148a1
Add Azure Functions HITL workflow sample
kshyju dc8a881
PR feedback fixes
kshyju 1179b4d
Minor comment cleanup
kshyju 719c525
Minor comment clReverted the `!context.IsReplaying` guards on `Pendin…
kshyju 927f172
fix for PR feedback
kshyju 1946f03
PR feedback updates
kshyju 5dfbe3e
Improvements to samples
kshyju 3d27a7a
Improvements to README
kshyju f7ae1ce
Update samples to use parallel request ports.
kshyju 9274da4
Unit tests
kshyju 38b807e
Introduce local variables to improve readability of Workflows.Workflo…
kshyju eb10d0a
Use GitHub-style callouts and add PowerShell command variants in HITL…
kshyju File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
43 changes: 43 additions & 0 deletions
43
dotnet/samples/Durable/Workflow/AzureFunctions/03_WorkflowHITL/03_WorkflowHITL.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| <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>WorkflowHITLFunctions</AssemblyName> | ||
| <RootNamespace>WorkflowHITLFunctions</RootNamespace> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <FrameworkReference Include="Microsoft.AspNetCore.App" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <None Include="local.settings.json" /> | ||
| </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> |
43 changes: 43 additions & 0 deletions
43
dotnet/samples/Durable/Workflow/AzureFunctions/03_WorkflowHITL/Executors.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| using Microsoft.Agents.AI.Workflows; | ||
|
|
||
| namespace WorkflowHITLFunctions; | ||
|
|
||
| /// <summary>Expense approval request passed to the RequestPort.</summary> | ||
| public record ApprovalRequest(string ExpenseId, decimal Amount, string EmployeeName); | ||
|
|
||
| /// <summary>Approval response received from the RequestPort.</summary> | ||
| public record ApprovalResponse(bool Approved, string? Comments); | ||
|
|
||
| /// <summary>Looks up expense details and creates an approval request.</summary> | ||
| internal sealed class CreateApprovalRequest() : Executor<string, ApprovalRequest>("RetrieveRequest") | ||
| { | ||
| public override ValueTask<ApprovalRequest> HandleAsync( | ||
| string message, | ||
| IWorkflowContext context, | ||
| CancellationToken cancellationToken = default) | ||
| { | ||
| // In a real scenario, this would look up expense details from a database | ||
| return new ValueTask<ApprovalRequest>(new ApprovalRequest(message, 1500.00m, "Jerry")); | ||
| } | ||
| } | ||
|
|
||
| /// <summary>Processes the expense reimbursement based on the approval decision.</summary> | ||
| internal sealed class ExpenseReimburse() : Executor<ApprovalResponse, string>("Reimburse") | ||
| { | ||
| public override async ValueTask<string> HandleAsync( | ||
| ApprovalResponse message, | ||
| IWorkflowContext context, | ||
| CancellationToken cancellationToken = default) | ||
| { | ||
| if (!message.Approved) | ||
| { | ||
| return $"Expense reimbursement denied. Comments: {message.Comments}"; | ||
| } | ||
|
|
||
| // Simulate payment processing | ||
| await Task.Delay(1000, cancellationToken); | ||
| return $"Expense reimbursed at {DateTime.UtcNow:O}"; | ||
| } | ||
| } |
36 changes: 36 additions & 0 deletions
36
dotnet/samples/Durable/Workflow/AzureFunctions/03_WorkflowHITL/Program.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| // This sample demonstrates a Human-in-the-Loop (HITL) workflow hosted in Azure Functions. | ||
| // Workflow: CreateApprovalRequest -> ManagerApproval (RequestPort/HITL pause) -> ExpenseReimburse | ||
| // | ||
| // The workflow pauses at a RequestPort and waits for an external approval response via HTTP. | ||
| // The framework auto-generates three HTTP endpoints for each workflow: | ||
| // POST /api/workflows/{name}/run - Start the workflow | ||
| // GET /api/workflows/{name}/status/{id} - Check status and pending approvals | ||
| // POST /api/workflows/{name}/respond/{id} - Send approval response to resume | ||
|
|
||
| using Microsoft.Agents.AI.Hosting.AzureFunctions; | ||
| using Microsoft.Agents.AI.Workflows; | ||
| using Microsoft.Azure.Functions.Worker.Builder; | ||
| using Microsoft.Extensions.Hosting; | ||
| using WorkflowHITLFunctions; | ||
|
|
||
| // Define executors and a RequestPort for the HITL pause point | ||
| CreateApprovalRequest createRequest = new(); | ||
| RequestPort<ApprovalRequest, ApprovalResponse> managerApproval = RequestPort.Create<ApprovalRequest, ApprovalResponse>("ManagerApproval"); | ||
| ExpenseReimburse reimburse = new(); | ||
|
|
||
| // Build the workflow: CreateApprovalRequest -> ManagerApproval (HITL) -> ExpenseReimburse | ||
| Workflow expenseApproval = new WorkflowBuilder(createRequest) | ||
| .WithName("ExpenseReimbursement") | ||
| .WithDescription("Expense reimbursement with manager approval") | ||
| .AddEdge(createRequest, managerApproval) | ||
| .AddEdge(managerApproval, reimburse) | ||
| .Build(); | ||
|
|
||
| using IHost app = FunctionsApplication | ||
| .CreateBuilder(args) | ||
| .ConfigureFunctionsWebApplication() | ||
| .ConfigureDurableWorkflows(workflows => workflows.AddWorkflow(expenseApproval, exposeStatusEndpoint: true)) | ||
| .Build(); | ||
| app.Run(); |
58 changes: 58 additions & 0 deletions
58
dotnet/samples/Durable/Workflow/AzureFunctions/03_WorkflowHITL/README.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| # Human-in-the-Loop (HITL) Workflow — Azure Functions | ||
|
|
||
| This sample demonstrates a durable workflow with Human-in-the-Loop support hosted in Azure Functions. The workflow pauses at a `RequestPort` and waits for an external approval response sent via an HTTP endpoint. | ||
|
|
||
| ## Key Concepts Demonstrated | ||
|
|
||
| - Using `RequestPort` for human-in-the-loop interaction in a durable workflow | ||
| - Auto-generated HTTP endpoints for running workflows, checking status, and sending HITL responses | ||
| - Pausing orchestrations via `WaitForExternalEvent` and resuming via `RaiseEventAsync` | ||
|
|
||
| ## Workflow | ||
|
|
||
| `CreateApprovalRequest` → `ManagerApproval` (HITL pause) → `ExpenseReimburse` | ||
|
|
||
| ## HTTP Endpoints | ||
|
|
||
| The framework auto-generates these endpoints for workflows with `RequestPort` nodes: | ||
|
|
||
| | Method | Endpoint | Description | | ||
| |--------|----------|-------------| | ||
| | POST | `/api/workflows/ExpenseReimbursement/run` | Start the workflow | | ||
| | GET | `/api/workflows/ExpenseReimbursement/status/{runId}` | Check status and pending approvals | | ||
| | POST | `/api/workflows/ExpenseReimbursement/respond/{runId}` | Send approval response to resume | | ||
|
|
||
| ## Environment Setup | ||
|
|
||
| See the [README.md](../../README.md) file in the parent directory for information on how to configure the environment, including how to install and run the Durable Task Scheduler. | ||
|
|
||
| ## Running the Sample | ||
|
|
||
| Use the `demo.http` file or the steps below: | ||
|
|
||
| ### Step 1: Start the Workflow | ||
|
|
||
| ```bash | ||
| curl -X POST "http://localhost:7071/api/workflows/ExpenseReimbursement/run?runId=expense-001" \ | ||
| -H "Content-Type: text/plain" -d "EXP-2025-001" | ||
| ``` | ||
|
|
||
| ### Step 2: Check Workflow Status | ||
|
|
||
| The workflow pauses at the `ManagerApproval` RequestPort: | ||
|
|
||
| ```bash | ||
| curl http://localhost:7071/api/workflows/ExpenseReimbursement/status/expense-001 | ||
| ``` | ||
|
|
||
| ### Step 3: Send Approval Response | ||
|
|
||
| ```bash | ||
| curl -X POST http://localhost:7071/api/workflows/ExpenseReimbursement/respond/expense-001 \ | ||
| -H "Content-Type: application/json" \ | ||
| -d '{"eventName": "ManagerApproval", "response": {"Approved": true, "Comments": "Looks good!"}}' | ||
| ``` | ||
|
|
||
| ### DTS Dashboard | ||
|
|
||
| If using the DTS emulator, the dashboard is available at `http://localhost:8082` to visualize the orchestration and inspect the external event interaction. | ||
|
kshyju marked this conversation as resolved.
Outdated
|
||
32 changes: 32 additions & 0 deletions
32
dotnet/samples/Durable/Workflow/AzureFunctions/03_WorkflowHITL/demo.http
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| # Default endpoint address for local testing | ||
| @authority=http://localhost:7071 | ||
|
|
||
| ### Step 1: Start the expense reimbursement workflow | ||
| POST {{authority}}/api/workflows/ExpenseReimbursement/run | ||
| Content-Type: text/plain | ||
|
|
||
| EXP-2025-001 | ||
|
|
||
| ### Step 1 (alternative): Start the workflow with a custom run ID | ||
| POST {{authority}}/api/workflows/ExpenseReimbursement/run?runId=expense-001 | ||
| Content-Type: text/plain | ||
|
|
||
| EXP-2025-001 | ||
|
|
||
| ### Step 2: Check workflow status (replace {runId} with actual run ID from Step 1) | ||
| GET {{authority}}/api/workflows/ExpenseReimbursement/status/{runId} | ||
|
|
||
| ### Step 3: Approve the expense (replace {runId} with actual run ID from Step 1) | ||
| POST {{authority}}/api/workflows/ExpenseReimbursement/respond/{runId} | ||
| Content-Type: application/json | ||
|
|
||
| {"eventName": "ManagerApproval", "response": {"Approved": true, "Comments": "Approved by manager. Looks good!"}} | ||
|
|
||
| ### Step 3 (alternative): Deny the expense | ||
| POST {{authority}}/api/workflows/ExpenseReimbursement/respond/expense-001 | ||
| Content-Type: application/json | ||
|
|
||
| {"eventName": "ManagerApproval", "response": {"Approved": false, "Comments": "Insufficient documentation. Please resubmit."}} | ||
|
|
||
| ### Step 4: Check final workflow status after approval | ||
| GET {{authority}}/api/workflows/ExpenseReimbursement/status/expense-001 |
20 changes: 20 additions & 0 deletions
20
dotnet/samples/Durable/Workflow/AzureFunctions/03_WorkflowHITL/host.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
| } | ||
| } | ||
| } | ||
| } |
28 changes: 28 additions & 0 deletions
28
dotnet/samples/Durable/Workflow/ConsoleApps/08_WorkflowHITL/08_WorkflowHITL.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
| <PropertyGroup> | ||
| <TargetFrameworks>net10.0</TargetFrameworks> | ||
| <OutputType>Exe</OutputType> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| <Nullable>enable</Nullable> | ||
| <AssemblyName>WorkflowHITL</AssemblyName> | ||
| <RootNamespace>WorkflowHITL</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" /> | ||
| </ItemGroup> | ||
| </Project> |
79 changes: 79 additions & 0 deletions
79
dotnet/samples/Durable/Workflow/ConsoleApps/08_WorkflowHITL/Executors.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| using Microsoft.Agents.AI.Workflows; | ||
|
|
||
| namespace WorkflowHITL; | ||
|
|
||
| /// <summary> | ||
| /// Represents an expense approval request. | ||
| /// </summary> | ||
| /// <param name="ExpenseId">The unique identifier of the expense.</param> | ||
| /// <param name="Amount">The amount of the expense.</param> | ||
| /// <param name="EmployeeName">The name of the employee submitting the expense.</param> | ||
| public record ApprovalRequest(string ExpenseId, decimal Amount, string EmployeeName); | ||
|
|
||
| /// <summary> | ||
| /// Represents the response to an approval request. | ||
| /// </summary> | ||
| /// <param name="Approved">Whether the expense was approved.</param> | ||
| /// <param name="Comments">Optional comments from the approver.</param> | ||
| public record ApprovalResponse(bool Approved, string? Comments); | ||
|
|
||
| /// <summary> | ||
| /// Retrieves expense details and creates an approval request. | ||
| /// </summary> | ||
| internal sealed class CreateApprovalRequest() : Executor<string, ApprovalRequest>("RetrieveRequest") | ||
| { | ||
| /// <inheritdoc/> | ||
| public override ValueTask<ApprovalRequest> HandleAsync( | ||
| string message, | ||
| IWorkflowContext context, | ||
| CancellationToken cancellationToken = default) | ||
| { | ||
| // In a real scenario, this would look up expense details from a database | ||
| return new ValueTask<ApprovalRequest>(new ApprovalRequest(message, 1500.00m, "Jerry")); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Prepares the approval request for finance review after manager approval. | ||
| /// </summary> | ||
| internal sealed class PrepareFinanceReview() : Executor<ApprovalResponse, ApprovalRequest>("PrepareFinanceReview") | ||
| { | ||
| /// <inheritdoc/> | ||
| public override ValueTask<ApprovalRequest> HandleAsync( | ||
| ApprovalResponse message, | ||
| IWorkflowContext context, | ||
| CancellationToken cancellationToken = default) | ||
| { | ||
| if (!message.Approved) | ||
| { | ||
| throw new InvalidOperationException("Cannot proceed to finance review — manager denied the expense."); | ||
| } | ||
|
|
||
| // In a real scenario, this would retrieve the original expense details | ||
| return new ValueTask<ApprovalRequest>(new ApprovalRequest("EXP-2025-001", 1500.00m, "Jerry")); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Processes the expense reimbursement based on the final approval response. | ||
| /// </summary> | ||
| internal sealed class ExpenseReimburse() : Executor<ApprovalResponse, string>("Reimburse") | ||
| { | ||
| /// <inheritdoc/> | ||
| public override async ValueTask<string> HandleAsync( | ||
| ApprovalResponse message, | ||
| IWorkflowContext context, | ||
| CancellationToken cancellationToken = default) | ||
| { | ||
| if (!message.Approved) | ||
| { | ||
| return $"Expense reimbursement denied by finance. Comments: {message.Comments}"; | ||
| } | ||
|
|
||
| // Simulate payment processing | ||
| await Task.Delay(1000, cancellationToken); | ||
| return $"Expense reimbursed at {DateTime.UtcNow:O}"; | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.