mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: Workflow getting started samples in .Net (Second batch) (#650)
* Add loop and agents in workflows samples * Add foundry agent and workflow as agent samples * Checkpoint sample WIP * Checkpoint sample 1 Done * Add HIL samples * Fix formatting * Force folder name change step 1 * Force folder name change step 2 * Fix formatting * Fix formatting * Add checkpoint and rehydrate sample * _Foundational * Fix formatting
This commit is contained in:
committed by
GitHub
Unverified
parent
2393351a03
commit
89c8418705
@@ -83,14 +83,30 @@
|
||||
<File Path="../workflows/README.md" />
|
||||
<File Path="../workflows/wttr.json" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/Foundational/">
|
||||
<Project Path="samples/GettingStarted/Workflows/Foundational/01_ExecutorsAndEdges/01_ExecutorsAndEdges.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Foundational/02_Streaming/02_Streaming.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Foundational/03_AgentsInWorkflows/03_AgentsInWorkflows.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/SharedStates/">
|
||||
<Project Path="samples/GettingStarted/Workflows/SharedStates/SharedStates.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/Loop/">
|
||||
<Project Path="samples/GettingStarted/Workflows/Loop/Loop.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/Agents/">
|
||||
<Project Path="samples/GettingStarted/Workflows/Agents/CustomAgentExecutors/CustomAgentExecutors.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Agents/FoundryAgent/FoundryAgent.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowAsAnAgent.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/Checkpoint/">
|
||||
<Project Path="samples/GettingStarted/Workflows/Checkpoint/CheckpointAndResume/CheckpointAndResume.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Checkpoint/CheckpointAndRehydrate/CheckpointAndRehydrate.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Checkpoint/CheckpointWithHumanInTheLoop/CheckpointWithHumanInTheLoop.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/HumanInTheLoop/">
|
||||
<Project Path="samples/GettingStarted/Workflows/HumanInTheLoop/HumanInTheLoopBasic/HumanInTheLoopBasic.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/_Foundational/">
|
||||
<Project Path="samples/GettingStarted/Workflows/_Foundational/01_ExecutorsAndEdges/01_ExecutorsAndEdges.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/_Foundational/02_Streaming/02_Streaming.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/_Foundational/03_AgentsInWorkflows/03_AgentsInWorkflows.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/SemanticKernelMigration/" />
|
||||
<Folder Name="/Samples/SemanticKernelMigration/AzureAIFoundry/">
|
||||
<Project Path="samples/SemanticKernelMigration/AzureAIFoundry/Step01_Basics/AzureAIFoundry_Step01_Basics.csproj" />
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
// 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 WorkflowCustomAgentExecutorsSample;
|
||||
|
||||
/// <summary>
|
||||
/// This sample demonstrates how to create custom executors for AI agents.
|
||||
/// This is useful when you want more control over the agent's behaviors in a workflow.
|
||||
///
|
||||
/// In this example, we create two custom executors:
|
||||
/// 1. SloganWriterExecutor: An AI agent that generates slogans based on a given task.
|
||||
/// 2. FeedbackExecutor: An AI agent that provides feedback on the generated slogans.
|
||||
/// (These two executors manage the agent instances and their conversation threads.)
|
||||
///
|
||||
/// The workflow alternates between these two executors until the slogan meets a certain
|
||||
/// quality threshold or a maximum number of attempts is reached.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Pre-requisites:
|
||||
/// - Foundational samples should be completed first.
|
||||
/// - 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 the executors
|
||||
var sloganWriter = new SloganWriterExecutor(chatClient);
|
||||
var feedbackProvider = new FeedbackExecutor(chatClient);
|
||||
|
||||
// Build the workflow by adding executors and connecting them
|
||||
WorkflowBuilder builder = new(sloganWriter);
|
||||
builder.AddEdge(sloganWriter, feedbackProvider);
|
||||
builder.AddEdge(feedbackProvider, sloganWriter);
|
||||
var workflow = builder.Build<string>();
|
||||
|
||||
// Execute the workflow
|
||||
var ask = "Create a slogan for a new electric SUV that is affordable and fun to drive.";
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflow, ask);
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (evt is SloganGeneratedEvent || evt is FeedbackEvent)
|
||||
{
|
||||
// Custom events to allow us to monitor the progress of the workflow.
|
||||
Console.WriteLine($"{evt}");
|
||||
}
|
||||
|
||||
if (evt is WorkflowCompletedEvent completedEvent)
|
||||
{
|
||||
Console.WriteLine($"{completedEvent}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A class representing the output of the slogan writer agent.
|
||||
/// </summary>
|
||||
public sealed class SloganResult
|
||||
{
|
||||
[JsonPropertyName("task")]
|
||||
public required string Task { get; set; }
|
||||
|
||||
[JsonPropertyName("slogan")]
|
||||
public required string Slogan { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A class representing the output of the feedback agent.
|
||||
/// </summary>
|
||||
public sealed class FeedbackResult
|
||||
{
|
||||
[JsonPropertyName("comments")]
|
||||
public string Comments { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("rating")]
|
||||
public int Rating { get; set; }
|
||||
|
||||
[JsonPropertyName("actions")]
|
||||
public string Actions { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A custom event to indicate that a slogan has been generated.
|
||||
/// </summary>
|
||||
internal sealed class SloganGeneratedEvent(SloganResult sloganResult) : WorkflowEvent(sloganResult)
|
||||
{
|
||||
public override string ToString() => $"Slogan: {sloganResult.Slogan}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A custom executor that uses an AI agent to generate slogans based on a given task.
|
||||
/// Note that this executor has two message handlers:
|
||||
/// 1. HandleAsync(string message): Handles the initial task to create a slogan.
|
||||
/// 2. HandleAsync(Feedback message): Handles feedback to improve the slogan.
|
||||
/// </summary>
|
||||
internal sealed class SloganWriterExecutor
|
||||
: ReflectingExecutor<SloganWriterExecutor>,
|
||||
IMessageHandler<string, SloganResult>,
|
||||
IMessageHandler<FeedbackResult, SloganResult>
|
||||
{
|
||||
private const string Instruction = """
|
||||
You are a professional slogan writer. You will be given a task to create a slogan.
|
||||
""";
|
||||
|
||||
private readonly AIAgent _agent;
|
||||
private readonly AgentThread _thread;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SloganWriterExecutor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="chatClient">The chat client to use for the AI agent.</param>
|
||||
public SloganWriterExecutor(IChatClient chatClient)
|
||||
{
|
||||
var agentOptions = new ChatClientAgentOptions(instructions: Instruction)
|
||||
{
|
||||
ChatOptions = new()
|
||||
{
|
||||
ResponseFormat = ChatResponseFormatJson.ForJsonSchema(
|
||||
schema: AIJsonUtilities.CreateJsonSchema(typeof(SloganResult))
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
this._agent = new ChatClientAgent(chatClient, agentOptions);
|
||||
this._thread = this._agent.GetNewThread();
|
||||
}
|
||||
|
||||
public async ValueTask<SloganResult> HandleAsync(string message, IWorkflowContext context)
|
||||
{
|
||||
var result = await this._agent.RunAsync(message, this._thread);
|
||||
|
||||
var sloganResult = JsonSerializer.Deserialize<SloganResult>(result.Text) ?? throw new InvalidOperationException("Failed to deserialize slogan result.");
|
||||
|
||||
await context.AddEventAsync(new SloganGeneratedEvent(sloganResult));
|
||||
return sloganResult;
|
||||
}
|
||||
|
||||
public async ValueTask<SloganResult> HandleAsync(FeedbackResult message, IWorkflowContext context)
|
||||
{
|
||||
var feedbackMessage = $"""
|
||||
Here is the feedback on your previous slogan:
|
||||
Comments: {message.Comments}
|
||||
Rating: {message.Rating}
|
||||
Suggested Actions: {message.Actions}
|
||||
|
||||
Please use this feedback to improve your slogan.
|
||||
""";
|
||||
|
||||
var result = await this._agent.RunAsync(feedbackMessage, this._thread);
|
||||
var sloganResult = JsonSerializer.Deserialize<SloganResult>(result.Text) ?? throw new InvalidOperationException("Failed to deserialize slogan result.");
|
||||
|
||||
await context.AddEventAsync(new SloganGeneratedEvent(sloganResult));
|
||||
return sloganResult;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A custom event to indicate that feedback has been provided.
|
||||
/// </summary>
|
||||
internal sealed class FeedbackEvent(FeedbackResult feedbackResult) : WorkflowEvent(feedbackResult)
|
||||
{
|
||||
private readonly JsonSerializerOptions _options = new() { WriteIndented = true };
|
||||
public override string ToString() => $"Feedback:\n{JsonSerializer.Serialize(feedbackResult, this._options)}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A custom executor that uses an AI agent to provide feedback on a slogan.
|
||||
/// </summary>
|
||||
internal sealed class FeedbackExecutor : ReflectingExecutor<FeedbackExecutor>, IMessageHandler<SloganResult>
|
||||
{
|
||||
private const string Instruction = """
|
||||
You are a professional editor. You will be given a slogan and the task it is meant to accomplish.
|
||||
""";
|
||||
|
||||
private readonly AIAgent _agent;
|
||||
private readonly AgentThread _thread;
|
||||
|
||||
public int MinimumRating { get; init; } = 8;
|
||||
|
||||
public int MaxAttempts { get; init; } = 3;
|
||||
|
||||
private int _attempts = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FeedbackExecutor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="chatClient">The chat client to use for the AI agent.</param>
|
||||
public FeedbackExecutor(IChatClient chatClient)
|
||||
{
|
||||
var agentOptions = new ChatClientAgentOptions(instructions: Instruction)
|
||||
{
|
||||
ChatOptions = new()
|
||||
{
|
||||
ResponseFormat = ChatResponseFormatJson.ForJsonSchema(
|
||||
schema: AIJsonUtilities.CreateJsonSchema(typeof(FeedbackResult))
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
this._agent = new ChatClientAgent(chatClient, agentOptions);
|
||||
this._thread = this._agent.GetNewThread();
|
||||
}
|
||||
|
||||
public async ValueTask HandleAsync(SloganResult message, IWorkflowContext context)
|
||||
{
|
||||
var sloganMessage = $"""
|
||||
Here is a slogan for the task '{message.Task}':
|
||||
Slogan: {message.Slogan}
|
||||
Please provide feedback on this slogan, including comments, a rating from 1 to 10, and suggested actions for improvement.
|
||||
""";
|
||||
|
||||
var response = await this._agent.RunAsync(sloganMessage, this._thread);
|
||||
var feedback = JsonSerializer.Deserialize<FeedbackResult>(response.Text) ?? throw new InvalidOperationException("Failed to deserialize feedback.");
|
||||
|
||||
await context.AddEventAsync(new FeedbackEvent(feedback));
|
||||
if (feedback.Rating >= this.MinimumRating)
|
||||
{
|
||||
await context.AddEventAsync(new WorkflowCompletedEvent($"The following slogan was accepted:\n\n{message.Slogan}"));
|
||||
return;
|
||||
}
|
||||
if (this._attempts >= this.MaxAttempts)
|
||||
{
|
||||
await context.AddEventAsync(new WorkflowCompletedEvent($"The slogan was rejected after {this.MaxAttempts} attempts. Final slogan:\n\n{message.Slogan}"));
|
||||
return;
|
||||
}
|
||||
|
||||
await context.SendMessageAsync(feedback);
|
||||
this._attempts++;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<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.Agents.Persistent" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</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>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.AI.Agents.Persistent;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.Workflows;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
|
||||
namespace WorkflowFoundryAgentSample;
|
||||
|
||||
/// <summary>
|
||||
/// This sample shows how to use Azure Foundry Agents within a workflow.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Pre-requisites:
|
||||
/// - Foundational samples should be completed first.
|
||||
/// - An Azure Foundry project endpoint and model id.
|
||||
/// </remarks>
|
||||
public static class Program
|
||||
{
|
||||
private static async Task Main()
|
||||
{
|
||||
// Set up the Azure OpenAI client
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT")
|
||||
?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var model = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_MODEL_ID") ?? "gpt-4o-mini";
|
||||
var persistentAgentsClient = new PersistentAgentsClient(endpoint, new AzureCliCredential());
|
||||
|
||||
// Create agents
|
||||
AIAgent frenchAgent = await GetTranslationAgentAsync("French", persistentAgentsClient, model);
|
||||
AIAgent spanishAgent = await GetTranslationAgentAsync("Spanish", persistentAgentsClient, model);
|
||||
AIAgent englishAgent = await GetTranslationAgentAsync("English", persistentAgentsClient, model);
|
||||
|
||||
// Build the workflow by adding executors and connecting them
|
||||
WorkflowBuilder builder = new(frenchAgent);
|
||||
builder.AddEdge(frenchAgent, spanishAgent);
|
||||
builder.AddEdge(spanishAgent, englishAgent);
|
||||
var workflow = builder.Build<ChatMessage>();
|
||||
|
||||
// Execute the workflow
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflow, new ChatMessage(ChatRole.User, "Hello World!"));
|
||||
// Must send the turn token to trigger the agents.
|
||||
// The agents are wrapped as executors. When they receive messages,
|
||||
// they will cache the messages and only start processing when they receive a TurnToken.
|
||||
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (evt is AgentRunUpdateEvent executorComplete)
|
||||
{
|
||||
Console.WriteLine($"{executorComplete.ExecutorId}: {executorComplete.Data}");
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup the agents created for the sample.
|
||||
await persistentAgentsClient.Administration.DeleteAgentAsync(frenchAgent.Id);
|
||||
await persistentAgentsClient.Administration.DeleteAgentAsync(spanishAgent.Id);
|
||||
await persistentAgentsClient.Administration.DeleteAgentAsync(englishAgent.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a translation agent for the specified target language.
|
||||
/// </summary>
|
||||
/// <param name="targetLanguage">The target language for translation</param>
|
||||
/// <param name="persistentAgentsClient">The PersistentAgentsClient to create the agent</param>
|
||||
/// <param name="model">The model to use for the agent</param>
|
||||
/// <returns>A ChatClientAgent configured for the specified language</returns>
|
||||
private static async Task<ChatClientAgent> GetTranslationAgentAsync(
|
||||
string targetLanguage,
|
||||
PersistentAgentsClient persistentAgentsClient,
|
||||
string model)
|
||||
{
|
||||
string instructions = $"You are a translation assistant that translates the provided text to {targetLanguage}.";
|
||||
var agentMetadata = await persistentAgentsClient.Administration.CreateAgentAsync(
|
||||
model: model,
|
||||
name: $"{targetLanguage} Translator",
|
||||
instructions: instructions);
|
||||
|
||||
return await persistentAgentsClient.GetAIAgentAsync(agentMetadata.Value.Id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.Workflows;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
|
||||
namespace WorkflowAsAnAgentsSample;
|
||||
|
||||
/// <summary>
|
||||
/// This sample introduces the concepts workflows as agents, where a workflow can be
|
||||
/// treated as an <see cref="AIAgent"/>. This allows you to interact with a workflow
|
||||
/// as if it were a single agent.
|
||||
///
|
||||
/// In this example, we create a workflow that uses two language agents to process
|
||||
/// input concurrently, one that responds in French and another that responds in English.
|
||||
///
|
||||
/// You will interact with the workflow in an interactive loop, sending messages and receiving
|
||||
/// streaming responses from the workflow as if it were an agent who responds in both languages.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Pre-requisites:
|
||||
/// - Foundational samples should be completed first.
|
||||
/// - This sample uses concurrent processing.
|
||||
/// - An Azure OpenAI endpoint and deployment name.
|
||||
/// </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 the workflow and turn it into an agent
|
||||
var workflow = WorkflowHelper.GetWorkflow(chatClient);
|
||||
var agent = workflow.AsAgent("workflow-agent", "Workflow Agent");
|
||||
var thread = agent.GetNewThread();
|
||||
|
||||
// Start an interactive loop to interact with the workflow as if it were an agent
|
||||
while (true)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.Write("User (or 'exit' to quit): ");
|
||||
string? input = Console.ReadLine();
|
||||
if (string.IsNullOrWhiteSpace(input) || input.Equals("exit", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
await ProcessInputAsync(agent, thread, input);
|
||||
}
|
||||
|
||||
// Helper method to process user input and display streaming responses. To display
|
||||
// multiple interleaved responses correctly, we buffer updates by message ID and
|
||||
// re-render all messages on each update.
|
||||
static async Task ProcessInputAsync(AIAgent agent, AgentThread thread, string input)
|
||||
{
|
||||
Dictionary<string, List<AgentRunResponseUpdate>> buffer = [];
|
||||
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(input, thread).ConfigureAwait(false))
|
||||
{
|
||||
if (update.MessageId == null)
|
||||
{
|
||||
// skip updates that don't have a message ID
|
||||
continue;
|
||||
}
|
||||
Console.Clear();
|
||||
|
||||
if (!buffer.TryGetValue(update.MessageId, out List<AgentRunResponseUpdate>? value))
|
||||
{
|
||||
value = [];
|
||||
buffer[update.MessageId] = value;
|
||||
}
|
||||
value.Add(update);
|
||||
|
||||
foreach (var (messageId, segments) in buffer)
|
||||
{
|
||||
string combinedText = string.Concat(segments);
|
||||
Console.WriteLine($"{segments[0].AuthorName}: {combinedText}");
|
||||
Console.WriteLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
<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>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows;
|
||||
using Microsoft.Agents.Workflows.Reflection;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
|
||||
namespace WorkflowAsAnAgentsSample;
|
||||
|
||||
internal static class WorkflowHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a workflow that uses two language agents to process input concurrently.
|
||||
/// </summary>
|
||||
/// <param name="chatClient">The chat client to use for the agents</param>
|
||||
/// <returns>A workflow that processes input using two language agents</returns>
|
||||
internal static Workflow<List<ChatMessage>> GetWorkflow(IChatClient chatClient)
|
||||
{
|
||||
// Create executors
|
||||
var startExecutor = new ConcurrentStartExecutor();
|
||||
var aggregationExecutor = new ConcurrentAggregationExecutor();
|
||||
AIAgent frenchAgent = GetLanguageAgent("French", chatClient);
|
||||
AIAgent englishAgent = GetLanguageAgent("English", chatClient);
|
||||
|
||||
// Build the workflow by adding executors and connecting them
|
||||
WorkflowBuilder builder = new(startExecutor);
|
||||
builder.AddFanOutEdge(startExecutor, targets: [frenchAgent, englishAgent]);
|
||||
builder.AddFanInEdge(aggregationExecutor, sources: [frenchAgent, englishAgent]);
|
||||
|
||||
return builder.Build<List<ChatMessage>>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a language agent for the specified target language.
|
||||
/// </summary>
|
||||
/// <param name="targetLanguage">The target language for translation</param>
|
||||
/// <param name="chatClient">The chat client to use for the agent</param>
|
||||
/// <returns>A ChatClientAgent configured for the specified language</returns>
|
||||
private static ChatClientAgent GetLanguageAgent(string targetLanguage, IChatClient chatClient)
|
||||
{
|
||||
string instructions = $"You're a helpful assistant who always responds in {targetLanguage}.";
|
||||
return new ChatClientAgent(chatClient, instructions, name: $"{targetLanguage}Agent");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executor that starts the concurrent processing by sending messages to the agents.
|
||||
/// </summary>
|
||||
private sealed class ConcurrentStartExecutor() :
|
||||
ReflectingExecutor<ConcurrentStartExecutor>("ConcurrentStartExecutor"),
|
||||
IMessageHandler<List<ChatMessage>>
|
||||
{
|
||||
/// <summary>
|
||||
/// Starts the concurrent processing by sending messages to the agents.
|
||||
/// </summary>
|
||||
/// <param name="message">The user message to process</param>
|
||||
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
|
||||
public async ValueTask HandleAsync(List<ChatMessage> 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(message);
|
||||
// Broadcast the turn token to kick off the agents.
|
||||
await context.SendMessageAsync(new TurnToken(emitEvents: true));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executor that aggregates the results from the concurrent agents.
|
||||
/// </summary>
|
||||
private sealed class ConcurrentAggregationExecutor() :
|
||||
ReflectingExecutor<ConcurrentAggregationExecutor>("ConcurrentAggregationExecutor"),
|
||||
IMessageHandler<ChatMessage>
|
||||
{
|
||||
private readonly List<ChatMessage> _messages = [];
|
||||
|
||||
/// <summary>
|
||||
/// Handles incoming messages from the agents and aggregates their responses.
|
||||
/// </summary>
|
||||
/// <param name="message">The message from the agent</param>
|
||||
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
|
||||
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.Text}"));
|
||||
await context.AddEventAsync(new WorkflowCompletedEvent(formattedMessages));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows;
|
||||
|
||||
namespace WorkflowCheckpointAndRehydrateSample;
|
||||
|
||||
/// <summary>
|
||||
/// This sample introduces the concepts of check points and shows how to save and restore
|
||||
/// the state of a workflow using checkpoints.
|
||||
/// This sample demonstrates checkpoints, which allow you to save and restore a workflow's state.
|
||||
/// Key concepts:
|
||||
/// - Super Steps: A workflow executes in stages called "super steps". Each super step runs
|
||||
/// one or more executors and completes when all those executors finish their work.
|
||||
/// - Checkpoints: The system automatically saves the workflow's state at the end of each
|
||||
/// super step. You can use these checkpoints to resume the workflow from any saved point.
|
||||
/// - Rehydration: You can rehydrate a new workflow instance from a saved checkpoint, allowing
|
||||
/// you to continue execution from that point.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Pre-requisites:
|
||||
/// - Foundational samples should be completed first.
|
||||
/// </remarks>
|
||||
public static class Program
|
||||
{
|
||||
private static async Task Main()
|
||||
{
|
||||
// Create the workflow
|
||||
var workflow = WorkflowHelper.GetWorkflow();
|
||||
|
||||
// Create checkpoint manager
|
||||
var checkpointManager = new CheckpointManager();
|
||||
var checkpoints = new List<CheckpointInfo>();
|
||||
|
||||
// Execute the workflow and save checkpoints
|
||||
Checkpointed<StreamingRun> checkpointedRun = await InProcessExecution
|
||||
.StreamAsync(workflow, NumberSignal.Init, checkpointManager)
|
||||
.ConfigureAwait(false);
|
||||
await foreach (WorkflowEvent evt in checkpointedRun.Run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (evt is ExecutorCompletedEvent executorCompletedEvt)
|
||||
{
|
||||
Console.WriteLine($"* Executor {executorCompletedEvt.ExecutorId} completed.");
|
||||
}
|
||||
|
||||
if (evt is SuperStepCompletedEvent superStepCompletedEvt)
|
||||
{
|
||||
// Checkpoints are automatically created at the end of each super step when a
|
||||
// checkpoint manager is provided. You can store the checkpoint info for later use.
|
||||
CheckpointInfo? checkpoint = superStepCompletedEvt.CompletionInfo!.Checkpoint;
|
||||
if (checkpoint != null)
|
||||
{
|
||||
checkpoints.Add(checkpoint);
|
||||
Console.WriteLine($"** Checkpoint created at step {checkpoints.Count}.");
|
||||
}
|
||||
}
|
||||
|
||||
if (evt is WorkflowCompletedEvent workflowCompletedEvt)
|
||||
{
|
||||
Console.WriteLine($"Workflow completed with result: {workflowCompletedEvt.Data}");
|
||||
}
|
||||
}
|
||||
|
||||
if (checkpoints.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("No checkpoints were created during the workflow execution.");
|
||||
}
|
||||
Console.WriteLine($"Number of checkpoints created: {checkpoints.Count}");
|
||||
|
||||
// Rehydrate a new workflow instance from a saved checkpoint and continue execution
|
||||
var newWorkflow = WorkflowHelper.GetWorkflow();
|
||||
var checkpointIndex = 5;
|
||||
Console.WriteLine($"\n\nHydrating a new workflow instance from the {checkpointIndex + 1}th checkpoint.");
|
||||
CheckpointInfo savedCheckpoint = checkpoints[checkpointIndex];
|
||||
|
||||
Checkpointed<StreamingRun> newCheckpointedRun = await InProcessExecution
|
||||
.StreamAsync(newWorkflow, NumberSignal.Init, checkpointManager)
|
||||
.ConfigureAwait(false);
|
||||
await newCheckpointedRun.RestoreCheckpointAsync(savedCheckpoint, CancellationToken.None).ConfigureAwait(false);
|
||||
await foreach (WorkflowEvent evt in newCheckpointedRun.Run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (evt is ExecutorCompletedEvent executorCompletedEvt)
|
||||
{
|
||||
Console.WriteLine($"* Executor {executorCompletedEvt.ExecutorId} completed.");
|
||||
}
|
||||
|
||||
if (evt is WorkflowCompletedEvent workflowCompletedEvt)
|
||||
{
|
||||
Console.WriteLine($"Workflow completed with result: {workflowCompletedEvt.Data}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows;
|
||||
using Microsoft.Agents.Workflows.Reflection;
|
||||
|
||||
namespace WorkflowCheckpointAndRehydrateSample;
|
||||
|
||||
internal static class WorkflowHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Get a workflow that plays a number guessing game with checkpointing support.
|
||||
/// The workflow consists of two executors that are connected in a feedback loop:
|
||||
/// 1. GuessNumberExecutor: Makes a guess based on the current known bounds.
|
||||
/// 2. JudgeExecutor: Evaluates the guess and provides feedback.
|
||||
/// The workflow continues until the correct number is guessed.
|
||||
/// </summary>
|
||||
internal static Workflow<NumberSignal> GetWorkflow()
|
||||
{
|
||||
// Create the executors
|
||||
GuessNumberExecutor guessNumberExecutor = new(1, 100);
|
||||
JudgeExecutor judgeExecutor = new(42);
|
||||
|
||||
// Build the workflow by connecting executors in a loop
|
||||
var workflow = new WorkflowBuilder(guessNumberExecutor)
|
||||
.AddEdge(guessNumberExecutor, judgeExecutor)
|
||||
.AddEdge(judgeExecutor, guessNumberExecutor)
|
||||
.Build<NumberSignal>();
|
||||
|
||||
return workflow;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signals used for communication between GuessNumberExecutor and JudgeExecutor.
|
||||
/// </summary>
|
||||
internal enum NumberSignal
|
||||
{
|
||||
Init,
|
||||
Above,
|
||||
Below,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executor that makes a guess based on the current bounds.
|
||||
/// </summary>
|
||||
internal sealed class GuessNumberExecutor() : ReflectingExecutor<GuessNumberExecutor>("Guess"), IMessageHandler<NumberSignal>
|
||||
{
|
||||
/// <summary>
|
||||
/// The lower bound of the guessing range.
|
||||
/// </summary>
|
||||
public int LowerBound { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The upper bound of the guessing range.
|
||||
/// </summary>
|
||||
public int UpperBound { get; private set; }
|
||||
|
||||
private const string StateKey = "GuessNumberExecutorState";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GuessNumberExecutor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="lowerBound">The initial lower bound of the guessing range.</param>
|
||||
/// <param name="upperBound">The initial upper bound of the guessing range.</param>
|
||||
public GuessNumberExecutor(int lowerBound, int upperBound) : this()
|
||||
{
|
||||
this.LowerBound = lowerBound;
|
||||
this.UpperBound = upperBound;
|
||||
}
|
||||
|
||||
private int NextGuess => (this.LowerBound + this.UpperBound) / 2;
|
||||
|
||||
public async ValueTask HandleAsync(NumberSignal message, IWorkflowContext context)
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case NumberSignal.Init:
|
||||
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
|
||||
break;
|
||||
case NumberSignal.Above:
|
||||
this.UpperBound = this.NextGuess - 1;
|
||||
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
|
||||
break;
|
||||
case NumberSignal.Below:
|
||||
this.LowerBound = this.NextGuess + 1;
|
||||
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checkpoint the current state of the executor.
|
||||
/// This must be overridden to save any state that is needed to resume the executor.
|
||||
/// </summary>
|
||||
protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellation = default)
|
||||
{
|
||||
return context.QueueStateUpdateAsync(StateKey, (this.LowerBound, this.UpperBound));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restore the state of the executor from a checkpoint.
|
||||
/// This must be overridden to restore any state that was saved during checkpointing.
|
||||
/// </summary>
|
||||
protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellation = default)
|
||||
{
|
||||
(this.LowerBound, this.UpperBound) = await context.ReadStateAsync<(int, int)>(StateKey).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executor that judges the guess and provides feedback.
|
||||
/// </summary>
|
||||
internal sealed class JudgeExecutor() : ReflectingExecutor<JudgeExecutor>("Judge"), IMessageHandler<int>
|
||||
{
|
||||
private readonly int _targetNumber;
|
||||
private int _tries = 0;
|
||||
private const string StateKey = "JudgeExecutorState";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JudgeExecutor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="targetNumber">The number to be guessed.</param>
|
||||
public JudgeExecutor(int targetNumber) : this()
|
||||
{
|
||||
this._targetNumber = targetNumber;
|
||||
}
|
||||
|
||||
public async ValueTask HandleAsync(int message, IWorkflowContext context)
|
||||
{
|
||||
this._tries++;
|
||||
if (message == this._targetNumber)
|
||||
{
|
||||
await context.AddEventAsync(new WorkflowCompletedEvent($"{this._targetNumber} found in {this._tries} tries!"))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else if (message < this._targetNumber)
|
||||
{
|
||||
await context.SendMessageAsync(NumberSignal.Below).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await context.SendMessageAsync(NumberSignal.Above).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checkpoint the current state of the executor.
|
||||
/// This must be overridden to save any state that is needed to resume the executor.
|
||||
/// </summary>
|
||||
protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellation = default)
|
||||
{
|
||||
return context.QueueStateUpdateAsync(StateKey, this._tries);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restore the state of the executor from a checkpoint.
|
||||
/// This must be overridden to restore any state that was saved during checkpointing.
|
||||
/// </summary>
|
||||
protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellation = default)
|
||||
{
|
||||
this._tries = await context.ReadStateAsync<int>(StateKey).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.Workflows\Microsoft.Agents.Workflows.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows;
|
||||
|
||||
namespace WorkflowCheckpointAndResumeSample;
|
||||
|
||||
/// <summary>
|
||||
/// This sample introduces the concepts of check points and shows how to save and restore
|
||||
/// the state of a workflow using checkpoints.
|
||||
/// This sample demonstrates checkpoints, which allow you to save and restore a workflow's state.
|
||||
/// Key concepts:
|
||||
/// - Super Steps: A workflow executes in stages called "super steps". Each super step runs
|
||||
/// one or more executors and completes when all those executors finish their work.
|
||||
/// - Checkpoints: The system automatically saves the workflow's state at the end of each
|
||||
/// super step. You can use these checkpoints to resume the workflow from any saved point.
|
||||
/// - Resume: If needed, you can restore a checkpoint and continue execution from that state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Pre-requisites:
|
||||
/// - Foundational samples should be completed first.
|
||||
/// </remarks>
|
||||
public static class Program
|
||||
{
|
||||
private static async Task Main()
|
||||
{
|
||||
// Create the workflow
|
||||
var workflow = WorkflowHelper.GetWorkflow();
|
||||
|
||||
// Create checkpoint manager
|
||||
var checkpointManager = new CheckpointManager();
|
||||
var checkpoints = new List<CheckpointInfo>();
|
||||
|
||||
// Execute the workflow and save checkpoints
|
||||
Checkpointed<StreamingRun> checkpointedRun = await InProcessExecution
|
||||
.StreamAsync(workflow, NumberSignal.Init, checkpointManager)
|
||||
.ConfigureAwait(false);
|
||||
await foreach (WorkflowEvent evt in checkpointedRun.Run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (evt is ExecutorCompletedEvent executorCompletedEvt)
|
||||
{
|
||||
Console.WriteLine($"* Executor {executorCompletedEvt.ExecutorId} completed.");
|
||||
}
|
||||
|
||||
if (evt is SuperStepCompletedEvent superStepCompletedEvt)
|
||||
{
|
||||
// Checkpoints are automatically created at the end of each super step when a
|
||||
// checkpoint manager is provided. You can store the checkpoint info for later use.
|
||||
CheckpointInfo? checkpoint = superStepCompletedEvt.CompletionInfo!.Checkpoint;
|
||||
if (checkpoint != null)
|
||||
{
|
||||
checkpoints.Add(checkpoint);
|
||||
Console.WriteLine($"** Checkpoint created at step {checkpoints.Count}.");
|
||||
}
|
||||
}
|
||||
|
||||
if (evt is WorkflowCompletedEvent workflowCompletedEvt)
|
||||
{
|
||||
Console.WriteLine($"Workflow completed with result: {workflowCompletedEvt.Data}");
|
||||
}
|
||||
}
|
||||
|
||||
if (checkpoints.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("No checkpoints were created during the workflow execution.");
|
||||
}
|
||||
Console.WriteLine($"Number of checkpoints created: {checkpoints.Count}");
|
||||
|
||||
// Restoring from a checkpoint and resuming execution
|
||||
var checkpointIndex = 5;
|
||||
Console.WriteLine($"\n\nRestoring from the {checkpointIndex + 1}th checkpoint.");
|
||||
CheckpointInfo savedCheckpoint = checkpoints[checkpointIndex];
|
||||
// Note that we are restoring the state directly to the same run instance.
|
||||
await checkpointedRun.RestoreCheckpointAsync(savedCheckpoint, CancellationToken.None).ConfigureAwait(false);
|
||||
await foreach (WorkflowEvent evt in checkpointedRun.Run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (evt is ExecutorCompletedEvent executorCompletedEvt)
|
||||
{
|
||||
Console.WriteLine($"* Executor {executorCompletedEvt.ExecutorId} completed.");
|
||||
}
|
||||
|
||||
if (evt is WorkflowCompletedEvent workflowCompletedEvt)
|
||||
{
|
||||
Console.WriteLine($"Workflow completed with result: {workflowCompletedEvt.Data}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows;
|
||||
using Microsoft.Agents.Workflows.Reflection;
|
||||
|
||||
namespace WorkflowCheckpointAndResumeSample;
|
||||
|
||||
internal static class WorkflowHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Get a workflow that plays a number guessing game with checkpointing support.
|
||||
/// The workflow consists of two executors that are connected in a feedback loop:
|
||||
/// 1. GuessNumberExecutor: Makes a guess based on the current known bounds.
|
||||
/// 2. JudgeExecutor: Evaluates the guess and provides feedback.
|
||||
/// The workflow continues until the correct number is guessed.
|
||||
/// </summary>
|
||||
internal static Workflow<NumberSignal> GetWorkflow()
|
||||
{
|
||||
// Create the executors
|
||||
GuessNumberExecutor guessNumberExecutor = new(1, 100);
|
||||
JudgeExecutor judgeExecutor = new(42);
|
||||
|
||||
// Build the workflow by connecting executors in a loop
|
||||
var workflow = new WorkflowBuilder(guessNumberExecutor)
|
||||
.AddEdge(guessNumberExecutor, judgeExecutor)
|
||||
.AddEdge(judgeExecutor, guessNumberExecutor)
|
||||
.Build<NumberSignal>();
|
||||
|
||||
return workflow;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signals used for communication between GuessNumberExecutor and JudgeExecutor.
|
||||
/// </summary>
|
||||
internal enum NumberSignal
|
||||
{
|
||||
Init,
|
||||
Above,
|
||||
Below,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executor that makes a guess based on the current bounds.
|
||||
/// </summary>
|
||||
internal sealed class GuessNumberExecutor() : ReflectingExecutor<GuessNumberExecutor>("Guess"), IMessageHandler<NumberSignal>
|
||||
{
|
||||
/// <summary>
|
||||
/// The lower bound of the guessing range.
|
||||
/// </summary>
|
||||
public int LowerBound { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The upper bound of the guessing range.
|
||||
/// </summary>
|
||||
public int UpperBound { get; private set; }
|
||||
|
||||
private const string StateKey = "GuessNumberExecutorState";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GuessNumberExecutor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="lowerBound">The initial lower bound of the guessing range.</param>
|
||||
/// <param name="upperBound">The initial upper bound of the guessing range.</param>
|
||||
public GuessNumberExecutor(int lowerBound, int upperBound) : this()
|
||||
{
|
||||
this.LowerBound = lowerBound;
|
||||
this.UpperBound = upperBound;
|
||||
}
|
||||
|
||||
private int NextGuess => (this.LowerBound + this.UpperBound) / 2;
|
||||
|
||||
public async ValueTask HandleAsync(NumberSignal message, IWorkflowContext context)
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case NumberSignal.Init:
|
||||
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
|
||||
break;
|
||||
case NumberSignal.Above:
|
||||
this.UpperBound = this.NextGuess - 1;
|
||||
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
|
||||
break;
|
||||
case NumberSignal.Below:
|
||||
this.LowerBound = this.NextGuess + 1;
|
||||
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checkpoint the current state of the executor.
|
||||
/// This must be overridden to save any state that is needed to resume the executor.
|
||||
/// </summary>
|
||||
protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellation = default)
|
||||
{
|
||||
return context.QueueStateUpdateAsync(StateKey, (this.LowerBound, this.UpperBound));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restore the state of the executor from a checkpoint.
|
||||
/// This must be overridden to restore any state that was saved during checkpointing.
|
||||
/// </summary>
|
||||
protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellation = default)
|
||||
{
|
||||
(this.LowerBound, this.UpperBound) = await context.ReadStateAsync<(int, int)>(StateKey).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executor that judges the guess and provides feedback.
|
||||
/// </summary>
|
||||
internal sealed class JudgeExecutor() : ReflectingExecutor<JudgeExecutor>("Judge"), IMessageHandler<int>
|
||||
{
|
||||
private readonly int _targetNumber;
|
||||
private int _tries = 0;
|
||||
private const string StateKey = "JudgeExecutorState";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JudgeExecutor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="targetNumber">The number to be guessed.</param>
|
||||
public JudgeExecutor(int targetNumber) : this()
|
||||
{
|
||||
this._targetNumber = targetNumber;
|
||||
}
|
||||
|
||||
public async ValueTask HandleAsync(int message, IWorkflowContext context)
|
||||
{
|
||||
this._tries++;
|
||||
if (message == this._targetNumber)
|
||||
{
|
||||
await context.AddEventAsync(new WorkflowCompletedEvent($"{this._targetNumber} found in {this._tries} tries!"))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else if (message < this._targetNumber)
|
||||
{
|
||||
await context.SendMessageAsync(NumberSignal.Below).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await context.SendMessageAsync(NumberSignal.Above).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checkpoint the current state of the executor.
|
||||
/// This must be overridden to save any state that is needed to resume the executor.
|
||||
/// </summary>
|
||||
protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellation = default)
|
||||
{
|
||||
return context.QueueStateUpdateAsync(StateKey, this._tries);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restore the state of the executor from a checkpoint.
|
||||
/// This must be overridden to restore any state that was saved during checkpointing.
|
||||
/// </summary>
|
||||
protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellation = default)
|
||||
{
|
||||
this._tries = await context.ReadStateAsync<int>(StateKey).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows;
|
||||
|
||||
namespace WorkflowCheckpointWithHumanInTheLoopSample;
|
||||
|
||||
/// <summary>
|
||||
/// This sample demonstrates how to create a workflow with human-in-the-loop interaction and
|
||||
/// checkpointing support. The workflow plays a number guessing game where the user provides
|
||||
/// guesses based on feedback from the workflow. The workflow state is checkpointed at the end
|
||||
/// of each super step, allowing it to be restored and resumed later.
|
||||
/// Each InputPort request and response cycle takes two super steps:
|
||||
/// 1. The InputPort sends a RequestInfoEvent to request input from the external world.
|
||||
/// 2. The external world sends a response back to the InputPort.
|
||||
/// Thus, two checkpoints are created for each human-in-the-loop interaction.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Pre-requisites:
|
||||
/// - Foundational samples should be completed first.
|
||||
/// - This sample builds upon the HumanInTheLoopBasic sample. It's recommended to go through that
|
||||
/// sample first to understand the basics of human-in-the-loop workflows.
|
||||
/// - This sample also builds upon the CheckpointAndResume sample. It's recommended to
|
||||
/// go through that sample first to understand the basics of checkpointing and resuming workflows.
|
||||
/// </remarks>
|
||||
public static class Program
|
||||
{
|
||||
private static async Task Main()
|
||||
{
|
||||
// Create the workflow
|
||||
var workflow = WorkflowHelper.GetWorkflow();
|
||||
|
||||
// Create checkpoint manager
|
||||
var checkpointManager = new CheckpointManager();
|
||||
var checkpoints = new List<CheckpointInfo>();
|
||||
|
||||
// Execute the workflow and save checkpoints
|
||||
Checkpointed<StreamingRun> checkpointedRun = await InProcessExecution
|
||||
.StreamAsync(workflow, new SignalWithNumber(NumberSignal.Init), checkpointManager)
|
||||
.ConfigureAwait(false);
|
||||
await foreach (WorkflowEvent evt in checkpointedRun.Run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
switch (evt)
|
||||
{
|
||||
case RequestInfoEvent requestInputEvt:
|
||||
// Handle `RequestInfoEvent` from the workflow
|
||||
ExternalResponse response = HandleExternalRequest(requestInputEvt.Request);
|
||||
await checkpointedRun.Run.SendResponseAsync(response).ConfigureAwait(false);
|
||||
break;
|
||||
case ExecutorCompletedEvent executorCompletedEvt:
|
||||
Console.WriteLine($"* Executor {executorCompletedEvt.ExecutorId} completed.");
|
||||
break;
|
||||
case SuperStepCompletedEvent superStepCompletedEvt:
|
||||
// Checkpoints are automatically created at the end of each super step when a
|
||||
// checkpoint manager is provided. You can store the checkpoint info for later use.
|
||||
CheckpointInfo? checkpoint = superStepCompletedEvt.CompletionInfo!.Checkpoint;
|
||||
if (checkpoint != null)
|
||||
{
|
||||
checkpoints.Add(checkpoint);
|
||||
Console.WriteLine($"** Checkpoint created at step {checkpoints.Count}.");
|
||||
}
|
||||
break;
|
||||
case WorkflowCompletedEvent workflowCompletedEvt:
|
||||
Console.WriteLine($"Workflow completed with result: {workflowCompletedEvt.Data}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (checkpoints.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("No checkpoints were created during the workflow execution.");
|
||||
}
|
||||
Console.WriteLine($"Number of checkpoints created: {checkpoints.Count}");
|
||||
|
||||
// Restoring from a checkpoint and resuming execution
|
||||
var checkpointIndex = 1;
|
||||
Console.WriteLine($"\n\nRestoring from the {checkpointIndex + 1}th checkpoint.");
|
||||
CheckpointInfo savedCheckpoint = checkpoints[checkpointIndex];
|
||||
// Note that we are restoring the state directly to the same run instance.
|
||||
await checkpointedRun.RestoreCheckpointAsync(savedCheckpoint, CancellationToken.None).ConfigureAwait(false);
|
||||
await foreach (WorkflowEvent evt in checkpointedRun.Run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
switch (evt)
|
||||
{
|
||||
case RequestInfoEvent requestInputEvt:
|
||||
// Handle `RequestInfoEvent` from the workflow
|
||||
ExternalResponse response = HandleExternalRequest(requestInputEvt.Request);
|
||||
await checkpointedRun.Run.SendResponseAsync(response).ConfigureAwait(false);
|
||||
break;
|
||||
case ExecutorCompletedEvent executorCompletedEvt:
|
||||
Console.WriteLine($"* Executor {executorCompletedEvt.ExecutorId} completed.");
|
||||
break;
|
||||
case WorkflowCompletedEvent workflowCompletedEvt:
|
||||
Console.WriteLine($"Workflow completed with result: {workflowCompletedEvt.Data}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ExternalResponse HandleExternalRequest(ExternalRequest request)
|
||||
{
|
||||
if (request.Port.Request == typeof(SignalWithNumber))
|
||||
{
|
||||
var signal = (SignalWithNumber)request.Data;
|
||||
switch (signal.Signal)
|
||||
{
|
||||
case NumberSignal.Init:
|
||||
int initialGuess = ReadIntegerFromConsole("Please provide your initial guess: ");
|
||||
return request.CreateResponse<int>(initialGuess);
|
||||
case NumberSignal.Above:
|
||||
int lowerGuess = ReadIntegerFromConsole($"You previously guessed {signal.Number} too large. Please provide a new guess: ");
|
||||
return request.CreateResponse<int>(lowerGuess);
|
||||
case NumberSignal.Below:
|
||||
int higherGuess = ReadIntegerFromConsole($"You previously guessed {signal.Number} too small. Please provide a new guess: ");
|
||||
return request.CreateResponse<int>(higherGuess);
|
||||
}
|
||||
}
|
||||
|
||||
throw new NotSupportedException($"Request {request.Port.Request} is not supported");
|
||||
}
|
||||
|
||||
private static int ReadIntegerFromConsole(string prompt)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
Console.Write(prompt);
|
||||
string? input = Console.ReadLine();
|
||||
if (int.TryParse(input, out int value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
Console.WriteLine("Invalid input. Please enter a valid integer.");
|
||||
}
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows;
|
||||
using Microsoft.Agents.Workflows.Reflection;
|
||||
|
||||
namespace WorkflowCheckpointWithHumanInTheLoopSample;
|
||||
|
||||
internal static class WorkflowHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Get a workflow that plays a number guessing game with human-in-the-loop interaction.
|
||||
/// An input port allows the external world to provide inputs to the workflow upon requests.
|
||||
/// </summary>
|
||||
internal static Workflow<SignalWithNumber> GetWorkflow()
|
||||
{
|
||||
// Create the executors
|
||||
InputPort numberInputPort = InputPort.Create<SignalWithNumber, int>("GuessNumber");
|
||||
JudgeExecutor judgeExecutor = new(42);
|
||||
|
||||
// Build the workflow by connecting executors in a loop
|
||||
var workflow = new WorkflowBuilder(numberInputPort)
|
||||
.AddEdge(numberInputPort, judgeExecutor)
|
||||
.AddEdge(judgeExecutor, numberInputPort)
|
||||
.Build<SignalWithNumber>();
|
||||
|
||||
return workflow;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signals indicating if the guess was too high, too low, or an initial guess.
|
||||
/// </summary>
|
||||
internal enum NumberSignal
|
||||
{
|
||||
Init,
|
||||
Above,
|
||||
Below,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signals used for communication between guesses and the JudgeExecutor.
|
||||
/// </summary>
|
||||
internal sealed class SignalWithNumber
|
||||
{
|
||||
public NumberSignal Signal { get; }
|
||||
public int? Number { get; }
|
||||
|
||||
public SignalWithNumber(NumberSignal signal, int? number = null)
|
||||
{
|
||||
this.Signal = signal;
|
||||
this.Number = number;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executor that judges the guess and provides feedback.
|
||||
/// </summary>
|
||||
internal sealed class JudgeExecutor() : ReflectingExecutor<JudgeExecutor>("Judge"), IMessageHandler<int>
|
||||
{
|
||||
private readonly int _targetNumber;
|
||||
private int _tries = 0;
|
||||
private const string StateKey = "JudgeExecutorState";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JudgeExecutor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="targetNumber">The number to be guessed.</param>
|
||||
public JudgeExecutor(int targetNumber) : this()
|
||||
{
|
||||
this._targetNumber = targetNumber;
|
||||
}
|
||||
|
||||
public async ValueTask HandleAsync(int message, IWorkflowContext context)
|
||||
{
|
||||
this._tries++;
|
||||
if (message == this._targetNumber)
|
||||
{
|
||||
await context.AddEventAsync(new WorkflowCompletedEvent($"{this._targetNumber} found in {this._tries} tries!"))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else if (message < this._targetNumber)
|
||||
{
|
||||
await context.SendMessageAsync(new SignalWithNumber(NumberSignal.Below, message)).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await context.SendMessageAsync(new SignalWithNumber(NumberSignal.Above, message)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checkpoint the current state of the executor.
|
||||
/// This must be overridden to save any state that is needed to resume the executor.
|
||||
/// </summary>
|
||||
protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellation = default)
|
||||
{
|
||||
return context.QueueStateUpdateAsync(StateKey, this._tries);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restore the state of the executor from a checkpoint.
|
||||
/// This must be overridden to restore any state that was saved during checkpointing.
|
||||
/// </summary>
|
||||
protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellation = default)
|
||||
{
|
||||
this._tries = await context.ReadStateAsync<int>(StateKey).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@ internal sealed class ConcurrentStartExecutor() :
|
||||
/// </summary>
|
||||
/// <param name="message">The user message to process</param>
|
||||
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
|
||||
/// <returns></returns>
|
||||
/// <returns>A task representing the asynchronous operation</returns>
|
||||
public async ValueTask HandleAsync(string message, IWorkflowContext context)
|
||||
{
|
||||
// Broadcast the message to all connected agents. Receiving agents will queue
|
||||
@@ -110,7 +110,7 @@ internal sealed class ConcurrentAggregationExecutor() :
|
||||
/// </summary>
|
||||
/// <param name="message">The message from the agent</param>
|
||||
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
|
||||
/// <returns></returns>
|
||||
/// <returns>A task representing the asynchronous operation</returns>
|
||||
public async ValueTask HandleAsync(ChatMessage message, IWorkflowContext context)
|
||||
{
|
||||
this._messages.Add(message);
|
||||
|
||||
@@ -92,13 +92,13 @@ internal sealed class Program
|
||||
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (evt is ExecutorInvokeEvent executorInvoked)
|
||||
if (evt is ExecutorInvokedEvent executorInvoked)
|
||||
{
|
||||
Debug.WriteLine($"EXECUTOR ENTER #{executorInvoked.ExecutorId}");
|
||||
}
|
||||
else if (evt is ExecutorCompleteEvent executorComplete)
|
||||
else if (evt is ExecutorCompletedEvent executorCompleted)
|
||||
{
|
||||
Debug.WriteLine($"EXECUTOR EXIT #{executorComplete.ExecutorId}");
|
||||
Debug.WriteLine($"EXECUTOR EXIT #{executorCompleted.ExecutorId}");
|
||||
}
|
||||
if (evt is DeclarativeActionInvokeEvent actionInvoked)
|
||||
{
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.Workflows\Microsoft.Agents.Workflows.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows;
|
||||
|
||||
namespace WorkflowHumanInTheLoopBasicSample;
|
||||
|
||||
/// <summary>
|
||||
/// This sample introduces the concept of InputPort and ExternalRequest to enable
|
||||
/// human-in-the-loop interaction scenarios.
|
||||
/// An input port can be used as if it were an executor in the workflow graph. Upon receiving
|
||||
/// a message, the input port generates an RequestInfoEvent that gets emitted to the external world.
|
||||
/// The external world can then respond to the request by sending an ExternalResponse back to
|
||||
/// the workflow.
|
||||
/// The sample implements a simple number guessing game where the external user tries to guess
|
||||
/// a pre-defined target number. The workflow consists of a single JudgeExecutor that judges
|
||||
/// the user's guesses and provides feedback.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Pre-requisites:
|
||||
/// - Foundational samples should be completed first.
|
||||
/// </remarks>
|
||||
public static class Program
|
||||
{
|
||||
private static async Task Main()
|
||||
{
|
||||
// Create the workflow
|
||||
var workflow = WorkflowHelper.GetWorkflow();
|
||||
|
||||
// Execute the workflow
|
||||
StreamingRun handle = await InProcessExecution.StreamAsync(workflow, NumberSignal.Init).ConfigureAwait(false);
|
||||
await foreach (WorkflowEvent evt in handle.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
switch (evt)
|
||||
{
|
||||
case RequestInfoEvent requestInputEvt:
|
||||
// Handle `RequestInfoEvent` from the workflow
|
||||
ExternalResponse response = HandleExternalRequest(requestInputEvt.Request);
|
||||
await handle.SendResponseAsync(response).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
case WorkflowCompletedEvent workflowCompleteEvt:
|
||||
// The workflow has completed successfully
|
||||
Console.WriteLine($"Workflow completed with result: {workflowCompleteEvt.Data}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ExternalResponse HandleExternalRequest(ExternalRequest request)
|
||||
{
|
||||
if (request.Port.Request == typeof(NumberSignal))
|
||||
{
|
||||
var signal = (NumberSignal)request.Data;
|
||||
switch (signal)
|
||||
{
|
||||
case NumberSignal.Init:
|
||||
int initialGuess = ReadIntegerFromConsole("Please provide your initial guess: ");
|
||||
return request.CreateResponse<int>(initialGuess);
|
||||
case NumberSignal.Above:
|
||||
int lowerGuess = ReadIntegerFromConsole("You previously guessed too large. Please provide a new guess: ");
|
||||
return request.CreateResponse<int>(lowerGuess);
|
||||
case NumberSignal.Below:
|
||||
int higherGuess = ReadIntegerFromConsole("You previously guessed too small. Please provide a new guess: ");
|
||||
return request.CreateResponse<int>(higherGuess);
|
||||
}
|
||||
}
|
||||
|
||||
throw new NotSupportedException($"Request {request.Port.Request} is not supported");
|
||||
}
|
||||
|
||||
private static int ReadIntegerFromConsole(string prompt)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
Console.Write(prompt);
|
||||
string? input = Console.ReadLine();
|
||||
if (int.TryParse(input, out int value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
Console.WriteLine("Invalid input. Please enter a valid integer.");
|
||||
}
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows;
|
||||
using Microsoft.Agents.Workflows.Reflection;
|
||||
|
||||
namespace WorkflowHumanInTheLoopBasicSample;
|
||||
|
||||
internal static class WorkflowHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Get a workflow that plays a number guessing game with human-in-the-loop interaction.
|
||||
/// An input port allows the external world to provide inputs to the workflow upon requests.
|
||||
/// </summary>
|
||||
internal static Workflow<NumberSignal> GetWorkflow()
|
||||
{
|
||||
// Create the executors
|
||||
InputPort numberInputPort = InputPort.Create<NumberSignal, int>("GuessNumber");
|
||||
JudgeExecutor judgeExecutor = new(42);
|
||||
|
||||
// Build the workflow by connecting executors in a loop
|
||||
var workflow = new WorkflowBuilder(numberInputPort)
|
||||
.AddEdge(numberInputPort, judgeExecutor)
|
||||
.AddEdge(judgeExecutor, numberInputPort)
|
||||
.Build<NumberSignal>();
|
||||
|
||||
return workflow;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signals used for communication between guesses and the JudgeExecutor.
|
||||
/// </summary>
|
||||
internal enum NumberSignal
|
||||
{
|
||||
Init,
|
||||
Above,
|
||||
Below,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executor that judges the guess and provides feedback.
|
||||
/// </summary>
|
||||
internal sealed class JudgeExecutor() : ReflectingExecutor<JudgeExecutor>("Judge"), IMessageHandler<int>
|
||||
{
|
||||
private readonly int _targetNumber;
|
||||
private int _tries = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JudgeExecutor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="targetNumber">The number to be guessed.</param>
|
||||
public JudgeExecutor(int targetNumber) : this()
|
||||
{
|
||||
this._targetNumber = targetNumber;
|
||||
}
|
||||
|
||||
public async ValueTask HandleAsync(int message, IWorkflowContext context)
|
||||
{
|
||||
this._tries++;
|
||||
if (message == this._targetNumber)
|
||||
{
|
||||
await context.AddEventAsync(new WorkflowCompletedEvent($"{this._targetNumber} found in {this._tries} tries!"))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else if (message < this._targetNumber)
|
||||
{
|
||||
await context.SendMessageAsync(NumberSignal.Below).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await context.SendMessageAsync(NumberSignal.Above).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.Workflows\Microsoft.Agents.Workflows.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,139 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Workflows;
|
||||
using Microsoft.Agents.Workflows.Reflection;
|
||||
|
||||
namespace WorkflowLoopSample;
|
||||
|
||||
/// <summary>
|
||||
/// This sample demonstrates a simple number guessing game using a workflow with looping behavior.
|
||||
///
|
||||
/// The workflow consists of two executors that are connected in a feedback loop:
|
||||
/// 1. GuessNumberExecutor: Makes a guess based on the current known bounds.
|
||||
/// 2. JudgeExecutor: Evaluates the guess and provides feedback.
|
||||
/// The workflow continues until the correct number is guessed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Pre-requisites:
|
||||
/// - Foundational samples should be completed first.
|
||||
/// </remarks>
|
||||
public static class Program
|
||||
{
|
||||
private static async Task Main()
|
||||
{
|
||||
// Create the executors
|
||||
GuessNumberExecutor guessNumberExecutor = new(1, 100);
|
||||
JudgeExecutor judgeExecutor = new(42);
|
||||
|
||||
// Build the workflow by connecting executors in a loop
|
||||
var workflow = new WorkflowBuilder(guessNumberExecutor)
|
||||
.AddEdge(guessNumberExecutor, judgeExecutor)
|
||||
.AddEdge(judgeExecutor, guessNumberExecutor)
|
||||
.Build<NumberSignal>();
|
||||
|
||||
// Execute the workflow
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflow, NumberSignal.Init).ConfigureAwait(false);
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (evt is WorkflowCompletedEvent workflowCompleteEvt)
|
||||
{
|
||||
Console.WriteLine($"Result: {workflowCompleteEvt}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signals used for communication between GuessNumberExecutor and JudgeExecutor.
|
||||
/// </summary>
|
||||
internal enum NumberSignal
|
||||
{
|
||||
Init,
|
||||
Above,
|
||||
Below,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executor that makes a guess based on the current bounds.
|
||||
/// </summary>
|
||||
internal sealed class GuessNumberExecutor : ReflectingExecutor<GuessNumberExecutor>, IMessageHandler<NumberSignal>
|
||||
{
|
||||
/// <summary>
|
||||
/// The lower bound of the guessing range.
|
||||
/// </summary>
|
||||
public int LowerBound { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The upper bound of the guessing range.
|
||||
/// </summary>
|
||||
public int UpperBound { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GuessNumberExecutor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="lowerBound">The initial lower bound of the guessing range.</param>
|
||||
/// <param name="upperBound">The initial upper bound of the guessing range.</param>
|
||||
public GuessNumberExecutor(int lowerBound, int upperBound)
|
||||
{
|
||||
this.LowerBound = lowerBound;
|
||||
this.UpperBound = upperBound;
|
||||
}
|
||||
|
||||
private int NextGuess => (this.LowerBound + this.UpperBound) / 2;
|
||||
|
||||
public async ValueTask HandleAsync(NumberSignal message, IWorkflowContext context)
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case NumberSignal.Init:
|
||||
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
|
||||
break;
|
||||
case NumberSignal.Above:
|
||||
this.UpperBound = this.NextGuess - 1;
|
||||
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
|
||||
break;
|
||||
case NumberSignal.Below:
|
||||
this.LowerBound = this.NextGuess + 1;
|
||||
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executor that judges the guess and provides feedback.
|
||||
/// </summary>
|
||||
internal sealed class JudgeExecutor : ReflectingExecutor<JudgeExecutor>, IMessageHandler<int>
|
||||
{
|
||||
private readonly int _targetNumber;
|
||||
private int _tries = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JudgeExecutor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="targetNumber">The number to be guessed.</param>
|
||||
public JudgeExecutor(int targetNumber)
|
||||
{
|
||||
this._targetNumber = targetNumber;
|
||||
}
|
||||
|
||||
public async ValueTask HandleAsync(int message, IWorkflowContext context)
|
||||
{
|
||||
this._tries++;
|
||||
if (message == this._targetNumber)
|
||||
{
|
||||
await context.AddEventAsync(new WorkflowCompletedEvent($"{this._targetNumber} found in {this._tries} tries!"))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else if (message < this._targetNumber)
|
||||
{
|
||||
await context.SendMessageAsync(NumberSignal.Below).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await context.SendMessageAsync(NumberSignal.Above).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,24 +6,40 @@ The getting started with workflow samples demonstrate the fundamental concepts a
|
||||
|
||||
### Foundational Concepts - Start Here
|
||||
|
||||
Please begin with the [Foundational](./Foundational) samples in order. These three samples introduce the core concepts of executors, edges, agents in workflows, streaming, and workflow construction.
|
||||
Please begin with the [Foundational](./_Foundational) samples in order. These three samples introduce the core concepts of executors, edges, agents in workflows, streaming, and workflow construction.
|
||||
|
||||
> The folder name starts with an underscore (`_Foundational`) to ensure it appears first in the explorer view.
|
||||
|
||||
| Sample | Concepts |
|
||||
|--------|----------|
|
||||
| [Executors and Edges](./Foundational/01_ExecutorsAndEdges) | Minimal workflow with basic executors and edges |
|
||||
| [Streaming](./Foundational/02_Streaming) | Extends workflows with event streaming |
|
||||
| [Agents](./Foundational/03_AgentsInWorkflows) | Use agents in workflows |
|
||||
| [Executors and Edges](./_Foundational/01_ExecutorsAndEdges) | Minimal workflow with basic executors and edges |
|
||||
| [Streaming](./_Foundational/02_Streaming) | Extends workflows with event streaming |
|
||||
| [Agents](./_Foundational/03_AgentsInWorkflows) | Use agents in workflows |
|
||||
|
||||
Once completed, please proceed to other samples listed below.
|
||||
|
||||
> Note that you don't need to follow a strict order after the foundational samples. However, some samples build upon concepts from previous ones, so it's beneficial to be aware of the dependencies.
|
||||
|
||||
### Agents
|
||||
|
||||
| Sample | Concepts |
|
||||
|--------|----------|
|
||||
| [Foundry Agents in Workflows](./Agents/FoundryAgent) | Demonstrates using Azure Foundry Agents within a workflow |
|
||||
| [Custom Agent Executors](./Agents/CustomAgentExecutors) | Shows how to create a custom agent executor for more complex scenarios |
|
||||
| [Workflow as an Agent](./Agents/WorkflowAsAgent) | Illustrates how to encapsulate a workflow as an agent |
|
||||
|
||||
### Concurrent Execution
|
||||
|
||||
| Sample | Concepts |
|
||||
|--------|----------|
|
||||
| [Fan-Out and Fan-In](./Concurrent) | Introduces parallel processing with fan-out and fan-in patterns |
|
||||
|
||||
### Loop
|
||||
|
||||
| Sample | Concepts |
|
||||
|--------|----------|
|
||||
| [Looping](./Loop) | Shows how to create a loop within a workflow |
|
||||
|
||||
### Workflow Shared States
|
||||
|
||||
| Sample | Concepts |
|
||||
@@ -40,10 +56,22 @@ Once completed, please proceed to other samples listed below.
|
||||
|
||||
> These 3 samples build upon each other. It's recommended to explore them in sequence to fully grasp the concepts.
|
||||
|
||||
|
||||
### Declarative Workflows
|
||||
|
||||
| Sample | Concepts |
|
||||
|--------|----------|
|
||||
| [DeclarativeWorkflow](./DeclarativeWorkflow) | Demonstrates execution of declartive workflows. |
|
||||
|
||||
### Checkpointing
|
||||
|
||||
| Sample | Concepts |
|
||||
|--------|----------|
|
||||
| [Checkpoint and Resume](./Checkpoint/CheckpointAndResume) | Introduces checkpoints for saving and restoring workflow state for time travel purposes |
|
||||
| [Checkpoint and Rehydrate](./Checkpoint/CheckpointAndRehydrate) | Demonstrates hydrating a new workflow instance from a saved checkpoint |
|
||||
| [Checkpoint with Human-in-the-Loop](./Checkpoint/CheckpointWithHumanInTheLoop) | Combines checkpointing with human-in-the-loop interactions |
|
||||
|
||||
### Human-in-the-Loop
|
||||
|
||||
| Sample | Concepts |
|
||||
|--------|----------|
|
||||
| [Basic Human-in-the-Loop](./HumanInTheLoop/HumanIntheLoopBasic) | Introduces human-in-the-loop interaction using input ports and external requests |
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.Workflows\Microsoft.Agents.Workflows.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+1
-1
@@ -35,7 +35,7 @@ public static class Program
|
||||
Run run = await InProcessExecution.RunAsync(workflow, "Hello, World!");
|
||||
foreach (WorkflowEvent evt in run.NewEvents)
|
||||
{
|
||||
if (evt is ExecutorCompleteEvent executorComplete)
|
||||
if (evt is ExecutorCompletedEvent executorComplete)
|
||||
{
|
||||
Console.WriteLine($"{executorComplete.ExecutorId}: {executorComplete.Data}");
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.Workflows\Microsoft.Agents.Workflows.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+2
-2
@@ -34,9 +34,9 @@ public static class Program
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflow, "Hello, World!");
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (evt is ExecutorCompleteEvent executorComplete)
|
||||
if (evt is ExecutorCompletedEvent executorCompleted)
|
||||
{
|
||||
Console.WriteLine($"{executorComplete.ExecutorId}: {executorComplete.Data}");
|
||||
Console.WriteLine($"{executorCompleted.ExecutorId}: {executorCompleted.Data}");
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
<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>
|
||||
|
||||
</Project>
|
||||
@@ -67,7 +67,7 @@ public abstract class Executor : IIdentified
|
||||
/// <exception cref="TargetInvocationException">An exception is generated while handling the message.</exception>
|
||||
public async ValueTask<object?> ExecuteAsync(object message, Type messageType, IWorkflowContext context)
|
||||
{
|
||||
await context.AddEventAsync(new ExecutorInvokeEvent(this.Id, message)).ConfigureAwait(false);
|
||||
await context.AddEventAsync(new ExecutorInvokedEvent(this.Id, message)).ConfigureAwait(false);
|
||||
|
||||
CallResult? result = await this.Router.RouteMessageAsync(message, context, requireRoute: true)
|
||||
.ConfigureAwait(false);
|
||||
@@ -75,7 +75,7 @@ public abstract class Executor : IIdentified
|
||||
ExecutorEvent executionResult;
|
||||
if (result == null || result.IsSuccess)
|
||||
{
|
||||
executionResult = new ExecutorCompleteEvent(this.Id, result?.Result);
|
||||
executionResult = new ExecutorCompletedEvent(this.Id, result?.Result);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
+1
-1
@@ -7,4 +7,4 @@ namespace Microsoft.Agents.Workflows;
|
||||
/// </summary>
|
||||
/// <param name="executorId">The unique identifier of the executor that has completed.</param>
|
||||
/// <param name="result">The result produced by the executor upon completion, or <c>null</c> if no result is available.</param>
|
||||
public sealed class ExecutorCompleteEvent(string executorId, object? result) : ExecutorEvent(executorId, data: result);
|
||||
public sealed class ExecutorCompletedEvent(string executorId, object? result) : ExecutorEvent(executorId, data: result);
|
||||
+1
-1
@@ -7,4 +7,4 @@ namespace Microsoft.Agents.Workflows;
|
||||
/// </summary>
|
||||
/// <param name="executorId">The unique identifier of the executor being invoked.</param>
|
||||
/// <param name="message">The invocation message.</param>
|
||||
public sealed class ExecutorInvokeEvent(string executorId, object message) : ExecutorEvent(executorId, data: message);
|
||||
public sealed class ExecutorInvokedEvent(string executorId, object message) : ExecutorEvent(executorId, data: message);
|
||||
+7
-7
@@ -208,20 +208,20 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow
|
||||
|
||||
private void AssertExecutionCount(int expectedCount)
|
||||
{
|
||||
Assert.Equal(expectedCount + 2, this.WorkflowEventCounts[typeof(ExecutorInvokeEvent)]);
|
||||
Assert.Equal(expectedCount + 2, this.WorkflowEventCounts[typeof(ExecutorCompleteEvent)]);
|
||||
Assert.Equal(expectedCount + 2, this.WorkflowEventCounts[typeof(ExecutorInvokedEvent)]);
|
||||
Assert.Equal(expectedCount + 2, this.WorkflowEventCounts[typeof(ExecutorCompletedEvent)]);
|
||||
}
|
||||
|
||||
private void AssertNotExecuted(string executorId)
|
||||
{
|
||||
Assert.DoesNotContain(this.WorkflowEvents.OfType<ExecutorInvokeEvent>(), e => e.ExecutorId == executorId);
|
||||
Assert.DoesNotContain(this.WorkflowEvents.OfType<ExecutorCompleteEvent>(), e => e.ExecutorId == executorId);
|
||||
Assert.DoesNotContain(this.WorkflowEvents.OfType<ExecutorInvokedEvent>(), e => e.ExecutorId == executorId);
|
||||
Assert.DoesNotContain(this.WorkflowEvents.OfType<ExecutorCompletedEvent>(), e => e.ExecutorId == executorId);
|
||||
}
|
||||
|
||||
private void AssertExecuted(string executorId)
|
||||
{
|
||||
Assert.Contains(this.WorkflowEvents.OfType<ExecutorInvokeEvent>(), e => e.ExecutorId == executorId);
|
||||
Assert.Contains(this.WorkflowEvents.OfType<ExecutorCompleteEvent>(), e => e.ExecutorId == executorId);
|
||||
Assert.Contains(this.WorkflowEvents.OfType<ExecutorInvokedEvent>(), e => e.ExecutorId == executorId);
|
||||
Assert.Contains(this.WorkflowEvents.OfType<ExecutorCompletedEvent>(), e => e.ExecutorId == executorId);
|
||||
}
|
||||
|
||||
private void AssertMessage(string message)
|
||||
@@ -244,7 +244,7 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow
|
||||
this.WorkflowEvents = run.WatchStreamAsync().ToEnumerable().ToImmutableList();
|
||||
foreach (WorkflowEvent workflowEvent in this.WorkflowEvents)
|
||||
{
|
||||
if (workflowEvent is ExecutorInvokeEvent invokeEvent)
|
||||
if (workflowEvent is ExecutorInvokedEvent invokeEvent)
|
||||
{
|
||||
DeclarativeExecutorResult? message = invokeEvent.Data as DeclarativeExecutorResult;
|
||||
this.Output.WriteLine($"EXEC: {invokeEvent.ExecutorId} << {message?.ExecutorId ?? "?"} [{message?.Result ?? "-"}]");
|
||||
|
||||
+2
-2
@@ -28,9 +28,9 @@ internal static class Step1EntryPoint
|
||||
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (evt is ExecutorCompleteEvent executorComplete)
|
||||
if (evt is ExecutorCompletedEvent executorCompleted)
|
||||
{
|
||||
writer.WriteLine($"{executorComplete.ExecutorId}: {executorComplete.Data}");
|
||||
writer.WriteLine($"{executorCompleted.ExecutorId}: {executorCompleted.Data}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -17,9 +17,9 @@ internal static class Step1aEntryPoint
|
||||
|
||||
foreach (WorkflowEvent evt in run.NewEvents)
|
||||
{
|
||||
if (evt is ExecutorCompleteEvent executorComplete)
|
||||
if (evt is ExecutorCompletedEvent executorCompleted)
|
||||
{
|
||||
writer.WriteLine($"{executorComplete.ExecutorId}: {executorComplete.Data}");
|
||||
writer.WriteLine($"{executorCompleted.ExecutorId}: {executorCompleted.Data}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -39,8 +39,8 @@ internal static class Step2EntryPoint
|
||||
string workflowResult = workflowCompleteEvt.Data!.ToString()!;
|
||||
writer.WriteLine($"Result: {workflowResult}");
|
||||
return workflowResult;
|
||||
case ExecutorCompleteEvent executorCompleteEvt:
|
||||
writer.WriteLine($"'{executorCompleteEvt.ExecutorId}: {executorCompleteEvt.Data}");
|
||||
case ExecutorCompletedEvent executorCompletedEvt:
|
||||
writer.WriteLine($"'{executorCompletedEvt.ExecutorId}: {executorCompletedEvt.Data}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +37,8 @@ internal static class Step3EntryPoint
|
||||
string workflowResult = workflowCompleteEvt.Data!.ToString()!;
|
||||
writer.WriteLine($"Result: {workflowResult}");
|
||||
return workflowResult;
|
||||
case ExecutorCompleteEvent executorCompleteEvt:
|
||||
writer.WriteLine($"'{executorCompleteEvt.ExecutorId}: {executorCompleteEvt.Data}");
|
||||
case ExecutorCompletedEvent executorCompletedEvt:
|
||||
writer.WriteLine($"'{executorCompletedEvt.ExecutorId}: {executorCompletedEvt.Data}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -46,8 +46,8 @@ internal static class Step4EntryPoint
|
||||
string workflowResult = workflowCompleteEvt.Data!.ToString()!;
|
||||
writer.WriteLine($"Result: {workflowResult}");
|
||||
return workflowResult;
|
||||
case ExecutorCompleteEvent executorCompleteEvt:
|
||||
writer.WriteLine($"'{executorCompleteEvt.ExecutorId}: {executorCompleteEvt.Data}");
|
||||
case ExecutorCompletedEvent executorCompletedEvt:
|
||||
writer.WriteLine($"'{executorCompletedEvt.ExecutorId}: {executorCompletedEvt.Data}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -85,7 +85,7 @@ internal static class Step5EntryPoint
|
||||
string workflowResult = workflowCompleteEvt.Data!.ToString()!;
|
||||
writer.WriteLine($"Result: {workflowResult}");
|
||||
return workflowResult;
|
||||
case ExecutorCompleteEvent executorCompleteEvt:
|
||||
case ExecutorCompletedEvent executorCompleteEvt:
|
||||
writer.WriteLine($"'{executorCompleteEvt.ExecutorId}: {executorCompleteEvt.Data}");
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -37,9 +37,9 @@ internal static class Step6EntryPoint
|
||||
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (evt is ExecutorCompleteEvent executorComplete)
|
||||
if (evt is ExecutorCompletedEvent executorCompleted)
|
||||
{
|
||||
Debug.WriteLine($"{executorComplete.ExecutorId}: {executorComplete.Data}");
|
||||
Debug.WriteLine($"{executorCompleted.ExecutorId}: {executorCompleted.Data}");
|
||||
}
|
||||
else if (evt is AgentRunUpdateEvent update)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user