Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,7 @@ protected override void Visit(GotoAction item)
// Represent action with default executor
DefaultActionExecutor action = new(item, this._workflowState);
this.ContinueWith(action);
// Transition to target action
this._workflowModel.AddLink(action.Id, item.ActionId.Value);
this.InjectParentCompletionEvents(action.Id, item.ActionId.Value, action.ParentId);
// Define a clean-start to ensure "goto" is not a source for any edge
this.RestartAfter(action.Id, action.ParentId);
}
Expand Down Expand Up @@ -219,8 +218,7 @@ protected override void Visit(BreakLoop item)
// Represent action with default executor
DefaultActionExecutor action = new(item, this._workflowState);
this.ContinueWith(action);
// Transition to post action
this._workflowModel.AddLink(action.Id, Steps.Post(loopAction.Id));
this.InjectParentCompletionEvents(action.Id, Steps.Post(loopAction.Id), action.ParentId, stopBeforeParentId: loopAction.Id);
// Define a clean-start to ensure "break" is not a source for any edge
this.RestartAfter(action.Id, action.ParentId);
}
Expand All @@ -238,8 +236,7 @@ protected override void Visit(ContinueLoop item)
// Represent action with default executor
DefaultActionExecutor action = new(item, this._workflowState);
this.ContinueWith(action);
// Transition to select the next item
this._workflowModel.AddLink(action.Id, ForeachExecutor.Steps.Next(loopAction.Id));
this.InjectParentCompletionEvents(action.Id, ForeachExecutor.Steps.Next(loopAction.Id), action.ParentId, stopBeforeParentId: loopAction.Id);
// Define a clean-start to ensure "continue" is not a source for any edge
this.RestartAfter(action.Id, action.ParentId);
}
Expand Down Expand Up @@ -302,8 +299,28 @@ protected override void Visit(EndDialog item)
// Represent action with default executor
DefaultActionExecutor action = new(item, this._workflowState);
this.ContinueWith(action);

var nonDiscreteAncestors = this._workflowModel
.LocateNonDiscreteAncestors<DeclarativeActionExecutor>(action.Id)
.ToList();

string currentId = action.Id;
foreach (var ancestor in nonDiscreteAncestors)
{
string completionId = $"{action.Id}_Complete_{ancestor.Id}";
var completionStep = new DelegateActionExecutor(
completionId,
this._workflowState,
async (context, _, ct) => await context.RaiseCompletionEventAsync(ancestor.Model, ct).ConfigureAwait(false),
emitResult: false);

this._workflowModel.AddNode(completionStep, action.ParentId);
this._workflowModel.AddLink(currentId, completionId);
currentId = completionId;
}

// Define a clean-start to ensure "end" is not a source for any edge
this.RestartAfter(item.Id.Value, action.ParentId);
this.RestartAfter(currentId, action.ParentId);
}

protected override void Visit(EndConversation item)
Expand All @@ -313,8 +330,28 @@ protected override void Visit(EndConversation item)
// Represent action with default executor
DefaultActionExecutor action = new(item, this._workflowState);
this.ContinueWith(action);

var nonDiscreteAncestors = this._workflowModel
.LocateNonDiscreteAncestors<DeclarativeActionExecutor>(action.Id)
.ToList();

string currentId = action.Id;
foreach (var ancestor in nonDiscreteAncestors)
{
string completionId = $"{action.Id}_Complete_{ancestor.Id}";
var completionStep = new DelegateActionExecutor(
completionId,
this._workflowState,
async (context, _, ct) => await context.RaiseCompletionEventAsync(ancestor.Model, ct).ConfigureAwait(false),
emitResult: false);

this._workflowModel.AddNode(completionStep, action.ParentId);
this._workflowModel.AddLink(currentId, completionId);
currentId = completionId;
}

// Define a clean-start to ensure "end" is not a source for any edge
this.RestartAfter(action.Id, action.ParentId);
this.RestartAfter(currentId, action.ParentId);
}

protected override void Visit(CancelAllDialogs item)
Expand All @@ -324,8 +361,28 @@ protected override void Visit(CancelAllDialogs item)
// Represent action with default executor
DefaultActionExecutor action = new(item, this._workflowState);
this.ContinueWith(action);

var nonDiscreteAncestors = this._workflowModel
.LocateNonDiscreteAncestors<DeclarativeActionExecutor>(action.Id)
.ToList();

string currentId = action.Id;
foreach (var ancestor in nonDiscreteAncestors)
{
string completionId = $"{action.Id}_Complete_{ancestor.Id}";
var completionStep = new DelegateActionExecutor(
completionId,
this._workflowState,
async (context, _, ct) => await context.RaiseCompletionEventAsync(ancestor.Model, ct).ConfigureAwait(false),
emitResult: false);

this._workflowModel.AddNode(completionStep, action.ParentId);
this._workflowModel.AddLink(currentId, completionId);
currentId = completionId;
}

// Define a clean-start to ensure "end" is not a source for any edge
this.RestartAfter(item.Id.Value, action.ParentId);
this.RestartAfter(currentId, action.ParentId);
}

protected override void Visit(CancelDialog item)
Expand All @@ -335,8 +392,27 @@ protected override void Visit(CancelDialog item)
// Represent action with default executor
DefaultActionExecutor action = new(item, this._workflowState);
this.ContinueWith(action);
// Define a clean-start to ensure "end" is not a source for any edge
this.RestartAfter(action.Id, action.ParentId);

var nonDiscreteAncestors = this._workflowModel
.LocateNonDiscreteAncestors<DeclarativeActionExecutor>(action.Id)
.ToList();

