chore: import upstream snapshot with attribution
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:25 +08:00
commit db620d33df
5151 changed files with 925932 additions and 0 deletions
@@ -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>LongRunningTools</AssemblyName>
<RootNamespace>LongRunningTools</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,152 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.DurableTask;
using Microsoft.Azure.Functions.Worker;
using Microsoft.DurableTask;
using Microsoft.Extensions.Logging;
namespace LongRunningTools;
public static class FunctionTriggers
{
[Function(nameof(RunOrchestrationAsync))]
public static async Task<object> RunOrchestrationAsync(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
// Get the input from the orchestration
ContentGenerationInput input = context.GetInput<ContentGenerationInput>()
?? throw new InvalidOperationException("Content generation input is required");
// Get the writer agent
DurableAIAgent writerAgent = context.GetAgent("Writer");
AgentSession writerSession = await writerAgent.CreateSessionAsync();
// Set initial status
context.SetCustomStatus($"Starting content generation for topic: {input.Topic}");
// Step 1: Generate initial content
AgentResponse<GeneratedContent> writerResponse = await writerAgent.RunAsync<GeneratedContent>(
message: $"Write a short article about '{input.Topic}'.",
session: writerSession);
GeneratedContent content = writerResponse.Result;
// Human-in-the-loop iteration - we set a maximum number of attempts to avoid infinite loops
int iterationCount = 0;
while (iterationCount++ < input.MaxReviewAttempts)
{
// NOTE: CustomStatus has a 16 KB UTF-16 limit in Durable Functions.
// Only include short metadata here - the full content is passed via activity inputs/outputs.
context.SetCustomStatus(
new
{
message = "Requesting human feedback.",
approvalTimeoutHours = input.ApprovalTimeoutHours,
iterationCount,
contentTitle = content.Title,
});
// Step 2: Notify user to review the content
await context.CallActivityAsync(nameof(NotifyUserForApproval), content);
// Step 3: Wait for human feedback with configurable timeout
HumanApprovalResponse humanResponse;
try
{
humanResponse = await context.WaitForExternalEvent<HumanApprovalResponse>(
eventName: "HumanApproval",
timeout: TimeSpan.FromHours(input.ApprovalTimeoutHours));
}
catch (OperationCanceledException)
{
// Timeout occurred - treat as rejection
context.SetCustomStatus(
new
{
message = $"Human approval timed out after {input.ApprovalTimeoutHours} hour(s). Treating as rejection.",
iterationCount,
});
throw new TimeoutException($"Human approval timed out after {input.ApprovalTimeoutHours} hour(s).");
}
if (humanResponse.Approved)
{
context.SetCustomStatus(new
{
message = "Content approved by human reviewer. Publishing content...",
contentTitle = content.Title,
});
// Step 4: Publish the approved content
await context.CallActivityAsync(nameof(PublishContent), content);
context.SetCustomStatus(new
{
message = $"Content published successfully at {context.CurrentUtcDateTime:s}",
humanFeedback = humanResponse,
contentTitle = content.Title,
});
return new { content = content.Content };
}
context.SetCustomStatus(new
{
message = "Content rejected by human reviewer. Incorporating feedback and regenerating...",
humanFeedback = humanResponse,
contentTitle = content.Title,
});
// Incorporate human feedback and regenerate
writerResponse = await writerAgent.RunAsync<GeneratedContent>(
message: $"""
The content was rejected by a human reviewer. Please rewrite the article incorporating their feedback.
Human Feedback: {humanResponse.Feedback}
""",
session: writerSession);
content = writerResponse.Result;
}
// If we reach here, it means we exhausted the maximum number of iterations
throw new InvalidOperationException(
$"Content could not be approved after {input.MaxReviewAttempts} iterations.");
}
[Function(nameof(NotifyUserForApproval))]
public static void NotifyUserForApproval(
[ActivityTrigger] GeneratedContent content,
FunctionContext functionContext)
{
ILogger logger = functionContext.GetLogger(nameof(NotifyUserForApproval));
// In a real implementation, this would send notifications via email, SMS, etc.
logger.LogInformation(
"""
NOTIFICATION: Please review the following content for approval:
Title: {Title}
Content: {Content}
Use the approval endpoint to approve or reject this content.
""",
content.Title,
content.Content);
}
[Function(nameof(PublishContent))]
public static void PublishContent(
[ActivityTrigger] GeneratedContent content,
FunctionContext functionContext)
{
ILogger logger = functionContext.GetLogger(nameof(PublishContent));
// In a real implementation, this would publish to a CMS, website, etc.
logger.LogInformation(
"""
PUBLISHING: Content has been published successfully.
Title: {Title}
Content: {Content}
""",
content.Title,
content.Content);
}
}
@@ -0,0 +1,44 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace LongRunningTools;
/// <summary>
/// Represents the input for the content generation workflow.
/// </summary>
public sealed class ContentGenerationInput
{
[JsonPropertyName("topic")]
public string Topic { get; set; } = string.Empty;
[JsonPropertyName("max_review_attempts")]
public int MaxReviewAttempts { get; set; } = 3;
[JsonPropertyName("approval_timeout_hours")]
public float ApprovalTimeoutHours { get; set; } = 72;
}
/// <summary>
/// Represents the content generated by the writer agent.
/// </summary>
public sealed class GeneratedContent
{
[JsonPropertyName("title")]
public string Title { get; set; } = string.Empty;
[JsonPropertyName("content")]
public string Content { get; set; } = string.Empty;
}
/// <summary>
/// Represents the human approval response.
/// </summary>
public sealed class HumanApprovalResponse
{
[JsonPropertyName("approved")]
public bool Approved { get; set; }
[JsonPropertyName("feedback")]
public string Feedback { get; set; } = string.Empty;
}
@@ -0,0 +1,78 @@
// Copyright (c) Microsoft. All rights reserved.
#pragma warning disable IDE0002 // Simplify Member Access
using Azure;
using Azure.AI.OpenAI;
using Azure.Identity;
using LongRunningTools;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using OpenAI.Chat;
// Get the Azure OpenAI endpoint and deployment name from environment variables.
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME")
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
string? azureOpenAiKey = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY");
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
: new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential());
// Agent used by the orchestration to write content.
const string WriterAgentName = "Writer";
const string WriterAgentInstructions =
"""
You are a professional content writer who creates high-quality articles on various topics.
You write engaging, informative, and well-structured content that follows best practices for readability and accuracy.
""";
AIAgent writerAgent = client.GetChatClient(deploymentName).AsAIAgent(WriterAgentInstructions, WriterAgentName);
// Agent that can start content generation workflows using tools
const string PublisherAgentName = "Publisher";
const string PublisherAgentInstructions =
"""
You are a publishing agent that can manage content generation workflows.
You have access to tools to start, monitor, and raise events for content generation workflows.
""";
using IHost app = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableAgents(options =>
{
// Add the writer agent used by the orchestration
options.AddAIAgent(writerAgent);
// Define the agent that can start orchestrations from tool calls
options.AddAIAgentFactory(PublisherAgentName, sp =>
{
// Initialize the tools to be used by the agent.
Tools publisherTools = new(sp.GetRequiredService<ILogger<Tools>>());
return client.GetChatClient(deploymentName).AsAIAgent(
instructions: PublisherAgentInstructions,
name: PublisherAgentName,
services: sp,
tools: [
AIFunctionFactory.Create(publisherTools.StartContentGenerationWorkflow),
AIFunctionFactory.Create(publisherTools.GetWorkflowStatusAsync),
AIFunctionFactory.Create(publisherTools.SubmitHumanApprovalAsync),
]);
});
})
.Build();
app.Run();
@@ -0,0 +1,129 @@
# Long Running Tools Sample
This sample demonstrates how to use the Durable Agent Framework (DAFx) to create agents with long running tools. This sample builds on the [05_AgentOrchestration_HITL](../05_AgentOrchestration_HITL) sample by adding a publisher agent that can start and manage content generation workflows. A key difference is that the publisher agent knows the IDs of the workflows it starts, so it can check the status of the workflows and approve or reject them without being explicitly given the context (instance IDs, etc).
## Key Concepts Demonstrated
The same key concepts as the [05_AgentOrchestration_HITL](../05_AgentOrchestration_HITL) sample are demonstrated, but with the following additional concepts:
- **Long running tools**: Using `DurableAgentContext.Current` to start orchestrations from tool calls
- **Multi-agent orchestration**: Agents can start and manage workflows that orchestrate other agents
- **Human-in-the-loop (with delegation)**: The agent acts as an intermediary between the human and the workflow. The human remains in the loop, but delegates to the agent to start the workflow and approve or reject the content.
## Environment Setup
See the [README.md](../README.md) file in the parent directory for more information on how to configure the environment, including how to install and run common sample dependencies.
## Running the Sample
With the environment setup and function app running, you can test the sample by sending an HTTP request to start the agent, which will then trigger the content generation workflow.
You can use the `demo.http` file to send requests to the agent, or a command line tool like `curl` as shown below.
Bash (Linux/macOS/WSL):
```bash
curl -i -X POST http://localhost:7071/api/agents/publisher/run \
-D headers.txt \
-H "Content-Type: text/plain" \
-d 'Start a content generation workflow for the topic \"The Future of Artificial Intelligence\"'
# Save the thread ID to a variable and print it to the terminal
threadId=$(cat headers.txt | grep "x-ms-thread-id" | cut -d' ' -f2)
echo "Thread ID: $threadId"
```
PowerShell:
```powershell
Invoke-RestMethod -Method Post `
-Uri http://localhost:7071/api/agents/publisher/run `
-ResponseHeadersVariable ResponseHeaders `
-ContentType text/plain `
-Body 'Start a content generation workflow for the topic \"The Future of Artificial Intelligence\"' `
# Save the thread ID to a variable and print it to the console
$threadId = $ResponseHeaders['x-ms-thread-id']
Write-Host "Thread ID: $threadId"
```
The response will be a text string that looks something like the following, indicating that the agent request has been received and will be processed:
```http
HTTP/1.1 200 OK
Content-Type: text/plain
x-ms-thread-id: 351ec855-7f4d-4527-a60d-498301ced36d
```
The `x-ms-thread-id` response header contains the thread ID, which can be used to continue the conversation by passing it as a query parameter (`thread_id`) to the `run` endpoint. The commands above show how to save the thread ID to a `$threadId` variable for use in subsequent requests.
Behind the scenes, the publisher agent will:
1. Start the content generation workflow via a tool call
1. The workflow will generate initial content using the Writer agent and wait for human approval, which will be visible in the logs
Once the workflow is waiting for human approval, you can send approval or rejection by prompting the publisher agent accordingly (e.g. "Approve the content" or "Reject the content with feedback: The article needs more technical depth and better examples."):
Bash (Linux/macOS/WSL):
```bash
# Approve the content
curl -X POST "http://localhost:7071/api/agents/publisher/run?thread_id=$threadId" \
-H "Content-Type: text/plain" \
-d 'Approve the content'
# Reject the content with feedback
curl -X POST "http://localhost:7071/api/agents/publisher/run?thread_id=$threadId" \
-H "Content-Type: text/plain" \
-d 'Reject the content with feedback: The article needs more technical depth and better examples.'
```
PowerShell:
```powershell
# Approve the content
Invoke-RestMethod -Method Post `
-Uri "http://localhost:7071/api/agents/publisher/run?thread_id=$threadId" `
-ContentType text/plain `
-Body 'Approve the content'
# Reject the content with feedback
Invoke-RestMethod -Method Post `
-Uri "http://localhost:7071/api/agents/publisher/run?thread_id=$threadId" `
-ContentType text/plain `
-Body 'Reject the content with feedback: The article needs more technical depth and better examples.'
```
Once the workflow has completed, you can get the status by prompting the publisher agent to give you the status.
Bash (Linux/macOS/WSL):
```bash
curl -X POST "http://localhost:7071/api/agents/publisher/run?thread_id=$threadId" \
-H "Content-Type: text/plain" \
-d 'Get the status of the workflow you previously started'
```
PowerShell:
```powershell
Invoke-RestMethod -Method Post `
-Uri "http://localhost:7071/api/agents/publisher/run?thread_id=$threadId" `
-ContentType text/plain `
-Body 'Get the status of the workflow you previously started'
```
The response from the publisher agent will look something like the following:
```text
The status of the workflow with instance ID **ab1076d6e7ec49d8a2c2474d09b69ded** is as follows:
- **Execution Status:** Completed
- **Workflow Status:** Content published successfully at `2025-10-24T20:42:02`
- **Created At:** `2025-10-24T20:41:40.7531781+00:00`
- **Last Updated At:** `2025-10-24T20:42:02.1410736+00:00`
The content has been successfully published.
```
```
@@ -0,0 +1,93 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using Microsoft.Agents.AI.DurableTask;
using Microsoft.DurableTask.Client;
using Microsoft.Extensions.Logging;
namespace LongRunningTools;
/// <summary>
/// Tools that demonstrate starting orchestrations from agent tool calls.
/// </summary>
internal sealed class Tools(ILogger<Tools> logger)
{
private readonly ILogger<Tools> _logger = logger;
[Description("Starts a content generation workflow and returns the instance ID for tracking.")]
public string StartContentGenerationWorkflow([Description("The topic for content generation")] string topic)
{
this._logger.LogInformation("Starting content generation workflow for topic: {Topic}", SanitizeLogValue(topic));
const int MaxReviewAttempts = 3;
const float ApprovalTimeoutHours = 72;
// Schedule the orchestration, which will start running after the tool call completes.
string instanceId = DurableAgentContext.Current.ScheduleNewOrchestration(
name: nameof(FunctionTriggers.RunOrchestrationAsync),
input: new ContentGenerationInput
{
Topic = topic,
MaxReviewAttempts = MaxReviewAttempts,
ApprovalTimeoutHours = ApprovalTimeoutHours
});
this._logger.LogInformation(
"Content generation workflow scheduled to be started for topic '{Topic}' with instance ID: {InstanceId}",
SanitizeLogValue(topic),
instanceId);
return $"Workflow started with instance ID: {instanceId}";
}
[Description("Gets the status of a workflow orchestration.")]
public async Task<object> GetWorkflowStatusAsync(
[Description("The instance ID of the workflow to check")] string instanceId,
[Description("Whether to include detailed information")] bool includeDetails = true)
{
this._logger.LogInformation("Getting status for workflow instance: {InstanceId}", SanitizeLogValue(instanceId));
// Get the current agent context using the session-static property
OrchestrationMetadata? status = await DurableAgentContext.Current.GetOrchestrationStatusAsync(
instanceId,
includeDetails);
if (status is null)
{
this._logger.LogInformation("Workflow instance '{InstanceId}' not found.", SanitizeLogValue(instanceId));
return new
{
instanceId,
error = $"Workflow instance '{instanceId}' not found.",
};
}
return new
{
instanceId = status.InstanceId,
createdAt = status.CreatedAt,
executionStatus = status.RuntimeStatus,
workflowStatus = status.SerializedCustomStatus,
lastUpdatedAt = status.LastUpdatedAt,
failureDetails = status.FailureDetails
};
}
[Description("Raises a feedback event for the content generation workflow.")]
public async Task SubmitHumanApprovalAsync(
[Description("The instance ID of the workflow to submit feedback for")] string instanceId,
[Description("Feedback to submit")] HumanApprovalResponse feedback)
{
this._logger.LogInformation("Submitting human approval for workflow instance: {InstanceId}", SanitizeLogValue(instanceId));
await DurableAgentContext.Current.RaiseOrchestrationEventAsync(instanceId, "HumanApproval", feedback);
}
/// <summary>
/// Sanitizes a user-provided value for safe inclusion in log entries
/// by removing control characters that could be used for log forging.
/// </summary>
private static string SanitizeLogValue(string value) =>
value
.Replace("\r", string.Empty, StringComparison.Ordinal)
.Replace("\n", string.Empty, StringComparison.Ordinal);
}
@@ -0,0 +1,27 @@
### Run an agent that can schedule orchestrations as tool calls
POST http://localhost:7071/api/agents/publisher/run
Content-Type: text/plain
Start a content generation workflow for the topic 'The Future of Artificial Intelligence'
### Save the session ID from the response to continue the conversation
@threadId = <YOUR_THREAD_ID>
### Check the status of the workflow
POST http://localhost:7071/api/agents/publisher/run?thread_id={{threadId}}
Content-Type: text/plain
Check the status of the workflow you previously started
### Reject content with feedback
POST http://localhost:7071/api/agents/publisher/run?thread_id={{threadId}}
Content-Type: text/plain
Reject the content with feedback: The article needs more technical depth and better examples.
### Approve content
POST http://localhost:7071/api/agents/publisher/run?thread_id={{threadId}}
Content-Type: text/plain
Approve the content
@@ -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"
}
}
}
}