test
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
Python - Merge - Tests / paths-filter (push) Has been cancelled
Python - Merge - Tests / Python Tests - Core (integration, ubuntu-latest, 3.10) (push) Has been cancelled
Python - Merge - Tests / Python Tests - Azure AI (integration, ubuntu-latest, 3.10) (push) Has been cancelled
Python - Merge - Tests / python-integration-tests-check (push) Has been cancelled
Python - Lab Tests / paths-filter (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.10) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.11) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.12) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.13) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.14) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.10) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.11) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.12) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.13) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.14) (push) Has been cancelled
Check .md links / markdown-link-check (push) Has been cancelled
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
Python - Merge - Tests / paths-filter (push) Has been cancelled
Python - Merge - Tests / Python Tests - Core (integration, ubuntu-latest, 3.10) (push) Has been cancelled
Python - Merge - Tests / Python Tests - Azure AI (integration, ubuntu-latest, 3.10) (push) Has been cancelled
Python - Merge - Tests / python-integration-tests-check (push) Has been cancelled
Python - Lab Tests / paths-filter (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.10) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.11) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.12) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.13) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.14) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.10) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.11) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.12) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.13) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.14) (push) Has been cancelled
Check .md links / markdown-link-check (push) Has been cancelled
This commit is contained in:
@@ -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>AgentOrchestration_HITL</AssemblyName>
|
||||
<RootNamespace>AgentOrchestration_HITL</RootNamespace>
|
||||
<NoWarn>$(NoWarn);DURABLE0001;DURABLE0002;DURABLE0003;DURABLE0004;DURABLE0005;DURABLE0006</NoWarn>
|
||||
</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,229 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.DurableTask;
|
||||
using Microsoft.Azure.Functions.Worker;
|
||||
using Microsoft.Azure.Functions.Worker.Http;
|
||||
using Microsoft.DurableTask;
|
||||
using Microsoft.DurableTask.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace AgentOrchestration_HITL;
|
||||
|
||||
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("WriterAgent");
|
||||
AgentThread writerThread = await writerAgent.GetNewThreadAsync();
|
||||
|
||||
// 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}'.",
|
||||
thread: writerThread);
|
||||
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)
|
||||
{
|
||||
context.SetCustomStatus(
|
||||
$"Requesting human feedback. Iteration #{iterationCount}. Timeout: {input.ApprovalTimeoutHours} hour(s).");
|
||||
|
||||
// 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(
|
||||
$"Human approval timed out after {input.ApprovalTimeoutHours} hour(s). Treating as rejection.");
|
||||
throw new TimeoutException($"Human approval timed out after {input.ApprovalTimeoutHours} hour(s).");
|
||||
}
|
||||
|
||||
if (humanResponse.Approved)
|
||||
{
|
||||
context.SetCustomStatus("Content approved by human reviewer. Publishing content...");
|
||||
|
||||
// Step 4: Publish the approved content
|
||||
await context.CallActivityAsync(nameof(PublishContent), content);
|
||||
|
||||
context.SetCustomStatus($"Content published successfully at {context.CurrentUtcDateTime:s}");
|
||||
return new { content = content.Content };
|
||||
}
|
||||
|
||||
context.SetCustomStatus("Content rejected by human reviewer. Incorporating feedback and regenerating...");
|
||||
|
||||
// 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}
|
||||
""",
|
||||
thread: writerThread);
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
// POST /hitl/run
|
||||
[Function(nameof(StartOrchestrationAsync))]
|
||||
public static async Task<HttpResponseData> StartOrchestrationAsync(
|
||||
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "hitl/run")] HttpRequestData req,
|
||||
[DurableClient] DurableTaskClient client)
|
||||
{
|
||||
// Read the input from the request body
|
||||
ContentGenerationInput? input = await req.ReadFromJsonAsync<ContentGenerationInput>();
|
||||
if (input is null || string.IsNullOrWhiteSpace(input.Topic))
|
||||
{
|
||||
HttpResponseData badRequestResponse = req.CreateResponse(HttpStatusCode.BadRequest);
|
||||
await badRequestResponse.WriteAsJsonAsync(new { error = "Topic is required" });
|
||||
return badRequestResponse;
|
||||
}
|
||||
|
||||
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
|
||||
orchestratorName: nameof(RunOrchestrationAsync),
|
||||
input: input);
|
||||
|
||||
HttpResponseData response = req.CreateResponse(HttpStatusCode.Accepted);
|
||||
await response.WriteAsJsonAsync(new
|
||||
{
|
||||
message = "HITL content generation orchestration started.",
|
||||
topic = input.Topic,
|
||||
instanceId,
|
||||
statusQueryGetUri = GetStatusQueryGetUri(req, instanceId),
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
// POST /hitl/approve/{instanceId}
|
||||
[Function(nameof(SendHumanApprovalAsync))]
|
||||
public static async Task<HttpResponseData> SendHumanApprovalAsync(
|
||||
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "hitl/approve/{instanceId}")] HttpRequestData req,
|
||||
string instanceId,
|
||||
[DurableClient] DurableTaskClient client)
|
||||
{
|
||||
// Read the approval response from the request body
|
||||
HumanApprovalResponse? approvalResponse = await req.ReadFromJsonAsync<HumanApprovalResponse>();
|
||||
if (approvalResponse is null)
|
||||
{
|
||||
HttpResponseData badRequestResponse = req.CreateResponse(HttpStatusCode.BadRequest);
|
||||
await badRequestResponse.WriteAsJsonAsync(new { error = "Approval response is required" });
|
||||
return badRequestResponse;
|
||||
}
|
||||
|
||||
// Send the approval event to the orchestration
|
||||
await client.RaiseEventAsync(instanceId, "HumanApproval", approvalResponse);
|
||||
|
||||
HttpResponseData response = req.CreateResponse(HttpStatusCode.OK);
|
||||
await response.WriteAsJsonAsync(new
|
||||
{
|
||||
message = "Human approval sent to orchestration.",
|
||||
instanceId,
|
||||
approved = approvalResponse.Approved
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
// GET /hitl/status/{instanceId}
|
||||
[Function(nameof(GetOrchestrationStatusAsync))]
|
||||
public static async Task<HttpResponseData> GetOrchestrationStatusAsync(
|
||||
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "hitl/status/{instanceId}")] HttpRequestData req,
|
||||
string instanceId,
|
||||
[DurableClient] DurableTaskClient client)
|
||||
{
|
||||
OrchestrationMetadata? status = await client.GetInstanceAsync(
|
||||
instanceId,
|
||||
getInputsAndOutputs: true,
|
||||
req.FunctionContext.CancellationToken);
|
||||
|
||||
if (status is null)
|
||||
{
|
||||
HttpResponseData notFound = req.CreateResponse(HttpStatusCode.NotFound);
|
||||
await notFound.WriteAsJsonAsync(new { error = "Instance not found" });
|
||||
return notFound;
|
||||
}
|
||||
|
||||
HttpResponseData response = req.CreateResponse(HttpStatusCode.OK);
|
||||
await response.WriteAsJsonAsync(new
|
||||
{
|
||||
instanceId = status.InstanceId,
|
||||
runtimeStatus = status.RuntimeStatus.ToString(),
|
||||
workflowStatus = status.SerializedCustomStatus is not null ? (object)status.ReadCustomStatusAs<JsonElement>() : null,
|
||||
input = status.SerializedInput is not null ? (object)status.ReadInputAs<JsonElement>() : null,
|
||||
output = status.SerializedOutput is not null ? (object)status.ReadOutputAs<JsonElement>() : null,
|
||||
failureDetails = status.FailureDetails
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
private static string GetStatusQueryGetUri(HttpRequestData req, string instanceId)
|
||||
{
|
||||
// NOTE: This can be made more robust by considering the value of
|
||||
// request headers like "X-Forwarded-Host" and "X-Forwarded-Proto".
|
||||
string authority = $"{req.Url.Scheme}://{req.Url.Authority}";
|
||||
return $"{authority}/api/hitl/status/{instanceId}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AgentOrchestration_HITL;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the input for the Human-in-the-Loop 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,40 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Azure;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hosting.AzureFunctions;
|
||||
using Microsoft.Azure.Functions.Worker.Builder;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
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")
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT is not set.");
|
||||
|
||||
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
|
||||
string? azureOpenAiKey = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
|
||||
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
|
||||
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
|
||||
|
||||
// Single agent used by the orchestration to demonstrate human-in-the-loop workflow.
|
||||
const string WriterName = "WriterAgent";
|
||||
const string WriterInstructions =
|
||||
"""
|
||||
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(WriterInstructions, WriterName);
|
||||
|
||||
using IHost app = FunctionsApplication
|
||||
.CreateBuilder(args)
|
||||
.ConfigureFunctionsWebApplication()
|
||||
.ConfigureDurableAgents(options => options.AddAIAgent(writerAgent))
|
||||
.Build();
|
||||
|
||||
app.Run();
|
||||
@@ -0,0 +1,126 @@
|
||||
# Multi-Agent Orchestration with Human-in-the-Loop Sample
|
||||
|
||||
This sample demonstrates how to use the Durable Agent Framework (DAFx) to create a human-in-the-loop (HITL) workflow using a single AI agent. The workflow uses a writer agent to generate content and requires human approval on every iteration, emphasizing the human-in-the-loop pattern.
|
||||
|
||||
## Key Concepts Demonstrated
|
||||
|
||||
- Single-agent orchestration
|
||||
- Human-in-the-loop feedback loop using external events (`WaitForExternalEvent`)
|
||||
- Activity functions for non-agentic workflow steps
|
||||
- Iterative content refinement based on human feedback
|
||||
- Custom status tracking for workflow visibility
|
||||
- Error handling with maximum retry attempts and timeout handling for human approval
|
||||
|
||||
## 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 with a topic to start the content generation workflow.
|
||||
|
||||
You can use the `demo.http` file to send a topic to the agents, or a command line tool like `curl` as shown below:
|
||||
|
||||
Bash (Linux/macOS/WSL):
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:7071/api/hitl/run \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"topic": "The Future of Artificial Intelligence",
|
||||
"max_review_attempts": 3,
|
||||
"timeout_minutes": 5
|
||||
}'
|
||||
```
|
||||
|
||||
PowerShell:
|
||||
|
||||
```powershell
|
||||
$body = @{
|
||||
topic = "The Future of Artificial Intelligence"
|
||||
max_review_attempts = 3
|
||||
timeout_minutes = 5
|
||||
} | ConvertTo-Json
|
||||
|
||||
Invoke-RestMethod -Method Post `
|
||||
-Uri http://localhost:7071/api/hitl/run `
|
||||
-ContentType application/json `
|
||||
-Body $body
|
||||
```
|
||||
|
||||
The response will be a JSON object that looks something like the following, which indicates that the orchestration has started.
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "HITL content generation orchestration started.",
|
||||
"topic": "The Future of Artificial Intelligence",
|
||||
"instanceId": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
|
||||
"statusQueryGetUri": "http://localhost:7071/api/hitl/status/a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
|
||||
}
|
||||
```
|
||||
|
||||
The orchestration will:
|
||||
|
||||
1. Generate initial content using the WriterAgent
|
||||
2. Notify the user to review the content
|
||||
3. Wait for human feedback via external event (configurable timeout)
|
||||
4. If approved by human, publish the content
|
||||
5. If rejected by human, incorporate feedback and regenerate content
|
||||
6. If approval timeout occurs, treat as rejection and fail the orchestration
|
||||
7. Repeat until human approval is received or maximum loop iterations are reached
|
||||
|
||||
Once the orchestration is waiting for human approval, you can send approval or rejection using the approval endpoint:
|
||||
|
||||
Bash (Linux/macOS/WSL):
|
||||
|
||||
```bash
|
||||
# Approve the content
|
||||
curl -X POST http://localhost:7071/api/hitl/approve/a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"approved": true,
|
||||
"feedback": "Great article! The content is well-structured and informative."
|
||||
}'
|
||||
|
||||
# Reject the content with feedback
|
||||
curl -X POST http://localhost:7071/api/hitl/approve/a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"approved": false,
|
||||
"feedback": "The article needs more technical depth and better examples."
|
||||
}'
|
||||
```
|
||||
|
||||
PowerShell:
|
||||
|
||||
```powershell
|
||||
# Approve the content
|
||||
Invoke-RestMethod -Method Post `
|
||||
-Uri http://localhost:7071/api/hitl/approve/a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 `
|
||||
-ContentType application/json `
|
||||
-Body '{ "approved": true, "feedback": "Great article! The content is well-structured and informative." }'
|
||||
|
||||
# Reject the content with feedback
|
||||
Invoke-RestMethod -Method Post `
|
||||
-Uri http://localhost:7071/api/hitl/approve/a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 `
|
||||
-ContentType application/json `
|
||||
-Body '{ "approved": false, "feedback": "The article needs more technical depth and better examples." }'
|
||||
```
|
||||
|
||||
Once the orchestration has completed, you can get the status by sending a GET request to the `statusQueryGetUri` URL. The response will be a JSON object that looks something like the following:
|
||||
|
||||
```json
|
||||
{
|
||||
"failureDetails": null,
|
||||
"input": {
|
||||
"topic": "The Future of Artificial Intelligence",
|
||||
"max_review_attempts": 3
|
||||
},
|
||||
"instanceId": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
|
||||
"output": {
|
||||
"content": "The Future of Artificial Intelligence is..."
|
||||
},
|
||||
"runtimeStatus": "Completed",
|
||||
"workflowStatus": "Content published successfully at 2025-10-15T12:00:00Z"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,44 @@
|
||||
### Start the HITL content generation orchestration with default timeout (30 days)
|
||||
POST http://localhost:7071/api/hitl/run
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"topic": "The Future of Artificial Intelligence",
|
||||
"max_review_attempts": 3
|
||||
}
|
||||
|
||||
|
||||
### Start the HITL content generation orchestration with very short timeout for demonstration (~4 seconds)
|
||||
POST http://localhost:7071/api/hitl/run
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"topic": "The Future of Artificial Intelligence",
|
||||
"max_review_attempts": 3,
|
||||
"approval_timeout_hours": 0.001
|
||||
}
|
||||
|
||||
|
||||
### Copy/paste the instanceId from the response above
|
||||
@instanceId=INSTANCE_ID_GOES_HERE
|
||||
|
||||
### Check the status of the orchestration (replace {instanceId} with the actual instance ID from the response above)
|
||||
GET http://localhost:7071/api/hitl/status/{{instanceId}}
|
||||
|
||||
### Send human approval (replace {instanceId} with the actual instance ID)
|
||||
POST http://localhost:7071/api/hitl/approve/{{instanceId}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"approved": true,
|
||||
"feedback": "Great article! The content is well-structured and informative."
|
||||
}
|
||||
|
||||
### Send human rejection with feedback (replace {instanceId} with the actual instance ID)
|
||||
POST http://localhost:7071/api/hitl/approve/{{instanceId}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"approved": false,
|
||||
"feedback": "The article needs more technical depth and better examples. Please add more specific use cases and implementation details."
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"IsEncrypted": false,
|
||||
"Values": {
|
||||
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
|
||||
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
|
||||
"DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None",
|
||||
"AZURE_OPENAI_ENDPOINT": "<AZURE_OPENAI_ENDPOINT>",
|
||||
"AZURE_OPENAI_DEPLOYMENT": "<AZURE_OPENAI_DEPLOYMENT>"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user