// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Azure.AI.OpenAI; using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Agents.Workflows; using Microsoft.Agents.Workflows.Reflection; using Microsoft.Extensions.AI; namespace WorkflowConcurrentSample; /// /// This sample introduces concurrent execution using "fan-out" and "fan-in" patterns. /// /// Unlike sequential workflows where executors run one after another, this workflow /// runs multiple executors in parallel to process the same input simultaneously. /// /// The workflow structure: /// 1. StartExecutor sends the same question to two AI agents concurrently (fan-out) /// 2. Physicist Agent and Chemist Agent answer independently and in parallel /// 3. AggregationExecutor collects both responses and combines them (fan-in) /// /// This pattern is useful when you want multiple perspectives on the same input, /// or when you can break work into independent parallel tasks for better performance. /// /// /// Pre-requisites: /// - Foundational samples should be completed first. /// - An Azure OpenAI chat completion deployment 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-4o-mini"; var chatClient = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()).GetChatClient(deploymentName).AsIChatClient(); // Create the executors ChatClientAgent physicist = new( chatClient, name: "Physicist", instructions: "You are an expert in physics. You answer questions from a physics perspective." ); ChatClientAgent chemist = new( chatClient, name: "Chemist", instructions: "You are an expert in chemistry. You answer questions from a chemistry perspective." ); var startExecutor = new ConcurrentStartExecutor(); var aggregationExecutor = new ConcurrentAggregationExecutor(); // Build the workflow by adding executors and connecting them var workflow = new WorkflowBuilder(startExecutor) .AddFanOutEdge(startExecutor, targets: [physicist, chemist]) .AddFanInEdge(aggregationExecutor, sources: [physicist, chemist]) .WithOutputFrom(aggregationExecutor) .Build(); // Execute the workflow in streaming mode StreamingRun run = await InProcessExecution.StreamAsync(workflow, "What is temperature?"); await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false)) { if (evt is WorkflowOutputEvent output) { Console.WriteLine($"Workflow completed with results:\n{output.Data}"); } } } } /// /// Executor that starts the concurrent processing by sending messages to the agents. /// internal sealed class ConcurrentStartExecutor() : ReflectingExecutor("ConcurrentStartExecutor"), IMessageHandler { /// /// Starts the concurrent processing by sending messages to the agents. /// /// The user message to process /// Workflow context for accessing workflow services and adding events /// A task representing the asynchronous operation public async ValueTask HandleAsync(string message, IWorkflowContext context) { // Broadcast the message to all connected agents. Receiving agents will queue // the message but will not start processing until they receive a turn token. await context.SendMessageAsync(new ChatMessage(ChatRole.User, message)); // Broadcast the turn token to kick off the agents. await context.SendMessageAsync(new TurnToken(emitEvents: true)); } } /// /// Executor that aggregates the results from the concurrent agents. /// internal sealed class ConcurrentAggregationExecutor() : ReflectingExecutor("ConcurrentAggregationExecutor"), IMessageHandler { private readonly List _messages = []; /// /// Handles incoming messages from the agents and aggregates their responses. /// /// The message from the agent /// Workflow context for accessing workflow services and adding events /// A task representing the asynchronous operation public async ValueTask HandleAsync(ChatMessage message, IWorkflowContext context) { this._messages.Add(message); if (this._messages.Count == 2) { var formattedMessages = string.Join(Environment.NewLine, this._messages.Select(m => $"{m.AuthorName}: {m.Text}")); await context.YieldOutputAsync(formattedMessages); } } }