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;