string currentId = action.Id;
foreach (var ancestor in nonDiscreteAncestors)
{
string completionId = $"{action.Id}_Complete_{ancestor.Id}";
var completionStep = new DelegateActionExecutor(
completionId,
this._workflowState,
async (context, _, ct) => await context.RaiseCompletionEventAsync(ancestor.Model, ct).ConfigureAwait(false),
emitResult: false);

this._workflowModel.AddNode(completionStep, action.ParentId);
this._workflowModel.AddLink(currentId, completionId);
currentId = completionId;
}

this.RestartAfter(currentId, action.ParentId);
}

protected override void Visit(CreateConversation item)
Expand Down Expand Up @@ -541,6 +617,44 @@ protected override void Visit(HttpRequestAction item)
this.ContinueWith(new HttpRequestExecutor(item, this._workflowOptions.HttpRequestHandler, this._workflowOptions.AgentProvider, this._workflowState));
}

private void InjectParentCompletionEvents(string terminalActionId, string targetId, string terminalParentId, string? stopBeforeParentId = null)
{
var nonDiscreteAncestors = this._workflowModel
.LocateNonDiscreteAncestors<DeclarativeActionExecutor>(terminalActionId)
.ToList();

if (nonDiscreteAncestors.Count == 0)
{
this._workflowModel.AddLink(terminalActionId, targetId);
return;
}

string currentId = terminalActionId;

foreach (var ancestor in nonDiscreteAncestors)
{
if (stopBeforeParentId is not null &&
string.Equals(ancestor.Id, stopBeforeParentId, StringComparison.Ordinal))
{
break;
}

string completionId = $"{terminalActionId}_Complete_{ancestor.Id}";
var completionStep = new DelegateActionExecutor(
completionId,
this._workflowState,
async (context, _, ct) => await context.RaiseCompletionEventAsync(ancestor.Model, ct).ConfigureAwait(false),
emitResult: false);

this._workflowModel.AddNode(completionStep, terminalParentId);
this._workflowModel.AddLink(currentId, completionId);

currentId = completionId;
}

this._workflowModel.AddLink(currentId, targetId);
}

#region Not supported

protected override void Visit(AnswerQuestionWithAI item) => this.NotSupported(item);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,43 @@ private ModelNode DefineNode(IModeledAction action, ModelNode? parentNode = null
return null;
}

public IEnumerable<TAction> LocateNonDiscreteAncestors<TAction>(string? itemId) where TAction : class, IModeledAction
{
if (string.IsNullOrEmpty(itemId))
{
yield break;
}

if (!this.Nodes.TryGetValue(itemId, out ModelNode? startNode))
{
yield break;
}

string? currentId = startNode.Parent?.Action.Id;

while (currentId is not null)
{
if (!this.Nodes.TryGetValue(currentId, out ModelNode? itemNode))
{
yield break;
}

if (itemNode.Action is TAction nonDiscreteAction)
{
Type actionType = nonDiscreteAction.GetType();
bool isNonDiscrete = actionType == typeof(Microsoft.Agents.AI.Workflows.Declarative.ObjectModel.ConditionGroupExecutor) ||
actionType == typeof(Microsoft.Agents.AI.Workflows.Declarative.ObjectModel.ForeachExecutor);

if (isNonDiscrete)
{
yield return nonDiscreteAction;
}
}

currentId = itemNode.Parent?.Action.Id;
}
}

private sealed class ModelNode(IModeledAction action, ModelNode? parent = null, Action? completionHandler = null)
{
public IModeledAction Action => action;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,21 @@ public async Task GotoActionAsync()
this.AssertNotExecuted("sendActivity_3");
}

[Fact]
public async Task GotoInsideCondition_RaisesParentCompletionEventAsync()
{
await this.RunWorkflowAsync("GotoInsideCondition.yaml", 1);

this.AssertExecuted("goto_end");

Assert.Contains(
this.WorkflowEvents.OfType<DeclarativeActionCompletedEvent>(),
e => e.ActionId == "condition_test");

this.AssertNotExecuted("send_activity_normal");
this.AssertNotExecuted("send_activity_after");
}

[Theory]
[InlineData(12)]
[InlineData(37)]
Expand Down Expand Up @@ -180,7 +195,7 @@ public async Task ConditionActionWithFallThroughAsync(int input, int expectedAct
[InlineData("ClearAllVariables.yaml", 1, "clear_all")]
[InlineData("ResetVariable.yaml", 2, "clear_var")]
[InlineData("MixedScopes.yaml", 2, "activity_input")]
[InlineData("CaseInsensitive.yaml", 6, "end_when_match")]
[InlineData("CaseInsensitive.yaml", 7, "end_when_match")]
[InlineData("HttpRequest.yaml", 1, "http_request")]
public async Task ExecuteActionAsync(string workflowFile, int expectedCount, string expectedId)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
kind: Workflow
trigger:

kind: OnConversationStart
id: my_workflow
actions:

- kind: SetVariable
id: set_input
variable: Local.TestValue
value: "=Value(System.LastMessageText)"

- kind: ConditionGroup
id: condition_test
conditions:
- id: condition_goto_branch
condition: "=Local.TestValue = 1"
actions:
- kind: GotoAction
id: goto_end
actionId: end_all

- id: condition_other_branch
condition: "=Local.TestValue <> 1"
actions:
- kind: SendActivity
id: send_activity_normal
activity: NORMAL PATH

- kind: SendActivity
id: send_activity_after
activity: SHOULD NOT SEE THIS

- kind: EndConversation
id: end_all
Loading