// 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 WorkflowEdgeConditionSample; /// /// This sample introduces conditional routing using edge conditions to create decision-based workflows. /// /// This workflow creates an automated email response system that routes emails down different paths based /// on spam detection results: /// /// 1. Spam Detection Agent analyzes incoming emails and classifies them as spam or legitimate /// 2. Based on the classification: /// - Legitimate emails → Email Assistant Agent → Send Email Executor /// - Spam emails → Handle Spam Executor (marks as spam) /// /// Edge conditions enable workflows to make intelligent routing decisions, allowing you to /// build sophisticated automation that responds differently based on the data being processed. /// /// /// Pre-requisites: /// - Foundational samples should be completed first. /// - Shared state is used in this sample to persist email data between executors. /// - 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 agents AIAgent spamDetectionAgent = GetSpamDetectionAgent(chatClient); AIAgent emailAssistantAgent = GetEmailAssistantAgent(chatClient); // Create executors var spamDetectionExecutor = new SpamDetectionExecutor(spamDetectionAgent); var emailAssistantExecutor = new EmailAssistantExecutor(emailAssistantAgent); var sendEmailExecutor = new SendEmailExecutor(); var handleSpamExecutor = new HandleSpamExecutor(); // Build the workflow by adding executors and connecting them var workflow = new WorkflowBuilder(spamDetectionExecutor) .AddEdge(spamDetectionExecutor, emailAssistantExecutor, condition: GetCondition(expectedResult: false)) .AddEdge(emailAssistantExecutor, sendEmailExecutor) .AddEdge(spamDetectionExecutor, handleSpamExecutor, condition: GetCondition(expectedResult: true)) .WithOutputFrom(handleSpamExecutor, sendEmailExecutor) .Build(); // Read a email from a text file string email = Resources.Read("spam.txt"); // Execute the workflow await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, new ChatMessage(ChatRole.User, email)); await run.TrySendMessageAsync(new TurnToken(emitEvents: true)); await foreach (WorkflowEvent evt in run.WatchStreamAsync()) { if (evt is WorkflowOutputEvent outputEvent) { Console.WriteLine($"{outputEvent}"); } 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(); } } } /// /// Creates a condition for routing messages based on the expected spam detection result. /// /// The expected spam detection result /// A function that evaluates whether a message meets the expected result private static Func GetCondition(bool expectedResult) => detectionResult => detectionResult is DetectionResult result && result.IsSpam == expectedResult; /// /// Creates a spam detection agent. /// /// A ChatClientAgent configured for spam detection private static ChatClientAgent GetSpamDetectionAgent(IChatClient chatClient) => new(chatClient, new ChatClientAgentOptions() { ChatOptions = new() { Instructions = "You are a spam detection assistant that identifies spam emails.", ResponseFormat = ChatResponseFormat.ForJsonSchema() } }); /// /// Creates an email assistant agent. /// /// A ChatClientAgent configured for email assistance private static ChatClientAgent GetEmailAssistantAgent(IChatClient chatClient) => new(chatClient, new ChatClientAgentOptions() { ChatOptions = new() { Instructions = "You are an email assistant that helps users draft responses to emails with professionalism.", ResponseFormat = ChatResponseFormat.ForJsonSchema() } }); } /// /// Constants for shared state scopes. /// internal static class EmailStateConstants { public const string EmailStateScope = "EmailState"; } /// /// Represents the result of spam detection. /// public sealed class DetectionResult { [JsonPropertyName("is_spam")] public bool IsSpam { get; set; } [JsonPropertyName("reason")] public string Reason { get; set; } = string.Empty; // Email ID is generated by the executor not the agent [JsonIgnore] public string EmailId { get; set; } = string.Empty; } /// /// Represents an email. /// internal sealed class Email { [JsonPropertyName("email_id")] public string EmailId { get; set; } = string.Empty; [JsonPropertyName("email_content")] public string EmailContent { get; set; } = string.Empty; } /// /// Executor that detects spam using an AI agent. /// internal sealed class SpamDetectionExecutor : Executor { private readonly AIAgent _spamDetectionAgent; /// /// Creates a new instance of the class. /// /// The AI agent used for spam detection public SpamDetectionExecutor(AIAgent spamDetectionAgent) : base("SpamDetectionExecutor") { this._spamDetectionAgent = spamDetectionAgent; } public override async ValueTask HandleAsync(ChatMessage message, IWorkflowContext context, CancellationToken cancellationToken = default) { // Generate a random email ID and store the email content to the shared state var newEmail = new Email { EmailId = Guid.NewGuid().ToString("N"), EmailContent = message.Text }; await context.QueueStateUpdateAsync(newEmail.EmailId, newEmail, scopeName: EmailStateConstants.EmailStateScope, cancellationToken); // Invoke the agent var response = await this._spamDetectionAgent.RunAsync(message, cancellationToken: cancellationToken); var detectionResult = JsonSerializer.Deserialize(response.Text); detectionResult!.EmailId = newEmail.EmailId; return detectionResult; } } /// /// Represents the response from the email assistant. /// public sealed class EmailResponse { [JsonPropertyName("response")] public string Response { get; set; } = string.Empty; } /// /// Executor that assists with email responses using an AI agent. /// internal sealed class EmailAssistantExecutor : Executor { private readonly AIAgent _emailAssistantAgent; /// /// Creates a new instance of the class. /// /// The AI agent used for email assistance public EmailAssistantExecutor(AIAgent emailAssistantAgent) : base("EmailAssistantExecutor") { this._emailAssistantAgent = emailAssistantAgent; } public override async ValueTask HandleAsync(DetectionResult message, IWorkflowContext context, CancellationToken cancellationToken = default) { if (message.IsSpam) { throw new InvalidOperationException("This executor should only handle non-spam messages."); } // Retrieve the email content from the shared state var email = await context.ReadStateAsync(message.EmailId, scopeName: EmailStateConstants.EmailStateScope, cancellationToken) ?? throw new InvalidOperationException("Email not found."); // Invoke the agent var response = await this._emailAssistantAgent.RunAsync(email.EmailContent, cancellationToken: cancellationToken); var emailResponse = JsonSerializer.Deserialize(response.Text); return emailResponse!; } } /// /// Executor that sends emails. /// [YieldsOutput(typeof(string))] internal sealed class SendEmailExecutor() : Executor("SendEmailExecutor") { /// /// Simulate the sending of an email. /// public override async ValueTask HandleAsync(EmailResponse message, IWorkflowContext context, CancellationToken cancellationToken = default) => await context.YieldOutputAsync($"Email sent: {message.Response}", cancellationToken); } /// /// Executor that handles spam messages. /// [YieldsOutput(typeof(string))] internal sealed class HandleSpamExecutor() : Executor("HandleSpamExecutor") { /// /// Simulate the handling of a spam message. /// public override async ValueTask HandleAsync(DetectionResult message, IWorkflowContext context, CancellationToken cancellationToken = default) { if (message.IsSpam) { await context.YieldOutputAsync($"Email marked as spam: {message.Reason}", cancellationToken); } else { throw new InvalidOperationException("This executor should only handle spam messages."); } } }