mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET Workflows - Update structure of samples (#645)
* Updated * Typos * Update readme fwiw
This commit is contained in:
+31
@@ -0,0 +1,31 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.Workflows\Microsoft.Agents.Workflows.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Extensions.AI.Agents.AzureAI\Microsoft.Extensions.AI.Agents.AzureAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Extensions.AI.Agents\Microsoft.Extensions.AI.Agents.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\Resources\*">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
<Link>Resources\%(Filename)%(Extension)</Link>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,280 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.Workflows;
|
||||
using Microsoft.Agents.Workflows.Reflection;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
|
||||
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 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 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
|
||||
WorkflowBuilder builder = new(spamDetectionExecutor);
|
||||
builder.AddEdge(spamDetectionExecutor, emailAssistantExecutor, condition: GetCondition(expectedResult: false));
|
||||
builder.AddEdge(emailAssistantExecutor, sendEmailExecutor);
|
||||
builder.AddEdge(spamDetectionExecutor, handleSpamExecutor, condition: GetCondition(expectedResult: true));
|
||||
var workflow = builder.Build<ChatMessage>();
|
||||
|
||||
// Read a email from a text file
|
||||
string email = Resources.Read("spam.txt");
|
||||
|
||||
// Execute the workflow
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflow, new ChatMessage(ChatRole.User, email));
|
||||
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (evt is WorkflowCompletedEvent completedEvent)
|
||||
{
|
||||
Console.WriteLine($"{completedEvent}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
return detectionResult =>
|
||||
{
|
||||
return 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(IChatClient chatClient)
|
||||
{
|
||||
string instructions = "You are a spam detection assistant that identifies spam emails.";
|
||||
var agentOptions = new ChatClientAgentOptions(instructions: instructions)
|
||||
{
|
||||
ChatOptions = new()
|
||||
{
|
||||
ResponseFormat = ChatResponseFormatJson.ForJsonSchema(
|
||||
schema: AIJsonUtilities.CreateJsonSchema(typeof(DetectionResult))
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
return new ChatClientAgent(chatClient, agentOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an email assistant agent.
|
||||
/// </summary>
|
||||
/// <returns>A ChatClientAgent configured for email assistance</returns>
|
||||
private static ChatClientAgent GetEmailAssistantAgent(IChatClient chatClient)
|
||||
{
|
||||
string instructions = "You are an email assistant that helps users draft responses to emails with professionalism.";
|
||||
var agentOptions = new ChatClientAgentOptions(instructions: instructions)
|
||||
{
|
||||
ChatOptions = new()
|
||||
{
|
||||
ResponseFormat = ChatResponseFormatJson.ForJsonSchema(
|
||||
schema: AIJsonUtilities.CreateJsonSchema(typeof(EmailResponse))
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
return new ChatClientAgent(chatClient, agentOptions);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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 : ReflectingExecutor<SpamDetectionExecutor>, IMessageHandler<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 async ValueTask<DetectionResult> HandleAsync(ChatMessage message, IWorkflowContext context)
|
||||
{
|
||||
// Generate a random email ID and store the email content to the shared state
|
||||
var newEmail = new Email
|
||||
{
|
||||
EmailId = Guid.NewGuid().ToString(),
|
||||
EmailContent = message.Text
|
||||
};
|
||||
await context.QueueStateUpdateAsync<Email>(newEmail.EmailId, newEmail, scopeName: EmailStateConstants.EmailStateScope);
|
||||
|
||||
// Invoke the agent
|
||||
var response = await this._spamDetectionAgent.RunAsync(message);
|
||||
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 : ReflectingExecutor<EmailAssistantExecutor>, IMessageHandler<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 async ValueTask<EmailResponse> HandleAsync(DetectionResult message, IWorkflowContext context)
|
||||
{
|
||||
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)
|
||||
?? throw new InvalidOperationException("Email not found.");
|
||||
|
||||
// Invoke the agent
|
||||
var response = await this._emailAssistantAgent.RunAsync(email.EmailContent);
|
||||
var emailResponse = JsonSerializer.Deserialize<EmailResponse>(response.Text);
|
||||
|
||||
return emailResponse!;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executor that sends emails.
|
||||
/// </summary>
|
||||
internal sealed class SendEmailExecutor() : ReflectingExecutor<SendEmailExecutor>("SendEmailExecutor"), IMessageHandler<EmailResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// Simulate the sending of an email.
|
||||
/// </summary>
|
||||
public async ValueTask HandleAsync(EmailResponse message, IWorkflowContext context)
|
||||
{
|
||||
await context.AddEventAsync(new WorkflowCompletedEvent($"Email sent: {message.Response}"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executor that handles spam messages.
|
||||
/// </summary>
|
||||
internal sealed class HandleSpamExecutor() : ReflectingExecutor<HandleSpamExecutor>("HandleSpamExecutor"), IMessageHandler<DetectionResult>
|
||||
{
|
||||
/// <summary>
|
||||
/// Simulate the handling of a spam message.
|
||||
/// </summary>
|
||||
public async ValueTask HandleAsync(DetectionResult message, IWorkflowContext context)
|
||||
{
|
||||
if (message.IsSpam)
|
||||
{
|
||||
await context.AddEventAsync(new WorkflowCompletedEvent($"Email marked as spam: {message.Reason}"));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("This executor should only handle spam messages.");
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.IO;
|
||||
|
||||
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($"{ResourceFolder}/{fileName}");
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.Workflows\Microsoft.Agents.Workflows.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Extensions.AI.Agents.AzureAI\Microsoft.Extensions.AI.Agents.AzureAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Extensions.AI.Agents\Microsoft.Extensions.AI.Agents.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\Resources\*">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
<Link>Resources\%(Filename)%(Extension)</Link>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,326 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.Workflows;
|
||||
using Microsoft.Agents.Workflows.Reflection;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
|
||||
namespace WorkflowSwitchCaseSample;
|
||||
|
||||
/// <summary>
|
||||
/// This sample introduces conditional routing using switch-case logic for complex decision trees.
|
||||
///
|
||||
/// Building on the previous email automation examples, this workflow adds a third decision path
|
||||
/// to handle ambiguous cases where spam detection is uncertain. Now the workflow can route emails
|
||||
/// three ways based on the detection result:
|
||||
///
|
||||
/// 1. Not Spam → Email Assistant → Send Email
|
||||
/// 2. Spam → Handle Spam Executor
|
||||
/// 3. Uncertain → Handle Uncertain Executor (default case)
|
||||
///
|
||||
/// The switch-case pattern provides cleaner syntax than multiple individual edge conditions,
|
||||
/// especially when dealing with multiple possible outcomes. This approach scales well for
|
||||
/// workflows that need to handle many different scenarios.
|
||||
/// </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 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 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();
|
||||
var handleUncertainExecutor = new HandleUncertainExecutor();
|
||||
|
||||
// Build the workflow by adding executors and connecting them
|
||||
WorkflowBuilder builder = new(spamDetectionExecutor);
|
||||
builder.AddSwitch(spamDetectionExecutor, switchBuilder =>
|
||||
switchBuilder
|
||||
.AddCase(
|
||||
GetCondition(expectedDecision: SpamDecision.NotSpam),
|
||||
emailAssistantExecutor
|
||||
)
|
||||
.AddCase(
|
||||
GetCondition(expectedDecision: SpamDecision.Spam),
|
||||
handleSpamExecutor
|
||||
)
|
||||
.WithDefault(
|
||||
handleUncertainExecutor
|
||||
)
|
||||
)
|
||||
// After the email assistant writes a response, it will be sent to the send email executor
|
||||
.AddEdge(emailAssistantExecutor, sendEmailExecutor);
|
||||
var workflow = builder.Build<ChatMessage>();
|
||||
|
||||
// Read a email from a text file
|
||||
string email = Resources.Read("ambiguous_email.txt");
|
||||
|
||||
// Execute the workflow
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflow, new ChatMessage(ChatRole.User, email));
|
||||
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (evt is WorkflowCompletedEvent completedEvent)
|
||||
{
|
||||
Console.WriteLine($"{completedEvent}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a condition for routing messages based on the expected spam detection result.
|
||||
/// </summary>
|
||||
/// <param name="expectedDecision">The expected spam detection decision</param>
|
||||
/// <returns>A function that evaluates whether a message meets the expected result</returns>
|
||||
private static Func<object?, bool> GetCondition(SpamDecision expectedDecision)
|
||||
{
|
||||
return detectionResult =>
|
||||
{
|
||||
return detectionResult is DetectionResult result && result.spamDecision == expectedDecision;
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a spam detection agent.
|
||||
/// </summary>
|
||||
/// <returns>A ChatClientAgent configured for spam detection</returns>
|
||||
private static ChatClientAgent GetSpamDetectionAgent(IChatClient chatClient)
|
||||
{
|
||||
string instructions = "You are a spam detection assistant that identifies spam emails. Be less confident in your assessments.";
|
||||
var agentOptions = new ChatClientAgentOptions(instructions: instructions)
|
||||
{
|
||||
ChatOptions = new()
|
||||
{
|
||||
ResponseFormat = ChatResponseFormatJson.ForJsonSchema(
|
||||
schema: AIJsonUtilities.CreateJsonSchema(typeof(DetectionResult))
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
return new ChatClientAgent(chatClient, agentOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an email assistant agent.
|
||||
/// </summary>
|
||||
/// <returns>A ChatClientAgent configured for email assistance</returns>
|
||||
private static ChatClientAgent GetEmailAssistantAgent(IChatClient chatClient)
|
||||
{
|
||||
string instructions = "You are an email assistant that helps users draft responses to emails with professionalism.";
|
||||
var agentOptions = new ChatClientAgentOptions(instructions: instructions)
|
||||
{
|
||||
ChatOptions = new()
|
||||
{
|
||||
ResponseFormat = ChatResponseFormatJson.ForJsonSchema(
|
||||
schema: AIJsonUtilities.CreateJsonSchema(typeof(EmailResponse))
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
return new ChatClientAgent(chatClient, agentOptions);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constants for shared email state.
|
||||
/// </summary>
|
||||
internal static class EmailStateConstants
|
||||
{
|
||||
public const string EmailStateScope = "EmailState";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the possible decisions for spam detection.
|
||||
/// </summary>
|
||||
public enum SpamDecision
|
||||
{
|
||||
NotSpam,
|
||||
Spam,
|
||||
Uncertain
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of spam detection.
|
||||
/// </summary>
|
||||
public sealed class DetectionResult
|
||||
{
|
||||
[JsonPropertyName("spam_decision")]
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public SpamDecision spamDecision { get; set; }
|
||||
|
||||
[JsonPropertyName("reason")]
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
|
||||
[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 : ReflectingExecutor<SpamDetectionExecutor>, IMessageHandler<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 async ValueTask<DetectionResult> HandleAsync(ChatMessage message, IWorkflowContext context)
|
||||
{
|
||||
// Generate a random email ID and store the email content
|
||||
var newEmail = new Email
|
||||
{
|
||||
EmailId = Guid.NewGuid().ToString(),
|
||||
EmailContent = message.Text
|
||||
};
|
||||
await context.QueueStateUpdateAsync<Email>(newEmail.EmailId, newEmail, scopeName: EmailStateConstants.EmailStateScope);
|
||||
|
||||
// Invoke the agent
|
||||
var response = await this._spamDetectionAgent.RunAsync(message);
|
||||
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 : ReflectingExecutor<EmailAssistantExecutor>, IMessageHandler<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 async ValueTask<EmailResponse> HandleAsync(DetectionResult message, IWorkflowContext context)
|
||||
{
|
||||
if (message.spamDecision == SpamDecision.Spam)
|
||||
{
|
||||
throw new InvalidOperationException("This executor should only handle non-spam messages.");
|
||||
}
|
||||
|
||||
// Retrieve the email content from the context
|
||||
var email = await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope);
|
||||
|
||||
// Invoke the agent
|
||||
var response = await this._emailAssistantAgent.RunAsync(email!.EmailContent);
|
||||
var emailResponse = JsonSerializer.Deserialize<EmailResponse>(response.Text);
|
||||
|
||||
return emailResponse!;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executor that sends emails.
|
||||
/// </summary>
|
||||
internal sealed class SendEmailExecutor() : ReflectingExecutor<SendEmailExecutor>("SendEmailExecutor"), IMessageHandler<EmailResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// Simulate the sending of an email.
|
||||
/// </summary>
|
||||
public async ValueTask HandleAsync(EmailResponse message, IWorkflowContext context)
|
||||
{
|
||||
await context.AddEventAsync(new WorkflowCompletedEvent($"Email sent: {message.Response}"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executor that handles spam messages.
|
||||
/// </summary>
|
||||
internal sealed class HandleSpamExecutor() : ReflectingExecutor<HandleSpamExecutor>("HandleSpamExecutor"), IMessageHandler<DetectionResult>
|
||||
{
|
||||
/// <summary>
|
||||
/// Simulate the handling of a spam message.
|
||||
/// </summary>
|
||||
public async ValueTask HandleAsync(DetectionResult message, IWorkflowContext context)
|
||||
{
|
||||
if (message.spamDecision == SpamDecision.Spam)
|
||||
{
|
||||
await context.AddEventAsync(new WorkflowCompletedEvent($"Email marked as spam: {message.Reason}"));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("This executor should only handle spam messages.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executor that handles uncertain emails.
|
||||
/// </summary>
|
||||
internal sealed class HandleUncertainExecutor() : ReflectingExecutor<HandleUncertainExecutor>("HandleUncertainExecutor"), IMessageHandler<DetectionResult>
|
||||
{
|
||||
/// <summary>
|
||||
/// Simulate the handling of an uncertain spam decision.
|
||||
/// </summary>
|
||||
public async ValueTask HandleAsync(DetectionResult message, IWorkflowContext context)
|
||||
{
|
||||
if (message.spamDecision == SpamDecision.Uncertain)
|
||||
{
|
||||
var email = await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope);
|
||||
await context.AddEventAsync(new WorkflowCompletedEvent($"Email marked as uncertain: {message.Reason}. Email content: {email?.EmailContent}"));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("This executor should only handle uncertain spam decisions.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace WorkflowSwitchCaseSample;
|
||||
|
||||
/// <summary>
|
||||
/// Resource helper to load resources.
|
||||
/// </summary>
|
||||
internal static class Resources
|
||||
{
|
||||
private const string ResourceFolder = "Resources";
|
||||
|
||||
public static string Read(string fileName) => File.ReadAllText($"{ResourceFolder}/{fileName}");
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.Workflows\Microsoft.Agents.Workflows.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Extensions.AI.Agents.AzureAI\Microsoft.Extensions.AI.Agents.AzureAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Extensions.AI.Agents\Microsoft.Extensions.AI.Agents.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\Resources\*">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
<Link>Resources\%(Filename)%(Extension)</Link>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+450
@@ -0,0 +1,450 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.Workflows;
|
||||
using Microsoft.Agents.Workflows.Reflection;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
|
||||
namespace WorkflowMultiSelectionSample;
|
||||
|
||||
/// <summary>
|
||||
/// This sample introduces multi-selection routing where one executor can trigger multiple downstream executors.
|
||||
///
|
||||
/// Extending the switch-case pattern from the previous sample, the workflow can now
|
||||
/// trigger multiple executors simultaneously when certain conditions are met.
|
||||
///
|
||||
/// Key features:
|
||||
/// - For legitimate emails: triggers Email Assistant (always) + Email Summary (if email is long)
|
||||
/// - For spam emails: triggers Handle Spam executor only
|
||||
/// - For uncertain emails: triggers Handle Uncertain executor only
|
||||
/// - Database logging happens for both short emails and summarized long emails
|
||||
///
|
||||
/// This pattern is powerful for workflows that need parallel processing based on data characteristics,
|
||||
/// such as triggering different analytics pipelines or multiple notification systems.
|
||||
/// </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 const int LongEmailThreshold = 100;
|
||||
|
||||
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 agents
|
||||
AIAgent emailAnalysisAgent = GetEmailAnalysisAgent(chatClient);
|
||||
AIAgent emailAssistantAgent = GetEmailAssistantAgent(chatClient);
|
||||
AIAgent emailSummaryAgent = GetEmailSummaryAgent(chatClient);
|
||||
|
||||
// Create executors
|
||||
var emailAnalysisExecutor = new EmailAnalysisExecutor(emailAnalysisAgent);
|
||||
var emailAssistantExecutor = new EmailAssistantExecutor(emailAssistantAgent);
|
||||
var emailSummaryExecutor = new EmailSummaryExecutor(emailSummaryAgent);
|
||||
var sendEmailExecutor = new SendEmailExecutor();
|
||||
var handleSpamExecutor = new HandleSpamExecutor();
|
||||
var handleUncertainExecutor = new HandleUncertainExecutor();
|
||||
var databaseAccessExecutor = new DatabaseAccessExecutor();
|
||||
|
||||
// Build the workflow by adding executors and connecting them
|
||||
WorkflowBuilder builder = new(emailAnalysisExecutor);
|
||||
builder.AddFanOutEdge(
|
||||
emailAnalysisExecutor,
|
||||
targets: [
|
||||
handleSpamExecutor,
|
||||
emailAssistantExecutor,
|
||||
emailSummaryExecutor,
|
||||
handleUncertainExecutor,
|
||||
],
|
||||
partitioner: GetPartitioner()
|
||||
)
|
||||
// After the email assistant writes a response, it will be sent to the send email executor
|
||||
.AddEdge(emailAssistantExecutor, sendEmailExecutor)
|
||||
// Save the analysis result to the database if summary is not needed
|
||||
.AddEdge(
|
||||
emailAnalysisExecutor,
|
||||
databaseAccessExecutor,
|
||||
condition: analysisResult => analysisResult is AnalysisResult result && result.EmailLength <= LongEmailThreshold)
|
||||
// Save the analysis result to the database with summary
|
||||
.AddEdge(emailSummaryExecutor, databaseAccessExecutor);
|
||||
var workflow = builder.Build<ChatMessage>();
|
||||
|
||||
// Read a email from a text file
|
||||
string email = Resources.Read("email.txt");
|
||||
|
||||
// Execute the workflow
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflow, new ChatMessage(ChatRole.User, email));
|
||||
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (evt is WorkflowCompletedEvent completedEvent)
|
||||
{
|
||||
Console.WriteLine($"{completedEvent}");
|
||||
}
|
||||
|
||||
if (evt is DatabaseEvent databaseEvent)
|
||||
{
|
||||
Console.WriteLine($"{databaseEvent}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a partitioner for routing messages based on the analysis result.
|
||||
/// </summary>
|
||||
/// <returns>A function that takes an analysis result and returns the target partitions.</returns>
|
||||
private static Func<object?, int, IEnumerable<int>> GetPartitioner()
|
||||
{
|
||||
return (analysisResult, targetCount) =>
|
||||
{
|
||||
if (analysisResult is AnalysisResult result)
|
||||
{
|
||||
if (result.spamDecision == SpamDecision.Spam)
|
||||
{
|
||||
return [0]; // Route to spam handler
|
||||
}
|
||||
else if (result.spamDecision == SpamDecision.NotSpam)
|
||||
{
|
||||
List<int> targets = [1]; // Route to the email assistant
|
||||
|
||||
if (result.EmailLength > LongEmailThreshold)
|
||||
{
|
||||
targets.Add(2); // Route to the email summarizer too
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
else
|
||||
{
|
||||
return [3];
|
||||
}
|
||||
}
|
||||
throw new InvalidOperationException("Invalid analysis result.");
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an email analysis agent.
|
||||
/// </summary>
|
||||
/// <returns>A ChatClientAgent configured for email analysis</returns>
|
||||
private static ChatClientAgent GetEmailAnalysisAgent(IChatClient chatClient)
|
||||
{
|
||||
string instructions = "You are a spam detection assistant that identifies spam emails.";
|
||||
var agentOptions = new ChatClientAgentOptions(instructions: instructions)
|
||||
{
|
||||
ChatOptions = new()
|
||||
{
|
||||
ResponseFormat = ChatResponseFormatJson.ForJsonSchema(
|
||||
schema: AIJsonUtilities.CreateJsonSchema(typeof(AnalysisResult))
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
return new ChatClientAgent(chatClient, agentOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an email assistant agent.
|
||||
/// </summary>
|
||||
/// <returns>A ChatClientAgent configured for email assistance</returns>
|
||||
private static ChatClientAgent GetEmailAssistantAgent(IChatClient chatClient)
|
||||
{
|
||||
string instructions = "You are an email assistant that helps users draft responses to emails with professionalism.";
|
||||
var agentOptions = new ChatClientAgentOptions(instructions: instructions)
|
||||
{
|
||||
ChatOptions = new()
|
||||
{
|
||||
ResponseFormat = ChatResponseFormatJson.ForJsonSchema(
|
||||
schema: AIJsonUtilities.CreateJsonSchema(typeof(EmailResponse))
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
return new ChatClientAgent(chatClient, agentOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an agent that summarizes emails.
|
||||
/// </summary>
|
||||
/// <returns>A ChatClientAgent configured for email summarization</returns>
|
||||
private static ChatClientAgent GetEmailSummaryAgent(IChatClient chatClient)
|
||||
{
|
||||
string instructions = "You are an assistant that helps users summarize emails.";
|
||||
var agentOptions = new ChatClientAgentOptions(instructions: instructions)
|
||||
{
|
||||
ChatOptions = new()
|
||||
{
|
||||
ResponseFormat = ChatResponseFormatJson.ForJsonSchema(
|
||||
schema: AIJsonUtilities.CreateJsonSchema(typeof(EmailSummary))
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
return new ChatClientAgent(chatClient, agentOptions);
|
||||
}
|
||||
}
|
||||
|
||||
internal static class EmailStateConstants
|
||||
{
|
||||
public const string EmailStateScope = "EmailState";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the possible decisions for spam detection.
|
||||
/// </summary>
|
||||
public enum SpamDecision
|
||||
{
|
||||
NotSpam,
|
||||
Spam,
|
||||
Uncertain
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of email analysis.
|
||||
/// </summary>
|
||||
public sealed class AnalysisResult
|
||||
{
|
||||
[JsonPropertyName("spam_decision")]
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public SpamDecision spamDecision { get; set; }
|
||||
|
||||
[JsonPropertyName("reason")]
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
|
||||
[JsonIgnore]
|
||||
public int EmailLength { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string EmailSummary { get; set; } = string.Empty;
|
||||
|
||||
[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 analyzes emails using an AI agent.
|
||||
/// </summary>
|
||||
internal sealed class EmailAnalysisExecutor : ReflectingExecutor<EmailAnalysisExecutor>, IMessageHandler<ChatMessage, AnalysisResult>
|
||||
{
|
||||
private readonly AIAgent _emailAnalysisAgent;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="EmailAnalysisExecutor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="emailAnalysisAgent">The AI agent used for email analysis</param>
|
||||
public EmailAnalysisExecutor(AIAgent emailAnalysisAgent) : base("EmailAnalysisExecutor")
|
||||
{
|
||||
this._emailAnalysisAgent = emailAnalysisAgent;
|
||||
}
|
||||
|
||||
public async ValueTask<AnalysisResult> HandleAsync(ChatMessage message, IWorkflowContext context)
|
||||
{
|
||||
// Generate a random email ID and store the email content
|
||||
var newEmail = new Email
|
||||
{
|
||||
EmailId = Guid.NewGuid().ToString(),
|
||||
EmailContent = message.Text
|
||||
};
|
||||
await context.QueueStateUpdateAsync<Email>(newEmail.EmailId, newEmail, scopeName: EmailStateConstants.EmailStateScope);
|
||||
|
||||
// Invoke the agent
|
||||
var response = await this._emailAnalysisAgent.RunAsync(message);
|
||||
var AnalysisResult = JsonSerializer.Deserialize<AnalysisResult>(response.Text);
|
||||
|
||||
AnalysisResult!.EmailId = newEmail.EmailId;
|
||||
AnalysisResult!.EmailLength = newEmail.EmailContent.Length;
|
||||
|
||||
return AnalysisResult;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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 : ReflectingExecutor<EmailAssistantExecutor>, IMessageHandler<AnalysisResult, 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 async ValueTask<EmailResponse> HandleAsync(AnalysisResult message, IWorkflowContext context)
|
||||
{
|
||||
if (message.spamDecision == SpamDecision.Spam)
|
||||
{
|
||||
throw new InvalidOperationException("This executor should only handle non-spam messages.");
|
||||
}
|
||||
|
||||
// Retrieve the email content from the context
|
||||
var email = await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope);
|
||||
|
||||
// Invoke the agent
|
||||
var response = await this._emailAssistantAgent.RunAsync(email!.EmailContent);
|
||||
var emailResponse = JsonSerializer.Deserialize<EmailResponse>(response.Text);
|
||||
|
||||
return emailResponse!;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executor that sends emails.
|
||||
/// </summary>
|
||||
internal sealed class SendEmailExecutor() : ReflectingExecutor<SendEmailExecutor>("SendEmailExecutor"), IMessageHandler<EmailResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// Simulate the sending of an email.
|
||||
/// </summary>
|
||||
public async ValueTask HandleAsync(EmailResponse message, IWorkflowContext context)
|
||||
{
|
||||
await context.AddEventAsync(new WorkflowCompletedEvent($"Email sent: {message.Response}"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executor that handles spam messages.
|
||||
/// </summary>
|
||||
internal sealed class HandleSpamExecutor() : ReflectingExecutor<HandleSpamExecutor>("HandleSpamExecutor"), IMessageHandler<AnalysisResult>
|
||||
{
|
||||
/// <summary>
|
||||
/// Simulate the handling of a spam message.
|
||||
/// </summary>
|
||||
public async ValueTask HandleAsync(AnalysisResult message, IWorkflowContext context)
|
||||
{
|
||||
if (message.spamDecision == SpamDecision.Spam)
|
||||
{
|
||||
await context.AddEventAsync(new WorkflowCompletedEvent($"Email marked as spam: {message.Reason}"));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("This executor should only handle spam messages.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executor that handles uncertain messages.
|
||||
/// </summary>
|
||||
internal sealed class HandleUncertainExecutor() : ReflectingExecutor<HandleUncertainExecutor>("HandleUncertainExecutor"), IMessageHandler<AnalysisResult>
|
||||
{
|
||||
/// <summary>
|
||||
/// Simulate the handling of an uncertain spam decision.
|
||||
/// </summary>
|
||||
public async ValueTask HandleAsync(AnalysisResult message, IWorkflowContext context)
|
||||
{
|
||||
if (message.spamDecision == SpamDecision.Uncertain)
|
||||
{
|
||||
var email = await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope);
|
||||
await context.AddEventAsync(new WorkflowCompletedEvent($"Email marked as uncertain: {message.Reason}. Email content: {email?.EmailContent}"));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("This executor should only handle uncertain spam decisions.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the response from the email summary agent.
|
||||
/// </summary>
|
||||
public sealed class EmailSummary
|
||||
{
|
||||
[JsonPropertyName("summary")]
|
||||
public string Summary { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executor that summarizes emails using an AI agent.
|
||||
/// </summary>
|
||||
internal sealed class EmailSummaryExecutor : ReflectingExecutor<EmailSummaryExecutor>, IMessageHandler<AnalysisResult, AnalysisResult>
|
||||
{
|
||||
private readonly AIAgent _emailSummaryAgent;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="EmailSummaryExecutor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="emailSummaryAgent">The AI agent used for email summarization</param>
|
||||
public EmailSummaryExecutor(AIAgent emailSummaryAgent) : base("EmailSummaryExecutor")
|
||||
{
|
||||
this._emailSummaryAgent = emailSummaryAgent;
|
||||
}
|
||||
|
||||
public async ValueTask<AnalysisResult> HandleAsync(AnalysisResult message, IWorkflowContext context)
|
||||
{
|
||||
// Read the email content from the shared states
|
||||
var email = await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope);
|
||||
|
||||
// Invoke the agent
|
||||
var response = await this._emailSummaryAgent.RunAsync(email!.EmailContent);
|
||||
var emailSummary = JsonSerializer.Deserialize<EmailSummary>(response.Text);
|
||||
message.EmailSummary = emailSummary!.Summary;
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A custom workflow event for database operations.
|
||||
/// </summary>
|
||||
/// <param name="message">The message associated with the event</param>
|
||||
internal sealed class DatabaseEvent(string message) : WorkflowEvent(message) { }
|
||||
|
||||
/// <summary>
|
||||
/// Executor that handles database access.
|
||||
/// </summary>
|
||||
internal sealed class DatabaseAccessExecutor() : ReflectingExecutor<DatabaseAccessExecutor>("DatabaseAccessExecutor"), IMessageHandler<AnalysisResult>
|
||||
{
|
||||
public async ValueTask HandleAsync(AnalysisResult message, IWorkflowContext context)
|
||||
{
|
||||
// 1. Save the email content
|
||||
var email = await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope);
|
||||
await Task.Delay(100); // Simulate database access delay
|
||||
|
||||
// 2. Save the analysis result
|
||||
await Task.Delay(100); // Simulate database access delay
|
||||
|
||||
// Not using the `WorkflowCompletedEvent` because this is not the end of the workflow.
|
||||
// The end of the workflow is signaled by the `SendEmailExecutor` or the `HandleUnknownExecutor`.
|
||||
await context.AddEventAsync(new DatabaseEvent($"Email {message.EmailId} saved to database."));
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace WorkflowMultiSelectionSample;
|
||||
|
||||
/// <summary>
|
||||
/// Resource helper to load resources.
|
||||
/// </summary>
|
||||
internal static class Resources
|
||||
{
|
||||
private const string ResourceFolder = "Resources";
|
||||
|
||||
public static string Read(string fileName) => File.ReadAllText($"{ResourceFolder}/{fileName}");
|
||||
}
|
||||
Reference in New Issue
Block a user