mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Merge branch 'main' into feature-azure-functions
This commit is contained in:
@@ -143,6 +143,7 @@
|
||||
<Project Path="samples/GettingStarted/Workflows/_Foundational/05_MultiModelService/05_MultiModelService.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/_Foundational/06_SubWorkflows/06_SubWorkflows.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/_Foundational/07_MixedWorkflowAgentsAndExecutors/07_MixedWorkflowAgentsAndExecutors.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/_Foundational/08_WriterCriticWorkflow/08_WriterCriticWorkflow.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/Catalog/">
|
||||
<Project Path="samples/Catalog/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj" />
|
||||
|
||||
@@ -19,6 +19,7 @@ Please begin with the [Foundational](./_Foundational) samples in order. These th
|
||||
| [Multi-Service Workflows](./_Foundational/05_MultiModelService) | Shows using multiple AI services in the same workflow |
|
||||
| [Sub-Workflows](./_Foundational/06_SubWorkflows) | Demonstrates composing workflows hierarchically by embedding workflows as executors |
|
||||
| [Mixed Workflow with Agents and Executors](./_Foundational/07_MixedWorkflowAgentsAndExecutors) | Shows how to mix agents and executors with adapter pattern for type conversion and protocol handling |
|
||||
| [Writer-Critic Workflow](./_Foundational/08_WriterCriticWorkflow) | Demonstrates iterative refinement with quality gates, max iteration safety, multiple message handlers, and conditional routing for feedback loops |
|
||||
|
||||
Once completed, please proceed to other samples listed below.
|
||||
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<RootNamespace>WriterCriticWorkflow</RootNamespace>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+409
@@ -0,0 +1,409 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace WriterCriticWorkflow;
|
||||
|
||||
/// <summary>
|
||||
/// This sample demonstrates an iterative refinement workflow between Writer and Critic agents.
|
||||
///
|
||||
/// The workflow implements a content creation and review loop that:
|
||||
/// 1. Writer creates initial content based on the user's request
|
||||
/// 2. Critic reviews the content and provides feedback using structured output
|
||||
/// 3. If approved: Summary executor presents the final content
|
||||
/// 4. If rejected: Writer revises based on feedback (loops back)
|
||||
/// 5. Continues until approval or max iterations (3) is reached
|
||||
///
|
||||
/// This pattern is useful when you need:
|
||||
/// - Iterative content improvement through feedback loops
|
||||
/// - Quality gates with reviewer approval
|
||||
/// - Maximum iteration limits to prevent infinite loops
|
||||
/// - Conditional workflow routing based on agent decisions
|
||||
/// - Structured output for reliable decision-making
|
||||
///
|
||||
/// Key Learning: Workflows can implement loops with conditional edges, shared state,
|
||||
/// and structured output for robust agent decision-making.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Pre-requisites:
|
||||
/// - Previous foundational samples should be completed first.
|
||||
/// - An Azure OpenAI chat completion deployment must be configured.
|
||||
/// </remarks>
|
||||
public static class Program
|
||||
{
|
||||
public const int MaxIterations = 3;
|
||||
|
||||
private static async Task Main()
|
||||
{
|
||||
Console.WriteLine("\n=== Writer-Critic Iteration Workflow ===\n");
|
||||
Console.WriteLine($"Writer and Critic will iterate up to {MaxIterations} times until approval.\n");
|
||||
|
||||
// Set up the Azure OpenAI client
|
||||
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
IChatClient chatClient = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()).GetChatClient(deploymentName).AsIChatClient();
|
||||
|
||||
// Create executors for content creation and review
|
||||
WriterExecutor writer = new(chatClient);
|
||||
CriticExecutor critic = new(chatClient);
|
||||
SummaryExecutor summary = new(chatClient);
|
||||
|
||||
// Build the workflow with conditional routing based on critic's decision
|
||||
WorkflowBuilder workflowBuilder = new WorkflowBuilder(writer)
|
||||
.AddEdge(writer, critic)
|
||||
.AddSwitch(critic, sw => sw
|
||||
.AddCase<CriticDecision>(cd => cd?.Approved == true, summary)
|
||||
.AddCase<CriticDecision>(cd => cd?.Approved == false, writer))
|
||||
.WithOutputFrom(summary);
|
||||
|
||||
// Execute the workflow with a sample task
|
||||
// The workflow loops back to Writer if content is rejected,
|
||||
// or proceeds to Summary if approved. State tracking ensures we don't loop forever.
|
||||
Console.WriteLine(new string('=', 80));
|
||||
Console.WriteLine("TASK: Write a short blog post about AI ethics (200 words)");
|
||||
Console.WriteLine(new string('=', 80) + "\n");
|
||||
|
||||
const string InitialTask = "Write a 200-word blog post about AI ethics. Make it thoughtful and engaging.";
|
||||
|
||||
Workflow workflow = workflowBuilder.Build();
|
||||
await ExecuteWorkflowAsync(workflow, InitialTask);
|
||||
|
||||
Console.WriteLine("\n✅ Sample Complete: Writer-Critic iteration demonstrates conditional workflow loops\n");
|
||||
Console.WriteLine("Key Concepts Demonstrated:");
|
||||
Console.WriteLine(" ✓ Iterative refinement loop with conditional routing");
|
||||
Console.WriteLine(" ✓ Shared workflow state for iteration tracking");
|
||||
Console.WriteLine($" ✓ Max iteration cap ({MaxIterations}) for safety");
|
||||
Console.WriteLine(" ✓ Multiple message handlers in a single executor");
|
||||
Console.WriteLine(" ✓ Streaming support with structured output\n");
|
||||
}
|
||||
|
||||
private static async Task ExecuteWorkflowAsync(Workflow workflow, string input)
|
||||
{
|
||||
// Execute in streaming mode to see real-time progress
|
||||
await using StreamingRun run = await InProcessExecution.StreamAsync<string>(workflow, input);
|
||||
|
||||
// Watch the workflow events
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
|
||||
{
|
||||
switch (evt)
|
||||
{
|
||||
case AgentRunUpdateEvent agentUpdate:
|
||||
// Stream agent output in real-time
|
||||
if (!string.IsNullOrEmpty(agentUpdate.Update.Text))
|
||||
{
|
||||
Console.Write(agentUpdate.Update.Text);
|
||||
}
|
||||
break;
|
||||
|
||||
case WorkflowOutputEvent output:
|
||||
Console.WriteLine("\n\n" + new string('=', 80));
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine("✅ FINAL APPROVED CONTENT");
|
||||
Console.ResetColor();
|
||||
Console.WriteLine(new string('=', 80));
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(output.Data);
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(new string('=', 80));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================================
|
||||
// Shared State for Iteration Tracking
|
||||
// ====================================
|
||||
|
||||
/// <summary>
|
||||
/// Tracks the current iteration and conversation history across workflow executions.
|
||||
/// </summary>
|
||||
internal sealed class FlowState
|
||||
{
|
||||
public int Iteration { get; set; } = 1;
|
||||
public List<ChatMessage> History { get; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constants for accessing the shared flow state in workflow context.
|
||||
/// </summary>
|
||||
internal static class FlowStateShared
|
||||
{
|
||||
public const string Scope = "FlowStateScope";
|
||||
public const string Key = "singleton";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper methods for reading and writing shared flow state.
|
||||
/// </summary>
|
||||
internal static class FlowStateHelpers
|
||||
{
|
||||
public static async Task<FlowState> ReadFlowStateAsync(IWorkflowContext context)
|
||||
{
|
||||
FlowState? state = await context.ReadStateAsync<FlowState>(FlowStateShared.Key, scopeName: FlowStateShared.Scope);
|
||||
return state ?? new FlowState();
|
||||
}
|
||||
|
||||
public static ValueTask SaveFlowStateAsync(IWorkflowContext context, FlowState state)
|
||||
=> context.QueueStateUpdateAsync(FlowStateShared.Key, state, scopeName: FlowStateShared.Scope);
|
||||
}
|
||||
|
||||
// ====================================
|
||||
// Data Transfer Objects
|
||||
// ====================================
|
||||
|
||||
/// <summary>
|
||||
/// Structured output schema for the Critic's decision.
|
||||
/// Uses JsonPropertyName and Description attributes for OpenAI's JSON schema.
|
||||
/// </summary>
|
||||
[Description("Critic's review decision including approval status and feedback")]
|
||||
[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated via JSON deserialization")]
|
||||
internal sealed class CriticDecision
|
||||
{
|
||||
[JsonPropertyName("approved")]
|
||||
[Description("Whether the content is approved (true) or needs revision (false)")]
|
||||
public bool Approved { get; set; }
|
||||
|
||||
[JsonPropertyName("feedback")]
|
||||
[Description("Specific feedback for improvements if not approved, empty if approved")]
|
||||
public string Feedback { get; set; } = "";
|
||||
|
||||
// Non-JSON properties for workflow use
|
||||
[JsonIgnore]
|
||||
public string Content { get; set; } = "";
|
||||
|
||||
[JsonIgnore]
|
||||
public int Iteration { get; set; }
|
||||
}
|
||||
|
||||
// ====================================
|
||||
// Custom Executors
|
||||
// ====================================
|
||||
|
||||
/// <summary>
|
||||
/// Executor that creates or revises content based on user requests or critic feedback.
|
||||
/// This executor demonstrates multiple message handlers for different input types.
|
||||
/// </summary>
|
||||
internal sealed class WriterExecutor : Executor
|
||||
{
|
||||
private readonly AIAgent _agent;
|
||||
|
||||
public WriterExecutor(IChatClient chatClient) : base("Writer")
|
||||
{
|
||||
this._agent = new ChatClientAgent(
|
||||
chatClient,
|
||||
name: "Writer",
|
||||
instructions: """
|
||||
You are a skilled writer. Create clear, engaging content.
|
||||
If you receive feedback, carefully revise the content to address all concerns.
|
||||
Maintain the same topic and length requirements.
|
||||
"""
|
||||
);
|
||||
}
|
||||
|
||||
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
|
||||
routeBuilder
|
||||
.AddHandler<string, ChatMessage>(this.HandleInitialRequestAsync)
|
||||
.AddHandler<CriticDecision, ChatMessage>(this.HandleRevisionRequestAsync);
|
||||
|
||||
/// <summary>
|
||||
/// Handles the initial writing request from the user.
|
||||
/// </summary>
|
||||
private async ValueTask<ChatMessage> HandleInitialRequestAsync(
|
||||
string message,
|
||||
IWorkflowContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await this.HandleAsyncCoreAsync(new ChatMessage(ChatRole.User, message), context, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles revision requests from the critic with feedback.
|
||||
/// </summary>
|
||||
private async ValueTask<ChatMessage> HandleRevisionRequestAsync(
|
||||
CriticDecision decision,
|
||||
IWorkflowContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
string prompt = "Revise the following content based on this feedback:\n\n" +
|
||||
$"Feedback: {decision.Feedback}\n\n" +
|
||||
$"Original Content:\n{decision.Content}";
|
||||
|
||||
return await this.HandleAsyncCoreAsync(new ChatMessage(ChatRole.User, prompt), context, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Core implementation for generating content (initial or revised).
|
||||
/// </summary>
|
||||
private async Task<ChatMessage> HandleAsyncCoreAsync(
|
||||
ChatMessage message,
|
||||
IWorkflowContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
FlowState state = await FlowStateHelpers.ReadFlowStateAsync(context);
|
||||
|
||||
Console.WriteLine($"\n=== Writer (Iteration {state.Iteration}) ===\n");
|
||||
|
||||
StringBuilder sb = new();
|
||||
await foreach (AgentRunResponseUpdate update in this._agent.RunStreamingAsync(message, cancellationToken: cancellationToken))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(update.Text))
|
||||
{
|
||||
sb.Append(update.Text);
|
||||
Console.Write(update.Text);
|
||||
}
|
||||
}
|
||||
Console.WriteLine("\n");
|
||||
|
||||
string text = sb.ToString();
|
||||
state.History.Add(new ChatMessage(ChatRole.Assistant, text));
|
||||
await FlowStateHelpers.SaveFlowStateAsync(context, state);
|
||||
|
||||
return new ChatMessage(ChatRole.User, text);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executor that reviews content and decides whether to approve or request revisions.
|
||||
/// Uses structured output with streaming for reliable decision-making.
|
||||
/// </summary>
|
||||
internal sealed class CriticExecutor : Executor<ChatMessage, CriticDecision>
|
||||
{
|
||||
private readonly AIAgent _agent;
|
||||
|
||||
public CriticExecutor(IChatClient chatClient) : base("Critic")
|
||||
{
|
||||
this._agent = new ChatClientAgent(chatClient, new ChatClientAgentOptions
|
||||
{
|
||||
Name = "Critic",
|
||||
Instructions = """
|
||||
You are a constructive critic. Review the content and provide specific feedback.
|
||||
Always try to provide actionable suggestions for improvement and strive to identify improvement points.
|
||||
Only approve if the content is high quality, clear, and meets the original requirements and you see no improvement points.
|
||||
|
||||
Provide your decision as structured output with:
|
||||
- approved: true if content is good, false if revisions needed
|
||||
- feedback: specific improvements needed (empty if approved)
|
||||
|
||||
Be concise but specific in your feedback.
|
||||
""",
|
||||
ChatOptions = new()
|
||||
{
|
||||
ResponseFormat = ChatResponseFormat.ForJsonSchema<CriticDecision>()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public override async ValueTask<CriticDecision> HandleAsync(
|
||||
ChatMessage message,
|
||||
IWorkflowContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
FlowState state = await FlowStateHelpers.ReadFlowStateAsync(context);
|
||||
|
||||
Console.WriteLine($"=== Critic (Iteration {state.Iteration}) ===\n");
|
||||
|
||||
// Use RunStreamingAsync to get streaming updates, then deserialize at the end
|
||||
IAsyncEnumerable<AgentRunResponseUpdate> updates = this._agent.RunStreamingAsync(message, cancellationToken: cancellationToken);
|
||||
|
||||
// Stream the output in real-time (for any rationale/explanation)
|
||||
await foreach (AgentRunResponseUpdate update in updates)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(update.Text))
|
||||
{
|
||||
Console.Write(update.Text);
|
||||
}
|
||||
}
|
||||
Console.WriteLine("\n");
|
||||
|
||||
// Convert the stream to a response and deserialize the structured output
|
||||
AgentRunResponse response = await updates.ToAgentRunResponseAsync(cancellationToken);
|
||||
CriticDecision decision = response.Deserialize<CriticDecision>(JsonSerializerOptions.Web);
|
||||
|
||||
Console.WriteLine($"Decision: {(decision.Approved ? "✅ APPROVED" : "❌ NEEDS REVISION")}");
|
||||
if (!string.IsNullOrEmpty(decision.Feedback))
|
||||
{
|
||||
Console.WriteLine($"Feedback: {decision.Feedback}");
|
||||
}
|
||||
Console.WriteLine();
|
||||
|
||||
// Safety: approve if max iterations reached
|
||||
if (!decision.Approved && state.Iteration >= Program.MaxIterations)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($"⚠️ Max iterations ({Program.MaxIterations}) reached - auto-approving");
|
||||
Console.ResetColor();
|
||||
decision.Approved = true;
|
||||
decision.Feedback = "";
|
||||
}
|
||||
|
||||
// Increment iteration ONLY if rejecting (will loop back to Writer)
|
||||
if (!decision.Approved)
|
||||
{
|
||||
state.Iteration++;
|
||||
}
|
||||
|
||||
// Store the decision in history
|
||||
state.History.Add(new ChatMessage(ChatRole.Assistant,
|
||||
$"[Decision: {(decision.Approved ? "Approved" : "Needs Revision")}] {decision.Feedback}"));
|
||||
await FlowStateHelpers.SaveFlowStateAsync(context, state);
|
||||
|
||||
// Populate workflow-specific fields
|
||||
decision.Content = message.Text ?? "";
|
||||
decision.Iteration = state.Iteration;
|
||||
|
||||
return decision;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executor that presents the final approved content to the user.
|
||||
/// </summary>
|
||||
internal sealed class SummaryExecutor : Executor<CriticDecision, ChatMessage>
|
||||
{
|
||||
private readonly AIAgent _agent;
|
||||
|
||||
public SummaryExecutor(IChatClient chatClient) : base("Summary")
|
||||
{
|
||||
this._agent = new ChatClientAgent(
|
||||
chatClient,
|
||||
name: "Summary",
|
||||
instructions: """
|
||||
You present the final approved content to the user.
|
||||
Simply output the polished content - no additional commentary needed.
|
||||
"""
|
||||
);
|
||||
}
|
||||
|
||||
public override async ValueTask<ChatMessage> HandleAsync(
|
||||
CriticDecision message,
|
||||
IWorkflowContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine("=== Summary ===\n");
|
||||
|
||||
string prompt = $"Present this approved content:\n\n{message.Content}";
|
||||
|
||||
StringBuilder sb = new();
|
||||
await foreach (AgentRunResponseUpdate update in this._agent.RunStreamingAsync(new ChatMessage(ChatRole.User, prompt), cancellationToken: cancellationToken))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(update.Text))
|
||||
{
|
||||
sb.Append(update.Text);
|
||||
}
|
||||
}
|
||||
|
||||
ChatMessage result = new(ChatRole.Assistant, sb.ToString());
|
||||
await context.YieldOutputAsync(result, cancellationToken);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -233,7 +233,7 @@ if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
**Note**: Advanced orchestration patterns like GroupChat, Sequential, and Concurrent orchestrations are coming soon.
|
||||
For more advanced orchestration patterns including Sequential, GroupChat, Concurrent, Magentic, and Handoff orchestrations, see the [orchestration samples](samples/getting_started/workflows/orchestration).
|
||||
|
||||
## More Examples & Samples
|
||||
|
||||
|
||||
Reference in New Issue
Block a user