Files
agent-framework/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowTemplateVisitor.cs
T
2025-10-02 20:06:40 +00:00

482 lines
17 KiB
C#

// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Diagnostics;
using System.Linq;
using Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.Bot.ObjectModel;
namespace Microsoft.Agents.AI.Workflows.Declarative.Interpreter;
internal sealed class WorkflowTemplateVisitor : DialogActionVisitor
{
private readonly string _rootId;
private readonly WorkflowModel<string> _workflowModel;
public WorkflowTemplateVisitor(
string workflowId,
WorkflowTypeInfo typeInfo)
{
this._rootId = workflowId;
this._workflowModel = new WorkflowModel<string>(new RootTemplate(workflowId, typeInfo));
WorkflowDiagnostics.SetFoundryProduct();
}
public bool HasUnsupportedActions { get; private set; }
public string Complete(string? workflowNamespace = null, string? workflowPrefix = null)
{
WorkflowCodeBuilder builder = new(this._rootId);
this._workflowModel.Build(builder);
return builder.GenerateCode(workflowNamespace, workflowPrefix);
}
protected override void Visit(ActionScope item)
{
this.Trace(item);
string parentId = GetParentId(item);
// Handle case where root element is its own parent
if (item.Id.Equals(parentId))
{
parentId = WorkflowActionVisitor.Steps.Root(parentId);
}
this.ContinueWith(new EmptyTemplate(item.Id.Value, this._rootId), parentId, condition: null, CompletionHandler);
//// Complete the action scope.
void CompletionHandler()
{
// No completion for root scope
if (this._workflowModel.GetDepth(item.Id.Value) > 1)
{
// Define post action for this scope
string completionId = this.ContinuationFor(item.Id.Value);
this._workflowModel.AddLinkFromPeer(item.Id.Value, completionId);
// Transition to post action of parent scope
this._workflowModel.AddLink(completionId, WorkflowActionVisitor.Steps.Post(parentId));
}
}
}
public override void VisitConditionItem(ConditionItem item)
{
this.Trace(item);
string parentId = GetParentId(item);
ConditionGroupTemplate? conditionGroup = this._workflowModel.LocateParent<ConditionGroupTemplate>(parentId);
if (conditionGroup is not null)
{
string stepId = ConditionGroupExecutor.Steps.Item(conditionGroup.Model, item);
this._workflowModel.AddNode(new EmptyTemplate(stepId, this._rootId), parentId, CompletionHandler);
base.VisitConditionItem(item);
// Complete the condition item.
void CompletionHandler()
{
string completionId = this.ContinuationFor(stepId);
this._workflowModel.AddLink(completionId, WorkflowActionVisitor.Steps.Post(conditionGroup.Id));
// Merge link when no action group is defined
if (!item.Actions.Any())
{
this._workflowModel.AddLink(stepId, completionId);
}
}
}
}
protected override void Visit(ConditionGroup item)
{
this.Trace(item);
ConditionGroupTemplate action = new(item);
this.ContinueWith(action);
this.ContinuationFor(action.Id, parentId: action.ParentId);
string? lastConditionItemId = null;
foreach (ConditionItem conditionItem in item.Conditions)
{
// Create conditional link for conditional action
lastConditionItemId = ConditionGroupExecutor.Steps.Item(item, conditionItem);
this._workflowModel.AddLink(action.Id, lastConditionItemId, $@"ActionExecutor.IsMatch(""{lastConditionItemId}"", result)");
conditionItem.Accept(this);
}
if (item.ElseActions?.Actions.Length > 0)
{
if (lastConditionItemId is not null)
{
// Create clean start for else action from prior conditions
this.RestartAfter(lastConditionItemId, action.Id);
}
// Create conditional link for else action
string stepId = ConditionGroupExecutor.Steps.Else(item);
this._workflowModel.AddLink(action.Id, stepId, $@"ActionExecutor.IsMatch(""{stepId}"", result)");
}
}
protected override void Visit(GotoAction item)
{
this.Trace(item);
// Represent action with default executor
DefaultTemplate action = new(item, this._rootId);
this.ContinueWith(action);
// Transition to target action
this._workflowModel.AddLink(action.Id, item.ActionId.Value);
// Define a clean-start to ensure "goto" is not a source for any edge
this.RestartAfter(action.Id, action.ParentId);
}
protected override void Visit(Foreach item)
{
this.Trace(item);
// Entry point for loop
ForeachTemplate action = new(item);
string loopId = ForeachExecutor.Steps.Next(action.Id);
this.ContinueWith(action, condition: null, CompletionHandler); // Foreach
// Transition to select the next item
this.ContinueWith(new EmptyTemplate(loopId, this._rootId, $"{action.Id.FormatName()}.{nameof(ForeachExecutor.TakeNextAsync)}"), action.Id);
// Transition to post action if no more items
string continuationId = this.ContinuationFor(action.Id, parentId: action.ParentId); // Action continuation
this._workflowModel.AddLink(loopId, continuationId, $"!{action.Id.FormatName()}.{nameof(ForeachExecutor.HasValue)}");
// Transition to start of inner actions if there is a current item
string startId = ForeachExecutor.Steps.Start(action.Id);
this._workflowModel.AddNode(new EmptyTemplate(startId, this._rootId), action.Id);
this._workflowModel.AddLink(loopId, startId, $"{action.Id.FormatName()}.{nameof(ForeachExecutor.HasValue)}");
void CompletionHandler()
{
// Transition to end of inner actions
string endActionsId = ForeachExecutor.Steps.End(action.Id); // Loop continuation
this.ContinueWith(new EmptyTemplate(endActionsId, this._rootId, $"{action.Id.FormatName()}.{nameof(ForeachExecutor.ResetAsync)}"), action.Id);
// Transition to select the next item
this._workflowModel.AddLink(endActionsId, loopId);
}
}
protected override void Visit(BreakLoop item)
{
this.Trace(item);
// Locate the nearest "Foreach" loop that contains this action
ForeachTemplate? loopAction = this._workflowModel.LocateParent<ForeachTemplate>(item.GetParentId());
// Skip action if its not contained a loop
if (loopAction is not null)
{
// Represent action with default executor
DefaultTemplate action = new(item, this._rootId);
this.ContinueWith(action);
// Transition to post action
this._workflowModel.AddLink(action.Id, WorkflowActionVisitor.Steps.Post(loopAction.Id));
// Define a clean-start to ensure "break" is not a source for any edge
this.RestartAfter(action.Id, action.ParentId);
}
}
protected override void Visit(ContinueLoop item)
{
this.Trace(item);
// Locate the nearest "Foreach" loop that contains this action
ForeachTemplate? loopAction = this._workflowModel.LocateParent<ForeachTemplate>(item.GetParentId());
// Skip action if its not contained a loop
if (loopAction is not null)
{
// Represent action with default executor
DefaultTemplate action = new(item, this._rootId);
this.ContinueWith(action);
// Transition to select the next item
this._workflowModel.AddLink(action.Id, ForeachExecutor.Steps.Start(loopAction.Id));
// Define a clean-start to ensure "continue" is not a source for any edge
this.RestartAfter(action.Id, action.ParentId);
}
}
protected override void Visit(Question item)
{
this.NotSupported(item);
//this.Trace(item);
//this.ContinueWith(new QuestionTemplate(item));
}
protected override void Visit(EndDialog item)
{
this.Trace(item);
// Represent action with default executor
DefaultTemplate action = new(item, this._rootId);
this.ContinueWith(action);
// Define a clean-start to ensure "end" is not a source for any edge
this.RestartAfter(action.Id, action.ParentId);
}
protected override void Visit(EndConversation item)
{
this.Trace(item);
// Represent action with default executor
DefaultTemplate action = new(item, this._rootId);
this.ContinueWith(action);
// Define a clean-start to ensure "end" is not a source for any edge
this.RestartAfter(action.Id, action.ParentId);
}
protected override void Visit(CreateConversation item)
{
this.Trace(item);
this.ContinueWith(new CreateConversationTemplate(item));
}
protected override void Visit(AddConversationMessage item)
{
this.Trace(item);
this.ContinueWith(new AddConversationMessageTemplate(item));
}
protected override void Visit(CopyConversationMessages item)
{
this.Trace(item);
this.ContinueWith(new CopyConversationMessagesTemplate(item));
}
protected override void Visit(InvokeAzureAgent item)
{
this.Trace(item);
this.ContinueWith(new InvokeAzureAgentTemplate(item));
}
protected override void Visit(RetrieveConversationMessage item)
{
this.Trace(item);
this.ContinueWith(new RetrieveConversationMessageTemplate(item));
}
protected override void Visit(RetrieveConversationMessages item)
{
this.Trace(item);
this.ContinueWith(new RetrieveConversationMessagesTemplate(item));
}
protected override void Visit(SetVariable item)
{
this.Trace(item);
this.ContinueWith(new SetVariableTemplate(item));
}
protected override void Visit(SetMultipleVariables item)
{
this.Trace(item);
this.ContinueWith(new SetMultipleVariablesTemplate(item));
}
protected override void Visit(SetTextVariable item)
{
this.Trace(item);
this.ContinueWith(new SetTextVariableTemplate(item));
}
protected override void Visit(ClearAllVariables item)
{
this.Trace(item);
this.ContinueWith(new ClearAllVariablesTemplate(item));
}
protected override void Visit(ResetVariable item)
{
this.Trace(item);
this.ContinueWith(new ResetVariableTemplate(item));
}
protected override void Visit(EditTable item)
{
this.NotSupported(item);
//this.Trace(item);
//this.ContinueWith(new EditTableTemplate(item));
}
protected override void Visit(EditTableV2 item)
{
this.NotSupported(item);
//this.Trace(item);
//this.ContinueWith(new EditTableV2Template(item));
}
protected override void Visit(ParseValue item)
{
this.Trace(item);
this.ContinueWith(new ParseValueTemplate(item));
}
protected override void Visit(SendActivity item)
{
this.Trace(item);
this.ContinueWith(new SendActivityTemplate(item));
}
#region Not supported
protected override void Visit(AnswerQuestionWithAI item) => this.NotSupported(item);
protected override void Visit(DeleteActivity item) => this.NotSupported(item);
protected override void Visit(GetActivityMembers item) => this.NotSupported(item);
protected override void Visit(UpdateActivity item) => this.NotSupported(item);
protected override void Visit(ActivateExternalTrigger item) => this.NotSupported(item);
protected override void Visit(DisableTrigger item) => this.NotSupported(item);
protected override void Visit(WaitForConnectorTrigger item) => this.NotSupported(item);
protected override void Visit(InvokeConnectorAction item) => this.NotSupported(item);
protected override void Visit(InvokeCustomModelAction item) => this.NotSupported(item);
protected override void Visit(InvokeFlowAction item) => this.NotSupported(item);
protected override void Visit(InvokeAIBuilderModelAction item) => this.NotSupported(item);
protected override void Visit(InvokeSkillAction item) => this.NotSupported(item);
protected override void Visit(AdaptiveCardPrompt item) => this.NotSupported(item);
protected override void Visit(CSATQuestion item) => this.NotSupported(item);
protected override void Visit(OAuthInput item) => this.NotSupported(item);
protected override void Visit(BeginDialog item) => this.NotSupported(item);
protected override void Visit(UnknownDialogAction item) => this.NotSupported(item);
protected override void Visit(RepeatDialog item) => this.NotSupported(item);
protected override void Visit(ReplaceDialog item) => this.NotSupported(item);
protected override void Visit(CancelAllDialogs item) => this.NotSupported(item);
protected override void Visit(CancelDialog item) => this.NotSupported(item);
protected override void Visit(EmitEvent item) => this.NotSupported(item);
protected override void Visit(GetConversationMembers item) => this.NotSupported(item);
protected override void Visit(HttpRequestAction item) => this.NotSupported(item);
protected override void Visit(RecognizeIntent item) => this.NotSupported(item);
protected override void Visit(TransferConversation item) => this.NotSupported(item);
protected override void Visit(TransferConversationV2 item) => this.NotSupported(item);
protected override void Visit(SignOutUser item) => this.NotSupported(item);
protected override void Visit(LogCustomTelemetryEvent item) => this.NotSupported(item);
protected override void Visit(DisconnectedNodeContainer item) => this.NotSupported(item);
protected override void Visit(CreateSearchQuery item) => this.NotSupported(item);
protected override void Visit(SearchKnowledgeSources item) => this.NotSupported(item);
protected override void Visit(SearchAndSummarizeWithCustomModel item) => this.NotSupported(item);
protected override void Visit(SearchAndSummarizeContent item) => this.NotSupported(item);
#endregion
private void ContinueWith(
ActionTemplate action,
string? condition = null,
Action? completionHandler = null)
{
this.ContinueWith(action, action.ParentId, condition, completionHandler);
}
private void ContinueWith(
IModeledAction action,
string parentId,
string? condition = null,
Action? completionHandler = null)
{
this._workflowModel.AddNode(action, parentId, completionHandler);
this._workflowModel.AddLinkFromPeer(parentId, action.Id, condition);
}
private string ContinuationFor(string parentId, string? stepAction = null) => this.ContinuationFor(parentId, parentId, stepAction);
private string ContinuationFor(string actionId, string parentId, string? stepAction = null)
{
actionId = WorkflowActionVisitor.Steps.Post(actionId);
this._workflowModel.AddNode(new EmptyTemplate(actionId, this._rootId, stepAction), parentId);
return actionId;
}
private void RestartAfter(string actionId, string parentId) =>
this._workflowModel.AddNode(new EmptyTemplate(WorkflowActionVisitor.Steps.Restart(actionId), this._rootId), parentId);
private static string GetParentId(BotElement item) =>
item.GetParentId() ??
throw new DeclarativeModelException($"Missing parent ID for action element: {item.GetId()} [{item.GetType().Name}].");
private void NotSupported(DialogAction item)
{
Debug.WriteLine($"> UNKNOWN: {FormatItem(item)} => {FormatParent(item)}");
this.HasUnsupportedActions = true;
}
private void Trace(BotElement item) =>
Debug.WriteLine($"> VISIT: {new string('\t', this._workflowModel.GetDepth(item.GetParentId()))}{FormatItem(item)} => {FormatParent(item)}");
private void Trace(DialogAction item)
{
string? parentId = item.GetParentId();
if (item.Id.Equals(parentId ?? string.Empty))
{
parentId = WorkflowActionVisitor.Steps.Root(parentId);
}
Debug.WriteLine($"> VISIT: {new string('\t', this._workflowModel.GetDepth(parentId))}{FormatItem(item)} => {FormatParent(item)}");
}
private static string FormatItem(BotElement element) => $"{element.GetType().Name} ({element.GetId()})";
private static string FormatParent(BotElement element) =>
element.Parent is null ?
throw new DeclarativeModelException($"Undefined parent for {element.GetType().Name} that is member of {element.GetId()}.") :
$"{element.Parent.GetType().Name} ({element.GetParentId()})";
}