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
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
+47
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user