From 3c13912c9da888adffb08fa5fb9efbd47fce6c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estefan=C3=ADa=20Tenorio?= <8483207+esttenorio@users.noreply.github.com> Date: Wed, 4 Dec 2024 08:59:45 -0800 Subject: [PATCH] .Net Processes - Refactoring/expanding Process Sample02 (#9811) ### Description - Updating Sample 02 to expand the usage and show case how to refactor the same process by using subprocesses as steps. - Improving README file with more details on this sample - Add more unit tests for Process testing/improving comments Fixes #9836 Fixes #9837 ### Contribution Checklist - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone :smile: --------- Co-authored-by: Estefania Tenorio --- .../GettingStartedWithProcesses/README.md | 99 ++++++++- .../Step02/Models/AccountDetails.cs | 2 +- .../Step02/Models/AccountOpeningEvents.cs | 4 +- .../Models/AccountUserInteractionDetails.cs | 2 +- .../Step02/Models/MarketingNewEntryDetails.cs | 2 +- .../Step02/Models/NewCustomerForm.cs | 2 +- .../Processes/NewAccountCreationProcess.cs | 70 ++++++ .../NewAccountVerificationProcess.cs | 39 ++++ ...ntOpening.cs => Step02a_AccountOpening.cs} | 46 +--- .../Step02/Step02b_AccountOpening.cs | 139 ++++++++++++ .../Step02/Steps/CRMRecordCreationStep.cs | 2 + .../Steps/CompleteNewCustomerFormStep.cs | 9 +- .../Step02/Steps/CreditScoreCheckStep.cs | 10 +- .../Step02/Steps/FraudDetectionStep.cs | 7 +- .../Step02/Steps/NewAccountStep.cs | 2 + .../Step02/Steps/NewMarketingEntryStep.cs | 2 + ...rInputCreditScoreFailureInteractionStep.cs | 20 ++ .../UserInputFraudFailureInteractionStep.cs | 20 ++ .../UserInputSuccessfulInteractionStep.cs | 20 ++ .../Step02/Steps/WelcomePacketStep.cs | 3 + .../ProcessCycleTestResources.cs | 59 ++++- .../ProcessTests.cs | 208 +++++++++++++++--- .../Process.LocalRuntime/LocalProcess.cs | 6 +- 23 files changed, 686 insertions(+), 87 deletions(-) create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step02/Processes/NewAccountCreationProcess.cs create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step02/Processes/NewAccountVerificationProcess.cs rename dotnet/samples/GettingStartedWithProcesses/Step02/{Step02_AccountOpening.cs => Step02a_AccountOpening.cs} (84%) create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step02/Step02b_AccountOpening.cs create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step02/Steps/TestInputs/UserInputCreditScoreFailureInteractionStep.cs create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step02/Steps/TestInputs/UserInputFraudFailureInteractionStep.cs create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step02/Steps/TestInputs/UserInputSuccessfulInteractionStep.cs diff --git a/dotnet/samples/GettingStartedWithProcesses/README.md b/dotnet/samples/GettingStartedWithProcesses/README.md index ff28c1a91a80..107a97afc319 100644 --- a/dotnet/samples/GettingStartedWithProcesses/README.md +++ b/dotnet/samples/GettingStartedWithProcesses/README.md @@ -23,7 +23,8 @@ Example|Description ---|--- [Step00_Processes](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithProcesses/Step00/Step00_Processes.cs)|How to create the simplest process with minimal code and event wiring [Step01_Processes](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithProcesses/Step01/Step01_Processes.cs)|How to create a simple process with a loop and a conditional exit -[Step02_AccountOpening](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithProcesses/Step02/Step02_AccountOpening.cs)|Showcasing processes cycles, fan in, fan out for opening an account. +[Step02a_AccountOpening](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithProcesses/Step02/Step02a_AccountOpening.cs)|Showcasing processes cycles, fan in, fan out for opening an account. +[Step02b_AccountOpening](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithProcesses/Step02/Step02b_AccountOpening.cs)|How to refactor processes and make use of smaller processes as steps in larger processes. [Step03a_FoodPreparation](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithProcesses/Step03/Step03a_FoodPreparation.cs)|Showcasing reuse of steps, creation of processes, spawning of multiple events, use of stateful steps with food preparation samples. [Step03b_FoodOrdering](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithProcesses/Step03/Step03b_FoodOrdering.cs)|Showcasing use of subprocesses as steps, spawning of multiple events conditionally reusing the food preparation samples. [Step04_AgentOrchestration](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithProcesses/Step04/Step04_AgentOrchestration.cs)|Showcasing use of process steps in conjunction with the _Agent Framework_. @@ -49,6 +50,27 @@ flowchart LR ### Step02_AccountOpening +The account opening sample has 2 different implementations covering the same scenario, it just uses different SK components to achieve the same goal. + +In addition, the sample introduces the concept of using smaller process as steps to maintain the main process readable and manageble for future improvements and unit testing. +Also introduces the use of SK Event Subscribers. + +A process for opening an account for this sample has the following steps: +- Fill New User Account Application Form +- Verify Applicant Credit Score +- Apply Fraud Detection Analysis to the Application Form +- Create New Entry in Core System Records +- Add new account to Marketing Records +- CRM Record Creation +- Mail user a user a notification about: + - Failure to open a new account due to Credit Score Check + - Failure to open a new account due to Fraud Detection Alert + - Welcome package including new account details + +A SK process that only connects the steps listed above as is (no use of subprocesses as steps) for opening an account look like this: + +#### Step02a_AccountOpening + ```mermaid flowchart LR User(User) -->|Provides user details| FillForm(Fill New
Customer
Form) @@ -79,6 +101,81 @@ flowchart LR Mailer -->|End of Interaction| User ``` +#### Step02b_AccountOpening + +After grouping steps that have a common theme/dependencies, and creating smaller subprocesses and using them as steps, +the root process looks like this: + +```mermaid +flowchart LR + User(User) + FillForm(Chat With User
to Fill New
Customer Form) + NewAccountVerification[[New Account Verification
Process]] + NewAccountCreation[[New Account Creation
Process]] + Mailer(Mail
Service) + + User<-->|Provides user details|FillForm + FillForm-->|New User Form|NewAccountVerification + NewAccountVerification-->|Account Credit Check
Verification Failed|Mailer + NewAccountVerification-->|Account Fraud
Detection Failed|Mailer + NewAccountVerification-->|Account Verification
Succeeded|NewAccountCreation + NewAccountCreation-->|Account Creation
Succeeded|Mailer +``` + +Where processes used as steps, which are reusing the same steps used [`Step02a_AccountOpening`](#step02a_accountopening), are: + +```mermaid +graph LR + NewUserForm([New User Form]) + NewUserFormConv([Form Filling Interaction]) + + subgraph AccountCreation[Account Creation Process] + direction LR + AccountValidation([Account Verification Passed]) + NewUser1([New User Form]) + NewUserFormConv1([Form Filling Interaction]) + + CoreSystem(Core System
Record
Creation) + Marketing(New Marketing
Record Creation) + CRM(CRM Record
Creation) + Welcome(Welcome
Packet) + NewAccountCreation([New Account Success]) + + NewUser1-->CoreSystem + NewUserFormConv1-->CoreSystem + + AccountValidation-->CoreSystem + CoreSystem-->CRM-->|Success|Welcome + CoreSystem-->Marketing-->|Success|Welcome + CoreSystem-->|Account Details|Welcome + + Welcome-->NewAccountCreation + end + + subgraph AccountVerification[Account Verification Process] + direction LR + NewUser2([New User Form]) + CreditScoreCheck[Credit Check
Step] + FraudCheck[Fraud Detection
Step] + AccountVerificationPass([Account Verification Passed]) + AccountCreditCheckFail([Credit Check Failed]) + AccountFraudCheckFail([Fraud Check Failed]) + + + NewUser2-->CreditScoreCheck-->|Credit Score
Check Passed|FraudCheck + FraudCheck-->AccountVerificationPass + + CreditScoreCheck-->AccountCreditCheckFail + FraudCheck-->AccountFraudCheckFail + end + + AccountVerificationPass-->AccountValidation + NewUserForm-->NewUser1 + NewUserForm-->NewUser2 + NewUserFormConv-->NewUserFormConv1 + +``` + ### Step03a_FoodPreparation This tutorial contains a set of food recipes associated with the Food Preparation Processes of a restaurant. diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountDetails.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountDetails.cs index 6f732669d5dc..09fe50e3ba32 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountDetails.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountDetails.cs @@ -4,7 +4,7 @@ namespace Step02.Models; /// /// Represents the data structure for a form capturing details of a new customer, including personal information, contact details, account id and account type.
-/// Class used in samples +/// Class used in , samples ///
public class AccountDetails : NewCustomerForm { diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountOpeningEvents.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountOpeningEvents.cs index de1110854e27..eda9fc8d4ea3 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountOpeningEvents.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountOpeningEvents.cs @@ -3,7 +3,7 @@ namespace Step02.Models; /// /// Processes Events related to Account Opening scenarios.
-/// Class used in samples +/// Class used in , samples ///
public static class AccountOpeningEvents { @@ -14,6 +14,8 @@ public static class AccountOpeningEvents public static readonly string NewCustomerFormNeedsMoreDetails = nameof(NewCustomerFormNeedsMoreDetails); public static readonly string CustomerInteractionTranscriptReady = nameof(CustomerInteractionTranscriptReady); + public static readonly string NewAccountVerificationCheckPassed = nameof(NewAccountVerificationCheckPassed); + public static readonly string CreditScoreCheckApproved = nameof(CreditScoreCheckApproved); public static readonly string CreditScoreCheckRejected = nameof(CreditScoreCheckRejected); diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountUserInteractionDetails.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountUserInteractionDetails.cs index 123f0b2e417d..05f22bf47610 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountUserInteractionDetails.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountUserInteractionDetails.cs @@ -7,7 +7,7 @@ namespace Step02.Models; /// /// Represents the details of interactions between a user and service, including a unique identifier for the account, /// a transcript of conversation with the user, and the type of user interaction.
-/// Class used in samples +/// Class used in , samples ///
public record AccountUserInteractionDetails { diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Models/MarketingNewEntryDetails.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Models/MarketingNewEntryDetails.cs index 057e97c81597..e87980316723 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step02/Models/MarketingNewEntryDetails.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Models/MarketingNewEntryDetails.cs @@ -4,7 +4,7 @@ namespace Step02.Models; /// /// Holds details for a new entry in a marketing database, including the account identifier, contact name, phone number, and email address.
-/// Class used in samples +/// Class used in , samples ///
public record MarketingNewEntryDetails { diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Models/NewCustomerForm.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Models/NewCustomerForm.cs index c000b8491d24..c1a3f2debe55 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step02/Models/NewCustomerForm.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Models/NewCustomerForm.cs @@ -7,7 +7,7 @@ namespace Step02.Models; /// /// Represents the data structure for a form capturing details of a new customer, including personal information and contact details.
-/// Class used in samples +/// Class used in , samples ///
public class NewCustomerForm { diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Processes/NewAccountCreationProcess.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Processes/NewAccountCreationProcess.cs new file mode 100644 index 000000000000..7e96b9544d28 --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Processes/NewAccountCreationProcess.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel; +using Step02.Models; +using Step02.Steps; + +namespace Step02.Processes; + +/// +/// Demonstrate creation of and +/// eliciting its response to five explicit user messages.
+/// For each test there is a different set of user messages that will cause different steps to be triggered using the same pipeline.
+/// For visual reference of the process check the diagram. +///
+public static class NewAccountCreationProcess +{ + public static ProcessBuilder CreateProcess() + { + ProcessBuilder process = new("AccountCreationProcess"); + + var coreSystemRecordCreationStep = process.AddStepFromType(); + var marketingRecordCreationStep = process.AddStepFromType(); + var crmRecordStep = process.AddStepFromType(); + var welcomePacketStep = process.AddStepFromType(); + + // When the newCustomerForm is completed... + process + .OnInputEvent(AccountOpeningEvents.NewCustomerFormCompleted) + // The information gets passed to the core system record creation step + .SendEventTo(new ProcessFunctionTargetBuilder(coreSystemRecordCreationStep, functionName: NewAccountStep.Functions.CreateNewAccount, parameterName: "customerDetails")); + + // When the newCustomerForm is completed, the user interaction transcript with the user is passed to the core system record creation step + process + .OnInputEvent(AccountOpeningEvents.CustomerInteractionTranscriptReady) + .SendEventTo(new ProcessFunctionTargetBuilder(coreSystemRecordCreationStep, functionName: NewAccountStep.Functions.CreateNewAccount, parameterName: "interactionTranscript")); + + // When the fraudDetectionCheck step passes, the information gets to core system record creation step to kickstart this step + process + .OnInputEvent(AccountOpeningEvents.NewAccountVerificationCheckPassed) + .SendEventTo(new ProcessFunctionTargetBuilder(coreSystemRecordCreationStep, functionName: NewAccountStep.Functions.CreateNewAccount, parameterName: "previousCheckSucceeded")); + + // When the coreSystemRecordCreation step successfully creates a new accountId, it will trigger the creation of a new marketing entry through the marketingRecordCreation step + coreSystemRecordCreationStep + .OnEvent(AccountOpeningEvents.NewMarketingRecordInfoReady) + .SendEventTo(new ProcessFunctionTargetBuilder(marketingRecordCreationStep, functionName: NewMarketingEntryStep.Functions.CreateNewMarketingEntry, parameterName: "userDetails")); + + // When the coreSystemRecordCreation step successfully creates a new accountId, it will trigger the creation of a new CRM entry through the crmRecord step + coreSystemRecordCreationStep + .OnEvent(AccountOpeningEvents.CRMRecordInfoReady) + .SendEventTo(new ProcessFunctionTargetBuilder(crmRecordStep, functionName: CRMRecordCreationStep.Functions.CreateCRMEntry, parameterName: "userInteractionDetails")); + + // ParameterName is necessary when the step has multiple input arguments like welcomePacketStep.CreateWelcomePacketAsync + // When the coreSystemRecordCreation step successfully creates a new accountId, it will pass the account information details to the welcomePacket step + coreSystemRecordCreationStep + .OnEvent(AccountOpeningEvents.NewAccountDetailsReady) + .SendEventTo(new ProcessFunctionTargetBuilder(welcomePacketStep, parameterName: "accountDetails")); + + // When the marketingRecordCreation step successfully creates a new marketing entry, it will notify the welcomePacket step it is ready + marketingRecordCreationStep + .OnEvent(AccountOpeningEvents.NewMarketingEntryCreated) + .SendEventTo(new ProcessFunctionTargetBuilder(welcomePacketStep, parameterName: "marketingEntryCreated")); + + // When the crmRecord step successfully creates a new CRM entry, it will notify the welcomePacket step it is ready + crmRecordStep + .OnEvent(AccountOpeningEvents.CRMRecordInfoEntryCreated) + .SendEventTo(new ProcessFunctionTargetBuilder(welcomePacketStep, parameterName: "crmRecordCreated")); + + return process; + } +} diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Processes/NewAccountVerificationProcess.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Processes/NewAccountVerificationProcess.cs new file mode 100644 index 000000000000..e4184a71bd1e --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Processes/NewAccountVerificationProcess.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel; +using Step02.Models; +using Step02.Steps; + +namespace Step02.Processes; + +/// +/// Demonstrate creation of and +/// eliciting its response to five explicit user messages.
+/// For each test there is a different set of user messages that will cause different steps to be triggered using the same pipeline.
+/// For visual reference of the process check the diagram. +///
+public static class NewAccountVerificationProcess +{ + public static ProcessBuilder CreateProcess() + { + ProcessBuilder process = new("AccountVerificationProcess"); + + var customerCreditCheckStep = process.AddStepFromType(); + var fraudDetectionCheckStep = process.AddStepFromType(); + + // When the newCustomerForm is completed... + process + .OnInputEvent(AccountOpeningEvents.NewCustomerFormCompleted) + // The information gets passed to the core system record creation step + .SendEventTo(new ProcessFunctionTargetBuilder(customerCreditCheckStep, functionName: CreditScoreCheckStep.Functions.DetermineCreditScore, parameterName: "customerDetails")) + // The information gets passed to the fraud detection step for validation + .SendEventTo(new ProcessFunctionTargetBuilder(fraudDetectionCheckStep, functionName: FraudDetectionStep.Functions.FraudDetectionCheck, parameterName: "customerDetails")); + + // When the creditScoreCheck step results in Approval, the information gets to the fraudDetection step to kickstart this step + customerCreditCheckStep + .OnEvent(AccountOpeningEvents.CreditScoreCheckApproved) + .SendEventTo(new ProcessFunctionTargetBuilder(fraudDetectionCheckStep, functionName: FraudDetectionStep.Functions.FraudDetectionCheck, parameterName: "previousCheckSucceeded")); + + return process; + } +} diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Step02_AccountOpening.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Step02a_AccountOpening.cs similarity index 84% rename from dotnet/samples/GettingStartedWithProcesses/Step02/Step02_AccountOpening.cs rename to dotnet/samples/GettingStartedWithProcesses/Step02/Step02a_AccountOpening.cs index a523dc4119a3..1564dc679eec 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step02/Step02_AccountOpening.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Step02a_AccountOpening.cs @@ -12,9 +12,9 @@ namespace Step02; /// Demonstrate creation of and /// eliciting its response to five explicit user messages.
/// For each test there is a different set of user messages that will cause different steps to be triggered using the same pipeline.
-/// For visual reference of the process check the diagram . +/// For visual reference of the process check the diagram. /// -public class Step02_AccountOpening(ITestOutputHelper output) : BaseTest(output, redirectSystemConsoleOutput: true) +public class Step02a_AccountOpening(ITestOutputHelper output) : BaseTest(output, redirectSystemConsoleOutput: true) { // Target Open AI Services protected override bool ForceOpenAI => true; @@ -144,22 +144,10 @@ private KernelProcess SetupAccountOpeningProcess() where TUserIn public async Task UseAccountOpeningProcessSuccessfulInteractionAsync() { Kernel kernel = CreateKernelWithChatCompletion(); - KernelProcess kernelProcess = SetupAccountOpeningProcess(); + KernelProcess kernelProcess = SetupAccountOpeningProcess(); using var runningProcess = await kernelProcess.StartAsync(kernel, new KernelProcessEvent() { Id = AccountOpeningEvents.StartProcess, Data = null }); } - private sealed class UserInputSuccessfulInteraction : ScriptedUserInputStep - { - public override void PopulateUserInputs(UserInputState state) - { - state.UserInputs.Add("I would like to open an account"); - state.UserInputs.Add("My name is John Contoso, dob 02/03/1990"); - state.UserInputs.Add("I live in Washington and my phone number es 222-222-1234"); - state.UserInputs.Add("My userId is 987-654-3210"); - state.UserInputs.Add("My email is john.contoso@contoso.com, what else do you need?"); - } - } - /// /// This test uses a specific DOB that makes the creditScore to fail /// @@ -167,22 +155,10 @@ public override void PopulateUserInputs(UserInputState state) public async Task UseAccountOpeningProcessFailureDueToCreditScoreFailureAsync() { Kernel kernel = CreateKernelWithChatCompletion(); - KernelProcess kernelProcess = SetupAccountOpeningProcess(); + KernelProcess kernelProcess = SetupAccountOpeningProcess(); using var runningProcess = await kernelProcess.StartAsync(kernel, new KernelProcessEvent() { Id = AccountOpeningEvents.StartProcess, Data = null }); } - private sealed class UserInputCreditScoreFailureInteraction : ScriptedUserInputStep - { - public override void PopulateUserInputs(UserInputState state) - { - state.UserInputs.Add("I would like to open an account"); - state.UserInputs.Add("My name is John Contoso, dob 01/01/1990"); - state.UserInputs.Add("I live in Washington and my phone number es 222-222-1234"); - state.UserInputs.Add("My userId is 987-654-3210"); - state.UserInputs.Add("My email is john.contoso@contoso.com, what else do you need?"); - } - } - /// /// This test uses a specific userId that makes the fraudDetection to fail /// @@ -190,19 +166,7 @@ public override void PopulateUserInputs(UserInputState state) public async Task UseAccountOpeningProcessFailureDueToFraudFailureAsync() { Kernel kernel = CreateKernelWithChatCompletion(); - KernelProcess kernelProcess = SetupAccountOpeningProcess(); + KernelProcess kernelProcess = SetupAccountOpeningProcess(); using var runningProcess = await kernelProcess.StartAsync(kernel, new KernelProcessEvent() { Id = AccountOpeningEvents.StartProcess, Data = null }); } - - private sealed class UserInputFraudFailureInteraction : ScriptedUserInputStep - { - public override void PopulateUserInputs(UserInputState state) - { - state.UserInputs.Add("I would like to open an account"); - state.UserInputs.Add("My name is John Contoso, dob 02/03/1990"); - state.UserInputs.Add("I live in Washington and my phone number es 222-222-1234"); - state.UserInputs.Add("My userId is 123-456-7890"); - state.UserInputs.Add("My email is john.contoso@contoso.com, what else do you need?"); - } - } } diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Step02b_AccountOpening.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Step02b_AccountOpening.cs new file mode 100644 index 000000000000..b14b659cd20f --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Step02b_AccountOpening.cs @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Events; +using Microsoft.SemanticKernel; +using SharedSteps; +using Step02.Models; +using Step02.Processes; +using Step02.Steps; + +namespace Step02; + +/// +/// Demonstrate creation of and +/// eliciting its response to five explicit user messages.
+/// For each test there is a different set of user messages that will cause different steps to be triggered using the same pipeline.
+/// For visual reference of the process check the diagram. +///
+public class Step02b_AccountOpening(ITestOutputHelper output) : BaseTest(output, redirectSystemConsoleOutput: true) +{ + // Target Open AI Services + protected override bool ForceOpenAI => true; + + private KernelProcess SetupAccountOpeningProcess() where TUserInputStep : ScriptedUserInputStep + { + ProcessBuilder process = new("AccountOpeningProcessWithSubprocesses"); + var newCustomerFormStep = process.AddStepFromType(); + var userInputStep = process.AddStepFromType(); + var displayAssistantMessageStep = process.AddStepFromType(); + + var accountVerificationStep = process.AddStepFromProcess(NewAccountVerificationProcess.CreateProcess()); + var accountCreationStep = process.AddStepFromProcess(NewAccountCreationProcess.CreateProcess()); + + var mailServiceStep = process.AddStepFromType(); + + process + .OnInputEvent(AccountOpeningEvents.StartProcess) + .SendEventTo(new ProcessFunctionTargetBuilder(newCustomerFormStep, CompleteNewCustomerFormStep.Functions.NewAccountWelcome)); + + // When the welcome message is generated, send message to displayAssistantMessageStep + newCustomerFormStep + .OnEvent(AccountOpeningEvents.NewCustomerFormWelcomeMessageComplete) + .SendEventTo(new ProcessFunctionTargetBuilder(displayAssistantMessageStep, DisplayAssistantMessageStep.Functions.DisplayAssistantMessage)); + + // When the userInput step emits a user input event, send it to the newCustomerForm step + // Function names are necessary when the step has multiple public functions like CompleteNewCustomerFormStep: NewAccountWelcome and NewAccountProcessUserInfo + userInputStep + .OnEvent(CommonEvents.UserInputReceived) + .SendEventTo(new ProcessFunctionTargetBuilder(newCustomerFormStep, CompleteNewCustomerFormStep.Functions.NewAccountProcessUserInfo, "userMessage")); + + userInputStep + .OnEvent(CommonEvents.Exit) + .StopProcess(); + + // When the newCustomerForm step emits needs more details, send message to displayAssistantMessage step + newCustomerFormStep + .OnEvent(AccountOpeningEvents.NewCustomerFormNeedsMoreDetails) + .SendEventTo(new ProcessFunctionTargetBuilder(displayAssistantMessageStep, DisplayAssistantMessageStep.Functions.DisplayAssistantMessage)); + + // After any assistant message is displayed, user input is expected to the next step is the userInputStep + displayAssistantMessageStep + .OnEvent(CommonEvents.AssistantResponseGenerated) + .SendEventTo(new ProcessFunctionTargetBuilder(userInputStep, ScriptedUserInputStep.Functions.GetUserInput)); + + // When the newCustomerForm is completed... + newCustomerFormStep + .OnEvent(AccountOpeningEvents.NewCustomerFormCompleted) + // The information gets passed to the account verificatino step + .SendEventTo(accountVerificationStep.WhereInputEventIs(AccountOpeningEvents.NewCustomerFormCompleted)) + // The information gets passed to the validation process step + .SendEventTo(accountCreationStep.WhereInputEventIs(AccountOpeningEvents.NewCustomerFormCompleted)); + + // When the newCustomerForm is completed, the user interaction transcript with the user is passed to the core system record creation step + newCustomerFormStep + .OnEvent(AccountOpeningEvents.CustomerInteractionTranscriptReady) + .SendEventTo(accountCreationStep.WhereInputEventIs(AccountOpeningEvents.CustomerInteractionTranscriptReady)); + + // When the creditScoreCheck step results in Rejection, the information gets to the mailService step to notify the user about the state of the application and the reasons + accountVerificationStep + .OnEvent(AccountOpeningEvents.CreditScoreCheckRejected) + .SendEventTo(new ProcessFunctionTargetBuilder(mailServiceStep)); + + // When the fraudDetectionCheck step fails, the information gets to the mailService step to notify the user about the state of the application and the reasons + accountVerificationStep + .OnEvent(AccountOpeningEvents.FraudDetectionCheckFailed) + .SendEventTo(new ProcessFunctionTargetBuilder(mailServiceStep)); + + // When the fraudDetectionCheck step passes, the information gets to core system record creation step to kickstart this step + accountVerificationStep + .OnEvent(AccountOpeningEvents.FraudDetectionCheckPassed) + .SendEventTo(accountCreationStep.WhereInputEventIs(AccountOpeningEvents.NewAccountVerificationCheckPassed)); + + // After crmRecord and marketing gets created, a welcome packet is created to then send information to the user with the mailService step + accountCreationStep + .OnEvent(AccountOpeningEvents.WelcomePacketCreated) + .SendEventTo(new ProcessFunctionTargetBuilder(mailServiceStep)); + + // All possible paths end up with the user being notified about the account creation decision throw the mailServiceStep completion + mailServiceStep + .OnEvent(AccountOpeningEvents.MailServiceSent) + .StopProcess(); + + KernelProcess kernelProcess = process.Build(); + + return kernelProcess; + } + + /// + /// This test uses a specific userId and DOB that makes the creditScore and Fraud detection to pass + /// + [Fact] + public async Task UseAccountOpeningProcessSuccessfulInteractionAsync() + { + Kernel kernel = CreateKernelWithChatCompletion(); + KernelProcess kernelProcess = SetupAccountOpeningProcess(); + using var runningProcess = await kernelProcess.StartAsync(kernel, new KernelProcessEvent() { Id = AccountOpeningEvents.StartProcess, Data = null }); + } + + /// + /// This test uses a specific DOB that makes the creditScore to fail + /// + [Fact] + public async Task UseAccountOpeningProcessFailureDueToCreditScoreFailureAsync() + { + Kernel kernel = CreateKernelWithChatCompletion(); + KernelProcess kernelProcess = SetupAccountOpeningProcess(); + using var runningProcess = await kernelProcess.StartAsync(kernel, new KernelProcessEvent() { Id = AccountOpeningEvents.StartProcess, Data = null }); + } + + /// + /// This test uses a specific userId that makes the fraudDetection to fail + /// + [Fact] + public async Task UseAccountOpeningProcessFailureDueToFraudFailureAsync() + { + Kernel kernel = CreateKernelWithChatCompletion(); + KernelProcess kernelProcess = SetupAccountOpeningProcess(); + using var runningProcess = await kernelProcess.StartAsync(kernel, new KernelProcessEvent() { Id = AccountOpeningEvents.StartProcess, Data = null }); + } +} diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/CRMRecordCreationStep.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/CRMRecordCreationStep.cs index 10eb2aee468e..e62e8aae45f4 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/CRMRecordCreationStep.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/CRMRecordCreationStep.cs @@ -18,6 +18,8 @@ public static class Functions [KernelFunction(Functions.CreateCRMEntry)] public async Task CreateCRMEntryAsync(KernelProcessStepContext context, AccountUserInteractionDetails userInteractionDetails, Kernel _kernel) { + Console.WriteLine($"[CRM ENTRY CREATION] New Account {userInteractionDetails.AccountId} created"); + // Placeholder for a call to API to create new CRM entry await context.EmitEventAsync(new() { Id = AccountOpeningEvents.CRMRecordInfoEntryCreated, Data = true }); } diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/CompleteNewCustomerFormStep.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/CompleteNewCustomerFormStep.cs index 2a347a96f89c..25d35872d0e0 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/CompleteNewCustomerFormStep.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/CompleteNewCustomerFormStep.cs @@ -39,6 +39,8 @@ The user may provide information to fill up multiple fields of the form in one m - Your goal is to help guide the user to provide the missing details on the current form. - Encourage the user to provide the remainingdetails with examples if necessary. - Fields with value 'Unanswered' need to be answered by the user. + - Format phone numbers and user ids correctly if the user does not provide the expected format. + - If the user does not make use of parenthesis in the phone number, add them. - For date fields, confirm with the user first if the date format is not clear. Example 02/03 03/02 could be March 2nd or February 3rd. """; @@ -100,7 +102,7 @@ public async Task CompleteNewCustomerFormAsync(KernelProcessStepContext context, ChatHistory chatHistory = new(); chatHistory.AddSystemMessage(_formCompletionSystemPrompt .Replace("{{current_form_state}}", JsonSerializer.Serialize(_state!.newCustomerForm.CopyWithDefaultValues(), _jsonOptions))); - chatHistory.AddUserMessage(userMessage); + chatHistory.AddRange(_state.conversation); IChatCompletionService chatService = kernel.Services.GetRequiredService(); ChatMessageContent response = await chatService.GetChatMessageContentAsync(chatHistory, settings, kernel).ConfigureAwait(false); var assistantResponse = ""; @@ -114,9 +116,10 @@ public async Task CompleteNewCustomerFormAsync(KernelProcessStepContext context, if (_state?.newCustomerForm != null && _state.newCustomerForm.IsFormCompleted()) { + Console.WriteLine($"[NEW_USER_FORM_COMPLETED]: {JsonSerializer.Serialize(_state?.newCustomerForm)}"); // All user information is gathered to proceed to the next step - await context.EmitEventAsync(new() { Id = AccountOpeningEvents.NewCustomerFormCompleted, Data = _state?.newCustomerForm }); - await context.EmitEventAsync(new() { Id = AccountOpeningEvents.CustomerInteractionTranscriptReady, Data = _state?.conversation }); + await context.EmitEventAsync(new() { Id = AccountOpeningEvents.NewCustomerFormCompleted, Data = _state?.newCustomerForm, Visibility = KernelProcessEventVisibility.Public }); + await context.EmitEventAsync(new() { Id = AccountOpeningEvents.CustomerInteractionTranscriptReady, Data = _state?.conversation, Visibility = KernelProcessEventVisibility.Public }); return; } diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/CreditScoreCheckStep.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/CreditScoreCheckStep.cs index 655902640ac7..8455237ea872 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/CreditScoreCheckStep.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/CreditScoreCheckStep.cs @@ -25,10 +25,16 @@ public async Task DetermineCreditScoreAsync(KernelProcessStepContext context, Ne if (creditScore >= MinCreditScore) { + Console.WriteLine("[CREDIT CHECK] Credit Score Check Passed"); await context.EmitEventAsync(new() { Id = AccountOpeningEvents.CreditScoreCheckApproved, Data = true }); return; } - - await context.EmitEventAsync(new() { Id = AccountOpeningEvents.CreditScoreCheckRejected, Data = $"We regret to inform you that your credit score of {creditScore} is insufficient to apply for an account of the type PRIME ABC" }); + Console.WriteLine("[CREDIT CHECK] Credit Score Check Failed"); + await context.EmitEventAsync(new() + { + Id = AccountOpeningEvents.CreditScoreCheckRejected, + Data = $"We regret to inform you that your credit score of {creditScore} is insufficient to apply for an account of the type PRIME ABC", + Visibility = KernelProcessEventVisibility.Public, + }); } } diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/FraudDetectionStep.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/FraudDetectionStep.cs index e6fa082f60f7..5461f13006d4 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/FraudDetectionStep.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/FraudDetectionStep.cs @@ -21,14 +21,17 @@ public async Task FraudDetectionCheckAsync(KernelProcessStepContext context, boo // Placeholder for a call to API to validate user details for fraud detection if (customerDetails.UserId == "123-456-7890") { + Console.WriteLine("[FRAUD CHECK] Fraud Check Failed"); await context.EmitEventAsync(new() { Id = AccountOpeningEvents.FraudDetectionCheckFailed, - Data = "We regret to inform you that we found some inconsistent details regarding the information you provided regarding the new account of the type PRIME ABC you applied." + Data = "We regret to inform you that we found some inconsistent details regarding the information you provided regarding the new account of the type PRIME ABC you applied.", + Visibility = KernelProcessEventVisibility.Public, }); return; } - await context.EmitEventAsync(new() { Id = AccountOpeningEvents.FraudDetectionCheckPassed, Data = true }); + Console.WriteLine("[FRAUD CHECK] Fraud Check Passed"); + await context.EmitEventAsync(new() { Id = AccountOpeningEvents.FraudDetectionCheckPassed, Data = true, Visibility = KernelProcessEventVisibility.Public }); } } diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/NewAccountStep.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/NewAccountStep.cs index 19314a0d0d43..5c79e9b1de76 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/NewAccountStep.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/NewAccountStep.cs @@ -33,6 +33,8 @@ public async Task CreateNewAccountAsync(KernelProcessStepContext context, bool p AccountType = AccountType.PrimeABC, }; + Console.WriteLine($"[ACCOUNT CREATION] New Account {accountId} created"); + await context.EmitEventAsync(new() { Id = AccountOpeningEvents.NewMarketingRecordInfoReady, diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/NewMarketingEntryStep.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/NewMarketingEntryStep.cs index 55da96d76a45..96bba3e8f02a 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/NewMarketingEntryStep.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/NewMarketingEntryStep.cs @@ -18,6 +18,8 @@ public static class Functions [KernelFunction(Functions.CreateNewMarketingEntry)] public async Task CreateNewMarketingEntryAsync(KernelProcessStepContext context, MarketingNewEntryDetails userDetails, Kernel _kernel) { + Console.WriteLine($"[MARKETING ENTRY CREATION] New Account {userDetails.AccountId} created"); + // Placeholder for a call to API to create new entry of user for marketing purposes await context.EmitEventAsync(new() { Id = AccountOpeningEvents.NewMarketingEntryCreated, Data = true }); } diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/TestInputs/UserInputCreditScoreFailureInteractionStep.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/TestInputs/UserInputCreditScoreFailureInteractionStep.cs new file mode 100644 index 000000000000..49f2a970343f --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/TestInputs/UserInputCreditScoreFailureInteractionStep.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft. All rights reserved. + +using SharedSteps; + +namespace Step02.Steps; + +/// +/// Step with interactions that makes the Process fail due credit score failure +/// +public sealed class UserInputCreditScoreFailureInteractionStep : ScriptedUserInputStep +{ + public override void PopulateUserInputs(UserInputState state) + { + state.UserInputs.Add("I would like to open an account"); + state.UserInputs.Add("My name is John Contoso, dob 01/01/1990"); + state.UserInputs.Add("I live in Washington and my phone number es 222-222-1234"); + state.UserInputs.Add("My userId is 987-654-3210"); + state.UserInputs.Add("My email is john.contoso@contoso.com, what else do you need?"); + } +} diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/TestInputs/UserInputFraudFailureInteractionStep.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/TestInputs/UserInputFraudFailureInteractionStep.cs new file mode 100644 index 000000000000..0d8b4580e876 --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/TestInputs/UserInputFraudFailureInteractionStep.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft. All rights reserved. + +using SharedSteps; + +namespace Step02.Steps; + +/// +/// Step with interactions that makes the Process fail due fraud detection failure +/// +public sealed class UserInputFraudFailureInteractionStep : ScriptedUserInputStep +{ + public override void PopulateUserInputs(UserInputState state) + { + state.UserInputs.Add("I would like to open an account"); + state.UserInputs.Add("My name is John Contoso, dob 02/03/1990"); + state.UserInputs.Add("I live in Washington and my phone number es 222-222-1234"); + state.UserInputs.Add("My userId is 123-456-7890"); + state.UserInputs.Add("My email is john.contoso@contoso.com, what else do you need?"); + } +} diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/TestInputs/UserInputSuccessfulInteractionStep.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/TestInputs/UserInputSuccessfulInteractionStep.cs new file mode 100644 index 000000000000..a8a50484b103 --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/TestInputs/UserInputSuccessfulInteractionStep.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft. All rights reserved. + +using SharedSteps; + +namespace Step02.Steps; + +/// +/// Step with interactions that makes the Process pass all steps and successfully open a new account +/// +public sealed class UserInputSuccessfulInteractionStep : ScriptedUserInputStep +{ + public override void PopulateUserInputs(UserInputState state) + { + state.UserInputs.Add("I would like to open an account"); + state.UserInputs.Add("My name is John Contoso, dob 02/03/1990"); + state.UserInputs.Add("I live in Washington and my phone number es 222-222-1234"); + state.UserInputs.Add("My userId is 987-654-3210"); + state.UserInputs.Add("My email is john.contoso@contoso.com, what else do you need?"); + } +} diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/WelcomePacketStep.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/WelcomePacketStep.cs index a316f29cde31..3f9349f5eeb3 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/WelcomePacketStep.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Steps/WelcomePacketStep.cs @@ -18,6 +18,8 @@ public static class Functions [KernelFunction(Functions.CreateWelcomePacket)] public async Task CreateWelcomePacketAsync(KernelProcessStepContext context, bool marketingEntryCreated, bool crmRecordCreated, AccountDetails accountDetails, Kernel _kernel) { + Console.WriteLine($"[WELCOME PACKET] New Account {accountDetails.AccountId} created"); + var mailMessage = $""" Dear {accountDetails.UserFirstName} {accountDetails.UserLastName} We are thrilled to inform you that you have successfully created a new PRIME ABC Account with us! @@ -40,6 +42,7 @@ await context.EmitEventAsync(new() { Id = AccountOpeningEvents.WelcomePacketCreated, Data = mailMessage, + Visibility = KernelProcessEventVisibility.Public, }); } } diff --git a/dotnet/src/Experimental/Process.IntegrationTests.Resources/ProcessCycleTestResources.cs b/dotnet/src/Experimental/Process.IntegrationTests.Resources/ProcessCycleTestResources.cs index 19a58bf5d35a..384bf9877919 100644 --- a/dotnet/src/Experimental/Process.IntegrationTests.Resources/ProcessCycleTestResources.cs +++ b/dotnet/src/Experimental/Process.IntegrationTests.Resources/ProcessCycleTestResources.cs @@ -3,6 +3,7 @@ using System; using System.Linq; using System.Runtime.Serialization; +using System.Threading; using System.Threading.Tasks; using Microsoft.SemanticKernel; @@ -124,7 +125,7 @@ public string Echo(string message) } /// -/// A step that repeats its input. +/// A step that repeats its input. Emits data internally AND publicly /// public sealed class RepeatStep : KernelProcessStep { @@ -149,6 +150,62 @@ public async Task RepeatAsync(string message, KernelProcessStepContext context, } } +/// +/// A step that emits the input received internally OR publicly. +/// +public sealed class EmitterStep : KernelProcessStep +{ + public const string EventId = "Next"; + public const string PublicEventId = "PublicNext"; + public const string InputEvent = "OnInput"; + public const string Name = nameof(EmitterStep); + + public const string InternalEventFunction = "SomeInternalFunctionName"; + public const string PublicEventFunction = "SomePublicFunctionName"; + public const string DualInputPublicEventFunction = "SomeDualInputPublicEventFunctionName"; + + private readonly int _sleepDurationMs = 150; + + private StepState? _state; + + public override ValueTask ActivateAsync(KernelProcessStepState state) + { + this._state = state.State; + return default; + } + + [KernelFunction(InternalEventFunction)] + public async Task InternalTestFunctionAsync(KernelProcessStepContext context, string data) + { + Thread.Sleep(this._sleepDurationMs); + + Console.WriteLine($"[EMIT_INTERNAL] {data}"); + this._state!.LastMessage = data; + await context.EmitEventAsync(new() { Id = EventId, Data = data }); + } + + [KernelFunction(PublicEventFunction)] + public async Task PublicTestFunctionAsync(KernelProcessStepContext context, string data) + { + Thread.Sleep(this._sleepDurationMs); + + Console.WriteLine($"[EMIT_PUBLIC] {data}"); + this._state!.LastMessage = data; + await context.EmitEventAsync(new() { Id = PublicEventId, Data = data, Visibility = KernelProcessEventVisibility.Public }); + } + + [KernelFunction(DualInputPublicEventFunction)] + public async Task DualInputPublicTestFunctionAsync(KernelProcessStepContext context, string firstInput, string secondInput) + { + Thread.Sleep(this._sleepDurationMs); + + string outputText = $"{firstInput}-{secondInput}"; + Console.WriteLine($"[EMIT_PUBLIC_DUAL] {outputText}"); + this._state!.LastMessage = outputText; + await context.EmitEventAsync(new() { Id = ProcessTestsEvents.OutputReadyPublic, Data = outputText, Visibility = KernelProcessEventVisibility.Public }); + } +} + /// /// A step that emits a startProcess event /// diff --git a/dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessTests.cs b/dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessTests.cs index 88abe1bab1e7..d5d2ca19934e 100644 --- a/dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessTests.cs +++ b/dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessTests.cs @@ -59,9 +59,7 @@ public async Task LinearProcessAsync() var processInfo = await processHandle.GetStateAsync(); // Assert - var repeatStepState = processInfo.Steps.Where(s => s.State.Name == nameof(RepeatStep)).FirstOrDefault()?.State as KernelProcessStepState; - Assert.NotNull(repeatStepState?.State); - Assert.Equal(string.Join(" ", Enumerable.Repeat(testInput, 2)), repeatStepState.State.LastMessage); + this.AssertStepStateLastMessage(processInfo, nameof(RepeatStep), expectedLastMessage: string.Join(" ", Enumerable.Repeat(testInput, 2))); } /// @@ -101,9 +99,7 @@ public async Task NestedProcessOuterToInnerWorksAsync() // Assert var innerProcess = processInfo.Steps.Where(s => s.State.Name == "Inner").Single() as KernelProcess; Assert.NotNull(innerProcess); - var repeatStepState = innerProcess.Steps.Where(s => s.State.Name == nameof(RepeatStep)).Single().State as KernelProcessStepState; - Assert.NotNull(repeatStepState?.State); - Assert.Equal(string.Join(" ", Enumerable.Repeat(testInput, 4)), repeatStepState.State.LastMessage); + this.AssertStepStateLastMessage(innerProcess, nameof(RepeatStep), expectedLastMessage: string.Join(" ", Enumerable.Repeat(testInput, 4))); } /// @@ -145,9 +141,7 @@ public async Task NestedProcessInnerToOuterWorksWithPublicEventAsync() var processInfo = await processHandle.GetStateAsync(); // Assert - var repeatStepState = processInfo.Steps.Where(s => s.State.Name == nameof(RepeatStep)).FirstOrDefault()?.State as KernelProcessStepState; - Assert.NotNull(repeatStepState?.State); - Assert.Equal(string.Join(" ", Enumerable.Repeat(testInput, 4)), repeatStepState.State.LastMessage); + this.AssertStepStateLastMessage(processInfo, nameof(RepeatStep), expectedLastMessage: string.Join(" ", Enumerable.Repeat(testInput, 4))); } /// @@ -189,9 +183,7 @@ public async Task NestedProcessInnerToOuterDoesNotWorkWithInternalEventAsync() var processInfo = await processHandle.GetStateAsync(); // Assert - var repeatStepState = processInfo.Steps.Where(s => s.State.Name == nameof(RepeatStep)).FirstOrDefault()?.State as KernelProcessStepState; - Assert.NotNull(repeatStepState); - Assert.Null(repeatStepState.State?.LastMessage); + this.AssertStepStateLastMessage(processInfo, nameof(RepeatStep), expectedLastMessage: null); } /// @@ -212,9 +204,7 @@ public async Task FanInProcessAsync() var processInfo = await processHandle.GetStateAsync(); // Assert - var outputStep = processInfo.Steps.Where(s => s.State.Name == nameof(FanInStep)).FirstOrDefault()?.State as KernelProcessStepState; - Assert.NotNull(outputStep?.State); - Assert.Equal($"{testInput}-{testInput} {testInput}", outputStep.State.LastMessage); + this.AssertStepStateLastMessage(processInfo, nameof(FanInStep), expectedLastMessage: $"{testInput}-{testInput} {testInput}"); } /// @@ -234,13 +224,8 @@ public async Task ProcessWithErrorEmitsErrorEventAsync() var processInfo = await processHandle.GetStateAsync(); // Assert - var reportStep = processInfo.Steps.Where(s => s.State.Name == nameof(ReportStep)).FirstOrDefault()?.State as KernelProcessStepState; - Assert.NotNull(reportStep?.State); - Assert.Equal(1, reportStep.State.InvocationCount); - - var repeatStep = processInfo.Steps.Where(s => s.State.Name == nameof(RepeatStep)).FirstOrDefault()?.State as KernelProcessStepState; - Assert.NotNull(repeatStep?.State); - Assert.Null(repeatStep.State.LastMessage); + this.AssertStepStateLastMessage(processInfo, nameof(ReportStep), expectedLastMessage: null, expectedInvocationCount: 1); + this.AssertStepStateLastMessage(processInfo, nameof(RepeatStep), expectedLastMessage: null); } /// @@ -267,13 +252,134 @@ public async Task StepAndFanInProcessAsync() var processInfo = await processHandle.GetStateAsync(); // Assert - var outputStep = (processInfo.Steps.Where(s => s.State.Name == fanInStepName).FirstOrDefault() as KernelProcess)?.Steps.Where(s => s.State.Name == nameof(FanInStep)).FirstOrDefault()?.State as KernelProcessStepState; - Assert.NotNull(outputStep?.State); - Assert.Equal($"{testInput}-{testInput} {testInput}", outputStep.State.LastMessage); + var subprocessStepInfo = processInfo.Steps.Where(s => s.State.Name == fanInStepName)?.FirstOrDefault() as KernelProcess; + Assert.NotNull(subprocessStepInfo); + this.AssertStepStateLastMessage(subprocessStepInfo, nameof(FanInStep), expectedLastMessage: $"{testInput}-{testInput} {testInput}"); + } + + /// + /// Process with multiple "long" nested sequential subprocesses and with multiple single step + /// output fan out only steps + /// + /// ┌───────────────────────────────────────────────┐ + /// │ ▼ + /// ┌───────┐ │ ┌──────────────┐ ┌──────────────┐ ┌──────┐ + /// │ 1st ├──┼──►│ 2nd-nested ├──┬─►│ 3rd-nested ├─┬─►│ last │ + /// └───────┘ │ └──────────────┘ │ └──────────────┘ │ └──────┘ + /// ▼ ▼ ▼ + /// ┌─────────┐ ┌─────────┐ ┌─────────┐ + /// │ output1 │ │ output2 │ │ output3 │ + /// └─────────┘ └─────────┘ └─────────┘ + /// + /// + /// + [Fact] + public async Task ProcessWith2NestedSubprocessSequentiallyAndMultipleOutputStepsAsync() + { + // Arrange + Kernel kernel = this._kernelBuilder.Build(); + string lastStepName = "lastEmitterStep"; + string outputStepName1 = "outputStep1"; + string outputStepName2 = "outputStep2"; + string outputStepName3 = "outputStep3"; + ProcessBuilder processBuilder = new(nameof(ProcessWith2NestedSubprocessSequentiallyAndMultipleOutputStepsAsync)); + + ProcessStepBuilder firstStep = processBuilder.AddStepFromType("firstEmitterStep"); + ProcessBuilder secondStep = processBuilder.AddStepFromProcess(this.CreateLongSequentialProcessWithFanInAsOutputStep("subprocess1")); + ProcessBuilder thirdStep = processBuilder.AddStepFromProcess(this.CreateLongSequentialProcessWithFanInAsOutputStep("subprocess2")); + ProcessStepBuilder outputStep1 = processBuilder.AddStepFromType(outputStepName1); + ProcessStepBuilder outputStep2 = processBuilder.AddStepFromType(outputStepName2); + ProcessStepBuilder outputStep3 = processBuilder.AddStepFromType(outputStepName3); + ProcessStepBuilder lastStep = processBuilder.AddStepFromType(lastStepName); + + processBuilder + .OnInputEvent(EmitterStep.InputEvent) + .SendEventTo(new ProcessFunctionTargetBuilder(firstStep, functionName: EmitterStep.InternalEventFunction)); + firstStep + .OnEvent(EmitterStep.EventId) + .SendEventTo(secondStep.WhereInputEventIs(EmitterStep.InputEvent)) + .SendEventTo(new ProcessFunctionTargetBuilder(outputStep1, functionName: EmitterStep.PublicEventFunction)); + secondStep + .OnEvent(ProcessTestsEvents.OutputReadyPublic) + .SendEventTo(thirdStep.WhereInputEventIs(EmitterStep.InputEvent)) + .SendEventTo(new ProcessFunctionTargetBuilder(outputStep2, functionName: EmitterStep.PublicEventFunction)); + thirdStep + .OnEvent(ProcessTestsEvents.OutputReadyPublic) + .SendEventTo(new ProcessFunctionTargetBuilder(lastStep, parameterName: "secondInput")) + .SendEventTo(new ProcessFunctionTargetBuilder(outputStep3, functionName: EmitterStep.PublicEventFunction)); + + firstStep + .OnEvent(EmitterStep.EventId) + .SendEventTo(new ProcessFunctionTargetBuilder(lastStep, parameterName: "firstInput")); + + KernelProcess process = processBuilder.Build(); + + // Act + string testInput = "SomeData"; + var processHandle = await this._fixture.StartProcessAsync(process, kernel, new KernelProcessEvent() { Id = EmitterStep.InputEvent, Data = testInput }); + var processInfo = await processHandle.GetStateAsync(); + + // Assert + this.AssertStepStateLastMessage(processInfo, outputStepName1, expectedLastMessage: testInput); + this.AssertStepStateLastMessage(processInfo, outputStepName2, expectedLastMessage: $"{testInput}-{testInput}"); + this.AssertStepStateLastMessage(processInfo, outputStepName3, expectedLastMessage: $"{testInput}-{testInput}-{testInput}-{testInput}"); + this.AssertStepStateLastMessage(processInfo, lastStepName, expectedLastMessage: $"{testInput}-{testInput}-{testInput}-{testInput}-{testInput}"); } + #region Predefined ProcessBuilders for testing /// - /// Creates a simple linear process with two steps. + /// Sample long sequential process, each step has a delay.
+ /// Input Event:
+ /// Output Event:
+ /// + /// ┌───────────────────────────────────────────────┐ + /// │ ▼ + /// ┌───────┐ │ ┌───────┐ ┌───────┐ ┌────────┐ ┌──────┐ + /// │ 1st ├──┴──►│ 2nd ├───►│ ... ├───►│ 10th ├───►│ last │ + /// └───────┘ └───────┘ └───────┘ └────────┘ └──────┘ + /// + ///
+ /// name of the process + /// + private ProcessBuilder CreateLongSequentialProcessWithFanInAsOutputStep(string name) + { + ProcessBuilder processBuilder = new(name); + ProcessStepBuilder firstNestedStep = processBuilder.AddStepFromType("firstNestedStep"); + ProcessStepBuilder secondNestedStep = processBuilder.AddStepFromType("secondNestedStep"); + ProcessStepBuilder thirdNestedStep = processBuilder.AddStepFromType("thirdNestedStep"); + ProcessStepBuilder fourthNestedStep = processBuilder.AddStepFromType("fourthNestedStep"); + ProcessStepBuilder fifthNestedStep = processBuilder.AddStepFromType("fifthNestedStep"); + ProcessStepBuilder sixthNestedStep = processBuilder.AddStepFromType("sixthNestedStep"); + ProcessStepBuilder seventhNestedStep = processBuilder.AddStepFromType("seventhNestedStep"); + ProcessStepBuilder eighthNestedStep = processBuilder.AddStepFromType("eighthNestedStep"); + ProcessStepBuilder ninthNestedStep = processBuilder.AddStepFromType("ninthNestedStep"); + ProcessStepBuilder tenthNestedStep = processBuilder.AddStepFromType("tenthNestedStep"); + + processBuilder.OnInputEvent(EmitterStep.InputEvent).SendEventTo(new ProcessFunctionTargetBuilder(firstNestedStep, functionName: EmitterStep.InternalEventFunction)); + firstNestedStep.OnEvent(EmitterStep.EventId).SendEventTo(new ProcessFunctionTargetBuilder(secondNestedStep, functionName: EmitterStep.InternalEventFunction)); + secondNestedStep.OnEvent(EmitterStep.EventId).SendEventTo(new ProcessFunctionTargetBuilder(thirdNestedStep, functionName: EmitterStep.InternalEventFunction)); + thirdNestedStep.OnEvent(EmitterStep.EventId).SendEventTo(new ProcessFunctionTargetBuilder(fourthNestedStep, functionName: EmitterStep.InternalEventFunction)); + fourthNestedStep.OnEvent(EmitterStep.EventId).SendEventTo(new ProcessFunctionTargetBuilder(fifthNestedStep, functionName: EmitterStep.InternalEventFunction)); + fifthNestedStep.OnEvent(EmitterStep.EventId).SendEventTo(new ProcessFunctionTargetBuilder(sixthNestedStep, functionName: EmitterStep.InternalEventFunction)); + sixthNestedStep.OnEvent(EmitterStep.EventId).SendEventTo(new ProcessFunctionTargetBuilder(seventhNestedStep, functionName: EmitterStep.InternalEventFunction)); + seventhNestedStep.OnEvent(EmitterStep.EventId).SendEventTo(new ProcessFunctionTargetBuilder(eighthNestedStep, functionName: EmitterStep.InternalEventFunction)); + eighthNestedStep.OnEvent(EmitterStep.EventId).SendEventTo(new ProcessFunctionTargetBuilder(ninthNestedStep, functionName: EmitterStep.InternalEventFunction)); + ninthNestedStep.OnEvent(EmitterStep.EventId).SendEventTo(new ProcessFunctionTargetBuilder(tenthNestedStep, functionName: EmitterStep.DualInputPublicEventFunction, parameterName: "secondInput")); + + firstNestedStep.OnEvent(EmitterStep.EventId).SendEventTo(new ProcessFunctionTargetBuilder(tenthNestedStep, functionName: EmitterStep.DualInputPublicEventFunction, parameterName: "firstInput")); + + return processBuilder; + } + + /// + /// Creates a simple linear process with two steps.
+ /// Input Event:
+ /// Output Events: [, ]
+ /// + /// ┌────────┐ ┌────────┐ + /// │ echo ├───►│ repeat │ + /// └────────┘ └────────┘ + /// ///
private ProcessBuilder CreateLinearProcess(string name) { @@ -290,13 +396,30 @@ private ProcessBuilder CreateLinearProcess(string name) return processBuilder; } + /// + /// Simple process with fan in functionality.
+ /// Input Event:
+ /// Output Events:
+ /// + /// ┌─────────┐ + /// │ echoA ├──────┐ + /// └─────────┘ ▼ + /// ┌────────┐ + /// │ fanInC │ + /// └────────┘ + /// ┌─────────┐ ▲ + /// │ repeatB ├──────┘ + /// └─────────┘ + /// + ///
+ /// name of the process + /// private ProcessBuilder CreateFanInProcess(string name) { var processBuilder = new ProcessBuilder(name); var echoAStep = processBuilder.AddStepFromType("EchoStepA"); var repeatBStep = processBuilder.AddStepFromType("RepeatStepB"); var fanInCStep = processBuilder.AddStepFromType(); - var echoDStep = processBuilder.AddStepFromType(); processBuilder.OnInputEvent(ProcessTestsEvents.StartProcess).SendEventTo(new ProcessFunctionTargetBuilder(echoAStep)); processBuilder.OnInputEvent(ProcessTestsEvents.StartProcess).SendEventTo(new ProcessFunctionTargetBuilder(repeatBStep, parameterName: "message")); @@ -307,6 +430,22 @@ private ProcessBuilder CreateFanInProcess(string name) return processBuilder; } + /// + /// Creates a simple linear process with that emit error events.
+ /// Input Event:
+ /// Output Events:
+ /// + /// ┌────────┐ + /// ┌───────►│ repeat │ + /// │ └────────┘ + /// ┌───┴───┐ + /// │ error │ + /// └───┬───┘ + /// │ ┌────────┐ + /// └───────►│ report │ + /// └────────┘ + /// + ///
private ProcessBuilder CreateProcessWithError(string name) { var processBuilder = new ProcessBuilder(name); @@ -320,4 +459,19 @@ private ProcessBuilder CreateProcessWithError(string name) return processBuilder; } + #endregion + #region Assert Utils + private void AssertStepStateLastMessage(KernelProcess processInfo, string stepName, string? expectedLastMessage, int? expectedInvocationCount = null) + { + KernelProcessStepInfo? stepInfo = processInfo.Steps.FirstOrDefault(s => s.State.Name == stepName); + Assert.NotNull(stepInfo); + var outputStepResult = stepInfo.State as KernelProcessStepState; + Assert.NotNull(outputStepResult?.State); + Assert.Equal(expectedLastMessage, outputStepResult.State.LastMessage); + if (expectedInvocationCount.HasValue) + { + Assert.Equal(expectedInvocationCount.Value, outputStepResult.State.InvocationCount); + } + } + #endregion } diff --git a/dotnet/src/Experimental/Process.LocalRuntime/LocalProcess.cs b/dotnet/src/Experimental/Process.LocalRuntime/LocalProcess.cs index c35f2ed96c49..b7a6695996f4 100644 --- a/dotnet/src/Experimental/Process.LocalRuntime/LocalProcess.cs +++ b/dotnet/src/Experimental/Process.LocalRuntime/LocalProcess.cs @@ -293,12 +293,8 @@ private async Task Internal_ExecuteAsync(Kernel? kernel = null, int maxSuperstep } finally { - if (this._processCancelSource?.IsCancellationRequested ?? false) - { - this._processCancelSource.Cancel(); - } - this._processCancelSource?.Dispose(); + this._processCancelSource = null; } return;