mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET Workflow - Declarative Workflow Event Fix (#765)
* Checkpoint * Update workflows/DeepResearch.yaml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Comment * Fix comment * Update package version * Fix nuget haxx * Checkpoint * Code complete * Testing * Message content workaround * Add sequential flow * Checkpoint * Integration test project * Checkpoint * Checkpoint cleanup * Complete * Checkpoint * Fix tests * Comment cleanup * Namespace * Formatting * Analyzer updates * Workflow update * Fixed! * Update dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs Co-authored-by: Tao Chen <taochen@microsoft.com> * Fix build error * Purge "immutable" set and dictionary * Fix as task * Collection expression * Another * Frozen => Readonly (perf) * Fix * Namespace * Checkpoint * Add test * Fix * Rollback workflow edit * nitty --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Tao Chen <taochen@microsoft.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@ namespace Microsoft.Agents.Workflows.Declarative;
|
||||
/// <summary>
|
||||
/// Event that indicates a declarative action has been invoked.
|
||||
/// </summary>
|
||||
public sealed class DeclarativeActionInvokeEvent : WorkflowEvent
|
||||
public sealed class DeclarativeActionInvokedEvent : WorkflowEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// The declarative action id.
|
||||
@@ -30,7 +30,7 @@ public sealed class DeclarativeActionInvokeEvent : WorkflowEvent
|
||||
/// </summary>
|
||||
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;
|
||||
+2
-2
@@ -8,7 +8,7 @@ namespace Microsoft.Agents.Workflows.Declarative;
|
||||
/// <summary>
|
||||
/// Event that indicates a declarative action has completed.
|
||||
/// </summary>
|
||||
public sealed class DeclarativeActionCompleteEvent : WorkflowEvent
|
||||
public sealed class DeclarativeActionCompletedEvent : WorkflowEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// The declarative action identifier.
|
||||
@@ -25,7 +25,7 @@ public sealed class DeclarativeActionCompleteEvent : WorkflowEvent
|
||||
/// </summary>
|
||||
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;
|
||||
+2
-2
@@ -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));
|
||||
|
||||
+18
-12
@@ -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<ExecutorResultMessage>? action = null;
|
||||
ConditionGroupExecutor? conditionGroup = this._workflowModel.LocateParent<ConditionGroupExecutor>(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<ForeachExecutor>(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<ForeachExecutor>(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)
|
||||
|
||||
+19
@@ -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<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
// No action needed - the edge will be followed automatically
|
||||
return default;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -74,7 +74,7 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output, AgentFixtu
|
||||
Workflow<TInput> workflow = DeclarativeWorkflowBuilder.Build<TInput>(workflowPath, workflowOptions);
|
||||
|
||||
WorkflowEvents workflowEvents = await WorkflowHarness.RunAsync(workflow, (TInput)GetInput<TInput>(testcase));
|
||||
foreach (DeclarativeActionInvokeEvent actionInvokeEvent in workflowEvents.ActionInvokeEvents)
|
||||
foreach (DeclarativeActionInvokedEvent actionInvokeEvent in workflowEvents.ActionInvokeEvents)
|
||||
{
|
||||
this.Output.WriteLine($"ACTION: {actionInvokeEvent.ActionId} [{actionInvokeEvent.ActionType}]");
|
||||
}
|
||||
|
||||
+4
-4
@@ -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<DeclarativeActionInvokeEvent>().ToImmutableList();
|
||||
this.ActionCompleteEvents = workflowEvents.OfType<DeclarativeActionCompleteEvent>().ToImmutableList();
|
||||
this.ActionInvokeEvents = workflowEvents.OfType<DeclarativeActionInvokedEvent>().ToImmutableList();
|
||||
this.ActionCompleteEvents = workflowEvents.OfType<DeclarativeActionCompletedEvent>().ToImmutableList();
|
||||
}
|
||||
|
||||
public ImmutableList<WorkflowEvent> Events { get; }
|
||||
public IImmutableDictionary<Type, int> EventCounts { get; }
|
||||
public ImmutableList<DeclarativeActionInvokeEvent> ActionInvokeEvents { get; }
|
||||
public ImmutableList<DeclarativeActionCompleteEvent> ActionCompleteEvents { get; private set; }
|
||||
public ImmutableList<DeclarativeActionInvokedEvent> ActionInvokeEvents { get; }
|
||||
public ImmutableList<DeclarativeActionCompletedEvent> ActionCompleteEvents { get; private set; }
|
||||
}
|
||||
|
||||
+28
-6
@@ -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<ExecutorCompletedEvent>(), e => e.ExecutorId == executorId);
|
||||
}
|
||||
|
||||
private void AssertExecuted(string executorId)
|
||||
private void AssertExecuted(string executorId, bool isScope = false)
|
||||
{
|
||||
Assert.Contains(this.WorkflowEvents.OfType<ExecutorInvokedEvent>(), e => e.ExecutorId == executorId);
|
||||
Assert.Contains(this.WorkflowEvents.OfType<ExecutorCompletedEvent>(), e => e.ExecutorId == executorId);
|
||||
if (!isScope)
|
||||
{
|
||||
Assert.Contains(this.WorkflowEvents.OfType<DeclarativeActionInvokedEvent>(), e => e.ActionId == executorId);
|
||||
Assert.Contains(this.WorkflowEvents.OfType<DeclarativeActionCompletedEvent>(), 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()}");
|
||||
|
||||
+2
-2
@@ -31,8 +31,8 @@ public abstract class WorkflowActionExecutorTest(ITestOutputHelper output) : Wor
|
||||
workflowBuilder.AddEdge(workflowExecutor, executor);
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflowBuilder.Build<WorkflowFormulaState>(), 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;
|
||||
}
|
||||
|
||||
|
||||
+13
@@ -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!
|
||||
@@ -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}
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user