From 7942350262f21305a1974ed8a76b4dda35842cae Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 15 Jul 2025 13:22:06 -0700 Subject: [PATCH 01/40] Ported from Fork --- dotnet/Directory.Packages.props | 2 + .../GettingStartedWithProcesses.csproj | 6 + .../Step02/Step02b_AccountOpening.cs | 1 - .../Step04/Steps/RenderMessageStep.cs | 2 +- .../Step06/DeepResearchAgent.fdl | 144 ++++++ .../Step06/Step06_WorkflowProcess.cs | 34 ++ .../Step06/conditionalTest.yaml | 72 +++ .../Step06/deepResearch.yaml | 473 ++++++++++++++++++ .../Step06/loopTest.yaml | 56 +++ .../KernelProcessDelegateStep.cs | 45 ++ .../KernelProcessDelegateStepInfo.cs | 31 ++ .../KernelProcessStepState.cs | 2 +- .../Process.Core/Process.Core.csproj | 12 +- .../Process.Core/ProcessBuilder.cs | 24 +- .../Process.Core/ProcessDelegateBuilder.cs | 43 ++ .../ProcessFunctionTargetBuilder.cs | 8 + .../Actions/AnswerQuestionWithAIAction.cs | 49 ++ .../ObjectModel/Actions/BeginDialogAction.cs | 22 + .../Actions/ConditionGroupAction.cs | 30 ++ .../ObjectModel/Actions/EditTableV2Action.cs | 82 +++ .../Actions/EndConversationAction.cs | 21 + .../ObjectModel/Actions/ParseValueAction.cs | 75 +++ .../ObjectModel/Actions/SendActivityAction.cs | 29 ++ .../ObjectModel/Actions/SetVariableAction.cs | 36 ++ .../Exceptions/InvalidActionException.cs | 35 ++ .../Exceptions/ProcessActionException.cs | 35 ++ .../ObjectModel/ObjectModelBuilder.cs | 27 + .../PowerFx/FormulaValueExtensions.cs | 68 +++ .../PowerFx/RecalcEngineExtensions.cs | 72 +++ .../PowerFx/RecalcEngineFactory.cs | 39 ++ .../ObjectModel/PowerFx/TemplateExtensions.cs | 49 ++ .../Workflow/ObjectModel/ProcessAction.cs | 60 +++ .../ObjectModel/ProcessActionContext.cs | 42 ++ .../ObjectModel/ProcessActionEnvironment.cs | 49 ++ .../ObjectModel/ProcessActionScopes.cs | 53 ++ .../ObjectModel/ProcessActionVisitor.cs | 469 +++++++++++++++++ .../ObjectModel/ProcessElementWalker.cs | 81 +++ .../Process.LocalRuntime/LocalDelegateStep.cs | 40 ++ .../Process.LocalRuntime/LocalProcess.cs | 8 + .../AnswerQuestionWithAIActionTest.cs | 21 + .../Workflow/ProcessActionTest.cs | 10 + .../Workflow/RecalcEngineFactoryTests.cs | 77 +++ .../Workflow/RecalcEngineTests.cs | 99 ++++ 43 files changed, 2615 insertions(+), 18 deletions(-) create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step06/DeepResearchAgent.fdl create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step06/conditionalTest.yaml create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step06/deepResearch.yaml create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step06/loopTest.yaml create mode 100644 dotnet/src/Experimental/Process.Abstractions/KernelProcessDelegateStep.cs create mode 100644 dotnet/src/Experimental/Process.Abstractions/KernelProcessDelegateStepInfo.cs create mode 100644 dotnet/src/Experimental/Process.Core/ProcessDelegateBuilder.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AnswerQuestionWithAIAction.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/BeginDialogAction.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ConditionGroupAction.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EditTableV2Action.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EndConversationAction.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ParseValueAction.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SendActivityAction.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetVariableAction.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/InvalidActionException.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/ProcessActionException.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/FormulaValueExtensions.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineFactory.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/TemplateExtensions.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessAction.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionContext.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionEnvironment.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionScopes.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessElementWalker.cs create mode 100644 dotnet/src/Experimental/Process.LocalRuntime/LocalDelegateStep.cs create mode 100644 dotnet/src/Experimental/Process.UnitTests/Workflow/AnswerQuestionWithAIActionTest.cs create mode 100644 dotnet/src/Experimental/Process.UnitTests/Workflow/ProcessActionTest.cs create mode 100644 dotnet/src/Experimental/Process.UnitTests/Workflow/RecalcEngineFactoryTests.cs create mode 100644 dotnet/src/Experimental/Process.UnitTests/Workflow/RecalcEngineTests.cs diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index c60c6aedd669..888fe0bd9158 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -53,6 +53,7 @@ + @@ -60,6 +61,7 @@ + diff --git a/dotnet/samples/GettingStartedWithProcesses/GettingStartedWithProcesses.csproj b/dotnet/samples/GettingStartedWithProcesses/GettingStartedWithProcesses.csproj index 7244d2cb967b..5b2c0008628f 100644 --- a/dotnet/samples/GettingStartedWithProcesses/GettingStartedWithProcesses.csproj +++ b/dotnet/samples/GettingStartedWithProcesses/GettingStartedWithProcesses.csproj @@ -66,4 +66,10 @@ + + + Always + + + \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Step02b_AccountOpening.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Step02b_AccountOpening.cs index 15e6fc692701..2110593cac5e 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step02/Step02b_AccountOpening.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Step02b_AccountOpening.cs @@ -29,7 +29,6 @@ private KernelProcess SetupAccountOpeningProcess() where TUserIn var accountVerificationStep = process.AddStepFromProcess(NewAccountVerificationProcess.CreateProcess()); var accountCreationStep = process.AddStepFromProcess(NewAccountCreationProcess.CreateProcess()); - var mailServiceStep = process.AddStepFromType(); process diff --git a/dotnet/samples/GettingStartedWithProcesses/Step04/Steps/RenderMessageStep.cs b/dotnet/samples/GettingStartedWithProcesses/Step04/Steps/RenderMessageStep.cs index c095373d5041..e843ca5c52a9 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step04/Steps/RenderMessageStep.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step04/Steps/RenderMessageStep.cs @@ -23,7 +23,7 @@ public static class ProcessStepFunctions public const string RenderUserText = nameof(RenderMessageStep.RenderUserText); } - private readonly static Stopwatch s_timer = Stopwatch.StartNew(); + private static readonly Stopwatch s_timer = Stopwatch.StartNew(); /// /// Render an explicit message to indicate the process has completed in the expected state. diff --git a/dotnet/samples/GettingStartedWithProcesses/Step06/DeepResearchAgent.fdl b/dotnet/samples/GettingStartedWithProcesses/Step06/DeepResearchAgent.fdl new file mode 100644 index 000000000000..536342dd325c --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step06/DeepResearchAgent.fdl @@ -0,0 +1,144 @@ +name: deepresearch +states: + - name: GatherFacts + actors: + - agent: LedgerFacts + inputs: + instructions: instructions + outputs: + task: task + facts: facts + thread: Planning + humanInLoopMode: onNoMessage + streamOutput: false + isFinal: false + - name: Plan + actors: + - agent: LedgerPlanner + inputs: + task: task + facts: facts + team: team + instructions: instructions + messagesOut: plannerMessages + thread: Planning + humanInLoopMode: never + streamOutput: true + isFinal: false + - name: ProcessProgress + actors: + - agent: ProgressLedger + inputs: + task: task + team: team + systemAgents: systemAgents + messagesOut: nextStepMessages + messagesIn: + - plannerMessages + thread: Run + humanInLoopMode: never + streamOutput: true + isFinal: false + - name: actionRouter + actors: + - agent: ActionRouterAgent + messagesIn: + - nextStepMessages + inputs: + team: team + systemAgents: systemAgents + outputs: + targetAgent: nextAgent + humanInLoopMode: never + streamOutput: true + - name: dynamicStepAgent + actors: + - agent: nextAgent + thread: Run + humanInLoopMode: never + streamOutput: true + - name: UpdateLedgerFact + actors: + - agent: LedgerFactsUpdate + thread: Run + inputs: + task: task + facts: facts + outputs: + updatedFacts: facts + humanInLoopMode: never + streamOutput: false + isFinal: false + - name: LedgerPlanUpdate + actors: + - agent: LedgerPlanUpdate + inputs: + facts: facts + team: team + messagesOut: plannerMessages + thread: Run + humanInLoopMode: never + streamOutput: true + isFinal: false + - name: Summarizer + actors: + - agent: FinalStepAgent + thread: Run + inputs: + task: task + humanInLoopMode: never + streamOutput: true + isFinal: true +transitions: + - from: GatherFacts + to: Plan + - from: Plan + to: ProcessProgress + - from: LedgerPlanUpdate + to: ProcessProgress + - from: ProcessProgress + to: actionRouter + - from: actionRouter + to: UpdateLedgerFact + condition: nextAgent.Equals(LedgerFactsUpdate) + - from: actionRouter + to: Summarizer + condition: nextAgent.Equals(FinalStepAgent) + - from: actionRouter + to: dynamicStepAgent + condition: nextAgent.NotContains(FinalStepAgent) + - from: dynamicStepAgent + to: ProcessProgress + - from: UpdateLedgerFact + to: LedgerPlanUpdate +variables: + - Type: userDefined + name: team + - Type: userDefined + name: instructions + - Type: userDefined + name: task + - Type: userDefined + name: facts + - Type: userDefined + name: plan + - Type: messages + name: plannerMessages + - Type: thread + name: Planning + - Type: thread + name: Run + - Type: messages + name: nextStepMessages + - Type: userDefined + name: nextAgent + - Type: userDefined + name: systemAgents + value: + - agent: FinalStepAgent + description: >- + Agent which summarizes the output after task is complete. When next speaker is none. + - agent: LedgerFactsUpdate + description: >- + Agent which can update the plan if we are looping without making progress or stall is detected. +startstate: GatherFacts diff --git a/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs b/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs new file mode 100644 index 000000000000..afce13adc282 --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel; + +namespace Step06; + +public class Step06_WorkflowProcess : BaseTest +{ + // Target Open AI Services + protected override bool ForceOpenAI => true; + + public Step06_WorkflowProcess(ITestOutputHelper output) + : base(output, redirectSystemConsoleOutput: true) { } + + [Fact] + public async Task RunWorkflowProcess() + { + const string InputEventId = "question"; + const string FileName = "conditionalTest"; + //const string FileName = "deepResearch"; + + Console.WriteLine("$$$ PROCESS INIT"); + + string yaml = File.ReadAllText(@$"{nameof(Step06)}\{FileName}.yaml"); + KernelProcess process = ObjectModelBuilder.Build(FileName, yaml, InputEventId); + + Console.WriteLine("$$$ PROCESS INVOKE"); + + Kernel kernel = this.CreateKernelWithChatCompletion(); + await using LocalKernelProcessContext context = await process.StartAsync(kernel, new KernelProcessEvent() { Id = InputEventId, Data = "Why is the sky blue?" }); + + Console.WriteLine("$$$ PROCESS DONE"); + } +} diff --git a/dotnet/samples/GettingStartedWithProcesses/Step06/conditionalTest.yaml b/dotnet/samples/GettingStartedWithProcesses/Step06/conditionalTest.yaml new file mode 100644 index 000000000000..5b0b62a75492 --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step06/conditionalTest.yaml @@ -0,0 +1,72 @@ +kind: AdaptiveDialog +beginDialog: + kind: OnActivity + id: main + type: Message + actions: + - kind: SetVariable + id: setVariable_u4cBtN + displayName: Invocation count + variable: Topic.Count + value: =0 + + - kind: GotoAction + id: goto_skJ8u + actionId: setVariable_a9f4o2 + + - kind: SendActivity + id: sendActivity_skJ8u + activity: NEVER A! + + - kind: SetVariable + id: setVariable_a9f4o2 + displayName: Invocation count + variable: Topic.Count + value: =Topic.Count + 1 + + - kind: SendActivity + id: sendActivity_aGsbRo + activity: Looping (x{Topic.Count}) + + - kind: ConditionGroup + id: conditionGroup_mVIecC + conditions: + - id: conditionItem_fj432c + condition: =Topic.Count < 5 + displayName: Just started + actions: + - kind: SendActivity + id: sendActivity_Pkkmpq + activity: Just started (x{Topic.Count}) + + - id: conditionItem_yiqund + condition: =Topic.Count > 5 && Topic.Count < 10 + displayName: Making progress + actions: + - kind: SendActivity + id: sendActivity_aLM1o3 + activity: Making progress (x{Topic.Count}) + + - kind: GotoAction + id: goto_LzfJ8u + actionId: setVariable_a9f4o2 + + elseActions: + - kind: SendActivity + id: sendActivity_rOk31p + activity: All done (x{Topic.Count}) + + - kind: EndConversation + id: end_SVoNSV + + - kind: SendActivity + id: sendActivity_fJsbRz + activity: Fallthrough (x{Topic.Count}) + + - kind: GotoAction + id: goto_fTJ8u + actionId: setVariable_a9f4o2 + + - kind: SendActivity + id: sendActivity_ohn03s + activity: NEVER B! diff --git a/dotnet/samples/GettingStartedWithProcesses/Step06/deepResearch.yaml b/dotnet/samples/GettingStartedWithProcesses/Step06/deepResearch.yaml new file mode 100644 index 000000000000..c31be83ada1b --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step06/deepResearch.yaml @@ -0,0 +1,473 @@ +kind: AdaptiveDialog +beginDialog: + kind: OnActivity + id: main + condition: =Global.OrchestratorRunning <> true + type: Message + actions: + - kind: SetVariable + id: setVariable_aASlmF + displayName: List all available agents for this orchestrator + variable: Topic.AgentToSchemaMapping + value: |- + =[ + { + name: "WeatherAgent", + description: "Able to retrieve weather information", + schema: "cr36e_agentRd2yAT.topic.Deterministic" + }, + { + name: "WebAgent", + description: "Able to perform generic websearches", + schema: "cr36e_agentRd2yAT.topic.WebSearch" + } + ] + + - kind: SetVariable + id: setVariable_u4cBtN + displayName: Get all names + variable: Topic.AvailableAgents + value: =DropColumns(Topic.AgentToSchemaMapping, description, schema) + + - kind: SetVariable + id: setVariable_V6yEbo + displayName: Get a summary of all the agents for use in prompts + variable: Topic.TeamDescription + value: "=Concat(ForAll(Topic.AgentToSchemaMapping, name & $\": \" & description), Value, \".\\n\\n\")" + + - kind: SetVariable + id: setVariable_eLmgKQ + displayName: Toggle Orchestration Enabled Flag + variable: Global.OrchestratorRunning + value: =true + + - kind: SetVariable + id: setVariable_NUiu0l + displayName: Set Task + variable: Topic.NewTask + value: =System.LastMessage.Text + + - kind: SetVariable + id: setVariable_PKmRsz + variable: Topic.ReTaskCount + value: 0 + + - kind: SetVariable + id: setVariable_EpFEKQ + displayName: Initialize Stall Count + variable: Topic.StallCount + value: =0 + + - kind: SendActivity + id: sendActivity_yFsbRz + activity: |- + Creating a Task Ledger, defining a plan, based on the following ask: + + {Topic.NewTask} + + - kind: SetVariable + id: setVariable_s8hR6q + variable: Topic.ContextHistory + value: |- + =["Below I will present you a request. Before we begin addressing the request, please answer the following pre-survey to the best of your ability. Keep in mind that you are Ken Jennings-level with trivia, and Mensa-level with puzzles, so there should be a deep well to draw from. + + Here is the request: + + " & Topic.NewTask & " + + Here is the pre-survey: + + 1. Please list any specific facts or figures that are GIVEN in the request itself. It is possible that there are none. + 2. Please list any facts that may need to be looked up, and WHERE SPECIFICALLY they might be found. In some cases, authoritative sources are mentioned in the request itself. + 3. Please list any facts that may need to be derived (e.g., via logical deduction, simulation, or computation) + 4. Please list any facts that are recalled from memory, hunches, well-reasoned guesses, etc. + + When answering this survey, keep in mind that 'facts' will typically be specific names, dates, statistics, etc. Your answer should use headings: + + 1. GIVEN OR VERIFIED FACTS + 2. FACTS TO LOOK UP + 3. FACTS TO DERIVE + 4. EDUCATED GUESSES + + DO NOT include any other headings or sections in your response. DO NOT list next steps or plans until asked to do so."] + + - kind: AnswerQuestionWithAI + id: question_wEJ123 + displayName: Get Facts Prompt + autoSend: false + variable: Topic.TaskFacts + userInput: =First(Topic.ContextHistory).Value + + - kind: EditTableV2 + id: editTableV2_Pry8em + displayName: Add Fact Response to Context + itemsVariable: Topic.ContextHistory + changeType: + kind: AddItemOperation + value: =Topic.TaskFacts + + - kind: AnswerQuestionWithAI + id: question_wEJ456 + displayName: Create a Plan Prompt + autoSend: false + variable: Topic.Plan + userInput: |- + ="Fantastic. To address this request we have assembled the following team: + + " & Topic.TeamDescription & " + + Based on the team composition, and known and unknown facts, please devise a short bullet-point plan for addressing the original request. Remember, there is no requirement to involve all team members -- a team member's particular expertise may not be needed for this task." + additionalInstructions: =Concat(Topic.ContextHistory, Value, \".\\n\\n\") + + - kind: SetVariable + id: setVariable_Kk2LDL + displayName: Set Plan as Context + variable: Topic.ContextHistory + value: |- + =[" + + We are working to address the following user request: + + " & Topic.NewTask &" + + + To answer this request we have assembled the following team: + + " & Topic.TeamDescription &" + + + Here is an initial fact sheet to consider: + + " & Topic.TaskFacts &" + + Here is the plan to follow as best as possible: + + " + & Topic.Plan + + ] + + - kind: SendActivity + id: sendActivity_bwNZiM + activity: "{First(Topic.ContextHistory).Value}" + + - kind: AnswerQuestionWithAI + id: sendActivity_YhpNE8 + displayName: Progress Ledger Prompt + autoSend: false + variable: Topic.ProgressLedgerUpdateString + userInput: |- + =" + Recall we are working on the following request: + + " & Topic.NewTask & " + + And we have assembled the following team: + + " & Topic.TeamDescription & " + + To make progress on the request, please answer the following questions, including necessary reasoning: + + - Is the request fully satisfied? (True if complete, or False if the original request has yet to be SUCCESSFULLY and FULLY addressed) + - Are we in a loop where we are repeating the same requests and / or getting the same responses from an agent multiple times? Loops can span multiple turns, and can include repeated actions like scrolling up or down more than a handful of times. + - Are we making forward progress? (True if just starting, or recent messages are adding value. False if recent messages show evidence of being stuck in a loop or if there is evidence of significant barriers to success such as the inability to read from a required file) + - Who should speak next? (select from: " & Concat(Topic.AvailableAgents, name, ",") & ") + - What instruction or question would you give this team member? (Phrase as if speaking directly to them, and include any specific information they may need) + + Please output an answer in pure JSON format according to the following schema. The JSON object must be parsable as-is. DO NOT OUTPUT ANYTHING OTHER THAN JSON, AND DO NOT DEVIATE FROM THIS SCHEMA: + + {{ + ""is_request_satisfied"": {{ + ""reason"": string, + ""answer"": boolean + }}, + ""is_in_loop"": {{ + ""reason"": string, + ""answer"": boolean + }}, + ""is_progress_being_made"": {{ + ""reason"": string, + ""answer"": boolean + }}, + ""next_speaker"": {{ + ""reason"": string, + ""answer"": string (select from: " & Concat(Topic.AvailableAgents, name, ",") & ") + }}, + ""instruction_or_question"": {{ + ""reason"": string, + ""answer"": string + }} + }} + " + additionalInstructions: =Concat(Topic.ContextHistory, Value, ".\\n\\n") + + - kind: ParseValue + id: rNZtlV + displayName: Parse ledger response + variable: Topic.TypedProgressLedger + valueType: + kind: Record + properties: + instruction_or_question: + type: + kind: Record + properties: + answer: String + reason: String + + is_in_loop: + type: + kind: Record + properties: + answer: Boolean + reason: String + + is_progress_being_made: + type: + kind: Record + properties: + answer: Boolean + reason: String + + is_request_satisfied: + type: + kind: Record + properties: + answer: Boolean + reason: String + + next_speaker: + type: + kind: Record + properties: + answer: String + reason: String + + value: =Topic.ProgressLedgerUpdateString + + - kind: SendActivity + id: sendActivity_1GMmNq + activity: |- + Progress Ledger response: + {Topic.ProgressLedgerUpdateString} + + - kind: ConditionGroup + id: conditionGroup_mVIecC + conditions: + - id: conditionItem_fj432c + condition: =Topic.TypedProgressLedger.is_request_satisfied.answer + displayName: If Done + actions: + - kind: AnswerQuestionWithAI + id: sendActivity_Pkkmpq + displayName: Generate Response + variable: Topic.FinalResponse + userInput: |- + =" + We are working on the following task: + " & Topic.NewTask & " + + We have completed the task. + + The above messages contain the conversation that took place to complete the task. + + Based on the information gathered, provide the final answer to the original request. + The answer should be phrased as if you were speaking to the user. + " + additionalInstructions: =Concat(Topic.ContextHistory, Value, \".\\n\\n\") + + - kind: SendActivity + id: sendActivity_fpaNL9 + activity: Done with Task! + + - kind: SetVariable + id: setVariable_H2GWZ4 + variable: Global.OrchestratorRunning + value: =false + + - kind: EndConversation + id: SVoNSV + + - id: conditionItem_yiqund + condition: =Topic.TypedProgressLedger.is_in_loop.answer || Not(Topic.TypedProgressLedger.is_progress_being_made.answer) + displayName: If Stalling + actions: + - kind: SetVariable + id: setVariable_H5lXdD + displayName: Increase stall count + variable: Topic.StallCount + value: =Topic.StallCount + 1 + + - kind: ConditionGroup + id: conditionGroup_xzNrdM + conditions: + - id: conditionItem_NlQTBv + condition: =Topic.StallCount > 2 + displayName: Stall Count Exceeded + actions: + - kind: ConditionGroup + id: conditionGroup_4s1Z27 + conditions: + - id: conditionItem_EXAlhZ + condition: =Topic.ReTaskCount > 2 + actions: + - kind: SendActivity + id: sendActivity_xKxFUU + activity: We tried to re-task 3 times. Short-Circuiting + + - kind: EndConversation + id: GHVrFh + + - kind: AnswerQuestionWithAI + id: question_wFJ123 + displayName: Get New Facts Prompt + autoSend: false + variable: Topic.TaskFacts + userInput: |- + ="As a reminder, we are working to solve the following task: + + " & Topic.NewTask & " + + It's clear we aren't making as much progress as we would like, but we may have learned something new. Please rewrite the following fact sheet, updating it to include anything new we have learned that may be helpful. Example edits can include (but are not limited to) adding new guesses, moving educated guesses to verified facts if appropriate, etc. Updates may be made to any section of the fact sheet, and more than one section of the fact sheet can be edited. This is an especially good time to update educated guesses, so please at least add or update one educated guess or hunch, and explain your reasoning. + + Here is the old fact sheet: + + " & Topic.TaskFacts + additionalInstructions: =Concat(Topic.ContextHistory, Value, ".\\n\\n") + + - kind: AnswerQuestionWithAI + id: question_uEJ456 + displayName: Create new Plan Prompt + autoSend: false + variable: Topic.Plan + userInput: |- + ="Please briefly explain what went wrong on this last run (the root cause of the failure), and then come up with a new plan that takes steps and/or includes hints to overcome prior challenges and especially avoids repeating the same mistakes. As before, the new plan should be concise, be expressed in bullet-point form, and consider the following team composition (do not involve any other outside people since we cannot contact anyone else): + + " & Topic.TeamDescription + additionalInstructions: =Concat(Topic.ContextHistory, Value, ".\\n\\n") + + - kind: EditTableV2 + id: editTableV2_jW7tmM + displayName: Add new plan to history + itemsVariable: Topic.ContextHistory + changeType: + kind: AddItemOperation + value: | + =" + + We are working to address the following user request: + + " & Topic.NewTask & " + + + To answer this request we have assembled the following team: + + " & Topic.TeamDescription & " + + + Here is an initial fact sheet to consider: + + " & Topic.TaskFacts & " + + Here is the plan to follow as best as possible: + + " + & Topic.Plan + + - kind: SetVariable + id: setVariable_6J2snP + displayName: Reset Stall count + variable: Topic.StallCount + value: 0 + + - kind: SetVariable + id: setVariable_S6HCgh + displayName: Increase ReTask count + variable: Topic.ReTaskCount + value: =Topic.ReTaskCount + 1 + + - kind: SendActivity + id: sendActivity_cwNZiM + activity: |- + We have Stalled - Adjusting plan: + + {Last(Topic.ContextHistory).Value} + + - kind: GotoAction + id: LzfJ8u + actionId: sendActivity_YhpNE8 + + elseActions: + - kind: SetVariable + id: setVariable_L7ooQO + variable: Topic.StallCount + value: 0 + + - kind: ConditionGroup + id: conditionGroup_QFPiF5 + conditions: + - id: conditionItem_GmigcU + condition: =CountRows(Search(Topic.AvailableAgents, Topic.TypedProgressLedger.next_speaker.answer, name)) > 0 + displayName: If next Agent tool Exists + actions: + - kind: SetVariable + id: setVariable_TdKfOn + displayName: Set Current Goal for Sub-Agent + variable: Global.AgentGoal + value: =Topic.TypedProgressLedger.instruction_or_question.answer + + - kind: SetVariable + id: setVariable_C2AoCu + displayName: Reset Output + variable: Global.AgentResponse + value: "\"\"" + + - kind: BeginDialog + id: fqLWPt + displayName: Invoke Agent + input: {} + dialog: =First(Search(Topic.AgentToSchemaMapping, Topic.TypedProgressLedger.next_speaker.answer, name)).schema + output: {} + + - kind: EditTableV2 + id: editTableV2_fhfYJi + displayName: Add agent response to context + itemsVariable: Topic.ContextHistory + changeType: + kind: AddItemOperation + value: |- + ="Agent: " & Topic.TypedProgressLedger.next_speaker.answer & " + + Question or Instruction: + + " + & Topic.TypedProgressLedger.instruction_or_question.answer & + " + + Agent Response: + + " + & Global.AgentResponse + + - kind: SendActivity + id: sendActivity_MjWETC + activity: |- + Agent invoked: + {Last(Topic.ContextHistory).Value} + + - kind: GotoAction + id: 76Hne8 + actionId: sendActivity_YhpNE8 + + elseActions: + - kind: SendActivity + id: sendActivity_BhcsI7 + activity: Redirecting to unknown agent + + - kind: SetVariable + id: setVariable_H2GW44 + variable: Global.OrchestratorRunning + value: =false + + - kind: EndConversation + id: 8nXE8H diff --git a/dotnet/samples/GettingStartedWithProcesses/Step06/loopTest.yaml b/dotnet/samples/GettingStartedWithProcesses/Step06/loopTest.yaml new file mode 100644 index 000000000000..84e4db170e48 --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step06/loopTest.yaml @@ -0,0 +1,56 @@ +kind: AdaptiveDialog +beginDialog: + kind: OnActivity + id: main + type: Message + actions: + - kind: SetVariable + id: setVariable_u4cBtN + displayName: Invocation count + variable: Topic.Count + value: =0 + + # - kind: SetVariable + # id: setVariable_a9f4o2 + # displayName: Invocation count + # variable: Topic.Count + # value: =Topic.Count + 1 + + # - kind: SendActivity + # id: sendActivity_aGsbRo + # activity: Looping (x{Topic.Count}) + + - kind: ConditionGroup + id: conditionGroup_mVIecC + conditions: + - id: conditionItem_fj432c + condition: =Topic.Count < 5 + displayName: Just started + actions: + - kind: SendActivity + id: sendActivity_Pkkmpq + activity: Just started (x{Topic.Count}) + + - id: conditionItem_yiqund + condition: =Topic.Count > 5 && Topic.Count < 10 + displayName: Making progress + actions: + - kind: SendActivity + id: sendActivity_aLM1o3 + activity: Making progress (x{Topic.Count}) + + - kind: GotoAction + id: goto_LzfJ8u + actionId: setVariable_a9f4o2 + + elseActions: + - kind: SendActivity + id: sendActivity_rOk31p + activity: All done (x{Topic.Count}) + + - kind: EndConversation + id: end_SVoNSV + + - kind: SendActivity + id: sendActivity_fJsbRz + activity: Complete! (x{Topic.Count}) diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessDelegateStep.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessDelegateStep.cs new file mode 100644 index 000000000000..275437bf5e36 --- /dev/null +++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessDelegateStep.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; + +namespace Microsoft.SemanticKernel; + +/// +/// %%% COMMENT +/// +/// +/// +public delegate Task StepFunction(Kernel kernel, KernelProcessStepContext context); + +/// +/// Step in a process that represents an ObjectModel. +/// +public class KernelDelegateProcessStep : KernelProcessStep +{ + /// + /// %%% COMMENT + /// + public const string FunctionName = "Invoke"; + + private readonly StepFunction _stepFunction; + + /// + /// Initializes a new instance of the class with the specified step function. + /// + /// + /// + public KernelDelegateProcessStep(StepFunction stepFunction) + { + this._stepFunction = stepFunction ?? throw new ArgumentNullException(nameof(stepFunction)); + } + + /// + /// Invokes the step function with the provided kernel and context. + /// + /// + /// + /// + [KernelFunction(FunctionName)] + public Task InvokeAsync(Kernel kernel, KernelProcessStepContext context) => this._stepFunction(kernel, context); +} diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessDelegateStepInfo.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessDelegateStepInfo.cs new file mode 100644 index 000000000000..fdb3c40426b1 --- /dev/null +++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessDelegateStepInfo.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; + +namespace Microsoft.SemanticKernel.Process; + +/// +/// Delegate step in a Kernel Process. +/// +public record KernelProcessDelegateStepInfo : KernelProcessStepInfo +{ + /// + /// Initializes a new instance of the class. + /// + /// // %%% COMMENTS + /// + /// + public KernelProcessDelegateStepInfo( + KernelProcessStepState state, + StepFunction stepFunction, + Dictionary> edges) : + base(typeof(KernelDelegateProcessStep), state, edges, incomingEdgeGroups: null) + { + this.StepFunction = stepFunction; + } + + /// + /// Ste funtion + /// + public StepFunction StepFunction { get; } +} diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepState.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepState.cs index e4e2b816cb8c..24bfd3e7128d 100644 --- a/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepState.cs +++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepState.cs @@ -17,7 +17,7 @@ public record KernelProcessStepState /// /// A set of known types that may be used in serialization. /// - private readonly static ConcurrentDictionary s_knownTypes = []; + private static readonly ConcurrentDictionary s_knownTypes = []; /// /// Used to dynamically provide the set of known types for serialization. diff --git a/dotnet/src/Experimental/Process.Core/Process.Core.csproj b/dotnet/src/Experimental/Process.Core/Process.Core.csproj index 74bd7123e780..dd9361bd0050 100644 --- a/dotnet/src/Experimental/Process.Core/Process.Core.csproj +++ b/dotnet/src/Experimental/Process.Core/Process.Core.csproj @@ -19,6 +19,12 @@ Semantic Kernel Process core. This package is automatically installed by Semantic Kernel Process packages if needed. + + + + + + @@ -34,12 +40,10 @@ + + - - - - diff --git a/dotnet/src/Experimental/Process.Core/ProcessBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessBuilder.cs index 830ed59f141c..0c44046dee6b 100644 --- a/dotnet/src/Experimental/Process.Core/ProcessBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/ProcessBuilder.cs @@ -138,17 +138,6 @@ internal override KernelProcessStepInfo BuildStep(ProcessBuilder processBuilder, return this.Build(stateMetadata as KernelProcessStateMetadata); } - /// - /// Add the provided step builder to the process. - /// - /// - /// Utilized by only. - /// - internal void AddStepFromBuilder(ProcessStepBuilder stepBuilder) - { - this._steps.Add(stepBuilder); - } - /// /// Check to ensure stepName is not used yet in another step /// @@ -228,6 +217,19 @@ public ProcessStepBuilder AddStepFromType(Type stepType, string? id = null, IRea return this.AddStep(stepBuilder, aliases); } + /// + /// Add a step + /// + /// + /// + /// Aliases that have been used by previous versions of the step, used for supporting backward compatibility when reading old version Process States + /// + public ProcessStepBuilder AddStepFromFunction(string id, StepFunction stepFunction, IReadOnlyList? aliases = null) + { + ProcessDelegateBuilder stepBuilder = new(id, stepFunction, this); + return this.AddStep(stepBuilder, aliases); + } + /// /// Adds a step to the process from a declarative agent. /// diff --git a/dotnet/src/Experimental/Process.Core/ProcessDelegateBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessDelegateBuilder.cs new file mode 100644 index 000000000000..508f583f8a19 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/ProcessDelegateBuilder.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.SemanticKernel.Process.Models; + +namespace Microsoft.SemanticKernel.Process; + +/// +/// Process step builder for a delegate step. +/// +public class ProcessDelegateBuilder : ProcessStepBuilder +{ + private readonly StepFunction _stepFunction; + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + public ProcessDelegateBuilder(string id, StepFunction stepFunction, ProcessBuilder? processBuilder) : base(id, processBuilder) + { + this._stepFunction = stepFunction ?? throw new ArgumentNullException(nameof(stepFunction), "Step function cannot be null."); + } + + internal override KernelProcessStepInfo BuildStep(ProcessBuilder processBuilder, KernelProcessStepStateMetadata? stateMetadata = null) // %%% METADATA ??? + { + // Build the edges first + var builtEdges = this.Edges.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Select(e => e.Build()).ToList()); + + KernelProcessStepState stateObject = new(this.Name, "none", this.Id); // %%% VERSION IS NOT USED, BUT REQUIRED BY THE BASE CLASS + + return new KernelProcessDelegateStepInfo(stateObject, this._stepFunction, builtEdges); + } + + internal override Dictionary GetFunctionMetadataMap() + { + // Nothing to do here, as this is a delegate step + return []; + } +} diff --git a/dotnet/src/Experimental/Process.Core/ProcessFunctionTargetBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessFunctionTargetBuilder.cs index d407e227eeca..4301248f7469 100644 --- a/dotnet/src/Experimental/Process.Core/ProcessFunctionTargetBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/ProcessFunctionTargetBuilder.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; +using Microsoft.SemanticKernel.Process; namespace Microsoft.SemanticKernel; @@ -165,6 +166,13 @@ public ProcessFunctionTargetBuilder(ProcessStepBuilder step, string? functionNam return; } + if (step is ProcessDelegateBuilder) + { + this.FunctionName = KernelDelegateProcessStep.FunctionName; + this.ParameterName = null; + return; + } + // Make sure the function target is valid. var target = step.ResolveFunctionTarget(functionName, parameterName); if (target == null) diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AnswerQuestionWithAIAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AnswerQuestionWithAIAction.cs new file mode 100644 index 000000000000..aaca337920eb --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AnswerQuestionWithAIAction.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.ObjectModel; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.PowerFx; +using Microsoft.PowerFx.Types; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Process.Workflows.PowerFx; + +namespace Microsoft.SemanticKernel.Process.Workflows.Actions; + +internal sealed class AnswerQuestionWithAIAction : AssignmentAction +{ + public AnswerQuestionWithAIAction(AnswerQuestionWithAI action) + : base(action, () => action.Variable?.Path) + { + if (string.IsNullOrWhiteSpace(action.UserInput?.ExpressionText)) + { + throw new InvalidActionException($"{nameof(AnswerQuestionWithAI)} must define {nameof(AnswerQuestionWithAI.UserInput)}"); + } + } + + public override async Task HandleAsync(KernelProcessStepContext context, ProcessActionScopes scopes, RecalcEngine engine, Kernel kernel, CancellationToken cancellationToken) + { + IChatCompletionService chatCompletion = kernel.Services.GetRequiredService(); + FormulaValue expressionResult = engine.Eval(this.Action.UserInput!.ExpressionText); + if (expressionResult is not StringValue stringResult) + { + throw new InvalidActionException($"{nameof(AnswerQuestionWithAI)} requires text for {nameof(AnswerQuestionWithAI.UserInput)}"); + } + + ChatHistory history = []; + if (this.Action.AdditionalInstructions is not null) + { + string? instructions = engine.Format(this.Action.AdditionalInstructions); + if (!string.IsNullOrWhiteSpace(instructions)) + { + history.AddSystemMessage(instructions); + } + } + history.AddUserMessage(stringResult.Value); + ChatMessageContent response = await chatCompletion.GetChatMessageContentAsync(history, cancellationToken: cancellationToken).ConfigureAwait(false); + StringValue responseValue = FormulaValue.New(response.ToString()); + + this.AssignTarget(engine, scopes, responseValue); + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/BeginDialogAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/BeginDialogAction.cs new file mode 100644 index 000000000000..d3bafae02b98 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/BeginDialogAction.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx; + +namespace Microsoft.SemanticKernel.Process.Workflows.Actions; + +internal sealed class BeginDialogAction : ProcessAction +{ + public BeginDialogAction(BeginDialog source) + : base(source) + { + } + + public override Task HandleAsync(KernelProcessStepContext context, ProcessActionScopes scopes, RecalcEngine engine, Kernel kernel, CancellationToken cancellationToken) + { + // %%% TODO + return Task.CompletedTask; + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ConditionGroupAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ConditionGroupAction.cs new file mode 100644 index 000000000000..c1b3c25b16bb --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ConditionGroupAction.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx; + +namespace Microsoft.SemanticKernel.Process.Workflows.Actions; + +internal sealed class ConditionGroupAction : ProcessAction +{ + public ConditionGroupAction(ConditionGroup source) + : base(source) + { + } + + public override Task HandleAsync(KernelProcessStepContext context, ProcessActionScopes scopes, RecalcEngine engine, Kernel kernel, CancellationToken cancellationToken) + { + // %%% REMOVE + //foreach (ConditionItem condition in this.Action.Conditions) + //{ + // if (engine.Eval(condition.Condition?.ExpressionText ?? "true").AsBoolean()) + // { + // // %%% VERIFY IF ONLY ONE CONDITION IS EXPECTED / ALLOWED + + // } + //} + return Task.CompletedTask; + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EditTableV2Action.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EditTableV2Action.cs new file mode 100644 index 000000000000..38b8ca0d4d48 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EditTableV2Action.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx; +using Microsoft.PowerFx.Types; +using Microsoft.SemanticKernel.Process.Workflows.PowerFx; + +namespace Microsoft.SemanticKernel.Process.Workflows.Actions; + +internal sealed class EditTableV2Action : AssignmentAction +{ + public EditTableV2Action(EditTableV2 source) + : base(source, () => source.ItemsVariable?.Path) + { + } + + public override async Task HandleAsync(KernelProcessStepContext context, ProcessActionScopes scopes, RecalcEngine engine, Kernel kernel, CancellationToken cancellationToken) + { + FormulaValue table = scopes[this.Target.VariableScopeName!][this.Target.VariableName!]; // %%% NULL OVERRIDE & MAKE UTILITY + TableValue tableValue = (TableValue)table; + + EditTableOperation? changeType = this.Action.ChangeType; + if (changeType is AddItemOperation addItemOperation) + { + FormulaValue result = engine.EvaluteExpression(addItemOperation.Value); + RecordValue newRecord = BuildRecord(tableValue.Type.ToRecord(), result); + await tableValue.AppendAsync(newRecord, cancellationToken).ConfigureAwait(false); + this.AssignTarget(engine, scopes, tableValue); + } + else if (changeType is ClearItemsOperation) + { + await tableValue.ClearAsync(cancellationToken).ConfigureAwait(false); + } + else if (changeType is RemoveItemOperation) // %%% SUPPORT + { + } + else if (changeType is TakeFirstItemOperation) // %%% SUPPORT + { + } + + static RecordValue BuildRecord(RecordType recordType, FormulaValue value) + { + return FormulaValue.NewRecordFromFields(recordType, GetValues()); + IEnumerable GetValues() + { + // %%% expression.StructuredRecordExpression.Properties ??? + foreach (NamedFormulaType fieldType in recordType.GetFieldTypes()) + { + if (value is RecordValue recordValue) + { + yield return new NamedValue(fieldType.Name, recordValue.GetField(fieldType.Name)); + } + else + { + yield return new NamedValue(fieldType.Name, value); + } + // %%% REMOVE ??? + //if (fieldType.Type is StringType) + //{ + // // For string fields, we can use a placeholder value + // yield return new NamedValue(fieldType.Name, value); // %%% TODO: VALUE + // continue; + //} + //if (fieldType.Type is BooleanType) + //{ + // // For boolean fields, we can use a default boolean value + // yield return new NamedValue(fieldType.Name, BooleanValue.New(true)); // %%% TODO: VALUE + // continue; + //} + //if (fieldType.Type is DecimalType) + //{ + // // For number fields, we can use a default numeric value + // yield return new NamedValue(fieldType.Name, NumberValue.New(-123)); // %%% TODO: VALUE + //} + } + } + } + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EndConversationAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EndConversationAction.cs new file mode 100644 index 000000000000..6b8fcddfc550 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EndConversationAction.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx; + +namespace Microsoft.SemanticKernel.Process.Workflows.Actions; + +internal sealed class EndConversationAction : ProcessAction +{ + public EndConversationAction(EndConversation source) + : base(source) + { + } + + public override Task HandleAsync(KernelProcessStepContext context, ProcessActionScopes scopes, RecalcEngine engine, Kernel kernel, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ParseValueAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ParseValueAction.cs new file mode 100644 index 000000000000..36de725b6a44 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ParseValueAction.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx; +using Microsoft.PowerFx.Types; +using Microsoft.SemanticKernel.Process.Workflows.PowerFx; + +namespace Microsoft.SemanticKernel.Process.Workflows.Actions; + +internal sealed class ParseValueAction : AssignmentAction +{ + public ParseValueAction(ParseValue source) + : base(source, () => source.Variable?.Path) + { + if (this.Action.Value is null) + { + throw new InvalidActionException($"{nameof(ParseValue)} must define {nameof(ParseValue.Value)}"); + } + } + + public override Task HandleAsync(KernelProcessStepContext context, ProcessActionScopes scopes, RecalcEngine engine, Kernel kernel, CancellationToken cancellationToken) + { + ValueExpression value = this.Action.Value!; + DataType valueType = this.Action.ValueType!; + + FormulaValue result = engine.EvaluteExpression(value); + + FormulaValue? parsedResult = null; + if (result is StringValue stringValue) + { + // %%% TODO: TRIM ```json ... ``` + if (valueType is RecordDataType recordType) + { + JsonDocument json = JsonDocument.Parse(stringValue.Value); + JsonElement currentElement = json.RootElement; + parsedResult = ParseRecord(currentElement, recordType); + } + } + + if (parsedResult is not null) + { + this.AssignTarget(engine, scopes, parsedResult); + } + // %%% ELSE THROW ??? + + return Task.CompletedTask; + } + + private static RecordValue ParseRecord(JsonElement currentElement, RecordDataType recordType) + { + return FormulaValue.NewRecordFromFields(ParseValues()); + + IEnumerable ParseValues() + { + foreach (KeyValuePair property in recordType.Properties) + { + JsonElement propertyElement = currentElement.GetProperty(property.Key); + FormulaValue? parsedValue = + property.Value.Type switch + { + StringDataType => StringValue.New(propertyElement.GetString()), + BooleanDataType => BooleanValue.New(propertyElement.GetBoolean()), + NumberDataType => NumberValue.New(propertyElement.GetDecimal()), + RecordDataType => ParseRecord(propertyElement, (RecordDataType)property.Value.Type), + _ => throw new InvalidActionException($"Unsupported data type '{property.Value.Type}' for property '{property.Key}'") // %%% EXCEPTION TYPE & MESSAGE + }; + yield return new NamedValue(property.Key, parsedValue); + } + } + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SendActivityAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SendActivityAction.cs new file mode 100644 index 000000000000..91265b9fbd5e --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SendActivityAction.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx; + +namespace Microsoft.SemanticKernel.Process.Workflows.Actions; + +internal sealed class SendActivityAction : ProcessAction +{ + private readonly ProcessActionEnvironment _environment; + + public SendActivityAction(SendActivity source, ProcessActionEnvironment environment) + : base(source) + { + if (source.Activity is null) + { + throw new InvalidActionException($"{nameof(SendActivity)} action must have an activity defined."); + } + + this._environment = environment; + } + + public override async Task HandleAsync(KernelProcessStepContext context, ProcessActionScopes scopes, RecalcEngine engine, Kernel kernel, CancellationToken cancellationToken) + { + await this._environment.ActivityNotificationHandler(this.Action.Activity!, engine).ConfigureAwait(false); // %%% NULL OVERRIDE + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetVariableAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetVariableAction.cs new file mode 100644 index 000000000000..9100b44ad389 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetVariableAction.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx; +using Microsoft.PowerFx.Types; +using Microsoft.SemanticKernel.Process.Workflows.PowerFx; + +namespace Microsoft.SemanticKernel.Process.Workflows.Actions; + +internal sealed class SetVariableAction : AssignmentAction +{ + public SetVariableAction(SetVariable action) + : base(action, () => action.Variable?.Path) + { + if (this.Action.Value is null) + { + throw new InvalidActionException($"{nameof(ParseValue)} must define {nameof(ParseValue.Value)}"); + } + } + + public override Task HandleAsync(KernelProcessStepContext context, ProcessActionScopes scopes, RecalcEngine engine, Kernel kernel, CancellationToken cancellationToken) + { + FormulaValue result = engine.EvaluteExpression(this.Action.Value!); + + if (result is ErrorValue errorVal) // %%% APPLY EVERYWHERE (OR CENTRAL) + { + throw new ProcessActionException($"Unable to evaluate expression. Error: {errorVal.Errors[0].Message}"); + } + + this.AssignTarget(engine, scopes, result); + + return Task.CompletedTask; + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/InvalidActionException.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/InvalidActionException.cs new file mode 100644 index 000000000000..be7842d6437e --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/InvalidActionException.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace Microsoft.SemanticKernel.Process.Workflows; + +/// +/// %%% COMMENT +/// +public sealed class InvalidActionException : ProcessActionException +{ + /// + /// Initializes a new instance of the class. + /// + public InvalidActionException() + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The error message that explains the reason for the exception. + public InvalidActionException(string? message) : base(message) + { + } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public InvalidActionException(string? message, Exception? innerException) : base(message, innerException) + { + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/ProcessActionException.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/ProcessActionException.cs new file mode 100644 index 000000000000..b7511cf0c2b5 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/ProcessActionException.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace Microsoft.SemanticKernel.Process.Workflows; + +/// +/// %%% COMMENT +/// +public class ProcessActionException : KernelException +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessActionException() + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The error message that explains the reason for the exception. + public ProcessActionException(string? message) : base(message) + { + } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public ProcessActionException(string? message, Exception? innerException) : base(message, innerException) + { + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs new file mode 100644 index 000000000000..a29900b92544 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel.Process.Workflows; + +namespace Microsoft.SemanticKernel; + +/// +/// Builder for converting CPS Topic ObjectModel YAML definition in a process. +/// +public static class ObjectModelBuilder +{ + /// + /// Builds a process from the provided YAML definition of a CPS Topic ObjectModel. + /// + /// // %%% COMMENT + /// The YAML string defining the CPS Topic ObjectModel. + /// // %%% COMMENT + /// // %%% COMMENT + /// The that corresponds with the YAML object model. + public static KernelProcess Build(string processId, string workflowYaml, string messageId, ProcessActionEnvironment? environment = null) // %%% REVISIT ENVIRONMENT + { + ProcessBuilder processBuilder = new(processId); + ProcessActionWalker walker = new(processBuilder, messageId, environment ?? ProcessActionEnvironment.Default); + walker.ProcessYaml(workflowYaml); + return processBuilder.Build(); + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/FormulaValueExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/FormulaValueExtensions.cs new file mode 100644 index 000000000000..b3567f2e4f60 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/FormulaValueExtensions.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.PowerFx.Types; + +namespace Microsoft.SemanticKernel.Process.Workflows.PowerFx; + +internal delegate object? GetFormulaValue(FormulaValue value); + +internal static class FormulaValueExtensions +{ + private static readonly ImmutableDictionary s_handlers = + new Dictionary() + { + { typeof(BooleanValue), FromBooleanValue }, + { typeof(DecimalValue), FromDecimalValue }, + { typeof(DateValue), FromDateTimeValue }, + { typeof(RecordValue), FromRecordValue }, + { typeof(StringValue), FromStringValue }, + }.ToImmutableDictionary(); + + public static string? Format(this FormulaValue value) + { + Type valueType = value.GetType(); + + if (s_handlers.TryGetValue(valueType, out GetFormulaValue? handler)) + { + return $"{handler.Invoke(value) ?? "-"} ({valueType.Name})"; + } + + foreach (KeyValuePair kvp in s_handlers) + { + if (kvp.Key.IsAssignableFrom(valueType)) + { + return $"{kvp.Value.Invoke(value) ?? "-"} ({valueType.Name})"; + } + } + + return value.ToString(); + } + + // %%% TODO + //VoidValue + //NamedValue + //BlobValue + //ErrorValue + //ColorValue + //NumberValue + //TableValue + //BlankValue + //DateValue + //GuidValue + //TimeValue + + private static object? FromBooleanValue(FormulaValue value) => ((BooleanValue)value).Value; + private static object? FromDecimalValue(FormulaValue value) => ((DecimalValue)value).Value; + private static object? FromDateTimeValue(FormulaValue value) => ((DateValue)value).GetConvertedValue(TimeZoneInfo.Local); + private static object? FromStringValue(FormulaValue value) => ((StringValue)value).Value; + private static object? FromRecordValue(FormulaValue value) => + $""" + [ + {string.Join(Environment.NewLine, ((RecordValue)value).Fields.Select(field => $" {field.Name}={field.Value.Format()}"))} + ] + """; +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs new file mode 100644 index 000000000000..710edaca5514 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx; +using Microsoft.PowerFx.Types; + +namespace Microsoft.SemanticKernel.Process.Workflows.PowerFx; + +internal static class RecalcEngineExtensions +{ + public static void SetScopedVariable(this RecalcEngine engine, ProcessActionScopes scopes, string scopeName, string varName, FormulaValue value) + { + // Validate inputs and assign value. + ProcessActionScope scope = scopes.AssignValue(scopeName, varName, value); + + // Rebuild scope record and update engine + RecordValue scopeRecord = scope.BuildRecord(); + engine.DeleteFormula(scopeName); + engine.UpdateVariable(scopeName, scopeRecord); + } + + public static async Task ExecuteActionsAsync(this RecalcEngine engine, KernelProcessStepContext context, ProcessActionScopes scopes, ProcessAction action, Kernel kernel, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + try + { + // Execute each action in the current context + //Console.WriteLine($"!!! ACTION {action.GetType().Name} [{action.Id}]"); // %%% DEVTRACE + await action.HandleAsync(context, scopes, engine, kernel, cancellationToken).ConfigureAwait(false); + } + catch (ProcessActionException exception) + { + Console.WriteLine($"*** ACTION [{action.Id}] ERROR - {exception.GetType().Name}\n{exception.Message}"); // %%% DEVTRACE + throw; + } + catch (Exception exception) + { + Console.WriteLine($"*** ACTION [{action.Id}] ERROR - {exception.GetType().Name}\n{exception.Message}"); // %%% DEVTRACE + throw new ProcessActionException($"Unexpected failure executing action #{action.Id} [{action.GetType().Name}]", exception); + } + } + + public static FormulaValue EvaluteExpression(this RecalcEngine engine, ValueExpression? value) + { + if (value is null) + { + return BlankValue.NewBlank(); // %%% HANDLE NULL CASE + } + + if (value.IsVariableReference) + { + return engine.Eval($"{value.VariableReference!.VariableScopeName}.{value.VariableReference!.VariableName}"); // %%% DRY + } + + if (value.IsExpression) + { + return engine.Eval(value.ExpressionText); + } + + if (value.IsLiteral) + { + DataValue? source = value.LiteralValue; // %%% TODO: TRANSLATE VALUE + return BlankValue.NewBlank(); // %%% HACK + } + // %%% TODO: value.StructuredRecordExpression ??? + return BlankValue.NewBlank(); + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineFactory.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineFactory.cs new file mode 100644 index 000000000000..b20e09772da1 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineFactory.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.PowerFx; +using Microsoft.PowerFx.Types; + +namespace Microsoft.SemanticKernel.Process.Workflows.PowerFx; + +internal static class RecalcEngineFactory +{ + public static RecalcEngine Create(ProcessActionScopes scopes, int maximumExpressionLength) + { + RecalcEngine engine = new(CreateConfig()); + + SetScope(ActionScopeTypes.Topic); + SetScope(ActionScopeTypes.Global); + SetScope(ActionScopeTypes.System); + + return engine; + + void SetScope(string scopeName) + { + RecordValue record = scopes[scopeName].BuildRecord(); + engine.UpdateVariable(scopeName, record); + } + + PowerFxConfig CreateConfig() + { + PowerFxConfig config = + new(Features.PowerFxV1) + { + MaximumExpressionLength = maximumExpressionLength + }; + + config.EnableSetFunction(); + + return config; + } + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/TemplateExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/TemplateExtensions.cs new file mode 100644 index 000000000000..27f63f7b3607 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/TemplateExtensions.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx; +using Microsoft.PowerFx.Types; + +namespace Microsoft.SemanticKernel.Process.Workflows.PowerFx; + +internal static class TemplateExtensions +{ + public static string? Format(this RecalcEngine engine, IEnumerable template) + { + return string.Concat(template.Select(t => engine.Format(t))); + } + + public static string? Format(this RecalcEngine engine, TemplateLine line) + { + return string.Concat(line.Segments.Select(s => engine.Format(s))); + } + + private static string? Format(this RecalcEngine engine, TemplateSegment segment) + { + if (segment is TextSegment textSegment) + { + return textSegment.Value; + } + + if (segment is ExpressionSegment expressionSegment) + { + if (expressionSegment.Expression is not null) + { + if (expressionSegment.Expression.ExpressionText is not null) + { + FormulaValue expressionValue = engine.Eval(expressionSegment.Expression.ExpressionText); + return expressionValue?.Format(); + } + if (expressionSegment.Expression.VariableReference is not null) + { + FormulaValue expressionValue = engine.Eval(expressionSegment.Expression.VariableReference.ToString()); + return expressionValue?.Format(); + } + } + } + + return $"UNSUPPORTED SEGEMENT: {segment.GetType().Name}"; // %%% LOG AND EMPTY STRING + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessAction.cs new file mode 100644 index 000000000000..c2115f75cd9b --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessAction.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx; +using Microsoft.PowerFx.Types; +using Microsoft.SemanticKernel.Process.Workflows.PowerFx; + +namespace Microsoft.SemanticKernel.Process.Workflows; + +internal abstract class ProcessAction(TAction action) : ProcessAction(action) where TAction : DialogAction +{ + public new TAction Action => action; +} + +internal abstract class ProcessAction(DialogAction action) +{ + public ActionId Id => action.Id; + + public DialogAction Action => action; + + public abstract Task HandleAsync(KernelProcessStepContext context, ProcessActionScopes scopes, RecalcEngine engine, Kernel kernel, CancellationToken cancellationToken); +} + +internal abstract class AssignmentAction : ProcessAction where TAction : DialogAction +{ + protected AssignmentAction(TAction action, Func resolver) + : base(action) + { + this.Target = + resolver.Invoke() ?? + throw new InvalidActionException($"Action '{action.GetType().Name}' must have a variable path defined."); + + if (string.IsNullOrWhiteSpace(this.Target.VariableScopeName)) + { + throw new InvalidActionException($"Action '{action.GetType().Name}' must define a variable scope."); + } + if (string.IsNullOrWhiteSpace(this.Target.VariableName)) + { + throw new InvalidActionException($"Action '{action.GetType().Name}' must define a variable name."); + } + } + + public PropertyPath Target { get; } + + protected void AssignTarget(RecalcEngine engine, ProcessActionScopes scopes, FormulaValue result) + { + engine.SetScopedVariable(scopes, this.Target.VariableScopeName!, this.Target.VariableName!, result); + string? resultValue = result.Format(); + string valuePosition = (resultValue?.IndexOf('\n') ?? -1) >= 0 ? Environment.NewLine : " "; + Console.WriteLine( // %%% DEVTRACE + $""" + !!! ASSIGN {this.GetType().Name} [{this.Id}] + NAME: {this.Target.VariableScopeName}.{this.Target.VariableName} + VALUE:{valuePosition}{result.Format()} + """); + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionContext.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionContext.cs new file mode 100644 index 000000000000..86601d0f2b84 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionContext.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Microsoft.PowerFx; + +namespace Microsoft.SemanticKernel.Process.Workflows; + +internal delegate Task ProcessActionHandler(KernelProcessStepContext context, ProcessActionScopes scopes, RecalcEngine engine, Kernel kernel); + +/// +/// Step context for the current step in a process. +/// +internal sealed class ProcessActionContext(ProcessStepBuilder step) +{ + /// + /// The current step for the context. + /// + public ProcessStepBuilder Step { get; set; } = step; + + /// + /// %%% COMMENT + /// + /// + public ProcessStepEdgeBuilder Then() => this.Step.OnFunctionResult(KernelDelegateProcessStep.FunctionName); + + /// + /// %%% COMMENT + /// + /// + /// + /// + public ProcessStepBuilder Then(ProcessStepBuilder step, KernelProcessEdgeCondition? condition = null) + { + // IN: Target the given step when the previous step ends + ProcessStepEdgeBuilder edge = this.Then(); + + edge.Condition = condition; + edge.SendEventTo(new ProcessFunctionTargetBuilder(step)); + + return step; + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionEnvironment.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionEnvironment.cs new file mode 100644 index 000000000000..59f1df9530a6 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionEnvironment.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; +using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx; +using Microsoft.SemanticKernel.Process.Workflows.PowerFx; + +namespace Microsoft.SemanticKernel; + +/// +/// %%% COMMENT +/// +public sealed class ProcessActionEnvironment +{ + /// + /// %%% COMMENT + /// + internal static ProcessActionEnvironment Default { get; } = new(); + + /// + /// %%% COMMENT + /// + public int MaximumExpressionLength { get; init; } = 3000; + + /// + /// %%% COMMENT + /// + /// + /// + /// + public Task ActivityNotificationHandler(ActivityTemplateBase activity, RecalcEngine engine) // %%% TODO: CONFIGURABLE + { + Console.WriteLine($"\nACTIVITY: {activity.GetType().Name}"); + + if (activity is MessageActivityTemplate messageActivity) + { + if (!string.IsNullOrEmpty(messageActivity.Summary)) + { + Console.WriteLine($"\t{messageActivity.Summary}"); // %%% DEVTRACE + } + + string? activityText = engine.Format(messageActivity.Text); + Console.WriteLine(activityText + Environment.NewLine); // %%% DEVTRACE + } + + return Task.CompletedTask; + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionScopes.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionScopes.cs new file mode 100644 index 000000000000..60f03be9c04c --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionScopes.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using Microsoft.PowerFx.Types; + +namespace Microsoft.SemanticKernel.Process.Workflows; + +internal static class ActionScopeTypes +{ + public const string Topic = nameof(Topic); + public const string Global = nameof(Global); + public const string System = nameof(System); +} + +internal sealed class ProcessActionScope : Dictionary; + +internal sealed class ProcessActionScopes : Dictionary +{ + public ProcessActionScopes() + { + this[ActionScopeTypes.Topic] = []; + this[ActionScopeTypes.Global] = []; + this[ActionScopeTypes.System] = []; + } +} + +internal static class ProcessActionScopeExtensions +{ + public static RecordValue BuildRecord(this ProcessActionScope scope) + { + return FormulaValue.NewRecordFromFields(GetFields()); + + IEnumerable GetFields() + { + foreach (KeyValuePair kvp in scope) + { + yield return new NamedValue(kvp.Key, kvp.Value); + } + } + } + + public static ProcessActionScope AssignValue(this ProcessActionScopes scopes, string scopeName, string varName, FormulaValue value) + { + if (!scopes.TryGetValue(scopeName, out ProcessActionScope? scope)) + { + throw new InvalidActionException("Unknown scope: " + scopeName); + } + + scope[varName] = value; + + return scope; + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs new file mode 100644 index 000000000000..8243640e4f12 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs @@ -0,0 +1,469 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx; +using Microsoft.SemanticKernel.Process.Workflows.Actions; +using Microsoft.SemanticKernel.Process.Workflows.PowerFx; + +namespace Microsoft.SemanticKernel.Process.Workflows; + +internal sealed class ProcessActionVisitor : DialogActionVisitor +{ + private readonly ProcessBuilder _processBuilder; + private readonly ProcessStepBuilder _unhandledErrorStep; + private readonly ProcessActionEnvironment _environment; + private readonly ProcessActionScopes _scopes; + private readonly Dictionary _steps; + private readonly Stack _contextStack; + private readonly List<(ActionId TargetId, ProcessStepEdgeBuilder SourceEdge)> _linkCache; + + public ProcessActionVisitor( + ProcessBuilder processBuilder, + ProcessActionEnvironment environment, + ProcessStepBuilder sourceStep, + ProcessActionScopes scopes) + { + ProcessActionContext rootContext = new(sourceStep); + this._contextStack = []; + this._contextStack.Push(rootContext); + this._steps = []; + this._linkCache = []; + this._processBuilder = processBuilder; + this._environment = environment; + this._scopes = scopes; + this._unhandledErrorStep = + processBuilder.AddStepFromFunction( + $"{processBuilder.Name}_unhandled_error", + (kernel, context) => + { + // Handle unhandled errors here + Console.WriteLine("*** PROCESS ERROR - Unhandled error"); // %%% DEVTRACE + return Task.CompletedTask; + }); + } + + public void Complete() + { + // Close the current context + this.CurrentContext.Then().StopProcess(); + + // Process the cached links + foreach ((ActionId targetId, ProcessStepEdgeBuilder sourceEdge) in this._linkCache) + { + // Link the queued context to the step + ProcessStepBuilder step = this._steps[targetId]; // %%% TRY + Console.WriteLine($"> CONNECTING {sourceEdge.Source.Id} => {targetId}"); + sourceEdge.SendEventTo(new ProcessFunctionTargetBuilder(step)); + } + this._linkCache.Clear(); + + // Visitor is complete, all actions have been processed + Console.WriteLine("> COMPLETE"); // %%% DEVTRACE + } + + private ProcessActionContext CurrentContext => this._contextStack.Peek(); + + protected override void Visit(ActionScope item) + { + Trace(item, isSkipped: false); + + this.AddContainer(item.Id.Value); + } + + protected override void Visit(ConditionGroup item) + { + Trace(item, isSkipped: false); + + this.AddAction(new ConditionGroupAction(item)); + + // Visit each action in the condition group + int index = 1; + foreach (ConditionItem conditionItem in item.Conditions) + { + ProcessStepBuilder step = this.CreateContainerStep(this.CurrentContext, conditionItem.Id ?? $"{item.Id.Value}_item{index}"); + this._contextStack.Push(new ProcessActionContext(step)); + + conditionItem.Accept(this); + + ProcessActionContext conditionContext = this._contextStack.Pop(); + KernelProcessEdgeCondition? condition = null; + + if (conditionItem.Condition is not null) + { + // %%% VERIFY IF ONLY ONE CONDITION IS EXPECTED / ALLOWED + condition = + new((stepEvent, state) => + { + RecalcEngine engine = RecalcEngineFactory.Create(this._scopes, this._environment.MaximumExpressionLength); // %%% DRY + bool result = engine.Eval(conditionItem.Condition.ExpressionText ?? "true").AsBoolean(); + Console.WriteLine($"!!! CONDITION: {conditionItem.Condition.ExpressionText ?? "true"}={result}"); + return Task.FromResult(result); + }); + } + + this.CurrentContext.Then(conditionContext.Step, condition); + + ++index; + } + } + + protected override void Visit(GotoAction item) + { + Trace(item, isSkipped: false); + + this.AddContainer(item.Id.Value); + // Store the link for processing after all actions have steps. + this._linkCache.Add((item.ActionId, this.CurrentContext.Then())); + // Create an orphaned context for continuity + this.AddDead(item.Id.Value); + } + + protected override void Visit(EndConversation item) + { + Trace(item, isSkipped: false); + + this.AddAction(new EndConversationAction(item)); + // Stop the process, this is a terminal action + this.CurrentContext.Then().StopProcess(); + // Create an orphaned context for continuity + this.AddDead(item.Id.Value); + } + + protected override void Visit(BeginDialog item) + { + Trace(item, isSkipped: false); + + this.AddAction(new BeginDialogAction(item)); + } + + protected override void Visit(UnknownDialogAction item) + { + Trace(item); + } + + protected override void Visit(EndDialog item) + { + Trace(item); + } + + protected override void Visit(AnswerQuestionWithAI item) + { + Trace(item, isSkipped: false); + + this.AddAction(new AnswerQuestionWithAIAction(item)); + } + + protected override void Visit(SetVariable item) + { + Trace(item, isSkipped: false); + + this.AddAction(new SetVariableAction(item)); + } + + protected override void Visit(SetTextVariable item) + { + Trace(item); + } + + protected override void Visit(EditTable item) + { + Trace(item); + } + + protected override void Visit(EditTableV2 item) + { + Trace(item, isSkipped: false); + + this.AddAction(new EditTableV2Action(item)); + } + + protected override void Visit(ParseValue item) + { + Trace(item, isSkipped: false); + + this.AddAction(new ParseValueAction(item)); + } + + protected override void Visit(SendActivity item) + { + Trace(item, isSkipped: false); + + this.AddAction(new SendActivityAction(item, this._environment)); + } + + #region Not implemented + + protected override void Visit(GetActivityMembers item) + { + Trace(item); + } + + protected override void Visit(UpdateActivity item) + { + Trace(item); + } + + protected override void Visit(DeleteActivity item) + { + Trace(item); + } + + protected override void Visit(InvokeFlowAction item) + { + Trace(item); + } + + protected override void Visit(InvokeAIBuilderModelAction item) + { + Trace(item); + } + + protected override void Visit(WaitForConnectorTrigger item) + { + Trace(item); + } + + protected override void Visit(InvokeConnectorAction item) + { + Trace(item); + } + + protected override void Visit(InvokeSkillAction item) + { + Trace(item); + } + + protected override void Visit(AdaptiveCardPrompt item) + { + Trace(item); + } + + protected override void Visit(Question item) + { + Trace(item); + } + + protected override void Visit(CSATQuestion item) + { + Trace(item); + } + + protected override void Visit(OAuthInput item) + { + Trace(item); + } + + protected override void Visit(Foreach item) + { + Trace(item); + } + + protected override void Visit(RepeatDialog item) + { + Trace(item); + } + + protected override void Visit(ActivateExternalTrigger item) + { + Trace(item); + } + + protected override void Visit(DisableTrigger item) + { + Trace(item); + } + + protected override void Visit(ReplaceDialog item) + { + Trace(item); + } + + protected override void Visit(CancelAllDialogs item) + { + Trace(item); + } + + protected override void Visit(CancelDialog item) + { + Trace(item); + } + + protected override void Visit(ClearAllVariables item) + { + Trace(item); + } + + protected override void Visit(BreakLoop item) + { + Trace(item); + } + + protected override void Visit(ContinueLoop item) + { + Trace(item); + } + + protected override void Visit(ResetVariable item) + { + Trace(item); + } + + protected override void Visit(EmitEvent item) + { + Trace(item); + } + + protected override void Visit(GetConversationMembers item) + { + Trace(item); + } + + protected override void Visit(HttpRequestAction item) + { + Trace(item); + } + + protected override void Visit(RecognizeIntent item) + { + Trace(item); + } + + protected override void Visit(TransferConversation item) + { + Trace(item); + } + + protected override void Visit(TransferConversationV2 item) + { + Trace(item); + } + + protected override void Visit(SignOutUser item) + { + Trace(item); + } + + protected override void Visit(LogCustomTelemetryEvent item) + { + Trace(item); + } + + protected override void Visit(DisconnectedNodeContainer item) + { + Trace(item); + } + + protected override void Visit(CreateSearchQuery item) + { + Trace(item); + } + + protected override void Visit(SearchKnowledgeSources item) + { + Trace(item); + } + + protected override void Visit(SearchAndSummarizeWithCustomModel item) + { + Trace(item); + } + + protected override void Visit(SearchAndSummarizeContent item) + { + Trace(item); + } + + protected override void Visit(InvokeCustomModelAction item) + { + Trace(item); + } + + #endregion + + private void AddAction(ProcessAction? action) + { + if (action is not null) + { + // Add the action to the existing context + this.AddStep(this.CreateActionStep(this.CurrentContext, action)); + } + } + + private void AddContainer(string contextId) + { + this.AddStep(this.CreateContainerStep(this.CurrentContext, contextId)); + } + + private void AddDead(string contextId) + { + this.CurrentContext.Step = this.CreateContainerStep(this.CurrentContext, $"dead_{contextId}"); + } + + private void AddStep(ProcessStepBuilder step) + { + this._steps[step.Id] = step; + this.ContinueWith(step); + } + + private ProcessStepBuilder CreateContainerStep(ProcessActionContext currentContext, string contextId) + { + return this.InitializeStep( + this._processBuilder.AddStepFromFunction( + contextId, + (kernel, context) => + { + Console.WriteLine($"!!! STEP [{contextId}]"); // %%% DEVTRACE + return Task.CompletedTask; + })); + } + + // This implementation accepts the context as a parameter in order to pin the context closure. + // The step cannot reference this.CurrentContext directly, as this will always be the final context. + private ProcessStepBuilder CreateActionStep(ProcessActionContext currentContext, ProcessAction action) + { + return this.InitializeStep( + this._processBuilder.AddStepFromFunction( + action.Id.Value, + async (kernel, context) => + { + try + { + Console.WriteLine($"!!! STEP [{action.Id}]"); // %%% DEVTRACE + RecalcEngine engine = RecalcEngineFactory.Create(this._scopes, this._environment.MaximumExpressionLength); // %%% DRY + await engine.ExecuteActionsAsync(context, this._scopes, action, kernel, cancellationToken: default).ConfigureAwait(false); + } + catch (ProcessActionException) + { + Console.WriteLine($"*** STEP [{action.Id}] ERROR - Action failure"); // %%% DEVTRACE + throw; + } + catch (Exception exception) + { + Console.WriteLine($"*** STEP [{action.Id}] ERROR - {exception.GetType().Name}\n{exception.Message}"); // %%% DEVTRACE + throw; + } + })); + } + + private ProcessStepBuilder InitializeStep(ProcessStepBuilder step) + { + // Capture unhandled errors for the given step + step.OnFunctionError(KernelDelegateProcessStep.FunctionName).SendEventTo(new ProcessFunctionTargetBuilder(this._unhandledErrorStep)); + + return step; + } + + private void ContinueWith(ProcessStepBuilder newStep, KernelProcessEdgeCondition? condition = null) + { + this.CurrentContext.Then(newStep, condition); + this.CurrentContext.Step = newStep; + } + + private static void Trace(DialogAction item, bool isSkipped = true) + { + Console.WriteLine($"> {(isSkipped ? "EMPTY" : "VISIT")} - {item.GetType().Name} [{item.Id.Value}]"); // %%% DEVTRACE + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessElementWalker.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessElementWalker.cs new file mode 100644 index 000000000000..c30da8b1eae1 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessElementWalker.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ComponentModel; +using System.Threading.Tasks; +using Microsoft.Bot.ObjectModel; +using Microsoft.Bot.ObjectModel.Yaml; +using Microsoft.PowerFx.Types; + +namespace Microsoft.SemanticKernel.Process.Workflows; + +internal sealed class ProcessActionWalker : BotElementWalker +{ + private readonly ProcessActionVisitor _visitor; + + public ProcessActionWalker(ProcessBuilder processBuilder, string messageId, ProcessActionEnvironment processEnvironment) + { + this._visitor = CreateActionVisitor(processBuilder, messageId, processEnvironment); + } + + public void ProcessYaml(string yaml) + { + Console.WriteLine("### PARSING YAML"); + BotElement root = YamlSerializer.Deserialize(yaml) ?? throw new KernelException("Unable to parse YAML content."); + Console.WriteLine("### INTERPRETING MODEL"); + this.Visit(root); + this._visitor.Complete(); + Console.WriteLine("### PROCESS CREATED"); + } + + public override bool DefaultVisit(BotElement definition) + { + if (definition is DialogAction action) + { + action.Accept(this._visitor); + } + + return true; + } + + private static ProcessActionVisitor CreateActionVisitor(ProcessBuilder processBuilder, string messageId, ProcessActionEnvironment processEnvironment) + { + ProcessActionScopes scopes = []; + + ProcessStepBuilder initStep = processBuilder.AddStepFromType(scopes, "init"); + + processBuilder.OnInputEvent(messageId).SendEventTo(new ProcessFunctionTargetBuilder(initStep)); + + return new ProcessActionVisitor(processBuilder, processEnvironment, initStep, scopes); + } + + private sealed class InitializeProcessStep : KernelProcessStep + { + private ProcessActionScopes? _scopes; + + public override ValueTask ActivateAsync(KernelProcessStepState state) + { + this._scopes = state.State; + +#if !NETCOREAPP + return new ValueTask(); +#else + return ValueTask.CompletedTask; +#endif + } + + [KernelFunction(KernelDelegateProcessStep.FunctionName)] + [Description("Initialize the process step")] + public void InitializeProcess(string message) + { + if (this._scopes == null) + { + throw new KernelException("Scopes have not been initialized. Ensure that the step is activated before calling this function."); + } + + Console.WriteLine("!!! INIT WORKFLOW"); + FormulaValue inputTask = StringValue.New(message); + this._scopes[ActionScopeTypes.System]["LastMessage"] = inputTask; // %%% MAGIC CONST + } + } +} diff --git a/dotnet/src/Experimental/Process.LocalRuntime/LocalDelegateStep.cs b/dotnet/src/Experimental/Process.LocalRuntime/LocalDelegateStep.cs new file mode 100644 index 000000000000..8ae715c965e7 --- /dev/null +++ b/dotnet/src/Experimental/Process.LocalRuntime/LocalDelegateStep.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Linq; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Process.Internal; + +namespace Microsoft.SemanticKernel.Process; + +internal class LocalDelegateStep : LocalStep +{ + private readonly KernelProcessDelegateStepInfo _delegateStep; + + public LocalDelegateStep(KernelProcessDelegateStepInfo stepInfo, Kernel kernel, string? parentProcessId = null) + : base(stepInfo, kernel, parentProcessId) + { + this._delegateStep = stepInfo; + } + + protected override ValueTask InitializeStepAsync() + { + this._stepInstance = new KernelDelegateProcessStep(this._delegateStep.StepFunction); + + var kernelPlugin = KernelPluginFactory.CreateFromObject(this._stepInstance, pluginName: this._stepInfo.State.Id); + + // Load the kernel functions + foreach (KernelFunction f in kernelPlugin) + { + this._functions.Add(f.Name, f); + } + + this._initialInputs = this.FindInputChannels(this._functions, logger: null, this.ExternalMessageChannel); // %%% LOGGER + this._inputs = this._initialInputs.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)); + +#if !NETCOREAPP + return new ValueTask(); +#else + return ValueTask.CompletedTask; +#endif + } +} diff --git a/dotnet/src/Experimental/Process.LocalRuntime/LocalProcess.cs b/dotnet/src/Experimental/Process.LocalRuntime/LocalProcess.cs index 77fa480c3b4e..1ac651747a09 100644 --- a/dotnet/src/Experimental/Process.LocalRuntime/LocalProcess.cs +++ b/dotnet/src/Experimental/Process.LocalRuntime/LocalProcess.cs @@ -266,6 +266,14 @@ private async ValueTask InitializeProcessAsync() ExternalMessageChannel = this.ExternalMessageChannel, }; } + else if (step is KernelProcessDelegateStepInfo delegateStep) + { + localStep = new LocalDelegateStep(delegateStep, this._kernel) + { + ParentProcessId = this.Id, + EventProxy = this.EventProxy, + }; + } else if (step is KernelProcessMap mapStep) { localStep = diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/AnswerQuestionWithAIActionTest.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/AnswerQuestionWithAIActionTest.cs new file mode 100644 index 000000000000..e40d0ff3b264 --- /dev/null +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/AnswerQuestionWithAIActionTest.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Microsoft.Bot.ObjectModel; +using Xunit; + +namespace Microsoft.SemanticKernel.Process.UnitTests.Workflows; + +/// +/// Base for directly testing AnswerQuestionWithAI. +/// +public sealed class AnswerQuestionWithAIActionTest +{ + [Fact] + public Task EmptyTestAsync() + { + AnswerQuestionWithAI action = new(); + // %%% TODO: Something + return Task.CompletedTask; + } +} diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/ProcessActionTest.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/ProcessActionTest.cs new file mode 100644 index 000000000000..e2aecf2564e1 --- /dev/null +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/ProcessActionTest.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.SemanticKernel.Process.UnitTests.Workflows; + +/// +/// Base for directly testing a process-action. +/// +internal sealed class ProcessActionTest +{ +} diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/RecalcEngineFactoryTests.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/RecalcEngineFactoryTests.cs new file mode 100644 index 000000000000..85388f4a1d3f --- /dev/null +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/RecalcEngineFactoryTests.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using Microsoft.PowerFx; +using Microsoft.SemanticKernel.Process.Workflows.PowerFx; +using Xunit; + +namespace Microsoft.SemanticKernel.Process.UnitTests.Workflows; + +public class RecalcEngineFactoryTests +{ + [Fact] + public void DefaultNotNull() + { + // Act + RecalcEngine engine = RecalcEngineFactory.Create([], 100); + + // Assert + Assert.NotNull(engine); + } + + [Fact] + public void NewInstanceEachTime() + { + // Act + RecalcEngine engine1 = RecalcEngineFactory.Create([], 100); + RecalcEngine engine2 = RecalcEngineFactory.Create([], 100); + + // Assert + Assert.NotNull(engine1); + Assert.NotNull(engine2); + Assert.NotSame(engine1, engine2); + } + + [Fact] + public void HasSetFunctionEnabled() + { + // Arrange + RecalcEngine engine = RecalcEngineFactory.Create([], 100); + + // Act + CheckResult result = engine.Check("1+1"); + + // Assert + Assert.True(result.IsSuccess); + } + + [Fact] + public void HasCorrectMaximumExpressionLength() + { + // Arrange + RecalcEngine engine = RecalcEngineFactory.Create([], 2000); + + // Act: Create a long expression that is within the limit + string goodExpression = string.Concat(GenerateExpression(999)); + CheckResult goodResult = engine.Check(goodExpression); + + // Assert + Assert.True(goodResult.IsSuccess); + + // Act: Create a long expression that exceeds the limit + string longExpression = string.Concat(GenerateExpression(1001)); + CheckResult longResult = engine.Check(longExpression); + + // Assert + Assert.False(longResult.IsSuccess); + + static IEnumerable GenerateExpression(int elements) + { + yield return "1"; + for (int i = 0; i < elements - 1; i++) + { + yield return "+1"; + } + } + } +} diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/RecalcEngineTests.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/RecalcEngineTests.cs new file mode 100644 index 000000000000..b243a435ff8b --- /dev/null +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/RecalcEngineTests.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.PowerFx; +using Microsoft.PowerFx.Types; +using Microsoft.SemanticKernel.Process.Workflows.PowerFx; +using Xunit; + +namespace Microsoft.SemanticKernel.Process.UnitTests.Workflows; + +#pragma warning disable CA1308 // Ignore "Normalize strings to uppercase" warning for test cases + +public sealed class RecalcEngineTests +{ + [Fact] + public void EvaluateConstant() + { + RecalcEngine engine = RecalcEngineFactory.Create([], 100); + + this.EvaluateExpression(engine, 0m, "0"); + this.EvaluateExpression(engine, -1m, "-1"); + this.EvaluateExpression(engine, true, "true"); + this.EvaluateExpression(engine, false, "false"); + this.EvaluateExpression(engine, (string?)null, string.Empty); + this.EvaluateExpression(engine, "Hi", "\"Hi\""); + } + + [Fact] + public void EvaluateInvalid() + { + RecalcEngine engine = RecalcEngineFactory.Create([], 100); + engine.UpdateVariable("Scoped.Value", DecimalValue.New(33)); + + this.EvaluateFailure(engine, "Hi"); + this.EvaluateFailure(engine, "True"); + this.EvaluateFailure(engine, "TRUE"); + this.EvaluateFailure(engine, "=1", canParse: false); + this.EvaluateFailure(engine, "=1+2", canParse: false); + this.EvaluateFailure(engine, "CustomValue"); + this.EvaluateFailure(engine, "CustomValue + 1"); + this.EvaluateFailure(engine, "Scoped.Value"); + this.EvaluateFailure(engine, "Scoped.Value + 1"); + this.EvaluateFailure(engine, "\"BEGIN-\" & Scoped.Value & \"-END\""); + } + + [Fact] + public void EvaluateFormula() + { + NamedValue[] recordValues = + [ + new NamedValue("Label", StringValue.New("Test")), + new NamedValue("Value", DecimalValue.New(54)), + ]; + FormulaValue complexValue = FormulaValue.NewRecordFromFields(recordValues); + + RecalcEngine engine = RecalcEngineFactory.Create([], 100); + engine.UpdateVariable("CustomLabel", StringValue.New("Note")); + engine.UpdateVariable("CustomValue", DecimalValue.New(42)); + engine.UpdateVariable("Scoped", complexValue); + + this.EvaluateExpression(engine, 2m, "1 + 1"); + this.EvaluateExpression(engine, 42m, "CustomValue"); + this.EvaluateExpression(engine, 43m, "CustomValue + 1"); + this.EvaluateExpression(engine, "Note", "CustomLabel"); + //this.EvaluateExpression(engine, "Note", "\"{CustomLabel}\""); + this.EvaluateExpression(engine, "BEGIN-42-END", "\"BEGIN-\" & CustomValue & \"-END\""); + this.EvaluateExpression(engine, 54m, "Scoped.Value"); + this.EvaluateExpression(engine, 55m, "Scoped.Value + 1"); + this.EvaluateExpression(engine, "Test", "Scoped.Label"); + //this.EvaluateExpression(engine, "Test", "\"{Scoped.Label}\""); + } + + private void EvaluateFailure(RecalcEngine engine, string sourceExpression, bool canParse = true) + { + CheckResult checkResult = engine.Check(sourceExpression); + Assert.False(checkResult.IsSuccess); + ParseResult parseResult = engine.Parse(sourceExpression); + Assert.Equal(canParse, parseResult.IsSuccess); + Assert.Throws(() => engine.Eval(sourceExpression)); + } + + private void EvaluateExpression(RecalcEngine engine, T expectedResult, string sourceExpression) + { + CheckResult checkResult = engine.Check(sourceExpression); + Assert.True(checkResult.IsSuccess); + ParseResult parseResult = engine.Parse(sourceExpression); + Assert.True(parseResult.IsSuccess); + FormulaValue valueResult = engine.Eval(sourceExpression); + if (expectedResult is null) + { + Assert.Null(valueResult.ToObject()); + } + else + { + Assert.IsType(valueResult.ToObject()); + Assert.Equal(expectedResult, valueResult.ToObject()); + } + } +} From 6f535d16a850084ab42b16c0197e1784ee20ed14 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 15 Jul 2025 13:28:16 -0700 Subject: [PATCH 02/40] Typos --- .../Process.Abstractions/KernelProcessDelegateStepInfo.cs | 2 +- .../Workflow/ObjectModel/Actions/EditTableV2Action.cs | 2 +- .../Workflow/ObjectModel/Actions/ParseValueAction.cs | 2 +- .../Workflow/ObjectModel/Actions/SetVariableAction.cs | 2 +- .../Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs | 2 +- .../Workflow/ObjectModel/PowerFx/TemplateExtensions.cs | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessDelegateStepInfo.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessDelegateStepInfo.cs index fdb3c40426b1..57774957595a 100644 --- a/dotnet/src/Experimental/Process.Abstractions/KernelProcessDelegateStepInfo.cs +++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessDelegateStepInfo.cs @@ -25,7 +25,7 @@ public KernelProcessDelegateStepInfo( } /// - /// Ste funtion + /// Step function /// public StepFunction StepFunction { get; } } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EditTableV2Action.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EditTableV2Action.cs index 38b8ca0d4d48..1b56aad95071 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EditTableV2Action.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EditTableV2Action.cs @@ -25,7 +25,7 @@ public override async Task HandleAsync(KernelProcessStepContext context, Process EditTableOperation? changeType = this.Action.ChangeType; if (changeType is AddItemOperation addItemOperation) { - FormulaValue result = engine.EvaluteExpression(addItemOperation.Value); + FormulaValue result = engine.EvaluateExpression(addItemOperation.Value); RecordValue newRecord = BuildRecord(tableValue.Type.ToRecord(), result); await tableValue.AppendAsync(newRecord, cancellationToken).ConfigureAwait(false); this.AssignTarget(engine, scopes, tableValue); diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ParseValueAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ParseValueAction.cs index 36de725b6a44..03b5f771b0f4 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ParseValueAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ParseValueAction.cs @@ -27,7 +27,7 @@ public override Task HandleAsync(KernelProcessStepContext context, ProcessAction ValueExpression value = this.Action.Value!; DataType valueType = this.Action.ValueType!; - FormulaValue result = engine.EvaluteExpression(value); + FormulaValue result = engine.EvaluateExpression(value); FormulaValue? parsedResult = null; if (result is StringValue stringValue) diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetVariableAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetVariableAction.cs index 9100b44ad389..e4b3919743c2 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetVariableAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetVariableAction.cs @@ -22,7 +22,7 @@ public SetVariableAction(SetVariable action) public override Task HandleAsync(KernelProcessStepContext context, ProcessActionScopes scopes, RecalcEngine engine, Kernel kernel, CancellationToken cancellationToken) { - FormulaValue result = engine.EvaluteExpression(this.Action.Value!); + FormulaValue result = engine.EvaluateExpression(this.Action.Value!); if (result is ErrorValue errorVal) // %%% APPLY EVERYWHERE (OR CENTRAL) { diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs index 710edaca5514..b3cb7b469f5e 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs @@ -44,7 +44,7 @@ public static async Task ExecuteActionsAsync(this RecalcEngine engine, KernelPro } } - public static FormulaValue EvaluteExpression(this RecalcEngine engine, ValueExpression? value) + public static FormulaValue EvaluateExpression(this RecalcEngine engine, ValueExpression? value) { if (value is null) { diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/TemplateExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/TemplateExtensions.cs index 27f63f7b3607..6bd35fecf360 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/TemplateExtensions.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/TemplateExtensions.cs @@ -44,6 +44,6 @@ internal static class TemplateExtensions } } - return $"UNSUPPORTED SEGEMENT: {segment.GetType().Name}"; // %%% LOG AND EMPTY STRING + return $"UNSUPPORTED SEGMENT: {segment.GetType().Name}"; // %%% LOG AND EMPTY STRING } } From 1194eec1debee8149d36bcecb9c19c3c2e200683 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 15 Jul 2025 13:34:20 -0700 Subject: [PATCH 03/40] Typo --- .../GettingStartedWithProcesses/Step06/deepResearch.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/samples/GettingStartedWithProcesses/Step06/deepResearch.yaml b/dotnet/samples/GettingStartedWithProcesses/Step06/deepResearch.yaml index c31be83ada1b..563d2ec8317f 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step06/deepResearch.yaml +++ b/dotnet/samples/GettingStartedWithProcesses/Step06/deepResearch.yaml @@ -42,7 +42,7 @@ beginDialog: value: =true - kind: SetVariable - id: setVariable_NUiu0l + id: setVariable_NZ2u0l displayName: Set Task variable: Topic.NewTask value: =System.LastMessage.Text From aab19d995b9fa8d8413cbdb03150cc869d28809c Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 16 Jul 2025 14:58:13 -0700 Subject: [PATCH 04/40] More conversion --- .../PowerFx/RecalcEngineExtensions.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs index b3cb7b469f5e..7a6c0d87258b 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs @@ -63,10 +63,25 @@ public static FormulaValue EvaluateExpression(this RecalcEngine engine, ValueExp if (value.IsLiteral) { - DataValue? source = value.LiteralValue; // %%% TODO: TRANSLATE VALUE - return BlankValue.NewBlank(); // %%% HACK + DataValue? source = value.LiteralValue; + return + source switch + { + null => FormulaValue.NewBlank(), + StringDataValue stringValue => FormulaValue.New(stringValue.Value), + NumberDataValue numberValue => FormulaValue.New(numberValue.Value), + BooleanDataValue boolValue => FormulaValue.New(boolValue.Value), + DateTimeDataValue dateTimeValue => FormulaValue.New(dateTimeValue.Value.DateTime), + DateDataValue dateValue => FormulaValue.New(dateValue.Value), + TimeDataValue timeValue => FormulaValue.New(timeValue.Value), + //RecordDataValue recordValue => FormulaValue.NewRecordFromFields(recordValue.Properties), // %%% TODO + //TableDataValue tableValue => FormulaValue.NewTable(), // %%% TODO + _ => FormulaValue.NewError(new Microsoft.PowerFx.ExpressionError { Message = $"Unknown literal type: {source.GetType().Name}" }), + }; } + // %%% TODO: value.StructuredRecordExpression ??? + return BlankValue.NewBlank(); } } From c85b55c5c5d6eaff067f4a7095b7d606ff620af6 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 16 Jul 2025 22:25:21 -0700 Subject: [PATCH 05/40] Clean-up and validation --- .../Step06/Step06_WorkflowProcess.cs | 3 +- .../Step06/loopTest.yaml | 56 ----- ...onditionalTest.yaml => testCondition.yaml} | 0 .../Step06/testLoop.yaml | 32 +++ .../KernelProcessDelegateStep.cs | 15 +- .../KernelProcessDelegateStepInfo.cs | 3 - .../Actions/AnswerQuestionWithAIAction.cs | 19 +- .../ObjectModel/Actions/AssignmentAction.cs | 43 ++++ .../ObjectModel/Actions/BeginDialogAction.cs | 7 +- .../Actions/ConditionGroupAction.cs | 7 +- .../ObjectModel/Actions/EditTableV2Action.cs | 34 +-- .../Actions/EndConversationAction.cs | 7 +- .../ObjectModel/Actions/ForeachAction.cs | 20 ++ .../ObjectModel/Actions/ParseValueAction.cs | 61 ++--- .../ObjectModel/Actions/SendActivityAction.cs | 11 +- .../Actions/SetTextVariableAction.cs | 30 +++ .../ObjectModel/Actions/SetVariableAction.cs | 20 +- .../Exceptions/InvalidActionException.cs | 6 +- .../Exceptions/InvalidExpressionException.cs | 35 +++ .../Exceptions/InvalidScopeException.cs | 35 +++ .../Exceptions/InvalidSegmentException.cs | 35 +++ .../Exceptions/ProcessActionException.cs | 4 +- .../Exceptions/ProcessWorkflowException.cs | 35 +++ .../ObjectModel/ObjectModelBuilder.cs | 8 +- .../PowerFx/DataValueExtensions.cs | 24 ++ .../PowerFx/FormulaValueExtensions.cs | 18 +- .../PowerFx/PropertyPathExtensions.cs | 10 + .../PowerFx/RecalcEngineExtensions.cs | 52 +---- .../PowerFx/RecalcEngineFactory.cs | 12 +- .../PowerFx/RecordDataTypeExtensions.cs | 38 ++++ .../ObjectModel/PowerFx/StringExtensions.cs | 21 ++ .../ObjectModel/PowerFx/TemplateExtensions.cs | 12 +- .../Workflow/ObjectModel/ProcessAction.cs | 56 ++--- .../ObjectModel/ProcessActionEnvironment.cs | 8 + .../ObjectModel/ProcessActionScopes.cs | 84 +++++-- .../ObjectModel/ProcessActionVisitor.cs | 30 ++- ...text.cs => ProcessActionVisitorContext.cs} | 2 +- .../ObjectModel/ProcessElementWalker.cs | 5 +- .../Process.UnitTests.csproj | 2 +- .../Process.UnitTests/TestOutputAdapter.cs | 73 ++++++ .../Workflow/ActionScopeTypeTests.cs | 146 ++++++++++++ .../MockKernelProcessMessageChannel.cs | 19 ++ .../Workflow/Actions/ProcessActionTest.cs | 46 ++++ .../Actions/SendActivityActionTest.cs | 69 ++++++ .../Actions/SetTextVariableActionTest.cs | 51 +++++ .../Workflow/Actions/SetVariableActionTest.cs | 211 ++++++++++++++++++ .../AnswerQuestionWithAIActionTest.cs | 21 -- .../RecalcEngineEvaluationTests.cs} | 22 +- .../{ => PowerFx}/RecalcEngineFactoryTests.cs | 16 +- .../Workflow/PowerFx/RecalcEngineTest.cs | 18 ++ .../PowerFx/TemplateExtensionsTests.cs | 173 ++++++++++++++ .../Workflow/ProcessActionScopesTests.cs | 164 ++++++++++++++ .../Workflow/ProcessActionTest.cs | 10 - .../Workflow/WorkflowTest.cs | 29 +++ 54 files changed, 1602 insertions(+), 366 deletions(-) delete mode 100644 dotnet/samples/GettingStartedWithProcesses/Step06/loopTest.yaml rename dotnet/samples/GettingStartedWithProcesses/Step06/{conditionalTest.yaml => testCondition.yaml} (100%) create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step06/testLoop.yaml create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AssignmentAction.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ForeachAction.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetTextVariableAction.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/InvalidExpressionException.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/InvalidScopeException.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/InvalidSegmentException.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/ProcessWorkflowException.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/DataValueExtensions.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/PropertyPathExtensions.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecordDataTypeExtensions.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/StringExtensions.cs rename dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/{ProcessActionContext.cs => ProcessActionVisitorContext.cs} (94%) create mode 100644 dotnet/src/Experimental/Process.UnitTests/TestOutputAdapter.cs create mode 100644 dotnet/src/Experimental/Process.UnitTests/Workflow/ActionScopeTypeTests.cs create mode 100644 dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/MockKernelProcessMessageChannel.cs create mode 100644 dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ProcessActionTest.cs create mode 100644 dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SendActivityActionTest.cs create mode 100644 dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SetTextVariableActionTest.cs create mode 100644 dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SetVariableActionTest.cs delete mode 100644 dotnet/src/Experimental/Process.UnitTests/Workflow/AnswerQuestionWithAIActionTest.cs rename dotnet/src/Experimental/Process.UnitTests/Workflow/{RecalcEngineTests.cs => PowerFx/RecalcEngineEvaluationTests.cs} (82%) rename dotnet/src/Experimental/Process.UnitTests/Workflow/{ => PowerFx}/RecalcEngineFactoryTests.cs (75%) create mode 100644 dotnet/src/Experimental/Process.UnitTests/Workflow/PowerFx/RecalcEngineTest.cs create mode 100644 dotnet/src/Experimental/Process.UnitTests/Workflow/PowerFx/TemplateExtensionsTests.cs create mode 100644 dotnet/src/Experimental/Process.UnitTests/Workflow/ProcessActionScopesTests.cs delete mode 100644 dotnet/src/Experimental/Process.UnitTests/Workflow/ProcessActionTest.cs create mode 100644 dotnet/src/Experimental/Process.UnitTests/Workflow/WorkflowTest.cs diff --git a/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs b/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs index afce13adc282..e50aa6277401 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs @@ -16,7 +16,8 @@ public Step06_WorkflowProcess(ITestOutputHelper output) public async Task RunWorkflowProcess() { const string InputEventId = "question"; - const string FileName = "conditionalTest"; + const string FileName = "testLoop"; + //const string FileName = "testCondition"; //const string FileName = "deepResearch"; Console.WriteLine("$$$ PROCESS INIT"); diff --git a/dotnet/samples/GettingStartedWithProcesses/Step06/loopTest.yaml b/dotnet/samples/GettingStartedWithProcesses/Step06/loopTest.yaml deleted file mode 100644 index 84e4db170e48..000000000000 --- a/dotnet/samples/GettingStartedWithProcesses/Step06/loopTest.yaml +++ /dev/null @@ -1,56 +0,0 @@ -kind: AdaptiveDialog -beginDialog: - kind: OnActivity - id: main - type: Message - actions: - - kind: SetVariable - id: setVariable_u4cBtN - displayName: Invocation count - variable: Topic.Count - value: =0 - - # - kind: SetVariable - # id: setVariable_a9f4o2 - # displayName: Invocation count - # variable: Topic.Count - # value: =Topic.Count + 1 - - # - kind: SendActivity - # id: sendActivity_aGsbRo - # activity: Looping (x{Topic.Count}) - - - kind: ConditionGroup - id: conditionGroup_mVIecC - conditions: - - id: conditionItem_fj432c - condition: =Topic.Count < 5 - displayName: Just started - actions: - - kind: SendActivity - id: sendActivity_Pkkmpq - activity: Just started (x{Topic.Count}) - - - id: conditionItem_yiqund - condition: =Topic.Count > 5 && Topic.Count < 10 - displayName: Making progress - actions: - - kind: SendActivity - id: sendActivity_aLM1o3 - activity: Making progress (x{Topic.Count}) - - - kind: GotoAction - id: goto_LzfJ8u - actionId: setVariable_a9f4o2 - - elseActions: - - kind: SendActivity - id: sendActivity_rOk31p - activity: All done (x{Topic.Count}) - - - kind: EndConversation - id: end_SVoNSV - - - kind: SendActivity - id: sendActivity_fJsbRz - activity: Complete! (x{Topic.Count}) diff --git a/dotnet/samples/GettingStartedWithProcesses/Step06/conditionalTest.yaml b/dotnet/samples/GettingStartedWithProcesses/Step06/testCondition.yaml similarity index 100% rename from dotnet/samples/GettingStartedWithProcesses/Step06/conditionalTest.yaml rename to dotnet/samples/GettingStartedWithProcesses/Step06/testCondition.yaml diff --git a/dotnet/samples/GettingStartedWithProcesses/Step06/testLoop.yaml b/dotnet/samples/GettingStartedWithProcesses/Step06/testLoop.yaml new file mode 100644 index 000000000000..d466da5af13e --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step06/testLoop.yaml @@ -0,0 +1,32 @@ +kind: AdaptiveDialog +beginDialog: + kind: OnActivity + id: main + type: Message + actions: + - kind: SetVariable + id: setVariable_u4cBtN + displayName: Invocation count + variable: Topic.Count + value: =0 + + - kind: SendActivity + id: sendActivity_aGsbRo + activity: Starting + + - kind: Foreach + id: foreach_mVIecC + items: ["a", "b", "c", "d", "e", "f"] + actions: + - kind: SetVariable + id: setVariable_A4iBtN + displayName: Invocation count + variable: Topic.Count + value: =Topic.Count + 1 + - kind: SendActivity + id: sendActivity_Pkkmpq + activity: Looping (x{Topic.Count}) + + - kind: SendActivity + id: sendActivity_fJsbRz + activity: Complete! (x{Topic.Count}) diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessDelegateStep.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessDelegateStep.cs index 275437bf5e36..f3100ed19dd1 100644 --- a/dotnet/src/Experimental/Process.Abstractions/KernelProcessDelegateStep.cs +++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessDelegateStep.cs @@ -6,10 +6,10 @@ namespace Microsoft.SemanticKernel; /// -/// %%% COMMENT +/// Signature for a step function targeted by . /// -/// -/// +/// The kernel instance used for processing. +/// The context for the step execution. public delegate Task StepFunction(Kernel kernel, KernelProcessStepContext context); /// @@ -18,7 +18,7 @@ namespace Microsoft.SemanticKernel; public class KernelDelegateProcessStep : KernelProcessStep { /// - /// %%% COMMENT + /// The name assigned to the delegate function that will be invoked by the step. /// public const string FunctionName = "Invoke"; @@ -27,7 +27,7 @@ public class KernelDelegateProcessStep : KernelProcessStep /// /// Initializes a new instance of the class with the specified step function. /// - /// + /// The step function to execute. /// public KernelDelegateProcessStep(StepFunction stepFunction) { @@ -37,9 +37,8 @@ public KernelDelegateProcessStep(StepFunction stepFunction) /// /// Invokes the step function with the provided kernel and context. /// - /// - /// - /// + /// The kernel instance used for processing. + /// The context for the step execution. [KernelFunction(FunctionName)] public Task InvokeAsync(Kernel kernel, KernelProcessStepContext context) => this._stepFunction(kernel, context); } diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessDelegateStepInfo.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessDelegateStepInfo.cs index 57774957595a..c133c19f7216 100644 --- a/dotnet/src/Experimental/Process.Abstractions/KernelProcessDelegateStepInfo.cs +++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessDelegateStepInfo.cs @@ -12,9 +12,6 @@ public record KernelProcessDelegateStepInfo : KernelProcessStepInfo /// /// Initializes a new instance of the class. /// - /// // %%% COMMENTS - /// - /// public KernelProcessDelegateStepInfo( KernelProcessStepState state, StepFunction stepFunction, diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AnswerQuestionWithAIAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AnswerQuestionWithAIAction.cs index aaca337920eb..08ac5092d1fd 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AnswerQuestionWithAIAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AnswerQuestionWithAIAction.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; using Microsoft.Extensions.DependencyInjection; -using Microsoft.PowerFx; using Microsoft.PowerFx.Types; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Process.Workflows.PowerFx; @@ -13,28 +12,28 @@ namespace Microsoft.SemanticKernel.Process.Workflows.Actions; internal sealed class AnswerQuestionWithAIAction : AssignmentAction { - public AnswerQuestionWithAIAction(AnswerQuestionWithAI action) - : base(action, () => action.Variable?.Path) + public AnswerQuestionWithAIAction(AnswerQuestionWithAI model) + : base(model, () => model.Variable?.Path) { - if (string.IsNullOrWhiteSpace(action.UserInput?.ExpressionText)) + if (string.IsNullOrWhiteSpace(model.UserInput?.ExpressionText)) { throw new InvalidActionException($"{nameof(AnswerQuestionWithAI)} must define {nameof(AnswerQuestionWithAI.UserInput)}"); } } - public override async Task HandleAsync(KernelProcessStepContext context, ProcessActionScopes scopes, RecalcEngine engine, Kernel kernel, CancellationToken cancellationToken) + protected override async Task HandleAsync(ProcessActionContext context, CancellationToken cancellationToken) { - IChatCompletionService chatCompletion = kernel.Services.GetRequiredService(); - FormulaValue expressionResult = engine.Eval(this.Action.UserInput!.ExpressionText); + IChatCompletionService chatCompletion = context.Kernel.Services.GetRequiredService(); + FormulaValue expressionResult = context.Engine.Eval(this.Model.UserInput!.ExpressionText); if (expressionResult is not StringValue stringResult) { throw new InvalidActionException($"{nameof(AnswerQuestionWithAI)} requires text for {nameof(AnswerQuestionWithAI.UserInput)}"); } ChatHistory history = []; - if (this.Action.AdditionalInstructions is not null) + if (this.Model.AdditionalInstructions is not null) { - string? instructions = engine.Format(this.Action.AdditionalInstructions); + string? instructions = context.Engine.Format(this.Model.AdditionalInstructions); if (!string.IsNullOrWhiteSpace(instructions)) { history.AddSystemMessage(instructions); @@ -44,6 +43,6 @@ public override async Task HandleAsync(KernelProcessStepContext context, Process ChatMessageContent response = await chatCompletion.GetChatMessageContentAsync(history, cancellationToken: cancellationToken).ConfigureAwait(false); StringValue responseValue = FormulaValue.New(response.ToString()); - this.AssignTarget(engine, scopes, responseValue); + this.AssignTarget(context, responseValue); } } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AssignmentAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AssignmentAction.cs new file mode 100644 index 000000000000..9224bd81d111 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AssignmentAction.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx.Types; +using Microsoft.SemanticKernel.Process.Workflows.PowerFx; + +namespace Microsoft.SemanticKernel.Process.Workflows.Actions; + +internal abstract class AssignmentAction : ProcessAction where TAction : DialogAction +{ + protected AssignmentAction(TAction model, Func resolver) + : base(model) + { + this.Target = + resolver.Invoke() ?? + throw new InvalidActionException($"Action '{model.GetType().Name}' must have a variable path defined."); + + if (string.IsNullOrWhiteSpace(this.Target.VariableScopeName)) + { + throw new InvalidActionException($"Action '{model.GetType().Name}' must define a variable scope."); + } + if (string.IsNullOrWhiteSpace(this.Target.VariableName)) + { + throw new InvalidActionException($"Action '{model.GetType().Name}' must define a variable name."); + } + } + + public PropertyPath Target { get; } + + protected void AssignTarget(ProcessActionContext context, FormulaValue result) + { + context.Engine.SetScopedVariable(context.Scopes, ActionScopeType.Parse(this.Target.VariableScopeName), this.Target.VariableName!, result); + string? resultValue = result.Format(); + string valuePosition = (resultValue?.IndexOf('\n') ?? -1) >= 0 ? Environment.NewLine : " "; + Console.WriteLine( // %%% DEVTRACE + $""" + !!! ASSIGN {this.GetType().Name} [{this.Id}] + NAME: {this.Target.VariableScopeName}.{this.Target.VariableName} + VALUE:{valuePosition}{result.Format()} ({result.GetType().Name}) + """); + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/BeginDialogAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/BeginDialogAction.cs index d3bafae02b98..29f6e06042cf 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/BeginDialogAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/BeginDialogAction.cs @@ -3,18 +3,17 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; -using Microsoft.PowerFx; namespace Microsoft.SemanticKernel.Process.Workflows.Actions; internal sealed class BeginDialogAction : ProcessAction { - public BeginDialogAction(BeginDialog source) - : base(source) + public BeginDialogAction(BeginDialog model) + : base(model) { } - public override Task HandleAsync(KernelProcessStepContext context, ProcessActionScopes scopes, RecalcEngine engine, Kernel kernel, CancellationToken cancellationToken) + protected override Task HandleAsync(ProcessActionContext context, CancellationToken cancellationToken) { // %%% TODO return Task.CompletedTask; diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ConditionGroupAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ConditionGroupAction.cs index c1b3c25b16bb..ea276e533f7e 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ConditionGroupAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ConditionGroupAction.cs @@ -3,18 +3,17 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; -using Microsoft.PowerFx; namespace Microsoft.SemanticKernel.Process.Workflows.Actions; internal sealed class ConditionGroupAction : ProcessAction { - public ConditionGroupAction(ConditionGroup source) - : base(source) + public ConditionGroupAction(ConditionGroup model) + : base(model) { } - public override Task HandleAsync(KernelProcessStepContext context, ProcessActionScopes scopes, RecalcEngine engine, Kernel kernel, CancellationToken cancellationToken) + protected override Task HandleAsync(ProcessActionContext context, CancellationToken cancellationToken) { // %%% REMOVE //foreach (ConditionItem condition in this.Action.Conditions) diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EditTableV2Action.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EditTableV2Action.cs index 1b56aad95071..6cfe938001eb 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EditTableV2Action.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EditTableV2Action.cs @@ -4,7 +4,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; -using Microsoft.PowerFx; using Microsoft.PowerFx.Types; using Microsoft.SemanticKernel.Process.Workflows.PowerFx; @@ -12,23 +11,23 @@ namespace Microsoft.SemanticKernel.Process.Workflows.Actions; internal sealed class EditTableV2Action : AssignmentAction { - public EditTableV2Action(EditTableV2 source) - : base(source, () => source.ItemsVariable?.Path) + public EditTableV2Action(EditTableV2 model) + : base(model, () => model.ItemsVariable?.Path) { } - public override async Task HandleAsync(KernelProcessStepContext context, ProcessActionScopes scopes, RecalcEngine engine, Kernel kernel, CancellationToken cancellationToken) + protected override async Task HandleAsync(ProcessActionContext context, CancellationToken cancellationToken) { - FormulaValue table = scopes[this.Target.VariableScopeName!][this.Target.VariableName!]; // %%% NULL OVERRIDE & MAKE UTILITY + FormulaValue table = context.Scopes.Get(this.Target.VariableName!, ActionScopeType.Parse(this.Target.VariableScopeName)); TableValue tableValue = (TableValue)table; - EditTableOperation? changeType = this.Action.ChangeType; + EditTableOperation? changeType = this.Model.ChangeType; if (changeType is AddItemOperation addItemOperation) { - FormulaValue result = engine.EvaluateExpression(addItemOperation.Value); + FormulaValue result = context.Engine.EvaluateExpression(addItemOperation.Value); RecordValue newRecord = BuildRecord(tableValue.Type.ToRecord(), result); await tableValue.AppendAsync(newRecord, cancellationToken).ConfigureAwait(false); - this.AssignTarget(engine, scopes, tableValue); + this.AssignTarget(context, tableValue); } else if (changeType is ClearItemsOperation) { @@ -44,6 +43,7 @@ public override async Task HandleAsync(KernelProcessStepContext context, Process static RecordValue BuildRecord(RecordType recordType, FormulaValue value) { return FormulaValue.NewRecordFromFields(recordType, GetValues()); + IEnumerable GetValues() { // %%% expression.StructuredRecordExpression.Properties ??? @@ -57,24 +57,6 @@ IEnumerable GetValues() { yield return new NamedValue(fieldType.Name, value); } - // %%% REMOVE ??? - //if (fieldType.Type is StringType) - //{ - // // For string fields, we can use a placeholder value - // yield return new NamedValue(fieldType.Name, value); // %%% TODO: VALUE - // continue; - //} - //if (fieldType.Type is BooleanType) - //{ - // // For boolean fields, we can use a default boolean value - // yield return new NamedValue(fieldType.Name, BooleanValue.New(true)); // %%% TODO: VALUE - // continue; - //} - //if (fieldType.Type is DecimalType) - //{ - // // For number fields, we can use a default numeric value - // yield return new NamedValue(fieldType.Name, NumberValue.New(-123)); // %%% TODO: VALUE - //} } } } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EndConversationAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EndConversationAction.cs index 6b8fcddfc550..09cd7f04801f 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EndConversationAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EndConversationAction.cs @@ -3,18 +3,17 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; -using Microsoft.PowerFx; namespace Microsoft.SemanticKernel.Process.Workflows.Actions; internal sealed class EndConversationAction : ProcessAction { - public EndConversationAction(EndConversation source) - : base(source) + public EndConversationAction(EndConversation model) + : base(model) { } - public override Task HandleAsync(KernelProcessStepContext context, ProcessActionScopes scopes, RecalcEngine engine, Kernel kernel, CancellationToken cancellationToken) + protected override Task HandleAsync(ProcessActionContext context, CancellationToken cancellationToken) { return Task.CompletedTask; } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ForeachAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ForeachAction.cs new file mode 100644 index 000000000000..ad41791f10e6 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ForeachAction.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.ObjectModel; + +namespace Microsoft.SemanticKernel.Process.Workflows.Actions; + +internal sealed class ForeachAction : ProcessAction +{ + public ForeachAction(Foreach model) + : base(model) + { + } + + protected override Task HandleAsync(ProcessActionContext context, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ParseValueAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ParseValueAction.cs index 03b5f771b0f4..18f572068409 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ParseValueAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ParseValueAction.cs @@ -1,11 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Collections.Generic; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; -using Microsoft.PowerFx; using Microsoft.PowerFx.Types; using Microsoft.SemanticKernel.Process.Workflows.PowerFx; @@ -16,60 +14,47 @@ internal sealed class ParseValueAction : AssignmentAction public ParseValueAction(ParseValue source) : base(source, () => source.Variable?.Path) { - if (this.Action.Value is null) + if (this.Model.Value is null) { throw new InvalidActionException($"{nameof(ParseValue)} must define {nameof(ParseValue.Value)}"); } } - public override Task HandleAsync(KernelProcessStepContext context, ProcessActionScopes scopes, RecalcEngine engine, Kernel kernel, CancellationToken cancellationToken) + protected override Task HandleAsync(ProcessActionContext context, CancellationToken cancellationToken) { - ValueExpression value = this.Action.Value!; - DataType valueType = this.Action.ValueType!; + FormulaValue? parsedResult = null; - FormulaValue result = engine.EvaluateExpression(value); + DataType valueType = this.Model.ValueType!; // %%% NULL OVERRIDE + FormulaValue result = context.Engine.EvaluateExpression(this.Model.Value); - FormulaValue? parsedResult = null; if (result is StringValue stringValue) { - // %%% TODO: TRIM ```json ... ``` - if (valueType is RecordDataType recordType) - { - JsonDocument json = JsonDocument.Parse(stringValue.Value); - JsonElement currentElement = json.RootElement; - parsedResult = ParseRecord(currentElement, recordType); - } + parsedResult = + valueType switch + { + StringDataType => stringValue, + NumberDataType => NumberValue.New(stringValue.Value), + BooleanDataType => BooleanValue.New(stringValue.Value), + RecordDataType recordType => ParseRecord(recordType, stringValue.Value), + _ => null + }; } - if (parsedResult is not null) + if (parsedResult is null) { - this.AssignTarget(engine, scopes, parsedResult); + throw new ProcessActionException($"Unable to parse {valueType.GetType().Name}"); } - // %%% ELSE THROW ??? + + this.AssignTarget(context, parsedResult); return Task.CompletedTask; } - private static RecordValue ParseRecord(JsonElement currentElement, RecordDataType recordType) + private static RecordValue ParseRecord(RecordDataType recordType, string rawText) { - return FormulaValue.NewRecordFromFields(ParseValues()); - - IEnumerable ParseValues() - { - foreach (KeyValuePair property in recordType.Properties) - { - JsonElement propertyElement = currentElement.GetProperty(property.Key); - FormulaValue? parsedValue = - property.Value.Type switch - { - StringDataType => StringValue.New(propertyElement.GetString()), - BooleanDataType => BooleanValue.New(propertyElement.GetBoolean()), - NumberDataType => NumberValue.New(propertyElement.GetDecimal()), - RecordDataType => ParseRecord(propertyElement, (RecordDataType)property.Value.Type), - _ => throw new InvalidActionException($"Unsupported data type '{property.Value.Type}' for property '{property.Key}'") // %%% EXCEPTION TYPE & MESSAGE - }; - yield return new NamedValue(property.Key, parsedValue); - } - } + string jsonText = rawText.TrimJsonDelimeter(); + JsonDocument json = JsonDocument.Parse(jsonText); + JsonElement currentElement = json.RootElement; + return recordType.ParseRecord(currentElement); } } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SendActivityAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SendActivityAction.cs index 91265b9fbd5e..030758186dbf 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SendActivityAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SendActivityAction.cs @@ -3,15 +3,14 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; -using Microsoft.PowerFx; namespace Microsoft.SemanticKernel.Process.Workflows.Actions; internal sealed class SendActivityAction : ProcessAction { - private readonly ProcessActionEnvironment _environment; + private readonly ActivityNotificationHandler _handler; - public SendActivityAction(SendActivity source, ProcessActionEnvironment environment) + public SendActivityAction(SendActivity source, ActivityNotificationHandler handler) : base(source) { if (source.Activity is null) @@ -19,11 +18,11 @@ public SendActivityAction(SendActivity source, ProcessActionEnvironment environm throw new InvalidActionException($"{nameof(SendActivity)} action must have an activity defined."); } - this._environment = environment; + this._handler = handler; } - public override async Task HandleAsync(KernelProcessStepContext context, ProcessActionScopes scopes, RecalcEngine engine, Kernel kernel, CancellationToken cancellationToken) + protected override async Task HandleAsync(ProcessActionContext context, CancellationToken cancellationToken) { - await this._environment.ActivityNotificationHandler(this.Action.Activity!, engine).ConfigureAwait(false); // %%% NULL OVERRIDE + await this._handler.Invoke(this.Model.Activity!, context.Engine).ConfigureAwait(false); } } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetTextVariableAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetTextVariableAction.cs new file mode 100644 index 000000000000..7d7128735b7c --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetTextVariableAction.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx.Types; +using Microsoft.SemanticKernel.Process.Workflows.PowerFx; + +namespace Microsoft.SemanticKernel.Process.Workflows.Actions; + +internal sealed class SetTextVariableAction : AssignmentAction +{ + public SetTextVariableAction(SetTextVariable model) + : base(model, () => model.Variable?.Path) + { + if (this.Model.Value is null) + { + throw new InvalidActionException($"{nameof(SetTextVariable)} must define {nameof(SetTextVariable.Value)}"); + } + } + + protected override Task HandleAsync(ProcessActionContext context, CancellationToken cancellationToken) + { + FormulaValue result = FormulaValue.New(context.Engine.Format(this.Model.Value)); + + this.AssignTarget(context, result); + + return Task.CompletedTask; + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetVariableAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetVariableAction.cs index e4b3919743c2..b459b6042a79 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetVariableAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetVariableAction.cs @@ -3,7 +3,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; -using Microsoft.PowerFx; using Microsoft.PowerFx.Types; using Microsoft.SemanticKernel.Process.Workflows.PowerFx; @@ -11,25 +10,20 @@ namespace Microsoft.SemanticKernel.Process.Workflows.Actions; internal sealed class SetVariableAction : AssignmentAction { - public SetVariableAction(SetVariable action) - : base(action, () => action.Variable?.Path) + public SetVariableAction(SetVariable model) + : base(model, () => model.Variable?.Path) { - if (this.Action.Value is null) + if (this.Model.Value is null) { - throw new InvalidActionException($"{nameof(ParseValue)} must define {nameof(ParseValue.Value)}"); + throw new InvalidActionException($"{nameof(SetVariable)} must define {nameof(SetVariable.Value)}"); } } - public override Task HandleAsync(KernelProcessStepContext context, ProcessActionScopes scopes, RecalcEngine engine, Kernel kernel, CancellationToken cancellationToken) + protected override Task HandleAsync(ProcessActionContext context, CancellationToken cancellationToken) { - FormulaValue result = engine.EvaluateExpression(this.Action.Value!); + FormulaValue result = context.Engine.EvaluateExpression(this.Model.Value).ThrowIfError(); - if (result is ErrorValue errorVal) // %%% APPLY EVERYWHERE (OR CENTRAL) - { - throw new ProcessActionException($"Unable to evaluate expression. Error: {errorVal.Errors[0].Message}"); - } - - this.AssignTarget(engine, scopes, result); + this.AssignTarget(context, result); return Task.CompletedTask; } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/InvalidActionException.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/InvalidActionException.cs index be7842d6437e..c55306c99abe 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/InvalidActionException.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/InvalidActionException.cs @@ -5,12 +5,12 @@ namespace Microsoft.SemanticKernel.Process.Workflows; /// -/// %%% COMMENT +/// Represents an exception that occurs when an action is invalid or cannot be processed. /// -public sealed class InvalidActionException : ProcessActionException +public sealed class InvalidActionException : ProcessWorkflowException { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public InvalidActionException() { diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/InvalidExpressionException.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/InvalidExpressionException.cs new file mode 100644 index 000000000000..bc1847271026 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/InvalidExpressionException.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace Microsoft.SemanticKernel.Process.Workflows; + +/// +/// Represents an exception that occurs when is invalid and cannot be evaluated. +/// +public sealed class InvalidExpressionException : ProcessWorkflowException +{ + /// + /// Initializes a new instance of the class. + /// + public InvalidExpressionException() + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The error message that explains the reason for the exception. + public InvalidExpressionException(string? message) : base(message) + { + } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public InvalidExpressionException(string? message, Exception? innerException) : base(message, innerException) + { + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/InvalidScopeException.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/InvalidScopeException.cs new file mode 100644 index 000000000000..adfda388ed82 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/InvalidScopeException.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace Microsoft.SemanticKernel.Process.Workflows; + +/// +/// Represents an exception that occurs when the specific scope is invalid. +/// +public sealed class InvalidScopeException : ProcessWorkflowException +{ + /// + /// Initializes a new instance of the class. + /// + public InvalidScopeException() + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The error message that explains the reason for the exception. + public InvalidScopeException(string? message) : base(message) + { + } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public InvalidScopeException(string? message, Exception? innerException) : base(message, innerException) + { + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/InvalidSegmentException.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/InvalidSegmentException.cs new file mode 100644 index 000000000000..20fdae88d56b --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/InvalidSegmentException.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace Microsoft.SemanticKernel.Process.Workflows; + +/// +/// Represents an exception that occurs when an action is invalid or cannot be processed. +/// +public sealed class InvalidSegmentException : ProcessWorkflowException +{ + /// + /// Initializes a new instance of the class. + /// + public InvalidSegmentException() + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The error message that explains the reason for the exception. + public InvalidSegmentException(string? message) : base(message) + { + } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public InvalidSegmentException(string? message, Exception? innerException) : base(message, innerException) + { + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/ProcessActionException.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/ProcessActionException.cs index b7511cf0c2b5..2b5c016bda6a 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/ProcessActionException.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/ProcessActionException.cs @@ -5,9 +5,9 @@ namespace Microsoft.SemanticKernel.Process.Workflows; /// -/// %%% COMMENT +/// Represents an exception that occurs during the execution of a process action. /// -public class ProcessActionException : KernelException +public class ProcessActionException : ProcessWorkflowException { /// /// Initializes a new instance of the class. diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/ProcessWorkflowException.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/ProcessWorkflowException.cs new file mode 100644 index 000000000000..b8fd4631f711 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/ProcessWorkflowException.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace Microsoft.SemanticKernel.Process.Workflows; + +/// +/// Represents any exception that occurs during the execution of a process workflow. +/// +public class ProcessWorkflowException : KernelException +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessWorkflowException() + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The error message that explains the reason for the exception. + public ProcessWorkflowException(string? message) : base(message) + { + } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public ProcessWorkflowException(string? message, Exception? innerException) : base(message, innerException) + { + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs index a29900b92544..1608bf5c3b43 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs @@ -12,12 +12,12 @@ public static class ObjectModelBuilder /// /// Builds a process from the provided YAML definition of a CPS Topic ObjectModel. /// - /// // %%% COMMENT + /// The identifier for the process. /// The YAML string defining the CPS Topic ObjectModel. - /// // %%% COMMENT - /// // %%% COMMENT + /// The identifier for the message. + /// The environment for the process actions. /// The that corresponds with the YAML object model. - public static KernelProcess Build(string processId, string workflowYaml, string messageId, ProcessActionEnvironment? environment = null) // %%% REVISIT ENVIRONMENT + public static KernelProcess Build(string processId, string workflowYaml, string messageId, ProcessActionEnvironment? environment = null) { ProcessBuilder processBuilder = new(processId); ProcessActionWalker walker = new(processBuilder, messageId, environment ?? ProcessActionEnvironment.Default); diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/DataValueExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/DataValueExtensions.cs new file mode 100644 index 000000000000..07c70199ef34 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/DataValueExtensions.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx.Types; + +namespace Microsoft.SemanticKernel.Process.Workflows.PowerFx; + +internal static class DataValueExtensions +{ + public static FormulaValue ToFormulaValue(this DataValue? value) => + value switch + { + null => FormulaValue.NewBlank(), + StringDataValue stringValue => FormulaValue.New(stringValue.Value), + NumberDataValue numberValue => FormulaValue.New(numberValue.Value), + BooleanDataValue boolValue => FormulaValue.New(boolValue.Value), + DateTimeDataValue dateTimeValue => FormulaValue.New(dateTimeValue.Value.DateTime), + DateDataValue dateValue => FormulaValue.New(dateValue.Value), + TimeDataValue timeValue => FormulaValue.New(timeValue.Value), + //RecordDataValue recordValue => FormulaValue.NewRecordFromFields(recordValue.Properties), // %%% TODO + //TableDataValue tableValue => FormulaValue.NewTable(), // %%% TODO + _ => FormulaValue.NewError(new Microsoft.PowerFx.ExpressionError { Message = $"Unknown literal type: {value.GetType().Name}" }), + }; +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/FormulaValueExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/FormulaValueExtensions.cs index b3567f2e4f60..5ca18d0cef93 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/FormulaValueExtensions.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/FormulaValueExtensions.cs @@ -22,27 +22,37 @@ internal static class FormulaValueExtensions { typeof(StringValue), FromStringValue }, }.ToImmutableDictionary(); + public static FormulaValue ThrowIfError(this FormulaValue value) + { + if (value is ErrorValue errorVal) + { + throw new InvalidExpressionException($"Failure evaluating expression. Error: {errorVal.Errors[0].Message}"); + } + + return value; + } + public static string? Format(this FormulaValue value) { Type valueType = value.GetType(); if (s_handlers.TryGetValue(valueType, out GetFormulaValue? handler)) { - return $"{handler.Invoke(value) ?? "-"} ({valueType.Name})"; + return $"{handler.Invoke(value)}"; } - foreach (KeyValuePair kvp in s_handlers) + foreach (KeyValuePair kvp in s_handlers) // %%% NEEDED ??? { if (kvp.Key.IsAssignableFrom(valueType)) { - return $"{kvp.Value.Invoke(value) ?? "-"} ({valueType.Name})"; + return $"{kvp.Value.Invoke(value)}"; } } return value.ToString(); } - // %%% TODO + // %%% TODO: Type conversion //VoidValue //NamedValue //BlobValue diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/PropertyPathExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/PropertyPathExtensions.cs new file mode 100644 index 000000000000..14b3bb00ff5f --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/PropertyPathExtensions.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Bot.ObjectModel; + +namespace Microsoft.SemanticKernel.Process.Workflows.PowerFx; + +internal static class PropertyPathExtensions +{ + public static string Format(this PropertyPath path) => $"{path.VariableScopeName}.{path.VariableName}"; +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs index 7a6c0d87258b..319c824c3b6e 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs @@ -11,49 +11,27 @@ namespace Microsoft.SemanticKernel.Process.Workflows.PowerFx; internal static class RecalcEngineExtensions { - public static void SetScopedVariable(this RecalcEngine engine, ProcessActionScopes scopes, string scopeName, string varName, FormulaValue value) + public static void SetScopedVariable(this RecalcEngine engine, ProcessActionScopes scopes, ActionScopeType scope, string varName, FormulaValue value) { // Validate inputs and assign value. - ProcessActionScope scope = scopes.AssignValue(scopeName, varName, value); + scopes.Set(varName, scope, value); // Rebuild scope record and update engine - RecordValue scopeRecord = scope.BuildRecord(); - engine.DeleteFormula(scopeName); - engine.UpdateVariable(scopeName, scopeRecord); - } - - public static async Task ExecuteActionsAsync(this RecalcEngine engine, KernelProcessStepContext context, ProcessActionScopes scopes, ProcessAction action, Kernel kernel, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - try - { - // Execute each action in the current context - //Console.WriteLine($"!!! ACTION {action.GetType().Name} [{action.Id}]"); // %%% DEVTRACE - await action.HandleAsync(context, scopes, engine, kernel, cancellationToken).ConfigureAwait(false); - } - catch (ProcessActionException exception) - { - Console.WriteLine($"*** ACTION [{action.Id}] ERROR - {exception.GetType().Name}\n{exception.Message}"); // %%% DEVTRACE - throw; - } - catch (Exception exception) - { - Console.WriteLine($"*** ACTION [{action.Id}] ERROR - {exception.GetType().Name}\n{exception.Message}"); // %%% DEVTRACE - throw new ProcessActionException($"Unexpected failure executing action #{action.Id} [{action.GetType().Name}]", exception); - } + RecordValue scopeRecord = scopes.BuildRecord(scope); + engine.DeleteFormula(scope.Name); + engine.UpdateVariable(scope.Name, scopeRecord); } public static FormulaValue EvaluateExpression(this RecalcEngine engine, ValueExpression? value) { if (value is null) { - return BlankValue.NewBlank(); // %%% HANDLE NULL CASE + return BlankValue.NewBlank(); } if (value.IsVariableReference) { - return engine.Eval($"{value.VariableReference!.VariableScopeName}.{value.VariableReference!.VariableName}"); // %%% DRY + return engine.Eval(value.VariableReference?.Format()); } if (value.IsExpression) @@ -63,21 +41,7 @@ public static FormulaValue EvaluateExpression(this RecalcEngine engine, ValueExp if (value.IsLiteral) { - DataValue? source = value.LiteralValue; - return - source switch - { - null => FormulaValue.NewBlank(), - StringDataValue stringValue => FormulaValue.New(stringValue.Value), - NumberDataValue numberValue => FormulaValue.New(numberValue.Value), - BooleanDataValue boolValue => FormulaValue.New(boolValue.Value), - DateTimeDataValue dateTimeValue => FormulaValue.New(dateTimeValue.Value.DateTime), - DateDataValue dateValue => FormulaValue.New(dateValue.Value), - TimeDataValue timeValue => FormulaValue.New(timeValue.Value), - //RecordDataValue recordValue => FormulaValue.NewRecordFromFields(recordValue.Properties), // %%% TODO - //TableDataValue tableValue => FormulaValue.NewTable(), // %%% TODO - _ => FormulaValue.NewError(new Microsoft.PowerFx.ExpressionError { Message = $"Unknown literal type: {source.GetType().Name}" }), - }; + return value.LiteralValue.ToFormulaValue(); } // %%% TODO: value.StructuredRecordExpression ??? diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineFactory.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineFactory.cs index b20e09772da1..0d9e562eeaec 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineFactory.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineFactory.cs @@ -11,16 +11,16 @@ public static RecalcEngine Create(ProcessActionScopes scopes, int maximumExpress { RecalcEngine engine = new(CreateConfig()); - SetScope(ActionScopeTypes.Topic); - SetScope(ActionScopeTypes.Global); - SetScope(ActionScopeTypes.System); + SetScope(ActionScopeType.Topic); + SetScope(ActionScopeType.Global); + SetScope(ActionScopeType.System); return engine; - void SetScope(string scopeName) + void SetScope(ActionScopeType scope) { - RecordValue record = scopes[scopeName].BuildRecord(); - engine.UpdateVariable(scopeName, record); + RecordValue record = scopes.BuildRecord(scope); + engine.UpdateVariable(scope.Name, record); } PowerFxConfig CreateConfig() diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecordDataTypeExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecordDataTypeExtensions.cs new file mode 100644 index 000000000000..a319de3e759d --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecordDataTypeExtensions.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json; +using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx.Types; + +namespace Microsoft.SemanticKernel.Process.Workflows.PowerFx; + +internal static class RecordDataTypeExtensions +{ + public static RecordValue ParseRecord(this RecordDataType recordType, JsonElement currentElement) + { + return FormulaValue.NewRecordFromFields(ParseValues()); + + IEnumerable ParseValues() + { + foreach (KeyValuePair property in recordType.Properties) + { + JsonElement propertyElement = currentElement.GetProperty(property.Key); + FormulaValue? parsedValue = + property.Value.Type switch + { + StringDataType => StringValue.New(propertyElement.GetString()), + NumberDataType => NumberValue.New(propertyElement.GetDecimal()), + BooleanDataType => BooleanValue.New(propertyElement.GetBoolean()), + DateTimeDataType dateTimeType => DateTimeValue.New(propertyElement.GetDateTime()), + DateDataType dateType => DateValue.New(propertyElement.GetDateTime()), + TimeDataType timeType => TimeValue.New(propertyElement.GetDateTimeOffset().TimeOfDay), + RecordDataType recordType => recordType.ParseRecord(propertyElement), + //TableDataValue tableValue => // %%% TODO + _ => throw new InvalidActionException($"Unsupported data type '{property.Value.Type}' for property '{property.Key}'") // %%% EXCEPTION TYPE & MESSAGE + }; + yield return new NamedValue(property.Key, parsedValue); + } + } + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/StringExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/StringExtensions.cs new file mode 100644 index 000000000000..5dda05b0dab8 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/StringExtensions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.RegularExpressions; + +namespace Microsoft.SemanticKernel.Process.Workflows.PowerFx; + +internal static class StringExtensions +{ + private static readonly Regex s_regex = new(@"^```json\s*([\s\S]*?)\s*```$", RegexOptions.Compiled | RegexOptions.Multiline); + + public static string TrimJsonDelimeter(this string value) + { + Match match = s_regex.Match(value); + if (match.Success) + { + return match.Groups[0].Value.Trim(); + } + + return value; + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/TemplateExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/TemplateExtensions.cs index 6bd35fecf360..8a32290cd627 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/TemplateExtensions.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/TemplateExtensions.cs @@ -12,12 +12,12 @@ internal static class TemplateExtensions { public static string? Format(this RecalcEngine engine, IEnumerable template) { - return string.Concat(template.Select(t => engine.Format(t))); + return string.Concat(template.Select(line => engine.Format(line))); } - public static string? Format(this RecalcEngine engine, TemplateLine line) + public static string? Format(this RecalcEngine engine, TemplateLine? line) { - return string.Concat(line.Segments.Select(s => engine.Format(s))); + return string.Concat(line?.Segments.Select(segment => engine.Format(segment)) ?? [string.Empty]); } private static string? Format(this RecalcEngine engine, TemplateSegment segment) @@ -34,16 +34,16 @@ internal static class TemplateExtensions if (expressionSegment.Expression.ExpressionText is not null) { FormulaValue expressionValue = engine.Eval(expressionSegment.Expression.ExpressionText); - return expressionValue?.Format(); + return expressionValue.Format(); } if (expressionSegment.Expression.VariableReference is not null) { FormulaValue expressionValue = engine.Eval(expressionSegment.Expression.VariableReference.ToString()); - return expressionValue?.Format(); + return expressionValue.Format(); } } } - return $"UNSUPPORTED SEGMENT: {segment.GetType().Name}"; // %%% LOG AND EMPTY STRING + throw new InvalidSegmentException($"Unsupported segment type: {segment.GetType().Name}"); } } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessAction.cs index c2115f75cd9b..82dc8bcd800c 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessAction.cs @@ -5,56 +5,42 @@ using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; using Microsoft.PowerFx; -using Microsoft.PowerFx.Types; -using Microsoft.SemanticKernel.Process.Workflows.PowerFx; namespace Microsoft.SemanticKernel.Process.Workflows; -internal abstract class ProcessAction(TAction action) : ProcessAction(action) where TAction : DialogAction +internal sealed record class ProcessActionContext(RecalcEngine Engine, ProcessActionScopes Scopes, Kernel Kernel); + +internal abstract class ProcessAction(TAction model) : ProcessAction(model) where TAction : DialogAction { - public new TAction Action => action; + public new TAction Model => (TAction)base.Model; } -internal abstract class ProcessAction(DialogAction action) +internal abstract class ProcessAction(DialogAction model) { - public ActionId Id => action.Id; - - public DialogAction Action => action; + public ActionId Id => model.Id; - public abstract Task HandleAsync(KernelProcessStepContext context, ProcessActionScopes scopes, RecalcEngine engine, Kernel kernel, CancellationToken cancellationToken); -} + public DialogAction Model => model; -internal abstract class AssignmentAction : ProcessAction where TAction : DialogAction -{ - protected AssignmentAction(TAction action, Func resolver) - : base(action) + public async Task ExecuteAsync(ProcessActionContext context, CancellationToken cancellationToken) { - this.Target = - resolver.Invoke() ?? - throw new InvalidActionException($"Action '{action.GetType().Name}' must have a variable path defined."); + cancellationToken.ThrowIfCancellationRequested(); - if (string.IsNullOrWhiteSpace(this.Target.VariableScopeName)) + try { - throw new InvalidActionException($"Action '{action.GetType().Name}' must define a variable scope."); + // Execute each action in the current context + await this.HandleAsync(context, cancellationToken).ConfigureAwait(false); } - if (string.IsNullOrWhiteSpace(this.Target.VariableName)) + catch (ProcessWorkflowException exception) { - throw new InvalidActionException($"Action '{action.GetType().Name}' must define a variable name."); + Console.WriteLine($"*** ACTION [{this.Id}] ERROR - {exception.GetType().Name}\n{exception.Message}"); // %%% DEVTRACE + throw; + } + catch (Exception exception) + { + Console.WriteLine($"*** ACTION [{this.Id}] ERROR - {exception.GetType().Name}\n{exception.Message}"); // %%% DEVTRACE + throw new ProcessWorkflowException($"Unexpected failure executing action #{this.Id} [{this.GetType().Name}]", exception); } } - public PropertyPath Target { get; } - - protected void AssignTarget(RecalcEngine engine, ProcessActionScopes scopes, FormulaValue result) - { - engine.SetScopedVariable(scopes, this.Target.VariableScopeName!, this.Target.VariableName!, result); - string? resultValue = result.Format(); - string valuePosition = (resultValue?.IndexOf('\n') ?? -1) >= 0 ? Environment.NewLine : " "; - Console.WriteLine( // %%% DEVTRACE - $""" - !!! ASSIGN {this.GetType().Name} [{this.Id}] - NAME: {this.Target.VariableScopeName}.{this.Target.VariableName} - VALUE:{valuePosition}{result.Format()} - """); - } + protected abstract Task HandleAsync(ProcessActionContext context, CancellationToken cancellationToken); } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionEnvironment.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionEnvironment.cs index 59f1df9530a6..0f09f22ffbb6 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionEnvironment.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionEnvironment.cs @@ -8,6 +8,14 @@ namespace Microsoft.SemanticKernel; +/// +/// %%% COMMENT +/// +/// +/// +/// +public delegate Task ActivityNotificationHandler(ActivityTemplateBase activity, RecalcEngine engine); + /// /// %%% COMMENT /// diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionScopes.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionScopes.cs index 60f03be9c04c..4ea3bee5b529 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionScopes.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionScopes.cs @@ -1,53 +1,93 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Collections.Generic; +using System.Collections.Immutable; using Microsoft.PowerFx.Types; namespace Microsoft.SemanticKernel.Process.Workflows; -internal static class ActionScopeTypes +/// +/// Describes the type of action scope. +/// +internal sealed class ActionScopeType { - public const string Topic = nameof(Topic); - public const string Global = nameof(Global); - public const string System = nameof(System); + public static readonly ActionScopeType Env = new(nameof(Env)); + public static readonly ActionScopeType Topic = new(nameof(Topic)); + public static readonly ActionScopeType Global = new(nameof(Global)); + public static readonly ActionScopeType System = new(nameof(System)); + + public static ActionScopeType Parse(string? scope) + { + return scope switch + { + nameof(Env) => Env, + nameof(Global) => Global, + nameof(System) => System, + nameof(Topic) => Topic, + null => throw new InvalidScopeException("Undefined action scope type."), + _ => throw new InvalidScopeException($"Unknown action scope type: {scope}."), + }; + } + + private ActionScopeType(string name) + { + this.Name = name; + } + + public string Name { get; } + + public override string ToString() => this.Name; + + public override int GetHashCode() => this.Name.GetHashCode(); + + public override bool Equals(object? obj) => + (obj is ActionScopeType other && this.Name.Equals(other.Name, StringComparison.Ordinal)) || + (obj is string name && this.Name.Equals(name, StringComparison.Ordinal)); } +/// +/// The set of variables for a specific action scope. +/// internal sealed class ProcessActionScope : Dictionary; -internal sealed class ProcessActionScopes : Dictionary +/// +/// Contains all action scopes for a process. +/// +internal sealed class ProcessActionScopes { + private readonly ImmutableDictionary _scopes; + public ProcessActionScopes() { - this[ActionScopeTypes.Topic] = []; - this[ActionScopeTypes.Global] = []; - this[ActionScopeTypes.System] = []; + Dictionary scopes = + new() + { + { ActionScopeType.Env, [] }, + { ActionScopeType.Topic, [] }, + { ActionScopeType.Global, [] }, + { ActionScopeType.System, [] }, + }; + + this._scopes = scopes.ToImmutableDictionary(); } -} -internal static class ProcessActionScopeExtensions -{ - public static RecordValue BuildRecord(this ProcessActionScope scope) + public RecordValue BuildRecord(ActionScopeType scope) { return FormulaValue.NewRecordFromFields(GetFields()); IEnumerable GetFields() { - foreach (KeyValuePair kvp in scope) + foreach (KeyValuePair kvp in this._scopes[scope]) { yield return new NamedValue(kvp.Key, kvp.Value); } } } - public static ProcessActionScope AssignValue(this ProcessActionScopes scopes, string scopeName, string varName, FormulaValue value) - { - if (!scopes.TryGetValue(scopeName, out ProcessActionScope? scope)) - { - throw new InvalidActionException("Unknown scope: " + scopeName); - } + public FormulaValue Get(string name, ActionScopeType? type = null) => this._scopes[type ?? ActionScopeType.Topic][name]; - scope[varName] = value; + public void Set(string name, FormulaValue value) => this.Set(name, ActionScopeType.Topic, value); - return scope; - } + public void Set(string name, ActionScopeType type, FormulaValue value) => this._scopes[type][name] = value; } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs index 8243640e4f12..9ed4566c4943 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs @@ -17,7 +17,7 @@ internal sealed class ProcessActionVisitor : DialogActionVisitor private readonly ProcessActionEnvironment _environment; private readonly ProcessActionScopes _scopes; private readonly Dictionary _steps; - private readonly Stack _contextStack; + private readonly Stack _contextStack; private readonly List<(ActionId TargetId, ProcessStepEdgeBuilder SourceEdge)> _linkCache; public ProcessActionVisitor( @@ -26,7 +26,7 @@ public ProcessActionVisitor( ProcessStepBuilder sourceStep, ProcessActionScopes scopes) { - ProcessActionContext rootContext = new(sourceStep); + ProcessActionVisitorContext rootContext = new(sourceStep); this._contextStack = []; this._contextStack.Push(rootContext); this._steps = []; @@ -64,7 +64,7 @@ public void Complete() Console.WriteLine("> COMPLETE"); // %%% DEVTRACE } - private ProcessActionContext CurrentContext => this._contextStack.Peek(); + private ProcessActionVisitorContext CurrentContext => this._contextStack.Peek(); protected override void Visit(ActionScope item) { @@ -84,11 +84,11 @@ protected override void Visit(ConditionGroup item) foreach (ConditionItem conditionItem in item.Conditions) { ProcessStepBuilder step = this.CreateContainerStep(this.CurrentContext, conditionItem.Id ?? $"{item.Id.Value}_item{index}"); - this._contextStack.Push(new ProcessActionContext(step)); + this._contextStack.Push(new ProcessActionVisitorContext(step)); conditionItem.Accept(this); - ProcessActionContext conditionContext = this._contextStack.Pop(); + ProcessActionVisitorContext conditionContext = this._contextStack.Pop(); KernelProcessEdgeCondition? condition = null; if (conditionItem.Condition is not null) @@ -97,7 +97,7 @@ protected override void Visit(ConditionGroup item) condition = new((stepEvent, state) => { - RecalcEngine engine = RecalcEngineFactory.Create(this._scopes, this._environment.MaximumExpressionLength); // %%% DRY + RecalcEngine engine = this.CreateEngine(); bool result = engine.Eval(conditionItem.Condition.ExpressionText ?? "true").AsBoolean(); Console.WriteLine($"!!! CONDITION: {conditionItem.Condition.ExpressionText ?? "true"}={result}"); return Task.FromResult(result); @@ -166,6 +166,8 @@ protected override void Visit(SetVariable item) protected override void Visit(SetTextVariable item) { Trace(item); + + this.AddAction(new SetTextVariableAction(item)); } protected override void Visit(EditTable item) @@ -191,7 +193,7 @@ protected override void Visit(SendActivity item) { Trace(item, isSkipped: false); - this.AddAction(new SendActivityAction(item, this._environment)); + this.AddAction(new SendActivityAction(item, this._environment.ActivityNotificationHandler)); } #region Not implemented @@ -259,6 +261,8 @@ protected override void Visit(OAuthInput item) protected override void Visit(Foreach item) { Trace(item); + + this.AddAction(new ForeachAction(item)); } protected override void Visit(RepeatDialog item) @@ -408,7 +412,7 @@ private void AddStep(ProcessStepBuilder step) this.ContinueWith(step); } - private ProcessStepBuilder CreateContainerStep(ProcessActionContext currentContext, string contextId) + private ProcessStepBuilder CreateContainerStep(ProcessActionVisitorContext currentContext, string contextId) { return this.InitializeStep( this._processBuilder.AddStepFromFunction( @@ -422,7 +426,7 @@ private ProcessStepBuilder CreateContainerStep(ProcessActionContext currentConte // This implementation accepts the context as a parameter in order to pin the context closure. // The step cannot reference this.CurrentContext directly, as this will always be the final context. - private ProcessStepBuilder CreateActionStep(ProcessActionContext currentContext, ProcessAction action) + private ProcessStepBuilder CreateActionStep(ProcessActionVisitorContext currentContext, ProcessAction action) { return this.InitializeStep( this._processBuilder.AddStepFromFunction( @@ -431,9 +435,9 @@ private ProcessStepBuilder CreateActionStep(ProcessActionContext currentContext, { try { - Console.WriteLine($"!!! STEP [{action.Id}]"); // %%% DEVTRACE - RecalcEngine engine = RecalcEngineFactory.Create(this._scopes, this._environment.MaximumExpressionLength); // %%% DRY - await engine.ExecuteActionsAsync(context, this._scopes, action, kernel, cancellationToken: default).ConfigureAwait(false); + Console.WriteLine($"!!! STEP {action.GetType().Name} [{action.Id}]"); // %%% DEVTRACE + ProcessActionContext actionContext = new(this.CreateEngine(), this._scopes, kernel); + await action.ExecuteAsync(actionContext, cancellationToken: default).ConfigureAwait(false); // %%% CANCEL TOKEN } catch (ProcessActionException) { @@ -462,6 +466,8 @@ private void ContinueWith(ProcessStepBuilder newStep, KernelProcessEdgeCondition this.CurrentContext.Step = newStep; } + private RecalcEngine CreateEngine() => RecalcEngineFactory.Create(this._scopes, this._environment.MaximumExpressionLength); + private static void Trace(DialogAction item, bool isSkipped = true) { Console.WriteLine($"> {(isSkipped ? "EMPTY" : "VISIT")} - {item.GetType().Name} [{item.Id.Value}]"); // %%% DEVTRACE diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionContext.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitorContext.cs similarity index 94% rename from dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionContext.cs rename to dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitorContext.cs index 86601d0f2b84..85cdbe08c891 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionContext.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitorContext.cs @@ -10,7 +10,7 @@ namespace Microsoft.SemanticKernel.Process.Workflows; /// /// Step context for the current step in a process. /// -internal sealed class ProcessActionContext(ProcessStepBuilder step) +internal sealed class ProcessActionVisitorContext(ProcessStepBuilder step) { /// /// The current step for the context. diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessElementWalker.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessElementWalker.cs index c30da8b1eae1..ad5ecebbd501 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessElementWalker.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessElementWalker.cs @@ -40,7 +40,7 @@ public override bool DefaultVisit(BotElement definition) private static ProcessActionVisitor CreateActionVisitor(ProcessBuilder processBuilder, string messageId, ProcessActionEnvironment processEnvironment) { - ProcessActionScopes scopes = []; + ProcessActionScopes scopes = new(); ProcessStepBuilder initStep = processBuilder.AddStepFromType(scopes, "init"); @@ -74,8 +74,7 @@ public void InitializeProcess(string message) } Console.WriteLine("!!! INIT WORKFLOW"); - FormulaValue inputTask = StringValue.New(message); - this._scopes[ActionScopeTypes.System]["LastMessage"] = inputTask; // %%% MAGIC CONST + this._scopes.Set("LastMessage", ActionScopeType.System, StringValue.New(message)); // %%% MAGIC CONST "LastMessage" } } } diff --git a/dotnet/src/Experimental/Process.UnitTests/Process.UnitTests.csproj b/dotnet/src/Experimental/Process.UnitTests/Process.UnitTests.csproj index 566e3f5559aa..f2c10974d010 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Process.UnitTests.csproj +++ b/dotnet/src/Experimental/Process.UnitTests/Process.UnitTests.csproj @@ -8,7 +8,7 @@ true false 12 - $(NoWarn);CA2007,CA1812,CA1861,CA1063,VSTHRD111,SKEXP0001,SKEXP0050,SKEXP0080,SKEXP0110;OPENAI001,CA1024 + $(NoWarn);CA2007;CA1812;CA1861;CA1063;IDE1006;VSTHRD111;SKEXP0001;SKEXP0050;SKEXP0080;SKEXP0110;OPENAI001;CA1024 diff --git a/dotnet/src/Experimental/Process.UnitTests/TestOutputAdapter.cs b/dotnet/src/Experimental/Process.UnitTests/TestOutputAdapter.cs new file mode 100644 index 000000000000..8f04bf98aa5f --- /dev/null +++ b/dotnet/src/Experimental/Process.UnitTests/TestOutputAdapter.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace Microsoft.SemanticKernel.Process.UnitTests; + +public sealed class TestOutputAdapter(ITestOutputHelper output) : TextWriter, ILogger, ILoggerFactory +{ + private readonly Stack _scopes = []; + + public override Encoding Encoding { get; } = Encoding.UTF8; + + public void AddProvider(ILoggerProvider provider) => throw new NotSupportedException(); + + public ILogger CreateLogger(string categoryName) => this; + + public bool IsEnabled(LogLevel logLevel) => true; + + public override void WriteLine(object? value = null) => this.SafeWrite($"{value}"); + + public override void WriteLine(string? format, params object?[] arg) => this.SafeWrite(string.Format(format ?? string.Empty, arg)); + + public override void WriteLine(string? value) => this.SafeWrite(value ?? string.Empty); + + public override void Write(object? value = null) => this.SafeWrite($"{value}"); + + public override void Write(char[]? buffer) => this.SafeWrite(new string(buffer)); + + public IDisposable BeginScope(TState state) where TState : notnull + { + this._scopes.Push($"{state}"); + return new LoggerScope(() => this._scopes.Pop()); + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + string message = formatter(state, exception); + string scope = this._scopes.Count > 0 ? $"[{this._scopes.Peek()}] " : string.Empty; + output.WriteLine($"{scope}{message}"); + } + + private void SafeWrite(string value) + { + try + { + output.WriteLine(value ?? string.Empty); + } + catch (InvalidOperationException exception) when (exception.Message == "There is no currently active test.") + { + // This exception is thrown when the test output is accessed outside of a test context. + // We can ignore it since we are not in a test context. + } + } + + private sealed class LoggerScope(Action action) : IDisposable + { + private bool _disposed; + + public void Dispose() + { + if (!this._disposed) + { + action.Invoke(); + this._disposed = true; + } + } + } +} diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/ActionScopeTypeTests.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/ActionScopeTypeTests.cs new file mode 100644 index 000000000000..7a9e490981a3 --- /dev/null +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/ActionScopeTypeTests.cs @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel.Process.Workflows; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.SemanticKernel.Process.UnitTests.Workflows; + +public sealed class ActionScopeTypeTests(ITestOutputHelper output) : WorkflowTest(output) +{ + [Fact] + public void VerifyStaticInstances() + { + // Arrange & Act & Assert + Assert.Equal(nameof(ActionScopeType.Env), ActionScopeType.Env.Name); + Assert.Equal(nameof(ActionScopeType.Topic), ActionScopeType.Topic.Name); + Assert.Equal(nameof(ActionScopeType.Global), ActionScopeType.Global.Name); + Assert.Equal(nameof(ActionScopeType.System), ActionScopeType.System.Name); + } + + [Theory] + [InlineData(nameof(ActionScopeType.Env))] + [InlineData(nameof(ActionScopeType.Topic))] + [InlineData(nameof(ActionScopeType.Global))] + [InlineData(nameof(ActionScopeType.System))] + public void ParseValidScopeNames(string scopeName) + { + // Arrange & Act + ActionScopeType result = ActionScopeType.Parse(scopeName); + + // Assert + Assert.Equal(scopeName, result.Name); + } + + [Fact] + public void ParseNullInput() + { + // Arrange & Act & Assert + InvalidScopeException exception = Assert.Throws(() => ActionScopeType.Parse(null)); + Assert.Equal("Undefined action scope type.", exception.Message); + } + + [Fact] + public void ParseInvalidScopeName() + { + // Arrange + string invalidScope = "InvalidScope"; + + // Act & Assert + InvalidScopeException exception = Assert.Throws(() => ActionScopeType.Parse(invalidScope)); + Assert.Equal($"Unknown action scope type: {invalidScope}.", exception.Message); + } + + [Fact] + public void ToStringReturnsName() + { + // Arrange + ActionScopeType scope = ActionScopeType.Env; + + // Act + string result = scope.ToString(); + + // Assert + Assert.Equal(nameof(ActionScopeType.Env), result); + } + + [Fact] + public void VerifyGetHashCode() + { + // Arrange + ActionScopeType scope = ActionScopeType.Topic; + int expectedHashCode = "Topic".GetHashCode(); + + // Act + int result = scope.GetHashCode(); + + // Assert + Assert.Equal(expectedHashCode, result); + } + + [Fact] + public void VerifyEqualsWithSameInstance() + { + // Arrange + ActionScopeType scope = ActionScopeType.Global; + + // Act + bool result = scope.Equals(scope); + + // Assert + Assert.True(result); + } + + [Fact] + public void VerifyEqualsWithName() + { + // Arrange + ActionScopeType scope = ActionScopeType.System; + + // Act + bool result = scope.Equals(nameof(ActionScopeType.System)); + + // Assert + Assert.True(result); + } + + [Fact] + public void VerifyInequalityWithName() + { + // Arrange + ActionScopeType scope = ActionScopeType.Env; + + // Act + bool result = scope.Equals("DifferentName"); + + // Assert + Assert.False(result); + } + + [Fact] + public void VerifyInequalityWithNull() + { + // Arrange + ActionScopeType scope = ActionScopeType.Topic; + + // Act + bool result = scope.Equals(null); + + // Assert + Assert.False(result); + } + + [Fact] + public void VerifyInequalityWithOtherType() + { + // Arrange + ActionScopeType scope = ActionScopeType.Global; + int differentTypeObject = 42; + + // Act + bool result = scope.Equals(differentTypeObject); + + // Assert + Assert.False(result); + } +} diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/MockKernelProcessMessageChannel.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/MockKernelProcessMessageChannel.cs new file mode 100644 index 000000000000..65c58c2f242e --- /dev/null +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/MockKernelProcessMessageChannel.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Moq; + +namespace Microsoft.SemanticKernel.Process.UnitTests.Workflows.Actions; + +internal sealed class MockKernelProcessMessageChannel : Mock +{ + public MockKernelProcessMessageChannel() + : base(MockBehavior.Strict) + { + this.Setup( + channel => + channel + .EmitEventAsync(It.IsAny())) + .Returns(ValueTask.CompletedTask); + } +} diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ProcessActionTest.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ProcessActionTest.cs new file mode 100644 index 000000000000..b5e418635499 --- /dev/null +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ProcessActionTest.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; +using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx.Types; +using Microsoft.SemanticKernel.Process.Workflows; +using Microsoft.SemanticKernel.Process.Workflows.PowerFx; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.SemanticKernel.Process.UnitTests.Workflows.Actions; + +/// +/// Base test class for implementations. +/// +public abstract class ProcessActionTest(ITestOutputHelper output) : WorkflowTest(output) +{ + internal ProcessActionScopes Scopes { get; } = new(); + + protected ActionId CreateActionId() => new($"{this.GetType().Name}_{Guid.NewGuid():N}"); + + protected string FormatDisplayName(string name) => $"{this.GetType().Name}_{name}"; + + internal Task ExecuteAction(ProcessAction action, Kernel? kernel = null) => + action.ExecuteAsync( + new ProcessActionContext( + RecalcEngineFactory.Create(this.Scopes, 5000), + this.Scopes, + kernel ?? new Kernel()), + cancellationToken: default); + + internal void VerifyModel(DialogAction model, ProcessAction action) + { + Assert.Equal(model.Id, action.Id); + Assert.Equal(model, action.Model); + } + + protected void VerifyState(string variableName, FormulaValue expectedValue) => this.VerifyState(variableName, ActionScopeType.Topic, expectedValue); + + internal void VerifyState(string variableName, ActionScopeType scope, FormulaValue expectedValue) + { + FormulaValue actualValue = this.Scopes.Get(variableName, scope); + Assert.Equivalent(expectedValue, actualValue); + } +} diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SendActivityActionTest.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SendActivityActionTest.cs new file mode 100644 index 000000000000..00af5fc31418 --- /dev/null +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SendActivityActionTest.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx; +using Microsoft.SemanticKernel.Process.Workflows.Actions; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.SemanticKernel.Process.UnitTests.Workflows.Actions; + +/// +/// Tests for . +/// +public sealed class SendActivityActionTest(ITestOutputHelper output) : ProcessActionTest(output) +{ + [Fact] + public async Task CaptureActivity() + { + // Arrange + ActivitySink activitySink = new(); + SendActivity model = + this.CreateModel( + this.FormatDisplayName(nameof(CaptureActivity)), + "Test activity message"); + + // Act + SendActivityAction action = new(model, activitySink.Handler); + await this.ExecuteAction(action); + + // Assert + this.VerifyModel(model, action); + Assert.Single(activitySink.Activities); + } + + private SendActivity CreateModel(string displayName, string activityMessage, string? summary = null) + { + MessageActivityTemplate.Builder activityBuilder = + new() + { + Summary = summary, + Text = { TemplateLine.Parse(activityMessage) }, + }; + SendActivity.Builder actionBuilder = + new() + { + Id = this.CreateActionId(), + DisplayName = this.FormatDisplayName(displayName), + Activity = activityBuilder.Build(), + }; + + SendActivity model = actionBuilder.Build(); + + return model; + } + + private sealed class ActivitySink + { + public List Activities { get; } = []; + + public Task Handler(ActivityTemplateBase activity, RecalcEngine engine) + { + this.Activities.Add(activity); + + return Task.CompletedTask; + } + } +} diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SetTextVariableActionTest.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SetTextVariableActionTest.cs new file mode 100644 index 000000000000..57011664c142 --- /dev/null +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SetTextVariableActionTest.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx.Types; +using Microsoft.SemanticKernel.Process.Workflows.Actions; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.SemanticKernel.Process.UnitTests.Workflows.Actions; + +/// +/// Tests for . +/// +public sealed class SetTextVariableActionTest(ITestOutputHelper output) : ProcessActionTest(output) +{ + [Fact] + public async Task CaptureActivity() + { + // Arrange + SetTextVariable model = + this.CreateModel( + this.FormatDisplayName(nameof(CaptureActivity)), + FormatVariablePath("TextVar"), + "Text variable value"); + + // Act + SetTextVariableAction action = new(model); + await this.ExecuteAction(action); + + // Assert + this.VerifyModel(model, action); + this.VerifyState("TextVar", FormulaValue.New("Text variable value")); + } + + private SetTextVariable CreateModel(string displayName, string variablePath, string textValue) + { + SetTextVariable.Builder actionBuilder = + new() + { + Id = this.CreateActionId(), + DisplayName = this.FormatDisplayName(displayName), + Variable = InitializablePropertyPath.Create(variablePath), + Value = TemplateLine.Parse(textValue), + }; + + SetTextVariable model = actionBuilder.Build(); + + return model; + } +} diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SetVariableActionTest.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SetVariableActionTest.cs new file mode 100644 index 000000000000..43afc100b0d3 --- /dev/null +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SetVariableActionTest.cs @@ -0,0 +1,211 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx.Types; +using Microsoft.SemanticKernel.Process.Workflows; +using Microsoft.SemanticKernel.Process.Workflows.Actions; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.SemanticKernel.Process.UnitTests.Workflows.Actions; + +/// +/// Tests for . +/// +public sealed class SetVariableActionTest(ITestOutputHelper output) : ProcessActionTest(output) +{ + [Fact] + public void InvalidModel() + { + // Arrange, Act, Assert + Assert.Throws(() => new SetVariableAction(new SetVariable())); + } + + [Fact] + public async Task SetNumericValue() + { + // Arrange, Act, Assert + await this.ExecuteTest( + displayName: nameof(SetNumericValue), + variableName: "TestVariable", + variableValue: new NumberDataValue(42), + expectedValue: FormulaValue.New(42)); + } + + [Fact] + public async Task SetStringValue() + { + // Arrange, Act, Assert + await this.ExecuteTest( + displayName: nameof(SetStringValue), + variableName: "TestVariable", + variableValue: new StringDataValue("Text"), + expectedValue: FormulaValue.New("Text")); + } + + [Fact] + public async Task SetBooleanValue() + { + // Arrange, Act, Assert + await this.ExecuteTest( + displayName: nameof(SetBooleanValue), + variableName: "TestVariable", + variableValue: new BooleanDataValue(true), + expectedValue: FormulaValue.New(true)); + } + + [Fact] + public async Task SetBooleanExpression() + { + // Arrange + ValueExpression.Builder expressionBuilder = new(ValueExpression.Expression("true || false")); + + // Act, Assert + await this.ExecuteTest( + displayName: nameof(SetBooleanExpression), + variableName: "TestVariable", + valueExpression: expressionBuilder, + expectedValue: FormulaValue.New(true)); + } + + [Fact] + public async Task SetNumberExpression() + { + // Arrange + ValueExpression.Builder expressionBuilder = new(ValueExpression.Expression("9 - 3")); + + // Act, Assert + await this.ExecuteTest( + displayName: nameof(SetBooleanExpression), + variableName: "TestVariable", + valueExpression: expressionBuilder, + expectedValue: FormulaValue.New(6)); + } + + [Fact] + public async Task SetStringExpression() + { + // Arrange + ValueExpression.Builder expressionBuilder = new(ValueExpression.Expression(@"Concatenate(""A"", ""B"", ""C"")")); + + // Act, Assert + await this.ExecuteTest( + displayName: nameof(SetBooleanExpression), + variableName: "TestVariable", + valueExpression: expressionBuilder, + expectedValue: FormulaValue.New("ABC")); + } + + [Fact] + public async Task SetBooleanVariable() + { + // Arrange + this.Scopes.Set("Source", FormulaValue.New(true)); + ValueExpression.Builder expressionBuilder = new(ValueExpression.Variable(PropertyPath.TopicVariable("Source"))); + + // Act, Assert + await this.ExecuteTest( + displayName: nameof(SetBooleanExpression), + variableName: "TestVariable", + valueExpression: expressionBuilder, + expectedValue: FormulaValue.New(true)); + } + + [Fact] + public async Task SetNumberVariable() + { + // Arrange + this.Scopes.Set("Source", FormulaValue.New(321)); + ValueExpression.Builder expressionBuilder = new(ValueExpression.Variable(PropertyPath.TopicVariable("Source"))); + + // Act, Assert + await this.ExecuteTest( + displayName: nameof(SetBooleanExpression), + variableName: "TestVariable", + valueExpression: expressionBuilder, + expectedValue: FormulaValue.New(321)); + } + + [Fact] + public async Task SetStringVariable() + { + // Arrange + this.Scopes.Set("Source", FormulaValue.New("Test")); + ValueExpression.Builder expressionBuilder = new(ValueExpression.Variable(PropertyPath.TopicVariable("Source"))); + + // Act, Assert + await this.ExecuteTest( + displayName: nameof(SetBooleanExpression), + variableName: "TestVariable", + valueExpression: expressionBuilder, + expectedValue: FormulaValue.New("Test")); + } + + [Fact] + public async Task UpdateExistingValue() + { + // Arrange + this.Scopes.Set("VarA", FormulaValue.New(33)); + + // Act, Assert + await this.ExecuteTest( + displayName: nameof(UpdateExistingValue), + variableName: "VarA", + variableValue: new NumberDataValue(42), + expectedValue: FormulaValue.New(42)); + } + + private Task ExecuteTest( + string displayName, + string variableName, + DataValue variableValue, + FormulaValue expectedValue) + { + // Arrange + ValueExpression.Builder expressionBuilder = new(ValueExpression.Literal(variableValue)); + + // Act & Assert + return this.ExecuteTest(displayName, variableName, expressionBuilder, expectedValue); + } + + private async Task ExecuteTest( + string displayName, + string variableName, + ValueExpression.Builder valueExpression, + FormulaValue expectedValue) + { + // Arrange + SetVariable model = + this.CreateModel( + displayName, + FormatVariablePath(variableName), + valueExpression); + + this.Scopes.Set(variableName, FormulaValue.New(33)); + + // Act + SetVariableAction action = new(model); + await this.ExecuteAction(action); + + // Assert + this.VerifyModel(model, action); + this.VerifyState(variableName, expectedValue); + } + + private SetVariable CreateModel(string displayName, string variablePath, ValueExpression.Builder valueExpression) + { + SetVariable.Builder actionBuilder = + new() + { + Id = this.CreateActionId(), + DisplayName = this.FormatDisplayName(displayName), + Variable = InitializablePropertyPath.Create(variablePath), + Value = valueExpression, + }; + + SetVariable model = actionBuilder.Build(); + + return model; + } +} diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/AnswerQuestionWithAIActionTest.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/AnswerQuestionWithAIActionTest.cs deleted file mode 100644 index e40d0ff3b264..000000000000 --- a/dotnet/src/Experimental/Process.UnitTests/Workflow/AnswerQuestionWithAIActionTest.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Threading.Tasks; -using Microsoft.Bot.ObjectModel; -using Xunit; - -namespace Microsoft.SemanticKernel.Process.UnitTests.Workflows; - -/// -/// Base for directly testing AnswerQuestionWithAI. -/// -public sealed class AnswerQuestionWithAIActionTest -{ - [Fact] - public Task EmptyTestAsync() - { - AnswerQuestionWithAI action = new(); - // %%% TODO: Something - return Task.CompletedTask; - } -} diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/RecalcEngineTests.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/PowerFx/RecalcEngineEvaluationTests.cs similarity index 82% rename from dotnet/src/Experimental/Process.UnitTests/Workflow/RecalcEngineTests.cs rename to dotnet/src/Experimental/Process.UnitTests/Workflow/PowerFx/RecalcEngineEvaluationTests.cs index b243a435ff8b..159cb61b1c0a 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Workflow/RecalcEngineTests.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/PowerFx/RecalcEngineEvaluationTests.cs @@ -3,19 +3,19 @@ using System; using Microsoft.PowerFx; using Microsoft.PowerFx.Types; -using Microsoft.SemanticKernel.Process.Workflows.PowerFx; using Xunit; +using Xunit.Abstractions; -namespace Microsoft.SemanticKernel.Process.UnitTests.Workflows; +namespace Microsoft.SemanticKernel.Process.UnitTests.Workflows.PowerFx; #pragma warning disable CA1308 // Ignore "Normalize strings to uppercase" warning for test cases -public sealed class RecalcEngineTests +public sealed class RecalcEngineEvaluationTests(ITestOutputHelper output) : RecalcEngineTest(output) { [Fact] public void EvaluateConstant() { - RecalcEngine engine = RecalcEngineFactory.Create([], 100); + RecalcEngine engine = this.CreateEngine(); this.EvaluateExpression(engine, 0m, "0"); this.EvaluateExpression(engine, -1m, "-1"); @@ -28,8 +28,8 @@ public void EvaluateConstant() [Fact] public void EvaluateInvalid() { - RecalcEngine engine = RecalcEngineFactory.Create([], 100); - engine.UpdateVariable("Scoped.Value", DecimalValue.New(33)); + RecalcEngine engine = this.CreateEngine(); + engine.UpdateVariable("Scoped.Value", FormulaValue.New(33)); this.EvaluateFailure(engine, "Hi"); this.EvaluateFailure(engine, "True"); @@ -48,14 +48,14 @@ public void EvaluateFormula() { NamedValue[] recordValues = [ - new NamedValue("Label", StringValue.New("Test")), - new NamedValue("Value", DecimalValue.New(54)), + new NamedValue("Label", FormulaValue.New("Test")), + new NamedValue("Value", FormulaValue.New(54)), ]; FormulaValue complexValue = FormulaValue.NewRecordFromFields(recordValues); - RecalcEngine engine = RecalcEngineFactory.Create([], 100); - engine.UpdateVariable("CustomLabel", StringValue.New("Note")); - engine.UpdateVariable("CustomValue", DecimalValue.New(42)); + RecalcEngine engine = this.CreateEngine(); + engine.UpdateVariable("CustomLabel", FormulaValue.New("Note")); + engine.UpdateVariable("CustomValue", FormulaValue.New(42)); engine.UpdateVariable("Scoped", complexValue); this.EvaluateExpression(engine, 2m, "1 + 1"); diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/RecalcEngineFactoryTests.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/PowerFx/RecalcEngineFactoryTests.cs similarity index 75% rename from dotnet/src/Experimental/Process.UnitTests/Workflow/RecalcEngineFactoryTests.cs rename to dotnet/src/Experimental/Process.UnitTests/Workflow/PowerFx/RecalcEngineFactoryTests.cs index 85388f4a1d3f..614d04c74112 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Workflow/RecalcEngineFactoryTests.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/PowerFx/RecalcEngineFactoryTests.cs @@ -2,18 +2,18 @@ using System.Collections.Generic; using Microsoft.PowerFx; -using Microsoft.SemanticKernel.Process.Workflows.PowerFx; using Xunit; +using Xunit.Abstractions; -namespace Microsoft.SemanticKernel.Process.UnitTests.Workflows; +namespace Microsoft.SemanticKernel.Process.UnitTests.Workflows.PowerFx; -public class RecalcEngineFactoryTests +public class RecalcEngineFactoryTests(ITestOutputHelper output) : RecalcEngineTest(output) { [Fact] public void DefaultNotNull() { // Act - RecalcEngine engine = RecalcEngineFactory.Create([], 100); + RecalcEngine engine = this.CreateEngine(); // Assert Assert.NotNull(engine); @@ -23,8 +23,8 @@ public void DefaultNotNull() public void NewInstanceEachTime() { // Act - RecalcEngine engine1 = RecalcEngineFactory.Create([], 100); - RecalcEngine engine2 = RecalcEngineFactory.Create([], 100); + RecalcEngine engine1 = this.CreateEngine(); + RecalcEngine engine2 = this.CreateEngine(); // Assert Assert.NotNull(engine1); @@ -36,7 +36,7 @@ public void NewInstanceEachTime() public void HasSetFunctionEnabled() { // Arrange - RecalcEngine engine = RecalcEngineFactory.Create([], 100); + RecalcEngine engine = this.CreateEngine(); // Act CheckResult result = engine.Check("1+1"); @@ -49,7 +49,7 @@ public void HasSetFunctionEnabled() public void HasCorrectMaximumExpressionLength() { // Arrange - RecalcEngine engine = RecalcEngineFactory.Create([], 2000); + RecalcEngine engine = this.CreateEngine(2000); // Act: Create a long expression that is within the limit string goodExpression = string.Concat(GenerateExpression(999)); diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/PowerFx/RecalcEngineTest.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/PowerFx/RecalcEngineTest.cs new file mode 100644 index 000000000000..366b2775accd --- /dev/null +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/PowerFx/RecalcEngineTest.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.PowerFx; +using Microsoft.SemanticKernel.Process.Workflows; +using Microsoft.SemanticKernel.Process.Workflows.PowerFx; +using Xunit.Abstractions; + +namespace Microsoft.SemanticKernel.Process.UnitTests.Workflows.PowerFx; + +/// +/// Base test class for PowerFx engine tests. +/// +public abstract class RecalcEngineTest(ITestOutputHelper output) : WorkflowTest(output) +{ + internal ProcessActionScopes Scopes { get; } = new(); + + protected RecalcEngine CreateEngine(int maximumExpressionLength = 100) => RecalcEngineFactory.Create(this.Scopes, maximumExpressionLength); +} diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/PowerFx/TemplateExtensionsTests.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/PowerFx/TemplateExtensionsTests.cs new file mode 100644 index 000000000000..0e9a2a3814ad --- /dev/null +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/PowerFx/TemplateExtensionsTests.cs @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx; +using Microsoft.PowerFx.Types; +using Microsoft.SemanticKernel.Process.Workflows; +using Microsoft.SemanticKernel.Process.Workflows.PowerFx; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.SemanticKernel.Process.UnitTests.Workflows.PowerFx; + +public class TemplateExtensionsTests(ITestOutputHelper output) : RecalcEngineTest(output) +{ + [Fact] + public void FormatTemplateLines() + { + // Arrange + List template = + [ + TemplateLine.Parse("Hello"), + TemplateLine.Parse(" "), + TemplateLine.Parse("World"), + ]; + RecalcEngine engine = this.CreateEngine(); + + // Act + string? result = engine.Format(template); + + // Assert + Assert.Equal("Hello World", result); + } + + [Fact] + public void FormatTemplateLinesEmpty() + { + // Arrange + List template = []; + RecalcEngine engine = this.CreateEngine(); + + // Act + string? result = engine.Format(template); + + // Assert + Assert.Equal(string.Empty, result); + } + + [Fact] + public void FormatTemplateLine() + { + // Arrange + TemplateLine line = TemplateLine.Parse("Test"); + RecalcEngine engine = this.CreateEngine(); + + // Act + string? result = engine.Format(line); + + // Assert + Assert.Equal("Test", result); + } + + [Fact] + public void FormatTemplateLineNull() + { + // Arrange + TemplateLine? line = null; + RecalcEngine engine = this.CreateEngine(); + + // Act + string? result = engine.Format(line); + + // Assert + Assert.Equal(string.Empty, result); + } + + [Fact] + public void FormatTextSegment() + { + // Arrange + TemplateSegment textSegment = TextSegment.FromText("Hello World"); + TemplateLine line = new([textSegment]); + RecalcEngine engine = this.CreateEngine(); + + // Act + string? result = engine.Format(line); + + // Assert + Assert.Equal("Hello World", result); + } + + [Fact] + public void FormatExpressionSegment() + { + // Arrange + ExpressionSegment expressionSegment = new(ValueExpression.Expression("1 + 1")); + TemplateLine line = new([expressionSegment]); + RecalcEngine engine = this.CreateEngine(); + + // Act + string? result = engine.Format(line); + + // Assert + Assert.Equal("2", result); + } + + [Fact] + public void FormatVariableSegment() + { + // Arrange + this.Scopes.Set("Source", FormulaValue.New("Hello World")); + ExpressionSegment expressionSegment = new(ValueExpression.Variable(PropertyPath.TopicVariable("Source"))); + TemplateLine line = new([expressionSegment]); + RecalcEngine engine = this.CreateEngine(); + + // Act + string? result = engine.Format(line); + + // Assert + Assert.Equal("Hello World", result); + } + + //[Fact] + //public void Format_WithExpressionSegmentWithVariableReference_ReturnsEvaluatedValue() + //{ + // // Arrange + // Mock mockVariableRef = new(); + // mockVariableRef.Setup(vr => vr.ToString()).Returns("myVariable"); + + // Expression expression = new() { VariableReference = mockVariableRef.Object }; + // ExpressionSegment expressionSegment = new() { Expression = expression }; + // TemplateLine line = new([expressionSegment]); + + // _mockFormulaValue.Setup(fv => fv.Format()).Returns("VariableValue"); + // _mockEngine.Setup(e => e.Eval("myVariable")).Returns(_mockFormulaValue.Object); + + // // Act + // string? result = engine.Format(line); + + // // Assert + // Assert.Equal("VariableValue", result); + // _mockEngine.Verify(e => e.Eval("myVariable"), Times.Once); + // _mockFormulaValue.Verify(fv => fv.Format(), Times.Once); + //} + + [Fact] + public void FormatExpressionSegmentUndefined() + { + // Arrange + ExpressionSegment expressionSegment = new(); + TemplateLine line = new([expressionSegment]); + RecalcEngine engine = this.CreateEngine(); + + // Act & Assert + Assert.Throws(() => engine.Format(line)); + } + + [Fact] + public void FormatMultipleSegments() + { + // Arrange + TemplateSegment textSegment = TextSegment.FromText("Hello "); + ExpressionSegment expressionSegment = new(ValueExpression.Expression(@"""World""")); + TemplateLine line = new([textSegment, expressionSegment]); + RecalcEngine engine = this.CreateEngine(); + + // Act + string? result = engine.Format(line); + + // Assert + Assert.Equal("Hello World", result); + } +} diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/ProcessActionScopesTests.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/ProcessActionScopesTests.cs new file mode 100644 index 000000000000..114cf4ca29b0 --- /dev/null +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/ProcessActionScopesTests.cs @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Linq; +using Microsoft.PowerFx.Types; +using Microsoft.SemanticKernel.Process.Workflows; +using Xunit; + +namespace Microsoft.SemanticKernel.Process.UnitTests.Workflows; + +public class ProcessActionScopesTests +{ + [Fact] + public void ConstructorInitializesAllScopes() + { + // Arrange & Act + ProcessActionScopes scopes = new(); + + // Assert + RecordValue envRecord = scopes.BuildRecord(ActionScopeType.Env); + RecordValue topicRecord = scopes.BuildRecord(ActionScopeType.Topic); + RecordValue globalRecord = scopes.BuildRecord(ActionScopeType.Global); + RecordValue systemRecord = scopes.BuildRecord(ActionScopeType.System); + + Assert.NotNull(envRecord); + Assert.NotNull(topicRecord); + Assert.NotNull(globalRecord); + Assert.NotNull(systemRecord); + } + + [Fact] + public void BuildRecordWhenEmpty() + { + // Arrange + ProcessActionScopes scopes = new(); + + // Act + RecordValue record = scopes.BuildRecord(ActionScopeType.Topic); + + // Assert + Assert.NotNull(record); + Assert.Empty(record.Fields); + } + + [Fact] + public void BuildRecordContainsSetValues() + { + // Arrange + ProcessActionScopes scopes = new(); + FormulaValue testValue = FormulaValue.New("test"); + scopes.Set("key1", ActionScopeType.Topic, testValue); + + // Act + RecordValue record = scopes.BuildRecord(ActionScopeType.Topic); + + // Assert + Assert.NotNull(record); + Assert.Single(record.Fields); + Assert.Equal("key1", record.Fields.First().Name); + Assert.Equal(testValue, record.Fields.First().Value); + } + + [Fact] + public void BuildRecordForAllScopeTypes() + { + // Arrange + ProcessActionScopes scopes = new(); + FormulaValue testValue = FormulaValue.New("test"); + + // Act & Assert + scopes.Set("envKey", ActionScopeType.Env, testValue); + RecordValue envRecord = scopes.BuildRecord(ActionScopeType.Env); + Assert.Single(envRecord.Fields); + + scopes.Set("topicKey", ActionScopeType.Topic, testValue); + RecordValue topicRecord = scopes.BuildRecord(ActionScopeType.Topic); + Assert.Single(topicRecord.Fields); + + scopes.Set("globalKey", ActionScopeType.Global, testValue); + RecordValue globalRecord = scopes.BuildRecord(ActionScopeType.Global); + Assert.Single(globalRecord.Fields); + + scopes.Set("systemKey", ActionScopeType.System, testValue); + RecordValue systemRecord = scopes.BuildRecord(ActionScopeType.System); + Assert.Single(systemRecord.Fields); + } + + [Fact] + public void GetWithImplicitScope() + { + // Arrange + ProcessActionScopes scopes = new(); + FormulaValue testValue = FormulaValue.New("test"); + scopes.Set("key1", ActionScopeType.Topic, testValue); + + // Act + FormulaValue result = scopes.Get("key1"); + + // Assert + Assert.Equal(testValue, result); + } + + [Fact] + public void GetWithSpecifiedScope() + { + // Arrange + ProcessActionScopes scopes = new(); + FormulaValue testValue = FormulaValue.New("test"); + scopes.Set("key1", ActionScopeType.Global, testValue); + + // Act + FormulaValue result = scopes.Get("key1", ActionScopeType.Global); + + // Assert + Assert.Equal(testValue, result); + } + + [Fact] + public void SetDefaultScope() + { + // Arrange + ProcessActionScopes scopes = new(); + FormulaValue testValue = FormulaValue.New("test"); + + // Act + scopes.Set("key1", testValue); + + // Assert + FormulaValue result = scopes.Get("key1", ActionScopeType.Topic); + Assert.Equal(testValue, result); + } + + [Fact] + public void SetSpecifiedScope() + { + // Arrange + ProcessActionScopes scopes = new(); + FormulaValue testValue = FormulaValue.New("test"); + + // Act + scopes.Set("key1", ActionScopeType.System, testValue); + + // Assert + FormulaValue result = scopes.Get("key1", ActionScopeType.System); + Assert.Equal(testValue, result); + } + + [Fact] + public void SetOverwritesExistingValue() + { + // Arrange + ProcessActionScopes scopes = new(); + FormulaValue initialValue = FormulaValue.New("initial"); + FormulaValue newValue = FormulaValue.New("new"); + + // Act + scopes.Set("key1", ActionScopeType.Topic, initialValue); + scopes.Set("key1", ActionScopeType.Topic, newValue); + + // Assert + FormulaValue result = scopes.Get("key1", ActionScopeType.Topic); + Assert.Equal(newValue, result); + } +} diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/ProcessActionTest.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/ProcessActionTest.cs deleted file mode 100644 index e2aecf2564e1..000000000000 --- a/dotnet/src/Experimental/Process.UnitTests/Workflow/ProcessActionTest.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -namespace Microsoft.SemanticKernel.Process.UnitTests.Workflows; - -/// -/// Base for directly testing a process-action. -/// -internal sealed class ProcessActionTest -{ -} diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/WorkflowTest.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/WorkflowTest.cs new file mode 100644 index 000000000000..ff955404c402 --- /dev/null +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/WorkflowTest.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.SemanticKernel.Process.Workflows; +using Xunit.Abstractions; + +namespace Microsoft.SemanticKernel.Process.UnitTests.Workflows; + +/// +/// Base class for workflow tests. +/// +public abstract class WorkflowTest : IDisposable +{ + public TestOutputAdapter Output { get; } + + protected WorkflowTest(ITestOutputHelper output) + { + this.Output = new TestOutputAdapter(output); + System.Console.SetOut(this.Output); + } + + public void Dispose() + { + this.Output.Dispose(); + GC.SuppressFinalize(this); + } + + internal static string FormatVariablePath(string variableName, ActionScopeType? scope = null) => $"{scope ?? ActionScopeType.Topic}.{variableName}"; +} From a04e7cf669343b3439c7b4817872d117a63d5713 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 16 Jul 2025 23:05:44 -0700 Subject: [PATCH 06/40] More tests and organization --- .../Actions/AnswerQuestionWithAIAction.cs | 2 +- .../ObjectModel/Actions/AssignmentAction.cs | 1 + .../ObjectModel/Actions/ParseValueAction.cs | 1 + .../Actions/SetTextVariableAction.cs | 2 +- .../ObjectModel/Actions/SetVariableAction.cs | 1 + .../DataValueExtensions.cs | 2 +- .../FormulaValueExtensions.cs | 2 +- .../PropertyPathExtensions.cs | 2 +- .../RecordDataTypeExtensions.cs | 2 +- .../Extensions/StringExtensions.cs | 21 +++ .../TemplateExtensions.cs | 2 +- .../PowerFx/RecalcEngineExtensions.cs | 4 +- .../ObjectModel/PowerFx/StringExtensions.cs | 21 --- .../ObjectModel/ProcessActionEnvironment.cs | 2 +- .../Extensions/StringExtensionsTests.cs | 163 ++++++++++++++++++ .../PowerFx/TemplateExtensionsTests.cs | 2 +- 16 files changed, 197 insertions(+), 33 deletions(-) rename dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/{PowerFx => Extensions}/DataValueExtensions.cs (94%) rename dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/{PowerFx => Extensions}/FormulaValueExtensions.cs (97%) rename dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/{PowerFx => Extensions}/PropertyPathExtensions.cs (79%) rename dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/{PowerFx => Extensions}/RecordDataTypeExtensions.cs (96%) create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/StringExtensions.cs rename dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/{PowerFx => Extensions}/TemplateExtensions.cs (96%) delete mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/StringExtensions.cs create mode 100644 dotnet/src/Experimental/Process.UnitTests/Workflow/Extensions/StringExtensionsTests.cs diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AnswerQuestionWithAIAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AnswerQuestionWithAIAction.cs index 08ac5092d1fd..1c44d340deda 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AnswerQuestionWithAIAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AnswerQuestionWithAIAction.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.PowerFx.Types; using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Process.Workflows.PowerFx; +using Microsoft.SemanticKernel.Process.Workflows.Extensions; namespace Microsoft.SemanticKernel.Process.Workflows.Actions; diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AssignmentAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AssignmentAction.cs index 9224bd81d111..3cf55e70ac87 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AssignmentAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AssignmentAction.cs @@ -3,6 +3,7 @@ using System; using Microsoft.Bot.ObjectModel; using Microsoft.PowerFx.Types; +using Microsoft.SemanticKernel.Process.Workflows.Extensions; using Microsoft.SemanticKernel.Process.Workflows.PowerFx; namespace Microsoft.SemanticKernel.Process.Workflows.Actions; diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ParseValueAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ParseValueAction.cs index 18f572068409..26dc850a1752 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ParseValueAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ParseValueAction.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; using Microsoft.PowerFx.Types; +using Microsoft.SemanticKernel.Process.Workflows.Extensions; using Microsoft.SemanticKernel.Process.Workflows.PowerFx; namespace Microsoft.SemanticKernel.Process.Workflows.Actions; diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetTextVariableAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetTextVariableAction.cs index 7d7128735b7c..f0fffdf56fed 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetTextVariableAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetTextVariableAction.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; using Microsoft.PowerFx.Types; -using Microsoft.SemanticKernel.Process.Workflows.PowerFx; +using Microsoft.SemanticKernel.Process.Workflows.Extensions; namespace Microsoft.SemanticKernel.Process.Workflows.Actions; diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetVariableAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetVariableAction.cs index b459b6042a79..193662ed3c2e 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetVariableAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetVariableAction.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; using Microsoft.PowerFx.Types; +using Microsoft.SemanticKernel.Process.Workflows.Extensions; using Microsoft.SemanticKernel.Process.Workflows.PowerFx; namespace Microsoft.SemanticKernel.Process.Workflows.Actions; diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/DataValueExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/DataValueExtensions.cs similarity index 94% rename from dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/DataValueExtensions.cs rename to dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/DataValueExtensions.cs index 07c70199ef34..19e81858f515 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/DataValueExtensions.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/DataValueExtensions.cs @@ -3,7 +3,7 @@ using Microsoft.Bot.ObjectModel; using Microsoft.PowerFx.Types; -namespace Microsoft.SemanticKernel.Process.Workflows.PowerFx; +namespace Microsoft.SemanticKernel.Process.Workflows.Extensions; internal static class DataValueExtensions { diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/FormulaValueExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/FormulaValueExtensions.cs similarity index 97% rename from dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/FormulaValueExtensions.cs rename to dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/FormulaValueExtensions.cs index 5ca18d0cef93..80efaf44d89a 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/FormulaValueExtensions.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/FormulaValueExtensions.cs @@ -6,7 +6,7 @@ using System.Linq; using Microsoft.PowerFx.Types; -namespace Microsoft.SemanticKernel.Process.Workflows.PowerFx; +namespace Microsoft.SemanticKernel.Process.Workflows.Extensions; internal delegate object? GetFormulaValue(FormulaValue value); diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/PropertyPathExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/PropertyPathExtensions.cs similarity index 79% rename from dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/PropertyPathExtensions.cs rename to dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/PropertyPathExtensions.cs index 14b3bb00ff5f..53dca4ce5aec 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/PropertyPathExtensions.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/PropertyPathExtensions.cs @@ -2,7 +2,7 @@ using Microsoft.Bot.ObjectModel; -namespace Microsoft.SemanticKernel.Process.Workflows.PowerFx; +namespace Microsoft.SemanticKernel.Process.Workflows.Extensions; internal static class PropertyPathExtensions { diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecordDataTypeExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/RecordDataTypeExtensions.cs similarity index 96% rename from dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecordDataTypeExtensions.cs rename to dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/RecordDataTypeExtensions.cs index a319de3e759d..1ccbc6405b30 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecordDataTypeExtensions.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/RecordDataTypeExtensions.cs @@ -5,7 +5,7 @@ using Microsoft.Bot.ObjectModel; using Microsoft.PowerFx.Types; -namespace Microsoft.SemanticKernel.Process.Workflows.PowerFx; +namespace Microsoft.SemanticKernel.Process.Workflows.Extensions; internal static class RecordDataTypeExtensions { diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/StringExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/StringExtensions.cs new file mode 100644 index 000000000000..e92a1120211c --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/StringExtensions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.RegularExpressions; + +namespace Microsoft.SemanticKernel.Process.Workflows.Extensions; + +internal static class StringExtensions +{ + private static readonly Regex s_regex = new(@"^```(?:\w*)\s*([\s\S]*?)\s*```$", RegexOptions.Compiled | RegexOptions.Multiline); + + public static string TrimJsonDelimeter(this string value) + { + Match match = s_regex.Match(value.Trim()); + if (match.Success) + { + return match.Groups[1].Value.Trim(); + } + + return value.Trim(); + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/TemplateExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/TemplateExtensions.cs similarity index 96% rename from dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/TemplateExtensions.cs rename to dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/TemplateExtensions.cs index 8a32290cd627..3fd67118f6b1 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/TemplateExtensions.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/TemplateExtensions.cs @@ -6,7 +6,7 @@ using Microsoft.PowerFx; using Microsoft.PowerFx.Types; -namespace Microsoft.SemanticKernel.Process.Workflows.PowerFx; +namespace Microsoft.SemanticKernel.Process.Workflows.Extensions; internal static class TemplateExtensions { diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs index 319c824c3b6e..ed896177f944 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs @@ -1,11 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. -using System; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; using Microsoft.PowerFx; using Microsoft.PowerFx.Types; +using Microsoft.SemanticKernel.Process.Workflows.Extensions; namespace Microsoft.SemanticKernel.Process.Workflows.PowerFx; diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/StringExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/StringExtensions.cs deleted file mode 100644 index 5dda05b0dab8..000000000000 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/StringExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.RegularExpressions; - -namespace Microsoft.SemanticKernel.Process.Workflows.PowerFx; - -internal static class StringExtensions -{ - private static readonly Regex s_regex = new(@"^```json\s*([\s\S]*?)\s*```$", RegexOptions.Compiled | RegexOptions.Multiline); - - public static string TrimJsonDelimeter(this string value) - { - Match match = s_regex.Match(value); - if (match.Success) - { - return match.Groups[0].Value.Trim(); - } - - return value; - } -} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionEnvironment.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionEnvironment.cs index 0f09f22ffbb6..29420e5eb777 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionEnvironment.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionEnvironment.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; using Microsoft.PowerFx; -using Microsoft.SemanticKernel.Process.Workflows.PowerFx; +using Microsoft.SemanticKernel.Process.Workflows.Extensions; namespace Microsoft.SemanticKernel; diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/Extensions/StringExtensionsTests.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/Extensions/StringExtensionsTests.cs new file mode 100644 index 000000000000..5aee37440f40 --- /dev/null +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/Extensions/StringExtensionsTests.cs @@ -0,0 +1,163 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel.Process.Workflows.Extensions; +using Xunit; + +namespace Microsoft.SemanticKernel.Process.UnitTests.Workflows.Extensions; + +public class StringExtensionsTests +{ + [Fact] + public void TrimJsonWithDelimeter() + { + // Arrange + const string input = + """ + ```json + { + "key": "value" + } + ``` + """; + + // Act + string result = input.TrimJsonDelimeter(); + + // Assert + Assert.Equal( + """ + { + "key": "value" + } + """, + result); + } + [Fact] + public void TrimJsonWithPadding() + { + // Arrange + const string input = + """ + + ```json + { + "key": "value" + } + ``` + """; + + // Act + string result = input.TrimJsonDelimeter(); + + // Assert + Assert.Equal( + """ + { + "key": "value" + } + """, + result); + } + + [Fact] + public void TrimJsonWithUnqualifiedDelimeter() + { + // Arrange + const string input = + """ + ``` + { + "key": "value" + } + ``` + """; + + // Act + string result = input.TrimJsonDelimeter(); + + // Assert + Assert.Equal( + """ + { + "key": "value" + } + """, + result); + } + + [Fact] + public void TrimJsonWithoutDelimeter() + { + // Arrange + const string input = + """ + { + "key": "value" + } + """; + + // Act + string result = input.TrimJsonDelimeter(); + + // Assert + Assert.Equal( + """ + { + "key": "value" + } + """, + result); + } + + [Fact] + public void TrimJsonWithoutDelimeterWithPadding() + { + // Arrange + const string input = + """ + + { + "key": "value" + } + """; + + // Act + string result = input.TrimJsonDelimeter(); + + // Assert + Assert.Equal( + """ + { + "key": "value" + } + """, + result); + } + + [Fact] + public void TrimMissingWithDelimeter() + { + // Arrange + const string input = + """ + ```json + ``` + """; + + // Act + string result = input.TrimJsonDelimeter(); + + // Assert + Assert.Equal(string.Empty, result); + } + + [Fact] + public void TrimEmptyString() + { + // Act + string result = string.Empty.TrimJsonDelimeter(); + + // Assert + Assert.Equal(string.Empty, result); + } +} diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/PowerFx/TemplateExtensionsTests.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/PowerFx/TemplateExtensionsTests.cs index 0e9a2a3814ad..04697d49059b 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Workflow/PowerFx/TemplateExtensionsTests.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/PowerFx/TemplateExtensionsTests.cs @@ -5,7 +5,7 @@ using Microsoft.PowerFx; using Microsoft.PowerFx.Types; using Microsoft.SemanticKernel.Process.Workflows; -using Microsoft.SemanticKernel.Process.Workflows.PowerFx; +using Microsoft.SemanticKernel.Process.Workflows.Extensions; using Xunit; using Xunit.Abstractions; From f11611218bbb747dd2a47c9de77bc70e64d7ee13 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 17 Jul 2025 10:36:40 -0700 Subject: [PATCH 07/40] Some more state actions --- .../Process.Core/Process.Core.csproj | 16 ++-- .../ObjectModel/Actions/AssignmentAction.cs | 2 +- .../Actions/ClearAllVariablesAction.cs | 27 +++++++ .../Actions/ResetVariableAction.cs | 30 ++++++++ .../PowerFx/RecalcEngineExtensions.cs | 15 +++- .../ObjectModel/ProcessActionScopes.cs | 4 + .../ObjectModel/ProcessActionVisitor.cs | 73 +++++++++++-------- .../Actions/ClearAllVariablesActionTest.cs | 69 ++++++++++++++++++ .../Workflow/Actions/ProcessActionTest.cs | 8 ++ .../Actions/ResetVariableActionTest.cs | 69 ++++++++++++++++++ .../Actions/SetTextVariableActionTest.cs | 25 ++++++- 11 files changed, 291 insertions(+), 47 deletions(-) create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ClearAllVariablesAction.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ResetVariableAction.cs create mode 100644 dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ClearAllVariablesActionTest.cs create mode 100644 dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ResetVariableActionTest.cs diff --git a/dotnet/src/Experimental/Process.Core/Process.Core.csproj b/dotnet/src/Experimental/Process.Core/Process.Core.csproj index dd9361bd0050..ceec1b1a10c1 100644 --- a/dotnet/src/Experimental/Process.Core/Process.Core.csproj +++ b/dotnet/src/Experimental/Process.Core/Process.Core.csproj @@ -19,12 +19,6 @@ Semantic Kernel Process core. This package is automatically installed by Semantic Kernel Process packages if needed. - - - - - - @@ -32,11 +26,6 @@ - - - - - @@ -46,4 +35,9 @@ + + + + + diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AssignmentAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AssignmentAction.cs index 3cf55e70ac87..1972c5e694a5 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AssignmentAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AssignmentAction.cs @@ -37,7 +37,7 @@ protected void AssignTarget(ProcessActionContext context, FormulaValue result) Console.WriteLine( // %%% DEVTRACE $""" !!! ASSIGN {this.GetType().Name} [{this.Id}] - NAME: {this.Target.VariableScopeName}.{this.Target.VariableName} + NAME: {this.Target.Format()} VALUE:{valuePosition}{result.Format()} ({result.GetType().Name}) """); } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ClearAllVariablesAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ClearAllVariablesAction.cs new file mode 100644 index 000000000000..7e96fb5438b3 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ClearAllVariablesAction.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.ObjectModel; + +namespace Microsoft.SemanticKernel.Process.Workflows.Actions; + +internal sealed class ClearAllVariablesAction : ProcessAction +{ + public ClearAllVariablesAction(ClearAllVariables source) + : base(source) + { + } + + protected override Task HandleAsync(ProcessActionContext context, CancellationToken cancellationToken) + { + DataValue literalValue = this.Model.Variables.GetLiteralValue(); + + if (literalValue is RecordDataValue recordValue) + { + //recordValue.Properties; // %%% TODO ?!?!!?! + } + + return Task.CompletedTask; + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ResetVariableAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ResetVariableAction.cs new file mode 100644 index 000000000000..22f56d681c1a --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ResetVariableAction.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.ObjectModel; +using Microsoft.SemanticKernel.Process.Workflows.Extensions; +using Microsoft.SemanticKernel.Process.Workflows.PowerFx; + +namespace Microsoft.SemanticKernel.Process.Workflows.Actions; + +internal sealed class ResetVariableAction : AssignmentAction +{ + public ResetVariableAction(ResetVariable model) + : base(model, () => model.Variable) + { + } + + protected override Task HandleAsync(ProcessActionContext context, CancellationToken cancellationToken) + { + context.Engine.ClearScopedVariable(context.Scopes, ActionScopeType.Parse(this.Target.VariableScopeName), this.Target.VariableName!); + Console.WriteLine( // %%% DEVTRACE + $""" + !!! CLEAR {this.GetType().Name} [{this.Id}] + NAME: {this.Model.Variable!.Format()} + """); + + return Task.CompletedTask; + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs index ed896177f944..bc7616023b9f 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs @@ -9,6 +9,17 @@ namespace Microsoft.SemanticKernel.Process.Workflows.PowerFx; internal static class RecalcEngineExtensions { + public static void ClearScopedVariable(this RecalcEngine engine, ProcessActionScopes scopes, ActionScopeType scope, string varName) + { + // Validate inputs and assign value. + scopes.Remove(varName, scope); // %%% CONSIDER: SET TO BLANK ??? + + // Rebuild scope record and update engine + RecordValue scopeRecord = scopes.BuildRecord(scope); + engine.DeleteFormula(scope.Name); + engine.UpdateVariable(scope.Name, scopeRecord); + } + public static void SetScopedVariable(this RecalcEngine engine, ProcessActionScopes scopes, ActionScopeType scope, string varName, FormulaValue value) { // Validate inputs and assign value. @@ -20,7 +31,7 @@ public static void SetScopedVariable(this RecalcEngine engine, ProcessActionScop engine.UpdateVariable(scope.Name, scopeRecord); } - public static FormulaValue EvaluateExpression(this RecalcEngine engine, ValueExpression? value) + public static FormulaValue EvaluateExpression(this RecalcEngine engine, ExpressionBase? value) { if (value is null) { @@ -39,7 +50,7 @@ public static FormulaValue EvaluateExpression(this RecalcEngine engine, ValueExp if (value.IsLiteral) { - return value.LiteralValue.ToFormulaValue(); + return value.GetLiteralValue().ToFormulaValue(); } // %%% TODO: value.StructuredRecordExpression ??? diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionScopes.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionScopes.cs index 4ea3bee5b529..c5a11af4bfc9 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionScopes.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionScopes.cs @@ -87,6 +87,10 @@ IEnumerable GetFields() public FormulaValue Get(string name, ActionScopeType? type = null) => this._scopes[type ?? ActionScopeType.Topic][name]; + public void Remove(string name) => this.Remove(name, ActionScopeType.Topic); + + public void Remove(string name, ActionScopeType type) => this._scopes[type].Remove(name); + public void Set(string name, FormulaValue value) => this.Set(name, ActionScopeType.Topic, value); public void Set(string name, ActionScopeType type, FormulaValue value) => this._scopes[type][name] = value; diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs index 9ed4566c4943..bbfb1d621416 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs @@ -121,6 +121,23 @@ protected override void Visit(GotoAction item) this.AddDead(item.Id.Value); } + protected override void Visit(Foreach item) + { + Trace(item); + + this.AddAction(new ForeachAction(item)); + } + + protected override void Visit(BreakLoop item) + { + Trace(item); + } + + protected override void Visit(ContinueLoop item) + { + Trace(item); + } + protected override void Visit(EndConversation item) { Trace(item, isSkipped: false); @@ -170,6 +187,20 @@ protected override void Visit(SetTextVariable item) this.AddAction(new SetTextVariableAction(item)); } + protected override void Visit(ClearAllVariables item) + { + Trace(item); + + this.AddAction(new ClearAllVariablesAction(item)); + } + + protected override void Visit(ResetVariable item) + { + Trace(item); + + this.AddAction(new ResetVariableAction(item)); + } + protected override void Visit(EditTable item) { Trace(item); @@ -198,17 +229,17 @@ protected override void Visit(SendActivity item) #region Not implemented - protected override void Visit(GetActivityMembers item) + protected override void Visit(DeleteActivity item) { Trace(item); } - protected override void Visit(UpdateActivity item) + protected override void Visit(GetActivityMembers item) { Trace(item); } - protected override void Visit(DeleteActivity item) + protected override void Visit(UpdateActivity item) { Trace(item); } @@ -258,13 +289,6 @@ protected override void Visit(OAuthInput item) Trace(item); } - protected override void Visit(Foreach item) - { - Trace(item); - - this.AddAction(new ForeachAction(item)); - } - protected override void Visit(RepeatDialog item) { Trace(item); @@ -295,26 +319,6 @@ protected override void Visit(CancelDialog item) Trace(item); } - protected override void Visit(ClearAllVariables item) - { - Trace(item); - } - - protected override void Visit(BreakLoop item) - { - Trace(item); - } - - protected override void Visit(ContinueLoop item) - { - Trace(item); - } - - protected override void Visit(ResetVariable item) - { - Trace(item); - } - protected override void Visit(EmitEvent item) { Trace(item); @@ -433,9 +437,16 @@ private ProcessStepBuilder CreateActionStep(ProcessActionVisitorContext currentC action.Id.Value, async (kernel, context) => { + Console.WriteLine($"!!! STEP {action.GetType().Name} [{action.Id}]"); // %%% DEVTRACE + + if (action.Model.Disabled) // %%% VALIDATE + { + Console.WriteLine($"!!! DISABLED {action.GetType().Name} [{action.Id}]"); // %%% DEVTRACE + return; + } + try { - Console.WriteLine($"!!! STEP {action.GetType().Name} [{action.Id}]"); // %%% DEVTRACE ProcessActionContext actionContext = new(this.CreateEngine(), this._scopes, kernel); await action.ExecuteAsync(actionContext, cancellationToken: default).ConfigureAwait(false); // %%% CANCEL TOKEN } diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ClearAllVariablesActionTest.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ClearAllVariablesActionTest.cs new file mode 100644 index 000000000000..af3b1f5c2916 --- /dev/null +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ClearAllVariablesActionTest.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx.Types; +using Microsoft.SemanticKernel.Process.Workflows.Actions; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.SemanticKernel.Process.UnitTests.Workflows.Actions; + +/// +/// Tests for . +/// +public sealed class ClearAllVariablesActionTest(ITestOutputHelper output) : ProcessActionTest(output) +{ + [Fact] + public async Task ClearUserScope() + { + // Arrange + this.Scopes.Set("NoVar", FormulaValue.New("Old value")); + + ClearAllVariables model = + this.CreateModel( + this.FormatDisplayName(nameof(ClearUserScope)), + VariablesToClear.UserScopedVariables); + + // Act + ClearAllVariablesAction action = new(model); + await this.ExecuteAction(action); + + // Assert + this.VerifyModel(model, action); + this.VerifyUndefined("NoVar"); + } + + [Fact] + public async Task ClearUndefinedScope() + { + // Arrange + ClearAllVariables model = + this.CreateModel( + this.FormatDisplayName(nameof(ClearUndefinedScope)), + VariablesToClear.UserScopedVariables); + + // Act + ClearAllVariablesAction action = new(model); + await this.ExecuteAction(action); + + // Assert + this.VerifyModel(model, action); + this.VerifyUndefined("NoVar"); + } + + private ClearAllVariables CreateModel(string displayName, VariablesToClear variableTarget) + { + ClearAllVariables.Builder actionBuilder = + new() + { + Id = this.CreateActionId(), + DisplayName = this.FormatDisplayName(displayName), + Variables = VariablesToClearWrapper.Get(variableTarget), + }; + + ClearAllVariables model = actionBuilder.Build(); + + return model; + } +} diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ProcessActionTest.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ProcessActionTest.cs index b5e418635499..391df60cbe89 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ProcessActionTest.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ProcessActionTest.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; using Microsoft.PowerFx.Types; @@ -43,4 +44,11 @@ internal void VerifyState(string variableName, ActionScopeType scope, FormulaVal FormulaValue actualValue = this.Scopes.Get(variableName, scope); Assert.Equivalent(expectedValue, actualValue); } + + protected void VerifyUndefined(string variableName) => this.VerifyUndefined(variableName, ActionScopeType.Topic); + + internal void VerifyUndefined(string variableName, ActionScopeType scope) + { + Assert.Throws(() => this.Scopes.Get(variableName, scope)); + } } diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ResetVariableActionTest.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ResetVariableActionTest.cs new file mode 100644 index 000000000000..174aabd7b2f1 --- /dev/null +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ResetVariableActionTest.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx.Types; +using Microsoft.SemanticKernel.Process.Workflows.Actions; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.SemanticKernel.Process.UnitTests.Workflows.Actions; + +/// +/// Tests for . +/// +public sealed class ResetVariableTest(ITestOutputHelper output) : ProcessActionTest(output) +{ + [Fact] + public async Task ResetDefinedValue() + { + // Arrange + this.Scopes.Set("MyVar", FormulaValue.New("Old value")); + + ResetVariable model = + this.CreateModel( + this.FormatDisplayName(nameof(ResetDefinedValue)), + FormatVariablePath("MyVar")); + + // Act + ResetVariableAction action = new(model); + await this.ExecuteAction(action); + + // Assert + this.VerifyModel(model, action); + this.VerifyUndefined("MyVar"); + } + + [Fact] + public async Task ResetUndefinedValue() + { + // Arrange + ResetVariable model = + this.CreateModel( + this.FormatDisplayName(nameof(ResetUndefinedValue)), + FormatVariablePath("NoVar")); + + // Act + ResetVariableAction action = new(model); + await this.ExecuteAction(action); + + // Assert + this.VerifyModel(model, action); + this.VerifyUndefined("NoVar"); + } + + private ResetVariable CreateModel(string displayName, string variablePath) + { + ResetVariable.Builder actionBuilder = + new() + { + Id = this.CreateActionId(), + DisplayName = this.FormatDisplayName(displayName), + Variable = InitializablePropertyPath.Create(variablePath), + }; + + ResetVariable model = actionBuilder.Build(); + + return model; + } +} diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SetTextVariableActionTest.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SetTextVariableActionTest.cs index 57011664c142..0b46c65c0fbb 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SetTextVariableActionTest.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SetTextVariableActionTest.cs @@ -15,12 +15,12 @@ namespace Microsoft.SemanticKernel.Process.UnitTests.Workflows.Actions; public sealed class SetTextVariableActionTest(ITestOutputHelper output) : ProcessActionTest(output) { [Fact] - public async Task CaptureActivity() + public async Task SetLiteralValue() { // Arrange SetTextVariable model = this.CreateModel( - this.FormatDisplayName(nameof(CaptureActivity)), + this.FormatDisplayName(nameof(SetLiteralValue)), FormatVariablePath("TextVar"), "Text variable value"); @@ -33,6 +33,27 @@ public async Task CaptureActivity() this.VerifyState("TextVar", FormulaValue.New("Text variable value")); } + [Fact] + public async Task UpdateExistingValue() + { + // Arrange + this.Scopes.Set("TextVar", FormulaValue.New("Old value")); + + SetTextVariable model = + this.CreateModel( + this.FormatDisplayName(nameof(UpdateExistingValue)), + FormatVariablePath("TextVar"), + "New value"); + + // Act + SetTextVariableAction action = new(model); + await this.ExecuteAction(action); + + // Assert + this.VerifyModel(model, action); + this.VerifyState("TextVar", FormulaValue.New("New value")); + } + private SetTextVariable CreateModel(string displayName, string variablePath, string textValue) { SetTextVariable.Builder actionBuilder = From 27f49c53c4d836423d78b37a2fa88262280febbc Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 17 Jul 2025 13:04:08 -0700 Subject: [PATCH 08/40] Checkpoint (pre-refactor) --- .../Step06/Step06_WorkflowProcess.cs | 14 +- .../ObjectModel/ProcessActionVisitor.cs | 167 ++++++++++-------- .../ProcessActionVisitorContext.cs | 5 +- 3 files changed, 100 insertions(+), 86 deletions(-) diff --git a/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs b/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs index e50aa6277401..f2c3abeb4cc9 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs @@ -12,18 +12,18 @@ public class Step06_WorkflowProcess : BaseTest public Step06_WorkflowProcess(ITestOutputHelper output) : base(output, redirectSystemConsoleOutput: true) { } - [Fact] - public async Task RunWorkflowProcess() + [Theory] + [InlineData("testLoop")] + [InlineData("testCondition")] + [InlineData("deepResearch")] + public async Task RunWorkflow(string fileName) { const string InputEventId = "question"; - const string FileName = "testLoop"; - //const string FileName = "testCondition"; - //const string FileName = "deepResearch"; Console.WriteLine("$$$ PROCESS INIT"); - string yaml = File.ReadAllText(@$"{nameof(Step06)}\{FileName}.yaml"); - KernelProcess process = ObjectModelBuilder.Build(FileName, yaml, InputEventId); + string yaml = File.ReadAllText(@$"{nameof(Step06)}\{fileName}.yaml"); + KernelProcess process = ObjectModelBuilder.Build(fileName, yaml, InputEventId); Console.WriteLine("$$$ PROCESS INVOKE"); diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs index bbfb1d621416..af0f783d78e6 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs @@ -68,14 +68,14 @@ public void Complete() protected override void Visit(ActionScope item) { - Trace(item, isSkipped: false); + this.Trace(item, isSkipped: false); this.AddContainer(item.Id.Value); } protected override void Visit(ConditionGroup item) { - Trace(item, isSkipped: false); + this.Trace(item, isSkipped: false); this.AddAction(new ConditionGroupAction(item)); @@ -112,35 +112,35 @@ protected override void Visit(ConditionGroup item) protected override void Visit(GotoAction item) { - Trace(item, isSkipped: false); + this.Trace(item, isSkipped: false); this.AddContainer(item.Id.Value); // Store the link for processing after all actions have steps. - this._linkCache.Add((item.ActionId, this.CurrentContext.Then())); + this._linkCache.Add((item.ActionId, this.CurrentContext.Then())); // %%% DRY // Create an orphaned context for continuity this.AddDead(item.Id.Value); } protected override void Visit(Foreach item) { - Trace(item); + this.Trace(item); this.AddAction(new ForeachAction(item)); } protected override void Visit(BreakLoop item) { - Trace(item); + this.Trace(item); } protected override void Visit(ContinueLoop item) { - Trace(item); + this.Trace(item); } protected override void Visit(EndConversation item) { - Trace(item, isSkipped: false); + this.Trace(item, isSkipped: false); this.AddAction(new EndConversationAction(item)); // Stop the process, this is a terminal action @@ -149,80 +149,63 @@ protected override void Visit(EndConversation item) this.AddDead(item.Id.Value); } - protected override void Visit(BeginDialog item) - { - Trace(item, isSkipped: false); - - this.AddAction(new BeginDialogAction(item)); - } - - protected override void Visit(UnknownDialogAction item) - { - Trace(item); - } - - protected override void Visit(EndDialog item) - { - Trace(item); - } - protected override void Visit(AnswerQuestionWithAI item) { - Trace(item, isSkipped: false); + this.Trace(item, isSkipped: false); this.AddAction(new AnswerQuestionWithAIAction(item)); } protected override void Visit(SetVariable item) { - Trace(item, isSkipped: false); + this.Trace(item, isSkipped: false); this.AddAction(new SetVariableAction(item)); } protected override void Visit(SetTextVariable item) { - Trace(item); + this.Trace(item, isSkipped: false); this.AddAction(new SetTextVariableAction(item)); } protected override void Visit(ClearAllVariables item) { - Trace(item); + this.Trace(item, isSkipped: false); this.AddAction(new ClearAllVariablesAction(item)); } protected override void Visit(ResetVariable item) { - Trace(item); + this.Trace(item, isSkipped: false); this.AddAction(new ResetVariableAction(item)); } protected override void Visit(EditTable item) { - Trace(item); + this.Trace(item); } protected override void Visit(EditTableV2 item) { - Trace(item, isSkipped: false); + this.Trace(item, isSkipped: false); this.AddAction(new EditTableV2Action(item)); } protected override void Visit(ParseValue item) { - Trace(item, isSkipped: false); + this.Trace(item, isSkipped: false); this.AddAction(new ParseValueAction(item)); } protected override void Visit(SendActivity item) { - Trace(item, isSkipped: false); + this.Trace(item, isSkipped: false); this.AddAction(new SendActivityAction(item, this._environment.ActivityNotificationHandler)); } @@ -231,162 +214,179 @@ protected override void Visit(SendActivity item) protected override void Visit(DeleteActivity item) { - Trace(item); + this.Trace(item); } protected override void Visit(GetActivityMembers item) { - Trace(item); + this.Trace(item); } protected override void Visit(UpdateActivity item) { - Trace(item); + this.Trace(item); } - protected override void Visit(InvokeFlowAction item) + protected override void Visit(ActivateExternalTrigger item) { - Trace(item); + this.Trace(item); } - protected override void Visit(InvokeAIBuilderModelAction item) + protected override void Visit(DisableTrigger item) { - Trace(item); + this.Trace(item); } protected override void Visit(WaitForConnectorTrigger item) { - Trace(item); + this.Trace(item); } protected override void Visit(InvokeConnectorAction item) { - Trace(item); + this.Trace(item); + } + + protected override void Visit(InvokeCustomModelAction item) + { + this.Trace(item); + } + + protected override void Visit(InvokeFlowAction item) + { + this.Trace(item); + } + + protected override void Visit(InvokeAIBuilderModelAction item) + { + this.Trace(item); } protected override void Visit(InvokeSkillAction item) { - Trace(item); + this.Trace(item); } protected override void Visit(AdaptiveCardPrompt item) { - Trace(item); + this.Trace(item); } protected override void Visit(Question item) { - Trace(item); + this.Trace(item); } protected override void Visit(CSATQuestion item) { - Trace(item); + this.Trace(item); } protected override void Visit(OAuthInput item) { - Trace(item); + this.Trace(item); } - protected override void Visit(RepeatDialog item) + protected override void Visit(BeginDialog item) { - Trace(item); + this.Trace(item); + + this.AddAction(new BeginDialogAction(item)); } - protected override void Visit(ActivateExternalTrigger item) + protected override void Visit(UnknownDialogAction item) { - Trace(item); + this.Trace(item); } - protected override void Visit(DisableTrigger item) + protected override void Visit(EndDialog item) + { + this.Trace(item); + } + + protected override void Visit(RepeatDialog item) { - Trace(item); + this.Trace(item); } protected override void Visit(ReplaceDialog item) { - Trace(item); + this.Trace(item); } protected override void Visit(CancelAllDialogs item) { - Trace(item); + this.Trace(item); } protected override void Visit(CancelDialog item) { - Trace(item); + this.Trace(item); } protected override void Visit(EmitEvent item) { - Trace(item); + this.Trace(item); } protected override void Visit(GetConversationMembers item) { - Trace(item); + this.Trace(item); } protected override void Visit(HttpRequestAction item) { - Trace(item); + this.Trace(item); } protected override void Visit(RecognizeIntent item) { - Trace(item); + this.Trace(item); } protected override void Visit(TransferConversation item) { - Trace(item); + this.Trace(item); } protected override void Visit(TransferConversationV2 item) { - Trace(item); + this.Trace(item); } protected override void Visit(SignOutUser item) { - Trace(item); + this.Trace(item); } protected override void Visit(LogCustomTelemetryEvent item) { - Trace(item); + this.Trace(item); } protected override void Visit(DisconnectedNodeContainer item) { - Trace(item); + this.Trace(item); } protected override void Visit(CreateSearchQuery item) { - Trace(item); + this.Trace(item); } protected override void Visit(SearchKnowledgeSources item) { - Trace(item); + this.Trace(item); } protected override void Visit(SearchAndSummarizeWithCustomModel item) { - Trace(item); + this.Trace(item); } protected override void Visit(SearchAndSummarizeContent item) { - Trace(item); - } - - protected override void Visit(InvokeCustomModelAction item) - { - Trace(item); + this.Trace(item); } #endregion @@ -479,8 +479,21 @@ private void ContinueWith(ProcessStepBuilder newStep, KernelProcessEdgeCondition private RecalcEngine CreateEngine() => RecalcEngineFactory.Create(this._scopes, this._environment.MaximumExpressionLength); - private static void Trace(DialogAction item, bool isSkipped = true) + private void Trace(DialogAction item, bool isSkipped = true) { - Console.WriteLine($"> {(isSkipped ? "EMPTY" : "VISIT")} - {item.GetType().Name} [{item.Id.Value}]"); // %%% DEVTRACE + //Console.WriteLine($"> {(isSkipped ? "EMPTY" : "VISIT")}{new string('\t', this._contextStack.Count - 1)} - {this.Format(item)} => {this.Format(item.Parent)}"); // %%% DEVTRACE + Console.WriteLine($"> {(isSkipped ? "EMPTY" : "VISIT")} x{this._contextStack.Count} - {this.Format(item)} => {this.Format(item.Parent)}"); // %%% DEVTRACE } + + private string Format(DialogAction action) => $"{action.GetType().Name} [{action.Id.Value}]"; + + private string Format(BotElement? element) => + element switch + { + null => "(root)", + DialogAction action => this.Format(action), + ConditionItem conditionItem => $"{conditionItem.GetType().Name} [{conditionItem.Id}]", + OnActivity activity => $"{activity.GetType().Name} (workflow)", + _ => $"{element.GetType().Name} (unknown element)" + }; } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitorContext.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitorContext.cs index 85cdbe08c891..46966a5323cc 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitorContext.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitorContext.cs @@ -32,10 +32,11 @@ internal sealed class ProcessActionVisitorContext(ProcessStepBuilder step) public ProcessStepBuilder Then(ProcessStepBuilder step, KernelProcessEdgeCondition? condition = null) { // IN: Target the given step when the previous step ends - ProcessStepEdgeBuilder edge = this.Then(); + ProcessStepEdgeBuilder edge = + this.Then() + .SendEventTo(new ProcessFunctionTargetBuilder(step)); edge.Condition = condition; - edge.SendEventTo(new ProcessFunctionTargetBuilder(step)); return step; } From 14f1b0e09bb867d580951067bc3a66508f5baad6 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 18 Jul 2025 15:31:52 -0700 Subject: [PATCH 09/40] Usage definition --- .../Workflow/ObjectModel/usage.md | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/usage.md diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/usage.md b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/usage.md new file mode 100644 index 000000000000..51c2a853a126 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/usage.md @@ -0,0 +1,58 @@ +# Declarative Process Workflows + +### Goal + +Use the _Process Framework_ to execute a workflow defined with _Copilot Studio Declarative Language_ (CPSDL). + + +### Requirements + +- Translate a workflow defined in CPSDL into a _Process_ for execution. +- Host the resulting _Process_ workflow in the same runtime as any other _Process_. + + +### Dependencies + +Outside of the _Process Framework_, the following packages are utilized to parse and define a workflow based on CSPDL: + +- `Microsoft.Bot.ObjectModel` +- `Microsoft.PowerFx.Interpreter` + + +### Inputs + +The runtime is expected to provide the following inputs: + +- Host Context: Configuration and channels provided by the runtime. +- Workflow Definition: A declarative YAML workflow that defines CPSDL actions. +- Task: User / application input that informs workflow execution. + + +### Invocation Pattern + +The only consideration for hosting a workflow process based on CSPDL that differs from +a pro-code workflow is how the builder is invoked: + +```c# +// Context defined by the runtime to provide configuration and channels +HostingContext hostContext = ...; +// Customer defined CPSDL workflow as YAML string +string yamlText = ...; +// User input specific to this workflow execution +string inputMessage = "Why is the sky blue?"; + +// Parse the CPSDL yaml and provide the resulting KernelProcess instance +// This is should be the _only_ difference compared to running a "pro-code" process +KernelProcess process = ObjectModelBuilder.Build(yamlText, hostContext); + +// Execute process with CPSDL specific extension +process.StartAsync(inputMessage); +``` + +### Open Issues + +- **Schema Versioning:** + + How will the runtime associate workflows defined with different versions of the CPSDL object model with the appropriate builder? + + Is this distinction necessary if object model maintains backward compatibility? \ No newline at end of file From 8dc5a01819d211f3971cd431c98d0e8d04ce10a4 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 18 Jul 2025 15:34:36 -0700 Subject: [PATCH 10/40] rename --- .../Process.Core/Workflow/ObjectModel/{usage.md => readme.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/{usage.md => readme.md} (100%) diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/usage.md b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/readme.md similarity index 100% rename from dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/usage.md rename to dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/readme.md From 45243cecd2de63eeeb4c2702bf4621f5df4df9f5 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 21 Jul 2025 16:04:16 -0700 Subject: [PATCH 11/40] Update readme --- .../Process.Core/Workflow/ObjectModel/readme.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/readme.md b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/readme.md index 51c2a853a126..844bc2d7812a 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/readme.md +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/readme.md @@ -36,14 +36,14 @@ a pro-code workflow is how the builder is invoked: ```c# // Context defined by the runtime to provide configuration and channels HostingContext hostContext = ...; -// Customer defined CPSDL workflow as YAML string -string yamlText = ...; +// Customer defined CPSDL workflow as YAML +TextReader yamlReader = ...; // User input specific to this workflow execution string inputMessage = "Why is the sky blue?"; // Parse the CPSDL yaml and provide the resulting KernelProcess instance // This is should be the _only_ difference compared to running a "pro-code" process -KernelProcess process = ObjectModelBuilder.Build(yamlText, hostContext); +KernelProcess process = ObjectModelBuilder.Build(yamlReader, hostContext); // Execute process with CPSDL specific extension process.StartAsync(inputMessage); From 9d654de225896148f1216ba60b38c40265b43c80 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 22 Jul 2025 13:53:10 -0700 Subject: [PATCH 12/40] Requirements --- .../ObjectModel/requirements_objectmodel.md | 68 +++++++++++++++++++ .../ObjectModel/requirements_process.md | 25 +++++++ 2 files changed, 93 insertions(+) create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/requirements_objectmodel.md create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/requirements_process.md diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/requirements_objectmodel.md b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/requirements_objectmodel.md new file mode 100644 index 000000000000..5de06994158b --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/requirements_objectmodel.md @@ -0,0 +1,68 @@ +## Object Model Requirements + +#### Definition + +Name|Copilot Studio|Foundry|Note +:--|:--:|:--:|:-- +ActionScope|✔|✔|Container for actions +ActivateExternalTrigger|✔|❌ +AdaptiveCardPrompt|✔|❌ +AnswerQuestionWithAI|✔|✔|Requires additional property: model (`ExternalModelConfiguration`) +BeginDialog|✔|❌ +BreakLoop|✔|✔ +CSATQuestion|✔|❌ +CancelAllDialogs|✔|❌ +CancelDialog|✔|❌ +ClearAllVariables|✔|✔ +ConditionGroup|✔|✔|Includes one or more `ConditionItem` and an `ElseActions` +ContinueLoop|✔|✔ +CreateSearchQuery|✔|❌ +DeleteActivity|✔|❓ +DisableTrigger|✔|❌ +DisconnectedNodeContainer|✔|❌ +EditTable|✔|❓ +EditTableV2|✔|✔|Are both `EditTable*` actions needed? +EmitEvent|✔|❓ +EndConversation|✔|✔ +EndDialog|✔|❌ +Foreach|✔|✔ +GetActivityMembers|✔|❓ +GetConversationMembers|✔|❓ +GotoAction|✔|✔ +HttpRequestAction|✔|❌ +InvokeAIBuilderModelAction|✔|❌ +InvokeAgent|❌|✔|Based on _Foundry_ agent identifier +InvokeConnectorAction|✔|❌ +InvokeCustomModelAction|✔|❌ +InvokeFlowAction|✔|❌ +InvokeSkillAction|✔|❌ +LogCustomTelemetryEvent|✔|✔ +OAuthInput|✔|❌ +ParseValue|✔|✔ +Question|✔|❓|Solicits user input +RecognizeIntent|✔|❓ +RepeatDialog|✔|❌ +ReplaceDialog|✔|❌ +ResetVariable|✔|✔ +SearchAndSummarizeContent|✔|❌ +SearchAndSummarizeWithCustomModel|✔|❌ +SearchKnowledgeSources|✔|❌ +SendActivity|✔|✔ +SetTextVariable|✔|✔ +SetVariable|✔|✔ +SignOutUser|✔|❌ +TransferConversation|✔|❌ +TransferConversationV2|✔|❌ +UnknownDialogAction|✔|❓ +UpdateActivity|✔|❓ +WaitForConnectorTrigger|✔|❌ + +#### Open Questions +- Can a _Copilot Studio_ workflow be hosted in _Foundry_? **NO** +- Can a _Foundry_ workflow be utlized in _Copilot Studio_? **NO** +- Can a _Foundry_ workflow be hosted in different projects without modification? **NO** + (Agent identifiers differ even if model deployments match.) +- Is a _Foundry_ workfow specific to a single _Foundry_ project? **YES** +- Can user defined workflow YAML be uploaded to a _Foundry_ project? **YES** + (YAML can be authored in VS Code via a designer extension.) +- What is the validation process for a declarative YAML workflow? diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/requirements_process.md b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/requirements_process.md new file mode 100644 index 000000000000..329554701fca --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/requirements_process.md @@ -0,0 +1,25 @@ +## Process Flavors + +#### Types of Processes +- Pro-Code: Developer coded process hosted as a foundry workflow +- Declarative: Process built by interpreting declarative yaml +- Custom: Developer coded process hosted in whatever service they choose + +#### Process Comparison +Aspect|Pro-Code|Declarative|Custom +:--|:--:|:--:|:--: +Accepts `ILoggerFactory`|✔|✔|✔ +Checkpoint & restore process|✔|✔|✔ +Hosted in _Foundry_ |✔|✔|❓ +Requires _Foundry_ endpoint|✔|✔|❌ +Supports custom steps|✔|❌|✔ +Emits execution events|❓|✔|❓ + +#### Open Questions +- Can _any_ process be hosted in _Foundry_? + (Is a "Pro-Code" process and "Custom" process equivalent?) +- Can a "Pro-Code" process be represented as a declarative workflow? **NO** +- Do execute events need to be emitted for any process? **YES** +- Can a _Foundry_ workflow be hosted in different projects without modification? **NO** + (Agent identifiers differ even if model deployments match.) +- Is a _Foundry_ workfow specific to a single _Foundry_ project? **YES** From 4ced8ec7fa8334dfc144166df33251bbfe1d45ef Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 22 Jul 2025 14:44:53 -0700 Subject: [PATCH 13/40] Definition update --- .../GettingStartedWithProcesses/Step06/deepResearch.yaml | 4 +++- .../GettingStartedWithProcesses/Step06/testCondition.yaml | 2 +- .../samples/GettingStartedWithProcesses/Step06/testLoop.yaml | 5 +++-- .../Workflow/ObjectModel/requirements_objectmodel.md | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/dotnet/samples/GettingStartedWithProcesses/Step06/deepResearch.yaml b/dotnet/samples/GettingStartedWithProcesses/Step06/deepResearch.yaml index 563d2ec8317f..4763cb17515e 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step06/deepResearch.yaml +++ b/dotnet/samples/GettingStartedWithProcesses/Step06/deepResearch.yaml @@ -1,7 +1,9 @@ +# TaskDialog +# AgentDialog kind: AdaptiveDialog beginDialog: kind: OnActivity - id: main + id: activity_xyz123 condition: =Global.OrchestratorRunning <> true type: Message actions: diff --git a/dotnet/samples/GettingStartedWithProcesses/Step06/testCondition.yaml b/dotnet/samples/GettingStartedWithProcesses/Step06/testCondition.yaml index 5b0b62a75492..8fbb3d703d3a 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step06/testCondition.yaml +++ b/dotnet/samples/GettingStartedWithProcesses/Step06/testCondition.yaml @@ -1,7 +1,7 @@ kind: AdaptiveDialog beginDialog: kind: OnActivity - id: main + id: activity_xyz123 type: Message actions: - kind: SetVariable diff --git a/dotnet/samples/GettingStartedWithProcesses/Step06/testLoop.yaml b/dotnet/samples/GettingStartedWithProcesses/Step06/testLoop.yaml index d466da5af13e..c2034d4d9126 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step06/testLoop.yaml +++ b/dotnet/samples/GettingStartedWithProcesses/Step06/testLoop.yaml @@ -1,7 +1,7 @@ kind: AdaptiveDialog beginDialog: kind: OnActivity - id: main + id: activity_xyz123 type: Message actions: - kind: SetVariable @@ -17,6 +17,7 @@ beginDialog: - kind: Foreach id: foreach_mVIecC items: ["a", "b", "c", "d", "e", "f"] + variable: Topic.LoopValue actions: - kind: SetVariable id: setVariable_A4iBtN @@ -25,7 +26,7 @@ beginDialog: value: =Topic.Count + 1 - kind: SendActivity id: sendActivity_Pkkmpq - activity: Looping (x{Topic.Count}) + activity: Looping (x{Topic.Count}) - {Topic.LoopValue} - kind: SendActivity id: sendActivity_fJsbRz diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/requirements_objectmodel.md b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/requirements_objectmodel.md index 5de06994158b..aab4727cfa9f 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/requirements_objectmodel.md +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/requirements_objectmodel.md @@ -31,7 +31,7 @@ GetConversationMembers|✔|❓ GotoAction|✔|✔ HttpRequestAction|✔|❌ InvokeAIBuilderModelAction|✔|❌ -InvokeAgent|❌|✔|Based on _Foundry_ agent identifier +InvokeFoundryAgent|❌|✔|Based on _Foundry_ agent identifier InvokeConnectorAction|✔|❌ InvokeCustomModelAction|✔|❌ InvokeFlowAction|✔|❌ From 8d305e7e7fdabc229beb99d3e307fd17651e6bda Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 22 Jul 2025 18:49:55 -0700 Subject: [PATCH 14/40] Update requirements --- .../ObjectModel/requirements_objectmodel.md | 148 ++++++++++++------ 1 file changed, 102 insertions(+), 46 deletions(-) diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/requirements_objectmodel.md b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/requirements_objectmodel.md index aab4727cfa9f..1e1e9c90c16d 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/requirements_objectmodel.md +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/requirements_objectmodel.md @@ -1,68 +1,124 @@ -## Object Model Requirements +# Object Model Requirements -#### Definition +## **Types** -Name|Copilot Studio|Foundry|Note -:--|:--:|:--:|:-- -ActionScope|✔|✔|Container for actions -ActivateExternalTrigger|✔|❌ +- ✔ **Message** + - Corresponds with user input or an agent response. + - Requires robust support for how to access, identify most recent, transfer, count, etc... + +- ❌ **Thread** + - Every workflow invocation is associated with a single "user thread" + - Each agent is associated with its own dedicated thread that is implicitly managed. + - Exposing an explicit thread primative within a workflow introduces additional complexity that is not required. + +## **Actions** + +The following table enumerates the actions currently supported by _Copilot Studio_ +and those to be introduced specific to _Foundry_ workflows. +_CopilotStudio_ actions not supported by _Foundry_ workflows are marked with `❌`. + +Name|Copilot Studio|Foundry|Priority|Note +:--|:--:|:--:|:--:|:-- +ActionScope|✔|✔|✔|Container for actions +ActivateExternalTrigger|✔|❓||Not supported for v0. Evaluate trigger actions in next phase. AdaptiveCardPrompt|✔|❌ -AnswerQuestionWithAI|✔|✔|Requires additional property: model (`ExternalModelConfiguration`) +AnswerQuestionWithAI|✔|❌||Insufficient definition for _Foundry_. Use `InvokeChatCompletion` instead. BeginDialog|✔|❌ -BreakLoop|✔|✔ +BreakLoop|✔|✔|✔ CSATQuestion|✔|❌ CancelAllDialogs|✔|❌ CancelDialog|✔|❌ -ClearAllVariables|✔|✔ -ConditionGroup|✔|✔|Includes one or more `ConditionItem` and an `ElseActions` -ContinueLoop|✔|✔ +ClearAllVariables|✔|✔|✔ +ConditionGroup|✔|✔|✔|Includes one or more `ConditionItem` and an `ElseActions`. +ContinueLoop|✔|✔|✔ CreateSearchQuery|✔|❌ -DeleteActivity|✔|❓ -DisableTrigger|✔|❌ +DeleteActivity|✔|❓|❓|How does an _Activity_ differ from a _Message_? +DisableTrigger|✔|❓||Not supported for v0. Evaluate trigger actions in next phase. DisconnectedNodeContainer|✔|❌ -EditTable|✔|❓ -EditTableV2|✔|✔|Are both `EditTable*` actions needed? -EmitEvent|✔|❓ -EndConversation|✔|✔ +EditTable|✔|❌||Favor `EditTableV2` instead. +EditTableV2|✔|✔|✔ +EmitEvent|✔|❌ +EndConversation|✔|✔|✔|Terminal action when specified. A sequential workflow will automatically end after the final action. EndDialog|✔|❌ -Foreach|✔|✔ -GetActivityMembers|✔|❓ -GetConversationMembers|✔|❓ -GotoAction|✔|✔ -HttpRequestAction|✔|❌ +Foreach|✔|✔|✔ +GetActivityMembers|✔|❌ +GetConversationMembers|✔|❌ +GotoAction|✔|✔|✔ +HttpRequestAction|✔|❌||Favor usage of `InvokeTool` instead. InvokeAIBuilderModelAction|✔|❌ -InvokeFoundryAgent|❌|✔|Based on _Foundry_ agent identifier +InvokeAgent|❌|✔|✔|Produce a response for _Foundry_ agent based on its name or identifier. +InvokeChatCompletion|❌|✔|✔|Invoke model using chat-completion API. InvokeConnectorAction|✔|❌ InvokeCustomModelAction|✔|❌ InvokeFlowAction|✔|❌ +InvokeResponse|❌|✔||Invoke model using response API. Not supported for v0. InvokeSkillAction|✔|❌ -LogCustomTelemetryEvent|✔|✔ +InvokeTool|❌|✔|❓|Unify how tools are defined and invoke directly (outside of agent invocation). Can include _Open API_, _Azure Function_, _Search_, etc... +LogCustomTelemetryEvent|✔|❓||Not supported for v0. Could be captured as part of a [_Foundry_ Observability](https://learn.microsoft.com/azure/ai-foundry/agents/concepts/tracing). OAuthInput|✔|❌ -ParseValue|✔|✔ -Question|✔|❓|Solicits user input -RecognizeIntent|✔|❓ +ParseValue|✔|✔|✔ +Question|✔|✔|✔|Solicits user input (human-in-the-loop). +RecognizeIntent|✔|❌ RepeatDialog|✔|❌ ReplaceDialog|✔|❌ -ResetVariable|✔|✔ +ResetVariable|✔|✔|✔ SearchAndSummarizeContent|✔|❌ SearchAndSummarizeWithCustomModel|✔|❌ SearchKnowledgeSources|✔|❌ -SendActivity|✔|✔ -SetTextVariable|✔|✔ -SetVariable|✔|✔ +SendActivity|✔|❓|❓|How does an _Activity_ differ from a _Message_? +SetTextVariable|✔|✔|✔ +SetVariable|✔|✔|✔ SignOutUser|✔|❌ -TransferConversation|✔|❌ -TransferConversationV2|✔|❌ -UnknownDialogAction|✔|❓ -UpdateActivity|✔|❓ -WaitForConnectorTrigger|✔|❌ - -#### Open Questions -- Can a _Copilot Studio_ workflow be hosted in _Foundry_? **NO** -- Can a _Foundry_ workflow be utlized in _Copilot Studio_? **NO** -- Can a _Foundry_ workflow be hosted in different projects without modification? **NO** - (Agent identifiers differ even if model deployments match.) -- Is a _Foundry_ workfow specific to a single _Foundry_ project? **YES** -- Can user defined workflow YAML be uploaded to a _Foundry_ project? **YES** - (YAML can be authored in VS Code via a designer extension.) -- What is the validation process for a declarative YAML workflow? +TransferConversation|✔|❌||Favor `TransferConversationV2` instead, if at all. +TransferConversationV2|✔|❌||Not supported for v0. Could invoke invoke a different workflow; although, overloading action may be undesirable. +UnknownDialogAction|✔|❌||Serialization construct that represents an unknown action. Not explicitly expressed as a workflow action. +UpdateActivity|✔|❓|❓|How does an _Activity_ differ from a _Message_? +WaitForConnectorTrigger|✔|❓||Not supported for v0. Evaluate trigger actions in next phase. + +## Behaviors + +### Is a _Foundry_ workfow specific to a single _Foundry_ project? + +Always. +This implies that a _Foundry_ workflow has access to the resources associated with its project, +including: models, agents, and connections. + +### Can user defined workflow YAML be uploaded to a _Foundry_ project? + +Yes. +As _VS Code_ extension will support the authoring of _Foundry_ workflows in YAML. +The resulting YAML can be uploaded to a _Foundry_ project. +Ostensibly, one could directly author raw YAML and upload. + + +### Can a Foundry workflow be hosted in different projects without modification? + +Agent identifiers differ across projects even if model deployments match. +This creates an incompatibility between projects, +exception in the case where an agent is identified by name only. + + +### Can a _Copilot Studio_ workflow be hosted in _Foundry_ or vice-versa? + +Since the actions diverge between what is supported for _Copilot Studio_ and _Foundry_, +interoperability between platforms is not generally supported. +The special case where a workflow contains only the core actions that are supported by both platforms +might allow for interoperablity. + +### How and when are declarative workflows (YAML) validated? + +Validating a declarative workflow prior to execution provides a superior user experience. +This validation should be triggered for any workflow update: +- Uploading a YAML file that creates or overwrites a workflow. +- Creating or editing a workflow from the designer. + +Validation must include: +- Schema validation: + Can the YAML be deserialized and parsed? +- Functional validation: + Does the YAML consist of only the actions supported by the _Foundry_ object model? + Is each action properly defined with valid identifiers? +- Reference validation: + Are all referenced resources (models, agents, connections) valid and accessible? + +At runtime the references shall be re-validated and, where relevant, logical names translated into physical identifiers. From 64c68b2c6fd0f7e8838b291c7ce1ef8619cd5520 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 22 Jul 2025 18:53:17 -0700 Subject: [PATCH 15/40] Typos --- .../ObjectModel/requirements_objectmodel.md | 4 ++-- .../ObjectModel/requirements_process.md | 2 +- .../Extensions/StringExtensionsTests.cs | 24 +++++++++---------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/requirements_objectmodel.md b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/requirements_objectmodel.md index 1e1e9c90c16d..d71e687ee23d 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/requirements_objectmodel.md +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/requirements_objectmodel.md @@ -9,7 +9,7 @@ - ❌ **Thread** - Every workflow invocation is associated with a single "user thread" - Each agent is associated with its own dedicated thread that is implicitly managed. - - Exposing an explicit thread primative within a workflow introduces additional complexity that is not required. + - Exposing an explicit thread primitive within a workflow introduces additional complexity that is not required. ## **Actions** @@ -77,7 +77,7 @@ WaitForConnectorTrigger|✔|❓||Not supported for v0. Evaluate trigger actions ## Behaviors -### Is a _Foundry_ workfow specific to a single _Foundry_ project? +### Is a _Foundry_ workflow specific to a single _Foundry_ project? Always. This implies that a _Foundry_ workflow has access to the resources associated with its project, diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/requirements_process.md b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/requirements_process.md index 329554701fca..cbb5156cd43b 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/requirements_process.md +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/requirements_process.md @@ -22,4 +22,4 @@ Emits execution events|❓|✔|❓ - Do execute events need to be emitted for any process? **YES** - Can a _Foundry_ workflow be hosted in different projects without modification? **NO** (Agent identifiers differ even if model deployments match.) -- Is a _Foundry_ workfow specific to a single _Foundry_ project? **YES** +- Is a _Foundry_ workflow specific to a single _Foundry_ project? **YES** diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/Extensions/StringExtensionsTests.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/Extensions/StringExtensionsTests.cs index 5aee37440f40..2e6b5b107a5a 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Workflow/Extensions/StringExtensionsTests.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/Extensions/StringExtensionsTests.cs @@ -8,7 +8,7 @@ namespace Microsoft.SemanticKernel.Process.UnitTests.Workflows.Extensions; public class StringExtensionsTests { [Fact] - public void TrimJsonWithDelimeter() + public void TrimJsonWithDelimiter() { // Arrange const string input = @@ -21,7 +21,7 @@ public void TrimJsonWithDelimeter() """; // Act - string result = input.TrimJsonDelimeter(); + string result = input.TrimJsonDelimiter(); // Assert Assert.Equal( @@ -47,7 +47,7 @@ public void TrimJsonWithPadding() """; // Act - string result = input.TrimJsonDelimeter(); + string result = input.TrimJsonDelimiter(); // Assert Assert.Equal( @@ -60,7 +60,7 @@ public void TrimJsonWithPadding() } [Fact] - public void TrimJsonWithUnqualifiedDelimeter() + public void TrimJsonWithUnqualifiedDelimiter() { // Arrange const string input = @@ -73,7 +73,7 @@ public void TrimJsonWithUnqualifiedDelimeter() """; // Act - string result = input.TrimJsonDelimeter(); + string result = input.TrimJsonDelimiter(); // Assert Assert.Equal( @@ -86,7 +86,7 @@ public void TrimJsonWithUnqualifiedDelimeter() } [Fact] - public void TrimJsonWithoutDelimeter() + public void TrimJsonWithoutDelimiter() { // Arrange const string input = @@ -97,7 +97,7 @@ public void TrimJsonWithoutDelimeter() """; // Act - string result = input.TrimJsonDelimeter(); + string result = input.TrimJsonDelimiter(); // Assert Assert.Equal( @@ -110,7 +110,7 @@ public void TrimJsonWithoutDelimeter() } [Fact] - public void TrimJsonWithoutDelimeterWithPadding() + public void TrimJsonWithoutDelimiterWithPadding() { // Arrange const string input = @@ -122,7 +122,7 @@ public void TrimJsonWithoutDelimeterWithPadding() """; // Act - string result = input.TrimJsonDelimeter(); + string result = input.TrimJsonDelimiter(); // Assert Assert.Equal( @@ -135,7 +135,7 @@ public void TrimJsonWithoutDelimeterWithPadding() } [Fact] - public void TrimMissingWithDelimeter() + public void TrimMissingWithDelimiter() { // Arrange const string input = @@ -145,7 +145,7 @@ public void TrimMissingWithDelimeter() """; // Act - string result = input.TrimJsonDelimeter(); + string result = input.TrimJsonDelimiter(); // Assert Assert.Equal(string.Empty, result); @@ -155,7 +155,7 @@ public void TrimMissingWithDelimeter() public void TrimEmptyString() { // Act - string result = string.Empty.TrimJsonDelimeter(); + string result = string.Empty.TrimJsonDelimiter(); // Assert Assert.Equal(string.Empty, result); From 2fbdfbc8106102dcb5538a74978379a43a45ac36 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 22 Jul 2025 18:59:08 -0700 Subject: [PATCH 16/40] Typo --- .../Workflow/ObjectModel/Actions/ParseValueAction.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ParseValueAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ParseValueAction.cs index 26dc850a1752..b1b7804435fd 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ParseValueAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ParseValueAction.cs @@ -53,7 +53,7 @@ protected override Task HandleAsync(ProcessActionContext context, CancellationTo private static RecordValue ParseRecord(RecordDataType recordType, string rawText) { - string jsonText = rawText.TrimJsonDelimeter(); + string jsonText = rawText.TrimJsonDelimiter(); JsonDocument json = JsonDocument.Parse(jsonText); JsonElement currentElement = json.RootElement; return recordType.ParseRecord(currentElement); From 3b0cb2ac380dd7a4a97ef7c5a3a334acae877f4d Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 22 Jul 2025 19:13:12 -0700 Subject: [PATCH 17/40] Typo --- .../Workflow/ObjectModel/Extensions/StringExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/StringExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/StringExtensions.cs index e92a1120211c..6b8f0969dd7d 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/StringExtensions.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/StringExtensions.cs @@ -8,7 +8,7 @@ internal static class StringExtensions { private static readonly Regex s_regex = new(@"^```(?:\w*)\s*([\s\S]*?)\s*```$", RegexOptions.Compiled | RegexOptions.Multiline); - public static string TrimJsonDelimeter(this string value) + public static string TrimJsonDelimiter(this string value) { Match match = s_regex.Match(value.Trim()); if (match.Success) From 380fd1f325e2d6995de813505fa9b58c569ed77f Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 22 Jul 2025 20:50:50 -0700 Subject: [PATCH 18/40] Validation checkpoint --- .../Step06/Step06_WorkflowProcess.cs | 13 +- .../Exceptions/UnknownActionException.cs | 35 +++++ .../Extensions/BotElementExtensions.cs | 24 +++ .../Extensions/DataValueExtensions.cs | 2 +- ...essActionEnvironment.cs => HostContext.cs} | 4 +- .../ObjectModel/ObjectModelBuilder.cs | 119 +++++++++++++- .../Workflow/ObjectModel/ProcessAction.cs | 9 +- .../ObjectModel/ProcessActionWalker.cs | 27 ++++ .../ObjectModel/ProcessElementWalker.cs | 80 ---------- .../Validation/ActionValidationFailure.cs | 22 +++ .../Validation/ElementValidationFailure.cs | 34 ++++ .../Validation/ExceptionValidationFailure.cs | 25 +++ .../Validation/ProcessValidationWalker.cs | 46 ++++++ .../Validation/ValidationFailure.cs | 22 +++ .../Workflow/WorkflowValidationTests.cs | 147 ++++++++++++++++++ 15 files changed, 510 insertions(+), 99 deletions(-) create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/UnknownActionException.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/BotElementExtensions.cs rename dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/{ProcessActionEnvironment.cs => HostContext.cs} (93%) create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionWalker.cs delete mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessElementWalker.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ActionValidationFailure.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ElementValidationFailure.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ExceptionValidationFailure.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ProcessValidationWalker.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ValidationFailure.cs create mode 100644 dotnet/src/Experimental/Process.UnitTests/Workflow/WorkflowValidationTests.cs diff --git a/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs b/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs index f2c3abeb4cc9..4d6db6c43474 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs @@ -20,16 +20,15 @@ public async Task RunWorkflow(string fileName) { const string InputEventId = "question"; - Console.WriteLine("$$$ PROCESS INIT"); + Console.WriteLine("PROCESS INIT\n"); - string yaml = File.ReadAllText(@$"{nameof(Step06)}\{fileName}.yaml"); - KernelProcess process = ObjectModelBuilder.Build(fileName, yaml, InputEventId); + using StreamReader yamlReader = File.OpenText(@$"{nameof(Step06)}\{fileName}.yaml"); + KernelProcess process = ObjectModelBuilder.Build(yamlReader, InputEventId); - Console.WriteLine("$$$ PROCESS INVOKE"); + Console.WriteLine("\nPROCESS INVOKE\n"); - Kernel kernel = this.CreateKernelWithChatCompletion(); + Kernel kernel = this.CreateKernelWithChatCompletion(); // %%% HOST CONTEXT await using LocalKernelProcessContext context = await process.StartAsync(kernel, new KernelProcessEvent() { Id = InputEventId, Data = "Why is the sky blue?" }); - - Console.WriteLine("$$$ PROCESS DONE"); + Console.WriteLine("\nPROCESS DONE"); } } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/UnknownActionException.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/UnknownActionException.cs new file mode 100644 index 000000000000..b30dbc4c8330 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/UnknownActionException.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace Microsoft.SemanticKernel.Process.Workflows; + +/// +/// Represents an exception that occurs when an action is invalid or cannot be processed. +/// +public sealed class UnknownActionException : ProcessWorkflowException +{ + /// + /// Initializes a new instance of the class. + /// + public UnknownActionException() + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The error message that explains the reason for the exception. + public UnknownActionException(string? message) : base(message) + { + } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public UnknownActionException(string? message, Exception? innerException) : base(message, innerException) + { + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/BotElementExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/BotElementExtensions.cs new file mode 100644 index 000000000000..57b0ebfe2616 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/BotElementExtensions.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Bot.ObjectModel; + +namespace Microsoft.SemanticKernel.Process.Workflows.Extensions; + +internal static class DataValueExtensions +{ + public static string GetParentId(this BotElement element) => + element.Parent is null ? + throw new InvalidActionException($"Undefined parent for {element.GetType().Name} that is member of {element.GetId()}.") : + element.Parent.GetId(); + + public static string GetId(this BotElement element) + { + return element switch + { + DialogAction action => action.Id.Value, + ConditionItem conditionItem => conditionItem.Id ?? throw new InvalidActionException($"Undefined identifier for {nameof(ConditionItem)} that is member of {conditionItem.GetParentId()}."), + OnActivity activity => activity.Id.Value, + _ => throw new InvalidActionException($"Unknown element type: {element.GetType().Name}"), + }; + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/DataValueExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/DataValueExtensions.cs index 19e81858f515..cbe356b77d40 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/DataValueExtensions.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/DataValueExtensions.cs @@ -5,7 +5,7 @@ namespace Microsoft.SemanticKernel.Process.Workflows.Extensions; -internal static class DataValueExtensions +internal static class BotElementExtensions { public static FormulaValue ToFormulaValue(this DataValue? value) => value switch diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionEnvironment.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/HostContext.cs similarity index 93% rename from dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionEnvironment.cs rename to dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/HostContext.cs index 29420e5eb777..6e7c4eb9d8cb 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionEnvironment.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/HostContext.cs @@ -19,12 +19,12 @@ namespace Microsoft.SemanticKernel; /// /// %%% COMMENT /// -public sealed class ProcessActionEnvironment +public sealed class HostContext { /// /// %%% COMMENT /// - internal static ProcessActionEnvironment Default { get; } = new(); + internal static HostContext Default { get; } = new(); /// /// %%% COMMENT diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs index 1608bf5c3b43..853eabaeb508 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs @@ -1,27 +1,132 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Immutable; +using System.ComponentModel; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Bot.ObjectModel; +using Microsoft.Bot.ObjectModel.Yaml; +using Microsoft.PowerFx.Types; +using Microsoft.SemanticKernel.Process.Workflow.ObjectModel.Validation; using Microsoft.SemanticKernel.Process.Workflows; namespace Microsoft.SemanticKernel; /// -/// Builder for converting CPS Topic ObjectModel YAML definition in a process. +/// Builder for converting a Foundry workflow object-model YAML definition into a process. /// public static class ObjectModelBuilder { + /// + /// %%% COMMENT + /// + /// The reader that provides the the workflow object model YAML. + /// + /// + public static bool TryValidate(TextReader yamlReader, out ImmutableArray failures) + { + BotElement? rootElement = null; + try + { + rootElement = YamlSerializer.Deserialize(yamlReader); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception exception) +#pragma warning restore CA1031 // Do not catch general exception types + { + failures = SingleFailure(new ExceptionValidationFailure(exception, "Unable to serialize object model")); + return false; + } + if (rootElement is null) + { + failures = SingleFailure(new ValidationFailure("Root element is null. Ensure the YAML content is valid.")); + return false; + } + + ProcessValidationWalker walker = new(rootElement); + failures = walker.Failures; + return walker.IsValid; + + static ImmutableArray SingleFailure(ValidationFailure failure) + { + ValidationFailure[] failures = [failure]; + return failures.ToImmutableArray(); + } + } + /// /// Builds a process from the provided YAML definition of a CPS Topic ObjectModel. /// - /// The identifier for the process. - /// The YAML string defining the CPS Topic ObjectModel. + /// The reader that provides the the workflow object model YAML. /// The identifier for the message. /// The environment for the process actions. /// The that corresponds with the YAML object model. - public static KernelProcess Build(string processId, string workflowYaml, string messageId, ProcessActionEnvironment? environment = null) + public static KernelProcess Build(TextReader yamlReader, string messageId, HostContext? environment = null) { - ProcessBuilder processBuilder = new(processId); - ProcessActionWalker walker = new(processBuilder, messageId, environment ?? ProcessActionEnvironment.Default); - walker.ProcessYaml(workflowYaml); + Console.WriteLine("@ PARSING YAML"); + BotElement rootElement = YamlSerializer.Deserialize(yamlReader) ?? throw new KernelException("Unable to parse YAML content."); + string rootId = GetRootId(rootElement); + + Console.WriteLine("@ INITIALIZING BUILDER"); + ProcessActionScopes scopes = new(); + ProcessBuilder processBuilder = new(rootId); + ProcessStepBuilder initStep = processBuilder.AddStepFromType(scopes, rootId); + processBuilder.OnInputEvent(messageId).SendEventTo(new ProcessFunctionTargetBuilder(initStep)); + + Console.WriteLine("@ INTERPRETING MODEL"); + ProcessActionVisitor visitor = new(processBuilder, environment ?? HostContext.Default, scopes); + ProcessActionWalker walker = new(rootElement, visitor); + + Console.WriteLine("@ FINALIZING PROCESS"); + ProcessStepBuilder errorHandler = // %%% DYNAMIC/CONTEXT ??? + processBuilder.AddStepFromFunction( + $"{processBuilder.Name}_unhandled_error", + (kernel, context) => + { + // Handle unhandled errors here + Console.WriteLine("*** PROCESS ERROR - Unhandled error"); // %%% EXTERNAL + return Task.CompletedTask; + }); + processBuilder.OnError().SendEventTo(new ProcessFunctionTargetBuilder(errorHandler)); + + Console.WriteLine("@ PROCESS DEFINED"); return processBuilder.Build(); } + + private static string GetRootId(BotElement element) => + element switch + { + AdaptiveDialog adaptiveDialog => adaptiveDialog.BeginDialog?.Id.Value ?? throw new InvalidOperationException("Undefined dialog"), // %%% EXCEPTION TYPE + _ => throw new KernelException($"Unsupported root element: {element.GetType().Name}."), // %%% EXCEPTION TYPE + }; + + private sealed class RootWorkflowStep : KernelProcessStep + { + private ProcessActionScopes? _scopes; + + public override ValueTask ActivateAsync(KernelProcessStepState state) + { + this._scopes = state.State; + +#if !NETCOREAPP + return new ValueTask(); +#else + return ValueTask.CompletedTask; +#endif + } + + [KernelFunction(KernelDelegateProcessStep.FunctionName)] + [Description("Initialize the process step")] + public void InitializeProcess(string message) + { + if (this._scopes == null) + { + throw new KernelException("Scopes have not been initialized. Ensure that the step is activated before calling this function."); + } + + Console.WriteLine("!!! INIT WORKFLOW"); + this._scopes.Set("LastMessage", ActionScopeType.System, StringValue.New(message)); // %%% MAGIC CONST "LastMessage" + } + } } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessAction.cs index 82dc8bcd800c..1fec12c8e778 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessAction.cs @@ -5,19 +5,24 @@ using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; using Microsoft.PowerFx; +using Microsoft.SemanticKernel.Process.Workflows.Extensions; namespace Microsoft.SemanticKernel.Process.Workflows; internal sealed record class ProcessActionContext(RecalcEngine Engine, ProcessActionScopes Scopes, Kernel Kernel); -internal abstract class ProcessAction(TAction model) : ProcessAction(model) where TAction : DialogAction +internal abstract class ProcessAction(TAction model) : + ProcessAction(model) + where TAction : DialogAction { public new TAction Model => (TAction)base.Model; } internal abstract class ProcessAction(DialogAction model) { - public ActionId Id => model.Id; + public string Id => model.Id.Value; + + public string ParentId { get; } = model.GetParentId(); public DialogAction Model => model; diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionWalker.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionWalker.cs new file mode 100644 index 000000000000..56f5d18a2035 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionWalker.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Bot.ObjectModel; + +namespace Microsoft.SemanticKernel.Process.Workflows; + +internal sealed class ProcessActionWalker : BotElementWalker +{ + private readonly ProcessActionVisitor _visitor; + + public ProcessActionWalker(BotElement rootElement, ProcessActionVisitor visitor) + { + this._visitor = visitor; + this.Visit(rootElement); + this._visitor.Complete(); + } + + public override bool DefaultVisit(BotElement definition) + { + if (definition is DialogAction action) + { + action.Accept(this._visitor); + } + + return true; + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessElementWalker.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessElementWalker.cs deleted file mode 100644 index ad5ecebbd501..000000000000 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessElementWalker.cs +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.ComponentModel; -using System.Threading.Tasks; -using Microsoft.Bot.ObjectModel; -using Microsoft.Bot.ObjectModel.Yaml; -using Microsoft.PowerFx.Types; - -namespace Microsoft.SemanticKernel.Process.Workflows; - -internal sealed class ProcessActionWalker : BotElementWalker -{ - private readonly ProcessActionVisitor _visitor; - - public ProcessActionWalker(ProcessBuilder processBuilder, string messageId, ProcessActionEnvironment processEnvironment) - { - this._visitor = CreateActionVisitor(processBuilder, messageId, processEnvironment); - } - - public void ProcessYaml(string yaml) - { - Console.WriteLine("### PARSING YAML"); - BotElement root = YamlSerializer.Deserialize(yaml) ?? throw new KernelException("Unable to parse YAML content."); - Console.WriteLine("### INTERPRETING MODEL"); - this.Visit(root); - this._visitor.Complete(); - Console.WriteLine("### PROCESS CREATED"); - } - - public override bool DefaultVisit(BotElement definition) - { - if (definition is DialogAction action) - { - action.Accept(this._visitor); - } - - return true; - } - - private static ProcessActionVisitor CreateActionVisitor(ProcessBuilder processBuilder, string messageId, ProcessActionEnvironment processEnvironment) - { - ProcessActionScopes scopes = new(); - - ProcessStepBuilder initStep = processBuilder.AddStepFromType(scopes, "init"); - - processBuilder.OnInputEvent(messageId).SendEventTo(new ProcessFunctionTargetBuilder(initStep)); - - return new ProcessActionVisitor(processBuilder, processEnvironment, initStep, scopes); - } - - private sealed class InitializeProcessStep : KernelProcessStep - { - private ProcessActionScopes? _scopes; - - public override ValueTask ActivateAsync(KernelProcessStepState state) - { - this._scopes = state.State; - -#if !NETCOREAPP - return new ValueTask(); -#else - return ValueTask.CompletedTask; -#endif - } - - [KernelFunction(KernelDelegateProcessStep.FunctionName)] - [Description("Initialize the process step")] - public void InitializeProcess(string message) - { - if (this._scopes == null) - { - throw new KernelException("Scopes have not been initialized. Ensure that the step is activated before calling this function."); - } - - Console.WriteLine("!!! INIT WORKFLOW"); - this._scopes.Set("LastMessage", ActionScopeType.System, StringValue.New(message)); // %%% MAGIC CONST "LastMessage" - } - } -} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ActionValidationFailure.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ActionValidationFailure.cs new file mode 100644 index 000000000000..4d7e321b9436 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ActionValidationFailure.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Bot.ObjectModel; + +namespace Microsoft.SemanticKernel.Process.Workflow.ObjectModel.Validation; + +/// +/// %%% COMMENT +/// +public class ActionValidationFailure : ElementValidationFailure +{ + internal ActionValidationFailure(DialogAction action, string message) + : base(action, message) + { + this.Id = action.Id.Value; + } + + /// + /// %%% COMMENT + /// + public string Id { get; } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ElementValidationFailure.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ElementValidationFailure.cs new file mode 100644 index 000000000000..75b18dec79af --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ElementValidationFailure.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Bot.ObjectModel; + +namespace Microsoft.SemanticKernel.Process.Workflow.ObjectModel.Validation; + +/// +/// %%% COMMENT +/// +public class ElementValidationFailure : ValidationFailure +{ + internal ElementValidationFailure(BotElement element, string message) + : base(message) + { + this.Kind = element.Kind; + this.StartPosition = element.Syntax?.Position; + this.EndPosition = element.Syntax?.EndPosition; + } + + /// + /// %%% COMMENT + /// + public BotElementKind Kind { get; } + + /// + /// %%% COMMENT + /// + public int? StartPosition { get; } + + /// + /// %%% COMMENT + /// + public int? EndPosition { get; } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ExceptionValidationFailure.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ExceptionValidationFailure.cs new file mode 100644 index 000000000000..508999463d9d --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ExceptionValidationFailure.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace Microsoft.SemanticKernel.Process.Workflow.ObjectModel.Validation; + +/// +/// %%% COMMENT +/// +public class ExceptionValidationFailure : ValidationFailure +{ + internal ExceptionValidationFailure(Exception exception, string message) + : base(message) + { + this.Exception = exception; + } + + /// + /// %%% COMMENT + /// + public Exception Exception { get; } + + /// + public override string ToString() => $"{this.Message} - {this.Exception.Message} [{this.Exception.GetType().Name}]"; +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ProcessValidationWalker.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ProcessValidationWalker.cs new file mode 100644 index 000000000000..9fd3db2301c7 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ProcessValidationWalker.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using Microsoft.Bot.ObjectModel; + +namespace Microsoft.SemanticKernel.Process.Workflow.ObjectModel.Validation; + +internal sealed class ProcessValidationWalker : BotElementWalker +{ + private readonly List _failures; + + public ProcessValidationWalker(BotElement rootElement) + { + this._failures = []; + this.Visit(rootElement); + this.Failures = this._failures.ToImmutableArray(); + } + + public bool IsValid => this.Failures.Length == 0; + + public ImmutableArray Failures { get; } + + public override bool DefaultVisit(BotElement definition) + { + Console.WriteLine($"> {definition.GetType().Name}"); + if (definition is UnknownBotElement) + { + this._failures.Add(new ElementValidationFailure(definition, "Unknown element")); + } + else if (definition is UnknownDialogAction unknownAction) + { + this._failures.Add(new ActionValidationFailure(unknownAction, "Unknown action")); + } + else if (definition is DialogAction action) + { + if (!action.HasRequiredProperties) + { + this._failures.Add(new ActionValidationFailure(action, "Missing required properties")); + } + } + + return true; + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ValidationFailure.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ValidationFailure.cs new file mode 100644 index 000000000000..d1669146360d --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ValidationFailure.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.SemanticKernel.Process.Workflow.ObjectModel.Validation; + +/// +/// %%% COMMENT +/// +public class ValidationFailure +{ + internal ValidationFailure(string message) + { + this.Message = message; + } + + /// + /// %%% COMMENT + /// + public string Message { get; } + + /// + public override string ToString() => this.Message; +} diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/WorkflowValidationTests.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/WorkflowValidationTests.cs new file mode 100644 index 000000000000..e681dc589447 --- /dev/null +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/WorkflowValidationTests.cs @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Immutable; +using System.IO; +using Microsoft.SemanticKernel.Process.Workflow.ObjectModel.Validation; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.SemanticKernel.Process.UnitTests.Workflows; + +public sealed class WorkflowValidationTests(ITestOutputHelper output) : WorkflowTest(output) +{ + [Fact] + public void VerifyInvalidStreamFailure() + { + using StringReader reader = new(Workflows.SimpleWorkflow); + reader.Close(); + ObjectModelBuilder.TryValidate(reader, out ImmutableArray failures); + DumpFailures(failures); + Assert.Single(failures); + Assert.IsType(failures[0]); + } + + [Theory] + [InlineData(nameof(Workflows.EmptyWorkflow))] + [InlineData(nameof(Workflows.OnlyComment))] + [InlineData(nameof(Workflows.JsonExpression))] + [InlineData(nameof(Workflows.InvalidYaml))] + public void VerifyDeserializationFailure(string invalidYaml) + { + ImmutableArray failures = Validate(invalidYaml, expectValid: false); + Assert.Single(failures); + Assert.IsType(failures[0]); + } + + [Theory] + [InlineData(nameof(Workflows.NotWorkflow), 1)] + public void VerifyDefinitionFailure(string invalidYaml, int expectedFailureCount) + { + ImmutableArray failures = Validate(invalidYaml, expectValid: false); + Assert.Equal(expectedFailureCount, failures.Length); + } + + private static ImmutableArray Validate(string workflowDefinition, bool expectValid = true) + { + using StringReader reader = new(GetWorkflowDefinition(workflowDefinition)); + bool isValid = ObjectModelBuilder.TryValidate(reader, out ImmutableArray failures); + Assert.Equal(expectValid, isValid); + DumpFailures(failures); + return failures; + } + + private static void DumpFailures(ImmutableArray failures) + { + if (failures.IsEmpty) + { + Console.WriteLine("# NO FAILURES"); + return; + } + + int index = 1; + foreach (ValidationFailure failure in failures) + { + Console.WriteLine($"# FAILURE {index}"); + Console.WriteLine($"[{failure.GetType().Name}] {failure}"); + ++index; + } + } + + private static string GetWorkflowDefinition(string workflowName) => + typeof(Workflows).GetField(workflowName)?.GetValue(null) as string ?? + throw new InvalidOperationException($"Unknown workflow definition: {workflowName}"); + + private static class Workflows + { + public const string EmptyWorkflow = + """ + + """; + + public const string OnlyComment = + """ + # This is a comment + """; + + public const string NotWorkflow = + """ + users: + - firstName: Alice + lastName: Brown + age: 61 + email: alice.brown@example.com + - firstName: Alice + lastName: Edwards + age: 44 + email: alice.edwards@example.com + """; + + public const string JsonExpression = + """ + { + "fistName": "Alice", + "lastName": "Brown", + "age": 61, + "email": "alice.brown@example.com" + } + """; + + public const string InvalidYaml = + """ + kind: AdaptiveDialog + beginDialog: + kind: OnActivity + id: activity_xyz123 + type: Message + actions: + - kind: SetVariable + id: setVariable_u4cBtN + displayName: Invocation count + variable: Topic.Count + value: 0 + - kind: SendActivity + id: sendActivity_aGsbRo + activity: Starting + """; + + public const string SimpleWorkflow = + """ + kind: AdaptiveDialog + beginDialog: + kind: OnActivity + id: activity_xyz123 + type: Message + actions: + - kind: SetVariable + id: setVariable_u4cBtN + displayName: Invocation count + variable: Topic.Count + value: 0 + + - kind: SendActivity + id: sendActivity_aGsbRo + activity: Starting + """; + } +} From c95f776538d889401303657954b1bb5d7fd6fb31 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 22 Jul 2025 21:30:53 -0700 Subject: [PATCH 19/40] Build checkpoint --- .../Workflow/ObjectModel/ObjectModelBuilder.cs | 6 +++--- .../Workflow/ObjectModel/ProcessActionVisitor.cs | 12 ++++++------ .../Actions/ClearAllVariablesActionTest.cs | 2 +- .../Workflow/Actions/ProcessActionTest.cs | 15 +++++++++++++++ .../Workflow/Actions/ResetVariableActionTest.cs | 2 +- .../Workflow/Actions/SendActivityActionTest.cs | 2 +- .../Workflow/Actions/SetTextVariableActionTest.cs | 2 +- .../Workflow/Actions/SetVariableActionTest.cs | 2 +- 8 files changed, 29 insertions(+), 14 deletions(-) diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs index 853eabaeb508..5562127dc76a 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs @@ -21,7 +21,7 @@ public static class ObjectModelBuilder /// /// %%% COMMENT /// - /// The reader that provides the the workflow object model YAML. + /// The reader that provides the workflow object model YAML. /// /// public static bool TryValidate(TextReader yamlReader, out ImmutableArray failures) @@ -58,7 +58,7 @@ static ImmutableArray SingleFailure(ValidationFailure failure /// /// Builds a process from the provided YAML definition of a CPS Topic ObjectModel. /// - /// The reader that provides the the workflow object model YAML. + /// The reader that provides the workflow object model YAML. /// The identifier for the message. /// The environment for the process actions. /// The that corresponds with the YAML object model. @@ -75,7 +75,7 @@ public static KernelProcess Build(TextReader yamlReader, string messageId, HostC processBuilder.OnInputEvent(messageId).SendEventTo(new ProcessFunctionTargetBuilder(initStep)); Console.WriteLine("@ INTERPRETING MODEL"); - ProcessActionVisitor visitor = new(processBuilder, environment ?? HostContext.Default, scopes); + ProcessActionVisitor visitor = new(processBuilder, environment ?? HostContext.Default, initStep, scopes); ProcessActionWalker walker = new(rootElement, visitor); Console.WriteLine("@ FINALIZING PROCESS"); diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs index af0f783d78e6..7e4ab6de33e9 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs @@ -14,7 +14,7 @@ internal sealed class ProcessActionVisitor : DialogActionVisitor { private readonly ProcessBuilder _processBuilder; private readonly ProcessStepBuilder _unhandledErrorStep; - private readonly ProcessActionEnvironment _environment; + private readonly HostContext _context; private readonly ProcessActionScopes _scopes; private readonly Dictionary _steps; private readonly Stack _contextStack; @@ -22,7 +22,7 @@ internal sealed class ProcessActionVisitor : DialogActionVisitor public ProcessActionVisitor( ProcessBuilder processBuilder, - ProcessActionEnvironment environment, + HostContext context, ProcessStepBuilder sourceStep, ProcessActionScopes scopes) { @@ -32,7 +32,7 @@ public ProcessActionVisitor( this._steps = []; this._linkCache = []; this._processBuilder = processBuilder; - this._environment = environment; + this._context = context; this._scopes = scopes; this._unhandledErrorStep = processBuilder.AddStepFromFunction( @@ -207,7 +207,7 @@ protected override void Visit(SendActivity item) { this.Trace(item, isSkipped: false); - this.AddAction(new SendActivityAction(item, this._environment.ActivityNotificationHandler)); + this.AddAction(new SendActivityAction(item, this._context.ActivityNotificationHandler)); } #region Not implemented @@ -434,7 +434,7 @@ private ProcessStepBuilder CreateActionStep(ProcessActionVisitorContext currentC { return this.InitializeStep( this._processBuilder.AddStepFromFunction( - action.Id.Value, + action.Id, async (kernel, context) => { Console.WriteLine($"!!! STEP {action.GetType().Name} [{action.Id}]"); // %%% DEVTRACE @@ -477,7 +477,7 @@ private void ContinueWith(ProcessStepBuilder newStep, KernelProcessEdgeCondition this.CurrentContext.Step = newStep; } - private RecalcEngine CreateEngine() => RecalcEngineFactory.Create(this._scopes, this._environment.MaximumExpressionLength); + private RecalcEngine CreateEngine() => RecalcEngineFactory.Create(this._scopes, this._context.MaximumExpressionLength); private void Trace(DialogAction item, bool isSkipped = true) { diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ClearAllVariablesActionTest.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ClearAllVariablesActionTest.cs index af3b1f5c2916..5b92dbd6d763 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ClearAllVariablesActionTest.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ClearAllVariablesActionTest.cs @@ -62,7 +62,7 @@ private ClearAllVariables CreateModel(string displayName, VariablesToClear varia Variables = VariablesToClearWrapper.Get(variableTarget), }; - ClearAllVariables model = actionBuilder.Build(); + ClearAllVariables model = this.AssignParent(actionBuilder); return model; } diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ProcessActionTest.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ProcessActionTest.cs index 391df60cbe89..5e05f87349f5 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ProcessActionTest.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ProcessActionTest.cs @@ -51,4 +51,19 @@ internal void VerifyUndefined(string variableName, ActionScopeType scope) { Assert.Throws(() => this.Scopes.Get(variableName, scope)); } + + protected TAction AssignParent(DialogAction.Builder actionBuilder) where TAction : DialogAction + { + OnActivity.Builder activityBuilder = + new() + { + Id = new("root"), + }; + + activityBuilder.Actions.Add(actionBuilder); + + OnActivity model = activityBuilder.Build(); + + return (TAction)model.Actions[0]; + } } diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ResetVariableActionTest.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ResetVariableActionTest.cs index 174aabd7b2f1..a6aeb551592e 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ResetVariableActionTest.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ResetVariableActionTest.cs @@ -62,7 +62,7 @@ private ResetVariable CreateModel(string displayName, string variablePath) Variable = InitializablePropertyPath.Create(variablePath), }; - ResetVariable model = actionBuilder.Build(); + ResetVariable model = this.AssignParent(actionBuilder); return model; } diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SendActivityActionTest.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SendActivityActionTest.cs index 00af5fc31418..82d4cffe88a5 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SendActivityActionTest.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SendActivityActionTest.cs @@ -50,7 +50,7 @@ private SendActivity CreateModel(string displayName, string activityMessage, str Activity = activityBuilder.Build(), }; - SendActivity model = actionBuilder.Build(); + SendActivity model = this.AssignParent(actionBuilder); return model; } diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SetTextVariableActionTest.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SetTextVariableActionTest.cs index 0b46c65c0fbb..bdcc8edef45b 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SetTextVariableActionTest.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SetTextVariableActionTest.cs @@ -65,7 +65,7 @@ private SetTextVariable CreateModel(string displayName, string variablePath, str Value = TemplateLine.Parse(textValue), }; - SetTextVariable model = actionBuilder.Build(); + SetTextVariable model = this.AssignParent(actionBuilder); return model; } diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SetVariableActionTest.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SetVariableActionTest.cs index 43afc100b0d3..82b33a89b076 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SetVariableActionTest.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SetVariableActionTest.cs @@ -204,7 +204,7 @@ private SetVariable CreateModel(string displayName, string variablePath, ValueEx Value = valueExpression, }; - SetVariable model = actionBuilder.Build(); + SetVariable model = this.AssignParent(actionBuilder); return model; } From 0a233d3a56d2c82cbcc7dfc26ecf1c675e313fee Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 22 Jul 2025 22:00:38 -0700 Subject: [PATCH 20/40] ClearAllVariables experiment --- .../Workflow/ObjectModel/Actions/ClearAllVariablesAction.cs | 2 +- .../Workflow/Actions/ClearAllVariablesActionTest.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ClearAllVariablesAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ClearAllVariablesAction.cs index 7e96fb5438b3..c49563c9a241 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ClearAllVariablesAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ClearAllVariablesAction.cs @@ -19,7 +19,7 @@ protected override Task HandleAsync(ProcessActionContext context, CancellationTo if (literalValue is RecordDataValue recordValue) { - //recordValue.Properties; // %%% TODO ?!?!!?! + // %%% TODO ?!?!!?! } return Task.CompletedTask; diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ClearAllVariablesActionTest.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ClearAllVariablesActionTest.cs index 5b92dbd6d763..c2445bef46ac 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ClearAllVariablesActionTest.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ClearAllVariablesActionTest.cs @@ -59,7 +59,7 @@ private ClearAllVariables CreateModel(string displayName, VariablesToClear varia { Id = this.CreateActionId(), DisplayName = this.FormatDisplayName(displayName), - Variables = VariablesToClearWrapper.Get(variableTarget), + Variables = EnumExpression.Literal(VariablesToClearWrapper.Get(variableTarget)), }; ClearAllVariables model = this.AssignParent(actionBuilder); From a47e34419eb22e011be4b742c11a6ab39ee68fde Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 23 Jul 2025 09:22:15 -0700 Subject: [PATCH 21/40] Requirements --- .../ObjectModel/requirements_objectmodel.md | 116 +++++++++++++----- 1 file changed, 83 insertions(+), 33 deletions(-) diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/requirements_objectmodel.md b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/requirements_objectmodel.md index d71e687ee23d..883a839d9d39 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/requirements_objectmodel.md +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/requirements_objectmodel.md @@ -1,5 +1,53 @@ # Object Model Requirements +## Behaviors + +### Is a _Foundry_ workflow specific to a single _Foundry_ project? + +Always. +This implies that a _Foundry_ workflow has access to the resources associated with its project, +including: models, agents, and connections. + +### Can user defined workflow YAML be uploaded to a _Foundry_ project? + +Yes. +As _VS Code_ extension will support the authoring of _Foundry_ workflows in YAML. +The resulting YAML can be uploaded to a _Foundry_ project. +Ostensibly, one could directly author raw YAML and upload. + + +### Can a Foundry workflow be hosted in different projects without modification? + +Agent identifiers differ across projects even if model deployments match. +This creates an incompatibility between projects, +exception in the case where an agent is identified by name only. + + +### Can a _Copilot Studio_ workflow be hosted in _Foundry_ or vice-versa? + +Since the actions diverge between what is supported for _Copilot Studio_ and _Foundry_, +interoperability between platforms is not generally supported. +The special case where a workflow contains only the core actions that are supported by both platforms +might allow for interoperablity. + +### How and when are declarative workflows (YAML) validated? + +Validating a declarative workflow prior to execution provides a superior user experience. +This validation should be triggered for any workflow update: +- Uploading a YAML file that creates or overwrites a workflow. +- Creating or editing a workflow from the designer. + +Validation must include: +- Schema validation: + Can the YAML be deserialized and parsed? +- Functional validation: + Does the YAML consist of only the actions supported by the _Foundry_ object model? + Is each action properly defined with valid identifiers? +- Reference validation: + Are all referenced resources (models, agents, connections) valid and accessible? + +At runtime the references shall be re-validated and, where relevant, logical names translated into physical identifiers. + ## **Types** - ✔ **Message** @@ -75,50 +123,52 @@ UnknownDialogAction|✔|❌||Serialization construct that represents an unknown UpdateActivity|✔|❓|❓|How does an _Activity_ differ from a _Message_? WaitForConnectorTrigger|✔|❓||Not supported for v0. Evaluate trigger actions in next phase. -## Behaviors +## **Definitions** -### Is a _Foundry_ workflow specific to a single _Foundry_ project? +The following definitions are used to describe the actions specific to _Foundry_ workflows. -Always. -This implies that a _Foundry_ workflow has access to the resources associated with its project, -including: models, agents, and connections. +### `InvokeAgent` -### Can user defined workflow YAML be uploaded to a _Foundry_ project? +Agent may be idenfiied by name, identifier, or both. +When identifier is provided, it take precedence. -Yes. -As _VS Code_ extension will support the authoring of _Foundry_ workflows in YAML. -The resulting YAML can be uploaded to a _Foundry_ project. -Ostensibly, one could directly author raw YAML and upload. +> What other parameters or options are supported? +```yaml +- kind: InvokeAgent + id: invokeAgent_u4cBtN + name: Fred +``` -### Can a Foundry workflow be hosted in different projects without modification? +```yaml +- kind: InvokeAgent + id: invokeAgent_u4cBtN + agentid: asst_ymran0gQaXGqyG0QZ4f1Yqxi +``` -Agent identifiers differ across projects even if model deployments match. -This creates an incompatibility between projects, -exception in the case where an agent is identified by name only. +```yaml +- kind: InvokeAgent + id: invokeAgent_u4cBtN + name: Fred + agentid: asst_ymran0gQaXGqyG0QZ4f1Yqxi +``` +### `InvokeChatCompletion` -### Can a _Copilot Studio_ workflow be hosted in _Foundry_ or vice-versa? +A chat-completion request requires that the target model is specified. -Since the actions diverge between what is supported for _Copilot Studio_ and _Foundry_, -interoperability between platforms is not generally supported. -The special case where a workflow contains only the core actions that are supported by both platforms -might allow for interoperablity. +> What other parameters or options are supported? -### How and when are declarative workflows (YAML) validated? +```yaml +- kind: InvokeChatCompletion + id: invokeModel_u4cBtN + model: gpt-4.1-mini +``` -Validating a declarative workflow prior to execution provides a superior user experience. -This validation should be triggered for any workflow update: -- Uploading a YAML file that creates or overwrites a workflow. -- Creating or editing a workflow from the designer. +### `InvokeResponse` -Validation must include: -- Schema validation: - Can the YAML be deserialized and parsed? -- Functional validation: - Does the YAML consist of only the actions supported by the _Foundry_ object model? - Is each action properly defined with valid identifiers? -- Reference validation: - Are all referenced resources (models, agents, connections) valid and accessible? +> TBD -At runtime the references shall be re-validated and, where relevant, logical names translated into physical identifiers. +### `InvokeTool` + +> TBD \ No newline at end of file From bb597d12d2ee52a26ac4c974b5de1442b9a9c53f Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 23 Jul 2025 16:06:37 -0700 Subject: [PATCH 22/40] Checkpoint --- .../Step06/Step06_WorkflowProcess.cs | 2 + .../Step06/testEnd.yaml | 18 ++ .../Step06/testGoto.yaml | 45 ++++ .../Step06/testLoop.yaml | 7 +- .../Process.Abstractions/KernelProcessEdge.cs | 4 +- .../Process.Core/ProcessDelegateBuilder.cs | 5 +- .../Process.Core/ProcessStepEdgeBuilder.cs | 5 +- .../ObjectModel/Actions/AssignmentAction.cs | 2 +- .../ObjectModel/Actions/BeginDialogAction.cs | 21 -- .../Actions/ClearAllVariablesAction.cs | 5 +- .../ObjectModel/Actions/EditTableV2Action.cs | 2 +- .../ObjectModel/Actions/ForeachAction.cs | 38 +++ .../Actions/ResetVariableAction.cs | 2 +- .../Exceptions/UnknownDataTypeException.cs | 35 +++ .../Extensions/DataValueExtensions.cs | 4 +- .../Extensions/FormulaValueExtensions.cs | 1 + .../Extensions/RecordDataTypeExtensions.cs | 4 +- .../Workflow/ObjectModel/HostContext.cs | 4 +- .../ObjectModel/ObjectModelBuilder.cs | 2 +- .../PowerFx/RecalcEngineExtensions.cs | 2 +- .../Workflow/ObjectModel/ProcessAction.cs | 7 +- .../ObjectModel/ProcessActionStack.cs | 46 ++++ .../ObjectModel/ProcessActionVisitor.cs | 251 ++++++++---------- .../ProcessActionVisitorContext.cs | 43 --- .../ObjectModel/ProcessWorkflowBuilder.cs | 129 +++++++++ .../Validation/ActionValidationFailure.cs | 9 +- .../Validation/ElementValidationFailure.cs | 13 +- .../Validation/ExceptionValidationFailure.cs | 9 +- .../Validation/ValidationFailure.cs | 4 +- .../Process.LocalRuntime/LocalDelegateStep.cs | 2 +- .../Process.LocalRuntime/LocalProcess.cs | 19 +- .../Workflow/ProcessActionScopesTests.cs | 1 - .../Workflow/ProcessActionStackTests.cs | 122 +++++++++ 33 files changed, 612 insertions(+), 251 deletions(-) create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step06/testEnd.yaml create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step06/testGoto.yaml delete mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/BeginDialogAction.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/UnknownDataTypeException.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionStack.cs delete mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitorContext.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessWorkflowBuilder.cs create mode 100644 dotnet/src/Experimental/Process.UnitTests/Workflow/ProcessActionStackTests.cs diff --git a/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs b/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs index 4d6db6c43474..cddf57952370 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs @@ -13,6 +13,8 @@ public Step06_WorkflowProcess(ITestOutputHelper output) : base(output, redirectSystemConsoleOutput: true) { } [Theory] + [InlineData("testEnd")] + [InlineData("testGoto")] [InlineData("testLoop")] [InlineData("testCondition")] [InlineData("deepResearch")] diff --git a/dotnet/samples/GettingStartedWithProcesses/Step06/testEnd.yaml b/dotnet/samples/GettingStartedWithProcesses/Step06/testEnd.yaml new file mode 100644 index 000000000000..5c6e1b7ac96b --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step06/testEnd.yaml @@ -0,0 +1,18 @@ +kind: AdaptiveDialog +beginDialog: + kind: OnActivity + id: activity_xyz123 + type: Message + actions: + + - kind: SendActivity + id: sendActivity_aGsbRo + activity: Starting + + - kind: EndConversation + id: end_skJ8u + actionId: sendActivity_fJsbRz + + - kind: SendActivity + id: sendActivity_SVoNSV + activity: Never diff --git a/dotnet/samples/GettingStartedWithProcesses/Step06/testGoto.yaml b/dotnet/samples/GettingStartedWithProcesses/Step06/testGoto.yaml new file mode 100644 index 000000000000..99caa8e7f2b8 --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step06/testGoto.yaml @@ -0,0 +1,45 @@ +kind: AdaptiveDialog +beginDialog: + kind: OnActivity + id: activity_xyz123 + type: Message + actions: + + - kind: SendActivity + id: sendActivity_aGsbRo + activity: First + + - kind: GotoAction + id: goto_skJ8u + actionId: sendActivity_fJsbRz + + - kind: SendActivity + id: sendActivity_nev1 + activity: NEVER! + + - kind: SendActivity + id: sendActivity_SVoNSV + activity: Last + + - kind: GotoAction + id: goto_SVoNSV + actionId: end_SVoNSV + + - kind: SendActivity + id: sendActivity_nev2 + activity: NEVER! + + - kind: SendActivity + id: sendActivity_fJsbRz + activity: Next + + - kind: GotoAction + id: goto_ajd01z + actionId: sendActivity_SVoNSV + + - kind: SendActivity + id: sendActivity_nev3 + activity: NEVER! + + - kind: EndConversation + id: end_SVoNSV diff --git a/dotnet/samples/GettingStartedWithProcesses/Step06/testLoop.yaml b/dotnet/samples/GettingStartedWithProcesses/Step06/testLoop.yaml index c2034d4d9126..6be5bba164c7 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step06/testLoop.yaml +++ b/dotnet/samples/GettingStartedWithProcesses/Step06/testLoop.yaml @@ -16,8 +16,9 @@ beginDialog: - kind: Foreach id: foreach_mVIecC - items: ["a", "b", "c", "d", "e", "f"] - variable: Topic.LoopValue + items: =["a", "b", "c", "d", "e", "f"] + index: Topic.LoopIndex + value: Topic.LoopValue actions: - kind: SetVariable id: setVariable_A4iBtN @@ -26,7 +27,7 @@ beginDialog: value: =Topic.Count + 1 - kind: SendActivity id: sendActivity_Pkkmpq - activity: Looping (x{Topic.Count}) - {Topic.LoopValue} + activity: Looping (x{Topic.Count}) - {Topic.LoopValue} [{Topic.LoopIndex}] - kind: SendActivity id: sendActivity_fJsbRz diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessEdge.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessEdge.cs index 403fa2537d2e..df1743e019c2 100644 --- a/dotnet/src/Experimental/Process.Abstractions/KernelProcessEdge.cs +++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessEdge.cs @@ -33,7 +33,7 @@ public sealed class KernelProcessEdge /// /// The condition that must be met for the edge to be activated. /// - public KernelProcessEdgeCondition Condition { get; init; } + public KernelProcessEdgeCondition? Condition { get; init; } /// /// The list of variable updates to be performed when the edge fires. @@ -51,7 +51,7 @@ public KernelProcessEdge(string sourceStepId, KernelProcessTarget outputTarget, this.SourceStepId = sourceStepId; this.OutputTarget = outputTarget; this.GroupId = groupId; - this.Condition = condition ?? new KernelProcessEdgeCondition(callback: (_, _) => Task.FromResult(true)); + this.Condition = condition; //this.Metadata = metadata ?? []; this.Update = update; } diff --git a/dotnet/src/Experimental/Process.Core/ProcessDelegateBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessDelegateBuilder.cs index 508f583f8a19..83583605658c 100644 --- a/dotnet/src/Experimental/Process.Core/ProcessDelegateBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/ProcessDelegateBuilder.cs @@ -17,9 +17,6 @@ public class ProcessDelegateBuilder : ProcessStepBuilder /// /// Initializes a new instance of the class. /// - /// - /// - /// public ProcessDelegateBuilder(string id, StepFunction stepFunction, ProcessBuilder? processBuilder) : base(id, processBuilder) { this._stepFunction = stepFunction ?? throw new ArgumentNullException(nameof(stepFunction), "Step function cannot be null."); @@ -30,7 +27,7 @@ internal override KernelProcessStepInfo BuildStep(ProcessBuilder processBuilder, // Build the edges first var builtEdges = this.Edges.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Select(e => e.Build()).ToList()); - KernelProcessStepState stateObject = new(this.Name, "none", this.Id); // %%% VERSION IS NOT USED, BUT REQUIRED BY THE BASE CLASS + KernelProcessStepState stateObject = new(this.Name, "none", this.Id); return new KernelProcessDelegateStepInfo(stateObject, this._stepFunction, builtEdges); } diff --git a/dotnet/src/Experimental/Process.Core/ProcessStepEdgeBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessStepEdgeBuilder.cs index d37350743b23..d00713472501 100644 --- a/dotnet/src/Experimental/Process.Core/ProcessStepEdgeBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/ProcessStepEdgeBuilder.cs @@ -97,9 +97,8 @@ public ProcessStepEdgeBuilder SendEventTo(ProcessTargetBuilder target) /// /// /// - public ProcessStepEdgeBuilder OnCondition(KernelProcessEdgeCondition condition) + public ProcessStepEdgeBuilder OnCondition(KernelProcessEdgeCondition? condition) { - Verify.NotNull(condition, nameof(condition)); this.Condition = condition; return this; } @@ -129,7 +128,7 @@ internal virtual ProcessStepEdgeBuilder SendEventTo_Internal(ProcessTargetBuilde this.Target = target; this.Source.LinkTo(this.EventData.EventId, this); - return new ProcessStepEdgeBuilder(this.Source, this.EventData.EventId, this.EventData.EventName, this.EdgeGroupBuilder, this.Condition); + return this; } /// diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AssignmentAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AssignmentAction.cs index 1972c5e694a5..18ecbbe4f744 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AssignmentAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AssignmentAction.cs @@ -34,7 +34,7 @@ protected void AssignTarget(ProcessActionContext context, FormulaValue result) context.Engine.SetScopedVariable(context.Scopes, ActionScopeType.Parse(this.Target.VariableScopeName), this.Target.VariableName!, result); string? resultValue = result.Format(); string valuePosition = (resultValue?.IndexOf('\n') ?? -1) >= 0 ? Environment.NewLine : " "; - Console.WriteLine( // %%% DEVTRACE + Console.WriteLine( // %%% LOGGER $""" !!! ASSIGN {this.GetType().Name} [{this.Id}] NAME: {this.Target.Format()} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/BeginDialogAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/BeginDialogAction.cs deleted file mode 100644 index 29f6e06042cf..000000000000 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/BeginDialogAction.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Bot.ObjectModel; - -namespace Microsoft.SemanticKernel.Process.Workflows.Actions; - -internal sealed class BeginDialogAction : ProcessAction -{ - public BeginDialogAction(BeginDialog model) - : base(model) - { - } - - protected override Task HandleAsync(ProcessActionContext context, CancellationToken cancellationToken) - { - // %%% TODO - return Task.CompletedTask; - } -} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ClearAllVariablesAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ClearAllVariablesAction.cs index c49563c9a241..8332d01b2b6b 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ClearAllVariablesAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ClearAllVariablesAction.cs @@ -17,10 +17,7 @@ protected override Task HandleAsync(ProcessActionContext context, CancellationTo { DataValue literalValue = this.Model.Variables.GetLiteralValue(); - if (literalValue is RecordDataValue recordValue) - { - // %%% TODO ?!?!!?! - } + // %%% TODO: VariablesToClear ?!?!!?! return Task.CompletedTask; } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EditTableV2Action.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EditTableV2Action.cs index 6cfe938001eb..e88ef53a3a31 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EditTableV2Action.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EditTableV2Action.cs @@ -46,7 +46,7 @@ static RecordValue BuildRecord(RecordType recordType, FormulaValue value) IEnumerable GetValues() { - // %%% expression.StructuredRecordExpression.Properties ??? + // %%% TODO: expression.StructuredRecordExpression.Properties ??? foreach (NamedFormulaType fieldType in recordType.GetFieldTypes()) { if (value is RecordValue recordValue) diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ForeachAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ForeachAction.cs index ad41791f10e6..0211447b1e45 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ForeachAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ForeachAction.cs @@ -1,20 +1,58 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx.Types; +using Microsoft.SemanticKernel.Process.Workflows.PowerFx; namespace Microsoft.SemanticKernel.Process.Workflows.Actions; internal sealed class ForeachAction : ProcessAction { + private int _index; + private FormulaValue[] _values; + public ForeachAction(Foreach model) : base(model) { + this._values = []; } + public bool HasValue { get; private set; } + protected override Task HandleAsync(ProcessActionContext context, CancellationToken cancellationToken) { + // %%% TODO: HACK: Assumes array + this._index = 0; + FormulaValue values = context.Engine.EvaluateExpression(this.Model.Items); + TableValue tableValue = (TableValue)values; + this._values = [.. tableValue.Rows.Select(row => row.Value.Fields.First().Value)]; return Task.CompletedTask; } + + public void TakeNext(ProcessActionContext context) + { + if (this.HasValue = (this._index < this._values.Length)) + { + FormulaValue value = this._values[this._index]; + this._index++; + + context.Engine.SetScopedVariable( + context.Scopes, + ActionScopeType.Parse(this.Model.Value!.Path.VariableScopeName), // %%% NULL OVERRIDE + this.Model.Value.Path.VariableName!, + value); + + if (this.Model.Index != null) + { + context.Engine.SetScopedVariable( + context.Scopes, + ActionScopeType.Parse(this.Model.Index.Path.VariableScopeName), + this.Model.Index.Path.VariableName!, + FormulaValue.New(this._index)); + } + } + } } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ResetVariableAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ResetVariableAction.cs index 22f56d681c1a..3996b751d8ec 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ResetVariableAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ResetVariableAction.cs @@ -19,7 +19,7 @@ public ResetVariableAction(ResetVariable model) protected override Task HandleAsync(ProcessActionContext context, CancellationToken cancellationToken) { context.Engine.ClearScopedVariable(context.Scopes, ActionScopeType.Parse(this.Target.VariableScopeName), this.Target.VariableName!); - Console.WriteLine( // %%% DEVTRACE + Console.WriteLine( // %%% LOGGER $""" !!! CLEAR {this.GetType().Name} [{this.Id}] NAME: {this.Model.Variable!.Format()} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/UnknownDataTypeException.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/UnknownDataTypeException.cs new file mode 100644 index 000000000000..065ac6c222df --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/UnknownDataTypeException.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace Microsoft.SemanticKernel.Process.Workflows; + +/// +/// Represents an exception that occurs when an unknown data type is encountered. +/// +public sealed class UnknownDataTypeException : ProcessWorkflowException +{ + /// + /// Initializes a new instance of the class. + /// + public UnknownDataTypeException() + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The error message that explains the reason for the exception. + public UnknownDataTypeException(string? message) : base(message) + { + } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public UnknownDataTypeException(string? message, Exception? innerException) : base(message, innerException) + { + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/DataValueExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/DataValueExtensions.cs index cbe356b77d40..b7360b7621cd 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/DataValueExtensions.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/DataValueExtensions.cs @@ -17,8 +17,8 @@ public static FormulaValue ToFormulaValue(this DataValue? value) => DateTimeDataValue dateTimeValue => FormulaValue.New(dateTimeValue.Value.DateTime), DateDataValue dateValue => FormulaValue.New(dateValue.Value), TimeDataValue timeValue => FormulaValue.New(timeValue.Value), - //RecordDataValue recordValue => FormulaValue.NewRecordFromFields(recordValue.Properties), // %%% TODO - //TableDataValue tableValue => FormulaValue.NewTable(), // %%% TODO + //RecordDataValue recordValue => FormulaValue.NewRecordFromFields(recordValue.Properties), // %%% SUPPORT + //TableDataValue tableValue => FormulaValue.NewTable(), // %%% SUPPORT _ => FormulaValue.NewError(new Microsoft.PowerFx.ExpressionError { Message = $"Unknown literal type: {value.GetType().Name}" }), }; } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/FormulaValueExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/FormulaValueExtensions.cs index 80efaf44d89a..d90c047adfb9 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/FormulaValueExtensions.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/FormulaValueExtensions.cs @@ -55,6 +55,7 @@ public static FormulaValue ThrowIfError(this FormulaValue value) // %%% TODO: Type conversion //VoidValue //NamedValue + //TableValue //BlobValue //ErrorValue //ColorValue diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/RecordDataTypeExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/RecordDataTypeExtensions.cs index 1ccbc6405b30..e06d92dbc171 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/RecordDataTypeExtensions.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/RecordDataTypeExtensions.cs @@ -28,8 +28,8 @@ IEnumerable ParseValues() DateDataType dateType => DateValue.New(propertyElement.GetDateTime()), TimeDataType timeType => TimeValue.New(propertyElement.GetDateTimeOffset().TimeOfDay), RecordDataType recordType => recordType.ParseRecord(propertyElement), - //TableDataValue tableValue => // %%% TODO - _ => throw new InvalidActionException($"Unsupported data type '{property.Value.Type}' for property '{property.Key}'") // %%% EXCEPTION TYPE & MESSAGE + //TableDataValue tableValue => // %%% SUPPORT + _ => throw new UnknownDataTypeException($"Unsupported data type '{property.Value.Type}' for property '{property.Key}'") }; yield return new NamedValue(property.Key, parsedValue); } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/HostContext.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/HostContext.cs index 6e7c4eb9d8cb..41c09bc667a6 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/HostContext.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/HostContext.cs @@ -45,11 +45,11 @@ public Task ActivityNotificationHandler(ActivityTemplateBase activity, RecalcEng { if (!string.IsNullOrEmpty(messageActivity.Summary)) { - Console.WriteLine($"\t{messageActivity.Summary}"); // %%% DEVTRACE + Console.WriteLine($"\t{messageActivity.Summary}"); // %%% LOGGER } string? activityText = engine.Format(messageActivity.Text); - Console.WriteLine(activityText + Environment.NewLine); // %%% DEVTRACE + Console.WriteLine(activityText + Environment.NewLine); // %%% LOGGER } return Task.CompletedTask; diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs index 5562127dc76a..f6087aefd7e8 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs @@ -75,7 +75,7 @@ public static KernelProcess Build(TextReader yamlReader, string messageId, HostC processBuilder.OnInputEvent(messageId).SendEventTo(new ProcessFunctionTargetBuilder(initStep)); Console.WriteLine("@ INTERPRETING MODEL"); - ProcessActionVisitor visitor = new(processBuilder, environment ?? HostContext.Default, initStep, scopes); + ProcessActionVisitor visitor = new(processBuilder, environment ?? HostContext.Default, scopes); ProcessActionWalker walker = new(rootElement, visitor); Console.WriteLine("@ FINALIZING PROCESS"); diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs index bc7616023b9f..a269b47941f9 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs @@ -12,7 +12,7 @@ internal static class RecalcEngineExtensions public static void ClearScopedVariable(this RecalcEngine engine, ProcessActionScopes scopes, ActionScopeType scope, string varName) { // Validate inputs and assign value. - scopes.Remove(varName, scope); // %%% CONSIDER: SET TO BLANK ??? + scopes.Remove(varName, scope); // Rebuild scope record and update engine RecordValue scopeRecord = scopes.BuildRecord(scope); diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessAction.cs index 1fec12c8e778..c55f29e9dda5 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessAction.cs @@ -4,12 +4,13 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; +using Microsoft.Extensions.Logging; using Microsoft.PowerFx; using Microsoft.SemanticKernel.Process.Workflows.Extensions; namespace Microsoft.SemanticKernel.Process.Workflows; -internal sealed record class ProcessActionContext(RecalcEngine Engine, ProcessActionScopes Scopes, Kernel Kernel); +internal sealed record class ProcessActionContext(RecalcEngine Engine, ProcessActionScopes Scopes, Kernel Kernel, ILogger logger); internal abstract class ProcessAction(TAction model) : ProcessAction(model) @@ -37,12 +38,12 @@ public async Task ExecuteAsync(ProcessActionContext context, CancellationToken c } catch (ProcessWorkflowException exception) { - Console.WriteLine($"*** ACTION [{this.Id}] ERROR - {exception.GetType().Name}\n{exception.Message}"); // %%% DEVTRACE + Console.WriteLine($"*** ACTION [{this.Id}] ERROR - {exception.GetType().Name}\n{exception.Message}"); // %%% LOGGER throw; } catch (Exception exception) { - Console.WriteLine($"*** ACTION [{this.Id}] ERROR - {exception.GetType().Name}\n{exception.Message}"); // %%% DEVTRACE + Console.WriteLine($"*** ACTION [{this.Id}] ERROR - {exception.GetType().Name}\n{exception.Message}"); // %%% LOGGER throw new ProcessWorkflowException($"Unexpected failure executing action #{this.Id} [{this.GetType().Name}]", exception); } } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionStack.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionStack.cs new file mode 100644 index 000000000000..16d00c1b1796 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionStack.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; + +namespace Microsoft.SemanticKernel.Process.Workflows; + +internal delegate void ScopeCompletionAction(string scopeId); // %%% NEEDED: scopeId ??? + +internal sealed class ProcessActionStack +{ + private readonly Stack _actionStack = []; + private readonly Dictionary _actionScopes = []; + + public string CurrentScope => + this._actionStack.Count > 0 ? + this._actionStack.Peek() : + throw new InvalidOperationException("No scope defined"); // %%% EXCEPTION TYPE + + public void Recognize(string scopeId, ScopeCompletionAction? callback = null) + { +#if NET + if (this._actionScopes.TryAdd(scopeId, callback)) + { +#else + if (!this._actionScopes.ContainsKey(scopeId)) + { + this._actionScopes[scopeId] = callback; +#endif + // If the scope is new, push it onto the stack + this._actionStack.Push(scopeId); + } + else + { + // Otherwise, unwind the stack to the given scope + string currentScopeId; + while ((currentScopeId = this.CurrentScope) != scopeId) + { + ScopeCompletionAction? unwoundCallback = this._actionScopes[currentScopeId]; + unwoundCallback?.Invoke(currentScopeId); + this._actionStack.Pop(); + this._actionScopes.Remove(currentScopeId); + } + } + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs index 7e4ab6de33e9..e9ab07f10bf6 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs @@ -1,11 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.PowerFx; using Microsoft.SemanticKernel.Process.Workflows.Actions; +using Microsoft.SemanticKernel.Process.Workflows.Extensions; using Microsoft.SemanticKernel.Process.Workflows.PowerFx; namespace Microsoft.SemanticKernel.Process.Workflows; @@ -13,127 +15,116 @@ namespace Microsoft.SemanticKernel.Process.Workflows; internal sealed class ProcessActionVisitor : DialogActionVisitor { private readonly ProcessBuilder _processBuilder; - private readonly ProcessStepBuilder _unhandledErrorStep; + private readonly ProcessWorkflowBuilder _workflowBuilder; + private readonly ProcessActionStack _actionStack; private readonly HostContext _context; private readonly ProcessActionScopes _scopes; - private readonly Dictionary _steps; - private readonly Stack _contextStack; - private readonly List<(ActionId TargetId, ProcessStepEdgeBuilder SourceEdge)> _linkCache; public ProcessActionVisitor( ProcessBuilder processBuilder, HostContext context, - ProcessStepBuilder sourceStep, ProcessActionScopes scopes) { - ProcessActionVisitorContext rootContext = new(sourceStep); - this._contextStack = []; - this._contextStack.Push(rootContext); - this._steps = []; - this._linkCache = []; + this._actionStack = new ProcessActionStack(); + this._workflowBuilder = new ProcessWorkflowBuilder(processBuilder.Steps.Single()); this._processBuilder = processBuilder; this._context = context; this._scopes = scopes; - this._unhandledErrorStep = - processBuilder.AddStepFromFunction( - $"{processBuilder.Name}_unhandled_error", - (kernel, context) => - { - // Handle unhandled errors here - Console.WriteLine("*** PROCESS ERROR - Unhandled error"); // %%% DEVTRACE - return Task.CompletedTask; - }); } public void Complete() { - // Close the current context - this.CurrentContext.Then().StopProcess(); - // Process the cached links - foreach ((ActionId targetId, ProcessStepEdgeBuilder sourceEdge) in this._linkCache) - { - // Link the queued context to the step - ProcessStepBuilder step = this._steps[targetId]; // %%% TRY - Console.WriteLine($"> CONNECTING {sourceEdge.Source.Id} => {targetId}"); - sourceEdge.SendEventTo(new ProcessFunctionTargetBuilder(step)); - } - this._linkCache.Clear(); - - // Visitor is complete, all actions have been processed - Console.WriteLine("> COMPLETE"); // %%% DEVTRACE + this._workflowBuilder.ConnectNodes(); } - private ProcessActionVisitorContext CurrentContext => this._contextStack.Peek(); - protected override void Visit(ActionScope item) { - this.Trace(item, isSkipped: false); + this.Trace(item, isSkipped: true); - this.AddContainer(item.Id.Value); + //string parentId = item.GetParentId(); + //this._workflowBuilder.AddNode(this.CreateEmptyStep(item.Id.Value), parentId); + //this._workflowBuilder.AddLink(parentId, item.Id.Value); } protected override void Visit(ConditionGroup item) { this.Trace(item, isSkipped: false); - this.AddAction(new ConditionGroupAction(item)); + this.ContinueWith(new ConditionGroupAction(item)); + + // %%% SUPPORT: item.ElseActions - // Visit each action in the condition group int index = 1; foreach (ConditionItem conditionItem in item.Conditions) { - ProcessStepBuilder step = this.CreateContainerStep(this.CurrentContext, conditionItem.Id ?? $"{item.Id.Value}_item{index}"); - this._contextStack.Push(new ProcessActionVisitorContext(step)); + //KernelProcessEdgeCondition? condition = null; - conditionItem.Accept(this); + //if (conditionItem.Condition is not null) + //{ + // // %%% VERIFY IF ONLY ONE CONDITION IS EXPECTED / ALLOWED + // condition = + // new((stepEvent, state) => + // { + // RecalcEngine engine = this.CreateEngine(); + // bool result = engine.Eval(conditionItem.Condition.ExpressionText ?? "true").AsBoolean(); + // Console.WriteLine($"!!! CONDITION: {conditionItem.Condition.ExpressionText ?? "true"}={result}"); + // return Task.FromResult(result); + // }); + //} - ProcessActionVisitorContext conditionContext = this._contextStack.Pop(); - KernelProcessEdgeCondition? condition = null; + ////this.AddScope(conditionItem.Id ?? $"{item.Id.Value}_item{index}", condition); - if (conditionItem.Condition is not null) - { - // %%% VERIFY IF ONLY ONE CONDITION IS EXPECTED / ALLOWED - condition = - new((stepEvent, state) => - { - RecalcEngine engine = this.CreateEngine(); - bool result = engine.Eval(conditionItem.Condition.ExpressionText ?? "true").AsBoolean(); - Console.WriteLine($"!!! CONDITION: {conditionItem.Condition.ExpressionText ?? "true"}={result}"); - return Task.FromResult(result); - }); - } + //// Visit each action in the condition item + //conditionItem.Accept(this); - this.CurrentContext.Then(conditionContext.Step, condition); + ////this._workflowBuilder.RemoveScope(); ++index; } } + public override void VisitConditionItem(ConditionItem item) + { + Console.WriteLine($"###### ITEM {item.Id}"); + base.VisitConditionItem(item); // %%% + } + protected override void Visit(GotoAction item) { this.Trace(item, isSkipped: false); - this.AddContainer(item.Id.Value); - // Store the link for processing after all actions have steps. - this._linkCache.Add((item.ActionId, this.CurrentContext.Then())); // %%% DRY - // Create an orphaned context for continuity - this.AddDead(item.Id.Value); + string parentId = item.GetParentId(); + this.ContinueWith(this.CreateStep(item.Id.Value), parentId); + this._workflowBuilder.AddLink(item.Id.Value, item.ActionId.Value); + this.RestartFrom(item.Id.Value, parentId); } protected override void Visit(Foreach item) { - this.Trace(item); + this.Trace(item, isSkipped: false); - this.AddAction(new ForeachAction(item)); + ForeachAction action = new(item); + this.ContinueWith(action); + string restartId = this.RestartFrom(action); + string loopId = $"next_{action.Id}"; + this.ContinueWith(this.CreateStep(loopId, action.TakeNext), action.Id, callback: CompletionHandler); + this._workflowBuilder.AddLink(loopId, restartId, () => !action.HasValue); + this.ContinueWith(this.CreateStep($"start_{action.Id}"), action.Id, () => action.HasValue); + void CompletionHandler(string scopeId) + { + string completionId = $"end_{action.Id}"; + this.ContinueWith(this.CreateStep(completionId), action.Id); + this._workflowBuilder.AddLink(completionId, loopId); + } } - protected override void Visit(BreakLoop item) + protected override void Visit(BreakLoop item) // %%% SUPPORT { this.Trace(item); } - protected override void Visit(ContinueLoop item) + protected override void Visit(ContinueLoop item) // %%% SUPPORT { this.Trace(item); } @@ -142,46 +133,44 @@ protected override void Visit(EndConversation item) { this.Trace(item, isSkipped: false); - this.AddAction(new EndConversationAction(item)); - // Stop the process, this is a terminal action - this.CurrentContext.Then().StopProcess(); - // Create an orphaned context for continuity - this.AddDead(item.Id.Value); + EndConversationAction action = new(item); + this.ContinueWith(action); + this.RestartFrom(action); } protected override void Visit(AnswerQuestionWithAI item) { this.Trace(item, isSkipped: false); - this.AddAction(new AnswerQuestionWithAIAction(item)); + this.ContinueWith(new AnswerQuestionWithAIAction(item)); } protected override void Visit(SetVariable item) { this.Trace(item, isSkipped: false); - this.AddAction(new SetVariableAction(item)); + this.ContinueWith(new SetVariableAction(item)); } protected override void Visit(SetTextVariable item) { this.Trace(item, isSkipped: false); - this.AddAction(new SetTextVariableAction(item)); + this.ContinueWith(new SetTextVariableAction(item)); } protected override void Visit(ClearAllVariables item) { this.Trace(item, isSkipped: false); - this.AddAction(new ClearAllVariablesAction(item)); + this.ContinueWith(new ClearAllVariablesAction(item)); } protected override void Visit(ResetVariable item) { this.Trace(item, isSkipped: false); - this.AddAction(new ResetVariableAction(item)); + this.ContinueWith(new ResetVariableAction(item)); } protected override void Visit(EditTable item) @@ -193,21 +182,21 @@ protected override void Visit(EditTableV2 item) { this.Trace(item, isSkipped: false); - this.AddAction(new EditTableV2Action(item)); + this.ContinueWith(new EditTableV2Action(item)); } protected override void Visit(ParseValue item) { this.Trace(item, isSkipped: false); - this.AddAction(new ParseValueAction(item)); + this.ContinueWith(new ParseValueAction(item)); } protected override void Visit(SendActivity item) { this.Trace(item, isSkipped: false); - this.AddAction(new SendActivityAction(item, this._context.ActivityNotificationHandler)); + this.ContinueWith(new SendActivityAction(item, this._context.ActivityNotificationHandler)); } #region Not implemented @@ -291,7 +280,7 @@ protected override void Visit(BeginDialog item) { this.Trace(item); - this.AddAction(new BeginDialogAction(item)); + this.ContinueWith(new BeginDialogAction(item)); } protected override void Visit(UnknownDialogAction item) @@ -391,109 +380,97 @@ protected override void Visit(SearchAndSummarizeContent item) #endregion - private void AddAction(ProcessAction? action) - { - if (action is not null) - { - // Add the action to the existing context - this.AddStep(this.CreateActionStep(this.CurrentContext, action)); - } - } + private void ContinueWith( + ProcessAction action, + Func? condition = null, + ScopeCompletionAction? callback = null) => + this.ContinueWith(this.CreateActionStep(action), action.ParentId, condition, callback); - private void AddContainer(string contextId) + private void ContinueWith( + ProcessStepBuilder step, + string parentId, + Func? condition = null, + ScopeCompletionAction? callback = null) { - this.AddStep(this.CreateContainerStep(this.CurrentContext, contextId)); + this._actionStack.Recognize(parentId, callback); + this._workflowBuilder.AddNode(step, parentId); + this._workflowBuilder.AddLinkFromPeer(parentId, step.Id, condition); } - private void AddDead(string contextId) - { - this.CurrentContext.Step = this.CreateContainerStep(this.CurrentContext, $"dead_{contextId}"); - } - private void AddStep(ProcessStepBuilder step) + private string RestartFrom(ProcessAction action) => + this.RestartFrom(action.Id, action.ParentId); + + private string RestartFrom(string actionId, string parentId) { - this._steps[step.Id] = step; - this.ContinueWith(step); + string restartId = $"post_{actionId}"; + this._workflowBuilder.AddNode(this.CreateStep(restartId), parentId); + return restartId; } - private ProcessStepBuilder CreateContainerStep(ProcessActionVisitorContext currentContext, string contextId) + private ProcessStepBuilder CreateStep(string actionId, Action? stepAction = null) { - return this.InitializeStep( + return this._processBuilder.AddStepFromFunction( - contextId, + actionId, (kernel, context) => { - Console.WriteLine($"!!! STEP [{contextId}]"); // %%% DEVTRACE + Console.WriteLine($"!!! STEP CUSTOM [{actionId}]"); // %%% LOGGER + stepAction?.Invoke(this.CreateActionContext(kernel)); return Task.CompletedTask; - })); + }); } // This implementation accepts the context as a parameter in order to pin the context closure. // The step cannot reference this.CurrentContext directly, as this will always be the final context. - private ProcessStepBuilder CreateActionStep(ProcessActionVisitorContext currentContext, ProcessAction action) + private ProcessStepBuilder CreateActionStep(ProcessAction action) { - return this.InitializeStep( + return this._processBuilder.AddStepFromFunction( action.Id, async (kernel, context) => { - Console.WriteLine($"!!! STEP {action.GetType().Name} [{action.Id}]"); // %%% DEVTRACE + Console.WriteLine($"!!! STEP {action.GetType().Name} [{action.Id}]"); // %%% LOGGER if (action.Model.Disabled) // %%% VALIDATE { - Console.WriteLine($"!!! DISABLED {action.GetType().Name} [{action.Id}]"); // %%% DEVTRACE + Console.WriteLine($"!!! DISABLED {action.GetType().Name} [{action.Id}]"); // %%% LOGGER return; } try { - ProcessActionContext actionContext = new(this.CreateEngine(), this._scopes, kernel); - await action.ExecuteAsync(actionContext, cancellationToken: default).ConfigureAwait(false); // %%% CANCEL TOKEN + await action.ExecuteAsync( + this.CreateActionContext(kernel), + cancellationToken: default).ConfigureAwait(false); // %%% CANCEL TOKEN } catch (ProcessActionException) { - Console.WriteLine($"*** STEP [{action.Id}] ERROR - Action failure"); // %%% DEVTRACE + Console.WriteLine($"*** STEP [{action.Id}] ERROR - Action failure"); // %%% LOGGER throw; } catch (Exception exception) { - Console.WriteLine($"*** STEP [{action.Id}] ERROR - {exception.GetType().Name}\n{exception.Message}"); // %%% DEVTRACE + Console.WriteLine($"*** STEP [{action.Id}] ERROR - {exception.GetType().Name}\n{exception.Message}"); // %%% LOGGER throw; } - })); - } - - private ProcessStepBuilder InitializeStep(ProcessStepBuilder step) - { - // Capture unhandled errors for the given step - step.OnFunctionError(KernelDelegateProcessStep.FunctionName).SendEventTo(new ProcessFunctionTargetBuilder(this._unhandledErrorStep)); - - return step; + }); } - private void ContinueWith(ProcessStepBuilder newStep, KernelProcessEdgeCondition? condition = null) - { - this.CurrentContext.Then(newStep, condition); - this.CurrentContext.Step = newStep; - } + private ProcessActionContext CreateActionContext(Kernel kernel) => new(this.CreateEngine(), this._scopes, kernel, NullLogger.Instance); // %%% LOGGER private RecalcEngine CreateEngine() => RecalcEngineFactory.Create(this._scopes, this._context.MaximumExpressionLength); private void Trace(DialogAction item, bool isSkipped = true) { - //Console.WriteLine($"> {(isSkipped ? "EMPTY" : "VISIT")}{new string('\t', this._contextStack.Count - 1)} - {this.Format(item)} => {this.Format(item.Parent)}"); // %%% DEVTRACE - Console.WriteLine($"> {(isSkipped ? "EMPTY" : "VISIT")} x{this._contextStack.Count} - {this.Format(item)} => {this.Format(item.Parent)}"); // %%% DEVTRACE + //Console.WriteLine($"> {(isSkipped ? "EMPTY" : "VISIT")}{new string('\t', this._contextStack.Count - 1)} - {this.Format(item)} => {this.Format(item.Parent)}"); // %%% LOGGER + Console.WriteLine($"> {(isSkipped ? "EMPTY" : "VISIT")}: {new string('\t', this._workflowBuilder.GetDepth(item.GetParentId()))}{FormatItem(item)} => {FormatParent(item)}"); // %%% LOGGER } - private string Format(DialogAction action) => $"{action.GetType().Name} [{action.Id.Value}]"; + private static string FormatItem(BotElement element) => $"{element.GetType().Name} ({element.GetId()})"; - private string Format(BotElement? element) => - element switch - { - null => "(root)", - DialogAction action => this.Format(action), - ConditionItem conditionItem => $"{conditionItem.GetType().Name} [{conditionItem.Id}]", - OnActivity activity => $"{activity.GetType().Name} (workflow)", - _ => $"{element.GetType().Name} (unknown element)" - }; + private static string FormatParent(BotElement element) => + element.Parent is null ? + throw new InvalidActionException($"Undefined parent for {element.GetType().Name} that is member of {element.GetId()}.") : + $"{element.Parent.GetType().Name} ({element.GetParentId()})"; } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitorContext.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitorContext.cs deleted file mode 100644 index 46966a5323cc..000000000000 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitorContext.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Threading.Tasks; -using Microsoft.PowerFx; - -namespace Microsoft.SemanticKernel.Process.Workflows; - -internal delegate Task ProcessActionHandler(KernelProcessStepContext context, ProcessActionScopes scopes, RecalcEngine engine, Kernel kernel); - -/// -/// Step context for the current step in a process. -/// -internal sealed class ProcessActionVisitorContext(ProcessStepBuilder step) -{ - /// - /// The current step for the context. - /// - public ProcessStepBuilder Step { get; set; } = step; - - /// - /// %%% COMMENT - /// - /// - public ProcessStepEdgeBuilder Then() => this.Step.OnFunctionResult(KernelDelegateProcessStep.FunctionName); - - /// - /// %%% COMMENT - /// - /// - /// - /// - public ProcessStepBuilder Then(ProcessStepBuilder step, KernelProcessEdgeCondition? condition = null) - { - // IN: Target the given step when the previous step ends - ProcessStepEdgeBuilder edge = - this.Then() - .SendEventTo(new ProcessFunctionTargetBuilder(step)); - - edge.Condition = condition; - - return step; - } -} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessWorkflowBuilder.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessWorkflowBuilder.cs new file mode 100644 index 000000000000..e2ff65efe7ef --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessWorkflowBuilder.cs @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.SemanticKernel.Process.Workflows; + +/// +/// Provides builder patterns for constructing a declarative process workflow. +/// +internal sealed class ProcessWorkflowBuilder +{ + public ProcessWorkflowBuilder(ProcessStepBuilder rootStep) + { + this.RootNode = this.DefineNode(rootStep); + } + + private ProcessWorkflowNode RootNode { get; } + + private Dictionary Steps { get; } = []; + + private List Links { get; } = []; + + public int GetDepth(string nodeId) + { + if (!this.Steps.TryGetValue(nodeId, out ProcessWorkflowNode? sourceNode)) + { + throw new UnknownActionException($"Unresolved step: {nodeId}."); + } + + return sourceNode.Depth; + } + + public void AddNode(ProcessStepBuilder step, string parentId) + { + if (!this.Steps.TryGetValue(parentId, out ProcessWorkflowNode? parentNode)) + { + throw new UnknownActionException($"Unresolved parent for {step.Id}: {parentId}."); + } + + ProcessWorkflowNode stepNode = this.DefineNode(step, parentNode); + + parentNode.Children.Add(stepNode); + } + + public void AddLinkFromPeer(string parentId, string targetId, Func? condition = null) + { + if (!this.Steps.TryGetValue(parentId, out ProcessWorkflowNode? parentNode)) + { + throw new UnknownActionException($"Unresolved step: {parentId}."); + } + + if (parentNode.Children.Count == 0) + { + throw new InvalidOperationException($"Cannot add a link from a node with no children: {parentId}."); // %%% TODO: EXCEPTION TYPE + } + + ProcessWorkflowNode sourceNode = parentNode.Children.Count == 1 ? parentNode : parentNode.Children[^2]; + + this.Links.Add(new ProcessWorkflowLink(sourceNode, targetId, condition)); + } + + public void AddLink(string sourceId, string targetId, Func? condition = null) + { + if (!this.Steps.TryGetValue(sourceId, out ProcessWorkflowNode? sourceNode)) + { + throw new UnknownActionException($"Unresolved step: {sourceId}."); + } + + this.Links.Add(new ProcessWorkflowLink(sourceNode, targetId, condition)); + } + + //public void AddStop(string nodeId) // %%% REMOVE + //{ + // if (!this.Steps.TryGetValue(nodeId, out ProcessWorkflowNode? sourceNode)) + // { + // throw new UnknownActionException($"Unresolved node: {nodeId}."); + // } + + // sourceNode.Step.OnFunctionResult(KernelDelegateProcessStep.FunctionName).StopProcess(); + //} + + public void ConnectNodes() + { + foreach (ProcessWorkflowLink link in this.Links) + { + if (!this.Steps.TryGetValue(link.TargetId, out ProcessWorkflowNode? targetNode)) + { + throw new InvalidOperationException($"Unresolved target for {link.Source.Id}: {link.TargetId}."); // %%% TODO: Exception Type + } + + Console.WriteLine($"> CONNECT: {link.Source.Id} => {link.TargetId}"); // %%% LOGGER + + link.Source.Step.OnFunctionResult(KernelDelegateProcessStep.FunctionName) + .SendEventTo(new ProcessFunctionTargetBuilder(targetNode.Step)) + .OnCondition(link.CreateEdgeCondition()); + } + } + + private ProcessWorkflowNode DefineNode(ProcessStepBuilder step, ProcessWorkflowNode? parentNode = null) + { + ProcessWorkflowNode stepNode = new(step, parentNode); + this.Steps[stepNode.Id] = stepNode; + + return stepNode; + } + + private sealed class ProcessWorkflowNode(ProcessStepBuilder step, ProcessWorkflowNode? parent = null) + { + public string Id => step.Id; + + public ProcessStepBuilder Step => step; + + public ProcessWorkflowNode? Parent { get; } = parent; + + public List Children { get; } = []; + + public int Depth => this.Parent?.Depth + 1 ?? 0; + } + + private sealed record class ProcessWorkflowLink(ProcessWorkflowNode Source, string TargetId, Func? Condition = null) + { + public KernelProcessEdgeCondition? CreateEdgeCondition() => + this.Condition == null ? + null : + new KernelProcessEdgeCondition((stepEvent, state) => Task.FromResult(this.Condition.Invoke())); + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ActionValidationFailure.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ActionValidationFailure.cs index 4d7e321b9436..4e094d189a5a 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ActionValidationFailure.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ActionValidationFailure.cs @@ -5,10 +5,15 @@ namespace Microsoft.SemanticKernel.Process.Workflow.ObjectModel.Validation; /// -/// %%% COMMENT +/// Represents a validation failure based on a . /// public class ActionValidationFailure : ElementValidationFailure { + /// + /// Initializes a new instance of the class with the specified action and error message. + /// + /// The that caused the validation failure. + /// The validation error message. internal ActionValidationFailure(DialogAction action, string message) : base(action, message) { @@ -16,7 +21,7 @@ internal ActionValidationFailure(DialogAction action, string message) } /// - /// %%% COMMENT + /// Gets the identifier of the that caused the validation failure. /// public string Id { get; } } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ElementValidationFailure.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ElementValidationFailure.cs index 75b18dec79af..2de2ac647763 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ElementValidationFailure.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ElementValidationFailure.cs @@ -5,10 +5,15 @@ namespace Microsoft.SemanticKernel.Process.Workflow.ObjectModel.Validation; /// -/// %%% COMMENT +/// Represents a validation failure that is associated with a specific . /// public class ElementValidationFailure : ValidationFailure { + /// + /// Initializes a new instance of the class with the specified element and error message. + /// + /// The that caused the validation failure. + /// The validation error message. internal ElementValidationFailure(BotElement element, string message) : base(message) { @@ -18,17 +23,17 @@ internal ElementValidationFailure(BotElement element, string message) } /// - /// %%% COMMENT + /// Gets the kind of the that caused the validation failure. /// public BotElementKind Kind { get; } /// - /// %%% COMMENT + /// Gets the start position of the in the source, if available. /// public int? StartPosition { get; } /// - /// %%% COMMENT + /// Gets the end position of the in the source, if available. /// public int? EndPosition { get; } } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ExceptionValidationFailure.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ExceptionValidationFailure.cs index 508999463d9d..6a68e975c06f 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ExceptionValidationFailure.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ExceptionValidationFailure.cs @@ -5,10 +5,15 @@ namespace Microsoft.SemanticKernel.Process.Workflow.ObjectModel.Validation; /// -/// %%% COMMENT +/// Represents a validation failure that is associated with an exception. /// public class ExceptionValidationFailure : ValidationFailure { + /// + /// Initializes a new instance of the class with the specified exception and message. + /// + /// The exception that caused the validation failure. + /// The validation failure message. internal ExceptionValidationFailure(Exception exception, string message) : base(message) { @@ -16,7 +21,7 @@ internal ExceptionValidationFailure(Exception exception, string message) } /// - /// %%% COMMENT + /// Gets the exception that caused the validation failure. /// public Exception Exception { get; } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ValidationFailure.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ValidationFailure.cs index d1669146360d..2c3694b968a9 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ValidationFailure.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ValidationFailure.cs @@ -3,7 +3,7 @@ namespace Microsoft.SemanticKernel.Process.Workflow.ObjectModel.Validation; /// -/// %%% COMMENT +/// Represents a failure that occurred during validation. /// public class ValidationFailure { @@ -13,7 +13,7 @@ internal ValidationFailure(string message) } /// - /// %%% COMMENT + /// Gets the message that describes the validation failure. /// public string Message { get; } diff --git a/dotnet/src/Experimental/Process.LocalRuntime/LocalDelegateStep.cs b/dotnet/src/Experimental/Process.LocalRuntime/LocalDelegateStep.cs index 8ae715c965e7..1f88666ee9ca 100644 --- a/dotnet/src/Experimental/Process.LocalRuntime/LocalDelegateStep.cs +++ b/dotnet/src/Experimental/Process.LocalRuntime/LocalDelegateStep.cs @@ -28,7 +28,7 @@ protected override ValueTask InitializeStepAsync() this._functions.Add(f.Name, f); } - this._initialInputs = this.FindInputChannels(this._functions, logger: null, this.ExternalMessageChannel); // %%% LOGGER + this._initialInputs = this.FindInputChannels(this._functions, logger: null, this.ExternalMessageChannel); this._inputs = this._initialInputs.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)); #if !NETCOREAPP diff --git a/dotnet/src/Experimental/Process.LocalRuntime/LocalProcess.cs b/dotnet/src/Experimental/Process.LocalRuntime/LocalProcess.cs index 1ac651747a09..5816395d0aa6 100644 --- a/dotnet/src/Experimental/Process.LocalRuntime/LocalProcess.cs +++ b/dotnet/src/Experimental/Process.LocalRuntime/LocalProcess.cs @@ -407,16 +407,19 @@ private async Task EnqueueEdgesAsync(IEnumerable edges, Queue List defaultConditionedEdges = []; foreach (var edge in edges) { - if (edge.Condition.DeclarativeDefinition?.Equals(ProcessConstants.Declarative.DefaultCondition, StringComparison.OrdinalIgnoreCase) ?? false) + if (edge.Condition is not null) { - defaultConditionedEdges.Add(edge); - continue; - } + if (edge.Condition.DeclarativeDefinition?.Equals(ProcessConstants.Declarative.DefaultCondition, StringComparison.OrdinalIgnoreCase) ?? false) + { + defaultConditionedEdges.Add(edge); + continue; + } - bool isConditionMet = await edge.Condition.Callback(processEvent.ToKernelProcessEvent(), this._processStateManager?.GetState()).ConfigureAwait(false); - if (!isConditionMet) - { - continue; + bool isConditionMet = await edge.Condition.Callback(processEvent.ToKernelProcessEvent(), this._processStateManager?.GetState()).ConfigureAwait(false); + if (!isConditionMet) + { + continue; + } } // Handle different target types diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/ProcessActionScopesTests.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/ProcessActionScopesTests.cs index 114cf4ca29b0..63a669edacc5 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Workflow/ProcessActionScopesTests.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/ProcessActionScopesTests.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Linq; using Microsoft.PowerFx.Types; using Microsoft.SemanticKernel.Process.Workflows; diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/ProcessActionStackTests.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/ProcessActionStackTests.cs new file mode 100644 index 000000000000..fb0e418fb1c3 --- /dev/null +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/ProcessActionStackTests.cs @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using Microsoft.SemanticKernel.Process.Workflows; +using Xunit; + +namespace Microsoft.SemanticKernel.Process.UnitTests.Workflows; + +public sealed class ProcessActionStackTests +{ + [Fact] + public void EmptyStackFailure() + { + // Arrange + ProcessActionStack stack = new(); + + // Act & Assert + InvalidOperationException exception = Assert.Throws(() => stack.CurrentScope); + Assert.Equal("No scope defined", exception.Message); + } + + [Fact] + public void RootScope() + { + // Arrange + ProcessActionStack stack = new(); + + // Act + stack.Recognize("missing"); + string actualScope = stack.CurrentScope; + + // Assert + Assert.Equal("missing", actualScope); + } + + [Fact] + public void NewScope() + { + // Arrange + ProcessActionStack stack = new(); + + // Act + stack.Recognize("rootscope"); + stack.Recognize("childscope"); + + // Assert + Assert.Equal("childscope", stack.CurrentScope); + } + + [Fact] + public void SameConsecutiveScope() + { + // Arrange + ProcessActionStack stack = new(); + + // Act + stack.Recognize("rootscope"); + stack.Recognize("childscope"); + stack.Recognize("childscope"); + + // Assert + Assert.Equal("childscope", stack.CurrentScope); + } + + [Fact] + public void CompletedScope() + { + // Arrange + ProcessActionStack stack = new(); + + // Act + stack.Recognize("rootscope"); + stack.Recognize("childscope"); + stack.Recognize("deepscope"); + stack.Recognize("childscope"); + + // Assert + Assert.Equal("childscope", stack.CurrentScope); + } + + [Fact] + public void DeepScope() + { + // Arrange + ProcessActionStack stack = new(); + + // Act + stack.Recognize("rootscope"); + stack.Recognize("childscope"); + stack.Recognize("deepscope"); + stack.Recognize("rootscope"); + + // Assert + Assert.Equal("rootscope", stack.CurrentScope); + } + + [Fact] + public void ScopeCallback() + { + // Arrange + ProcessActionStack stack = new(); + HashSet completedScopes = []; + + // Act + stack.Recognize("rootscope", HandleCompletion); + stack.Recognize("childscope", HandleCompletion); + stack.Recognize("deepscope", HandleCompletion); + stack.Recognize("rootscope", HandleCompletion); + + // Assert + Assert.Equal("rootscope", stack.CurrentScope); + Assert.Contains("deepscope", completedScopes); + Assert.Contains("childscope", completedScopes); + Assert.DoesNotContain("rootscope", completedScopes); + + void HandleCompletion(string scopeId) + { + completedScopes.Add(scopeId); + } + } +} From ca2a960dc49d1bdc1c2d850a6f03d2cc5f0508c6 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 23 Jul 2025 16:33:29 -0700 Subject: [PATCH 23/40] Cleanup --- .../Step06/Step06_WorkflowProcess.cs | 15 +++++++- .../Process.Core/ProcessDelegateBuilder.cs | 2 +- .../Exceptions/WorkflowBuilderException.cs | 35 +++++++++++++++++++ .../Extensions/FormulaValueExtensions.cs | 4 ++- .../ObjectModel/ObjectModelBuilder.cs | 5 +-- .../Workflow/ObjectModel/ProcessAction.cs | 4 +-- .../ObjectModel/ProcessActionVisitor.cs | 17 ++++----- .../ObjectModel/ProcessWorkflowBuilder.cs | 4 +-- .../Process.Core/Workflow/WorkflowBuilder.cs | 2 +- .../Workflow/Actions/ProcessActionTest.cs | 3 +- 10 files changed, 69 insertions(+), 22 deletions(-) create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/WorkflowBuilderException.cs diff --git a/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs b/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs index cddf57952370..5c2d0dc3065a 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; namespace Step06; @@ -29,8 +31,19 @@ public async Task RunWorkflow(string fileName) Console.WriteLine("\nPROCESS INVOKE\n"); - Kernel kernel = this.CreateKernelWithChatCompletion(); // %%% HOST CONTEXT + Kernel kernel = this.CreateKernel(); await using LocalKernelProcessContext context = await process.StartAsync(kernel, new KernelProcessEvent() { Id = InputEventId, Data = "Why is the sky blue?" }); Console.WriteLine("\nPROCESS DONE"); } + + private Kernel CreateKernel() + { + IKernelBuilder kernelBuilder = Kernel.CreateBuilder(); + + kernelBuilder.Services.AddSingleton(this.LoggerFactory); + this.AddChatCompletionToKernel(kernelBuilder); + this.AddChatClientToKernel(kernelBuilder); + + return kernelBuilder.Build(); + } } diff --git a/dotnet/src/Experimental/Process.Core/ProcessDelegateBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessDelegateBuilder.cs index 83583605658c..5d3ac7f34cd1 100644 --- a/dotnet/src/Experimental/Process.Core/ProcessDelegateBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/ProcessDelegateBuilder.cs @@ -22,7 +22,7 @@ public ProcessDelegateBuilder(string id, StepFunction stepFunction, ProcessBuild this._stepFunction = stepFunction ?? throw new ArgumentNullException(nameof(stepFunction), "Step function cannot be null."); } - internal override KernelProcessStepInfo BuildStep(ProcessBuilder processBuilder, KernelProcessStepStateMetadata? stateMetadata = null) // %%% METADATA ??? + internal override KernelProcessStepInfo BuildStep(ProcessBuilder processBuilder, KernelProcessStepStateMetadata? stateMetadata = null) // %%% NEEDED: stateMetadata ??? { // Build the edges first var builtEdges = this.Edges.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Select(e => e.Build()).ToList()); diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/WorkflowBuilderException.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/WorkflowBuilderException.cs new file mode 100644 index 000000000000..ed7d2c8ecc77 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/WorkflowBuilderException.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace Microsoft.SemanticKernel.Process.Workflows; + +/// +/// Represents an exception that occurs when building the process workflow. +/// +public class WorkflowBuilderException : ProcessWorkflowException +{ + /// + /// Initializes a new instance of the class. + /// + public WorkflowBuilderException() + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The error message that explains the reason for the exception. + public WorkflowBuilderException(string? message) : base(message) + { + } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public WorkflowBuilderException(string? message, Exception? innerException) : base(message, innerException) + { + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/FormulaValueExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/FormulaValueExtensions.cs index d90c047adfb9..0e47e33966db 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/FormulaValueExtensions.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/FormulaValueExtensions.cs @@ -36,12 +36,14 @@ public static FormulaValue ThrowIfError(this FormulaValue value) { Type valueType = value.GetType(); + // Lookup the handler for the specific type first if (s_handlers.TryGetValue(valueType, out GetFormulaValue? handler)) { return $"{handler.Invoke(value)}"; } - foreach (KeyValuePair kvp in s_handlers) // %%% NEEDED ??? + // Could be a derived type, so check all handlers + foreach (KeyValuePair kvp in s_handlers) { if (kvp.Key.IsAssignableFrom(valueType)) { diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs index f6087aefd7e8..b1fc3a87a83a 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs @@ -8,6 +8,7 @@ using Microsoft.Bot.ObjectModel; using Microsoft.Bot.ObjectModel.Yaml; using Microsoft.PowerFx.Types; +using Microsoft.SemanticKernel.Process; using Microsoft.SemanticKernel.Process.Workflow.ObjectModel.Validation; using Microsoft.SemanticKernel.Process.Workflows; @@ -118,14 +119,14 @@ public override ValueTask ActivateAsync(KernelProcessStepState this.RestartFrom(action.Id, action.ParentId); @@ -415,8 +411,8 @@ private ProcessStepBuilder CreateStep(string actionId, Action { - Console.WriteLine($"!!! STEP CUSTOM [{actionId}]"); // %%% LOGGER - stepAction?.Invoke(this.CreateActionContext(kernel)); + Console.WriteLine($"!!! STEP CUSTOM [{actionId}]"); // %%% REMOVE + stepAction?.Invoke(this.CreateActionContext(actionId, kernel)); return Task.CompletedTask; }); } @@ -430,18 +426,18 @@ private ProcessStepBuilder CreateActionStep(ProcessAction action) action.Id, async (kernel, context) => { - Console.WriteLine($"!!! STEP {action.GetType().Name} [{action.Id}]"); // %%% LOGGER + Console.WriteLine($"!!! STEP {action.GetType().Name} [{action.Id}]"); // %%% REMOVE if (action.Model.Disabled) // %%% VALIDATE { - Console.WriteLine($"!!! DISABLED {action.GetType().Name} [{action.Id}]"); // %%% LOGGER + Console.WriteLine($"!!! DISABLED {action.GetType().Name} [{action.Id}]"); // %%% REMOVE return; } try { await action.ExecuteAsync( - this.CreateActionContext(kernel), + this.CreateActionContext(action.Id, kernel), cancellationToken: default).ConfigureAwait(false); // %%% CANCEL TOKEN } catch (ProcessActionException) @@ -457,13 +453,12 @@ await action.ExecuteAsync( }); } - private ProcessActionContext CreateActionContext(Kernel kernel) => new(this.CreateEngine(), this._scopes, kernel, NullLogger.Instance); // %%% LOGGER + private ProcessActionContext CreateActionContext(string actionId, Kernel kernel) => new(this.CreateEngine(), this._scopes, kernel, kernel.LoggerFactory.CreateLogger(actionId)); private RecalcEngine CreateEngine() => RecalcEngineFactory.Create(this._scopes, this._context.MaximumExpressionLength); private void Trace(DialogAction item, bool isSkipped = true) { - //Console.WriteLine($"> {(isSkipped ? "EMPTY" : "VISIT")}{new string('\t', this._contextStack.Count - 1)} - {this.Format(item)} => {this.Format(item.Parent)}"); // %%% LOGGER Console.WriteLine($"> {(isSkipped ? "EMPTY" : "VISIT")}: {new string('\t', this._workflowBuilder.GetDepth(item.GetParentId()))}{FormatItem(item)} => {FormatParent(item)}"); // %%% LOGGER } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessWorkflowBuilder.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessWorkflowBuilder.cs index e2ff65efe7ef..c3cb00114b88 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessWorkflowBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessWorkflowBuilder.cs @@ -53,7 +53,7 @@ public void AddLinkFromPeer(string parentId, string targetId, Func? condit if (parentNode.Children.Count == 0) { - throw new InvalidOperationException($"Cannot add a link from a node with no children: {parentId}."); // %%% TODO: EXCEPTION TYPE + throw new WorkflowBuilderException($"Cannot add a link from a node with no children: {parentId}."); } ProcessWorkflowNode sourceNode = parentNode.Children.Count == 1 ? parentNode : parentNode.Children[^2]; @@ -87,7 +87,7 @@ public void ConnectNodes() { if (!this.Steps.TryGetValue(link.TargetId, out ProcessWorkflowNode? targetNode)) { - throw new InvalidOperationException($"Unresolved target for {link.Source.Id}: {link.TargetId}."); // %%% TODO: Exception Type + throw new WorkflowBuilderException($"Unresolved target for {link.Source.Id}: {link.TargetId}."); } Console.WriteLine($"> CONNECT: {link.Source.Id} => {link.TargetId}"); // %%% LOGGER diff --git a/dotnet/src/Experimental/Process.Core/Workflow/WorkflowBuilder.cs b/dotnet/src/Experimental/Process.Core/Workflow/WorkflowBuilder.cs index 38d9d2b90a00..c2d83082c2b2 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/WorkflowBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/WorkflowBuilder.cs @@ -549,7 +549,7 @@ private static string ResolveEventName(string eventName) int index = eventName.IndexOf(ProcessConstants.EventIdSeparator); if (index > 0) { - eventName = eventName.Substring(index + 1); + eventName = eventName[(index + 1)..]; } return eventName; diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ProcessActionTest.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ProcessActionTest.cs index 5e05f87349f5..de41c0826802 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ProcessActionTest.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ProcessActionTest.cs @@ -28,7 +28,8 @@ internal Task ExecuteAction(ProcessAction action, Kernel? kernel = null) => new ProcessActionContext( RecalcEngineFactory.Create(this.Scopes, 5000), this.Scopes, - kernel ?? new Kernel()), + kernel ?? new Kernel(), + this.Output), cancellationToken: default); internal void VerifyModel(DialogAction model, ProcessAction action) From aa3e664ac01a9c30ed54cf146c9bb7a69c13cddb Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 23 Jul 2025 16:50:32 -0700 Subject: [PATCH 24/40] Fix init step --- .../Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs index b1fc3a87a83a..53ec023361d9 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs @@ -119,7 +119,7 @@ public override ValueTask ActivateAsync(KernelProcessStepState Date: Wed, 23 Jul 2025 17:11:49 -0700 Subject: [PATCH 25/40] More cleanup --- .../ObjectModel/Actions/SendActivityAction.cs | 26 ++++++++-- .../Workflow/ObjectModel/HostContext.cs | 48 ++++--------------- .../ObjectModel/ObjectModelBuilder.cs | 7 ++- .../ObjectModel/ProcessActionVisitor.cs | 4 +- .../Actions/SendActivityActionTest.cs | 2 +- 5 files changed, 37 insertions(+), 50 deletions(-) diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SendActivityAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SendActivityAction.cs index 030758186dbf..806f23cfbf96 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SendActivityAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SendActivityAction.cs @@ -1,16 +1,19 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; +using Microsoft.SemanticKernel.Process.Workflows.Extensions; namespace Microsoft.SemanticKernel.Process.Workflows.Actions; internal sealed class SendActivityAction : ProcessAction { - private readonly ActivityNotificationHandler _handler; + private readonly TextWriter _activityWriter; - public SendActivityAction(SendActivity source, ActivityNotificationHandler handler) + public SendActivityAction(SendActivity source, TextWriter activityWriter) : base(source) { if (source.Activity is null) @@ -18,11 +21,24 @@ public SendActivityAction(SendActivity source, ActivityNotificationHandler handl throw new InvalidActionException($"{nameof(SendActivity)} action must have an activity defined."); } - this._handler = handler; + this._activityWriter = activityWriter; } - protected override async Task HandleAsync(ProcessActionContext context, CancellationToken cancellationToken) + protected override Task HandleAsync(ProcessActionContext context, CancellationToken cancellationToken) { - await this._handler.Invoke(this.Model.Activity!, context.Engine).ConfigureAwait(false); + Console.WriteLine($"\nACTIVITY: {this.Model.Activity?.GetType().Name ?? "Unknown"}"); // %%% LOGGER + + if (this.Model.Activity is MessageActivityTemplate messageActivity) + { + if (!string.IsNullOrEmpty(messageActivity.Summary)) + { + this._activityWriter.WriteLine($"\t{messageActivity.Summary}"); + } + + string? activityText = context.Engine.Format(messageActivity.Text); + this._activityWriter.WriteLine(activityText + Environment.NewLine); + } + + return Task.CompletedTask; } } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/HostContext.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/HostContext.cs index 41c09bc667a6..19b6277f9969 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/HostContext.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/HostContext.cs @@ -1,57 +1,29 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Threading.Tasks; -using Microsoft.Bot.ObjectModel; -using Microsoft.PowerFx; -using Microsoft.SemanticKernel.Process.Workflows.Extensions; +using System.IO; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; namespace Microsoft.SemanticKernel; /// -/// %%% COMMENT -/// -/// -/// -/// -public delegate Task ActivityNotificationHandler(ActivityTemplateBase activity, RecalcEngine engine); - -/// -/// %%% COMMENT +/// Provides configuration and services for workflow execution, including logging and output channels. /// public sealed class HostContext { /// - /// %%% COMMENT + /// Gets the maximum allowed length for expressions evaluated in the workflow. /// - internal static HostContext Default { get; } = new(); + public int MaximumExpressionLength { get; init; } = 3000; /// - /// %%% COMMENT + /// Gets the used for activity output and diagnostics. /// - public int MaximumExpressionLength { get; init; } = 3000; + public TextWriter ActivityChannel { get; init; } = Console.Out; /// - /// %%% COMMENT + /// Gets the used to create loggers for workflow components. /// - /// - /// - /// - public Task ActivityNotificationHandler(ActivityTemplateBase activity, RecalcEngine engine) // %%% TODO: CONFIGURABLE - { - Console.WriteLine($"\nACTIVITY: {activity.GetType().Name}"); - - if (activity is MessageActivityTemplate messageActivity) - { - if (!string.IsNullOrEmpty(messageActivity.Summary)) - { - Console.WriteLine($"\t{messageActivity.Summary}"); // %%% LOGGER - } - - string? activityText = engine.Format(messageActivity.Text); - Console.WriteLine(activityText + Environment.NewLine); // %%% LOGGER - } - - return Task.CompletedTask; - } + public ILoggerFactory LoggerFactory { get; init; } = NullLoggerFactory.Instance; } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs index 53ec023361d9..d04bc2a4cdab 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs @@ -8,7 +8,6 @@ using Microsoft.Bot.ObjectModel; using Microsoft.Bot.ObjectModel.Yaml; using Microsoft.PowerFx.Types; -using Microsoft.SemanticKernel.Process; using Microsoft.SemanticKernel.Process.Workflow.ObjectModel.Validation; using Microsoft.SemanticKernel.Process.Workflows; @@ -61,9 +60,9 @@ static ImmutableArray SingleFailure(ValidationFailure failure /// /// The reader that provides the workflow object model YAML. /// The identifier for the message. - /// The environment for the process actions. + /// The hosting context for the workflow. /// The that corresponds with the YAML object model. - public static KernelProcess Build(TextReader yamlReader, string messageId, HostContext? environment = null) + public static KernelProcess Build(TextReader yamlReader, string messageId, HostContext? context = null) { Console.WriteLine("@ PARSING YAML"); BotElement rootElement = YamlSerializer.Deserialize(yamlReader) ?? throw new KernelException("Unable to parse YAML content."); @@ -76,7 +75,7 @@ public static KernelProcess Build(TextReader yamlReader, string messageId, HostC processBuilder.OnInputEvent(messageId).SendEventTo(new ProcessFunctionTargetBuilder(initStep)); Console.WriteLine("@ INTERPRETING MODEL"); - ProcessActionVisitor visitor = new(processBuilder, environment ?? HostContext.Default, scopes); + ProcessActionVisitor visitor = new(processBuilder, context ?? new HostContext(), scopes); ProcessActionWalker walker = new(rootElement, visitor); Console.WriteLine("@ FINALIZING PROCESS"); diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs index 48df7842382a..ed88ce66c03a 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs @@ -195,7 +195,7 @@ protected override void Visit(SendActivity item) { this.Trace(item, isSkipped: false); - this.ContinueWith(new SendActivityAction(item, this._context.ActivityNotificationHandler)); + this.ContinueWith(new SendActivityAction(item, this._context.ActivityChannel)); } #region Not implemented @@ -438,7 +438,7 @@ private ProcessStepBuilder CreateActionStep(ProcessAction action) { await action.ExecuteAsync( this.CreateActionContext(action.Id, kernel), - cancellationToken: default).ConfigureAwait(false); // %%% CANCEL TOKEN + cancellationToken: default).ConfigureAwait(false); // %%% CANCELTOKEN } catch (ProcessActionException) { diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SendActivityActionTest.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SendActivityActionTest.cs index 82d4cffe88a5..4ebdd8824f2a 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SendActivityActionTest.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SendActivityActionTest.cs @@ -26,7 +26,7 @@ public async Task CaptureActivity() "Test activity message"); // Act - SendActivityAction action = new(model, activitySink.Handler); + SendActivityAction action = new(model, this.Output); await this.ExecuteAction(action); // Assert From a5aeccb27b24190be012911cc64d577ca9000cd9 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 24 Jul 2025 10:57:27 -0700 Subject: [PATCH 26/40] Clean-up --- .../Actions/AnswerQuestionWithAIAction.cs | 6 +-- .../ObjectModel/Actions/AssignmentAction.cs | 17 ++------- .../Actions/ClearAllVariablesAction.cs | 5 ++- .../ObjectModel/Actions/EditTableV2Action.cs | 2 +- .../Actions/EndConversationAction.cs | 2 +- .../ObjectModel/Actions/ForeachAction.cs | 23 ++++++------ .../ObjectModel/Actions/ParseValueAction.cs | 13 +++---- .../Actions/ResetVariableAction.cs | 4 +- .../ObjectModel/Actions/SendActivityAction.cs | 5 --- .../Actions/SetTextVariableAction.cs | 6 +-- .../ObjectModel/Actions/SetVariableAction.cs | 6 +-- .../PowerFx/RecalcEngineExtensions.cs | 37 ++++++++++++++----- .../ObjectModel/ProcessActionScopes.cs | 2 + .../Actions/SendActivityActionTest.cs | 22 +++-------- .../Workflow/Actions/SetVariableActionTest.cs | 4 +- 15 files changed, 68 insertions(+), 86 deletions(-) diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AnswerQuestionWithAIAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AnswerQuestionWithAIAction.cs index 1c44d340deda..2bbdd7ecefa7 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AnswerQuestionWithAIAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AnswerQuestionWithAIAction.cs @@ -13,12 +13,8 @@ namespace Microsoft.SemanticKernel.Process.Workflows.Actions; internal sealed class AnswerQuestionWithAIAction : AssignmentAction { public AnswerQuestionWithAIAction(AnswerQuestionWithAI model) - : base(model, () => model.Variable?.Path) + : base(model, Throw.IfNull(model.Variable?.Path, $"{nameof(model)}.{nameof(model.Variable)}.{nameof(InitializablePropertyPath.Path)}")) { - if (string.IsNullOrWhiteSpace(model.UserInput?.ExpressionText)) - { - throw new InvalidActionException($"{nameof(AnswerQuestionWithAI)} must define {nameof(AnswerQuestionWithAI.UserInput)}"); - } } protected override async Task HandleAsync(ProcessActionContext context, CancellationToken cancellationToken) diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AssignmentAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AssignmentAction.cs index 18ecbbe4f744..ad39543ec1c0 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AssignmentAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AssignmentAction.cs @@ -10,28 +10,17 @@ namespace Microsoft.SemanticKernel.Process.Workflows.Actions; internal abstract class AssignmentAction : ProcessAction where TAction : DialogAction { - protected AssignmentAction(TAction model, Func resolver) + protected AssignmentAction(TAction model, PropertyPath assignmentTarget) : base(model) { - this.Target = - resolver.Invoke() ?? - throw new InvalidActionException($"Action '{model.GetType().Name}' must have a variable path defined."); - - if (string.IsNullOrWhiteSpace(this.Target.VariableScopeName)) - { - throw new InvalidActionException($"Action '{model.GetType().Name}' must define a variable scope."); - } - if (string.IsNullOrWhiteSpace(this.Target.VariableName)) - { - throw new InvalidActionException($"Action '{model.GetType().Name}' must define a variable name."); - } + this.Target = assignmentTarget; } public PropertyPath Target { get; } protected void AssignTarget(ProcessActionContext context, FormulaValue result) { - context.Engine.SetScopedVariable(context.Scopes, ActionScopeType.Parse(this.Target.VariableScopeName), this.Target.VariableName!, result); + context.Engine.SetScopedVariable(context.Scopes, this.Target, result); string? resultValue = result.Format(); string valuePosition = (resultValue?.IndexOf('\n') ?? -1) >= 0 ? Environment.NewLine : " "; Console.WriteLine( // %%% LOGGER diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ClearAllVariablesAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ClearAllVariablesAction.cs index 8332d01b2b6b..89136c12e936 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ClearAllVariablesAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ClearAllVariablesAction.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; +using Microsoft.SemanticKernel.Process.Workflows.PowerFx; namespace Microsoft.SemanticKernel.Process.Workflows.Actions; @@ -15,9 +16,9 @@ public ClearAllVariablesAction(ClearAllVariables source) protected override Task HandleAsync(ProcessActionContext context, CancellationToken cancellationToken) { - DataValue literalValue = this.Model.Variables.GetLiteralValue(); + DataValue literalValue = this.Model.Variables.GetLiteralValue(); // %%% DON'T USE GetLiteralValue - // %%% TODO: VariablesToClear ?!?!!?! + context.Engine.ClearScope(context.Scopes, ActionScopeType.Topic); // %%% EVALUTE "Variables" return Task.CompletedTask; } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EditTableV2Action.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EditTableV2Action.cs index e88ef53a3a31..8f34596428df 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EditTableV2Action.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EditTableV2Action.cs @@ -12,7 +12,7 @@ namespace Microsoft.SemanticKernel.Process.Workflows.Actions; internal sealed class EditTableV2Action : AssignmentAction { public EditTableV2Action(EditTableV2 model) - : base(model, () => model.ItemsVariable?.Path) + : base(model, Throw.IfNull(model.ItemsVariable?.Path, $"{nameof(model)}.{nameof(model.ItemsVariable)}.{nameof(InitializablePropertyPath.Path)}")) { } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EndConversationAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EndConversationAction.cs index 09cd7f04801f..1db9f20c6352 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EndConversationAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EndConversationAction.cs @@ -6,7 +6,7 @@ namespace Microsoft.SemanticKernel.Process.Workflows.Actions; -internal sealed class EndConversationAction : ProcessAction +internal sealed class EndConversationAction : ProcessAction // %%% REMOVE ??? { public EndConversationAction(EndConversation model) : base(model) diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ForeachAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ForeachAction.cs index 0211447b1e45..ebd34df7afb6 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ForeachAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ForeachAction.cs @@ -39,20 +39,21 @@ public void TakeNext(ProcessActionContext context) FormulaValue value = this._values[this._index]; this._index++; - context.Engine.SetScopedVariable( - context.Scopes, - ActionScopeType.Parse(this.Model.Value!.Path.VariableScopeName), // %%% NULL OVERRIDE - this.Model.Value.Path.VariableName!, - value); + context.Engine.SetScopedVariable(context.Scopes, Throw.IfNull(this.Model.Value), value); - if (this.Model.Index != null) + if (this.Model.Index is not null) { - context.Engine.SetScopedVariable( - context.Scopes, - ActionScopeType.Parse(this.Model.Index.Path.VariableScopeName), - this.Model.Index.Path.VariableName!, - FormulaValue.New(this._index)); + context.Engine.SetScopedVariable(context.Scopes, this.Model.Index.Path, FormulaValue.New(this._index)); } } } + + public void Reset(ProcessActionContext context) + { + context.Engine.ClearScopedVariable(context.Scopes, Throw.IfNull(this.Model.Value)); + if (this.Model.Index is not null) + { + context.Engine.ClearScopedVariable(context.Scopes, this.Model.Index); + } + } } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ParseValueAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ParseValueAction.cs index b1b7804435fd..219aaac4c0e7 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ParseValueAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ParseValueAction.cs @@ -12,8 +12,8 @@ namespace Microsoft.SemanticKernel.Process.Workflows.Actions; internal sealed class ParseValueAction : AssignmentAction { - public ParseValueAction(ParseValue source) - : base(source, () => source.Variable?.Path) + public ParseValueAction(ParseValue model) + : base(model, Throw.IfNull(model.Variable?.Path, $"{nameof(model)}.{nameof(model.Variable)}.{nameof(InitializablePropertyPath.Path)}")) { if (this.Model.Value is null) { @@ -23,15 +23,14 @@ public ParseValueAction(ParseValue source) protected override Task HandleAsync(ProcessActionContext context, CancellationToken cancellationToken) { - FormulaValue? parsedResult = null; - - DataType valueType = this.Model.ValueType!; // %%% NULL OVERRIDE FormulaValue result = context.Engine.EvaluateExpression(this.Model.Value); + FormulaValue? parsedResult = null; + if (result is StringValue stringValue) { parsedResult = - valueType switch + this.Model.ValueType switch { StringDataType => stringValue, NumberDataType => NumberValue.New(stringValue.Value), @@ -43,7 +42,7 @@ protected override Task HandleAsync(ProcessActionContext context, CancellationTo if (parsedResult is null) { - throw new ProcessActionException($"Unable to parse {valueType.GetType().Name}"); + throw new ProcessActionException($"Unable to parse {result.Type.GetType().Name}"); } this.AssignTarget(context, parsedResult); diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ResetVariableAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ResetVariableAction.cs index 3996b751d8ec..d87c5ba29991 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ResetVariableAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ResetVariableAction.cs @@ -12,13 +12,13 @@ namespace Microsoft.SemanticKernel.Process.Workflows.Actions; internal sealed class ResetVariableAction : AssignmentAction { public ResetVariableAction(ResetVariable model) - : base(model, () => model.Variable) + : base(model, Throw.IfNull(model.Variable, $"{nameof(model)}.{nameof(model.Variable)}")) { } protected override Task HandleAsync(ProcessActionContext context, CancellationToken cancellationToken) { - context.Engine.ClearScopedVariable(context.Scopes, ActionScopeType.Parse(this.Target.VariableScopeName), this.Target.VariableName!); + context.Engine.ClearScopedVariable(context.Scopes, this.Target); Console.WriteLine( // %%% LOGGER $""" !!! CLEAR {this.GetType().Name} [{this.Id}] diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SendActivityAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SendActivityAction.cs index 806f23cfbf96..7a95ec232247 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SendActivityAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SendActivityAction.cs @@ -16,11 +16,6 @@ internal sealed class SendActivityAction : ProcessAction public SendActivityAction(SendActivity source, TextWriter activityWriter) : base(source) { - if (source.Activity is null) - { - throw new InvalidActionException($"{nameof(SendActivity)} action must have an activity defined."); - } - this._activityWriter = activityWriter; } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetTextVariableAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetTextVariableAction.cs index f0fffdf56fed..bd02431fa592 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetTextVariableAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetTextVariableAction.cs @@ -11,12 +11,8 @@ namespace Microsoft.SemanticKernel.Process.Workflows.Actions; internal sealed class SetTextVariableAction : AssignmentAction { public SetTextVariableAction(SetTextVariable model) - : base(model, () => model.Variable?.Path) + : base(model, Throw.IfNull(model.Variable?.Path, $"{nameof(model)}.{nameof(model.Variable)}.{nameof(InitializablePropertyPath.Path)}")) { - if (this.Model.Value is null) - { - throw new InvalidActionException($"{nameof(SetTextVariable)} must define {nameof(SetTextVariable.Value)}"); - } } protected override Task HandleAsync(ProcessActionContext context, CancellationToken cancellationToken) diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetVariableAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetVariableAction.cs index 193662ed3c2e..6fcb56607396 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetVariableAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetVariableAction.cs @@ -12,12 +12,8 @@ namespace Microsoft.SemanticKernel.Process.Workflows.Actions; internal sealed class SetVariableAction : AssignmentAction { public SetVariableAction(SetVariable model) - : base(model, () => model.Variable?.Path) + : base(model, Throw.IfNull(model.Variable?.Path, $"{nameof(model)}.{nameof(model.Variable)}.{nameof(InitializablePropertyPath.Path)}")) { - if (this.Model.Value is null) - { - throw new InvalidActionException($"{nameof(SetVariable)} must define {nameof(SetVariable.Value)}"); - } } protected override Task HandleAsync(ProcessActionContext context, CancellationToken cancellationToken) diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs index a269b47941f9..360f8df329a5 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs @@ -9,26 +9,37 @@ namespace Microsoft.SemanticKernel.Process.Workflows.PowerFx; internal static class RecalcEngineExtensions { + public static void ClearScope(this RecalcEngine engine, ProcessActionScopes scopes, ActionScopeType scope) + { + // Clear all scope values. + scopes.Clear(scope); + + // Rebuild scope record and update engine + UpdateScope(engine, scopes, scope); + } + + public static void ClearScopedVariable(this RecalcEngine engine, ProcessActionScopes scopes, PropertyPath variablePath) => + engine.ClearScopedVariable(scopes, ActionScopeType.Parse(variablePath.VariableScopeName), Throw.IfNull(variablePath.VariableName)); + public static void ClearScopedVariable(this RecalcEngine engine, ProcessActionScopes scopes, ActionScopeType scope, string varName) { - // Validate inputs and assign value. + // Clear value. scopes.Remove(varName, scope); // Rebuild scope record and update engine - RecordValue scopeRecord = scopes.BuildRecord(scope); - engine.DeleteFormula(scope.Name); - engine.UpdateVariable(scope.Name, scopeRecord); + UpdateScope(engine, scopes, scope); } + public static void SetScopedVariable(this RecalcEngine engine, ProcessActionScopes scopes, PropertyPath variablePath, FormulaValue value) => + engine.SetScopedVariable(scopes, ActionScopeType.Parse(variablePath.VariableScopeName), Throw.IfNull(variablePath.VariableName), value); + public static void SetScopedVariable(this RecalcEngine engine, ProcessActionScopes scopes, ActionScopeType scope, string varName, FormulaValue value) { - // Validate inputs and assign value. + // Assign value. scopes.Set(varName, scope, value); // Rebuild scope record and update engine - RecordValue scopeRecord = scopes.BuildRecord(scope); - engine.DeleteFormula(scope.Name); - engine.UpdateVariable(scope.Name, scopeRecord); + UpdateScope(engine, scopes, scope); } public static FormulaValue EvaluateExpression(this RecalcEngine engine, ExpressionBase? value) @@ -50,11 +61,19 @@ public static FormulaValue EvaluateExpression(this RecalcEngine engine, Expressi if (value.IsLiteral) { - return value.GetLiteralValue().ToFormulaValue(); + return value.GetLiteralValue().ToFormulaValue(); // %%% GetLiteralValue } // %%% TODO: value.StructuredRecordExpression ??? + // %%% TODO: ArrayExpression return BlankValue.NewBlank(); } + + private static void UpdateScope(RecalcEngine engine, ProcessActionScopes scopes, ActionScopeType scope) + { + RecordValue scopeRecord = scopes.BuildRecord(scope); + engine.DeleteFormula(scope.Name); + engine.UpdateVariable(scope.Name, scopeRecord); + } } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionScopes.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionScopes.cs index c5a11af4bfc9..1b73ccf33898 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionScopes.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionScopes.cs @@ -87,6 +87,8 @@ IEnumerable GetFields() public FormulaValue Get(string name, ActionScopeType? type = null) => this._scopes[type ?? ActionScopeType.Topic][name]; + public void Clear(ActionScopeType type) => this._scopes[type].Clear(); + public void Remove(string name) => this.Remove(name, ActionScopeType.Topic); public void Remove(string name, ActionScopeType type) => this._scopes[type].Remove(name); diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SendActivityActionTest.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SendActivityActionTest.cs index 4ebdd8824f2a..163a33a9053e 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SendActivityActionTest.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SendActivityActionTest.cs @@ -1,9 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; -using Microsoft.PowerFx; using Microsoft.SemanticKernel.Process.Workflows.Actions; using Xunit; using Xunit.Abstractions; @@ -19,19 +18,20 @@ public sealed class SendActivityActionTest(ITestOutputHelper output) : ProcessAc public async Task CaptureActivity() { // Arrange - ActivitySink activitySink = new(); SendActivity model = this.CreateModel( this.FormatDisplayName(nameof(CaptureActivity)), "Test activity message"); + await using StringWriter activityWriter = new(); // Act - SendActivityAction action = new(model, this.Output); + SendActivityAction action = new(model, activityWriter); await this.ExecuteAction(action); + activityWriter.Flush(); // Assert this.VerifyModel(model, action); - Assert.Single(activitySink.Activities); + Assert.NotEmpty(activityWriter.ToString()); } private SendActivity CreateModel(string displayName, string activityMessage, string? summary = null) @@ -54,16 +54,4 @@ private SendActivity CreateModel(string displayName, string activityMessage, str return model; } - - private sealed class ActivitySink - { - public List Activities { get; } = []; - - public Task Handler(ActivityTemplateBase activity, RecalcEngine engine) - { - this.Activities.Add(activity); - - return Task.CompletedTask; - } - } } diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SetVariableActionTest.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SetVariableActionTest.cs index 82b33a89b076..877d74870221 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SetVariableActionTest.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/SetVariableActionTest.cs @@ -1,9 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; using Microsoft.PowerFx.Types; -using Microsoft.SemanticKernel.Process.Workflows; using Microsoft.SemanticKernel.Process.Workflows.Actions; using Xunit; using Xunit.Abstractions; @@ -19,7 +19,7 @@ public sealed class SetVariableActionTest(ITestOutputHelper output) : ProcessAct public void InvalidModel() { // Arrange, Act, Assert - Assert.Throws(() => new SetVariableAction(new SetVariable())); + Assert.Throws(() => new SetVariableAction(new SetVariable())); } [Fact] From f72748859ba39436610b9a17ff9b5d1b48013027 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 28 Jul 2025 21:34:59 -0700 Subject: [PATCH 27/40] Checkpoint for demo --- .../Step06/Step06_WorkflowProcess.cs | 11 ++-- .../Step06/demo250729.yaml | 57 +++++++++++++++++++ .../Actions/AnswerQuestionWithAIAction.cs | 3 +- .../ObjectModel/Actions/AssignmentAction.cs | 19 +++++-- .../ObjectModel/Actions/ForeachAction.cs | 3 +- .../ObjectModel/Actions/SendActivityAction.cs | 18 ++++-- .../ObjectModel/ObjectModelBuilder.cs | 2 +- .../Workflow/ObjectModel/ProcessAction.cs | 6 +- 8 files changed, 98 insertions(+), 21 deletions(-) create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step06/demo250729.yaml diff --git a/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs b/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs index 5c2d0dc3065a..31887aedeae7 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; namespace Step06; @@ -20,6 +19,7 @@ public Step06_WorkflowProcess(ITestOutputHelper output) [InlineData("testLoop")] [InlineData("testCondition")] [InlineData("deepResearch")] + [InlineData("demo250729")] public async Task RunWorkflow(string fileName) { const string InputEventId = "question"; @@ -32,15 +32,18 @@ public async Task RunWorkflow(string fileName) Console.WriteLine("\nPROCESS INVOKE\n"); Kernel kernel = this.CreateKernel(); - await using LocalKernelProcessContext context = await process.StartAsync(kernel, new KernelProcessEvent() { Id = InputEventId, Data = "Why is the sky blue?" }); + await using LocalKernelProcessContext context = await process.StartAsync(kernel, new KernelProcessEvent() { Id = InputEventId, Data = "" }); Console.WriteLine("\nPROCESS DONE"); } - private Kernel CreateKernel() + private Kernel CreateKernel(bool withLogger = false) { IKernelBuilder kernelBuilder = Kernel.CreateBuilder(); - kernelBuilder.Services.AddSingleton(this.LoggerFactory); + if (withLogger) + { + kernelBuilder.Services.AddSingleton(this.LoggerFactory); + } this.AddChatCompletionToKernel(kernelBuilder); this.AddChatClientToKernel(kernelBuilder); diff --git a/dotnet/samples/GettingStartedWithProcesses/Step06/demo250729.yaml b/dotnet/samples/GettingStartedWithProcesses/Step06/demo250729.yaml new file mode 100644 index 000000000000..611ec7912b13 --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step06/demo250729.yaml @@ -0,0 +1,57 @@ +kind: AdaptiveDialog +beginDialog: + kind: OnActivity + id: activity_xyz123 + actions: + + # Capture optional agent instructions + - kind: SetVariable + id: setVariable_NZ2u0l + variable: Topic.Instructions + value: =System.LastMessage.Text + + # Assign a list of inputs in JSON format to a variable + - kind: SetVariable + id: setVariable_aASlmF + displayName: List all of questions for LLM + variable: Topic.Questions + value: |- + =[ + "Why is the sky blue?", + "What is the capital of France?", + "Where do rainbows come from?", + ] + + # Loop over each question in the list + - kind: Foreach + id: foreach_mVIecC + items: =Topic.Questions + index: Topic.LoopIndex + value: Topic.Question + actions: + + # Display the current question + - kind: SendActivity + id: sendActivity_lMn07p + activity: "Question {Topic.LoopIndex + 1} - {Topic.Question}" + + # Use AI to answer the question + - kind: AnswerQuestionWithAI + id: question_wEJ456 + variable: Topic.Answer + userInput: =Topic.Question + additionalInstructions: "{Topic.Instructions}" + + # Display the AI's answer + - kind: SendActivity + id: sendActivity_zA3f0p + activity: "AI - {Topic.Answer}" + + # After processing all questions, display a completion message + - kind: SendActivity + id: sendActivity_SVoNSV + activity: Complete! + + # End the conversation + - kind: EndConversation + id: end_8nXE8H diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AnswerQuestionWithAIAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AnswerQuestionWithAIAction.cs index 2bbdd7ecefa7..46646749acc5 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AnswerQuestionWithAIAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AnswerQuestionWithAIAction.cs @@ -7,6 +7,7 @@ using Microsoft.PowerFx.Types; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Process.Workflows.Extensions; +using Microsoft.SemanticKernel.Process.Workflows.PowerFx; namespace Microsoft.SemanticKernel.Process.Workflows.Actions; @@ -20,7 +21,7 @@ public AnswerQuestionWithAIAction(AnswerQuestionWithAI model) protected override async Task HandleAsync(ProcessActionContext context, CancellationToken cancellationToken) { IChatCompletionService chatCompletion = context.Kernel.Services.GetRequiredService(); - FormulaValue expressionResult = context.Engine.Eval(this.Model.UserInput!.ExpressionText); + FormulaValue expressionResult = context.Engine.EvaluateExpression(this.Model.UserInput); if (expressionResult is not StringValue stringResult) { throw new InvalidActionException($"{nameof(AnswerQuestionWithAI)} requires text for {nameof(AnswerQuestionWithAI.UserInput)}"); diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AssignmentAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AssignmentAction.cs index ad39543ec1c0..b6c002bf818f 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AssignmentAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AssignmentAction.cs @@ -2,6 +2,7 @@ using System; using Microsoft.Bot.ObjectModel; +using Microsoft.Extensions.Logging; using Microsoft.PowerFx.Types; using Microsoft.SemanticKernel.Process.Workflows.Extensions; using Microsoft.SemanticKernel.Process.Workflows.PowerFx; @@ -23,11 +24,17 @@ protected void AssignTarget(ProcessActionContext context, FormulaValue result) context.Engine.SetScopedVariable(context.Scopes, this.Target, result); string? resultValue = result.Format(); string valuePosition = (resultValue?.IndexOf('\n') ?? -1) >= 0 ? Environment.NewLine : " "; - Console.WriteLine( // %%% LOGGER - $""" - !!! ASSIGN {this.GetType().Name} [{this.Id}] - NAME: {this.Target.Format()} - VALUE:{valuePosition}{result.Format()} ({result.GetType().Name}) - """); + context.Logger.LogDebug( + """ + !!! ASSIGN {ActionName} [{ActionId}] + NAME: {TargetName} + VALUE:{ValuePosition}{Result} ({ResultType}) + """, + this.GetType().Name, + this.Id, + this.Target.Format(), + valuePosition, + result.Format(), + result.GetType().Name); } } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ForeachAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ForeachAction.cs index ebd34df7afb6..f649d5f70df0 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ForeachAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ForeachAction.cs @@ -37,7 +37,6 @@ public void TakeNext(ProcessActionContext context) if (this.HasValue = (this._index < this._values.Length)) { FormulaValue value = this._values[this._index]; - this._index++; context.Engine.SetScopedVariable(context.Scopes, Throw.IfNull(this.Model.Value), value); @@ -45,6 +44,8 @@ public void TakeNext(ProcessActionContext context) { context.Engine.SetScopedVariable(context.Scopes, this.Model.Index.Path, FormulaValue.New(this._index)); } + + this._index++; } } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SendActivityAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SendActivityAction.cs index 7a95ec232247..faf5a28a919f 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SendActivityAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SendActivityAction.cs @@ -25,13 +25,21 @@ protected override Task HandleAsync(ProcessActionContext context, CancellationTo if (this.Model.Activity is MessageActivityTemplate messageActivity) { - if (!string.IsNullOrEmpty(messageActivity.Summary)) + Console.ForegroundColor = ConsoleColor.Yellow; + try { - this._activityWriter.WriteLine($"\t{messageActivity.Summary}"); - } + if (!string.IsNullOrEmpty(messageActivity.Summary)) + { + this._activityWriter.WriteLine($"\t{messageActivity.Summary}"); + } - string? activityText = context.Engine.Format(messageActivity.Text); - this._activityWriter.WriteLine(activityText + Environment.NewLine); + string? activityText = context.Engine.Format(messageActivity.Text); + this._activityWriter.WriteLine(activityText + Environment.NewLine); + } + finally + { + Console.ResetColor(); + } } return Task.CompletedTask; diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs index d04bc2a4cdab..8d1b426bb999 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs @@ -118,7 +118,7 @@ public override ValueTask ActivateAsync(KernelProcessStepState(TAction model) : ProcessAction(model) @@ -38,12 +38,12 @@ public async Task ExecuteAsync(ProcessActionContext context, CancellationToken c } catch (ProcessWorkflowException exception) { - context.logger.LogError(exception, "*** ACTION [{Id}] ERROR - {TypeName}\n{Message}", this.Id, this.GetType().Name, exception.Message); + context.Logger.LogError(exception, "*** ACTION [{Id}] ERROR - {TypeName}\n{Message}", this.Id, this.GetType().Name, exception.Message); throw; } catch (Exception exception) { - context.logger.LogError(exception, "*** ACTION [{Id}] ERROR - {TypeName}\n{Message}", this.Id, this.GetType().Name, exception.Message); + context.Logger.LogError(exception, "*** ACTION [{Id}] ERROR - {TypeName}\n{Message}", this.Id, this.GetType().Name, exception.Message); throw new ProcessWorkflowException($"Unexpected failure executing action #{this.Id} [{this.GetType().Name}]", exception); } } From 7caa5e674a069141c0d225f7bec11d9e944ea307 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 28 Jul 2025 22:13:36 -0700 Subject: [PATCH 28/40] Demo --- dotnet/SK-dotnet.slnx | 1 + .../DeclarativeWorkflow.csproj | 35 +++++++++ .../Demos/DeclarativeWorkflow/Program.cs | 75 +++++++++++++++++++ .../Demos/DeclarativeWorkflow/demo250729.yaml | 57 ++++++++++++++ .../Demos/DeclarativeWorkflow/readme.md | 24 ++++++ 5 files changed, 192 insertions(+) create mode 100644 dotnet/samples/Demos/DeclarativeWorkflow/DeclarativeWorkflow.csproj create mode 100644 dotnet/samples/Demos/DeclarativeWorkflow/Program.cs create mode 100644 dotnet/samples/Demos/DeclarativeWorkflow/demo250729.yaml create mode 100644 dotnet/samples/Demos/DeclarativeWorkflow/readme.md diff --git a/dotnet/SK-dotnet.slnx b/dotnet/SK-dotnet.slnx index b662baf25562..cc045e5d73ff 100644 --- a/dotnet/SK-dotnet.slnx +++ b/dotnet/SK-dotnet.slnx @@ -35,6 +35,7 @@ + diff --git a/dotnet/samples/Demos/DeclarativeWorkflow/DeclarativeWorkflow.csproj b/dotnet/samples/Demos/DeclarativeWorkflow/DeclarativeWorkflow.csproj new file mode 100644 index 000000000000..071493533e0e --- /dev/null +++ b/dotnet/samples/Demos/DeclarativeWorkflow/DeclarativeWorkflow.csproj @@ -0,0 +1,35 @@ + + + + Exe + net8.0 + enable + enable + CA2007;CS0612;VSTHRD111;SKEXP0080 + 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0 + + + + + + + + + + + + + + + + + + + + + + Always + + + + diff --git a/dotnet/samples/Demos/DeclarativeWorkflow/Program.cs b/dotnet/samples/Demos/DeclarativeWorkflow/Program.cs new file mode 100644 index 000000000000..f33efe5e54f3 --- /dev/null +++ b/dotnet/samples/Demos/DeclarativeWorkflow/Program.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics; +using System.Reflection; +using Azure.Identity; +using Microsoft.Extensions.Configuration; +using Microsoft.SemanticKernel; + +namespace Demo.DeclarativeWorkflow; + +internal static class Program +{ + private const string InputEventId = "start-workflow"; + + public static async Task Main(string[] args) + { + // Load configuration and create kernel with Azure OpenAI Chat Completion service + IConfiguration config = InitializeConfig(); + Kernel kernel = CreateKernel(config["AzureOpenAI:Endpoint"]!, config["AzureOpenAI:ChatDeploymentName"]!); + + Notify("PROCESS INIT\n"); + + Stopwatch timer = Stopwatch.StartNew(); + + // Interpret the workflow YAML into a KernelProcess + using StreamReader yamlReader = File.OpenText("demo250729.yaml"); + KernelProcess process = ObjectModelBuilder.Build(yamlReader, InputEventId); + + Notify($"\nPROCESS DEFINED: {timer.Elapsed}\n"); + + Notify("\nPROCESS INVOKE\n"); + + // Run the process, just like any other KernelProcess + await using LocalKernelProcessContext context = + await process.StartAsync( + kernel, + new KernelProcessEvent() + { + Id = InputEventId, + // Pass the first argument as the input data for the process, if present. + Data = args.FirstOrDefault() ?? string.Empty + }); + + Notify("\nPROCESS DONE"); + } + + // Load configuration from user-secrets + private static IConfigurationRoot InitializeConfig() => + new ConfigurationBuilder() + .AddUserSecrets(Assembly.GetExecutingAssembly()) + .Build(); + + // Create kernel with Azure OpenAI Chat Completion service + private static Kernel CreateKernel(string endpoint, string model) + { + IKernelBuilder kernelBuilder = Kernel.CreateBuilder(); + + kernelBuilder.AddAzureOpenAIChatCompletion(model, endpoint, new AzureCliCredential()); + + return kernelBuilder.Build(); + } + + private static void Notify(string message) + { + Console.ForegroundColor = ConsoleColor.Cyan; + try + { + Console.WriteLine(message); + } + finally + { + Console.ResetColor(); + } + } +} diff --git a/dotnet/samples/Demos/DeclarativeWorkflow/demo250729.yaml b/dotnet/samples/Demos/DeclarativeWorkflow/demo250729.yaml new file mode 100644 index 000000000000..611ec7912b13 --- /dev/null +++ b/dotnet/samples/Demos/DeclarativeWorkflow/demo250729.yaml @@ -0,0 +1,57 @@ +kind: AdaptiveDialog +beginDialog: + kind: OnActivity + id: activity_xyz123 + actions: + + # Capture optional agent instructions + - kind: SetVariable + id: setVariable_NZ2u0l + variable: Topic.Instructions + value: =System.LastMessage.Text + + # Assign a list of inputs in JSON format to a variable + - kind: SetVariable + id: setVariable_aASlmF + displayName: List all of questions for LLM + variable: Topic.Questions + value: |- + =[ + "Why is the sky blue?", + "What is the capital of France?", + "Where do rainbows come from?", + ] + + # Loop over each question in the list + - kind: Foreach + id: foreach_mVIecC + items: =Topic.Questions + index: Topic.LoopIndex + value: Topic.Question + actions: + + # Display the current question + - kind: SendActivity + id: sendActivity_lMn07p + activity: "Question {Topic.LoopIndex + 1} - {Topic.Question}" + + # Use AI to answer the question + - kind: AnswerQuestionWithAI + id: question_wEJ456 + variable: Topic.Answer + userInput: =Topic.Question + additionalInstructions: "{Topic.Instructions}" + + # Display the AI's answer + - kind: SendActivity + id: sendActivity_zA3f0p + activity: "AI - {Topic.Answer}" + + # After processing all questions, display a completion message + - kind: SendActivity + id: sendActivity_SVoNSV + activity: Complete! + + # End the conversation + - kind: EndConversation + id: end_8nXE8H diff --git a/dotnet/samples/Demos/DeclarativeWorkflow/readme.md b/dotnet/samples/Demos/DeclarativeWorkflow/readme.md new file mode 100644 index 000000000000..dc4d8ee9b6e4 --- /dev/null +++ b/dotnet/samples/Demos/DeclarativeWorkflow/readme.md @@ -0,0 +1,24 @@ +# Summary + +This demo showcases the ability to parse a YAML workflow based on Copilo Studio actions +and produce a `KernelProcess` that can be executed in the same fashion as any other `KernelProcess`. + +## Key Features + +This demo illustrates the following capabilities: + +- Parse YAML workflow actions using `Microsoft.Bot.ObjectModel` +- Store and retrieve variable state +- Evaluate expressions using `Microsoft.PowerFx.Interpreter` +- Support control flow (foreach, goto, etc...) +- Generate response from LLM using _Semantic Kernel_ + +## Status Details + +- This is using a POC based on the _Process Framework_ from the _Semantic Kernel_ repo. + - When the redesigned _Process Framework_ is available in the _Agent Framework_ repo it must + be re-implemented using the new API patterns. + - Capturing and restoring workflow state is not yet available in either version of the _Process Framework_. + - The ability to emit events from the _KernelProcess_ to the host API is not yet supported. +- `Microsoft.Bot.ObjectModel` is not (yet) available as a dependency that may be referenced by a _GitHub_ repository. +- The full set of CPSDL actions to be supported is not fully defined, nor are the "Pri-0" samples. From a2eba0fe431bb8cfadd7ca829470107ebcd0f128 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 28 Jul 2025 23:22:06 -0700 Subject: [PATCH 29/40] Namespace --- .../Workflow/ObjectModel/Validation/ActionValidationFailure.cs | 2 +- .../Workflow/ObjectModel/Validation/ElementValidationFailure.cs | 2 +- .../ObjectModel/Validation/ExceptionValidationFailure.cs | 2 +- .../Workflow/ObjectModel/Validation/ProcessValidationWalker.cs | 2 +- .../Workflow/ObjectModel/Validation/ValidationFailure.cs | 2 +- .../Process.UnitTests/Workflow/WorkflowValidationTests.cs | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ActionValidationFailure.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ActionValidationFailure.cs index 4e094d189a5a..03fc4ffc45be 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ActionValidationFailure.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ActionValidationFailure.cs @@ -2,7 +2,7 @@ using Microsoft.Bot.ObjectModel; -namespace Microsoft.SemanticKernel.Process.Workflow.ObjectModel.Validation; +namespace Microsoft.SemanticKernel.Process.Workflow.Validation; /// /// Represents a validation failure based on a . diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ElementValidationFailure.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ElementValidationFailure.cs index 2de2ac647763..9c54c54293aa 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ElementValidationFailure.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ElementValidationFailure.cs @@ -2,7 +2,7 @@ using Microsoft.Bot.ObjectModel; -namespace Microsoft.SemanticKernel.Process.Workflow.ObjectModel.Validation; +namespace Microsoft.SemanticKernel.Process.Workflow.Validation; /// /// Represents a validation failure that is associated with a specific . diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ExceptionValidationFailure.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ExceptionValidationFailure.cs index 6a68e975c06f..36d599ba46d7 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ExceptionValidationFailure.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ExceptionValidationFailure.cs @@ -2,7 +2,7 @@ using System; -namespace Microsoft.SemanticKernel.Process.Workflow.ObjectModel.Validation; +namespace Microsoft.SemanticKernel.Process.Workflow.Validation; /// /// Represents a validation failure that is associated with an exception. diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ProcessValidationWalker.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ProcessValidationWalker.cs index 9fd3db2301c7..97dbabe21b65 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ProcessValidationWalker.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ProcessValidationWalker.cs @@ -5,7 +5,7 @@ using System.Collections.Immutable; using Microsoft.Bot.ObjectModel; -namespace Microsoft.SemanticKernel.Process.Workflow.ObjectModel.Validation; +namespace Microsoft.SemanticKernel.Process.Workflow.Validation; internal sealed class ProcessValidationWalker : BotElementWalker { diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ValidationFailure.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ValidationFailure.cs index 2c3694b968a9..3c3f28da5b47 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ValidationFailure.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ValidationFailure.cs @@ -1,6 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. -namespace Microsoft.SemanticKernel.Process.Workflow.ObjectModel.Validation; +namespace Microsoft.SemanticKernel.Process.Workflow.Validation; /// /// Represents a failure that occurred during validation. diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/WorkflowValidationTests.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/WorkflowValidationTests.cs index e681dc589447..83c100869689 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Workflow/WorkflowValidationTests.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/WorkflowValidationTests.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Immutable; using System.IO; -using Microsoft.SemanticKernel.Process.Workflow.ObjectModel.Validation; +using Microsoft.SemanticKernel.Process.Workflow.Validation; using Xunit; using Xunit.Abstractions; From e20766729d20017888e834c592b30a8c8b46a3f1 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 28 Jul 2025 23:25:29 -0700 Subject: [PATCH 30/40] Namespace --- .../Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs index 8d1b426bb999..06bfed2721dd 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs @@ -8,7 +8,7 @@ using Microsoft.Bot.ObjectModel; using Microsoft.Bot.ObjectModel.Yaml; using Microsoft.PowerFx.Types; -using Microsoft.SemanticKernel.Process.Workflow.ObjectModel.Validation; +using Microsoft.SemanticKernel.Process.Workflow.Validation; using Microsoft.SemanticKernel.Process.Workflows; namespace Microsoft.SemanticKernel; From c10012eb237a0ace7a3a13372b0939857951415e Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 30 Jul 2025 13:21:44 -0700 Subject: [PATCH 31/40] Demo comments --- dotnet/samples/Demos/DeclarativeWorkflow/Program.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/dotnet/samples/Demos/DeclarativeWorkflow/Program.cs b/dotnet/samples/Demos/DeclarativeWorkflow/Program.cs index f33efe5e54f3..b7955aa66fd7 100644 --- a/dotnet/samples/Demos/DeclarativeWorkflow/Program.cs +++ b/dotnet/samples/Demos/DeclarativeWorkflow/Program.cs @@ -16,21 +16,27 @@ public static async Task Main(string[] args) { // Load configuration and create kernel with Azure OpenAI Chat Completion service IConfiguration config = InitializeConfig(); + // Note: "Kernel" isn't required as part of the new "process framework". Kernel kernel = CreateKernel(config["AzureOpenAI:Endpoint"]!, config["AzureOpenAI:ChatDeploymentName"]!); Notify("PROCESS INIT\n"); Stopwatch timer = Stopwatch.StartNew(); + ////////////////////////////////////////////// // Interpret the workflow YAML into a KernelProcess using StreamReader yamlReader = File.OpenText("demo250729.yaml"); KernelProcess process = ObjectModelBuilder.Build(yamlReader, InputEventId); + ////////////////////////////////////////////// Notify($"\nPROCESS DEFINED: {timer.Elapsed}\n"); Notify("\nPROCESS INVOKE\n"); + ////////////////////////////////////////////// // Run the process, just like any other KernelProcess + // NOTE: The pattern here is expected to change in the new "process framework" + // (ideally, it will become less complex) await using LocalKernelProcessContext context = await process.StartAsync( kernel, @@ -40,6 +46,7 @@ await process.StartAsync( // Pass the first argument as the input data for the process, if present. Data = args.FirstOrDefault() ?? string.Empty }); + ////////////////////////////////////////////// Notify("\nPROCESS DONE"); } From b65fcc94ddfbbbcae83ac8ee0f6fa76aeb8309ac Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 1 Aug 2025 10:51:28 -0700 Subject: [PATCH 32/40] Dev sample --- .../Step06/Step06_WorkflowProcess.cs | 1 + .../Step06/testExpression.yaml | 37 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step06/testExpression.yaml diff --git a/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs b/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs index 31887aedeae7..9857f1474b63 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs @@ -18,6 +18,7 @@ public Step06_WorkflowProcess(ITestOutputHelper output) [InlineData("testGoto")] [InlineData("testLoop")] [InlineData("testCondition")] + [InlineData("testExpression")] [InlineData("deepResearch")] [InlineData("demo250729")] public async Task RunWorkflow(string fileName) diff --git a/dotnet/samples/GettingStartedWithProcesses/Step06/testExpression.yaml b/dotnet/samples/GettingStartedWithProcesses/Step06/testExpression.yaml new file mode 100644 index 000000000000..2db7bc7a199e --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step06/testExpression.yaml @@ -0,0 +1,37 @@ +kind: AdaptiveDialog +beginDialog: + kind: OnActivity + id: activity_xyz123 + type: Message + actions: + - kind: SetVariable + id: setVariable1 + variable: Topic.TestList + value: =["zaz", "zbz", "zcz", "zdz", "zez", "zfz"] + + - kind: SetVariable + id: setVariable2 + variable: Topic.TestResult + value: |- + =CountIf(Topic.TestList, !IsBlank(Find("e", Value))) > 0 && + !IsBlank(3) + +# value: =CountIf(Topic.TestList, !IsBlank(Find("e", Value))) > 0 + #value: =CountIf(Topic.TestList, 1) +# value: =!IsBlank(Topic.TestList) + + - kind: SetVariable + id: setVariable3 + variable: Topic.TestFind + value: =Find("e", "abcdefg") + #value: =CountIf(Topic.TestList, 1) +# value: =!IsBlank(Topic.TestList) + + - kind: SendActivity + id: sendActivity2 + activity: "Result (CountIf): {Topic.TestResult}" + + + - kind: SendActivity + id: sendActivity3 + activity: "Result (Find): {Topic.TestFind}" From 1dfb5b9cbf5bee0f2ea31e45ad39681530081d5a Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 1 Aug 2025 10:57:07 -0700 Subject: [PATCH 33/40] Typo --- .../Workflow/ObjectModel/Actions/ClearAllVariablesAction.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ClearAllVariablesAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ClearAllVariablesAction.cs index 89136c12e936..424a554c884b 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ClearAllVariablesAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ClearAllVariablesAction.cs @@ -18,7 +18,7 @@ protected override Task HandleAsync(ProcessActionContext context, CancellationTo { DataValue literalValue = this.Model.Variables.GetLiteralValue(); // %%% DON'T USE GetLiteralValue - context.Engine.ClearScope(context.Scopes, ActionScopeType.Topic); // %%% EVALUTE "Variables" + context.Engine.ClearScope(context.Scopes, ActionScopeType.Topic); // %%% EVALUATE "Variables" return Task.CompletedTask; } From 06b2ce23d1fa52ff2dc59f41b92c7e9e661307dc Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 1 Aug 2025 11:09:29 -0700 Subject: [PATCH 34/40] Contetx rename / update --- .../Workflow/ObjectModel/HostContext.cs | 29 ------------ .../ObjectModel/ObjectModelBuilder.cs | 4 +- .../PowerFx/RecalcEngineFactory.cs | 21 ++++++--- .../ObjectModel/ProcessActionVisitor.cs | 4 +- .../Workflow/ObjectModel/WorkflowContext.cs | 46 +++++++++++++++++++ 5 files changed, 65 insertions(+), 39 deletions(-) delete mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/HostContext.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/WorkflowContext.cs diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/HostContext.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/HostContext.cs deleted file mode 100644 index 19b6277f9969..000000000000 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/HostContext.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.IO; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace Microsoft.SemanticKernel; - -/// -/// Provides configuration and services for workflow execution, including logging and output channels. -/// -public sealed class HostContext -{ - /// - /// Gets the maximum allowed length for expressions evaluated in the workflow. - /// - public int MaximumExpressionLength { get; init; } = 3000; - - /// - /// Gets the used for activity output and diagnostics. - /// - public TextWriter ActivityChannel { get; init; } = Console.Out; - - /// - /// Gets the used to create loggers for workflow components. - /// - public ILoggerFactory LoggerFactory { get; init; } = NullLoggerFactory.Instance; -} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs index 06bfed2721dd..6dbefe7bdbb1 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs @@ -62,7 +62,7 @@ static ImmutableArray SingleFailure(ValidationFailure failure /// The identifier for the message. /// The hosting context for the workflow. /// The that corresponds with the YAML object model. - public static KernelProcess Build(TextReader yamlReader, string messageId, HostContext? context = null) + public static KernelProcess Build(TextReader yamlReader, string messageId, WorkflowContext? context = null) { Console.WriteLine("@ PARSING YAML"); BotElement rootElement = YamlSerializer.Deserialize(yamlReader) ?? throw new KernelException("Unable to parse YAML content."); @@ -75,7 +75,7 @@ public static KernelProcess Build(TextReader yamlReader, string messageId, HostC processBuilder.OnInputEvent(messageId).SendEventTo(new ProcessFunctionTargetBuilder(initStep)); Console.WriteLine("@ INTERPRETING MODEL"); - ProcessActionVisitor visitor = new(processBuilder, context ?? new HostContext(), scopes); + ProcessActionVisitor visitor = new(processBuilder, context ?? new WorkflowContext(), scopes); ProcessActionWalker walker = new(rootElement, visitor); Console.WriteLine("@ FINALIZING PROCESS"); diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineFactory.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineFactory.cs index 0d9e562eeaec..6cd216f98394 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineFactory.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineFactory.cs @@ -7,7 +7,10 @@ namespace Microsoft.SemanticKernel.Process.Workflows.PowerFx; internal static class RecalcEngineFactory { - public static RecalcEngine Create(ProcessActionScopes scopes, int maximumExpressionLength) + public static RecalcEngine Create( + ProcessActionScopes scopes, + int? maximumExpressionLength = null, + int? maximumCallDepth = null) { RecalcEngine engine = new(CreateConfig()); @@ -25,11 +28,17 @@ void SetScope(ActionScopeType scope) PowerFxConfig CreateConfig() { - PowerFxConfig config = - new(Features.PowerFxV1) - { - MaximumExpressionLength = maximumExpressionLength - }; + PowerFxConfig config = new(Features.PowerFxV1); + + if (maximumExpressionLength is not null) + { + config.MaximumExpressionLength = maximumExpressionLength.Value; + } + + if (maximumCallDepth is not null) + { + config.MaxCallDepth = maximumCallDepth.Value; + } config.EnableSetFunction(); diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs index ed88ce66c03a..655db66a4fd5 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs @@ -16,12 +16,12 @@ internal sealed class ProcessActionVisitor : DialogActionVisitor private readonly ProcessBuilder _processBuilder; private readonly ProcessWorkflowBuilder _workflowBuilder; private readonly ProcessActionStack _actionStack; - private readonly HostContext _context; + private readonly WorkflowContext _context; private readonly ProcessActionScopes _scopes; public ProcessActionVisitor( ProcessBuilder processBuilder, - HostContext context, + WorkflowContext context, ProcessActionScopes scopes) { this._actionStack = new ProcessActionStack(); diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/WorkflowContext.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/WorkflowContext.cs new file mode 100644 index 000000000000..f3e5429947cb --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/WorkflowContext.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.IO; +using Azure.Core; +using Azure.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Microsoft.SemanticKernel; + +/// +/// Provides configuration and context for workflow execution. +/// +public sealed class WorkflowContext +{ + /// + /// Defines the endpoint for the Foundry project. + /// + public string ProjectEndpoint { get; init; } = string.Empty; + + /// + /// Defines the credentials that authorize access to the Foundry project. + /// + public TokenCredential ProjectCredentials { get; init; } = new DefaultAzureCredential(); + + /// + /// Defines the maximum number of nested calls allowed in a PowerFx formula. + /// + public int? MaximumCallDepth { get; init; } + + /// + /// Defines the maximum allowed length for expressions evaluated in the workflow. + /// + public int? MaximumExpressionLength { get; init; } + + /// + /// Gets the used to create loggers for workflow components. + /// + public ILoggerFactory LoggerFactory { get; init; } = NullLoggerFactory.Instance; + + /// + /// Gets the used for activity output and diagnostics. + /// + public TextWriter ActivityChannel { get; init; } = Console.Out; // %%% REMOVE: For POC only +} From b0e5c5be0e52c0dc1531baeb6daa17016fa24d8b Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 5 Aug 2025 10:13:22 -0700 Subject: [PATCH 35/40] Clean-up debug trace --- .../ObjectModel/ProcessActionVisitor.cs | 20 +++++++++---------- .../Process.Core/Workflow/WorkflowBuilder.cs | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs index ed88ce66c03a..ded00595898d 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs @@ -94,9 +94,9 @@ protected override void Visit(GotoAction item) this.Trace(item, isSkipped: false); string parentId = item.GetParentId(); - this.ContinueWith(this.CreateStep(item.Id.Value), parentId); + this.ContinueWith(this.CreateStep(item.Id.Value, nameof(GotoAction)), parentId); this._workflowBuilder.AddLink(item.Id.Value, item.ActionId.Value); - this.RestartFrom(item.Id.Value, parentId); + this.RestartFrom(item.Id.Value, nameof(GotoAction), parentId); } protected override void Visit(Foreach item) @@ -107,13 +107,13 @@ protected override void Visit(Foreach item) this.ContinueWith(action); string restartId = this.RestartFrom(action); string loopId = $"next_{action.Id}"; - this.ContinueWith(this.CreateStep(loopId, action.TakeNext), action.Id, callback: CompletionHandler); + this.ContinueWith(this.CreateStep(loopId, $"{nameof(ForeachAction)}_Next", action.TakeNext), action.Id, callback: CompletionHandler); this._workflowBuilder.AddLink(loopId, restartId, () => !action.HasValue); - this.ContinueWith(this.CreateStep($"start_{action.Id}"), action.Id, () => action.HasValue); + this.ContinueWith(this.CreateStep($"start_{action.Id}", $"{nameof(ForeachAction)}_Start"), action.Id, () => action.HasValue); void CompletionHandler(string scopeId) { string completionId = $"end_{action.Id}"; - this.ContinueWith(this.CreateStep(completionId), action.Id); + this.ContinueWith(this.CreateStep(completionId, $"{nameof(ForeachAction)}_End"), action.Id); this._workflowBuilder.AddLink(completionId, loopId); } } @@ -395,23 +395,23 @@ private void ContinueWith( } private string RestartFrom(ProcessAction action) => - this.RestartFrom(action.Id, action.ParentId); + this.RestartFrom(action.Id, action.GetType().Name, action.ParentId); - private string RestartFrom(string actionId, string parentId) + private string RestartFrom(string actionId, string name, string parentId) { string restartId = $"post_{actionId}"; - this._workflowBuilder.AddNode(this.CreateStep(restartId), parentId); + this._workflowBuilder.AddNode(this.CreateStep(restartId, $"{name}_Restart"), parentId); return restartId; } - private ProcessStepBuilder CreateStep(string actionId, Action? stepAction = null) + private ProcessStepBuilder CreateStep(string actionId, string name, Action? stepAction = null) { return this._processBuilder.AddStepFromFunction( actionId, (kernel, context) => { - Console.WriteLine($"!!! STEP CUSTOM [{actionId}]"); // %%% REMOVE + Console.WriteLine($"!!! STEP {name} [{actionId}]"); // %%% REMOVE stepAction?.Invoke(this.CreateActionContext(actionId, kernel)); return Task.CompletedTask; }); diff --git a/dotnet/src/Experimental/Process.Core/Workflow/WorkflowBuilder.cs b/dotnet/src/Experimental/Process.Core/Workflow/WorkflowBuilder.cs index c2d83082c2b2..c8bb2ce62cc7 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/WorkflowBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/WorkflowBuilder.cs @@ -435,7 +435,7 @@ private static Node BuildNode(KernelProcessStepInfo step, List { From e6caf5952913331b4e72c6674043c8f1615ef093 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 6 Aug 2025 13:50:07 -0700 Subject: [PATCH 36/40] Checkpoint --- .../Actions/AnswerQuestionWithAIAction.cs | 10 +- .../Actions/ClearAllVariablesAction.cs | 33 ++- .../ObjectModel/Actions/EditTableV2Action.cs | 7 +- .../ObjectModel/Actions/ForeachAction.cs | 17 +- .../ObjectModel/Actions/ParseValueAction.cs | 33 ++- .../ObjectModel/Actions/SetVariableAction.cs | 13 +- .../Exceptions/InvalidExpressionException.cs | 35 --- .../Extensions/BotElementExtensions.cs | 7 +- .../Extensions/DataValueExtensions.cs | 50 +++- .../Extensions/FormulaValueExtensions.cs | 132 ++++++---- .../ObjectModel/ObjectModelBuilder.cs | 39 --- .../PowerFx/FoundryExpressionEngine.cs | 243 ++++++++++++++++++ .../PowerFx/RecalcEngineExtensions.cs | 40 +-- .../PowerFx/RecalcEngineFactory.cs | 1 + .../Workflow/ObjectModel/ProcessAction.cs | 33 ++- .../ObjectModel/ProcessActionScopes.cs | 65 ++++- .../Validation/ActionValidationFailure.cs | 27 -- .../Validation/ElementValidationFailure.cs | 39 --- .../Validation/ExceptionValidationFailure.cs | 30 --- .../Validation/ProcessValidationWalker.cs | 46 ---- .../Validation/ValidationFailure.cs | 22 -- .../Actions/ClearAllVariablesActionTest.cs | 6 +- .../Workflow/Actions/ProcessActionTest.cs | 3 +- .../Extensions/FormulaValueExtensionsTests.cs | 147 +++++++++++ .../PowerFx/FoundryExpressionEngineTests.cs | 30 +++ .../Workflow/PowerFx/RecalcEngineTest.cs | 2 +- .../Workflow/WorkflowValidationTests.cs | 147 ----------- 27 files changed, 716 insertions(+), 541 deletions(-) delete mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/InvalidExpressionException.cs create mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/FoundryExpressionEngine.cs delete mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ActionValidationFailure.cs delete mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ElementValidationFailure.cs delete mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ExceptionValidationFailure.cs delete mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ProcessValidationWalker.cs delete mode 100644 dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ValidationFailure.cs create mode 100644 dotnet/src/Experimental/Process.UnitTests/Workflow/Extensions/FormulaValueExtensionsTests.cs create mode 100644 dotnet/src/Experimental/Process.UnitTests/Workflow/PowerFx/FoundryExpressionEngineTests.cs delete mode 100644 dotnet/src/Experimental/Process.UnitTests/Workflow/WorkflowValidationTests.cs diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AnswerQuestionWithAIAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AnswerQuestionWithAIAction.cs index 46646749acc5..6e6d72f74ecb 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AnswerQuestionWithAIAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AnswerQuestionWithAIAction.cs @@ -3,11 +3,11 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; +using Microsoft.Bot.ObjectModel.Abstractions; using Microsoft.Extensions.DependencyInjection; using Microsoft.PowerFx.Types; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Process.Workflows.Extensions; -using Microsoft.SemanticKernel.Process.Workflows.PowerFx; namespace Microsoft.SemanticKernel.Process.Workflows.Actions; @@ -21,11 +21,7 @@ public AnswerQuestionWithAIAction(AnswerQuestionWithAI model) protected override async Task HandleAsync(ProcessActionContext context, CancellationToken cancellationToken) { IChatCompletionService chatCompletion = context.Kernel.Services.GetRequiredService(); - FormulaValue expressionResult = context.Engine.EvaluateExpression(this.Model.UserInput); - if (expressionResult is not StringValue stringResult) - { - throw new InvalidActionException($"{nameof(AnswerQuestionWithAI)} requires text for {nameof(AnswerQuestionWithAI.UserInput)}"); - } + EvaluationResult result = context.ExpressionEngine.GetValue(this.Model.UserInput!, context.Scopes); // %%% FAILURE CASE (CATCH) & NULL OVERRIDE ChatHistory history = []; if (this.Model.AdditionalInstructions is not null) @@ -36,7 +32,7 @@ protected override async Task HandleAsync(ProcessActionContext context, Cancella history.AddSystemMessage(instructions); } } - history.AddUserMessage(stringResult.Value); + history.AddUserMessage(result.Value); ChatMessageContent response = await chatCompletion.GetChatMessageContentAsync(history, cancellationToken: cancellationToken).ConfigureAwait(false); StringValue responseValue = FormulaValue.New(response.ToString()); diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ClearAllVariablesAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ClearAllVariablesAction.cs index 424a554c884b..710d6ef9cd77 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ClearAllVariablesAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ClearAllVariablesAction.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; +using Microsoft.Bot.ObjectModel.Abstractions; using Microsoft.SemanticKernel.Process.Workflows.PowerFx; namespace Microsoft.SemanticKernel.Process.Workflows.Actions; @@ -16,10 +17,38 @@ public ClearAllVariablesAction(ClearAllVariables source) protected override Task HandleAsync(ProcessActionContext context, CancellationToken cancellationToken) { - DataValue literalValue = this.Model.Variables.GetLiteralValue(); // %%% DON'T USE GetLiteralValue + EvaluationResult result = context.ExpressionEngine.GetValue(this.Model.Variables, context.Scopes); // %%% FAILURE CASE (CATCH) & NULL OVERRIDE - context.Engine.ClearScope(context.Scopes, ActionScopeType.Topic); // %%% EVALUATE "Variables" + result.Value.Handle(new ScopeHandler(context)); return Task.CompletedTask; } + + private sealed class ScopeHandler(ProcessActionContext context) : IEnumVariablesToClearHandler + { + public void HandleAllGlobalVariables() + { + context.Engine.ClearScope(context.Scopes, ActionScopeType.Global); + } + + public void HandleConversationHistory() + { + throw new System.NotImplementedException(); // %%% LOG / NO EXCEPTION - Is this to be supported ??? + } + + public void HandleConversationScopedVariables() + { + context.Engine.ClearScope(context.Scopes, ActionScopeType.Topic); + } + + public void HandleUnknownValue() + { + throw new System.NotImplementedException(); // %%% LOG / NO EXCEPTION + } + + public void HandleUserScopedVariables() + { + context.Engine.ClearScope(context.Scopes, ActionScopeType.Env); + } + } } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EditTableV2Action.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EditTableV2Action.cs index 8f34596428df..40858f8fcab7 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EditTableV2Action.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/EditTableV2Action.cs @@ -4,8 +4,9 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; +using Microsoft.Bot.ObjectModel.Abstractions; using Microsoft.PowerFx.Types; -using Microsoft.SemanticKernel.Process.Workflows.PowerFx; +using Microsoft.SemanticKernel.Process.Workflows.Extensions; namespace Microsoft.SemanticKernel.Process.Workflows.Actions; @@ -24,8 +25,8 @@ protected override async Task HandleAsync(ProcessActionContext context, Cancella EditTableOperation? changeType = this.Model.ChangeType; if (changeType is AddItemOperation addItemOperation) { - FormulaValue result = context.Engine.EvaluateExpression(addItemOperation.Value); - RecordValue newRecord = BuildRecord(tableValue.Type.ToRecord(), result); + EvaluationResult result = context.ExpressionEngine.GetValue(addItemOperation.Value!, context.Scopes); // %%% FAILURE CASE (CATCH) & NULL OVERRIDE + RecordValue newRecord = BuildRecord(tableValue.Type.ToRecord(), result.Value.ToFormulaValue()); await tableValue.AppendAsync(newRecord, cancellationToken).ConfigureAwait(false); this.AssignTarget(context, tableValue); } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ForeachAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ForeachAction.cs index f649d5f70df0..b695b6bb0ec9 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ForeachAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ForeachAction.cs @@ -4,7 +4,9 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; +using Microsoft.Bot.ObjectModel.Abstractions; using Microsoft.PowerFx.Types; +using Microsoft.SemanticKernel.Process.Workflows.Extensions; using Microsoft.SemanticKernel.Process.Workflows.PowerFx; namespace Microsoft.SemanticKernel.Process.Workflows.Actions; @@ -24,11 +26,18 @@ public ForeachAction(Foreach model) protected override Task HandleAsync(ProcessActionContext context, CancellationToken cancellationToken) { - // %%% TODO: HACK: Assumes array this._index = 0; - FormulaValue values = context.Engine.EvaluateExpression(this.Model.Items); - TableValue tableValue = (TableValue)values; - this._values = [.. tableValue.Rows.Select(row => row.Value.Fields.First().Value)]; + + if (this.Model.Items is null) + { + this._values = []; + this.HasValue = false; + return Task.CompletedTask; + } + + EvaluationResult result = context.ExpressionEngine.GetValue(this.Model.Items, context.Scopes); + TableDataValue tableValue = (TableDataValue)result.Value; // %%% CAST - TYPE ASSUMPTION (TableDataValue) + this._values = [.. tableValue.Values.Select(value => value.Properties.Values.First().ToFormulaValue())]; return Task.CompletedTask; } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ParseValueAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ParseValueAction.cs index 219aaac4c0e7..80cfd23c7768 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ParseValueAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ParseValueAction.cs @@ -4,9 +4,9 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; +using Microsoft.Bot.ObjectModel.Abstractions; using Microsoft.PowerFx.Types; using Microsoft.SemanticKernel.Process.Workflows.Extensions; -using Microsoft.SemanticKernel.Process.Workflows.PowerFx; namespace Microsoft.SemanticKernel.Process.Workflows.Actions; @@ -23,26 +23,33 @@ public ParseValueAction(ParseValue model) protected override Task HandleAsync(ProcessActionContext context, CancellationToken cancellationToken) { - FormulaValue result = context.Engine.EvaluateExpression(this.Model.Value); + EvaluationResult result = context.ExpressionEngine.GetValue(this.Model.Value!, context.Scopes); // %%% FAILURE CASE (CATCH) & NULL OVERRIDE FormulaValue? parsedResult = null; - if (result is StringValue stringValue) + if (result.Value is StringDataValue stringValue) { - parsedResult = - this.Model.ValueType switch - { - StringDataType => stringValue, - NumberDataType => NumberValue.New(stringValue.Value), - BooleanDataType => BooleanValue.New(stringValue.Value), - RecordDataType recordType => ParseRecord(recordType, stringValue.Value), - _ => null - }; + if (string.IsNullOrWhiteSpace(stringValue.Value)) + { + parsedResult = FormulaValue.NewBlank(); + } + else + { + parsedResult = + this.Model.ValueType switch + { + StringDataType => StringValue.New(stringValue.Value), + NumberDataType => NumberValue.New(stringValue.Value), + BooleanDataType => BooleanValue.New(stringValue.Value), + RecordDataType recordType => ParseRecord(recordType, stringValue.Value), + _ => null + }; + } } if (parsedResult is null) { - throw new ProcessActionException($"Unable to parse {result.Type.GetType().Name}"); + throw new ProcessActionException($"Unable to parse {result.Value.GetType().Name}"); } this.AssignTarget(context, parsedResult); diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetVariableAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetVariableAction.cs index 6fcb56607396..61f60c20de91 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetVariableAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/SetVariableAction.cs @@ -3,9 +3,9 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; +using Microsoft.Bot.ObjectModel.Abstractions; using Microsoft.PowerFx.Types; using Microsoft.SemanticKernel.Process.Workflows.Extensions; -using Microsoft.SemanticKernel.Process.Workflows.PowerFx; namespace Microsoft.SemanticKernel.Process.Workflows.Actions; @@ -18,9 +18,16 @@ public SetVariableAction(SetVariable model) protected override Task HandleAsync(ProcessActionContext context, CancellationToken cancellationToken) { - FormulaValue result = context.Engine.EvaluateExpression(this.Model.Value).ThrowIfError(); + if (this.Model.Value is null) + { + this.AssignTarget(context, FormulaValue.NewBlank()); + } + else + { + EvaluationResult result = context.ExpressionEngine.GetValue(this.Model.Value, context.Scopes); // %%% FAILURE CASE (CATCH) - this.AssignTarget(context, result); + this.AssignTarget(context, result.Value.ToFormulaValue()); + } return Task.CompletedTask; } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/InvalidExpressionException.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/InvalidExpressionException.cs deleted file mode 100644 index bc1847271026..000000000000 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Exceptions/InvalidExpressionException.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; - -namespace Microsoft.SemanticKernel.Process.Workflows; - -/// -/// Represents an exception that occurs when is invalid and cannot be evaluated. -/// -public sealed class InvalidExpressionException : ProcessWorkflowException -{ - /// - /// Initializes a new instance of the class. - /// - public InvalidExpressionException() - { - } - - /// - /// Initializes a new instance of the class with a specified error message. - /// - /// The error message that explains the reason for the exception. - public InvalidExpressionException(string? message) : base(message) - { - } - - /// - /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. - public InvalidExpressionException(string? message, Exception? innerException) : base(message, innerException) - { - } -} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/BotElementExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/BotElementExtensions.cs index 57b0ebfe2616..bbed0205f0df 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/BotElementExtensions.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/BotElementExtensions.cs @@ -6,17 +6,14 @@ namespace Microsoft.SemanticKernel.Process.Workflows.Extensions; internal static class DataValueExtensions { - public static string GetParentId(this BotElement element) => - element.Parent is null ? - throw new InvalidActionException($"Undefined parent for {element.GetType().Name} that is member of {element.GetId()}.") : - element.Parent.GetId(); + public static string? GetParentId(this BotElement element) => element.Parent?.GetId(); public static string GetId(this BotElement element) { return element switch { DialogAction action => action.Id.Value, - ConditionItem conditionItem => conditionItem.Id ?? throw new InvalidActionException($"Undefined identifier for {nameof(ConditionItem)} that is member of {conditionItem.GetParentId()}."), + ConditionItem conditionItem => conditionItem.Id ?? throw new InvalidActionException($"Undefined identifier for {nameof(ConditionItem)} that is member of {conditionItem.GetParentId() ?? "(root)"}."), OnActivity activity => activity.Id.Value, _ => throw new InvalidActionException($"Unknown element type: {element.GetType().Name}"), }; diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/DataValueExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/DataValueExtensions.cs index b7360b7621cd..46ac2a79e0bc 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/DataValueExtensions.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/DataValueExtensions.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; +using System.Linq; using Microsoft.Bot.ObjectModel; using Microsoft.PowerFx.Types; @@ -11,14 +13,52 @@ public static FormulaValue ToFormulaValue(this DataValue? value) => value switch { null => FormulaValue.NewBlank(), - StringDataValue stringValue => FormulaValue.New(stringValue.Value), - NumberDataValue numberValue => FormulaValue.New(numberValue.Value), + BlankDataValue blankValue => BlankValue.NewBlank(), BooleanDataValue boolValue => FormulaValue.New(boolValue.Value), + NumberDataValue numberValue => FormulaValue.New(numberValue.Value), + FloatDataValue floatValue => FormulaValue.New(floatValue.Value), + StringDataValue stringValue => FormulaValue.New(stringValue.Value), DateTimeDataValue dateTimeValue => FormulaValue.New(dateTimeValue.Value.DateTime), - DateDataValue dateValue => FormulaValue.New(dateValue.Value), + DateDataValue dateValue => FormulaValue.NewDateOnly(dateValue.Value), TimeDataValue timeValue => FormulaValue.New(timeValue.Value), - //RecordDataValue recordValue => FormulaValue.NewRecordFromFields(recordValue.Properties), // %%% SUPPORT - //TableDataValue tableValue => FormulaValue.NewTable(), // %%% SUPPORT + TableDataValue tableValue => FormulaValue.NewTable(ParseRecordType(tableValue.Values.First()), tableValue.Values.Select(value => value.ToRecordValue())), // %%% TODO: RecordType + RecordDataValue recordValue => recordValue.ToRecordValue(), + //FileDataValue // %%% SUPPORT ??? + //OptionDataValue // %%% SUPPORT - Enum ??? _ => FormulaValue.NewError(new Microsoft.PowerFx.ExpressionError { Message = $"Unknown literal type: {value.GetType().Name}" }), }; + + public static FormulaType ToFormulaType(this DataType? type) => + type switch + { + null => FormulaType.Blank, + BooleanDataType => FormulaType.Boolean, + NumberDataType => FormulaType.Number, + FloatDataType => FormulaType.Decimal, + StringDataType => FormulaType.String, + DateTimeDataType => FormulaType.DateTime, + DateDataType => FormulaType.Date, + TimeDataType => FormulaType.Time, + //TableDataType => new TableType(), %%% ELEMENT TYPE + RecordDataType => RecordType.Empty(), + //FileDataType // %%% SUPPORT ??? + //OptionDataType // %%% SUPPORT - Enum ??? + DataType dataType => FormulaType.Blank, // %%% HANDLE ??? (FALLTHROUGH???) + //_ => FormulaType.Unknown, + }; + + public static RecordValue ToRecordValue(this RecordDataValue recordDataValue) => + FormulaValue.NewRecordFromFields( + recordDataValue.Properties.Select( + property => new NamedValue(property.Key, property.Value.ToFormulaValue()))); + + private static RecordType ParseRecordType(RecordDataValue record) + { + RecordType recordType = RecordType.Empty(); + foreach (KeyValuePair property in record.Properties) + { + recordType.Add(property.Key, property.Value.GetDataType().ToFormulaType()); + } + return recordType; + } } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/FormulaValueExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/FormulaValueExtensions.cs index 0e47e33966db..f05a4c87f488 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/FormulaValueExtensions.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/FormulaValueExtensions.cs @@ -3,8 +3,11 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Drawing; using System.Linq; +using Microsoft.Bot.ObjectModel; using Microsoft.PowerFx.Types; +using BlankType = Microsoft.PowerFx.Types.BlankType; namespace Microsoft.SemanticKernel.Process.Workflows.Extensions; @@ -12,70 +15,85 @@ namespace Microsoft.SemanticKernel.Process.Workflows.Extensions; internal static class FormulaValueExtensions { - private static readonly ImmutableDictionary s_handlers = - new Dictionary() + public static DataValue GetDataValue(this FormulaValue value) => + value switch { - { typeof(BooleanValue), FromBooleanValue }, - { typeof(DecimalValue), FromDecimalValue }, - { typeof(DateValue), FromDateTimeValue }, - { typeof(RecordValue), FromRecordValue }, - { typeof(StringValue), FromStringValue }, - }.ToImmutableDictionary(); + BooleanValue booleanValue => booleanValue.ToDataValue(), + DecimalValue decimalValue => decimalValue.ToDataValue(), + NumberValue numberValue => numberValue.ToDataValue(), + DateValue dateValue => dateValue.ToDataValue(), + DateTimeValue datetimeValue => datetimeValue.ToDataValue(), + TimeValue timeValue => timeValue.ToDataValue(), + StringValue stringValue => stringValue.ToDataValue(), + GuidValue guidValue => guidValue.ToDataValue(), // %%% CORRECT ??? + BlankValue blankValue => blankValue.ToDataValue(), + VoidValue voidValue => voidValue.ToDataValue(), + TableValue tableValue => tableValue.ToDataValue(), + RecordValue recordValue => recordValue.ToDataValue(), + //BlobValue // %%% DataValue ??? + //ErrorValue // %%% DataValue ??? + _ => throw new NotSupportedException($"Unsupported FormulaValue type: {value.GetType().Name}"), + }; - public static FormulaValue ThrowIfError(this FormulaValue value) - { - if (value is ErrorValue errorVal) + public static DataType GetDataType(this FormulaValue value) => + value.Type switch { - throw new InvalidExpressionException($"Failure evaluating expression. Error: {errorVal.Errors[0].Message}"); - } + null => DataType.Blank, + BooleanType => DataType.Boolean, + DecimalType => DataType.Number, + NumberType => DataType.Float, + DateType => DataType.Date, + DateTimeType => DataType.DateTime, + TimeType => DataType.Time, + StringType => DataType.String, + GuidType => DataType.String, + BlankType => DataType.String, + RecordType => DataType.EmptyRecord, + //BlobValue // %%% DataType ??? + //ErrorValue // %%% DataType ??? + UnknownType => DataType.Unspecified, + _ => DataType.Unspecified, + }; - return value; - } - - public static string? Format(this FormulaValue value) - { - Type valueType = value.GetType(); - - // Lookup the handler for the specific type first - if (s_handlers.TryGetValue(valueType, out GetFormulaValue? handler)) + public static string? Format(this FormulaValue value) => + value switch { - return $"{handler.Invoke(value)}"; - } + BooleanValue booleanValue => $"{booleanValue.Value}", + DecimalValue decimalValue => $"{decimalValue.Value}", + NumberValue numberValue => $"{numberValue.Value}", + DateValue dateValue => $"{dateValue.GetConvertedValue(TimeZoneInfo.Utc)}", + DateTimeValue datetimeValue => $"{datetimeValue.GetConvertedValue(TimeZoneInfo.Utc)}", + TimeValue timeValue => $"{timeValue.Value}", + StringValue stringValue => $"{stringValue.Value}", + GuidValue guidValue => $"{guidValue.Value}", + BlankValue blankValue => string.Empty, + VoidValue voidValue => string.Empty, + TableValue tableValue => tableValue.ToString(), // %%% WORK ??? + RecordValue recordValue => recordValue.ToString(), + //BlobValue // %%% DataValue ??? + //ErrorValue // %%% DataValue ??? + _ => throw new NotSupportedException($"Unsupported FormulaValue type: {value.GetType().Name}"), + }; - // Could be a derived type, so check all handlers - foreach (KeyValuePair kvp in s_handlers) - { - if (kvp.Key.IsAssignableFrom(valueType)) - { - return $"{kvp.Value.Invoke(value)}"; - } - } + // %%% TODO: Type conversion - return value.ToString(); - } + public static BooleanDataValue ToDataValue(this BooleanValue value) => BooleanDataValue.Create(value.Value); + public static NumberDataValue ToDataValue(this DecimalValue value) => NumberDataValue.Create(value.Value); + public static FloatDataValue ToDataValue(this NumberValue value) => FloatDataValue.Create(value.Value); + public static DateTimeDataValue ToDataValue(this DateTimeValue value) => DateTimeDataValue.Create(value.GetConvertedValue(TimeZoneInfo.Utc)); + public static DateDataValue ToDataValue(this DateValue value) => DateDataValue.Create(value.GetConvertedValue(TimeZoneInfo.Utc)); + public static TimeDataValue ToDataValue(this TimeValue value) => TimeDataValue.Create(value.Value); + public static StringDataValue ToDataValue(this StringValue value) => StringDataValue.Create(value.Value); + public static StringDataValue ToDataValue(this GuidValue value) => StringDataValue.Create(value.Value.ToString("N")); // %%% FORMAT ??? + public static DataValue ToDataValue(this BlankValue _) => BlankDataValue.Blank(); + public static DataValue ToDataValue(this VoidValue _) => BlankDataValue.Blank(); // %%% CORRECT ??? + public static StringDataValue ToDataValue(this ColorValue value) => StringDataValue.Create(Enum.GetName(typeof(Color), value.Value)!); // %%% CORRECT ??? - // %%% TODO: Type conversion - //VoidValue - //NamedValue - //TableValue - //BlobValue - //ErrorValue - //ColorValue - //NumberValue - //TableValue - //BlankValue - //DateValue - //GuidValue - //TimeValue + public static TableDataValue ToDataValue(this TableValue value) => + TableDataValue.TableFromRecords(value.Rows.Select(row => row.Value.ToDataValue()).ToImmutableArray()); + + public static RecordDataValue ToDataValue(this RecordValue value) => + RecordDataValue.RecordFromFields(value.Fields.Select(field => field.GetKeyValuePair()).ToImmutableArray()); - private static object? FromBooleanValue(FormulaValue value) => ((BooleanValue)value).Value; - private static object? FromDecimalValue(FormulaValue value) => ((DecimalValue)value).Value; - private static object? FromDateTimeValue(FormulaValue value) => ((DateValue)value).GetConvertedValue(TimeZoneInfo.Local); - private static object? FromStringValue(FormulaValue value) => ((StringValue)value).Value; - private static object? FromRecordValue(FormulaValue value) => - $""" - [ - {string.Join(Environment.NewLine, ((RecordValue)value).Fields.Select(field => $" {field.Name}={field.Value.Format()}"))} - ] - """; + private static KeyValuePair GetKeyValuePair(this NamedValue value) => new(value.Name, value.Value.GetDataValue()); } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs index 6dbefe7bdbb1..971cbd57e0fc 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ObjectModelBuilder.cs @@ -1,14 +1,12 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Collections.Immutable; using System.ComponentModel; using System.IO; using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; using Microsoft.Bot.ObjectModel.Yaml; using Microsoft.PowerFx.Types; -using Microsoft.SemanticKernel.Process.Workflow.Validation; using Microsoft.SemanticKernel.Process.Workflows; namespace Microsoft.SemanticKernel; @@ -18,43 +16,6 @@ namespace Microsoft.SemanticKernel; /// public static class ObjectModelBuilder { - /// - /// %%% COMMENT - /// - /// The reader that provides the workflow object model YAML. - /// - /// - public static bool TryValidate(TextReader yamlReader, out ImmutableArray failures) - { - BotElement? rootElement = null; - try - { - rootElement = YamlSerializer.Deserialize(yamlReader); - } -#pragma warning disable CA1031 // Do not catch general exception types - catch (Exception exception) -#pragma warning restore CA1031 // Do not catch general exception types - { - failures = SingleFailure(new ExceptionValidationFailure(exception, "Unable to serialize object model")); - return false; - } - if (rootElement is null) - { - failures = SingleFailure(new ValidationFailure("Root element is null. Ensure the YAML content is valid.")); - return false; - } - - ProcessValidationWalker walker = new(rootElement); - failures = walker.Failures; - return walker.IsValid; - - static ImmutableArray SingleFailure(ValidationFailure failure) - { - ValidationFailure[] failures = [failure]; - return failures.ToImmutableArray(); - } - } - /// /// Builds a process from the provided YAML definition of a CPS Topic ObjectModel. /// diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/FoundryExpressionEngine.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/FoundryExpressionEngine.cs new file mode 100644 index 000000000000..aa3a67431be0 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/FoundryExpressionEngine.cs @@ -0,0 +1,243 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Text.Json; +using Microsoft.Bot.ObjectModel; +using Microsoft.Bot.ObjectModel.Abstractions; +using Microsoft.Bot.ObjectModel.Exceptions; +using Microsoft.PowerFx; +using Microsoft.PowerFx.Types; +using Microsoft.SemanticKernel.Process.Workflows.Extensions; + +namespace Microsoft.SemanticKernel.Process.Workflows.PowerFx; + +internal class FoundryExpressionEngine : IExpressionEngine +{ + private static readonly JsonSerializerOptions s_options = new(); // %%% INVESTIGATE: ElementSerializer.CreateOptions(); + + private readonly RecalcEngine _engine; + + public FoundryExpressionEngine(RecalcEngine engine) + { + this._engine = engine; + } + + public EvaluationResult GetValue(BoolExpression boolean, ProcessActionScopes state) => this.GetValue(boolean, state, this.EvaluateScope); + + public EvaluationResult GetValue(BoolExpression boolean, RecordDataValue state) => this.GetValue(boolean, state, this.EvaluateState); + + public EvaluationResult GetValue(StringExpression expression, ProcessActionScopes state) => this.GetValue(expression, state, this.EvaluateScope); + + public EvaluationResult GetValue(StringExpression expression, RecordDataValue state) => this.GetValue(expression, state, this.EvaluateState); + + public EvaluationResult GetValue(ValueExpression expression, ProcessActionScopes state) => this.GetValue(expression, state, this.EvaluateScope); + + public EvaluationResult GetValue(ValueExpression expression, RecordDataValue state) => this.GetValue(expression, state, this.EvaluateState); + + public EvaluationResult GetValue(IntExpression expression, ProcessActionScopes state) => this.GetValue(expression, state, this.EvaluateScope); + + public EvaluationResult GetValue(IntExpression expression, RecordDataValue state) => this.GetValue(expression, state, this.EvaluateState); + + public EvaluationResult GetValue(NumberExpression expression, ProcessActionScopes state) => this.GetValue(expression, state, this.EvaluateScope); + + public EvaluationResult GetValue(NumberExpression expression, RecordDataValue state) => this.GetValue(expression, state, this.EvaluateState); + + public EvaluationResult GetValue(ObjectExpression expression, RecordDataValue state) where TValue : BotElement + { + throw new NotImplementedException(); // %%% TODO: IMPLEMENT + } + + public ImmutableArray GetValue(ArrayExpression expression, RecordDataValue state) + { + throw new NotImplementedException(); // %%% TODO: IMPLEMENT + } + + public ImmutableArray GetValue(ArrayExpressionOnly expression, RecordDataValue state) + { + throw new NotImplementedException(); // %%% TODO: IMPLEMENT + } + + public EvaluationResult GetValue(EnumExpression expression, ProcessActionScopes state) where TValue : EnumWrapper => + this.GetValue(expression, state, this.EvaluateScope); + + public EvaluationResult GetValue(EnumExpression expression, RecordDataValue state) where TValue : EnumWrapper => + this.GetValue(expression, state, this.EvaluateState); + + public DialogSchemaName GetValue(DialogExpression expression, RecordDataValue state) + { + throw new NotSupportedException(); + } + + public EvaluationResult GetValue(AdaptiveCardExpression expression, RecordDataValue state) + { + throw new NotSupportedException(); + } + + public EvaluationResult GetValue(FileExpression expression, RecordDataValue state) + { + throw new NotSupportedException(); + } + + private EvaluationResult GetValue(BoolExpression expression, TState state, Func evaluator) + { + Throw.IfNull(expression, nameof(expression)); + + if (expression.IsLiteral) + { + return new EvaluationResult(expression.LiteralValue, SensitivityLevel.None); // %%% SENSITIVITY ??? + } + + FormulaValue expressionValue = evaluator.Invoke(expression, state); + + if (expressionValue is BlankValue) + { + return new EvaluationResult(default, SensitivityLevel.None); // %%% SENSITIVITY ??? + } + + if (expressionValue is not BooleanValue formulaValue) + { + throw new InvalidExpressionOutputTypeException(expressionValue.GetDataType(), DataType.Boolean); + } + + return new EvaluationResult(formulaValue.Value, SensitivityLevel.None); // %%% SENSITIVITY ??? + } + + private EvaluationResult GetValue(StringExpression expression, TState state, Func evaluator) + { + Throw.IfNull(expression, nameof(expression)); + + if (expression.IsLiteral) + { + return new EvaluationResult(expression.LiteralValue, SensitivityLevel.None); // %%% SENSITIVITY ??? + } + + FormulaValue expressionValue = evaluator.Invoke(expression, state); + + if (expressionValue is BlankValue) + { + return new EvaluationResult(string.Empty, SensitivityLevel.None); // %%% SENSITIVITY ??? + } + + if (expressionValue is RecordValue recordValue) + { + return new EvaluationResult(JsonSerializer.Serialize(recordValue, s_options), SensitivityLevel.None); // %%% SENSITIVITY ??? + } + + if (expressionValue is not StringValue formulaValue) + { + throw new InvalidExpressionOutputTypeException(expressionValue.GetDataType(), DataType.String); + } + + return new EvaluationResult(formulaValue.Value, SensitivityLevel.None); // %%% SENSITIVITY ??? + } + + private EvaluationResult GetValue(IntExpression expression, TState state, Func evaluator) + { + Throw.IfNull(expression, nameof(expression)); + + if (expression.IsLiteral) + { + return new EvaluationResult(expression.LiteralValue, SensitivityLevel.None); // %%% SENSITIVITY ??? + } + + FormulaValue expressionValue = evaluator.Invoke(expression, state); + + if (expressionValue is not PrimitiveValue formulaValue) // %%% CORRECT ??? + { + throw new InvalidExpressionOutputTypeException(expressionValue.GetDataType(), DataType.Number); + } + + return new EvaluationResult(formulaValue.Value, SensitivityLevel.None); // %%% SENSITIVITY ??? + } + + private EvaluationResult GetValue(NumberExpression expression, TState state, Func evaluator) + { + Throw.IfNull(expression, nameof(expression)); + + if (expression.IsLiteral) + { + return new EvaluationResult(expression.LiteralValue, SensitivityLevel.None); // %%% SENSITIVITY ??? + } + + FormulaValue expressionValue = evaluator.Invoke(expression, state); + + if (expressionValue is not NumberValue formulaValue) + { + throw new InvalidExpressionOutputTypeException(expressionValue.GetDataType(), DataType.Number); + } + + return new EvaluationResult(formulaValue.Value, SensitivityLevel.None); // %%% SENSITIVITY ??? + } + + private EvaluationResult GetValue(ValueExpression expression, TState state, Func evaluator) + { + Throw.IfNull(expression, nameof(expression)); + + if (expression.IsLiteral) + { + return new EvaluationResult(expression.LiteralValue ?? BlankDataValue.Instance, SensitivityLevel.None); // %%% SENSITIVITY ??? + } + + FormulaValue expressionValue = evaluator.Invoke(expression, state); + + return new EvaluationResult(expressionValue.GetDataValue(), SensitivityLevel.None); // %%% SENSITIVITY ??? + } + + private EvaluationResult GetValue(EnumExpression expression, TState state, Func evaluator) where TValue : EnumWrapper + { + Throw.IfNull(expression, nameof(expression)); + + if (expression.IsLiteral) + { + return new EvaluationResult(expression.LiteralValue, SensitivityLevel.None); // %%% SENSITIVITY ??? + } + + FormulaValue expressionValue = evaluator.Invoke(expression, state); + SensitivityLevel expressionSensitivity = SensitivityLevel.None; // %%% SENSITIVITY ??? + + return expressionValue switch + { + BlankValue => new EvaluationResult(EnumWrapper.Create(0), expressionSensitivity), + StringValue s when s.Value is not null => new EvaluationResult(EnumWrapper.Create(s.Value), expressionSensitivity), + StringValue => new EvaluationResult(EnumWrapper.Create(0), expressionSensitivity), + NumberValue number => new EvaluationResult(EnumWrapper.Create((int)number.Value), expressionSensitivity), + //OptionDataValue option => new EvaluationResult(EnumWrapper.Create(option.Value.Value), expressionSensitivity), + _ => throw new InvalidExpressionOutputTypeException(expressionValue.GetDataType(), DataType.String), + }; + } + + private FormulaValue EvaluateState(ExpressionBase expression, RecordDataValue state) + { + foreach (KeyValuePair kvp in state.Properties) + { + if (kvp.Value is RecordDataValue scopeRecord) + { + this._engine.SetScope(kvp.Key, scopeRecord.ToRecordValue()); + } + } + + return this.Evaluate(expression); + } + + private FormulaValue EvaluateScope(ExpressionBase expression, ProcessActionScopes state) + { + this._engine.SetScope(ActionScopeType.System.Name, state.BuildRecord(ActionScopeType.System)); + this._engine.SetScope(ActionScopeType.Env.Name, state.BuildRecord(ActionScopeType.Env)); + this._engine.SetScope(ActionScopeType.Global.Name, state.BuildRecord(ActionScopeType.Global)); + this._engine.SetScope(ActionScopeType.Topic.Name, state.BuildRecord(ActionScopeType.Topic)); + + return this.Evaluate(expression); + } + + private FormulaValue Evaluate(ExpressionBase expression) + { + string? expressionText = + expression.IsVariableReference ? + expression.VariableReference?.Format() : + expression.ExpressionText; + + return this._engine.Eval(expressionText); + } +} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs index 360f8df329a5..1d97a695fe7f 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineExtensions.cs @@ -3,7 +3,6 @@ using Microsoft.Bot.ObjectModel; using Microsoft.PowerFx; using Microsoft.PowerFx.Types; -using Microsoft.SemanticKernel.Process.Workflows.Extensions; namespace Microsoft.SemanticKernel.Process.Workflows.PowerFx; @@ -15,7 +14,7 @@ public static void ClearScope(this RecalcEngine engine, ProcessActionScopes scop scopes.Clear(scope); // Rebuild scope record and update engine - UpdateScope(engine, scopes, scope); + engine.UpdateScope(scopes, scope); } public static void ClearScopedVariable(this RecalcEngine engine, ProcessActionScopes scopes, PropertyPath variablePath) => @@ -27,7 +26,7 @@ public static void ClearScopedVariable(this RecalcEngine engine, ProcessActionSc scopes.Remove(varName, scope); // Rebuild scope record and update engine - UpdateScope(engine, scopes, scope); + engine.UpdateScope(scopes, scope); } public static void SetScopedVariable(this RecalcEngine engine, ProcessActionScopes scopes, PropertyPath variablePath, FormulaValue value) => @@ -39,41 +38,18 @@ public static void SetScopedVariable(this RecalcEngine engine, ProcessActionScop scopes.Set(varName, scope, value); // Rebuild scope record and update engine - UpdateScope(engine, scopes, scope); + engine.UpdateScope(scopes, scope); } - public static FormulaValue EvaluateExpression(this RecalcEngine engine, ExpressionBase? value) + public static void SetScope(this RecalcEngine engine, string scopeName, RecordValue scopeRecord) { - if (value is null) - { - return BlankValue.NewBlank(); - } - - if (value.IsVariableReference) - { - return engine.Eval(value.VariableReference?.Format()); - } - - if (value.IsExpression) - { - return engine.Eval(value.ExpressionText); - } - - if (value.IsLiteral) - { - return value.GetLiteralValue().ToFormulaValue(); // %%% GetLiteralValue - } - - // %%% TODO: value.StructuredRecordExpression ??? - // %%% TODO: ArrayExpression - - return BlankValue.NewBlank(); + engine.DeleteFormula(scopeName); + engine.UpdateVariable(scopeName, scopeRecord); } - private static void UpdateScope(RecalcEngine engine, ProcessActionScopes scopes, ActionScopeType scope) + private static void UpdateScope(this RecalcEngine engine, ProcessActionScopes scopes, ActionScopeType scope) { RecordValue scopeRecord = scopes.BuildRecord(scope); - engine.DeleteFormula(scope.Name); - engine.UpdateVariable(scope.Name, scopeRecord); + engine.SetScope(scope.Name, scopeRecord); } } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineFactory.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineFactory.cs index 6cd216f98394..f64209891df2 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineFactory.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/RecalcEngineFactory.cs @@ -16,6 +16,7 @@ public static RecalcEngine Create( SetScope(ActionScopeType.Topic); SetScope(ActionScopeType.Global); + SetScope(ActionScopeType.Env); SetScope(ActionScopeType.System); return engine; diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessAction.cs index cb58cb6808dc..8c577c9b54d0 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessAction.cs @@ -7,25 +7,44 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerFx; using Microsoft.SemanticKernel.Process.Workflows.Extensions; +using Microsoft.SemanticKernel.Process.Workflows.PowerFx; namespace Microsoft.SemanticKernel.Process.Workflows; -internal sealed record class ProcessActionContext(RecalcEngine Engine, ProcessActionScopes Scopes, Kernel Kernel, ILogger Logger); +internal sealed record class ProcessActionContext(RecalcEngine Engine, ProcessActionScopes Scopes, Kernel Kernel, ILogger Logger) +{ + private FoundryExpressionEngine? _expressionEngine; + + public FoundryExpressionEngine ExpressionEngine => this._expressionEngine ??= new FoundryExpressionEngine(this.Engine); +} -internal abstract class ProcessAction(TAction model) : - ProcessAction(model) +internal abstract class ProcessAction(TAction model) : ProcessAction(model) where TAction : DialogAction { public new TAction Model => (TAction)base.Model; } -internal abstract class ProcessAction(DialogAction model) +internal abstract class ProcessAction { - public string Id => model.Id.Value; + public const string RootActionId = "(root)"; + + private string? _parentId; + + public string Id => this.Model.Id.Value; + + public string ParentId => this._parentId ??= this.Model.GetParentId() ?? RootActionId; - public string ParentId { get; } = model.GetParentId(); + public DialogAction Model { get; } - public DialogAction Model => model; + protected ProcessAction(DialogAction model) + { + if (!model.HasRequiredProperties) + { + throw new InvalidActionException($"Action {this.GetType().Name} [{model.Id}]"); + } + + this.Model = model; + } public async Task ExecuteAsync(ProcessActionContext context, CancellationToken cancellationToken) { diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionScopes.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionScopes.cs index 1b73ccf33898..5b61d6809d8f 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionScopes.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionScopes.cs @@ -3,19 +3,22 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using Microsoft.Bot.ObjectModel; using Microsoft.PowerFx.Types; +using Microsoft.SemanticKernel.Process.Workflows.Extensions; namespace Microsoft.SemanticKernel.Process.Workflows; /// /// Describes the type of action scope. /// -internal sealed class ActionScopeType +internal sealed class ActionScopeType // %%% NEEDED { - public static readonly ActionScopeType Env = new(nameof(Env)); - public static readonly ActionScopeType Topic = new(nameof(Topic)); - public static readonly ActionScopeType Global = new(nameof(Global)); - public static readonly ActionScopeType System = new(nameof(System)); + // https://msazure.visualstudio.com/CCI/_git/ObjectModel?path=/src/ObjectModel/Nodes/VariableScopeNames.cs&_a=contents&version=GBmain + public static readonly ActionScopeType Env = new(VariableScopeNames.Environment); + public static readonly ActionScopeType Topic = new(VariableScopeNames.Topic); + public static readonly ActionScopeType Global = new(VariableScopeNames.Global); + public static readonly ActionScopeType System = new(VariableScopeNames.System); public static ActionScopeType Parse(string? scope) { @@ -37,6 +40,8 @@ private ActionScopeType(string name) public string Name { get; } + public string Format(string name) => $"{this.Name}.{name}"; + public override string ToString() => this.Name; public override int GetHashCode() => this.Name.GetHashCode(); @@ -49,7 +54,33 @@ public override bool Equals(object? obj) => /// /// The set of variables for a specific action scope. /// -internal sealed class ProcessActionScope : Dictionary; +internal sealed class ProcessActionScope : Dictionary +{ + public RecordValue BuildRecord() + { + return FormulaValue.NewRecordFromFields(GetFields()); + + IEnumerable GetFields() + { + foreach (KeyValuePair kvp in this) + { + yield return new NamedValue(kvp.Key, kvp.Value); + } + } + } + + public RecordDataValue BuildState() + { + RecordDataValue.Builder recordBuilder = new(); + + foreach (KeyValuePair kvp in this) + { + recordBuilder.Properties.Add(kvp.Key, kvp.Value.GetDataValue()); + } + + return recordBuilder.Build(); + } +} /// /// Contains all action scopes for a process. @@ -72,20 +103,30 @@ public ProcessActionScopes() this._scopes = scopes.ToImmutableDictionary(); } - public RecordValue BuildRecord(ActionScopeType scope) + public RecordValue BuildRecord(ActionScopeType scope) => this._scopes[scope].BuildRecord(); + + public RecordDataValue BuildState() { - return FormulaValue.NewRecordFromFields(GetFields()); + return RecordDataValue.RecordFromFields(BuildStateFields()); - IEnumerable GetFields() + IEnumerable> BuildStateFields() { - foreach (KeyValuePair kvp in this._scopes[scope]) + foreach (KeyValuePair kvp in this._scopes) { - yield return new NamedValue(kvp.Key, kvp.Value); + yield return new(kvp.Key.Name, kvp.Value.BuildState()); } } } - public FormulaValue Get(string name, ActionScopeType? type = null) => this._scopes[type ?? ActionScopeType.Topic][name]; + public FormulaValue Get(string name, ActionScopeType? type = null) + { + if (this._scopes[type ?? ActionScopeType.Topic].TryGetValue(name, out FormulaValue? value)) + { + return value; + } + + return FormulaValue.NewBlank(); + } public void Clear(ActionScopeType type) => this._scopes[type].Clear(); diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ActionValidationFailure.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ActionValidationFailure.cs deleted file mode 100644 index 03fc4ffc45be..000000000000 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ActionValidationFailure.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Bot.ObjectModel; - -namespace Microsoft.SemanticKernel.Process.Workflow.Validation; - -/// -/// Represents a validation failure based on a . -/// -public class ActionValidationFailure : ElementValidationFailure -{ - /// - /// Initializes a new instance of the class with the specified action and error message. - /// - /// The that caused the validation failure. - /// The validation error message. - internal ActionValidationFailure(DialogAction action, string message) - : base(action, message) - { - this.Id = action.Id.Value; - } - - /// - /// Gets the identifier of the that caused the validation failure. - /// - public string Id { get; } -} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ElementValidationFailure.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ElementValidationFailure.cs deleted file mode 100644 index 9c54c54293aa..000000000000 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ElementValidationFailure.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Bot.ObjectModel; - -namespace Microsoft.SemanticKernel.Process.Workflow.Validation; - -/// -/// Represents a validation failure that is associated with a specific . -/// -public class ElementValidationFailure : ValidationFailure -{ - /// - /// Initializes a new instance of the class with the specified element and error message. - /// - /// The that caused the validation failure. - /// The validation error message. - internal ElementValidationFailure(BotElement element, string message) - : base(message) - { - this.Kind = element.Kind; - this.StartPosition = element.Syntax?.Position; - this.EndPosition = element.Syntax?.EndPosition; - } - - /// - /// Gets the kind of the that caused the validation failure. - /// - public BotElementKind Kind { get; } - - /// - /// Gets the start position of the in the source, if available. - /// - public int? StartPosition { get; } - - /// - /// Gets the end position of the in the source, if available. - /// - public int? EndPosition { get; } -} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ExceptionValidationFailure.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ExceptionValidationFailure.cs deleted file mode 100644 index 36d599ba46d7..000000000000 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ExceptionValidationFailure.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; - -namespace Microsoft.SemanticKernel.Process.Workflow.Validation; - -/// -/// Represents a validation failure that is associated with an exception. -/// -public class ExceptionValidationFailure : ValidationFailure -{ - /// - /// Initializes a new instance of the class with the specified exception and message. - /// - /// The exception that caused the validation failure. - /// The validation failure message. - internal ExceptionValidationFailure(Exception exception, string message) - : base(message) - { - this.Exception = exception; - } - - /// - /// Gets the exception that caused the validation failure. - /// - public Exception Exception { get; } - - /// - public override string ToString() => $"{this.Message} - {this.Exception.Message} [{this.Exception.GetType().Name}]"; -} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ProcessValidationWalker.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ProcessValidationWalker.cs deleted file mode 100644 index 97dbabe21b65..000000000000 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ProcessValidationWalker.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using Microsoft.Bot.ObjectModel; - -namespace Microsoft.SemanticKernel.Process.Workflow.Validation; - -internal sealed class ProcessValidationWalker : BotElementWalker -{ - private readonly List _failures; - - public ProcessValidationWalker(BotElement rootElement) - { - this._failures = []; - this.Visit(rootElement); - this.Failures = this._failures.ToImmutableArray(); - } - - public bool IsValid => this.Failures.Length == 0; - - public ImmutableArray Failures { get; } - - public override bool DefaultVisit(BotElement definition) - { - Console.WriteLine($"> {definition.GetType().Name}"); - if (definition is UnknownBotElement) - { - this._failures.Add(new ElementValidationFailure(definition, "Unknown element")); - } - else if (definition is UnknownDialogAction unknownAction) - { - this._failures.Add(new ActionValidationFailure(unknownAction, "Unknown action")); - } - else if (definition is DialogAction action) - { - if (!action.HasRequiredProperties) - { - this._failures.Add(new ActionValidationFailure(action, "Missing required properties")); - } - } - - return true; - } -} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ValidationFailure.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ValidationFailure.cs deleted file mode 100644 index 3c3f28da5b47..000000000000 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Validation/ValidationFailure.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -namespace Microsoft.SemanticKernel.Process.Workflow.Validation; - -/// -/// Represents a failure that occurred during validation. -/// -public class ValidationFailure -{ - internal ValidationFailure(string message) - { - this.Message = message; - } - - /// - /// Gets the message that describes the validation failure. - /// - public string Message { get; } - - /// - public override string ToString() => this.Message; -} diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ClearAllVariablesActionTest.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ClearAllVariablesActionTest.cs index c2445bef46ac..58d49a587f00 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ClearAllVariablesActionTest.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ClearAllVariablesActionTest.cs @@ -15,15 +15,15 @@ namespace Microsoft.SemanticKernel.Process.UnitTests.Workflows.Actions; public sealed class ClearAllVariablesActionTest(ITestOutputHelper output) : ProcessActionTest(output) { [Fact] - public async Task ClearUserScope() + public async Task ClearWorkflowScope() { // Arrange this.Scopes.Set("NoVar", FormulaValue.New("Old value")); ClearAllVariables model = this.CreateModel( - this.FormatDisplayName(nameof(ClearUserScope)), - VariablesToClear.UserScopedVariables); + this.FormatDisplayName(nameof(ClearWorkflowScope)), + VariablesToClear.ConversationScopedVariables); // Act ClearAllVariablesAction action = new(model); diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ProcessActionTest.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ProcessActionTest.cs index de41c0826802..14a277c6eae8 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ProcessActionTest.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ProcessActionTest.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; using Microsoft.PowerFx.Types; @@ -50,7 +49,7 @@ internal void VerifyState(string variableName, ActionScopeType scope, FormulaVal internal void VerifyUndefined(string variableName, ActionScopeType scope) { - Assert.Throws(() => this.Scopes.Get(variableName, scope)); + Assert.IsType(this.Scopes.Get(variableName, scope)); } protected TAction AssignParent(DialogAction.Builder actionBuilder) where TAction : DialogAction diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/Extensions/FormulaValueExtensionsTests.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/Extensions/FormulaValueExtensionsTests.cs new file mode 100644 index 000000000000..6c996b01ba8e --- /dev/null +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/Extensions/FormulaValueExtensionsTests.cs @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx.Types; +using Microsoft.SemanticKernel.Process.Workflows.Extensions; +using Xunit; + +namespace Microsoft.SemanticKernel.Process.UnitTests.Workflows.Extensions; + +public class FormulaValueExtensionsTests +{ + [Fact] + public void BooleanValue() + { + BooleanValue formulaValue = FormulaValue.New(true); + BooleanDataValue dataValue = formulaValue.ToDataValue(); + Assert.Equal(formulaValue.Value, dataValue.Value); + + BooleanValue formulaCopy = Assert.IsType(dataValue.ToFormulaValue()); + Assert.Equal(dataValue.Value, formulaCopy.Value); + + Assert.Equal(bool.TrueString, formulaValue.Format()); + } + + [Fact] + public void StringValues() + { + StringValue formulaValue = FormulaValue.New("test value"); + StringDataValue dataValue = formulaValue.ToDataValue(); + Assert.Equal(formulaValue.Value, dataValue.Value); + + StringValue formulaCopy = Assert.IsType(dataValue.ToFormulaValue()); + Assert.Equal(dataValue.Value, formulaCopy.Value); + + Assert.Equal(formulaValue.Value, formulaValue.Format()); + } + + [Fact] + public void DecimalValues() + { + DecimalValue formulaValue = FormulaValue.New(45.3m); + NumberDataValue dataValue = formulaValue.ToDataValue(); + Assert.Equal(formulaValue.Value, dataValue.Value); + + DecimalValue formulaCopy = Assert.IsType(dataValue.ToFormulaValue()); + Assert.Equal(dataValue.Value, formulaCopy.Value); + + Assert.Equal("45.3", formulaValue.Format()); + } + + [Fact] + public void NumberValues() + { + NumberValue formulaValue = FormulaValue.New(3.1415926535897); + FloatDataValue dataValue = formulaValue.ToDataValue(); + Assert.Equal(formulaValue.Value, dataValue.Value); + + NumberValue formulaCopy = Assert.IsType(dataValue.ToFormulaValue()); + Assert.Equal(dataValue.Value, formulaCopy.Value); + + Assert.Equal("3.1415926535897", formulaValue.Format()); + } + + [Fact] + public void BlankValues() + { + BlankValue formulaValue = FormulaValue.NewBlank(); + + BlankDataValue dataCopy = Assert.IsType(formulaValue.GetDataValue()); + + Assert.Equal(string.Empty, formulaValue.Format()); + } + + [Fact] + public void VoidValues() + { + VoidValue formulaValue = FormulaValue.NewVoid(); + BlankDataValue dataCopy = Assert.IsType(formulaValue.GetDataValue()); + } + + [Fact] + public void DateValues() + { + DateValue formulaValue = FormulaValue.NewDateOnly(DateTime.UtcNow.Date); + DateDataValue dataValue = formulaValue.ToDataValue(); + Assert.Equal(formulaValue.GetConvertedValue(TimeZoneInfo.Utc), dataValue.Value); + + DateValue formulaCopy = Assert.IsType(dataValue.ToFormulaValue()); + Assert.Equal(dataValue.Value, formulaCopy.GetConvertedValue(TimeZoneInfo.Utc)); + + //Assert.Equal("45.3", formulaValue.Format()); // %%% TEST ASSERT ??? + } + + [Fact] + public void DateTimeValues() + { + DateTimeValue formulaValue = FormulaValue.New(DateTime.UtcNow); + DateTimeDataValue dataValue = formulaValue.ToDataValue(); + Assert.Equal(formulaValue.GetConvertedValue(TimeZoneInfo.Utc), dataValue.Value); + + DateTimeValue formulaCopy = Assert.IsType(dataValue.ToFormulaValue()); + Assert.Equal(dataValue.Value, formulaCopy.GetConvertedValue(TimeZoneInfo.Utc)); + + //Assert.Equal("45.3", formulaValue.Format()); // %%% TEST ASSERT ??? + } + + [Fact] + public void TimeValues() + { + TimeValue formulaValue = FormulaValue.New(TimeSpan.Parse("10:35")); + TimeDataValue dataValue = formulaValue.ToDataValue(); + Assert.Equal(formulaValue.Value, dataValue.Value); + + TimeValue formulaCopy = Assert.IsType(dataValue.ToFormulaValue()); + Assert.Equal(dataValue.Value, formulaCopy.Value); + + Assert.Equal("10:35:00", formulaValue.Format()); + } + + [Fact] + public void RecordValues() + { + RecordValue formulaValue = FormulaValue.NewRecordFromFields( + new NamedValue("FieldA", FormulaValue.New("Value1")), + new NamedValue("FieldB", FormulaValue.New("Value2")), + new NamedValue("FieldC", FormulaValue.New("Value3"))); + + RecordDataValue dataValue = formulaValue.ToDataValue(); + Assert.Equal(formulaValue.Fields.Count(), dataValue.Properties.Count); + foreach (KeyValuePair property in dataValue.Properties) + { + Assert.Contains(property.Key, formulaValue.Fields.Select(field => field.Name)); + } + + RecordValue formulaCopy = Assert.IsType(dataValue.ToFormulaValue(), exactMatch: false); + Assert.Equal(formulaCopy.Fields.Count(), dataValue.Properties.Count); + foreach (NamedValue field in formulaCopy.Fields) + { + Assert.Contains(field.Name, dataValue.Properties.Keys); + } + + //Assert.Equal("45.3", formulaValue.Format()); // %%% TEST ASSERT ??? + } +} diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/PowerFx/FoundryExpressionEngineTests.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/PowerFx/FoundryExpressionEngineTests.cs new file mode 100644 index 000000000000..2e783436acaf --- /dev/null +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/PowerFx/FoundryExpressionEngineTests.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Bot.ObjectModel; +using Microsoft.Bot.ObjectModel.Abstractions; +using Microsoft.PowerFx; +using Microsoft.PowerFx.Types; +using Microsoft.SemanticKernel.Process.Workflows.PowerFx; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.SemanticKernel.Process.UnitTests.Workflows.PowerFx; + +public class FoundryExpressionEngineTests(ITestOutputHelper output) : RecalcEngineTest(output) +{ + [Fact] + public void DefaultNotNull() + { + // Act + RecalcEngine engine = this.CreateEngine(); + FoundryExpressionEngine expressionEngine = new(engine); + this.Scopes.Set("test", FormulaValue.New("value")); + engine.SetScopedVariable(this.Scopes, PropertyPath.TopicVariable("test"), FormulaValue.New("value")); + + EvaluationResult valueResult = expressionEngine.GetValue(StringExpression.Variable(PropertyPath.TopicVariable("test")), this.Scopes.BuildState()); + + // Assert + Assert.Equal("value", valueResult.Value); + Assert.Equal(SensitivityLevel.None, valueResult.Sensitivity); + } +} diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/PowerFx/RecalcEngineTest.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/PowerFx/RecalcEngineTest.cs index 366b2775accd..8a3fa6055a2a 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Workflow/PowerFx/RecalcEngineTest.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/PowerFx/RecalcEngineTest.cs @@ -14,5 +14,5 @@ public abstract class RecalcEngineTest(ITestOutputHelper output) : WorkflowTest( { internal ProcessActionScopes Scopes { get; } = new(); - protected RecalcEngine CreateEngine(int maximumExpressionLength = 100) => RecalcEngineFactory.Create(this.Scopes, maximumExpressionLength); + protected RecalcEngine CreateEngine(int maximumExpressionLength = 500) => RecalcEngineFactory.Create(this.Scopes, maximumExpressionLength); } diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/WorkflowValidationTests.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/WorkflowValidationTests.cs deleted file mode 100644 index 83c100869689..000000000000 --- a/dotnet/src/Experimental/Process.UnitTests/Workflow/WorkflowValidationTests.cs +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Immutable; -using System.IO; -using Microsoft.SemanticKernel.Process.Workflow.Validation; -using Xunit; -using Xunit.Abstractions; - -namespace Microsoft.SemanticKernel.Process.UnitTests.Workflows; - -public sealed class WorkflowValidationTests(ITestOutputHelper output) : WorkflowTest(output) -{ - [Fact] - public void VerifyInvalidStreamFailure() - { - using StringReader reader = new(Workflows.SimpleWorkflow); - reader.Close(); - ObjectModelBuilder.TryValidate(reader, out ImmutableArray failures); - DumpFailures(failures); - Assert.Single(failures); - Assert.IsType(failures[0]); - } - - [Theory] - [InlineData(nameof(Workflows.EmptyWorkflow))] - [InlineData(nameof(Workflows.OnlyComment))] - [InlineData(nameof(Workflows.JsonExpression))] - [InlineData(nameof(Workflows.InvalidYaml))] - public void VerifyDeserializationFailure(string invalidYaml) - { - ImmutableArray failures = Validate(invalidYaml, expectValid: false); - Assert.Single(failures); - Assert.IsType(failures[0]); - } - - [Theory] - [InlineData(nameof(Workflows.NotWorkflow), 1)] - public void VerifyDefinitionFailure(string invalidYaml, int expectedFailureCount) - { - ImmutableArray failures = Validate(invalidYaml, expectValid: false); - Assert.Equal(expectedFailureCount, failures.Length); - } - - private static ImmutableArray Validate(string workflowDefinition, bool expectValid = true) - { - using StringReader reader = new(GetWorkflowDefinition(workflowDefinition)); - bool isValid = ObjectModelBuilder.TryValidate(reader, out ImmutableArray failures); - Assert.Equal(expectValid, isValid); - DumpFailures(failures); - return failures; - } - - private static void DumpFailures(ImmutableArray failures) - { - if (failures.IsEmpty) - { - Console.WriteLine("# NO FAILURES"); - return; - } - - int index = 1; - foreach (ValidationFailure failure in failures) - { - Console.WriteLine($"# FAILURE {index}"); - Console.WriteLine($"[{failure.GetType().Name}] {failure}"); - ++index; - } - } - - private static string GetWorkflowDefinition(string workflowName) => - typeof(Workflows).GetField(workflowName)?.GetValue(null) as string ?? - throw new InvalidOperationException($"Unknown workflow definition: {workflowName}"); - - private static class Workflows - { - public const string EmptyWorkflow = - """ - - """; - - public const string OnlyComment = - """ - # This is a comment - """; - - public const string NotWorkflow = - """ - users: - - firstName: Alice - lastName: Brown - age: 61 - email: alice.brown@example.com - - firstName: Alice - lastName: Edwards - age: 44 - email: alice.edwards@example.com - """; - - public const string JsonExpression = - """ - { - "fistName": "Alice", - "lastName": "Brown", - "age": 61, - "email": "alice.brown@example.com" - } - """; - - public const string InvalidYaml = - """ - kind: AdaptiveDialog - beginDialog: - kind: OnActivity - id: activity_xyz123 - type: Message - actions: - - kind: SetVariable - id: setVariable_u4cBtN - displayName: Invocation count - variable: Topic.Count - value: 0 - - kind: SendActivity - id: sendActivity_aGsbRo - activity: Starting - """; - - public const string SimpleWorkflow = - """ - kind: AdaptiveDialog - beginDialog: - kind: OnActivity - id: activity_xyz123 - type: Message - actions: - - kind: SetVariable - id: setVariable_u4cBtN - displayName: Invocation count - variable: Topic.Count - value: 0 - - - kind: SendActivity - id: sendActivity_aGsbRo - activity: Starting - """; - } -} From c5df7312ce90cdb750a899b12d0420d1c2c2db41 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 6 Aug 2025 14:26:22 -0700 Subject: [PATCH 37/40] Checkpoint++ --- .../Workflow/ObjectModel/Extensions/FormulaValueExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/FormulaValueExtensions.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/FormulaValueExtensions.cs index f05a4c87f488..e2c837506fa3 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/FormulaValueExtensions.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Extensions/FormulaValueExtensions.cs @@ -93,7 +93,7 @@ public static TableDataValue ToDataValue(this TableValue value) => TableDataValue.TableFromRecords(value.Rows.Select(row => row.Value.ToDataValue()).ToImmutableArray()); public static RecordDataValue ToDataValue(this RecordValue value) => - RecordDataValue.RecordFromFields(value.Fields.Select(field => field.GetKeyValuePair()).ToImmutableArray()); + RecordDataValue.RecordFromFields(value.OriginalFields.Select(field => field.GetKeyValuePair()).ToImmutableArray()); private static KeyValuePair GetKeyValuePair(this NamedValue value) => new(value.Name, value.Value.GetDataValue()); } From 297e18e50555e7a47de284016ff35acc48ee4c33 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 6 Aug 2025 16:05:11 -0700 Subject: [PATCH 38/40] Loop update --- .../Step06/Step06_WorkflowProcess.cs | 2 + .../Step06/testLoopBreak.yaml | 36 ++++++++++++++++++ .../Step06/testLoopContinue.yaml | 36 ++++++++++++++++++ .../ObjectModel/ProcessActionVisitor.cs | 29 ++++++++++++--- .../ObjectModel/ProcessWorkflowBuilder.cs | 37 ++++++++++++++++--- 5 files changed, 130 insertions(+), 10 deletions(-) create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step06/testLoopBreak.yaml create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step06/testLoopContinue.yaml diff --git a/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs b/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs index 9857f1474b63..e0a6c86fc9a5 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs @@ -17,6 +17,8 @@ public Step06_WorkflowProcess(ITestOutputHelper output) [InlineData("testEnd")] [InlineData("testGoto")] [InlineData("testLoop")] + [InlineData("testLoopBreak")] + [InlineData("testLoopContinue")] [InlineData("testCondition")] [InlineData("testExpression")] [InlineData("deepResearch")] diff --git a/dotnet/samples/GettingStartedWithProcesses/Step06/testLoopBreak.yaml b/dotnet/samples/GettingStartedWithProcesses/Step06/testLoopBreak.yaml new file mode 100644 index 000000000000..072f1c58e6a5 --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step06/testLoopBreak.yaml @@ -0,0 +1,36 @@ +kind: AdaptiveDialog +beginDialog: + kind: OnActivity + id: activity_xyz123 + type: Message + actions: + - kind: SetVariable + id: setVariable_u4cBtN + displayName: Invocation count + variable: Topic.Count + value: =0 + + - kind: SendActivity + id: sendActivity_aGsbRo + activity: Starting + + - kind: Foreach + id: foreach_mVIecC + items: =["a", "b", "c", "d", "e", "f"] + index: Topic.LoopIndex + value: Topic.LoopValue + actions: + - kind: SetVariable + id: setVariable_A4iBtN + displayName: Invocation count + variable: Topic.Count + value: =Topic.Count + 1 + - kind: BreakLoop + id: breakLoop_9JsbRz + - kind: SendActivity + id: sendActivity_nev1 + activity: NEVER! + + - kind: SendActivity + id: sendActivity_fJsbRz + activity: Complete! (x{Topic.Count}) diff --git a/dotnet/samples/GettingStartedWithProcesses/Step06/testLoopContinue.yaml b/dotnet/samples/GettingStartedWithProcesses/Step06/testLoopContinue.yaml new file mode 100644 index 000000000000..fdb800187a1e --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step06/testLoopContinue.yaml @@ -0,0 +1,36 @@ +kind: AdaptiveDialog +beginDialog: + kind: OnActivity + id: activity_xyz123 + type: Message + actions: + - kind: SetVariable + id: setVariable_u4cBtN + displayName: Invocation count + variable: Topic.Count + value: =0 + + - kind: SendActivity + id: sendActivity_aGsbRo + activity: Starting + + - kind: Foreach + id: foreach_mVIecC + items: =["a", "b", "c", "d", "e", "f"] + index: Topic.LoopIndex + value: Topic.LoopValue + actions: + - kind: SetVariable + id: setVariable_A4iBtN + displayName: Invocation count + variable: Topic.Count + value: =Topic.Count + 1 + - kind: ContinueLoop + id: continueLoop_9JsbRz + - kind: SendActivity + id: sendActivity_nev1 + activity: NEVER! + + - kind: SendActivity + id: sendActivity_fJsbRz + activity: Complete! (x{Topic.Count}) diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs index 12897349ceb9..4a56e8b52ea3 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs @@ -93,7 +93,7 @@ protected override void Visit(GotoAction item) { this.Trace(item, isSkipped: false); - string parentId = item.GetParentId(); + string parentId = Throw.IfNull(item.GetParentId(), nameof(BotElement.Parent)); // %%% NULL PARENT CASE ??? this.ContinueWith(this.CreateStep(item.Id.Value, nameof(GotoAction)), parentId); this._workflowBuilder.AddLink(item.Id.Value, item.ActionId.Value); this.RestartFrom(item.Id.Value, nameof(GotoAction), parentId); @@ -120,12 +120,30 @@ void CompletionHandler(string scopeId) protected override void Visit(BreakLoop item) // %%% SUPPORT { - this.Trace(item); + this.Trace(item, isSkipped: false); + + string? loopId = this._workflowBuilder.LocateParent(item.GetParentId()); + if (loopId is not null) + { + string parentId = Throw.IfNull(item.GetParentId(), nameof(BotElement.Parent)); // %%% NULL PARENT CASE ??? + this.ContinueWith(this.CreateStep(item.Id.Value, nameof(BreakLoop)), parentId); + this._workflowBuilder.AddLink(item.Id.Value, $"post_{loopId}"); // %%% TODO: DRY + this.RestartFrom(item.Id.Value, nameof(BreakLoop), parentId); + } } protected override void Visit(ContinueLoop item) // %%% SUPPORT { - this.Trace(item); + this.Trace(item, isSkipped: false); + + string? loopId = this._workflowBuilder.LocateParent(item.GetParentId()); + if (loopId is not null) + { + string parentId = Throw.IfNull(item.GetParentId(), nameof(BotElement.Parent)); // %%% NULL PARENT CASE ??? + this.ContinueWith(this.CreateStep(item.Id.Value, nameof(ContinueLoop)), parentId); + this._workflowBuilder.AddLink(item.Id.Value, $"next_{loopId}"); // %%% TODO: DRY + this.RestartFrom(item.Id.Value, nameof(ContinueLoop), parentId); + } } protected override void Visit(EndConversation item) @@ -381,16 +399,17 @@ private void ContinueWith( ProcessAction action, Func? condition = null, ScopeCompletionAction? callback = null) => - this.ContinueWith(this.CreateActionStep(action), action.ParentId, condition, callback); + this.ContinueWith(this.CreateActionStep(action), action.ParentId, condition, action.GetType(), callback); private void ContinueWith( ProcessStepBuilder step, string parentId, Func? condition = null, + Type? actionType = null, ScopeCompletionAction? callback = null) { this._actionStack.Recognize(parentId, callback); - this._workflowBuilder.AddNode(step, parentId); + this._workflowBuilder.AddNode(step, parentId, actionType); this._workflowBuilder.AddLinkFromPeer(parentId, step.Id, condition); } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessWorkflowBuilder.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessWorkflowBuilder.cs index c3cb00114b88..549442c15e32 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessWorkflowBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessWorkflowBuilder.cs @@ -32,14 +32,14 @@ public int GetDepth(string nodeId) return sourceNode.Depth; } - public void AddNode(ProcessStepBuilder step, string parentId) + public void AddNode(ProcessStepBuilder step, string parentId, Type? actionType = null) { if (!this.Steps.TryGetValue(parentId, out ProcessWorkflowNode? parentNode)) { throw new UnknownActionException($"Unresolved parent for {step.Id}: {parentId}."); } - ProcessWorkflowNode stepNode = this.DefineNode(step, parentNode); + ProcessWorkflowNode stepNode = this.DefineNode(step, parentNode, actionType); parentNode.Children.Add(stepNode); } @@ -98,15 +98,40 @@ public void ConnectNodes() } } - private ProcessWorkflowNode DefineNode(ProcessStepBuilder step, ProcessWorkflowNode? parentNode = null) + private ProcessWorkflowNode DefineNode(ProcessStepBuilder step, ProcessWorkflowNode? parentNode = null, Type? actionType = null) { - ProcessWorkflowNode stepNode = new(step, parentNode); + ProcessWorkflowNode stepNode = new(step, parentNode, actionType); this.Steps[stepNode.Id] = stepNode; return stepNode; } - private sealed class ProcessWorkflowNode(ProcessStepBuilder step, ProcessWorkflowNode? parent = null) + internal string? LocateParent(string? itemId) + { + if (string.IsNullOrEmpty(itemId)) + { + return null; + } + + while (itemId != null) + { + if (!this.Steps.TryGetValue(itemId, out ProcessWorkflowNode? itemNode)) + { + throw new UnknownActionException($"Unresolved child: {itemId}."); + } + + if (itemNode.ActionType == typeof(TAction)) + { + return itemNode.Id; + } + + itemId = itemNode.Parent?.Id; + } + + return null; + } + + private sealed class ProcessWorkflowNode(ProcessStepBuilder step, ProcessWorkflowNode? parent = null, Type? actionType = null) { public string Id => step.Id; @@ -117,6 +142,8 @@ private sealed class ProcessWorkflowNode(ProcessStepBuilder step, ProcessWorkflo public List Children { get; } = []; public int Depth => this.Parent?.Depth + 1 ?? 0; + + public Type? ActionType => actionType; } private sealed record class ProcessWorkflowLink(ProcessWorkflowNode Source, string TargetId, Func? Condition = null) From f3bbe0d30aee907cb33cb90624ac8b819ed87505 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 6 Aug 2025 20:49:43 -0700 Subject: [PATCH 39/40] Intercept --- .../Step06/Step06_WorkflowProcess.cs | 75 ++++++++++++++++--- .../Step06/testChat.yaml | 16 ++++ .../Step06/testTopic.yaml | 26 +++++++ .../Actions/AnswerQuestionWithAIAction.cs | 36 +++++---- .../Actions/ClearAllVariablesAction.cs | 2 +- .../Workflow/ObjectModel/ProcessAction.cs | 3 +- .../ObjectModel/ProcessActionVisitor.cs | 25 ++++++- .../Workflow/ObjectModel/WorkflowContext.cs | 6 ++ 8 files changed, 161 insertions(+), 28 deletions(-) create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step06/testChat.yaml create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step06/testTopic.yaml diff --git a/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs b/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs index e0a6c86fc9a5..e70d2183b620 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_WorkflowProcess.cs @@ -1,7 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using Azure.Identity; using Microsoft.Extensions.DependencyInjection; using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; namespace Step06; @@ -14,32 +17,47 @@ public Step06_WorkflowProcess(ITestOutputHelper output) : base(output, redirectSystemConsoleOutput: true) { } [Theory] + [InlineData("deepResearch")] + [InlineData("demo250729")] + [InlineData("testChat")] + [InlineData("testCondition")] [InlineData("testEnd")] + [InlineData("testExpression")] [InlineData("testGoto")] [InlineData("testLoop")] [InlineData("testLoopBreak")] [InlineData("testLoopContinue")] - [InlineData("testCondition")] - [InlineData("testExpression")] - [InlineData("deepResearch")] - [InlineData("demo250729")] + [InlineData("testTopic")] public async Task RunWorkflow(string fileName) { + using InterceptHandler customHandler = new(); + using HttpClient customClient = new(customHandler, disposeHandler: false); + const string InputEventId = "question"; Console.WriteLine("PROCESS INIT\n"); using StreamReader yamlReader = File.OpenText(@$"{nameof(Step06)}\{fileName}.yaml"); - KernelProcess process = ObjectModelBuilder.Build(yamlReader, InputEventId); + WorkflowContext workflowContext = + new() + { + HttpClient = customClient, + LoggerFactory = this.LoggerFactory, + ActivityChannel = this.Console, + ProjectEndpoint = TestConfiguration.AzureAI.Endpoint, + ProjectCredentials = new AzureCliCredential(), + }; + KernelProcess process = ObjectModelBuilder.Build(yamlReader, InputEventId, workflowContext); Console.WriteLine("\nPROCESS INVOKE\n"); - Kernel kernel = this.CreateKernel(); + Kernel kernel = this.CreateKernel(customClient); + IChatCompletionService chatService = kernel.GetRequiredService(); await using LocalKernelProcessContext context = await process.StartAsync(kernel, new KernelProcessEvent() { Id = InputEventId, Data = "" }); Console.WriteLine("\nPROCESS DONE"); } - private Kernel CreateKernel(bool withLogger = false) + private Kernel CreateKernel(HttpClient httpClient, bool withLogger = false) { IKernelBuilder kernelBuilder = Kernel.CreateBuilder(); @@ -47,9 +65,48 @@ private Kernel CreateKernel(bool withLogger = false) { kernelBuilder.Services.AddSingleton(this.LoggerFactory); } - this.AddChatCompletionToKernel(kernelBuilder); - this.AddChatClientToKernel(kernelBuilder); + + kernelBuilder.AddAzureOpenAIChatCompletion( + TestConfiguration.AzureOpenAI.ChatDeploymentName, + TestConfiguration.AzureOpenAI.Endpoint, + new AzureCliCredential(), + httpClient: httpClient); return kernelBuilder.Build(); } } + +internal sealed class InterceptHandler : HttpClientHandler +{ + private static readonly JsonSerializerOptions s_options = new() { WriteIndented = true }; + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + // Call the inner handler to process the request and get the response + HttpResponseMessage response = await base.SendAsync(request, cancellationToken); + + // Intercept and modify the response + Console.WriteLine($"{request.Method} {request.RequestUri}"); + if (response.Content != null) + { + string responseContent; + try + { + JsonDocument responseDocument = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync(cancellationToken), cancellationToken: cancellationToken); + responseContent = JsonSerializer.Serialize(responseDocument, s_options); + } + catch (ArgumentException) + { + responseContent = await response.Content.ReadAsStringAsync(cancellationToken); + } + catch (JsonException) + { + responseContent = await response.Content.ReadAsStringAsync(cancellationToken); + } + response.Content = new StringContent(responseContent); + //Console.WriteLine(responseContent); // %%% RAISE EVENT + } + + return response; + } +} diff --git a/dotnet/samples/GettingStartedWithProcesses/Step06/testChat.yaml b/dotnet/samples/GettingStartedWithProcesses/Step06/testChat.yaml new file mode 100644 index 000000000000..fbf8af2bc5bb --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step06/testChat.yaml @@ -0,0 +1,16 @@ +kind: AdaptiveDialog +beginDialog: + kind: OnActivity + id: activity_xyz123 + actions: + + # Use AI to answer the question + - kind: AnswerQuestionWithAI + id: question_wEJ456 + variable: Topic.Answer + userInput: Why is the sky blue? + + # Display the AI's answer + - kind: SendActivity + id: sendActivity_zA3f0p + activity: "AI - {Topic.Answer}" diff --git a/dotnet/samples/GettingStartedWithProcesses/Step06/testTopic.yaml b/dotnet/samples/GettingStartedWithProcesses/Step06/testTopic.yaml new file mode 100644 index 000000000000..a4380697d993 --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step06/testTopic.yaml @@ -0,0 +1,26 @@ +kind: AdaptiveDialog +beginDialog: + kind: OnActivity + id: activity_xyz123 + type: Message + actions: + + - kind: SetVariable + id: setVariable_u4cBtN + displayName: Invocation count + variable: Topic.FirstValue + value: ABC + + - kind: SetVariable + id: setVariable_a8ybTn + displayName: Invocation count + variable: Workflow.SecondValue + value: 123 + + - kind: SendActivity + id: sendActivity_SVoNSV + activity: {Workflow.FirstValue} + + - kind: SendActivity + id: sendActivity_fJsbRz + activity: {Topic.SecondValue} diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AnswerQuestionWithAIAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AnswerQuestionWithAIAction.cs index 6e6d72f74ecb..d3bc0006670f 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AnswerQuestionWithAIAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/AnswerQuestionWithAIAction.cs @@ -1,12 +1,14 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Linq; using System.Threading; using System.Threading.Tasks; +using Azure.AI.Agents.Persistent; using Microsoft.Bot.ObjectModel; using Microsoft.Bot.ObjectModel.Abstractions; -using Microsoft.Extensions.DependencyInjection; using Microsoft.PowerFx.Types; -using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.AzureAI; using Microsoft.SemanticKernel.Process.Workflows.Extensions; namespace Microsoft.SemanticKernel.Process.Workflows.Actions; @@ -20,21 +22,27 @@ public AnswerQuestionWithAIAction(AnswerQuestionWithAI model) protected override async Task HandleAsync(ProcessActionContext context, CancellationToken cancellationToken) { - IChatCompletionService chatCompletion = context.Kernel.Services.GetRequiredService(); - EvaluationResult result = context.ExpressionEngine.GetValue(this.Model.UserInput!, context.Scopes); // %%% FAILURE CASE (CATCH) & NULL OVERRIDE + PersistentAgentsClient client = context.ClientFactory.Invoke(); + PersistentAgent model = await client.Administration.GetAgentAsync("asst_ueIjfGxAjsnZ4A61LlbjG9vJ", cancellationToken).ConfigureAwait(false); + AzureAIAgent agent = new(model, client); - ChatHistory history = []; - if (this.Model.AdditionalInstructions is not null) + string? userInput = null; + if (this.Model.UserInput is not null) { - string? instructions = context.Engine.Format(this.Model.AdditionalInstructions); - if (!string.IsNullOrWhiteSpace(instructions)) - { - history.AddSystemMessage(instructions); - } + EvaluationResult result = context.ExpressionEngine.GetValue(this.Model.UserInput!, context.Scopes); // %%% FAILURE CASE (CATCH) & NULL OVERRIDE + userInput = result.Value; } - history.AddUserMessage(result.Value); - ChatMessageContent response = await chatCompletion.GetChatMessageContentAsync(history, cancellationToken: cancellationToken).ConfigureAwait(false); - StringValue responseValue = FormulaValue.New(response.ToString()); + + AgentInvokeOptions options = + new() + { + AdditionalInstructions = context.Engine.Format(this.Model.AdditionalInstructions) ?? string.Empty, + }; + AgentResponseItem response = + userInput != null ? + await agent.InvokeAsync(userInput, thread: null, options, cancellationToken).LastAsync(cancellationToken).ConfigureAwait(false) : + await agent.InvokeAsync(thread: null, options, cancellationToken).LastAsync(cancellationToken).ConfigureAwait(false); + StringValue responseValue = FormulaValue.New(response.Message.ToString()); this.AssignTarget(context, responseValue); } diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ClearAllVariablesAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ClearAllVariablesAction.cs index 710d6ef9cd77..1cb1fa2e3c7e 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ClearAllVariablesAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/Actions/ClearAllVariablesAction.cs @@ -43,7 +43,7 @@ public void HandleConversationScopedVariables() public void HandleUnknownValue() { - throw new System.NotImplementedException(); // %%% LOG / NO EXCEPTION + // No scope to clear for unknown values. } public void HandleUserScopedVariables() diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessAction.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessAction.cs index 8c577c9b54d0..55117953b860 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessAction.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessAction.cs @@ -3,6 +3,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using Azure.AI.Agents.Persistent; using Microsoft.Bot.ObjectModel; using Microsoft.Extensions.Logging; using Microsoft.PowerFx; @@ -11,7 +12,7 @@ namespace Microsoft.SemanticKernel.Process.Workflows; -internal sealed record class ProcessActionContext(RecalcEngine Engine, ProcessActionScopes Scopes, Kernel Kernel, ILogger Logger) +internal sealed record class ProcessActionContext(RecalcEngine Engine, ProcessActionScopes Scopes, Func ClientFactory, ILogger Logger) { private FoundryExpressionEngine? _expressionEngine; diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs index 4a56e8b52ea3..02694b23f80c 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/ProcessActionVisitor.cs @@ -2,8 +2,14 @@ using System; using System.Linq; +using System.Net; +using System.Net.Http; using System.Threading.Tasks; +using Azure.AI.Agents.Persistent; +using Azure.Core; +using Azure.Core.Pipeline; using Microsoft.Bot.ObjectModel; +using Microsoft.Identity.Client.Platforms.Features.DesktopOs.Kerberos; using Microsoft.PowerFx; using Microsoft.SemanticKernel.Process.Workflows.Actions; using Microsoft.SemanticKernel.Process.Workflows.Extensions; @@ -431,7 +437,7 @@ private ProcessStepBuilder CreateStep(string actionId, string name, Action { Console.WriteLine($"!!! STEP {name} [{actionId}]"); // %%% REMOVE - stepAction?.Invoke(this.CreateActionContext(actionId, kernel)); + stepAction?.Invoke(this.CreateActionContext(actionId)); return Task.CompletedTask; }); } @@ -456,7 +462,7 @@ private ProcessStepBuilder CreateActionStep(ProcessAction action) try { await action.ExecuteAsync( - this.CreateActionContext(action.Id, kernel), + this.CreateActionContext(action.Id), cancellationToken: default).ConfigureAwait(false); // %%% CANCELTOKEN } catch (ProcessActionException) @@ -472,7 +478,20 @@ await action.ExecuteAsync( }); } - private ProcessActionContext CreateActionContext(string actionId, Kernel kernel) => new(this.CreateEngine(), this._scopes, kernel, kernel.LoggerFactory.CreateLogger(actionId)); + private ProcessActionContext CreateActionContext(string actionId) => new(this.CreateEngine(), this._scopes, this.CreateClient, this._context.LoggerFactory.CreateLogger(actionId)); + + private PersistentAgentsClient CreateClient() + { + PersistentAgentsAdministrationClientOptions clientOptions = new(); + + if (this._context.HttpClient is not null) + { + clientOptions.Transport = new HttpClientTransport(this._context.HttpClient); + //clientOptions.RetryPolicy = new RetryPolicy(maxRetries: 0); + } + + return new PersistentAgentsClient(this._context.ProjectEndpoint, this._context.ProjectCredentials, clientOptions); + } private RecalcEngine CreateEngine() => RecalcEngineFactory.Create(this._scopes, this._context.MaximumExpressionLength); diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/WorkflowContext.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/WorkflowContext.cs index f3e5429947cb..b56e2e7ffbb5 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/WorkflowContext.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/WorkflowContext.cs @@ -2,6 +2,7 @@ using System; using System.IO; +using System.Net.Http; using Azure.Core; using Azure.Identity; using Microsoft.Extensions.Logging; @@ -34,6 +35,11 @@ public sealed class WorkflowContext /// public int? MaximumExpressionLength { get; init; } + /// + /// Gets the instance used to send HTTP requests. + /// + public HttpClient? HttpClient { get; init; } + /// /// Gets the used to create loggers for workflow components. /// From d970f59ec238a6adabadd32d4e214f650a5c1727 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 7 Aug 2025 12:51:53 -0700 Subject: [PATCH 40/40] Complete expression support --- .../PowerFx/FoundryExpressionEngine.cs | 204 ++++++++++++------ .../Workflow/Actions/ProcessActionTest.cs | 2 +- 2 files changed, 144 insertions(+), 62 deletions(-) diff --git a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/FoundryExpressionEngine.cs b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/FoundryExpressionEngine.cs index aa3a67431be0..a0ac35113d2c 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/FoundryExpressionEngine.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/ObjectModel/PowerFx/FoundryExpressionEngine.cs @@ -44,20 +44,17 @@ public FoundryExpressionEngine(RecalcEngine engine) public EvaluationResult GetValue(NumberExpression expression, RecordDataValue state) => this.GetValue(expression, state, this.EvaluateState); - public EvaluationResult GetValue(ObjectExpression expression, RecordDataValue state) where TValue : BotElement - { - throw new NotImplementedException(); // %%% TODO: IMPLEMENT - } + public EvaluationResult GetValue(ObjectExpression expression, ProcessActionScopes state) where TValue : BotElement => this.GetValue(expression, state, this.EvaluateScope); - public ImmutableArray GetValue(ArrayExpression expression, RecordDataValue state) - { - throw new NotImplementedException(); // %%% TODO: IMPLEMENT - } + public EvaluationResult GetValue(ObjectExpression expression, RecordDataValue state) where TValue : BotElement => this.GetValue(expression, state, this.EvaluateState); - public ImmutableArray GetValue(ArrayExpressionOnly expression, RecordDataValue state) - { - throw new NotImplementedException(); // %%% TODO: IMPLEMENT - } + public ImmutableArray GetValue(ArrayExpression expression, ProcessActionScopes state) => this.GetValue(expression, state, this.EvaluateScope).Value; + + public ImmutableArray GetValue(ArrayExpression expression, RecordDataValue state) => this.GetValue(expression, state, this.EvaluateState).Value; + + public ImmutableArray GetValue(ArrayExpressionOnly expression, ProcessActionScopes state) => this.GetValue(expression, state, this.EvaluateScope).Value; + + public ImmutableArray GetValue(ArrayExpressionOnly expression, RecordDataValue state) => this.GetValue(expression, state, this.EvaluateState).Value; public EvaluationResult GetValue(EnumExpression expression, ProcessActionScopes state) where TValue : EnumWrapper => this.GetValue(expression, state, this.EvaluateScope); @@ -80,135 +77,220 @@ public EvaluationResult GetValue(AdaptiveCardExpression expression, Reco throw new NotSupportedException(); } - private EvaluationResult GetValue(BoolExpression expression, TState state, Func evaluator) + private EvaluationResult GetValue(BoolExpression expression, TState state, Func> evaluator) { Throw.IfNull(expression, nameof(expression)); if (expression.IsLiteral) { - return new EvaluationResult(expression.LiteralValue, SensitivityLevel.None); // %%% SENSITIVITY ??? + return new EvaluationResult(expression.LiteralValue, SensitivityLevel.None); } - FormulaValue expressionValue = evaluator.Invoke(expression, state); + EvaluationResult expressionResult = evaluator.Invoke(expression, state); - if (expressionValue is BlankValue) + if (expressionResult.Value is BlankValue) { - return new EvaluationResult(default, SensitivityLevel.None); // %%% SENSITIVITY ??? + return new EvaluationResult(default, SensitivityLevel.None); } - if (expressionValue is not BooleanValue formulaValue) + if (expressionResult.Value is not BooleanValue formulaValue) { - throw new InvalidExpressionOutputTypeException(expressionValue.GetDataType(), DataType.Boolean); + throw new InvalidExpressionOutputTypeException(expressionResult.Value.GetDataType(), DataType.Boolean); } - return new EvaluationResult(formulaValue.Value, SensitivityLevel.None); // %%% SENSITIVITY ??? + return new EvaluationResult(formulaValue.Value, expressionResult.Sensitivity); } - private EvaluationResult GetValue(StringExpression expression, TState state, Func evaluator) + private EvaluationResult GetValue(StringExpression expression, TState state, Func> evaluator) { Throw.IfNull(expression, nameof(expression)); if (expression.IsLiteral) { - return new EvaluationResult(expression.LiteralValue, SensitivityLevel.None); // %%% SENSITIVITY ??? + return new EvaluationResult(expression.LiteralValue, SensitivityLevel.None); } - FormulaValue expressionValue = evaluator.Invoke(expression, state); + EvaluationResult expressionResult = evaluator.Invoke(expression, state); - if (expressionValue is BlankValue) + if (expressionResult.Value is BlankValue) { - return new EvaluationResult(string.Empty, SensitivityLevel.None); // %%% SENSITIVITY ??? + return new EvaluationResult(string.Empty, expressionResult.Sensitivity); } - if (expressionValue is RecordValue recordValue) + if (expressionResult.Value is RecordValue recordValue) { - return new EvaluationResult(JsonSerializer.Serialize(recordValue, s_options), SensitivityLevel.None); // %%% SENSITIVITY ??? + return new EvaluationResult(JsonSerializer.Serialize(recordValue, s_options), expressionResult.Sensitivity); } - if (expressionValue is not StringValue formulaValue) + if (expressionResult.Value is not StringValue formulaValue) { - throw new InvalidExpressionOutputTypeException(expressionValue.GetDataType(), DataType.String); + throw new InvalidExpressionOutputTypeException(expressionResult.Value.GetDataType(), DataType.String); } - return new EvaluationResult(formulaValue.Value, SensitivityLevel.None); // %%% SENSITIVITY ??? + return new EvaluationResult(formulaValue.Value, expressionResult.Sensitivity); } - private EvaluationResult GetValue(IntExpression expression, TState state, Func evaluator) + private EvaluationResult GetValue(IntExpression expression, TState state, Func> evaluator) { Throw.IfNull(expression, nameof(expression)); if (expression.IsLiteral) { - return new EvaluationResult(expression.LiteralValue, SensitivityLevel.None); // %%% SENSITIVITY ??? + return new EvaluationResult(expression.LiteralValue, SensitivityLevel.None); } - FormulaValue expressionValue = evaluator.Invoke(expression, state); + EvaluationResult expressionResult = evaluator.Invoke(expression, state); - if (expressionValue is not PrimitiveValue formulaValue) // %%% CORRECT ??? + if (expressionResult.Value is not PrimitiveValue formulaValue) // %%% CORRECT ??? { - throw new InvalidExpressionOutputTypeException(expressionValue.GetDataType(), DataType.Number); + throw new InvalidExpressionOutputTypeException(expressionResult.Value.GetDataType(), DataType.Number); } - return new EvaluationResult(formulaValue.Value, SensitivityLevel.None); // %%% SENSITIVITY ??? + return new EvaluationResult(formulaValue.Value, expressionResult.Sensitivity); } - private EvaluationResult GetValue(NumberExpression expression, TState state, Func evaluator) + private EvaluationResult GetValue(NumberExpression expression, TState state, Func> evaluator) { Throw.IfNull(expression, nameof(expression)); if (expression.IsLiteral) { - return new EvaluationResult(expression.LiteralValue, SensitivityLevel.None); // %%% SENSITIVITY ??? + return new EvaluationResult(expression.LiteralValue, SensitivityLevel.None); } - FormulaValue expressionValue = evaluator.Invoke(expression, state); + EvaluationResult expressionResult = evaluator.Invoke(expression, state); - if (expressionValue is not NumberValue formulaValue) + if (expressionResult.Value is not NumberValue formulaValue) { - throw new InvalidExpressionOutputTypeException(expressionValue.GetDataType(), DataType.Number); + throw new InvalidExpressionOutputTypeException(expressionResult.Value.GetDataType(), DataType.Number); } - return new EvaluationResult(formulaValue.Value, SensitivityLevel.None); // %%% SENSITIVITY ??? + return new EvaluationResult(formulaValue.Value, expressionResult.Sensitivity); } - private EvaluationResult GetValue(ValueExpression expression, TState state, Func evaluator) + private EvaluationResult GetValue(ValueExpression expression, TState state, Func> evaluator) { Throw.IfNull(expression, nameof(expression)); if (expression.IsLiteral) { - return new EvaluationResult(expression.LiteralValue ?? BlankDataValue.Instance, SensitivityLevel.None); // %%% SENSITIVITY ??? + return new EvaluationResult(expression.LiteralValue ?? BlankDataValue.Instance, SensitivityLevel.None); } - FormulaValue expressionValue = evaluator.Invoke(expression, state); + EvaluationResult expressionResult = evaluator.Invoke(expression, state); - return new EvaluationResult(expressionValue.GetDataValue(), SensitivityLevel.None); // %%% SENSITIVITY ??? + return new EvaluationResult(expressionResult.Value.GetDataValue(), expressionResult.Sensitivity); } - private EvaluationResult GetValue(EnumExpression expression, TState state, Func evaluator) where TValue : EnumWrapper + private EvaluationResult GetValue(EnumExpression expression, TState state, Func> evaluator) where TValue : EnumWrapper { Throw.IfNull(expression, nameof(expression)); if (expression.IsLiteral) { - return new EvaluationResult(expression.LiteralValue, SensitivityLevel.None); // %%% SENSITIVITY ??? + return new EvaluationResult(expression.LiteralValue, SensitivityLevel.None); } - FormulaValue expressionValue = evaluator.Invoke(expression, state); - SensitivityLevel expressionSensitivity = SensitivityLevel.None; // %%% SENSITIVITY ??? + EvaluationResult expressionResult = evaluator.Invoke(expression, state); - return expressionValue switch + return expressionResult.Value switch { - BlankValue => new EvaluationResult(EnumWrapper.Create(0), expressionSensitivity), - StringValue s when s.Value is not null => new EvaluationResult(EnumWrapper.Create(s.Value), expressionSensitivity), - StringValue => new EvaluationResult(EnumWrapper.Create(0), expressionSensitivity), - NumberValue number => new EvaluationResult(EnumWrapper.Create((int)number.Value), expressionSensitivity), - //OptionDataValue option => new EvaluationResult(EnumWrapper.Create(option.Value.Value), expressionSensitivity), - _ => throw new InvalidExpressionOutputTypeException(expressionValue.GetDataType(), DataType.String), + BlankValue => new EvaluationResult(EnumWrapper.Create(0), expressionResult.Sensitivity), + StringValue s when s.Value is not null => new EvaluationResult(EnumWrapper.Create(s.Value), expressionResult.Sensitivity), + StringValue => new EvaluationResult(EnumWrapper.Create(0), expressionResult.Sensitivity), + NumberValue number => new EvaluationResult(EnumWrapper.Create((int)number.Value), expressionResult.Sensitivity), + //OptionDataValue option => new EvaluationResult(EnumWrapper.Create(option.Value.Value), expressionResult.Sensitivity), // %%% SUPPORT + _ => throw new InvalidExpressionOutputTypeException(expressionResult.Value.GetDataType(), DataType.String), }; } - private FormulaValue EvaluateState(ExpressionBase expression, RecordDataValue state) + private EvaluationResult GetValue(ObjectExpression expression, TState state, Func> evaluator) where TValue : BotElement + { + Throw.IfNull(expression, nameof(expression)); + + if (expression.LiteralValue != null) + { + return new EvaluationResult(expression.LiteralValue, SensitivityLevel.None); + } + + EvaluationResult expressionResult = evaluator.Invoke(expression, state); + + if (expressionResult.Value is BlankValue) + { + return new EvaluationResult(null, expressionResult.Sensitivity); + } + + if (expressionResult.Value is not RecordValue formulaValue) + { + throw new CannotParseObjectExpressionOutputException(typeof(TValue), expressionResult.Value.GetDataType()); + } + + try + { + return new EvaluationResult(ObjectExpressionParser.Parse(formulaValue.ToDataValue()), expressionResult.Sensitivity); + } + catch (Exception exception) + { + throw new CannotParseObjectExpressionOutputException(typeof(TValue), exception); + } + } + + private EvaluationResult> GetValue(ArrayExpression expression, TState state, Func> evaluator) + { + Throw.IfNull(expression, nameof(expression)); + + if (expression.IsLiteral) + { + return new EvaluationResult>(expression.LiteralValue, SensitivityLevel.None); + } + + EvaluationResult expressionResult = evaluator.Invoke(expression, state); + + return new EvaluationResult>(ParseArrayResults(expressionResult.Value), expressionResult.Sensitivity); + } + + private EvaluationResult> GetValue(ArrayExpressionOnly expression, TState state, Func> evaluator) + { + Throw.IfNull(expression, nameof(expression)); + + EvaluationResult expressionResult = evaluator.Invoke(expression, state); + + return new EvaluationResult>(ParseArrayResults(expressionResult.Value), expressionResult.Sensitivity); + } + + private static ImmutableArray ParseArrayResults(FormulaValue value) + { + if (value is BlankValue) + { + return ImmutableArray.Create(); + } + + if (value is not TableValue tableValue) + { + throw new CannotParseObjectExpressionOutputException(typeof(ImmutableArray), value.GetDataType()); + } + + TableDataValue tableDataValue = tableValue.ToDataValue(); + try + { + List list = []; + foreach (RecordDataValue row in tableDataValue.Values) + { + TValue? s = TableItemParser.Parse(row); + if (s != null) + { + list.Add(s); + } + } + return list.ToImmutableArray(); + } + catch (Exception exception) + { + throw new CannotParseObjectExpressionOutputException(typeof(TValue), exception); + } + } + + private EvaluationResult EvaluateState(ExpressionBase expression, RecordDataValue state) { foreach (KeyValuePair kvp in state.Properties) { @@ -221,7 +303,7 @@ private FormulaValue EvaluateState(ExpressionBase expression, RecordDataValue st return this.Evaluate(expression); } - private FormulaValue EvaluateScope(ExpressionBase expression, ProcessActionScopes state) + private EvaluationResult EvaluateScope(ExpressionBase expression, ProcessActionScopes state) { this._engine.SetScope(ActionScopeType.System.Name, state.BuildRecord(ActionScopeType.System)); this._engine.SetScope(ActionScopeType.Env.Name, state.BuildRecord(ActionScopeType.Env)); @@ -231,13 +313,13 @@ private FormulaValue EvaluateScope(ExpressionBase expression, ProcessActionScope return this.Evaluate(expression); } - private FormulaValue Evaluate(ExpressionBase expression) + private EvaluationResult Evaluate(ExpressionBase expression) { string? expressionText = expression.IsVariableReference ? expression.VariableReference?.Format() : expression.ExpressionText; - return this._engine.Eval(expressionText); + return new(this._engine.Eval(expressionText), SensitivityLevel.None); } } diff --git a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ProcessActionTest.cs b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ProcessActionTest.cs index 14a277c6eae8..5d8b9d2efc0d 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ProcessActionTest.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Workflow/Actions/ProcessActionTest.cs @@ -27,7 +27,7 @@ internal Task ExecuteAction(ProcessAction action, Kernel? kernel = null) => new ProcessActionContext( RecalcEngineFactory.Create(this.Scopes, 5000), this.Scopes, - kernel ?? new Kernel(), + () => null!, // %%% FIX this.Output), cancellationToken: default);