// Copyright (c) Microsoft. All rights reserved. using System; using System.Diagnostics; using System.Linq; using Microsoft.Agents.AI.Workflows.Declarative.Events; using Microsoft.Agents.AI.Workflows.Declarative.Extensions; using Microsoft.Agents.AI.Workflows.Declarative.Kit; using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; using Microsoft.Agents.ObjectModel; namespace Microsoft.Agents.AI.Workflows.Declarative.Interpreter; internal sealed class WorkflowActionVisitor : DialogActionVisitor { private const string DefaultWorkflowId = "workflow"; internal static class Steps { public static string Root(AdaptiveDialog action) => $"{action.BeginDialog?.Id.Value ?? DefaultWorkflowId}_{nameof(Root)}"; public static string Root(string? actionId = null) => $"{actionId ?? DefaultWorkflowId}_{nameof(Root)}"; public static string Post(string actionId) => $"{actionId}_{nameof(Post)}"; public static string Restart(string actionId) => $"{actionId}_{nameof(Restart)}"; } private readonly Executor _rootAction; private readonly WorkflowModel> _workflowModel; private readonly DeclarativeWorkflowOptions _workflowOptions; private readonly WorkflowFormulaState _workflowState; public WorkflowActionVisitor( Executor rootAction, WorkflowFormulaState state, DeclarativeWorkflowOptions options) { this._rootAction = rootAction; this._workflowModel = new WorkflowModel>((IModeledAction)rootAction); this._workflowOptions = options; this._workflowState = state; } public bool HasUnsupportedActions { get; private set; } public Workflow Complete() { WorkflowModelBuilder builder = new(this._rootAction); this._workflowModel.Build(builder); // Apply telemetry if configured if (this._workflowOptions.IsTelemetryEnabled) { builder.WorkflowBuilder.WithOpenTelemetry( this._workflowOptions.ConfigureTelemetry, this._workflowOptions.TelemetryActivitySource); } // Build final workflow return builder.WorkflowBuilder.Build(validateOrphans: false); } 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 = Steps.Root(parentId); } this.ContinueWith(new DelegateActionExecutor(item.Id.Value, this._workflowState), parentId, condition: null, CompletionHandler); // Complete the action scope. void CompletionHandler() { // No completion for root scope if (this._workflowModel.GetDepth(item.Id.Value) > 1) { DelegateAction? action = null; ConditionGroupExecutor? conditionGroup = this._workflowModel.LocateParent(parentId); if (conditionGroup is not null) { action = conditionGroup.DoneAsync; } // Define post action for this scope string completionId = this.ContinuationFor(item.Id.Value, action); this._workflowModel.AddLinkFromPeer(item.Id.Value, completionId); // Transition to post action of parent scope this._workflowModel.AddLink(completionId, Steps.Post(parentId)); } } } public override void VisitConditionItem(ConditionItem item) { this.Trace(item); string parentId = GetParentId(item); ConditionGroupExecutor? conditionGroup = this._workflowModel.LocateParent(parentId); if (conditionGroup is not null) { string stepId = ConditionGroupExecutor.Steps.Item(conditionGroup.Model, item); this._workflowModel.AddNode(new DelegateActionExecutor(stepId, this._workflowState), parentId, CompletionHandler); base.VisitConditionItem(item); // Complete the condition item. void CompletionHandler() { string completionId = this.ContinuationFor(stepId, conditionGroup.DoneAsync); // End items this._workflowModel.AddLink(completionId, Steps.Post(conditionGroup.Id)); // Merge with parent scope // 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); ConditionGroupExecutor action = new(item, this._workflowState); this.ContinueWith(action); this.ContinuationFor(action.Id, 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, (result) => action.IsMatch(conditionItem, result)); conditionItem.Accept(this); } if (lastConditionItemId is not null) { // Create clean start for else action from prior conditions this.RestartAfter(lastConditionItemId, action.Id); } if (item.ElseActions?.Actions.Length > 0) { // Create conditional link for else action string stepId = ConditionGroupExecutor.Steps.Else(item); this._workflowModel.AddLink(action.Id, stepId, action.IsElse); } else { string stepId = Steps.Post(action.Id); this._workflowModel.AddLink(action.Id, stepId, action.IsElse); } } protected override void Visit(GotoAction item) { this.Trace(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); // 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 ForeachExecutor action = new(item, this._workflowState); string loopId = ForeachExecutor.Steps.Next(action.Id); this.ContinueWith(action, condition: null, CompletionHandler); // Transition to select the next item this.ContinueWith(new DelegateActionExecutor(loopId, this._workflowState, action.TakeNextAsync), action.Id); // Transition to post action if no more items string continuationId = this.ContinuationFor(action.Id, action.ParentId); this._workflowModel.AddLink(loopId, continuationId, (_) => !action.HasValue); // Transition to start of inner actions if there is a current item string startId = ForeachExecutor.Steps.Start(action.Id); this._workflowModel.AddNode(new DelegateActionExecutor(startId, this._workflowState), action.Id); this._workflowModel.AddLink(loopId, startId, (_) => action.HasValue); void CompletionHandler() { // Transition to end of inner actions string endActionsId = ForeachExecutor.Steps.End(action.Id); this.ContinueWith(new DelegateActionExecutor(endActionsId, this._workflowState, action.CompleteAsync), 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 ForeachExecutor? loopAction = this._workflowModel.LocateParent(item.GetParentId()); // Skip action if its not contained a loop if (loopAction is not null) { // 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)); // 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 ForeachExecutor? loopAction = this._workflowModel.LocateParent(item.GetParentId()); // Skip action if its not contained a loop if (loopAction is not null) { // 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)); // 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.Trace(item); // Entry point for question QuestionExecutor action = new(item, this._workflowOptions.AgentProvider, this._workflowState); this.ContinueWith(action); // Transition to post action if complete string postId = Steps.Post(action.Id); this._workflowModel.AddLink(action.Id, postId, QuestionExecutor.IsComplete); // Perpare for input request if not complete string prepareId = QuestionExecutor.Steps.Prepare(action.Id); this.ContinueWith(new DelegateActionExecutor(prepareId, this._workflowState, action.PrepareResponseAsync, emitResult: false), action.ParentId, message => !QuestionExecutor.IsComplete(message)); // Define input action string inputId = QuestionExecutor.Steps.Input(action.Id); RequestPortAction inputPort = new(RequestPort.Create(inputId)); this._workflowModel.AddNode(inputPort, action.ParentId); this._workflowModel.AddLinkFromPeer(action.ParentId, inputId); // Capture input response string captureId = QuestionExecutor.Steps.Capture(action.Id); this.ContinueWith(new DelegateActionExecutor(captureId, this._workflowState, action.CaptureResponseAsync, emitResult: false), action.ParentId); // Transition to post action if complete this.ContinueWith(new DelegateActionExecutor(postId, this._workflowState, action.CompleteAsync), action.ParentId, QuestionExecutor.IsComplete); // Transition to prepare action if not complete this._workflowModel.AddLink(captureId, prepareId, message => !QuestionExecutor.IsComplete(message)); } protected override void Visit(RequestExternalInput item) { this.Trace(item); RequestExternalInputExecutor action = new(item, this._workflowOptions.AgentProvider, this._workflowState); this.ContinueWith(action); // Define input action string inputId = RequestExternalInputExecutor.Steps.Input(action.Id); RequestPortAction inputPort = new(RequestPort.Create(inputId)); this._workflowModel.AddNode(inputPort, action.ParentId); this._workflowModel.AddLinkFromPeer(action.ParentId, inputId); // Capture input response string captureId = RequestExternalInputExecutor.Steps.Capture(action.Id); this.ContinueWith(new DelegateActionExecutor(captureId, this._workflowState, action.CaptureResponseAsync), action.ParentId); } protected override void Visit(EndDialog item) { this.Trace(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(item.Id.Value, action.ParentId); } protected override void Visit(EndConversation item) { this.Trace(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); } protected override void Visit(CancelAllDialogs item) { this.Trace(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(item.Id.Value, action.ParentId); } protected override void Visit(CancelDialog item) { this.Trace(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); } protected override void Visit(CreateConversation item) { this.Trace(item); this.ContinueWith(new CreateConversationExecutor(item, this._workflowOptions.AgentProvider, this._workflowState)); } protected override void Visit(AddConversationMessage item) { this.Trace(item); this.ContinueWith(new AddConversationMessageExecutor(item, this._workflowOptions.AgentProvider, this._workflowState)); } protected override void Visit(CopyConversationMessages item) { this.Trace(item); this.ContinueWith(new CopyConversationMessagesExecutor(item, this._workflowOptions.AgentProvider, this._workflowState)); } protected override void Visit(InvokeAzureAgent item) { this.Trace(item); // Entry point to invoke agent InvokeAzureAgentExecutor action = new(item, this._workflowOptions.AgentProvider, this._workflowState); this.ContinueWith(action); // Transition to post action if complete string postId = Steps.Post(action.Id); this._workflowModel.AddLink(action.Id, postId, InvokeAzureAgentExecutor.RequiresNothing); // Define request-port for function calling action string externalInputPortId = InvokeAzureAgentExecutor.Steps.ExternalInput(action.Id); RequestPortAction externalInputPort = new(RequestPort.Create(externalInputPortId)); this._workflowModel.AddNode(externalInputPort, action.ParentId); this._workflowModel.AddLink(action.Id, externalInputPortId, InvokeAzureAgentExecutor.RequiresInput); // Request ports always transitions to resume string resumeId = InvokeAzureAgentExecutor.Steps.Resume(action.Id); this._workflowModel.AddNode(new DelegateActionExecutor(resumeId, this._workflowState, action.ResumeAsync, emitResult: false), action.ParentId); this._workflowModel.AddLink(externalInputPortId, resumeId); // Transition to post action if complete this._workflowModel.AddLink(resumeId, postId, InvokeAzureAgentExecutor.RequiresNothing); // Transition to request port if more input is required this._workflowModel.AddLink(resumeId, externalInputPortId, InvokeAzureAgentExecutor.RequiresInput); // Define post action this._workflowModel.AddNode(new DelegateActionExecutor(postId, this._workflowState, action.CompleteAsync), action.ParentId); } protected override void Visit(InvokeFunctionTool item) { this.Trace(item); // Entry point to invoke function tool - always yields for external execution InvokeFunctionToolExecutor action = new(item, this._workflowOptions.AgentProvider, this._workflowState); this.ContinueWith(action); // Define request-port for function tool invocation (always requires external input) string externalInputPortId = InvokeFunctionToolExecutor.Steps.ExternalInput(action.Id); RequestPortAction externalInputPort = new(RequestPort.Create(externalInputPortId)); this._workflowModel.AddNode(externalInputPort, action.ParentId); this._workflowModel.AddLinkFromPeer(action.ParentId, externalInputPortId); // Capture response when external input is received string resumeId = InvokeFunctionToolExecutor.Steps.Resume(action.Id); this.ContinueWith( new DelegateActionExecutor(resumeId, this._workflowState, action.CaptureResponseAsync), action.ParentId); } protected override void Visit(InvokeAzureResponse item) { this.NotSupported(item); } protected override void Visit(RetrieveConversationMessage item) { this.Trace(item); this.ContinueWith(new RetrieveConversationMessageExecutor(item, this._workflowOptions.AgentProvider, this._workflowState)); } protected override void Visit(RetrieveConversationMessages item) { this.Trace(item); this.ContinueWith(new RetrieveConversationMessagesExecutor(item, this._workflowOptions.AgentProvider, this._workflowState)); } protected override void Visit(SetVariable item) { this.Trace(item); this.ContinueWith(new SetVariableExecutor(item, this._workflowState)); } protected override void Visit(SetMultipleVariables item) { this.Trace(item); this.ContinueWith(new SetMultipleVariablesExecutor(item, this._workflowState)); } protected override void Visit(SetTextVariable item) { this.Trace(item); this.ContinueWith(new SetTextVariableExecutor(item, this._workflowState)); } protected override void Visit(ClearAllVariables item) { this.Trace(item); this.ContinueWith(new ClearAllVariablesExecutor(item, this._workflowState)); } protected override void Visit(ResetVariable item) { this.Trace(item); this.ContinueWith(new ResetVariableExecutor(item, this._workflowState)); } protected override void Visit(EditTable item) { this.Trace(item); this.ContinueWith(new EditTableExecutor(item, this._workflowState)); } protected override void Visit(EditTableV2 item) { this.Trace(item); this.ContinueWith(new EditTableV2Executor(item, this._workflowState)); } protected override void Visit(ParseValue item) { this.Trace(item); this.ContinueWith(new ParseValueExecutor(item, this._workflowState)); } protected override void Visit(SendActivity item) { this.Trace(item); this.ContinueWith(new SendActivityExecutor(item, this._workflowState)); } protected override void Visit(InvokeMcpTool item) { this.Trace(item); // Verify MCP handler is configured if (this._workflowOptions.McpToolHandler is null) { throw new DeclarativeModelException("MCP tool handler not configured. Set McpToolHandler in DeclarativeWorkflowOptions to use InvokeMcpTool actions."); } // Entry point to invoke MCP tool - may yield for approval InvokeMcpToolExecutor action = new(item, this._workflowOptions.McpToolHandler, this._workflowOptions.AgentProvider, this._workflowState); this.ContinueWith(action); // Transition to post action if no external input is required (no approval needed) string postId = Steps.Post(action.Id); this._workflowModel.AddLink(action.Id, postId, InvokeMcpToolExecutor.RequiresNothing); // If approval is required, define request-port for approval flow string externalInputPortId = InvokeMcpToolExecutor.Steps.ExternalInput(action.Id); RequestPortAction externalInputPort = new(RequestPort.Create(externalInputPortId)); this._workflowModel.AddNode(externalInputPort, action.ParentId); this._workflowModel.AddLink(action.Id, externalInputPortId, InvokeMcpToolExecutor.RequiresInput); // Capture response when external input is received string resumeId = InvokeMcpToolExecutor.Steps.Resume(action.Id); this._workflowModel.AddNode(new DelegateActionExecutor(resumeId, this._workflowState, action.CaptureResponseAsync), action.ParentId); this._workflowModel.AddLink(externalInputPortId, resumeId); // After resume, transition to post action this._workflowModel.AddLink(resumeId, postId); // Define post action (completion) this._workflowModel.AddNode(new DelegateActionExecutor(postId, this._workflowState, action.CompleteAsync), action.ParentId); } #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(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( DeclarativeActionExecutor executor, Func? condition = null, Action? completionHandler = null) { executor.Logger = this._workflowOptions.LoggerFactory.CreateLogger(executor.Id); this.ContinueWith(executor, executor.ParentId, condition, completionHandler); } private void ContinueWith( IModeledAction action, string parentId, Func? condition = null, Action? completionHandler = null) { this._workflowModel.AddNode(action, parentId, completionHandler); this._workflowModel.AddLinkFromPeer(parentId, action.Id, condition); } private string ContinuationFor(string parentId, DelegateAction? stepAction = null) => this.ContinuationFor(parentId, parentId, stepAction); private string ContinuationFor(string actionId, string parentId, DelegateAction? stepAction = null) { actionId = Steps.Post(actionId); this._workflowModel.AddNode(new DelegateActionExecutor(actionId, this._workflowState, stepAction), parentId); return actionId; } private void RestartAfter(string actionId, string parentId) => this._workflowModel.AddNode(new DelegateActionExecutor(Steps.Restart(actionId), this._workflowState), 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: {new string('\t', this._workflowModel.GetDepth(item.GetParentId()))}{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 = 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()})"; }