mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
39e071c430
* feat: Make Executor id field mandatory When checkpointing is involved, it is critical to keep executor ids consistent between runs, even when recreating a new object tree for the workflow. The default id-setting mechanism generated a guid for part of the id, making it not work when restoring from a checkpoint. This change prevents this situation from arising. * feat: Enable running untyped Workflows With the change to enable delay-instantiation of executors and support for async Executor factory methods, we must instantiate the starting executor to know what are the valid input types for the workflow. To avoid forcing instantiation every time, and to better support workflows with multiple input types, we enable support for build and interacting with the base Workflow type without type annotations, and remove the requirement to know a valid input type when initiating a run. * feat: Support Output from any executor and multiple outputs.
124 lines
4.7 KiB
C#
124 lines
4.7 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Agents.Workflows;
|
|
using Microsoft.Agents.Workflows.Reflection;
|
|
|
|
namespace WorkflowSharedStatesSample;
|
|
|
|
/// <summary>
|
|
/// This sample introduces the concept of shared states within a workflow.
|
|
/// It demonstrates how multiple executors can read from and write to shared states,
|
|
/// allowing for more complex data sharing and coordination between tasks.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Pre-requisites:
|
|
/// - Foundational samples should be completed first.
|
|
/// - This sample also uses the fan-out and fan-in patterns to achieve parallel processing.
|
|
/// </remarks>
|
|
public static class Program
|
|
{
|
|
private static async Task Main()
|
|
{
|
|
// Create the executors
|
|
var fileRead = new FileReadExecutor();
|
|
var wordCount = new WordCountingExecutor();
|
|
var paragraphCount = new ParagraphCountingExecutor();
|
|
var aggregate = new AggregationExecutor();
|
|
|
|
// Build the workflow by connecting executors sequentially
|
|
var workflow = new WorkflowBuilder(fileRead)
|
|
.AddFanOutEdge(fileRead, targets: [wordCount, paragraphCount])
|
|
.AddFanInEdge(aggregate, sources: [wordCount, paragraphCount])
|
|
.WithOutputFrom(aggregate)
|
|
.Build();
|
|
|
|
// Execute the workflow with input data
|
|
Run run = await InProcessExecution.RunAsync(workflow, "Lorem_Ipsum.txt");
|
|
foreach (WorkflowEvent evt in run.NewEvents)
|
|
{
|
|
if (evt is WorkflowOutputEvent outputEvent)
|
|
{
|
|
Console.WriteLine(outputEvent.Data);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constants for shared state scopes.
|
|
/// </summary>
|
|
internal static class FileContentStateConstants
|
|
{
|
|
public const string FileContentStateScope = "FileContentState";
|
|
}
|
|
|
|
internal sealed class FileReadExecutor() : ReflectingExecutor<FileReadExecutor>("FileReadExecutor"), IMessageHandler<string, string>
|
|
{
|
|
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context)
|
|
{
|
|
// Read file content from embedded resource
|
|
string fileContent = Resources.Read(message);
|
|
// Store file content in a shared state for access by other executors
|
|
string fileID = Guid.NewGuid().ToString("N");
|
|
await context.QueueStateUpdateAsync(fileID, fileContent, scopeName: FileContentStateConstants.FileContentStateScope);
|
|
|
|
return fileID;
|
|
}
|
|
}
|
|
|
|
internal sealed class FileStats
|
|
{
|
|
public int ParagraphCount { get; set; }
|
|
public int WordCount { get; set; }
|
|
}
|
|
|
|
internal sealed class WordCountingExecutor() : ReflectingExecutor<WordCountingExecutor>("WordCountingExecutor"), IMessageHandler<string, FileStats>
|
|
{
|
|
public async ValueTask<FileStats> HandleAsync(string message, IWorkflowContext context)
|
|
{
|
|
// Retrieve the file content from the shared state
|
|
var fileContent = await context.ReadStateAsync<string>(message, scopeName: FileContentStateConstants.FileContentStateScope)
|
|
?? throw new InvalidOperationException("File content state not found");
|
|
|
|
int wordCount = fileContent.Split([' ', '\n', '\r'], StringSplitOptions.RemoveEmptyEntries).Length;
|
|
|
|
return new FileStats { WordCount = wordCount };
|
|
}
|
|
}
|
|
|
|
internal sealed class ParagraphCountingExecutor() : ReflectingExecutor<ParagraphCountingExecutor>("ParagraphCountingExecutor"), IMessageHandler<string, FileStats>
|
|
{
|
|
public async ValueTask<FileStats> HandleAsync(string message, IWorkflowContext context)
|
|
{
|
|
// Retrieve the file content from the shared state
|
|
var fileContent = await context.ReadStateAsync<string>(message, scopeName: FileContentStateConstants.FileContentStateScope)
|
|
?? throw new InvalidOperationException("File content state not found");
|
|
|
|
int paragraphCount = fileContent.Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries).Length;
|
|
|
|
return new FileStats { ParagraphCount = paragraphCount };
|
|
}
|
|
}
|
|
|
|
internal sealed class AggregationExecutor() : ReflectingExecutor<AggregationExecutor>("AggregationExecutor"), IMessageHandler<FileStats>
|
|
{
|
|
private readonly List<FileStats> _messages = [];
|
|
|
|
public async ValueTask HandleAsync(FileStats message, IWorkflowContext context)
|
|
{
|
|
this._messages.Add(message);
|
|
|
|
if (this._messages.Count == 2)
|
|
{
|
|
// Aggregate the results from both executors
|
|
var totalParagraphCount = this._messages.Sum(m => m.ParagraphCount);
|
|
var totalWordCount = this._messages.Sum(m => m.WordCount);
|
|
await context.YieldOutputAsync($"Total Paragraphs: {totalParagraphCount}, Total Words: {totalWordCount}");
|
|
}
|
|
}
|
|
}
|