diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/Program.cs b/dotnet/samples/GettingStarted/Workflows/Declarative/Program.cs index 914365b779..478331efba 100644 --- a/dotnet/samples/GettingStarted/Workflows/Declarative/Program.cs +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/Program.cs @@ -143,11 +143,11 @@ internal sealed class Program Debug.WriteLine($"EXECUTOR EXIT #{executorCompleted.ExecutorId}"); break; - case DeclarativeActionInvokeEvent actionInvoked: + case DeclarativeActionInvokedEvent actionInvoked: Debug.WriteLine($"ACTION ENTER #{actionInvoked.ActionId} [{actionInvoked.ActionType}]"); break; - case DeclarativeActionCompleteEvent actionComplete: + case DeclarativeActionCompletedEvent actionComplete: Debug.WriteLine($"ACTION EXIT #{actionComplete.ActionId} [{actionComplete.ActionType}]"); break; diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionCompleteEvent.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionCompletedEvent.cs similarity index 85% rename from dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionCompleteEvent.cs rename to dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionCompletedEvent.cs index b574be1621..349028c96b 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionCompleteEvent.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionCompletedEvent.cs @@ -8,7 +8,7 @@ namespace Microsoft.Agents.Workflows.Declarative; /// /// Event that indicates a declarative action has been invoked. /// -public sealed class DeclarativeActionInvokeEvent : WorkflowEvent +public sealed class DeclarativeActionInvokedEvent : WorkflowEvent { /// /// The declarative action id. @@ -30,7 +30,7 @@ public sealed class DeclarativeActionInvokeEvent : WorkflowEvent /// public string? PriorActionId { get; } - internal DeclarativeActionInvokeEvent(DialogAction action, string? priorActionId) : base(action) + internal DeclarativeActionInvokedEvent(DialogAction action, string? priorActionId) : base(action) { this.ActionId = action.GetId(); this.ActionType = action.GetType().Name; diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionInvokeEvent.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionInvokedEvent.cs similarity index 84% rename from dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionInvokeEvent.cs rename to dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionInvokedEvent.cs index bb9f676635..16845124b6 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionInvokeEvent.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionInvokedEvent.cs @@ -8,7 +8,7 @@ namespace Microsoft.Agents.Workflows.Declarative; /// /// Event that indicates a declarative action has completed. /// -public sealed class DeclarativeActionCompleteEvent : WorkflowEvent +public sealed class DeclarativeActionCompletedEvent : WorkflowEvent { /// /// The declarative action identifier. @@ -25,7 +25,7 @@ public sealed class DeclarativeActionCompleteEvent : WorkflowEvent /// public string? ParentActionId { get; } - internal DeclarativeActionCompleteEvent(DialogAction action) : base(action) + internal DeclarativeActionCompletedEvent(DialogAction action) : base(action) { this.ActionId = action.GetId(); this.ActionType = action.GetType().Name; diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs index aa6fc18a98..dce481d5d0 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs @@ -12,10 +12,10 @@ namespace Microsoft.Agents.Workflows.Declarative.Extensions; internal static class IWorkflowContextExtensions { public static ValueTask RaiseInvocationEventAsync(this IWorkflowContext context, DialogAction action, string? priorEventId = null) => - context.AddEventAsync(new DeclarativeActionInvokeEvent(action, priorEventId)); + context.AddEventAsync(new DeclarativeActionInvokedEvent(action, priorEventId)); public static ValueTask RaiseCompletionEventAsync(this IWorkflowContext context, DialogAction action) => - context.AddEventAsync(new DeclarativeActionCompleteEvent(action)); + context.AddEventAsync(new DeclarativeActionCompletedEvent(action)); public static ValueTask SendResultMessageAsync(this IWorkflowContext context, string id, object? result = null, CancellationToken cancellationToken = default) => context.SendMessageAsync(new ExecutorResultMessage(id, result)); diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs index 7b573c69e2..61f00a5b50 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs @@ -68,7 +68,13 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor { if (this._workflowModel.GetDepth(item.Id.Value) > 1) { - string completionId = this.ContinuationFor(item.Id.Value); // End scope + DelegateAction? action = null; + ConditionGroupExecutor? conditionGroup = this._workflowModel.LocateParent(parentId); + if (conditionGroup is not null) + { + action = conditionGroup.DoneAsync; + } + string completionId = this.ContinuationFor(item.Id.Value, action); // End scope this._workflowModel.AddLinkFromPeer(item.Id.Value, completionId); // Connect with final action this._workflowModel.AddLink(completionId, Steps.Post(parentId)); // Merge with parent scope } @@ -175,10 +181,10 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor ForeachExecutor? loopExecutor = this._workflowModel.LocateParent(item.GetParentId()); if (loopExecutor is not null) { - string parentId = GetParentId(item); - this.ContinueWith(new DelegateActionExecutor(item.Id.Value, this._workflowState), parentId); - this._workflowModel.AddLink(item.Id.Value, Steps.Post(loopExecutor.Id)); - this.RestartAfter(item.Id.Value, parentId); + DefaultActionExecutor breakLoopExecutor = new(item, this._workflowState); + this.ContinueWith(breakLoopExecutor); + this._workflowModel.AddLink(breakLoopExecutor.Id, Steps.Post(loopExecutor.Id)); + this.RestartAfter(breakLoopExecutor.Id, breakLoopExecutor.ParentId); } } @@ -189,10 +195,10 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor ForeachExecutor? loopExecutor = this._workflowModel.LocateParent(item.GetParentId()); if (loopExecutor is not null) { - string parentId = GetParentId(item); - this.ContinueWith(new DelegateActionExecutor(item.Id.Value, this._workflowState), parentId); - this._workflowModel.AddLink(item.Id.Value, ForeachExecutor.Steps.Next(loopExecutor.Id)); - this.RestartAfter(item.Id.Value, parentId); + DefaultActionExecutor continueLoopExecutor = new(item, this._workflowState); + this.ContinueWith(continueLoopExecutor); + this._workflowModel.AddLink(continueLoopExecutor.Id, Steps.Post(loopExecutor.Id)); + this.RestartAfter(continueLoopExecutor.Id, continueLoopExecutor.ParentId); } } @@ -200,9 +206,9 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor { this.Trace(item); - string parentId = GetParentId(item); - this.ContinueWith(new DelegateActionExecutor(item.Id.Value, this._workflowState), parentId); - this.RestartAfter(item.Id.Value, parentId); + DefaultActionExecutor endExecutor = new(item, this._workflowState); + this.ContinueWith(endExecutor); + this.RestartAfter(item.Id.Value, endExecutor.ParentId); } protected override void Visit(Question item) diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/DefaultActionExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/DefaultActionExecutor.cs new file mode 100644 index 0000000000..b09c2b3b18 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/DefaultActionExecutor.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Agents.Workflows.Declarative.Interpreter; +using Microsoft.Agents.Workflows.Declarative.PowerFx; +using Microsoft.Bot.ObjectModel; + +namespace Microsoft.Agents.Workflows.Declarative.ObjectModel; + +internal sealed class DefaultActionExecutor(DialogAction model, WorkflowFormulaState state) : + DeclarativeActionExecutor(model, state) +{ + protected override ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) + { + // No action needed - the edge will be followed automatically + return default; + } +} diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/DeclarativeWorkflowTest.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/DeclarativeWorkflowTest.cs index ad58b2b8d2..b908779e7e 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/DeclarativeWorkflowTest.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/DeclarativeWorkflowTest.cs @@ -74,7 +74,7 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output, AgentFixtu Workflow workflow = DeclarativeWorkflowBuilder.Build(workflowPath, workflowOptions); WorkflowEvents workflowEvents = await WorkflowHarness.RunAsync(workflow, (TInput)GetInput(testcase)); - foreach (DeclarativeActionInvokeEvent actionInvokeEvent in workflowEvents.ActionInvokeEvents) + foreach (DeclarativeActionInvokedEvent actionInvokeEvent in workflowEvents.ActionInvokeEvents) { this.Output.WriteLine($"ACTION: {actionInvokeEvent.ActionId} [{actionInvokeEvent.ActionType}]"); } diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowEvents.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowEvents.cs index 163eb19fdf..5d2deb418a 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowEvents.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowEvents.cs @@ -12,12 +12,12 @@ internal sealed class WorkflowEvents { this.Events = workflowEvents; this.EventCounts = workflowEvents.GroupBy(e => e.GetType()).ToImmutableDictionary(e => e.Key, e => e.Count()); - this.ActionInvokeEvents = workflowEvents.OfType().ToImmutableList(); - this.ActionCompleteEvents = workflowEvents.OfType().ToImmutableList(); + this.ActionInvokeEvents = workflowEvents.OfType().ToImmutableList(); + this.ActionCompleteEvents = workflowEvents.OfType().ToImmutableList(); } public ImmutableList Events { get; } public IImmutableDictionary EventCounts { get; } - public ImmutableList ActionInvokeEvents { get; } - public ImmutableList ActionCompleteEvents { get; private set; } + public ImmutableList ActionInvokeEvents { get; } + public ImmutableList ActionCompleteEvents { get; private set; } } diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs index 59d7aa8d5c..d33139ece1 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs @@ -58,7 +58,7 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow public async Task LoopContinueAction() { await this.RunWorkflow("LoopContinue.yaml"); - this.AssertExecutionCount(expectedCount: 23); + this.AssertExecutionCount(expectedCount: 7); this.AssertExecuted("foreach_loop"); this.AssertExecuted("continueLoop_now"); this.AssertExecuted("end_all"); @@ -66,6 +66,15 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow this.AssertNotExecuted("sendActivity_loop"); } + [Fact] + public async Task EndConversationAction() + { + await this.RunWorkflow("EndConversation.yaml"); + this.AssertExecutionCount(expectedCount: 1); + this.AssertExecuted("end_all"); + this.AssertNotExecuted("sendActivity_1"); + } + [Fact] public async Task GotoAction() { @@ -89,7 +98,7 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow this.AssertExecuted("conditionGroup_test"); if (input % 2 == 0) { - this.AssertExecuted("conditionItem_even"); + this.AssertExecuted("conditionItem_even", isScope: true); this.AssertExecuted("sendActivity_even"); this.AssertNotExecuted("conditionItem_odd"); this.AssertNotExecuted("sendActivity_odd"); @@ -97,7 +106,7 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow } else { - this.AssertExecuted("conditionItem_odd"); + this.AssertExecuted("conditionItem_odd", isScope: true); this.AssertExecuted("sendActivity_odd"); this.AssertNotExecuted("conditionItem_even"); this.AssertNotExecuted("sendActivity_even"); @@ -117,13 +126,13 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow this.AssertExecuted("conditionGroup_test"); if (input % 2 == 0) { - this.AssertExecuted("sendActivity_else"); + this.AssertExecuted("sendActivity_else", isScope: true); this.AssertNotExecuted("conditionItem_odd"); this.AssertNotExecuted("sendActivity_odd"); } else { - this.AssertExecuted("conditionItem_odd"); + this.AssertExecuted("conditionItem_odd", isScope: true); this.AssertExecuted("sendActivity_odd"); this.AssertNotExecuted("sendActivity_else"); } @@ -220,10 +229,15 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow Assert.DoesNotContain(this.WorkflowEvents.OfType(), e => e.ExecutorId == executorId); } - private void AssertExecuted(string executorId) + private void AssertExecuted(string executorId, bool isScope = false) { Assert.Contains(this.WorkflowEvents.OfType(), e => e.ExecutorId == executorId); Assert.Contains(this.WorkflowEvents.OfType(), e => e.ExecutorId == executorId); + if (!isScope) + { + Assert.Contains(this.WorkflowEvents.OfType(), e => e.ActionId == executorId); + Assert.Contains(this.WorkflowEvents.OfType(), e => e.ActionId == executorId); + } } private void AssertMessage(string message) @@ -251,6 +265,14 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow ExecutorResultMessage? message = invokeEvent.Data as ExecutorResultMessage; this.Output.WriteLine($"EXEC: {invokeEvent.ExecutorId} << {message?.ExecutorId ?? "?"} [{message?.Result ?? "-"}]"); } + else if (workflowEvent is DeclarativeActionInvokedEvent actionInvokeEvent) + { + this.Output.WriteLine($"ACTION ENTER: {actionInvokeEvent.ActionId}"); + } + else if (workflowEvent is DeclarativeActionCompletedEvent actionCompleteEvent) + { + this.Output.WriteLine($"ACTION EXIT: {actionCompleteEvent.ActionId}"); + } else if (workflowEvent is AgentRunResponseEvent messageEvent) { this.Output.WriteLine($"MESSAGE: {messageEvent.Response.Messages[0].Text.Trim()}"); 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 26cb44f243..a9af40c682 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/WorkflowActionExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/WorkflowActionExecutorTest.cs @@ -31,8 +31,8 @@ public abstract class WorkflowActionExecutorTest(ITestOutputHelper output) : Wor workflowBuilder.AddEdge(workflowExecutor, executor); StreamingRun run = await InProcessExecution.StreamAsync(workflowBuilder.Build(), this.State); WorkflowEvent[] events = await run.WatchStreamAsync().ToArrayAsync(); - Assert.Contains(events, e => e is DeclarativeActionInvokeEvent); - Assert.Contains(events, e => e is DeclarativeActionCompleteEvent); + Assert.Contains(events, e => e is DeclarativeActionInvokedEvent); + Assert.Contains(events, e => e is DeclarativeActionCompletedEvent); return events; } diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/EndConversation.yaml b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/EndConversation.yaml new file mode 100644 index 0000000000..40444cb5cc --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/EndConversation.yaml @@ -0,0 +1,13 @@ +kind: AdaptiveDialog +beginDialog: + kind: OnActivity + id: my_workflow + type: Message + actions: + + - kind: EndConversation + id: end_all + + - kind: SendActivity + id: send_activity_1 + activity: NEVER 1! diff --git a/workflows/HumanInLoop.yaml b/workflows/HumanInLoop.yaml index a1a28d3b61..8d2b736cc5 100644 --- a/workflows/HumanInLoop.yaml +++ b/workflows/HumanInLoop.yaml @@ -29,10 +29,12 @@ beginDialog: entity: kind: StringPrebuiltEntity + # Confirm input - kind: ConditionGroup id: check_completion conditions: + # Didn't match - condition: =Topic.OriginalInput <> Topic.ConfirmedInput id: check_confirm actions: @@ -46,7 +48,8 @@ beginDialog: id: goto_again actionId: question_confirm - elseActions: # // %%% REMOVE / BUG + # Confirmed + elseActions: - kind: SendActivity id: sendActivity_confirmed activity: |- @@ -55,3 +58,6 @@ beginDialog: Confirmed input: {Topic.ConfirmedInput} + + +