diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/Program.cs b/dotnet/samples/GettingStarted/Workflows/Declarative/Program.cs index d2301c0311..78e582dfc9 100644 --- a/dotnet/samples/GettingStarted/Workflows/Declarative/Program.cs +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/Program.cs @@ -94,11 +94,19 @@ internal sealed class Program { if (evt is ExecutorInvokeEvent executorInvoked) { - Debug.WriteLine($"STEP ENTER #{executorInvoked.ExecutorId}"); + Debug.WriteLine($"EXECUTOR ENTER #{executorInvoked.ExecutorId}"); } else if (evt is ExecutorCompleteEvent executorComplete) { - Debug.WriteLine($"STEP EXIT #{executorComplete.ExecutorId}"); + Debug.WriteLine($"EXECUTOR EXIT #{executorComplete.ExecutorId}"); + } + if (evt is DeclarativeActionInvokeEvent actionInvoked) + { + Debug.WriteLine($"ACTION ENTER #{actionInvoked.ActionId} [{actionInvoked.ActionType}]"); + } + else if (evt is DeclarativeActionCompleteEvent actionComplete) + { + Debug.WriteLine($"ACTION EXIT #{actionComplete.ActionId} [{actionComplete.ActionType}]"); } else if (evt is ExecutorFailureEvent executorFailure) { diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ConversationUpdateEvent.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ConversationUpdateEvent.cs deleted file mode 100644 index 7984ffcd11..0000000000 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ConversationUpdateEvent.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -namespace Microsoft.Agents.Workflows.Declarative; - -/// -/// Event that represents a message produced by a declarative workflow. -/// -public class ConversationUpdateEvent(string executorid, string conversationId) : ExecutorEvent(executorid, conversationId) -{ - /// - /// The conversation ID associated with the workflow. - /// - public string ConversationId { get; } = conversationId; -} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowEvents.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowEvents.cs new file mode 100644 index 0000000000..74970f62f1 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowEvents.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Agents.Workflows.Declarative.Extensions; +using Microsoft.Bot.ObjectModel; + +namespace Microsoft.Agents.Workflows.Declarative; + +/// +/// Event that broadcasts the conversation identifier. +/// +public class ConversationUpdateEvent(string executorid, string conversationId) : ExecutorEvent(executorid, conversationId) +{ + /// + /// The conversation ID associated with the workflow. + /// + public string ConversationId { get; } = conversationId; +} + +/// +/// Event that indicates a declarative action has been invoked. +/// +public class DeclarativeActionInvokeEvent(string actionId, DialogAction action, string? priorActionId) : WorkflowEvent(action) +{ + /// + /// The declarative action id. + /// + public string ActionId => actionId; + + /// + /// The declarative action type name. + /// + public string ActionType => action.GetType().Name; + + /// + /// Identifier of the parent action. + /// + public string? ParentActionId => action.GetParentId(); + + /// + /// Identifier of the previous action. + /// + public string? PriorActionId => priorActionId; +} + +/// +/// Event that indicates a declarative action has completed. +/// +public class DeclarativeActionCompleteEvent(string actionId, DialogAction action) : WorkflowEvent(action) +{ + /// + /// The declarative action identifier. + /// + public string ActionId => actionId; + + /// + /// The declarative action type name. + /// + public string ActionType => action.GetType().Name; + + /// + /// Identifier of the parent action. + /// + public string? ParentActionId => action.GetParentId(); +} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs index 0d0138f261..2fa157bcca 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs @@ -7,7 +7,6 @@ using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.Agents.Workflows.Declarative.Extensions; -using Microsoft.Agents.Workflows.Reflection; using Microsoft.Bot.ObjectModel; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -25,9 +24,7 @@ internal abstract class DeclarativeActionExecutor(TAction model, Declar public new TAction Model => (TAction)base.Model; } -internal abstract class WorkflowActionExecutor : - ReflectingExecutor, - IMessageHandler +internal abstract class WorkflowActionExecutor : Executor { public const string RootActionId = "(root)"; @@ -60,8 +57,10 @@ internal abstract class WorkflowActionExecutor : protected DeclarativeWorkflowState State { get; } + protected virtual bool IsDiscreteAction => true; + /// - public async ValueTask HandleAsync(DeclarativeExecutorResult message, IWorkflowContext context) + public override async ValueTask HandleAsync(DeclarativeExecutorResult message, IWorkflowContext context) { if (this.Model.Disabled) { @@ -69,6 +68,8 @@ internal abstract class WorkflowActionExecutor : return; } + await this.RaiseInvocationEventAsync(context, message.ExecutorId).ConfigureAwait(false); + await this.State.RestoreAsync(context, default).ConfigureAwait(false); try @@ -87,6 +88,13 @@ internal abstract class WorkflowActionExecutor : Debug.WriteLine($"ERROR [{this.Id}] {exception.GetType().Name}\n{exception.Message}"); throw new DeclarativeActionException($"Unhandled workflow failure - #{this.Id} ({this.Model.GetType().Name})", exception); } + finally + { + if (this.IsDiscreteAction) + { + await this.RaiseCompletionEventAsync(context).ConfigureAwait(false); + } + } } protected abstract ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default); @@ -117,4 +125,8 @@ internal abstract class WorkflowActionExecutor : string message = $"Unexpected workflow failure during {this.Model.GetType().Name} [{this.Id}]: {text}"; return exception is null ? new(message) : new(message, exception); } + + protected ValueTask RaiseInvocationEventAsync(IWorkflowContext context, string? priorEventId = null) => context.AddEventAsync(new DeclarativeActionInvokeEvent(this.Id, this.Model, priorEventId)); + + protected ValueTask RaiseCompletionEventAsync(IWorkflowContext context) => context.AddEventAsync(new DeclarativeActionCompleteEvent(this.Id, this.Model)); } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowExecutor.cs index 573e931f9c..f65bb7f5f6 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowExecutor.cs @@ -3,7 +3,6 @@ using System; using System.Threading.Tasks; using Microsoft.Agents.Workflows.Declarative.PowerFx; -using Microsoft.Agents.Workflows.Reflection; using Microsoft.Extensions.AI; namespace Microsoft.Agents.Workflows.Declarative.Interpreter; @@ -11,12 +10,14 @@ namespace Microsoft.Agents.Workflows.Declarative.Interpreter; /// /// The root executor for a declarative workflow. /// -internal sealed class DeclarativeWorkflowExecutor(string workflowId, DeclarativeWorkflowState state, Func inputTransform) : - ReflectingExecutor>(workflowId), - IMessageHandler +internal sealed class DeclarativeWorkflowExecutor( + string workflowId, + DeclarativeWorkflowState state, + Func inputTransform) : + Executor(workflowId) where TInput : notnull { - public async ValueTask HandleAsync(TInput message, IWorkflowContext context) + public override async ValueTask HandleAsync(TInput message, IWorkflowContext context) { ChatMessage input = inputTransform.Invoke(message); await state.SetLastMessageAsync(context, input).ConfigureAwait(false); diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DelegateActionExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DelegateActionExecutor.cs index 7bcf0b9868..59afb516ff 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DelegateActionExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DelegateActionExecutor.cs @@ -2,13 +2,12 @@ using System.Threading; using System.Threading.Tasks; -using Microsoft.Agents.Workflows.Reflection; namespace Microsoft.Agents.Workflows.Declarative.Interpreter; internal delegate ValueTask DelegateAction(IWorkflowContext context, CancellationToken cancellationToken); -internal sealed class DelegateActionExecutor : ReflectingExecutor, IMessageHandler +internal sealed class DelegateActionExecutor : Executor { private readonly DelegateAction? _action; @@ -18,7 +17,7 @@ internal sealed class DelegateActionExecutor : ReflectingExecutor this.ContinuationFor(parentId, parentId); + private string ContinuationFor(string parentId, DelegateAction? stepAction = null) => this.ContinuationFor(parentId, parentId, stepAction); - private string ContinuationFor(string actionId, string parentId) + private string ContinuationFor(string actionId, string parentId, DelegateAction? stepAction = null) { actionId = PostId(actionId); - this._workflowModel.AddNode(this.CreateStep(actionId), parentId); + this._workflowModel.AddNode(this.CreateStep(actionId, stepAction), parentId); return actionId; } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ConditionGroupExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ConditionGroupExecutor.cs index a3cbfb9ff4..b8d1662210 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ConditionGroupExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ConditionGroupExecutor.cs @@ -31,6 +31,8 @@ internal sealed class ConditionGroupExecutor : DeclarativeActionExecutor false; + public bool IsMatch(ConditionItem conditionItem, object? result) { if (result is not DeclarativeExecutorResult message) @@ -72,4 +74,9 @@ internal sealed class ConditionGroupExecutor : DeclarativeActionExecutor public bool HasValue { get; private set; } + protected override bool IsDiscreteAction => false; + protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { this._index = 0; @@ -78,10 +80,17 @@ internal sealed class ForeachExecutor : DeclarativeActionExecutor public async ValueTask ResetAsync(IWorkflowContext context, CancellationToken cancellationToken) { - this.State.Reset(Throw.IfNull(this.Model.Value)); - if (this.Model.Index is not null) + try { - this.State.Reset(this.Model.Index); + this.State.Reset(Throw.IfNull(this.Model.Value)); + if (this.Model.Index is not null) + { + this.State.Reset(this.Model.Index); + } + } + finally + { + await this.RaiseCompletionEventAsync(context).ConfigureAwait(false); } } } diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/WorkflowActionExecutorTest.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/WorkflowActionExecutorTest.cs index 13ce04e1b9..8d7393864b 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/WorkflowActionExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/WorkflowActionExecutorTest.cs @@ -33,6 +33,8 @@ public abstract class WorkflowActionExecutorTest(ITestOutputHelper output) : Wor workflowBuilder.AddEdge(workflowExecutor, executor); StreamingRun run = await InProcessExecution.StreamAsync(workflowBuilder.Build(), this.Scopes); WorkflowEvent[] events = await run.WatchStreamAsync().ToArrayAsync(); + Assert.Contains(events, e => e is DeclarativeActionInvokeEvent); + Assert.Contains(events, e => e is DeclarativeActionCompleteEvent); return events; }