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,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Workflows.Generators\Microsoft.Agents.AI.Workflows.Generators.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,250 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using System.Text.Json.Serialization;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
namespace WorkflowCustomAgentExecutorsSample;
/// <summary>
/// This sample demonstrates how to create custom executors for AI agents.
/// This is useful when you want more control over the agent's behaviors in a workflow.
///
/// In this example, we create two custom executors:
/// 1. SloganWriterExecutor: An AI agent that generates slogans based on a given task.
/// 2. FeedbackExecutor: An AI agent that provides feedback on the generated slogans.
/// (These two executors manage the agent instances and their conversation threads.)
///
/// The workflow alternates between these two executors until the slogan meets a certain
/// quality threshold or a maximum number of attempts is reached.
/// </summary>
/// <remarks>
/// Pre-requisites:
/// - Foundational samples should be completed first.
/// - An Azure OpenAI chat completion deployment that supports structured outputs must be configured.
/// </remarks>
public static class Program
{
private static async Task Main()
{
// Set up the Azure AI Foundry client
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
// Create the executors
var sloganWriter = new SloganWriterExecutor("SloganWriter", aiProjectClient, deploymentName);
var feedbackProvider = new FeedbackExecutor("FeedbackProvider", aiProjectClient, deploymentName);
// Build the workflow by adding executors and connecting them
var workflow = new WorkflowBuilder(sloganWriter)
.AddEdge(sloganWriter, feedbackProvider)
.AddEdge(feedbackProvider, sloganWriter)
.WithOutputFrom(feedbackProvider)
.Build();
// Execute the workflow
await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, input: "Create a slogan for a new electric SUV that is affordable and fun to drive.");
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
if (evt is SloganGeneratedEvent or FeedbackEvent)
{
// Custom events to allow us to monitor the progress of the workflow.
Console.WriteLine($"{evt}");
}
if (evt is WorkflowOutputEvent outputEvent)
{
Console.WriteLine($"{outputEvent}");
}
if (evt is WorkflowErrorEvent errorEvent)
{
Console.WriteLine($"Workflow error: {errorEvent.Exception?.Message}");
Console.WriteLine($"Details: {errorEvent.Exception}");
}
}
}
}
/// <summary>
/// A class representing the output of the slogan writer agent.
/// </summary>
public sealed class SloganResult
{
[JsonPropertyName("task")]
public required string Task { get; set; }
[JsonPropertyName("slogan")]
public required string Slogan { get; set; }
}
/// <summary>
/// A class representing the output of the feedback agent.
/// </summary>
public sealed class FeedbackResult
{
[JsonPropertyName("comments")]
public string Comments { get; set; } = string.Empty;
[JsonPropertyName("rating")]
public int Rating { get; set; }
[JsonPropertyName("actions")]
public string Actions { get; set; } = string.Empty;
}
/// <summary>
/// A custom event to indicate that a slogan has been generated.
/// </summary>
internal sealed class SloganGeneratedEvent(SloganResult sloganResult) : WorkflowEvent(sloganResult)
{
public override string ToString() => $"Slogan: {sloganResult.Slogan}";
}
/// <summary>
/// A custom executor that uses an AI agent to generate slogans based on a given task.
/// Note that this executor has two message handlers:
/// 1. HandleAsync(string message): Handles the initial task to create a slogan.
/// 2. HandleAsync(Feedback message): Handles feedback to improve the slogan.
/// </summary>
internal sealed partial class SloganWriterExecutor : Executor
{
private readonly AIAgent _agent;
private AgentSession? _session;
/// <summary>
/// Initializes a new instance of the <see cref="SloganWriterExecutor"/> class.
/// </summary>
/// <param name="id">A unique identifier for the executor.</param>
/// <param name="chatClient">The AI project client to use for the AI agent.</param>
/// <param name="model">The model deployment name.</param>
public SloganWriterExecutor(string id, AIProjectClient chatClient, string model) : base(id)
{
ChatClientAgentOptions agentOptions = new()
{
ChatOptions = new()
{
ModelId = model,
Instructions = "You are a professional slogan writer. You will be given a task to create a slogan.",
ResponseFormat = ChatResponseFormat.ForJsonSchema<SloganResult>()
}
};
this._agent = chatClient.AsAIAgent(agentOptions);
}
[MessageHandler]
public async ValueTask<SloganResult> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
this._session ??= await this._agent.CreateSessionAsync(cancellationToken);
var result = await this._agent.RunAsync(message, this._session, cancellationToken: cancellationToken);
var sloganResult = JsonSerializer.Deserialize<SloganResult>(result.Text) ?? throw new InvalidOperationException("Failed to deserialize slogan result.");
await context.AddEventAsync(new SloganGeneratedEvent(sloganResult), cancellationToken);
return sloganResult;
}
[MessageHandler]
public async ValueTask<SloganResult> HandleAsync(FeedbackResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
var feedbackMessage = $"""
Here is the feedback on your previous slogan:
Comments: {message.Comments}
Rating: {message.Rating}
Suggested Actions: {message.Actions}
Please use this feedback to improve your slogan.
""";
var result = await this._agent.RunAsync(feedbackMessage, this._session, cancellationToken: cancellationToken);
var sloganResult = JsonSerializer.Deserialize<SloganResult>(result.Text) ?? throw new InvalidOperationException("Failed to deserialize slogan result.");
await context.AddEventAsync(new SloganGeneratedEvent(sloganResult), cancellationToken);
return sloganResult;
}
}
/// <summary>
/// A custom event to indicate that feedback has been provided.
/// </summary>
internal sealed class FeedbackEvent(FeedbackResult feedbackResult) : WorkflowEvent(feedbackResult)
{
private readonly JsonSerializerOptions _options = new() { WriteIndented = true };
public override string ToString() => $"Feedback:\n{JsonSerializer.Serialize(feedbackResult, this._options)}";
}
/// <summary>
/// A custom executor that uses an AI agent to provide feedback on a slogan.
/// </summary>
[SendsMessage(typeof(FeedbackResult))]
[YieldsOutput(typeof(string))]
internal sealed partial class FeedbackExecutor : Executor<SloganResult>
{
private readonly AIAgent _agent;
private AgentSession? _session;
public int MinimumRating { get; init; } = 8;
public int MaxAttempts { get; init; } = 3;
private int _attempts;
/// <summary>
/// Initializes a new instance of the <see cref="FeedbackExecutor"/> class.
/// </summary>
/// <param name="id">A unique identifier for the executor.</param>
/// <param name="chatClient">The AI project client to use for the AI agent.</param>
/// <param name="model">The model deployment name.</param>
public FeedbackExecutor(string id, AIProjectClient chatClient, string model) : base(id)
{
ChatClientAgentOptions agentOptions = new()
{
ChatOptions = new()
{
ModelId = model,
Instructions = "You are a professional editor. You will be given a slogan and the task it is meant to accomplish.",
ResponseFormat = ChatResponseFormat.ForJsonSchema<FeedbackResult>()
}
};
this._agent = chatClient.AsAIAgent(agentOptions);
}
public override async ValueTask HandleAsync(SloganResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
this._session ??= await this._agent.CreateSessionAsync(cancellationToken);
var sloganMessage = $"""
Here is a slogan for the task '{message.Task}':
Slogan: {message.Slogan}
Please provide feedback on this slogan, including comments, a rating from 1 to 10, and suggested actions for improvement.
""";
var response = await this._agent.RunAsync(sloganMessage, this._session, cancellationToken: cancellationToken);
var feedback = JsonSerializer.Deserialize<FeedbackResult>(response.Text) ?? throw new InvalidOperationException("Failed to deserialize feedback.");
await context.AddEventAsync(new FeedbackEvent(feedback), cancellationToken);
if (feedback.Rating >= this.MinimumRating)
{
await context.YieldOutputAsync($"The following slogan was accepted:\n\n{message.Slogan}", cancellationToken);
return;
}
if (this._attempts >= this.MaxAttempts)
{
await context.YieldOutputAsync($"The slogan was rejected after {this.MaxAttempts} attempts. Final slogan:\n\n{message.Slogan}", cancellationToken);
return;
}
await context.SendMessageAsync(feedback, cancellationToken: cancellationToken);
this._attempts++;
}
}
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,103 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure.AI.Projects;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
namespace WorkflowFoundryAgentSample;
/// <summary>
/// This sample shows how to use Microsoft Foundry Agents within a workflow.
/// </summary>
/// <remarks>
/// Pre-requisites:
/// - Foundational samples should be completed first.
/// - A Microsoft Foundry project endpoint and model ID.
/// </remarks>
public static class Program
{
private static async Task Main()
{
// Set up the Azure AI Project client
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
// 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.
var aiProjectClient = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential());
// Create agents
AIAgent frenchAgent = await CreateTranslationAgentAsync("French", aiProjectClient, deploymentName);
AIAgent spanishAgent = await CreateTranslationAgentAsync("Spanish", aiProjectClient, deploymentName);
AIAgent englishAgent = await CreateTranslationAgentAsync("English", aiProjectClient, deploymentName);
try
{
// Build the workflow by adding executors and connecting them
var workflow = new WorkflowBuilder(frenchAgent)
.AddEdge(frenchAgent, spanishAgent)
.AddEdge(spanishAgent, englishAgent)
.Build();
// Execute the workflow
await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, new ChatMessage(ChatRole.User, "Hello World!"));
// Must send the turn token to trigger the agents.
// The agents are wrapped as executors. When they receive messages,
// they will cache the messages and only start processing when they receive a TurnToken.
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
if (evt is AgentResponseUpdateEvent executorComplete)
{
Console.WriteLine($"{executorComplete.ExecutorId}: {executorComplete.Data}");
}
else if (evt is WorkflowErrorEvent workflowError)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.Error.WriteLine(workflowError.Exception?.ToString() ?? "Unknown workflow error occurred.");
Console.ResetColor();
}
else if (evt is ExecutorFailedEvent executorFailed)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.Error.WriteLine($"Executor '{executorFailed.ExecutorId}' failed with {(executorFailed.Data == null ? "unknown error" : $"exception {executorFailed.Data}")}.");
Console.ResetColor();
}
}
}
finally
{
// Cleanup the agents created for the sample.
await aiProjectClient.AgentAdministrationClient.DeleteAgentAsync(frenchAgent.Name);
await aiProjectClient.AgentAdministrationClient.DeleteAgentAsync(spanishAgent.Name);
await aiProjectClient.AgentAdministrationClient.DeleteAgentAsync(englishAgent.Name);
}
}
/// <summary>
/// Creates a translation agent for the specified target language.
/// </summary>
/// <param name="targetLanguage">The target language for translation</param>
/// <param name="aiProjectClient">The <see cref="AIProjectClient"/> to create the agent with.</param>
/// <param name="model">The model to use for the agent</param>
/// <returns>A FoundryAgent configured for the specified language</returns>
private static async Task<FoundryAgent> CreateTranslationAgentAsync(
string targetLanguage,
AIProjectClient aiProjectClient,
string model)
{
ProjectsAgentVersion agentVersion = await aiProjectClient.AgentAdministrationClient.CreateAgentVersionAsync(
$"{targetLanguage}Translator",
new ProjectsAgentVersionCreationOptions(
new DeclarativeAgentDefinition(model: model)
{
Instructions = $"You are a translation assistant that translates the provided text to {targetLanguage}.",
}));
return aiProjectClient.AsAIAgent(agentVersion);
}
}
@@ -0,0 +1,47 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
namespace WorkflowGroupChatToolApprovalSample;
/// <summary>
/// Custom GroupChatManager that selects the next speaker based on the conversation flow.
/// </summary>
/// <remarks>
/// This simple selector follows a predefined flow:
/// 1. QA Engineer runs tests
/// 2. DevOps Engineer checks staging and creates rollback plan
/// 3. DevOps Engineer deploys to production (triggers approval)
/// </remarks>
internal sealed class DeploymentGroupChatManager : GroupChatManager
{
private readonly IReadOnlyList<AIAgent> _agents;
public DeploymentGroupChatManager(IReadOnlyList<AIAgent> agents)
{
this._agents = agents;
}
protected override ValueTask<AIAgent> SelectNextAgentAsync(
IReadOnlyList<ChatMessage> history,
CancellationToken cancellationToken = default)
{
if (history.Count == 0)
{
throw new InvalidOperationException("Conversation is empty; cannot select next speaker.");
}
// First speaker after initial user message
if (this.IterationCount == 0)
{
AIAgent qaAgent = this._agents.First(a => a.Name == "QAEngineer");
return new ValueTask<AIAgent>(qaAgent);
}
// Subsequent speakers are DevOps Engineer
AIAgent devopsAgent = this._agents.First(a => a.Name == "DevOpsEngineer");
return new ValueTask<AIAgent>(devopsAgent);
}
}
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,175 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to use GroupChatBuilder with tools that require human
// approval before execution. A group of specialized agents collaborate on a task, and
// sensitive tool calls trigger human-in-the-loop approval.
//
// This sample works as follows:
// 1. A GroupChatBuilder workflow is created with multiple specialized agents.
// 2. A custom manager determines which agent speaks next based on conversation state.
// 3. Agents collaborate on a software deployment task.
// 4. When the deployment agent tries to deploy to production, it triggers an approval request.
// 5. The sample simulates human approval and the workflow completes.
//
// Purpose:
// Show how tool call approvals integrate with multi-agent group chat workflows where
// different agents have different levels of tool access.
//
// Demonstrate:
// - Using custom GroupChatManager with agents that have approval-required tools.
// - Handling ToolApprovalRequestContent in group chat scenarios.
// - Multi-round group chat with tool approval interruption and resumption.
using System.ComponentModel;
using System.Text.Json;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
namespace WorkflowGroupChatToolApprovalSample;
/// <summary>
/// This sample demonstrates how to use GroupChatBuilder with tools that require human
/// approval before execution.
/// </summary>
/// <remarks>
/// Pre-requisites:
/// - An Azure OpenAI chat completion deployment must be configured.
/// </remarks>
public static class Program
{
private static async Task Main()
{
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
// 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.
// 1. Create AI client
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
// 2. Create specialized agents with their tools
ChatClientAgent qaEngineer = aiProjectClient.AsAIAgent(
model: deploymentName,
instructions: "You are a QA engineer responsible for running tests before deployment. Run the appropriate test suites and report the results clearly in your response, including pass/fail counts.",
name: "QAEngineer",
description: "QA engineer who runs tests",
tools: [AIFunctionFactory.Create(RunTests)]);
ChatClientAgent devopsEngineer = aiProjectClient.AsAIAgent(
model: deploymentName,
instructions: "You are a DevOps engineer responsible for deployments. Call CheckStagingStatus, then CreateRollbackPlan, then DeployToProduction — in that order. Do not ask for confirmation before deploying; deployment approval is handled automatically by the system. After all tools complete, summarize each step and its result in your text response.",
name: "DevOpsEngineer",
description: "DevOps engineer who handles deployments",
tools:
[
AIFunctionFactory.Create(CheckStagingStatus),
AIFunctionFactory.Create(CreateRollbackPlan),
new ApprovalRequiredAIFunction(AIFunctionFactory.Create(DeployToProduction))
]);
// 3. Create custom GroupChatManager with speaker selection logic
DeploymentGroupChatManager manager = new([qaEngineer, devopsEngineer])
{
MaximumIterationCount = 4
};
// 4. Build a group chat workflow with the custom manager
Workflow workflow = AgentWorkflowBuilder
.CreateGroupChatBuilderWith(_ => manager)
.AddParticipants(qaEngineer, devopsEngineer)
.Build();
// 5. Start the workflow
Console.WriteLine("Starting group chat workflow for software deployment...");
Console.WriteLine($"Agents: [{qaEngineer.Name}, {devopsEngineer.Name}]");
Console.WriteLine(new string('-', 60));
List<ChatMessage> messages = [new(ChatRole.User, "We need to deploy version 2.4.0 to production. Please coordinate the deployment.")];
await using StreamingRun run = await InProcessExecution.Lockstep.RunStreamingAsync(workflow, messages);
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
string? lastExecutorId = null;
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
switch (evt)
{
case RequestInfoEvent e:
{
if (e.Request.TryGetDataAs(out ToolApprovalRequestContent? approvalRequestContent))
{
Console.WriteLine();
Console.WriteLine($"[APPROVAL REQUIRED] From agent: {e.Request.PortInfo.PortId}");
Console.WriteLine($" Tool: {((FunctionCallContent)approvalRequestContent.ToolCall).Name}");
Console.WriteLine($" Arguments: {JsonSerializer.Serialize(((FunctionCallContent)approvalRequestContent.ToolCall).Arguments)}");
Console.WriteLine();
// Approve the tool call request
Console.WriteLine($"Tool: {((FunctionCallContent)approvalRequestContent.ToolCall).Name} approved");
await run.SendResponseAsync(e.Request.CreateResponse(approvalRequestContent.CreateResponse(approved: true)));
}
break;
}
case AgentResponseUpdateEvent e:
{
if (e.ExecutorId != lastExecutorId)
{
if (lastExecutorId is not null)
{
Console.WriteLine();
}
Console.WriteLine($"- {e.ExecutorId}: ");
lastExecutorId = e.ExecutorId;
}
Console.Write(e.Update.Text);
break;
}
case WorkflowErrorEvent workflowError:
Console.ForegroundColor = ConsoleColor.Red;
Console.Error.WriteLine(workflowError.Exception?.ToString() ?? "Unknown workflow error occurred.");
Console.ResetColor();
break;
case ExecutorFailedEvent executorFailed:
Console.ForegroundColor = ConsoleColor.Red;
Console.Error.WriteLine($"Executor '{executorFailed.ExecutorId}' failed with {(executorFailed.Data == null ? "unknown error" : $"exception {executorFailed.Data}")}.");
Console.ResetColor();
break;
}
}
Console.WriteLine();
Console.WriteLine(new string('-', 60));
Console.WriteLine("Deployment workflow completed successfully!");
Console.WriteLine("All agents have finished their tasks.");
}
// Tool definitions - These are called by the agents during workflow execution
[Description("Run automated tests for the application.")]
private static string RunTests([Description("Name of the test suite to run")] string testSuite)
=> $"Test suite '{testSuite}' completed: 47 passed, 0 failed, 0 skipped";
[Description("Check the current status of the staging environment.")]
private static string CheckStagingStatus()
=> "Staging environment: Healthy, Version 2.3.0 deployed, All services running";
[Description("Deploy specified components to production. Requires human approval.")]
private static string DeployToProduction(
[Description("The version to deploy")] string version,
[Description("Comma-separated list of components to deploy")] string components)
=> $"Production deployment complete: Version {version}, Components: {components}";
[Description("Create a rollback plan for the deployment.")]
private static string CreateRollbackPlan([Description("The version being deployed")] string version)
=> $"Rollback plan created for version {version}: Automated rollback to v2.2.0 if health checks fail within 5 minutes";
}
@@ -0,0 +1,70 @@
# Group Chat with Tool Approval Sample
This sample demonstrates how to use `GroupChatBuilder` with tools that require human approval before execution. A group of specialized agents collaborate on a task, and sensitive tool calls trigger human-in-the-loop approval.
## What This Sample Demonstrates
- Using a custom `GroupChatManager` with agents that have approval-required tools
- Handling `FunctionApprovalRequestContent` in group chat scenarios
- Multi-round group chat with tool approval interruption and resumption
- Integrating tool call approvals with multi-agent workflows where different agents have different levels of tool access
## How It Works
1. A `GroupChatBuilder` workflow is created with multiple specialized agents
2. A custom `DeploymentGroupChatManager` determines which agent speaks next based on conversation state
3. Agents collaborate on a software deployment task:
- **QA Engineer**: Runs automated tests
- **DevOps Engineer**: Checks staging status, creates rollback plan, and deploys to production
4. When the deployment agent tries to deploy to production, it triggers an approval request
5. The sample simulates human approval and the workflow completes
## Key Components
### Approval-Required Tools
The `DeployToProduction` function is wrapped with `ApprovalRequiredAIFunction` to require human approval:
```csharp
new ApprovalRequiredAIFunction(AIFunctionFactory.Create(DeployToProduction))
```
### Custom Group Chat Manager
The `DeploymentGroupChatManager` implements custom speaker selection logic:
- First iteration: QA Engineer runs tests
- Subsequent iterations: DevOps Engineer handles deployment tasks
### Approval Handling
The sample demonstrates continuous event-driven execution with inline approval handling:
- The workflow runs in a single event loop.
- When an approval-required tool is invoked, the loop surfaces an approval request, processes the (simulated) human response, and then continues execution without starting a separate phase.
## Prerequisites
- Azure OpenAI or OpenAI configured with the required environment variables
- `AZURE_OPENAI_ENDPOINT` environment variable set
- `AZURE_OPENAI_DEPLOYMENT_NAME` environment variable (defaults to "gpt-5.4-mini")
## Running the Sample
```bash
dotnet run
```
## Expected Output
The sample will show:
1. QA Engineer running tests
2. DevOps Engineer checking staging and creating rollback plan
3. An approval request for production deployment
4. Simulated approval response
5. DevOps Engineer completing the deployment
6. Workflow completion message
## Related Samples
- [Agent Function Tools with Approvals](../../../02-agents/Agents/Agent_Step01_UsingFunctionToolsWithApprovals) - Basic function approval pattern
- [Agent Workflow Patterns](../../_StartHere/03_AgentWorkflowPatterns) - Group chat without approvals
- [Human-in-the-Loop Basic](../../HumanInTheLoop/HumanInTheLoopBasic) - Workflow-level human interaction
@@ -0,0 +1,97 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
namespace WorkflowAsAnAgentSample;
/// <summary>
/// This sample introduces the concept of workflows as agents, where a workflow can be
/// treated as an <see cref="AIAgent"/>. This allows you to interact with a workflow
/// as if it were a single agent.
///
/// In this example, we create a workflow that uses two language agents to process
/// input concurrently, one that responds in French and another that responds in English.
///
/// You will interact with the workflow in an interactive loop, sending messages and receiving
/// streaming responses from the workflow as if it were an agent who responds in both languages.
///
/// This sample also demonstrates <see cref="IResettableExecutor"/>, which is required
/// for stateful executors that are shared across multiple workflow runs. Each iteration
/// of the interactive loop triggers a new workflow run against the same workflow instance.
/// Between runs, the framework automatically calls <see cref="IResettableExecutor.ResetAsync"/>
/// on shared executors so that accumulated state (e.g., collected messages) is cleared
/// before the next run begins. See <c>WorkflowFactory.ConcurrentAggregationExecutor</c>
/// for the implementation.
/// </summary>
/// <remarks>
/// Pre-requisites:
/// - Foundational samples should be completed first.
/// - This sample uses concurrent processing.
/// - An Azure OpenAI endpoint and deployment name.
/// </remarks>
public static class Program
{
private static async Task Main()
{
// Set up the Azure AI Foundry client
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
// Create the workflow and turn it into an agent
var workflow = WorkflowFactory.BuildWorkflow(aiProjectClient, deploymentName);
var agent = workflow.AsAIAgent("workflow-agent", "Workflow Agent");
var session = await agent.CreateSessionAsync();
// Start an interactive loop to interact with the workflow as if it were an agent.
// Each iteration runs the workflow again on the same workflow instance. Between runs,
// the framework calls IResettableExecutor.ResetAsync() on shared stateful executors
// (like ConcurrentAggregationExecutor) to clear accumulated state from the previous run.
while (true)
{
Console.WriteLine();
Console.Write("User (or 'exit' to quit): ");
string? input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input) || input.Equals("exit", StringComparison.OrdinalIgnoreCase))
{
break;
}
await ProcessInputAsync(agent, session, input);
}
// Helper method to process user input and display streaming responses. To display
// multiple interleaved responses correctly, we buffer updates by message ID and
// re-render all messages on each update.
static async Task ProcessInputAsync(AIAgent agent, AgentSession? session, string input)
{
Dictionary<string, List<AgentResponseUpdate>> buffer = [];
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(input, session))
{
if (update.MessageId is null || string.IsNullOrEmpty(update.Text))
{
// skip updates that don't have a message ID or text
continue;
}
if (!buffer.TryGetValue(update.MessageId, out List<AgentResponseUpdate>? value))
{
value = [];
buffer[update.MessageId] = value;
}
value.Add(update);
foreach (var (messageId, segments) in buffer)
{
string combinedText = string.Concat(segments);
Console.WriteLine($"{segments[0].AuthorName}: {combinedText}");
Console.WriteLine();
}
}
}
}
}
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,100 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure.AI.Projects;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
namespace WorkflowAsAnAgentSample;
internal static class WorkflowFactory
{
/// <summary>
/// Creates a workflow that uses two language agents to process input concurrently.
///
/// In this workflow, the <c>Start</c> <see cref="ChatForwardingExecutor"/> and the
/// <see cref="ConcurrentAggregationExecutor"/> are provided as shared instances, meaning
/// the same executor objects are reused across multiple workflow runs. The language agents
/// (French and English) are created via a factory and instantiated per workflow run.
/// Stateful shared executors must implement <see cref="IResettableExecutor"/> so the
/// framework can clear their state between runs. Framework-provided executors like
/// <see cref="ChatForwardingExecutor"/> already implement this interface.
/// </summary>
/// <param name="client">The AI project client to use for the agents</param>
/// <param name="model">The model deployment name</param>
/// <returns>A workflow that processes input using two language agents</returns>
internal static Workflow BuildWorkflow(AIProjectClient client, string model)
{
// Create executors
var startExecutor = new ChatForwardingExecutor("Start");
var aggregationExecutor = new ConcurrentAggregationExecutor();
AIAgent frenchAgent = GetLanguageAgent("French", client, model);
AIAgent englishAgent = GetLanguageAgent("English", client, model);
// Build the workflow by adding executors and connecting them
return new WorkflowBuilder(startExecutor)
.AddFanOutEdge(startExecutor, [frenchAgent, englishAgent])
.AddFanInBarrierEdge([frenchAgent, englishAgent], aggregationExecutor)
.WithOutputFrom(aggregationExecutor)
.Build();
}
/// <summary>
/// Creates a language agent for the specified target language.
/// </summary>
/// <param name="targetLanguage">The target language for translation</param>
/// <param name="client">The AI project client to use for the agent</param>
/// <param name="model">The model deployment name</param>
/// <returns>A ChatClientAgent configured for the specified language</returns>
private static ChatClientAgent GetLanguageAgent(string targetLanguage, AIProjectClient client, string model) =>
client.AsAIAgent(model: model, instructions: $"You're a helpful assistant who always responds in {targetLanguage}.", name: $"{targetLanguage}Agent");
/// <summary>
/// Executor that aggregates the results from the concurrent agents.
///
/// This executor is stateful — it accumulates messages in <see cref="_messages"/>
/// as they arrive from each agent. Because it is provided as a shared instance
/// (not via a factory), the same object is reused across workflow runs. Implementing
/// <see cref="IResettableExecutor"/> allows the framework to call <see cref="ResetAsync"/>
/// between runs, clearing accumulated state so each run starts fresh.
///
/// Without <see cref="IResettableExecutor"/>, attempting to reuse a workflow containing
/// shared executor instances that do not implement this interface would throw an
/// <see cref="InvalidOperationException"/>.
/// </summary>
[YieldsOutput(typeof(string))]
private sealed class ConcurrentAggregationExecutor() :
Executor<List<ChatMessage>>("ConcurrentAggregationExecutor"), IResettableExecutor
{
private readonly List<ChatMessage> _messages = [];
/// <summary>
/// Handles incoming messages from the agents and aggregates their responses.
/// </summary>
/// <param name="message">The messages from the agent</param>
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
/// The default is <see cref="CancellationToken.None"/>.</param>
public override async ValueTask HandleAsync(List<ChatMessage> message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
this._messages.AddRange(message);
if (this._messages.Count == 2)
{
var formattedMessages = string.Join(Environment.NewLine, this._messages.Select(m => $"{m.Text}"));
await context.YieldOutputAsync(formattedMessages, cancellationToken);
}
}
/// <summary>
/// Resets the executor state between workflow runs by clearing accumulated messages.
/// The framework calls this automatically when a workflow run completes, before the
/// workflow can be used for another run.
/// </summary>
public ValueTask ResetAsync()
{
this._messages.Clear();
return default;
}
}
}