chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
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) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
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
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
<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>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\Resources\*">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
<Link>Resources\%(Filename)%(Extension)</Link>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,275 @@
|
||||
// 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 WorkflowEdgeConditionSample;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </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 agents
|
||||
AIAgent spamDetectionAgent = GetSpamDetectionAgent(aiProjectClient, deploymentName);
|
||||
AIAgent emailAssistantAgent = GetEmailAssistantAgent(aiProjectClient, deploymentName);
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a condition for routing messages based on the expected spam detection result.
|
||||
/// </summary>
|
||||
/// <param name="expectedResult">The expected spam detection result</param>
|
||||
/// <returns>A function that evaluates whether a message meets the expected result</returns>
|
||||
private static Func<object?, bool> GetCondition(bool expectedResult) =>
|
||||
detectionResult => detectionResult is DetectionResult result && result.IsSpam == expectedResult;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a spam detection agent.
|
||||
/// </summary>
|
||||
/// <returns>A ChatClientAgent configured for spam detection</returns>
|
||||
private static ChatClientAgent GetSpamDetectionAgent(AIProjectClient client, string model) =>
|
||||
client.AsAIAgent(new ChatClientAgentOptions()
|
||||
{
|
||||
ChatOptions = new()
|
||||
{
|
||||
ModelId = model,
|
||||
Instructions = "You are a spam detection assistant that identifies spam emails.",
|
||||
ResponseFormat = ChatResponseFormat.ForJsonSchema<DetectionResult>()
|
||||
}
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Creates an email assistant agent.
|
||||
/// </summary>
|
||||
/// <returns>A ChatClientAgent configured for email assistance</returns>
|
||||
private static ChatClientAgent GetEmailAssistantAgent(AIProjectClient client, string model) =>
|
||||
client.AsAIAgent(new ChatClientAgentOptions()
|
||||
{
|
||||
ChatOptions = new()
|
||||
{
|
||||
ModelId = model,
|
||||
Instructions = "You are an email assistant that helps users draft responses to emails with professionalism.",
|
||||
ResponseFormat = ChatResponseFormat.ForJsonSchema<EmailResponse>()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constants for shared state scopes.
|
||||
/// </summary>
|
||||
internal static class EmailStateConstants
|
||||
{
|
||||
public const string EmailStateScope = "EmailState";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of spam detection.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an email.
|
||||
/// </summary>
|
||||
internal sealed class Email
|
||||
{
|
||||
[JsonPropertyName("email_id")]
|
||||
public string EmailId { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("email_content")]
|
||||
public string EmailContent { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executor that detects spam using an AI agent.
|
||||
/// </summary>
|
||||
internal sealed class SpamDetectionExecutor : Executor<ChatMessage, DetectionResult>
|
||||
{
|
||||
private readonly AIAgent _spamDetectionAgent;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="SpamDetectionExecutor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="spamDetectionAgent">The AI agent used for spam detection</param>
|
||||
public SpamDetectionExecutor(AIAgent spamDetectionAgent) : base("SpamDetectionExecutor")
|
||||
{
|
||||
this._spamDetectionAgent = spamDetectionAgent;
|
||||
}
|
||||
|
||||
public override async ValueTask<DetectionResult> 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<DetectionResult>(response.Text);
|
||||
|
||||
detectionResult!.EmailId = newEmail.EmailId;
|
||||
|
||||
return detectionResult;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the response from the email assistant.
|
||||
/// </summary>
|
||||
public sealed class EmailResponse
|
||||
{
|
||||
[JsonPropertyName("response")]
|
||||
public string Response { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executor that assists with email responses using an AI agent.
|
||||
/// </summary>
|
||||
internal sealed class EmailAssistantExecutor : Executor<DetectionResult, EmailResponse>
|
||||
{
|
||||
private readonly AIAgent _emailAssistantAgent;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="EmailAssistantExecutor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="emailAssistantAgent">The AI agent used for email assistance</param>
|
||||
public EmailAssistantExecutor(AIAgent emailAssistantAgent) : base("EmailAssistantExecutor")
|
||||
{
|
||||
this._emailAssistantAgent = emailAssistantAgent;
|
||||
}
|
||||
|
||||
public override async ValueTask<EmailResponse> 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<Email>(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<EmailResponse>(response.Text);
|
||||
|
||||
return emailResponse!;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executor that sends emails.
|
||||
/// </summary>
|
||||
[YieldsOutput(typeof(string))]
|
||||
internal sealed class SendEmailExecutor() : Executor<EmailResponse>("SendEmailExecutor")
|
||||
{
|
||||
/// <summary>
|
||||
/// Simulate the sending of an email.
|
||||
/// </summary>
|
||||
public override async ValueTask HandleAsync(EmailResponse message, IWorkflowContext context, CancellationToken cancellationToken = default) =>
|
||||
await context.YieldOutputAsync($"Email sent: {message.Response}", cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executor that handles spam messages.
|
||||
/// </summary>
|
||||
[YieldsOutput(typeof(string))]
|
||||
internal sealed class HandleSpamExecutor() : Executor<DetectionResult>("HandleSpamExecutor")
|
||||
{
|
||||
/// <summary>
|
||||
/// Simulate the handling of a spam message.
|
||||
/// </summary>
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace WorkflowEdgeConditionSample;
|
||||
|
||||
/// <summary>
|
||||
/// Resource helper to load resources.
|
||||
/// </summary>
|
||||
internal static class Resources
|
||||
{
|
||||
private const string ResourceFolder = "Resources";
|
||||
|
||||
public static string Read(string fileName) => File.ReadAllText(Path.Combine(AppContext.BaseDirectory, ResourceFolder, fileName));
|
||||
}
|
||||
Reference in New Issue
Block a user