mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
refactor: [BREAKING] Remove generic Workflow<T> (#1551)
Remove input type checking in favour of explicit `.DescribeProtocolAsync()` flow. Also removes `.AsAgentAsync()` as the validation happens at workflow run time. This makes it easier to use Workflows with DI without resorting to async-over-sync.
This commit is contained in:
committed by
GitHub
Unverified
parent
7c1e3db846
commit
0ef4e739d5
@@ -60,10 +60,7 @@ builder.AddAIAgent("knights-and-knaves", (sp, key) =>
|
||||
If the user asks a general question about their surrounding, make something up which is consistent with the scenario.
|
||||
""", "Narrator");
|
||||
|
||||
// TODO: How to avoid sync-over-async here?
|
||||
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
|
||||
return AgentWorkflowBuilder.BuildConcurrent([knight, knave, narrator]).AsAgentAsync(name: key).AsTask().GetAwaiter().GetResult();
|
||||
#pragma warning restore VSTHRD002
|
||||
return AgentWorkflowBuilder.BuildConcurrent([knight, knave, narrator]).AsAgent(name: key);
|
||||
});
|
||||
|
||||
// Workflow consisting of multiple specialized agents
|
||||
|
||||
@@ -48,7 +48,7 @@ public static class Program
|
||||
.Build();
|
||||
|
||||
// Execute the workflow
|
||||
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, "Create a slogan for a new electric SUV that is affordable and fun to drive.");
|
||||
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, input: "Create a slogan for a new electric SUV that is affordable and fun to drive.");
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
|
||||
{
|
||||
if (evt is SloganGeneratedEvent or FeedbackEvent)
|
||||
|
||||
@@ -35,7 +35,7 @@ public static class Program
|
||||
var chatClient = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()).GetChatClient(deploymentName).AsIChatClient();
|
||||
|
||||
// Create the workflow and turn it into an agent
|
||||
var workflow = await WorkflowHelper.GetWorkflowAsync(chatClient);
|
||||
var workflow = WorkflowHelper.GetWorkflow(chatClient);
|
||||
var agent = workflow.AsAgent("workflow-agent", "Workflow Agent");
|
||||
var thread = agent.GetNewThread();
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ internal static class WorkflowHelper
|
||||
/// </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 ValueTask<Workflow<List<ChatMessage>>> GetWorkflowAsync(IChatClient chatClient)
|
||||
internal static Workflow GetWorkflow(IChatClient chatClient)
|
||||
{
|
||||
// Create executors
|
||||
var startExecutor = new ConcurrentStartExecutor();
|
||||
@@ -26,7 +26,7 @@ internal static class WorkflowHelper
|
||||
.AddFanOutEdge(startExecutor, targets: [frenchAgent, englishAgent])
|
||||
.AddFanInEdge(aggregationExecutor, sources: [frenchAgent, englishAgent])
|
||||
.WithOutputFrom(aggregationExecutor)
|
||||
.BuildAsync<List<ChatMessage>>();
|
||||
.Build();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+2
-2
@@ -25,7 +25,7 @@ public static class Program
|
||||
private static async Task Main()
|
||||
{
|
||||
// Create the workflow
|
||||
var workflow = await WorkflowHelper.GetWorkflowAsync();
|
||||
var workflow = WorkflowHelper.GetWorkflow();
|
||||
|
||||
// Create checkpoint manager
|
||||
var checkpointManager = CheckpointManager.Default;
|
||||
@@ -67,7 +67,7 @@ public static class Program
|
||||
Console.WriteLine($"Number of checkpoints created: {checkpoints.Count}");
|
||||
|
||||
// Rehydrate a new workflow instance from a saved checkpoint and continue execution
|
||||
var newWorkflow = await WorkflowHelper.GetWorkflowAsync();
|
||||
var newWorkflow = WorkflowHelper.GetWorkflow();
|
||||
const int CheckpointIndex = 5;
|
||||
Console.WriteLine($"\n\nHydrating a new workflow instance from the {CheckpointIndex + 1}th checkpoint.");
|
||||
CheckpointInfo savedCheckpoint = checkpoints[CheckpointIndex];
|
||||
|
||||
+2
-2
@@ -13,7 +13,7 @@ internal static class WorkflowHelper
|
||||
/// 2. JudgeExecutor: Evaluates the guess and provides feedback.
|
||||
/// The workflow continues until the correct number is guessed.
|
||||
/// </summary>
|
||||
internal static ValueTask<Workflow<NumberSignal>> GetWorkflowAsync()
|
||||
internal static Workflow GetWorkflow()
|
||||
{
|
||||
// Create the executors
|
||||
GuessNumberExecutor guessNumberExecutor = new(1, 100);
|
||||
@@ -24,7 +24,7 @@ internal static class WorkflowHelper
|
||||
.AddEdge(guessNumberExecutor, judgeExecutor)
|
||||
.AddEdge(judgeExecutor, guessNumberExecutor)
|
||||
.WithOutputFrom(judgeExecutor)
|
||||
.BuildAsync<NumberSignal>();
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ public static class Program
|
||||
private static async Task Main()
|
||||
{
|
||||
// Create the workflow
|
||||
var workflow = await WorkflowHelper.GetWorkflowAsync();
|
||||
var workflow = WorkflowHelper.GetWorkflow();
|
||||
|
||||
// Create checkpoint manager
|
||||
var checkpointManager = CheckpointManager.Default;
|
||||
|
||||
+2
-2
@@ -13,7 +13,7 @@ internal static class WorkflowHelper
|
||||
/// 2. JudgeExecutor: Evaluates the guess and provides feedback.
|
||||
/// The workflow continues until the correct number is guessed.
|
||||
/// </summary>
|
||||
internal static ValueTask<Workflow<NumberSignal>> GetWorkflowAsync()
|
||||
internal static Workflow GetWorkflow()
|
||||
{
|
||||
// Create the executors
|
||||
GuessNumberExecutor guessNumberExecutor = new(1, 100);
|
||||
@@ -24,7 +24,7 @@ internal static class WorkflowHelper
|
||||
.AddEdge(guessNumberExecutor, judgeExecutor)
|
||||
.AddEdge(judgeExecutor, guessNumberExecutor)
|
||||
.WithOutputFrom(judgeExecutor)
|
||||
.BuildAsync<NumberSignal>();
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ public static class Program
|
||||
private static async Task Main()
|
||||
{
|
||||
// Create the workflow
|
||||
var workflow = await WorkflowHelper.GetWorkflowAsync();
|
||||
var workflow = WorkflowHelper.GetWorkflow();
|
||||
|
||||
// Create checkpoint manager
|
||||
var checkpointManager = CheckpointManager.Default;
|
||||
|
||||
+2
-2
@@ -10,7 +10,7 @@ internal static class WorkflowHelper
|
||||
/// 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 ValueTask<Workflow<SignalWithNumber>> GetWorkflowAsync()
|
||||
internal static Workflow GetWorkflow()
|
||||
{
|
||||
// Create the executors
|
||||
RequestPort numberRequest = RequestPort.Create<SignalWithNumber, int>("GuessNumber");
|
||||
@@ -21,7 +21,7 @@ internal static class WorkflowHelper
|
||||
.AddEdge(numberRequest, judgeExecutor)
|
||||
.AddEdge(judgeExecutor, numberRequest)
|
||||
.WithOutputFrom(judgeExecutor)
|
||||
.BuildAsync<SignalWithNumber>();
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ public static class Program
|
||||
.Build();
|
||||
|
||||
// Execute the workflow in streaming mode
|
||||
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, "What is temperature?");
|
||||
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, input: "What is temperature?");
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
|
||||
{
|
||||
if (evt is WorkflowOutputEvent output)
|
||||
|
||||
@@ -99,7 +99,7 @@ public static class Program
|
||||
|
||||
// Step 2: Run the workflow
|
||||
Console.WriteLine("\n=== RUNNING WORKFLOW ===\n");
|
||||
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, rawText);
|
||||
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, input: rawText);
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
|
||||
{
|
||||
Console.WriteLine($"Event: {evt}");
|
||||
|
||||
@@ -44,7 +44,7 @@ internal sealed class Program
|
||||
|
||||
// Run the workflow, just like any other workflow
|
||||
string input = this.GetWorkflowInput();
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflow, input);
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflow, input: input);
|
||||
await this.MonitorAndDisposeWorkflowRunAsync(run);
|
||||
|
||||
Notify("\nWORKFLOW: Done!");
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ public static class Program
|
||||
private static async Task Main()
|
||||
{
|
||||
// Create the workflow
|
||||
var workflow = await WorkflowHelper.GetWorkflowAsync();
|
||||
var workflow = WorkflowHelper.GetWorkflow();
|
||||
|
||||
// Execute the workflow
|
||||
await using StreamingRun handle = await InProcessExecution.StreamAsync(workflow, NumberSignal.Init);
|
||||
|
||||
+2
-2
@@ -10,7 +10,7 @@ internal static class WorkflowHelper
|
||||
/// 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 ValueTask<Workflow<NumberSignal>> GetWorkflowAsync()
|
||||
internal static Workflow GetWorkflow()
|
||||
{
|
||||
// Create the executors
|
||||
RequestPort numberRequestPort = RequestPort.Create<NumberSignal, int>("GuessNumber");
|
||||
@@ -21,7 +21,7 @@ internal static class WorkflowHelper
|
||||
.AddEdge(numberRequestPort, judgeExecutor)
|
||||
.AddEdge(judgeExecutor, numberRequestPort)
|
||||
.WithOutputFrom(judgeExecutor)
|
||||
.BuildAsync<NumberSignal>();
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,11 +25,11 @@ public static class Program
|
||||
JudgeExecutor judgeExecutor = new("Judge", 42);
|
||||
|
||||
// Build the workflow by connecting executors in a loop
|
||||
var workflow = await new WorkflowBuilder(guessNumberExecutor)
|
||||
var workflow = new WorkflowBuilder(guessNumberExecutor)
|
||||
.AddEdge(guessNumberExecutor, judgeExecutor)
|
||||
.AddEdge(judgeExecutor, guessNumberExecutor)
|
||||
.WithOutputFrom(judgeExecutor)
|
||||
.BuildAsync<NumberSignal>();
|
||||
.Build();
|
||||
|
||||
// Execute the workflow
|
||||
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, NumberSignal.Init);
|
||||
|
||||
@@ -28,7 +28,7 @@ public static class Program
|
||||
var workflow = builder.Build();
|
||||
|
||||
// Execute the workflow in streaming mode
|
||||
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, "Hello, World!");
|
||||
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, input: "Hello, World!");
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
|
||||
{
|
||||
if (evt is ExecutorCompletedEvent executorCompleted)
|
||||
|
||||
+1
-2
@@ -57,8 +57,7 @@ AIAgent reporter = new ChatClientAgent(anthropic,
|
||||
description: "Summarize the researcher's essay into a single paragraph, focusing only on the fact checker's confirmed facts.");
|
||||
|
||||
// Build a sequential workflow: Researcher -> Fact-Checker -> Reporter
|
||||
AIAgent workflowAgent = await AgentWorkflowBuilder.BuildSequential(researcher, factChecker, reporter)
|
||||
.AsAgentAsync();
|
||||
AIAgent workflowAgent = AgentWorkflowBuilder.BuildSequential(researcher, factChecker, reporter).AsAgent();
|
||||
|
||||
// Run the workflow, streaming the output as it arrives.
|
||||
string? lastAuthor = null;
|
||||
|
||||
@@ -27,12 +27,7 @@ public static class HostedWorkflowBuilderExtensions
|
||||
public static IHostedAgentBuilder AddAsAIAgent(this IHostedWorkflowBuilder builder, string? name)
|
||||
{
|
||||
var agentName = name ?? builder.Name;
|
||||
return builder.HostApplicationBuilder.AddAIAgent(agentName, (sp, key) =>
|
||||
{
|
||||
var workflow = sp.GetRequiredKeyedService<Workflow>(key);
|
||||
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
|
||||
return workflow.AsAgentAsync(name: key).AsTask().GetAwaiter().GetResult();
|
||||
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
|
||||
});
|
||||
return builder.HostApplicationBuilder.AddAIAgent(agentName, (sp, key) => sp.GetRequiredKeyedService<Workflow>(key)
|
||||
.AsAgent(name: key));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace Microsoft.Agents.AI.Workflows;
|
||||
public static partial class AgentWorkflowBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds a <see cref="Workflow{T}"/> composed of a pipeline of agents where the output of one agent is the input to the next.
|
||||
/// Builds a <see cref="Workflow"/> composed of a pipeline of agents where the output of one agent is the input to the next.
|
||||
/// </summary>
|
||||
/// <param name="agents">The sequence of agents to compose into a sequential workflow.</param>
|
||||
/// <returns>The built workflow composed of the supplied <paramref name="agents"/>, in the order in which they were yielded from the source.</returns>
|
||||
@@ -24,7 +24,7 @@ public static partial class AgentWorkflowBuilder
|
||||
=> BuildSequentialCore(workflowName: null, agents);
|
||||
|
||||
/// <summary>
|
||||
/// Builds a <see cref="Workflow{T}"/> composed of a pipeline of agents where the output of one agent is the input to the next.
|
||||
/// Builds a <see cref="Workflow"/> composed of a pipeline of agents where the output of one agent is the input to the next.
|
||||
/// </summary>
|
||||
/// <param name="workflowName">The name of workflow.</param>
|
||||
/// <param name="agents">The sequence of agents to compose into a sequential workflow.</param>
|
||||
@@ -76,7 +76,7 @@ public static partial class AgentWorkflowBuilder
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a <see cref="Workflow{T}"/> composed of agents that operate concurrently on the same input,
|
||||
/// Builds a <see cref="Workflow"/> composed of agents that operate concurrently on the same input,
|
||||
/// aggregating their outputs into a single collection.
|
||||
/// </summary>
|
||||
/// <param name="agents">The set of agents to compose into a concurrent workflow.</param>
|
||||
@@ -92,7 +92,7 @@ public static partial class AgentWorkflowBuilder
|
||||
=> BuildConcurrentCore(workflowName: null, agents, aggregator);
|
||||
|
||||
/// <summary>
|
||||
/// Builds a <see cref="Workflow{T}"/> composed of agents that operate concurrently on the same input,
|
||||
/// Builds a <see cref="Workflow"/> composed of agents that operate concurrently on the same input,
|
||||
/// aggregating their outputs into a single collection.
|
||||
/// </summary>
|
||||
/// <param name="workflowName">The name of the workflow.</param>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for determining and enforcing whether a protocol descriptor represents the Agent Workflow
|
||||
/// Chat Protocol.
|
||||
///
|
||||
/// This is defined as supporting a <see cref="List{ChatMessage}"/> and <see cref="TurnToken"/> as input. Optional support
|
||||
/// for additional <see cref="ChatMessage"/> payloads (e.g. string, when a default role is defined), or other collections of
|
||||
/// messages are optional to support.
|
||||
/// </summary>
|
||||
public static class ChatProtocolExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether the specified protocol descriptor represents the Agent Workflow Chat Protocol.
|
||||
/// </summary>
|
||||
/// <param name="descriptor">The protocol descriptor to evaluate.</param>
|
||||
/// <returns><see langword="true"/> if the protocol descriptor represents a supported chat protocol; otherwise, <see
|
||||
/// langword="false"/>.</returns>
|
||||
public static bool IsChatProtocol(this ProtocolDescriptor descriptor)
|
||||
{
|
||||
bool foundListChatMessageInput = false;
|
||||
bool foundTurnTokenInput = false;
|
||||
|
||||
// We require that the workflow be a ChatProtocol; right now that is defined as accepting at
|
||||
// least List<ChatMessage> as input (pending polymorphism/interface-input support), as well as
|
||||
// TurnToken. Since output is mediated by events, which we forward, we don't need to validate
|
||||
// output type.
|
||||
foreach (Type inputType in descriptor.Accepts)
|
||||
{
|
||||
if (inputType == typeof(List<ChatMessage>))
|
||||
{
|
||||
foundListChatMessageInput = true;
|
||||
}
|
||||
else if (inputType == typeof(TurnToken))
|
||||
{
|
||||
foundTurnTokenInput = true;
|
||||
}
|
||||
}
|
||||
|
||||
return foundListChatMessageInput && foundTurnTokenInput;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an exception if the specified protocol descriptor does not represent a valid chat protocol.
|
||||
/// </summary>
|
||||
/// <param name="descriptor">The protocol descriptor to validate as a chat protocol. Cannot be null.</param>
|
||||
public static void ThrowIfNotChatProtocol(this ProtocolDescriptor descriptor)
|
||||
{
|
||||
if (!descriptor.IsChatProtocol())
|
||||
{
|
||||
throw new InvalidOperationException("Workflow does not support ChatProtocol: At least List<ChatMessage>" +
|
||||
" and TurnToken must be supported as input.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ internal static class RepresentationExtensions
|
||||
return new(new TypeId(port.Request), new TypeId(port.Response), port.Id);
|
||||
}
|
||||
|
||||
private static WorkflowInfo ToWorkflowInfo(this Workflow workflow, TypeId? inputType, TypeId? outputType, string? outputExecutorId)
|
||||
public static WorkflowInfo ToWorkflowInfo(this Workflow workflow)
|
||||
{
|
||||
Throw.IfNull(workflow);
|
||||
|
||||
@@ -48,12 +48,6 @@ internal static class RepresentationExtensions
|
||||
|
||||
HashSet<RequestPortInfo> inputPorts = new(workflow.Ports.Values.Select(ToPortInfo));
|
||||
|
||||
return new WorkflowInfo(executors, edges, inputPorts, inputType, workflow.StartExecutorId, workflow.OutputExecutors);
|
||||
return new WorkflowInfo(executors, edges, inputPorts, workflow.StartExecutorId, workflow.OutputExecutors);
|
||||
}
|
||||
|
||||
public static WorkflowInfo ToWorkflowInfo(this Workflow workflow)
|
||||
=> workflow.ToWorkflowInfo(inputType: null, outputType: null, outputExecutorId: null);
|
||||
|
||||
public static WorkflowInfo ToWorkflowInfo<TInput>(this Workflow<TInput> workflow)
|
||||
=> workflow.ToWorkflowInfo(inputType: new(workflow.InputType), outputType: null, outputExecutorId: null);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ internal sealed class WorkflowInfo
|
||||
Dictionary<string, ExecutorInfo> executors,
|
||||
Dictionary<string, List<EdgeInfo>> edges,
|
||||
HashSet<RequestPortInfo> requestPorts,
|
||||
TypeId? inputType,
|
||||
string startExecutorId,
|
||||
HashSet<string>? outputExecutorIds)
|
||||
{
|
||||
@@ -22,7 +21,6 @@ internal sealed class WorkflowInfo
|
||||
this.Edges = Throw.IfNull(edges);
|
||||
this.RequestPorts = Throw.IfNull(requestPorts);
|
||||
|
||||
this.InputType = inputType;
|
||||
this.StartExecutorId = Throw.IfNullOrEmpty(startExecutorId);
|
||||
this.OutputExecutorIds = outputExecutorIds ?? [];
|
||||
}
|
||||
@@ -91,7 +89,4 @@ internal sealed class WorkflowInfo
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool IsMatch<TInput>(Workflow<TInput> workflow) =>
|
||||
this.IsMatch(workflow as Workflow) && this.InputType?.IsMatch<TInput>() == true;
|
||||
}
|
||||
|
||||
@@ -200,6 +200,18 @@ public abstract class Executor : IIdentified
|
||||
/// </summary>
|
||||
public ISet<Type> OutputTypes { get; } = new HashSet<Type>([typeof(object)]);
|
||||
|
||||
/// <summary>
|
||||
/// Describes the protocol for communication with this <see cref="Executor"/>.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public ProtocolDescriptor DescribeProtocol()
|
||||
{
|
||||
// TODO: Once burden of annotating yield/output messages becomes easier for the non-Auto case,
|
||||
// we should (1) start checking for validity on output/send side, and (2) add the Yield/Send
|
||||
// types to the ProtocolDescriptor.
|
||||
return new(this.InputTypes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the executor can handle a specific message type.
|
||||
/// </summary>
|
||||
|
||||
@@ -21,9 +21,8 @@ public static class ExecutorIshConfigurationExtensions
|
||||
/// Note that Executor Ids must be unique within a workflow.
|
||||
///
|
||||
/// Although this will generally result in a delay-instantiated <see cref="Executor"/> once messages are available
|
||||
/// for it, if this is used as a start node of a typed <see cref="Workflow{TInput}"/> via <see cref="Workflow.TryPromoteAsync{TInput}"/>,
|
||||
/// it will be instantiated as part of the workflow's construction, to validate that its input type matches the
|
||||
/// demanded <c>TInput</c>.
|
||||
/// for it, it will be instantiated if a <see cref="ProtocolDescriptor"/> for the <see cref="Workflow"/> is requested,
|
||||
/// and it is the starting executor.
|
||||
/// </remarks>
|
||||
/// <typeparam name="TExecutor">The type of the resulting executor</typeparam>
|
||||
/// <param name="factoryAsync">The factory method.</param>
|
||||
@@ -38,9 +37,8 @@ public static class ExecutorIshConfigurationExtensions
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Although this will generally result in a delay-instantiated <see cref="Executor"/> once messages are available
|
||||
/// for it, if this is used as a start node of a typed <see cref="Workflow{TInput}"/> via <see cref="Workflow.TryPromoteAsync{TInput}"/>,
|
||||
/// it will be instantiated as part of the workflow's construction, to validate that its input type matches the
|
||||
/// demanded <c>TInput</c>.
|
||||
/// for it, it will be instantiated if a <see cref="ProtocolDescriptor"/> for the <see cref="Workflow"/> is requested,
|
||||
/// and it is the starting executor.
|
||||
/// </remarks>
|
||||
/// <typeparam name="TExecutor">The type of the resulting executor</typeparam>
|
||||
/// <param name="factoryAsync">The factory method.</param>
|
||||
@@ -56,9 +54,8 @@ public static class ExecutorIshConfigurationExtensions
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Although this will generally result in a delay-instantiated <see cref="Executor"/> once messages are available
|
||||
/// for it, if this is used as a start node of a typed <see cref="Workflow{TInput}"/> via <see cref="Workflow.TryPromoteAsync{TInput}"/>,
|
||||
/// it will be instantiated as part of the workflow's construction, to validate that its input type matches the
|
||||
/// demanded <c>TInput</c>.
|
||||
/// for it, it will be instantiated if a <see cref="ProtocolDescriptor"/> for the <see cref="Workflow"/> is requested,
|
||||
/// and it is the starting executor.
|
||||
/// </remarks>
|
||||
/// <typeparam name="TExecutor">The type of the resulting executor</typeparam>
|
||||
/// <typeparam name="TOptions">The type of options object to be passed to the factory method.</typeparam>
|
||||
|
||||
@@ -139,7 +139,7 @@ public sealed class HandoffsWorkflowBuilder
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a <see cref="Workflow{T}"/> composed of agents that operate via handoffs, with the next
|
||||
/// Builds a <see cref="Workflow"/> composed of agents that operate via handoffs, with the next
|
||||
/// agent to process messages selected by the current agent.
|
||||
/// </summary>
|
||||
/// <returns>The workflow built based on the handoffs in the builder.</returns>
|
||||
|
||||
@@ -11,6 +11,16 @@ namespace Microsoft.Agents.AI.Workflows;
|
||||
/// </summary>
|
||||
public interface IWorkflowExecutionEnvironment
|
||||
{
|
||||
/// <summary>
|
||||
/// Initiates a streaming run of the specified workflow without sending any initial input.
|
||||
/// </summary>
|
||||
/// <param name="workflow">The workflow to execute. Cannot be null.</param>
|
||||
/// <param name="runId">An optional identifier for the run. If null, a new run identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the streaming operation.</param>
|
||||
/// <returns>A ValueTask that represents the asynchronous operation. The result contains a StreamingRun object for accessing
|
||||
/// the streamed workflow output.</returns>
|
||||
ValueTask<StreamingRun> StreamAsync(Workflow workflow, string? runId = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Initiates an asynchronous streaming execution using the specified input.
|
||||
/// </summary>
|
||||
@@ -21,25 +31,24 @@ public interface IWorkflowExecutionEnvironment
|
||||
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
|
||||
/// <param name="input">The input message to be processed as part of the streaming run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{StreamingRun}"/> that represents the asynchronous operation. The result contains a <see
|
||||
/// cref="StreamingRun"/> for managing and interacting with the streaming run.</returns>
|
||||
ValueTask<StreamingRun> StreamAsync<TInput>(Workflow workflow, TInput input, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull;
|
||||
|
||||
/// <summary>
|
||||
/// Initiates an asynchronous streaming execution using the specified input.
|
||||
/// Initiates an asynchronous streaming execution without sending any initial input, with checkpointing.
|
||||
/// </summary>
|
||||
/// <remarks>The returned <see cref="StreamingRun"/> provides methods to observe and control
|
||||
/// the ongoing streaming execution. The operation will continue until the streaming execution is finished or
|
||||
/// cancelled.</remarks>
|
||||
/// <typeparam name="TInput">A type of input accepted by the workflow. Must be non-nullable.</typeparam>
|
||||
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
|
||||
/// <param name="input">The input message to be processed as part of the streaming run.</param>
|
||||
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{StreamingRun}"/> that represents the asynchronous operation. The result contains a <see
|
||||
/// cref="StreamingRun"/> for managing and interacting with the streaming run.</returns>
|
||||
ValueTask<StreamingRun> StreamAsync<TInput>(Workflow<TInput> workflow, TInput input, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull;
|
||||
ValueTask<Checkpointed<StreamingRun>> StreamAsync(Workflow workflow, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Initiates an asynchronous streaming execution using the specified input, with checkpointing.
|
||||
@@ -52,27 +61,11 @@ public interface IWorkflowExecutionEnvironment
|
||||
/// <param name="input">The input message to be processed as part of the streaming run.</param>
|
||||
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{StreamingRun}"/> that represents the asynchronous operation. The result contains a <see
|
||||
/// cref="StreamingRun"/> for managing and interacting with the streaming run.</returns>
|
||||
ValueTask<Checkpointed<StreamingRun>> StreamAsync<TInput>(Workflow workflow, TInput input, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull;
|
||||
|
||||
/// <summary>
|
||||
/// Initiates an asynchronous streaming execution using the specified input, with checkpointing.
|
||||
/// </summary>
|
||||
/// <remarks>The returned <see cref="StreamingRun"/> provides methods to observe and control
|
||||
/// the ongoing streaming execution. The operation will continue until the streaming execution is finished or
|
||||
/// cancelled.</remarks>
|
||||
/// <typeparam name="TInput">The type of input accepted by the workflow. Must be non-nullable.</typeparam>
|
||||
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
|
||||
/// <param name="input">The input message to be processed as part of the streaming run.</param>
|
||||
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{StreamingRun}"/> that represents the asynchronous operation. The result contains a <see
|
||||
/// cref="StreamingRun"/> for managing and interacting with the streaming run.</returns>
|
||||
ValueTask<Checkpointed<StreamingRun>> StreamAsync<TInput>(Workflow<TInput> workflow, TInput input, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull;
|
||||
|
||||
/// <summary>
|
||||
/// Resumes an asynchronous streaming execution for the specified input from a checkpoint.
|
||||
/// </summary>
|
||||
@@ -82,24 +75,10 @@ public interface IWorkflowExecutionEnvironment
|
||||
/// <param name="fromCheckpoint">The <see cref="CheckpointInfo"/> corresponding to the checkpoint from which to resume.</param>
|
||||
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="StreamingRun"/> that provides access to the results of the streaming run.</returns>
|
||||
ValueTask<Checkpointed<StreamingRun>> ResumeStreamAsync(Workflow workflow, CheckpointInfo fromCheckpoint, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Resumes an asynchronous streaming execution for the specified input from a checkpoint.
|
||||
/// </summary>
|
||||
/// <remarks>If the operation is cancelled via the <paramref name="cancellationToken"/> token, the streaming execution will
|
||||
/// be terminated.</remarks>
|
||||
/// <typeparam name="TInput">The type of input accepted by the workflow. Must be non-nullable.</typeparam>
|
||||
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
|
||||
/// <param name="fromCheckpoint">The <see cref="CheckpointInfo"/> corresponding to the checkpoint from which to resume.</param>
|
||||
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="StreamingRun"/> that provides access to the results of the streaming run.</returns>
|
||||
ValueTask<Checkpointed<StreamingRun>> ResumeStreamAsync<TInput>(Workflow<TInput> workflow, CheckpointInfo fromCheckpoint, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull;
|
||||
|
||||
/// <summary>
|
||||
/// Initiates a non-streaming execution of the workflow with the specified input.
|
||||
/// </summary>
|
||||
@@ -109,25 +88,11 @@ public interface IWorkflowExecutionEnvironment
|
||||
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
|
||||
/// <param name="input">The input message to be processed as part of the run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{Run}"/> that represents the asynchronous operation. The result contains a <see
|
||||
/// cref="Run"/> for managing and interacting with the streaming run.</returns>
|
||||
ValueTask<Run> RunAsync<TInput>(Workflow workflow, TInput input, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull;
|
||||
|
||||
/// <summary>
|
||||
/// Initiates a non-streaming execution of the workflow with the specified input.
|
||||
/// </summary>
|
||||
/// <remarks>The workflow will run until its first halt, and the returned <see cref="Run"/> will capture
|
||||
/// all outgoing events. Use the <c>Run</c> instance to resume execution with responses to outgoing events.</remarks>
|
||||
/// <typeparam name="TInput">The type of input accepted by the workflow. Must be non-nullable.</typeparam>
|
||||
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
|
||||
/// <param name="input">The input message to be processed as part of the run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{Run}"/> that represents the asynchronous operation. The result contains a <see
|
||||
/// cref="Run"/> for managing and interacting with the streaming run.</returns>
|
||||
ValueTask<Run> RunAsync<TInput>(Workflow<TInput> workflow, TInput input, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull;
|
||||
|
||||
/// <summary>
|
||||
/// Initiates a non-streaming execution of the workflow with the specified input, with checkpointing.
|
||||
/// </summary>
|
||||
@@ -138,26 +103,11 @@ public interface IWorkflowExecutionEnvironment
|
||||
/// <param name="input">The input message to be processed as part of the run.</param>
|
||||
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{Run}"/> that represents the asynchronous operation. The result contains a <see
|
||||
/// cref="Run"/> for managing and interacting with the streaming run.</returns>
|
||||
ValueTask<Checkpointed<Run>> RunAsync<TInput>(Workflow workflow, TInput input, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull;
|
||||
|
||||
/// <summary>
|
||||
/// Initiates a non-streaming execution of the workflow with the specified input, with checkpointing.
|
||||
/// </summary>
|
||||
/// <remarks>The workflow will run until its first halt, and the returned <see cref="Run"/> will capture
|
||||
/// all outgoing events. Use the <c>Run</c> instance to resume execution with responses to outgoing events.</remarks>
|
||||
/// <typeparam name="TInput">The type of input accepted by the workflow. Must be non-nullable.</typeparam>
|
||||
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
|
||||
/// <param name="input">The input message to be processed as part of the run.</param>
|
||||
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{Run}"/> that represents the asynchronous operation. The result contains a <see
|
||||
/// cref="Run"/> for managing and interacting with the streaming run.</returns>
|
||||
ValueTask<Checkpointed<Run>> RunAsync<TInput>(Workflow<TInput> workflow, TInput input, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull;
|
||||
|
||||
/// <summary>
|
||||
/// Resumes a non-streaming execution of the workflow from a checkpoint.
|
||||
/// </summary>
|
||||
@@ -167,22 +117,8 @@ public interface IWorkflowExecutionEnvironment
|
||||
/// <param name="fromCheckpoint">The <see cref="CheckpointInfo"/> corresponding to the checkpoint from which to resume.</param>
|
||||
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{Run}"/> that represents the asynchronous operation. The result contains a <see
|
||||
/// cref="Run"/> for managing and interacting with the streaming run.</returns>
|
||||
ValueTask<Checkpointed<Run>> ResumeAsync(Workflow workflow, CheckpointInfo fromCheckpoint, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Resumes a non-streaming execution of the workflow from a checkpoint.
|
||||
/// </summary>
|
||||
/// <remarks>The workflow will run until its first halt, and the returned <see cref="Run"/> will capture
|
||||
/// all outgoing events. Use the <c>Run</c> instance to resume execution with responses to outgoing events.</remarks>
|
||||
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
|
||||
/// <param name="fromCheckpoint">The <see cref="CheckpointInfo"/> corresponding to the checkpoint from which to resume.</param>
|
||||
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{Run}"/> that represents the asynchronous operation. The result contains a <see
|
||||
/// cref="Run"/> for managing and interacting with the streaming run.</returns>
|
||||
ValueTask<Checkpointed<Run>> ResumeAsync<TInput>(Workflow<TInput> workflow, CheckpointInfo fromCheckpoint, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,18 @@ public sealed class InProcessExecutionEnvironment : IWorkflowExecutionEnvironmen
|
||||
return runner.ResumeStreamAsync(this.ExecutionMode, fromCheckpoint, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<StreamingRun> StreamAsync(
|
||||
Workflow workflow,
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
AsyncRunHandle runHandle = await this.BeginRunAsync(workflow, checkpointManager: null, runId: runId, [], cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return new(runHandle);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<StreamingRun> StreamAsync<TInput>(
|
||||
Workflow workflow,
|
||||
@@ -50,16 +62,17 @@ public sealed class InProcessExecutionEnvironment : IWorkflowExecutionEnvironmen
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<StreamingRun> StreamAsync<TInput>(
|
||||
Workflow<TInput> workflow,
|
||||
TInput input,
|
||||
public async ValueTask<Checkpointed<StreamingRun>> StreamAsync(
|
||||
Workflow workflow,
|
||||
CheckpointManager checkpointManager,
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default) where TInput : notnull
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
AsyncRunHandle runHandle = await this.BeginRunAsync(workflow, checkpointManager: null, runId: runId, [typeof(TInput)], cancellationToken)
|
||||
AsyncRunHandle runHandle = await this.BeginRunAsync(workflow, checkpointManager, runId: runId, [], cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return await runHandle.EnqueueAndStreamAsync(input, cancellationToken).ConfigureAwait(false);
|
||||
return await runHandle.WithCheckpointingAsync<StreamingRun>(() => new(new StreamingRun(runHandle)))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -77,21 +90,6 @@ public sealed class InProcessExecutionEnvironment : IWorkflowExecutionEnvironmen
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<Checkpointed<StreamingRun>> StreamAsync<TInput>(
|
||||
Workflow<TInput> workflow,
|
||||
TInput input,
|
||||
CheckpointManager checkpointManager,
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default) where TInput : notnull
|
||||
{
|
||||
AsyncRunHandle runHandle = await this.BeginRunAsync(workflow, checkpointManager, runId: runId, [typeof(TInput)], cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return await runHandle.WithCheckpointingAsync(() => runHandle.EnqueueAndStreamAsync(input, cancellationToken))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<Checkpointed<StreamingRun>> ResumeStreamAsync(
|
||||
Workflow workflow,
|
||||
@@ -107,19 +105,24 @@ public sealed class InProcessExecutionEnvironment : IWorkflowExecutionEnvironmen
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<Checkpointed<StreamingRun>> ResumeStreamAsync<TInput>(
|
||||
Workflow<TInput> workflow,
|
||||
CheckpointInfo fromCheckpoint,
|
||||
CheckpointManager checkpointManager,
|
||||
private async ValueTask<AsyncRunHandle> BeginRunHandlingChatProtocolAsync<TInput>(Workflow workflow,
|
||||
TInput input,
|
||||
CheckpointManager? checkpointManager,
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default) where TInput : notnull
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
AsyncRunHandle runHandle = await this.ResumeRunAsync(workflow, checkpointManager, runId: runId, fromCheckpoint, [typeof(TInput)], cancellationToken)
|
||||
ProtocolDescriptor descriptor = await workflow.DescribeProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||
AsyncRunHandle runHandle = await this.BeginRunAsync(workflow, checkpointManager, runId, descriptor.Accepts, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return await runHandle.WithCheckpointingAsync<StreamingRun>(() => new(new StreamingRun(runHandle)))
|
||||
.ConfigureAwait(false);
|
||||
await runHandle.EnqueueMessageAsync(input, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (descriptor.IsChatProtocol() && input is not TurnToken)
|
||||
{
|
||||
await runHandle.EnqueueMessageAsync(new TurnToken(emitEvents: true), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return runHandle;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -129,21 +132,13 @@ public sealed class InProcessExecutionEnvironment : IWorkflowExecutionEnvironmen
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default) where TInput : notnull
|
||||
{
|
||||
var runHandle = await this.GetRunHandleWithTurnTokenAsync(workflow: workflow, input: input, checkpointManager: null, runId: runId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
Run run = new(runHandle);
|
||||
await run.RunToNextHaltAsync(cancellationToken).ConfigureAwait(false);
|
||||
return run;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<Run> RunAsync<TInput>(
|
||||
Workflow<TInput> workflow,
|
||||
TInput input,
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default) where TInput : notnull
|
||||
{
|
||||
var runHandle = await this.GetRunHandleWithTurnTokenAsync(workflow: workflow, input: input, checkpointManager: null, runId: runId, cancellationToken).ConfigureAwait(false);
|
||||
AsyncRunHandle runHandle = await this.BeginRunHandlingChatProtocolAsync(
|
||||
workflow,
|
||||
input,
|
||||
checkpointManager: null,
|
||||
runId,
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
Run run = new(runHandle);
|
||||
await run.RunToNextHaltAsync(cancellationToken).ConfigureAwait(false);
|
||||
@@ -158,23 +153,13 @@ public sealed class InProcessExecutionEnvironment : IWorkflowExecutionEnvironmen
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default) where TInput : notnull
|
||||
{
|
||||
var runHandle = await this.GetRunHandleWithTurnTokenAsync(workflow: workflow, input: input, checkpointManager: checkpointManager, runId: runId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
Run run = new(runHandle);
|
||||
await run.RunToNextHaltAsync(cancellationToken).ConfigureAwait(false);
|
||||
return await runHandle.WithCheckpointingAsync(() => new ValueTask<Run>(run))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<Checkpointed<Run>> RunAsync<TInput>(
|
||||
Workflow<TInput> workflow,
|
||||
TInput input,
|
||||
CheckpointManager checkpointManager,
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default) where TInput : notnull
|
||||
{
|
||||
var runHandle = await this.GetRunHandleWithTurnTokenAsync(workflow: workflow, input: input, checkpointManager: checkpointManager, runId: runId, cancellationToken).ConfigureAwait(false);
|
||||
AsyncRunHandle runHandle = await this.BeginRunHandlingChatProtocolAsync(
|
||||
workflow,
|
||||
input,
|
||||
checkpointManager,
|
||||
runId,
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
Run run = new(runHandle);
|
||||
await run.RunToNextHaltAsync(cancellationToken).ConfigureAwait(false);
|
||||
@@ -196,63 +181,4 @@ public sealed class InProcessExecutionEnvironment : IWorkflowExecutionEnvironmen
|
||||
return await runHandle.WithCheckpointingAsync<Run>(() => new(new Run(runHandle)))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<Checkpointed<Run>> ResumeAsync<TInput>(
|
||||
Workflow<TInput> workflow,
|
||||
CheckpointInfo fromCheckpoint,
|
||||
CheckpointManager checkpointManager,
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default) where TInput : notnull
|
||||
{
|
||||
AsyncRunHandle runHandle = await this.ResumeRunAsync(workflow, checkpointManager, runId: runId, fromCheckpoint, [typeof(TInput)], cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return await runHandle.WithCheckpointingAsync<Run>(() => new(new Run(runHandle)))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Helper to construct a RunHandle with the provided input enqueued. If the starting executor supports it, a TurnToken will be enqueued also.
|
||||
private async ValueTask<AsyncRunHandle> GetRunHandleWithTurnTokenAsync<TInput>(
|
||||
Workflow workflow,
|
||||
TInput input,
|
||||
CheckpointManager? checkpointManager,
|
||||
string? runId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var knownTypes = new List<Type>() { typeof(TInput) };
|
||||
var needsTurnToken = await StartingExecutorHandlesTurnTokenAsync<TInput>(workflow).ConfigureAwait(false);
|
||||
if (needsTurnToken)
|
||||
{
|
||||
knownTypes.Add(typeof(TurnToken));
|
||||
}
|
||||
|
||||
AsyncRunHandle runHandle = await this.BeginRunAsync(workflow, checkpointManager: checkpointManager, runId: runId, knownTypes, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await runHandle.EnqueueMessageAsync(input, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (needsTurnToken)
|
||||
{
|
||||
await runHandle.EnqueueMessageAsync(new TurnToken(emitEvents: true), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return runHandle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to detect if the starting executor of a given workflow accepts the provided input type as well as a TurnToken.
|
||||
/// </summary>
|
||||
private static async ValueTask<bool> StartingExecutorHandlesTurnTokenAsync<TInput>(Workflow workflow)
|
||||
{
|
||||
if (workflow.Registrations.TryGetValue(workflow.StartExecutorId, out var registration))
|
||||
{
|
||||
// Create instance to check type
|
||||
Executor startExecutor = await registration.CreateInstanceAsync(string.Empty)
|
||||
.ConfigureAwait(false);
|
||||
return startExecutor.CanHandle(typeof(TInput)) && startExecutor.CanHandle(typeof(TurnToken));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,51 +41,35 @@ public static class InProcessExecution
|
||||
/// </summary>
|
||||
internal static InProcessExecutionEnvironment Subworkflow { get; } = new(ExecutionMode.Subworkflow);
|
||||
|
||||
/// <inheritdoc cref="IWorkflowExecutionEnvironment.StreamAsync(Workflow, string?, CancellationToken)"/>
|
||||
public static ValueTask<StreamingRun> StreamAsync(Workflow workflow, string? runId = null, CancellationToken cancellationToken = default)
|
||||
=> Default.StreamAsync(workflow, runId, cancellationToken);
|
||||
|
||||
/// <inheritdoc cref="IWorkflowExecutionEnvironment.StreamAsync{TInput}(Workflow, TInput, string?, CancellationToken)"/>
|
||||
public static ValueTask<StreamingRun> StreamAsync<TInput>(Workflow workflow, TInput input, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull
|
||||
=> Default.StreamAsync(workflow, input, runId, cancellationToken);
|
||||
|
||||
/// <inheritdoc cref="IWorkflowExecutionEnvironment.StreamAsync{TInput}(Workflow{TInput}, TInput, string?, CancellationToken)"/>
|
||||
public static ValueTask<StreamingRun> StreamAsync<TInput>(Workflow<TInput> workflow, TInput input, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull
|
||||
=> Default.StreamAsync(workflow, input, runId, cancellationToken);
|
||||
/// <inheritdoc cref="IWorkflowExecutionEnvironment.StreamAsync(Workflow, CheckpointManager, string?, CancellationToken)"/>
|
||||
public static ValueTask<Checkpointed<StreamingRun>> StreamAsync(Workflow workflow, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default)
|
||||
=> Default.StreamAsync(workflow, checkpointManager, runId, cancellationToken);
|
||||
|
||||
/// <inheritdoc cref="IWorkflowExecutionEnvironment.StreamAsync{TInput}(Workflow, TInput, CheckpointManager, string?, CancellationToken)"/>
|
||||
public static ValueTask<Checkpointed<StreamingRun>> StreamAsync<TInput>(Workflow workflow, TInput input, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull
|
||||
=> Default.StreamAsync(workflow, input, checkpointManager, runId, cancellationToken);
|
||||
|
||||
/// <inheritdoc cref="IWorkflowExecutionEnvironment.StreamAsync{TInput}(Workflow{TInput}, TInput, CheckpointManager, string?, CancellationToken)"/>
|
||||
public static ValueTask<Checkpointed<StreamingRun>> StreamAsync<TInput>(Workflow<TInput> workflow, TInput input, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull
|
||||
=> Default.StreamAsync(workflow, input, checkpointManager, runId, cancellationToken);
|
||||
|
||||
/// <inheritdoc cref="IWorkflowExecutionEnvironment.ResumeStreamAsync(Workflow, CheckpointInfo, CheckpointManager, string?, CancellationToken)"/>
|
||||
public static ValueTask<Checkpointed<StreamingRun>> ResumeStreamAsync(Workflow workflow, CheckpointInfo fromCheckpoint, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default)
|
||||
=> Default.ResumeStreamAsync(workflow, fromCheckpoint, checkpointManager, runId, cancellationToken);
|
||||
|
||||
/// <inheritdoc cref="IWorkflowExecutionEnvironment.ResumeStreamAsync{TInput}(Workflow{TInput}, CheckpointInfo, CheckpointManager, string?, CancellationToken)"/>
|
||||
public static ValueTask<Checkpointed<StreamingRun>> ResumeStreamAsync<TInput>(Workflow<TInput> workflow, CheckpointInfo fromCheckpoint, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull
|
||||
=> Default.ResumeStreamAsync(workflow, fromCheckpoint, checkpointManager, runId, cancellationToken);
|
||||
|
||||
/// <inheritdoc cref="IWorkflowExecutionEnvironment.RunAsync{TInput}(Workflow, TInput, string?, CancellationToken)"/>
|
||||
public static ValueTask<Run> RunAsync<TInput>(Workflow workflow, TInput input, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull
|
||||
=> Default.RunAsync(workflow, input, runId, cancellationToken);
|
||||
|
||||
/// <inheritdoc cref="IWorkflowExecutionEnvironment.RunAsync{TInput}(Workflow{TInput}, TInput, string?, CancellationToken)"/>
|
||||
public static ValueTask<Run> RunAsync<TInput>(Workflow<TInput> workflow, TInput input, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull
|
||||
=> Default.RunAsync(workflow, input, runId, cancellationToken);
|
||||
|
||||
/// <inheritdoc cref="IWorkflowExecutionEnvironment.RunAsync{TInput}(Workflow, TInput, CheckpointManager, string?, CancellationToken)"/>
|
||||
public static ValueTask<Checkpointed<Run>> RunAsync<TInput>(Workflow workflow, TInput input, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull
|
||||
=> Default.RunAsync(workflow, input, checkpointManager, runId, cancellationToken);
|
||||
|
||||
/// <inheritdoc cref="IWorkflowExecutionEnvironment.RunAsync{TInput}(Workflow{TInput}, TInput, CheckpointManager, string?, CancellationToken)"/>
|
||||
public static ValueTask<Checkpointed<Run>> RunAsync<TInput>(Workflow<TInput> workflow, TInput input, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull
|
||||
=> Default.RunAsync(workflow, input, checkpointManager, runId, cancellationToken);
|
||||
|
||||
/// <inheritdoc cref="IWorkflowExecutionEnvironment.ResumeAsync(Workflow, CheckpointInfo, CheckpointManager, string?, CancellationToken)"/>
|
||||
public static ValueTask<Checkpointed<Run>> ResumeAsync(Workflow workflow, CheckpointInfo fromCheckpoint, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default)
|
||||
=> Default.ResumeAsync(workflow, fromCheckpoint, checkpointManager, runId, cancellationToken);
|
||||
|
||||
/// <inheritdoc cref="IWorkflowExecutionEnvironment.ResumeAsync{TInput}(Workflow{TInput}, CheckpointInfo, CheckpointManager, string?, CancellationToken)"/>
|
||||
public static ValueTask<Checkpointed<Run>> ResumeAsync<TInput>(Workflow<TInput> workflow, CheckpointInfo fromCheckpoint, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull
|
||||
=> Default.ResumeAsync(workflow, fromCheckpoint, checkpointManager, runId, cancellationToken);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Describes the protocol for communication with a <see cref="Workflow"/> or <see cref="Executor"/>.
|
||||
/// </summary>
|
||||
public class ProtocolDescriptor
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the collection of types accepted by the <see cref="Workflow"/> or <see cref="Executor"/>.
|
||||
/// </summary>
|
||||
public IEnumerable<Type> Accepts { get; }
|
||||
|
||||
internal ProtocolDescriptor(IEnumerable<Type> acceptedTypes)
|
||||
{
|
||||
this.Accepts = acceptedTypes.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -85,42 +85,6 @@ public class Workflow
|
||||
this.Description = description;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to promote the current workflow to a type pre-checked instance that can handle input of type <typeparamref name="TInput"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInput">The desired input type.</typeparam>
|
||||
/// <returns>A type-parametrized workflow definitely able to process input of type <typeparamref name="TInput"/> or
|
||||
/// <see langword="null" /> if the workflow does not accept that type of input.</returns>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
internal async ValueTask<Workflow<TInput>?> TryPromoteAsync<TInput>()
|
||||
{
|
||||
// Grab the start node, and make sure it has the right type?
|
||||
if (!this.Registrations.TryGetValue(this.StartExecutorId, out ExecutorRegistration? startRegistration))
|
||||
{
|
||||
// TODO: This should never be able to be hit
|
||||
throw new InvalidOperationException($"Start executor with ID '{this.StartExecutorId}' is not bound.");
|
||||
}
|
||||
|
||||
// TODO: Can we cache this somehow to avoid having to instantiate a new one when running?
|
||||
// Does that break some user expectations?
|
||||
Executor startExecutor = await startRegistration.CreateInstanceAsync(string.Empty).ConfigureAwait(false);
|
||||
|
||||
if (!startExecutor.InputTypes.Any(t => t.IsAssignableFrom(typeof(TInput))))
|
||||
{
|
||||
// We have no handlers for the input type T, which means the built workflow will not be able to
|
||||
// process messages of the desired type
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Workflow<TInput>(this.StartExecutorId)
|
||||
{
|
||||
Registrations = this.Registrations,
|
||||
Edges = this.Edges,
|
||||
Ports = this.Ports,
|
||||
OutputExecutors = this.OutputExecutors
|
||||
};
|
||||
}
|
||||
|
||||
private bool _needsReset;
|
||||
private bool IsResettable => this.Registrations.Values.All(registration => !registration.IsUnresettableSharedInstance);
|
||||
|
||||
@@ -215,27 +179,18 @@ public class Workflow
|
||||
|
||||
await this.TryResetExecutorRegistrationsAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a workflow that operates on data of type <typeparamref name="T"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of input to the workflow.</typeparam>
|
||||
public class Workflow<T> : Workflow
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Workflow{T}"/> class with the specified starting executor identifier
|
||||
/// Retrieves a <see cref="ProtocolDescriptor"/> defining how to interact with this workflow.
|
||||
/// </summary>
|
||||
/// <param name="startExecutorId">The unique identifier of the starting executor for the workflow. Cannot be <c>null</c>.</param>
|
||||
/// <param name="name">Optional human-readable name for the workflow.</param>
|
||||
/// <param name="description">Optional description of what the workflow does.</param>
|
||||
public Workflow(string startExecutorId, string? name = null, string? description = null)
|
||||
: base(startExecutorId, name, description)
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{ProtocolDescriptor}"/> that represents that asynchronous operation. The result contains
|
||||
/// a <see cref="ProtocolDescriptor"/> the protocol this <see cref="Workflow"/> follows.</returns>
|
||||
public async ValueTask<ProtocolDescriptor> DescribeProtocolAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
ExecutorRegistration startExecutorRegistration = this.Registrations[this.StartExecutorId];
|
||||
Executor startExecutor = await startExecutorRegistration.CreateInstanceAsync(string.Empty)
|
||||
.ConfigureAwait(false);
|
||||
return startExecutor.DescribeProtocol();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of input expected by the starting executor of the workflow.
|
||||
/// </summary>
|
||||
public Type InputType => typeof(T);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Checkpointing;
|
||||
using Microsoft.Agents.AI.Workflows.Observability;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
@@ -441,34 +440,4 @@ public class WorkflowBuilder
|
||||
|
||||
return workflow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to build a workflow instance configured to process messages of the specified input type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInput">The desired input type for the workflow.</typeparam>
|
||||
/// <exception cref="InvalidOperationException">Thrown if the built workflow cannot process messages of the specified input type,</exception>
|
||||
public async ValueTask<Workflow<TInput>> BuildAsync<TInput>() where TInput : notnull
|
||||
{
|
||||
using Activity? activity = s_activitySource.StartActivity(ActivityNames.WorkflowBuild);
|
||||
|
||||
Workflow<TInput>? maybeWorkflow = await this.BuildInternal(activity)
|
||||
.TryPromoteAsync<TInput>()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (maybeWorkflow is null)
|
||||
{
|
||||
var exception = new InvalidOperationException(
|
||||
$"The built workflow cannot process input of type '{typeof(TInput).FullName}'.");
|
||||
activity?.AddEvent(new ActivityEvent(EventNames.BuildError, tags: new() {
|
||||
{ Tags.BuildErrorMessage, exception.Message },
|
||||
{ Tags.BuildErrorType, exception.GetType().FullName }
|
||||
}));
|
||||
activity?.CaptureException(exception);
|
||||
throw exception;
|
||||
}
|
||||
|
||||
activity?.AddEvent(new ActivityEvent(EventNames.BuildCompleted));
|
||||
|
||||
return maybeWorkflow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,12 +18,14 @@ internal sealed class WorkflowHostAgent : AIAgent
|
||||
private readonly string? _id;
|
||||
private readonly CheckpointManager? _checkpointManager;
|
||||
private readonly IWorkflowExecutionEnvironment _executionEnvironment;
|
||||
private readonly Task<ProtocolDescriptor> _describeTask;
|
||||
|
||||
private readonly ConcurrentDictionary<string, string> _assignedRunIds = [];
|
||||
|
||||
public WorkflowHostAgent(Workflow<List<ChatMessage>> workflow, string? id = null, string? name = null, string? description = null, CheckpointManager? checkpointManager = null, IWorkflowExecutionEnvironment? executionEnvironment = null)
|
||||
public WorkflowHostAgent(Workflow workflow, string? id = null, string? name = null, string? description = null, CheckpointManager? checkpointManager = null, IWorkflowExecutionEnvironment? executionEnvironment = null)
|
||||
{
|
||||
this._workflow = Throw.IfNull(workflow);
|
||||
|
||||
this._executionEnvironment = executionEnvironment ?? (workflow.AllowConcurrent
|
||||
? InProcessExecution.Concurrent
|
||||
: InProcessExecution.OffThread);
|
||||
@@ -32,6 +34,9 @@ internal sealed class WorkflowHostAgent : AIAgent
|
||||
this._id = id;
|
||||
this.Name = name;
|
||||
this.Description = description;
|
||||
|
||||
// Kick off the typecheck right away by starting the DescribeProtocol task.
|
||||
this._describeTask = this._workflow.DescribeProtocolAsync().AsTask();
|
||||
}
|
||||
|
||||
public override string Id => this._id ?? base.Id;
|
||||
@@ -50,6 +55,12 @@ internal sealed class WorkflowHostAgent : AIAgent
|
||||
return result;
|
||||
}
|
||||
|
||||
private async ValueTask ValidateWorkflowAsync()
|
||||
{
|
||||
ProtocolDescriptor protocol = await this._describeTask.ConfigureAwait(false);
|
||||
protocol.ThrowIfNotChatProtocol();
|
||||
}
|
||||
|
||||
public override AgentThread GetNewThread() => new WorkflowThread(this._workflow, this.GenerateNewId(), this._executionEnvironment, this._checkpointManager);
|
||||
|
||||
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
@@ -75,6 +86,8 @@ internal sealed class WorkflowHostAgent : AIAgent
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await this.ValidateWorkflowAsync().ConfigureAwait(false);
|
||||
|
||||
WorkflowThread workflowThread = await this.UpdateThreadAsync(messages, thread, cancellationToken).ConfigureAwait(false);
|
||||
MessageMerger merger = new();
|
||||
|
||||
@@ -95,6 +108,8 @@ internal sealed class WorkflowHostAgent : AIAgent
|
||||
AgentRunOptions? options = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
await this.ValidateWorkflowAsync().ConfigureAwait(false);
|
||||
|
||||
WorkflowThread workflowThread = await this.UpdateThreadAsync(messages, thread, cancellationToken).ConfigureAwait(false);
|
||||
await foreach (AgentRunResponseUpdate update in workflowThread.InvokeStageAsync(cancellationToken)
|
||||
.ConfigureAwait(false)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows;
|
||||
@@ -25,29 +23,6 @@ public static class WorkflowHostingExtensions
|
||||
/// <see cref="InProcessExecution.Lockstep"/> for the in-process environments.</param>
|
||||
/// <returns></returns>
|
||||
public static AIAgent AsAgent(
|
||||
this Workflow<List<ChatMessage>> workflow,
|
||||
string? id = null,
|
||||
string? name = null,
|
||||
string? description = null,
|
||||
CheckpointManager? checkpointManager = null,
|
||||
IWorkflowExecutionEnvironment? executionEnvironment = null)
|
||||
{
|
||||
return new WorkflowHostAgent(workflow, id, name, description, checkpointManager, executionEnvironment);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a workflow with the appropriate primary input type to an <see cref="AIAgent"/>.
|
||||
/// </summary>
|
||||
/// <param name="workflow">The workflow to be hosted by the resulting <see cref="AIAgent"/></param>
|
||||
/// <param name="id">A unique id for the hosting <see cref="AIAgent"/>.</param>
|
||||
/// <param name="name">A name for the hosting <see cref="AIAgent"/>.</param>
|
||||
/// /// <param name="description">A description for the hosting <see cref="AIAgent"/>.</param>
|
||||
/// <param name="checkpointManager">A <see cref="CheckpointManager"/> to enable persistence of run state.</param>
|
||||
/// <param name="executionEnvironment">Specify the execution environment to use when running the workflows. See
|
||||
/// <see cref="InProcessExecution.OffThread"/>, <see cref="InProcessExecution.Concurrent"/> and
|
||||
/// <see cref="InProcessExecution.Lockstep"/> for the in-process environments.</param>
|
||||
/// <returns></returns>
|
||||
public static async ValueTask<AIAgent> AsAgentAsync(
|
||||
this Workflow workflow,
|
||||
string? id = null,
|
||||
string? name = null,
|
||||
@@ -55,15 +30,7 @@ public static class WorkflowHostingExtensions
|
||||
CheckpointManager? checkpointManager = null,
|
||||
IWorkflowExecutionEnvironment? executionEnvironment = null)
|
||||
{
|
||||
Workflow<List<ChatMessage>>? maybeTyped = await workflow.TryPromoteAsync<List<ChatMessage>>()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (maybeTyped is null)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot host a workflow that does not accept List<ChatMessage> as an input");
|
||||
}
|
||||
|
||||
return maybeTyped.AsAgent(id, name, description, checkpointManager, executionEnvironment);
|
||||
return new WorkflowHostAgent(workflow, id, name, description, checkpointManager, executionEnvironment);
|
||||
}
|
||||
|
||||
internal static FunctionCallContent ToFunctionCall(this ExternalRequest request)
|
||||
|
||||
@@ -102,6 +102,8 @@ internal sealed class WorkflowThread : AgentThread
|
||||
|
||||
private async ValueTask<Checkpointed<StreamingRun>> CreateOrResumeRunAsync(List<ChatMessage> messages, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// The workflow is validated to be a ChatProtocol workflow by the WorkflowHostAgent before creating the thread,
|
||||
// and does not need to be checked again here.
|
||||
if (this.LastCheckpoint is not null)
|
||||
{
|
||||
Checkpointed<StreamingRun> checkpointed =
|
||||
|
||||
@@ -40,12 +40,23 @@ public class ChatProtocolExecutorTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChatProtocolExecutor_DescribedProtocol_IsChatProtocol()
|
||||
{
|
||||
// Arrange
|
||||
TestChatProtocolExecutor executor = new();
|
||||
ProtocolDescriptor protocol = executor.DescribeProtocol();
|
||||
|
||||
// Act & Assert
|
||||
protocol.Should().Match<ProtocolDescriptor>(protocol => protocol.IsChatProtocol());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ChatProtocolExecutor_Handles_ListOfChatMessagesAsync()
|
||||
{
|
||||
// Arrange
|
||||
var executor = new TestChatProtocolExecutor();
|
||||
var context = new TestWorkflowContext(executor.Id);
|
||||
TestChatProtocolExecutor executor = new();
|
||||
TestWorkflowContext context = new(executor.Id);
|
||||
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
@@ -68,8 +79,8 @@ public class ChatProtocolExecutorTests
|
||||
public async Task ChatProtocolExecutor_Handles_ArrayOfChatMessagesAsync()
|
||||
{
|
||||
// Arrange
|
||||
var executor = new TestChatProtocolExecutor();
|
||||
var context = new TestWorkflowContext(executor.Id);
|
||||
TestChatProtocolExecutor executor = new();
|
||||
TestWorkflowContext context = new(executor.Id);
|
||||
|
||||
ChatMessage[] messages =
|
||||
[
|
||||
@@ -94,8 +105,8 @@ public class ChatProtocolExecutorTests
|
||||
public async Task ChatProtocolExecutor_Handles_SingleChatMessageAsync()
|
||||
{
|
||||
// Arrange
|
||||
var executor = new TestChatProtocolExecutor();
|
||||
var context = new TestWorkflowContext(executor.Id);
|
||||
TestChatProtocolExecutor executor = new();
|
||||
TestWorkflowContext context = new(executor.Id);
|
||||
|
||||
var message = new ChatMessage(ChatRole.User, "Single message");
|
||||
|
||||
@@ -112,8 +123,8 @@ public class ChatProtocolExecutorTests
|
||||
[Fact]
|
||||
public async Task ChatProtocolExecutor_AccumulatesAndClearsMessagesPerTurnAsync()
|
||||
{
|
||||
var executor = new TestChatProtocolExecutor();
|
||||
var context = new TestWorkflowContext(executor.Id);
|
||||
TestChatProtocolExecutor executor = new();
|
||||
TestWorkflowContext context = new(executor.Id);
|
||||
|
||||
// Send multiple message batches before taking a turn
|
||||
await executor.ExecuteAsync(new ChatMessage(ChatRole.User, "Message 1"), new TypeId(typeof(ChatMessage)), context);
|
||||
@@ -147,12 +158,12 @@ public class ChatProtocolExecutorTests
|
||||
[Fact]
|
||||
public async Task ChatProtocolExecutor_WithStringRole_ConvertsStringToMessageAsync()
|
||||
{
|
||||
var executor = new TestChatProtocolExecutor(
|
||||
TestChatProtocolExecutor executor = new(
|
||||
options: new ChatProtocolExecutorOptions
|
||||
{
|
||||
StringMessageChatRole = ChatRole.User
|
||||
});
|
||||
var context = new TestWorkflowContext(executor.Id);
|
||||
TestWorkflowContext context = new(executor.Id);
|
||||
|
||||
await executor.ExecuteAsync("String message", new TypeId(typeof(string)), context);
|
||||
await executor.TakeTurnAsync(new TurnToken(emitEvents: false), context);
|
||||
@@ -165,8 +176,8 @@ public class ChatProtocolExecutorTests
|
||||
[Fact]
|
||||
public async Task ChatProtocolExecutor_EmptyCollection_HandledCorrectlyAsync()
|
||||
{
|
||||
var executor = new TestChatProtocolExecutor();
|
||||
var context = new TestWorkflowContext(executor.Id);
|
||||
TestChatProtocolExecutor executor = new();
|
||||
TestWorkflowContext context = new(executor.Id);
|
||||
|
||||
await executor.ExecuteAsync(new List<ChatMessage>(), new TypeId(typeof(List<ChatMessage>)), context);
|
||||
await executor.ExecuteAsync(Array.Empty<ChatMessage>(), new TypeId(typeof(ChatMessage[])), context);
|
||||
@@ -181,8 +192,8 @@ public class ChatProtocolExecutorTests
|
||||
[InlineData(typeof(ChatMessage[]))]
|
||||
public async Task ChatProtocolExecutor_RoutesCollectionTypesAsync(Type collectionType)
|
||||
{
|
||||
var executor = new TestChatProtocolExecutor();
|
||||
var context = new TestWorkflowContext(executor.Id);
|
||||
TestChatProtocolExecutor executor = new();
|
||||
TestWorkflowContext context = new(executor.Id);
|
||||
|
||||
var sourceMessages = new[] { new ChatMessage(ChatRole.User, "Test message") };
|
||||
object messagesToSend = collectionType == typeof(List<ChatMessage>) ? sourceMessages.ToList() : sourceMessages;
|
||||
@@ -197,8 +208,8 @@ public class ChatProtocolExecutorTests
|
||||
[Fact]
|
||||
public async Task ChatProtocolExecutor_MultipleTurns_EachTurnProcessesSeparatelyAsync()
|
||||
{
|
||||
var executor = new TestChatProtocolExecutor();
|
||||
var context = new TestWorkflowContext(executor.Id);
|
||||
TestChatProtocolExecutor executor = new();
|
||||
TestWorkflowContext context = new(executor.Id);
|
||||
|
||||
await executor.ExecuteAsync(new List<ChatMessage> { new(ChatRole.User, "Turn 1") }, new TypeId(typeof(List<ChatMessage>)), context);
|
||||
await executor.TakeTurnAsync(new TurnToken(emitEvents: false), context);
|
||||
@@ -217,8 +228,8 @@ public class ChatProtocolExecutorTests
|
||||
[Fact]
|
||||
public async Task ChatProtocolExecutor_InitialWorkflowMessages_RoutedCorrectlyAsync()
|
||||
{
|
||||
var executor = new TestChatProtocolExecutor();
|
||||
var context = new TestWorkflowContext(executor.Id);
|
||||
TestChatProtocolExecutor executor = new();
|
||||
TestWorkflowContext context = new(executor.Id);
|
||||
|
||||
List<ChatMessage> initialMessages = [new ChatMessage(ChatRole.User, "Kick off the workflow")];
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ public class JsonSerializationTests
|
||||
private static RequestPortInfo IntToString => RequestPort.Create<int, string>(IntToStringId).ToPortInfo();
|
||||
private static RequestPortInfo StringToInt => RequestPort.Create<string, int>(StringToIntId).ToPortInfo();
|
||||
|
||||
private static ValueTask<Workflow<string>> CreateTestWorkflowAsync()
|
||||
private static Workflow CreateTestWorkflow()
|
||||
{
|
||||
ForwardMessageExecutor<string> forwardString = new(ForwardStringId);
|
||||
ForwardMessageExecutor<int> forwardInt = new(ForwardIntId);
|
||||
@@ -169,12 +169,12 @@ public class JsonSerializationTests
|
||||
.AddEdge(forwardInt, intToString)
|
||||
.AddEdge(intToString, StreamingAggregators.Last<int>().AsExecutor("Aggregate"));
|
||||
|
||||
return builder.BuildAsync<string>();
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
internal static async ValueTask<WorkflowInfo> CreateTestWorkflowInfoAsync()
|
||||
internal static WorkflowInfo CreateTestWorkflowInfo()
|
||||
{
|
||||
Workflow<string> testWorkflow = await CreateTestWorkflowAsync().ConfigureAwait(false);
|
||||
Workflow testWorkflow = CreateTestWorkflow();
|
||||
return testWorkflow.ToWorkflowInfo();
|
||||
}
|
||||
|
||||
@@ -232,7 +232,7 @@ public class JsonSerializationTests
|
||||
[Fact]
|
||||
public async Task Test_WorkflowInfo_JsonRoundtripAsync()
|
||||
{
|
||||
WorkflowInfo prototype = await CreateTestWorkflowInfoAsync();
|
||||
WorkflowInfo prototype = CreateTestWorkflowInfo();
|
||||
|
||||
JsonMarshaller marshaller = new();
|
||||
|
||||
@@ -637,7 +637,7 @@ public class JsonSerializationTests
|
||||
[Fact]
|
||||
public async Task Test_Checkpoint_JsonRoundTripAsync()
|
||||
{
|
||||
WorkflowInfo testWorkflowInfo = await CreateTestWorkflowInfoAsync();
|
||||
WorkflowInfo testWorkflowInfo = CreateTestWorkflowInfo();
|
||||
Checkpoint prototype = new(12, testWorkflowInfo, TestRunnerStateData, TestStateData, TestEdgeState, TestParentCheckpointInfo);
|
||||
Checkpoint result = RunJsonRoundtrip(prototype, TestCustomSerializedJsonOptions);
|
||||
|
||||
|
||||
@@ -157,23 +157,17 @@ public class RepresentationTests
|
||||
[Fact]
|
||||
public async Task Test_Sample_WorkflowInfosAsync()
|
||||
{
|
||||
Workflow<string> workflowStep1 = (await Step1EntryPoint.WorkflowInstance.TryPromoteAsync<string>())!;
|
||||
RunWorkflowInfoMatchTest(workflowStep1);
|
||||
|
||||
Workflow<string> workflowStep2 = (await Step2EntryPoint.WorkflowInstance.TryPromoteAsync<string>())!;
|
||||
RunWorkflowInfoMatchTest(workflowStep2);
|
||||
|
||||
RunWorkflowInfoMatchTest((await Step3EntryPoint.WorkflowInstance.TryPromoteAsync<NumberSignal>())!);
|
||||
|
||||
RunWorkflowInfoMatchTest((await Step4EntryPoint.WorkflowInstance.TryPromoteAsync<NumberSignal>())!);
|
||||
|
||||
RunWorkflowInfoMatchTest(Step1EntryPoint.WorkflowInstance);
|
||||
RunWorkflowInfoMatchTest(Step2EntryPoint.WorkflowInstance);
|
||||
RunWorkflowInfoMatchTest(Step3EntryPoint.WorkflowInstance);
|
||||
RunWorkflowInfoMatchTest(Step4EntryPoint.WorkflowInstance);
|
||||
// Step 5 reuses the workflow from Step 4, so we don't need to test it separately.
|
||||
RunWorkflowInfoMatchTest((await Step6EntryPoint.CreateWorkflow(2).TryPromoteAsync<List<ChatMessage>>())!);
|
||||
RunWorkflowInfoMatchTest(Step6EntryPoint.CreateWorkflow(maxTurns: 2));
|
||||
// Step 7 reuses the workflow from Step 6, so we don't need to test it separately.
|
||||
|
||||
RunWorkflowInfoMatchTest(workflowStep1, workflowStep2, expect: false);
|
||||
RunWorkflowInfoMatchTest(Step1EntryPoint.WorkflowInstance, Step2EntryPoint.WorkflowInstance, expect: false);
|
||||
|
||||
static void RunWorkflowInfoMatchTest<TInput>(Workflow<TInput> workflow, Workflow<TInput>? comparator = null, bool expect = true)
|
||||
static void RunWorkflowInfoMatchTest(Workflow workflow, Workflow? comparator = null, bool expect = true)
|
||||
{
|
||||
comparator ??= workflow;
|
||||
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ internal static class Step1EntryPoint
|
||||
|
||||
public static async ValueTask RunAsync(TextWriter writer, IWorkflowExecutionEnvironment environment)
|
||||
{
|
||||
StreamingRun run = await environment.StreamAsync(WorkflowInstance, "Hello, World!").ConfigureAwait(false);
|
||||
StreamingRun run = await environment.StreamAsync(WorkflowInstance, input: "Hello, World!").ConfigureAwait(false);
|
||||
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ internal static class Step2EntryPoint
|
||||
|
||||
public static async ValueTask<string> RunAsync(TextWriter writer, IWorkflowExecutionEnvironment environment, string input = "This is a spam message.")
|
||||
{
|
||||
StreamingRun handle = await environment.StreamAsync(WorkflowInstance, input).ConfigureAwait(false);
|
||||
StreamingRun handle = await environment.StreamAsync(WorkflowInstance, input: input).ConfigureAwait(false);
|
||||
await foreach (WorkflowEvent evt in handle.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
switch (evt)
|
||||
|
||||
-6
@@ -23,12 +23,6 @@ internal static class Step4EntryPoint
|
||||
.Build();
|
||||
}
|
||||
|
||||
public static ValueTask<Workflow<NumberSignal>?> GetPromotedWorklowInstanceAsync()
|
||||
{
|
||||
Workflow workflow = CreateWorkflowInstance(out _);
|
||||
return workflow.TryPromoteAsync<NumberSignal>();
|
||||
}
|
||||
|
||||
public static Workflow WorkflowInstance
|
||||
{
|
||||
get
|
||||
|
||||
+1
-4
@@ -1,6 +1,5 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
@@ -11,9 +10,7 @@ internal static class Step7EntryPoint
|
||||
{
|
||||
public static async ValueTask RunAsync(TextWriter writer, int maxSteps = 2)
|
||||
{
|
||||
Workflow<List<ChatMessage>> workflow = (await Step6EntryPoint.CreateWorkflow(maxSteps)
|
||||
.TryPromoteAsync<List<ChatMessage>>()
|
||||
.ConfigureAwait(false))!;
|
||||
Workflow workflow = Step6EntryPoint.CreateWorkflow(maxSteps);
|
||||
|
||||
AIAgent agent = workflow.AsAgent("group-chat-agent", "Group Chat Agent");
|
||||
|
||||
|
||||
@@ -538,7 +538,7 @@ public class StateManagerTests
|
||||
|
||||
Dictionary<ScopeKey, PortableValue> exportedState = await manager.ExportStateAsync();
|
||||
Dictionary<ScopeKey, PortableValue> serializedState = JsonSerializationTests.RunJsonRoundtrip(exportedState);
|
||||
Checkpoint testCheckpoint = new(0, await JsonSerializationTests.CreateTestWorkflowInfoAsync(), new([], [], []), serializedState, new());
|
||||
Checkpoint testCheckpoint = new(0, JsonSerializationTests.CreateTestWorkflowInfo(), new([], [], []), serializedState, new());
|
||||
|
||||
manager = new();
|
||||
await manager.ImportStateAsync(testCheckpoint);
|
||||
|
||||
Reference in New Issue
Block a user