// Copyright (c) Microsoft. All rights reserved. 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 WorkflowCustomAgentExecutorsSample; /// /// 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. /// /// /// Pre-requisites: /// - Foundational samples should be completed first. /// - An Azure OpenAI chat completion deployment that supports structured outputs must be configured. /// public static class Program { private static async Task Main() { // Set up the Azure OpenAI client var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini"; var chatClient = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()).GetChatClient(deploymentName).AsIChatClient(); // Create the executors var sloganWriter = new SloganWriterExecutor("SloganWriter", chatClient); var feedbackProvider = new FeedbackExecutor("FeedbackProvider", chatClient); // 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}"); } } } } /// /// A class representing the output of the slogan writer agent. /// public sealed class SloganResult { [JsonPropertyName("task")] public required string Task { get; set; } [JsonPropertyName("slogan")] public required string Slogan { get; set; } } /// /// A class representing the output of the feedback agent. /// 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; } /// /// A custom event to indicate that a slogan has been generated. /// internal sealed class SloganGeneratedEvent(SloganResult sloganResult) : WorkflowEvent(sloganResult) { public override string ToString() => $"Slogan: {sloganResult.Slogan}"; } /// /// 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. /// internal sealed partial class SloganWriterExecutor : Executor { private readonly AIAgent _agent; private AgentSession? _session; /// /// Initializes a new instance of the class. /// /// A unique identifier for the executor. /// The chat client to use for the AI agent. public SloganWriterExecutor(string id, IChatClient chatClient) : base(id) { ChatClientAgentOptions agentOptions = new() { ChatOptions = new() { Instructions = "You are a professional slogan writer. You will be given a task to create a slogan.", ResponseFormat = ChatResponseFormat.ForJsonSchema() } }; this._agent = new ChatClientAgent(chatClient, agentOptions); } [MessageHandler] public async ValueTask 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(result.Text) ?? throw new InvalidOperationException("Failed to deserialize slogan result."); await context.AddEventAsync(new SloganGeneratedEvent(sloganResult), cancellationToken); return sloganResult; } [MessageHandler] public async ValueTask 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(result.Text) ?? throw new InvalidOperationException("Failed to deserialize slogan result."); await context.AddEventAsync(new SloganGeneratedEvent(sloganResult), cancellationToken); return sloganResult; } } /// /// A custom event to indicate that feedback has been provided. /// 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)}"; } /// /// A custom executor that uses an AI agent to provide feedback on a slogan. /// [SendsMessage(typeof(FeedbackResult))] [YieldsOutput(typeof(string))] internal sealed partial class FeedbackExecutor : Executor { private readonly AIAgent _agent; private AgentSession? _session; public int MinimumRating { get; init; } = 8; public int MaxAttempts { get; init; } = 3; private int _attempts; /// /// Initializes a new instance of the class. /// /// A unique identifier for the executor. /// The chat client to use for the AI agent. public FeedbackExecutor(string id, IChatClient chatClient) : base(id) { ChatClientAgentOptions agentOptions = new() { ChatOptions = new() { Instructions = "You are a professional editor. You will be given a slogan and the task it is meant to accomplish.", ResponseFormat = ChatResponseFormat.ForJsonSchema() } }; this._agent = new ChatClientAgent(chatClient, 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(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++; } }