.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

<!-- Before submitting this PR, please make sure: -->

- [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 😄

---------

Co-authored-by: Estefania Tenorio <[email protected]>
This commit is contained in:
Estefanía Tenorio
2024-12-04 16:59:45 +00:00
committed by GitHub
co-authored by Estefania Tenorio
parent 30b67ec715
commit 3c13912c9d
23 changed files with 686 additions and 87 deletions
@@ -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 <br/> Customer <br/> 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 <br/> to Fill New <br/> Customer Form)
NewAccountVerification[[New Account Verification<br/> Process]]
NewAccountCreation[[New Account Creation<br/> Process]]
Mailer(Mail <br/> Service)
User<-->|Provides user details|FillForm
FillForm-->|New User Form|NewAccountVerification
NewAccountVerification-->|Account Credit Check<br/> Verification Failed|Mailer
NewAccountVerification-->|Account Fraud<br/> Detection Failed|Mailer
NewAccountVerification-->|Account Verification <br/> Succeeded|NewAccountCreation
NewAccountCreation-->|Account Creation <br/> 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 <br/> Record <br/> Creation)
Marketing(New Marketing <br/> Record Creation)
CRM(CRM Record <br/> Creation)
Welcome(Welcome <br/> 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 <br/> Step]
FraudCheck[Fraud Detection <br/> Step]
AccountVerificationPass([Account Verification Passed])
AccountCreditCheckFail([Credit Check Failed])
AccountFraudCheckFail([Fraud Check Failed])
NewUser2-->CreditScoreCheck-->|Credit Score <br/> 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.
@@ -4,7 +4,7 @@ namespace Step02.Models;
/// <summary>
/// Represents the data structure for a form capturing details of a new customer, including personal information, contact details, account id and account type.<br/>
/// Class used in <see cref="Step02_AccountOpening"/> samples
/// Class used in <see cref="Step02a_AccountOpening"/>, <see cref="Step02b_AccountOpening"/> samples
/// </summary>
public class AccountDetails : NewCustomerForm
{
@@ -3,7 +3,7 @@ namespace Step02.Models;
/// <summary>
/// Processes Events related to Account Opening scenarios.<br/>
/// Class used in <see cref="Step02_AccountOpening"/> samples
/// Class used in <see cref="Step02a_AccountOpening"/>, <see cref="Step02b_AccountOpening"/> samples
/// </summary>
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);
@@ -7,7 +7,7 @@ namespace Step02.Models;
/// <summary>
/// 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.<br/>
/// Class used in <see cref="Step02_AccountOpening"/> samples
/// Class used in <see cref="Step02a_AccountOpening"/>, <see cref="Step02b_AccountOpening"/> samples
/// </summary>
public record AccountUserInteractionDetails
{
@@ -4,7 +4,7 @@ namespace Step02.Models;
/// <summary>
/// Holds details for a new entry in a marketing database, including the account identifier, contact name, phone number, and email address.<br/>
/// Class used in <see cref="Step02_AccountOpening"/> samples
/// Class used in <see cref="Step02a_AccountOpening"/>, <see cref="Step02b_AccountOpening"/> samples
/// </summary>
public record MarketingNewEntryDetails
{
@@ -7,7 +7,7 @@ namespace Step02.Models;
/// <summary>
/// Represents the data structure for a form capturing details of a new customer, including personal information and contact details.<br/>
/// Class used in <see cref="Step02_AccountOpening"/> samples
/// Class used in <see cref="Step02a_AccountOpening"/>, <see cref="Step02b_AccountOpening"/> samples
/// </summary>
public class NewCustomerForm
{
@@ -0,0 +1,70 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.SemanticKernel;
using Step02.Models;
using Step02.Steps;
namespace Step02.Processes;
/// <summary>
/// Demonstrate creation of <see cref="KernelProcess"/> and
/// eliciting its response to five explicit user messages.<br/>
/// For each test there is a different set of user messages that will cause different steps to be triggered using the same pipeline.<br/>
/// For visual reference of the process check the <see href="https://github.com/microsoft/semantic-kernel/tree/main/dotnet/samples/GettingStartedWithProcesses/README.md#step02b_accountOpening" >diagram</see>.
/// </summary>
public static class NewAccountCreationProcess
{
public static ProcessBuilder CreateProcess()
{
ProcessBuilder process = new("AccountCreationProcess");
var coreSystemRecordCreationStep = process.AddStepFromType<NewAccountStep>();
var marketingRecordCreationStep = process.AddStepFromType<NewMarketingEntryStep>();
var crmRecordStep = process.AddStepFromType<CRMRecordCreationStep>();
var welcomePacketStep = process.AddStepFromType<WelcomePacketStep>();
// 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;
}
}
@@ -0,0 +1,39 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.SemanticKernel;
using Step02.Models;
using Step02.Steps;
namespace Step02.Processes;
/// <summary>
/// Demonstrate creation of <see cref="KernelProcess"/> and
/// eliciting its response to five explicit user messages.<br/>
/// For each test there is a different set of user messages that will cause different steps to be triggered using the same pipeline.<br/>
/// For visual reference of the process check the <see href="https://github.com/microsoft/semantic-kernel/tree/main/dotnet/samples/GettingStartedWithProcesses/README.md#step02b_accountOpening" >diagram</see>.
/// </summary>
public static class NewAccountVerificationProcess
{
public static ProcessBuilder CreateProcess()
{
ProcessBuilder process = new("AccountVerificationProcess");
var customerCreditCheckStep = process.AddStepFromType<CreditScoreCheckStep>();
var fraudDetectionCheckStep = process.AddStepFromType<FraudDetectionStep>();
// 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;
}
}
@@ -12,9 +12,9 @@ namespace Step02;
/// Demonstrate creation of <see cref="KernelProcess"/> and
/// eliciting its response to five explicit user messages.<br/>
/// For each test there is a different set of user messages that will cause different steps to be triggered using the same pipeline.<br/>
/// For visual reference of the process check the <see href="https://github.com/microsoft/semantic-kernel/tree/main/dotnet/samples/GettingStartedWithProcesses/README.md#step02_accountOpening" >diagram</see> .
/// For visual reference of the process check the <see href="https://github.com/microsoft/semantic-kernel/tree/main/dotnet/samples/GettingStartedWithProcesses/README.md#step02a_accountOpening" >diagram</see>.
/// </summary>
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 @@ public class Step02_AccountOpening(ITestOutputHelper output) : BaseTest(output,
public async Task UseAccountOpeningProcessSuccessfulInteractionAsync()
{
Kernel kernel = CreateKernelWithChatCompletion();
KernelProcess kernelProcess = SetupAccountOpeningProcess<UserInputSuccessfulInteraction>();
KernelProcess kernelProcess = SetupAccountOpeningProcess<UserInputSuccessfulInteractionStep>();
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 [email protected], what else do you need?");
}
}
/// <summary>
/// This test uses a specific DOB that makes the creditScore to fail
/// </summary>
@@ -167,22 +155,10 @@ public class Step02_AccountOpening(ITestOutputHelper output) : BaseTest(output,
public async Task UseAccountOpeningProcessFailureDueToCreditScoreFailureAsync()
{
Kernel kernel = CreateKernelWithChatCompletion();
KernelProcess kernelProcess = SetupAccountOpeningProcess<UserInputCreditScoreFailureInteraction>();
KernelProcess kernelProcess = SetupAccountOpeningProcess<UserInputCreditScoreFailureInteractionStep>();
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 [email protected], what else do you need?");
}
}
/// <summary>
/// This test uses a specific userId that makes the fraudDetection to fail
/// </summary>
@@ -190,19 +166,7 @@ public class Step02_AccountOpening(ITestOutputHelper output) : BaseTest(output,
public async Task UseAccountOpeningProcessFailureDueToFraudFailureAsync()
{
Kernel kernel = CreateKernelWithChatCompletion();
KernelProcess kernelProcess = SetupAccountOpeningProcess<UserInputFraudFailureInteraction>();
KernelProcess kernelProcess = SetupAccountOpeningProcess<UserInputFraudFailureInteractionStep>();
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 [email protected], what else do you need?");
}
}
}
@@ -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;
/// <summary>
/// Demonstrate creation of <see cref="KernelProcess"/> and
/// eliciting its response to five explicit user messages.<br/>
/// For each test there is a different set of user messages that will cause different steps to be triggered using the same pipeline.<br/>
/// For visual reference of the process check the <see href="https://github.com/microsoft/semantic-kernel/tree/main/dotnet/samples/GettingStartedWithProcesses/README.md#step02a_accountOpening" >diagram</see>.
/// </summary>
public class Step02b_AccountOpening(ITestOutputHelper output) : BaseTest(output, redirectSystemConsoleOutput: true)
{
// Target Open AI Services
protected override bool ForceOpenAI => true;
private KernelProcess SetupAccountOpeningProcess<TUserInputStep>() where TUserInputStep : ScriptedUserInputStep
{
ProcessBuilder process = new("AccountOpeningProcessWithSubprocesses");
var newCustomerFormStep = process.AddStepFromType<CompleteNewCustomerFormStep>();
var userInputStep = process.AddStepFromType<TUserInputStep>();
var displayAssistantMessageStep = process.AddStepFromType<DisplayAssistantMessageStep>();
var accountVerificationStep = process.AddStepFromProcess(NewAccountVerificationProcess.CreateProcess());
var accountCreationStep = process.AddStepFromProcess(NewAccountCreationProcess.CreateProcess());
var mailServiceStep = process.AddStepFromType<MailServiceStep>();
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;
}
/// <summary>
/// This test uses a specific userId and DOB that makes the creditScore and Fraud detection to pass
/// </summary>
[Fact]
public async Task UseAccountOpeningProcessSuccessfulInteractionAsync()
{
Kernel kernel = CreateKernelWithChatCompletion();
KernelProcess kernelProcess = SetupAccountOpeningProcess<UserInputSuccessfulInteractionStep>();
using var runningProcess = await kernelProcess.StartAsync(kernel, new KernelProcessEvent() { Id = AccountOpeningEvents.StartProcess, Data = null });
}
/// <summary>
/// This test uses a specific DOB that makes the creditScore to fail
/// </summary>
[Fact]
public async Task UseAccountOpeningProcessFailureDueToCreditScoreFailureAsync()
{
Kernel kernel = CreateKernelWithChatCompletion();
KernelProcess kernelProcess = SetupAccountOpeningProcess<UserInputCreditScoreFailureInteractionStep>();
using var runningProcess = await kernelProcess.StartAsync(kernel, new KernelProcessEvent() { Id = AccountOpeningEvents.StartProcess, Data = null });
}
/// <summary>
/// This test uses a specific userId that makes the fraudDetection to fail
/// </summary>
[Fact]
public async Task UseAccountOpeningProcessFailureDueToFraudFailureAsync()
{
Kernel kernel = CreateKernelWithChatCompletion();
KernelProcess kernelProcess = SetupAccountOpeningProcess<UserInputFraudFailureInteractionStep>();
using var runningProcess = await kernelProcess.StartAsync(kernel, new KernelProcessEvent() { Id = AccountOpeningEvents.StartProcess, Data = null });
}
}
@@ -18,6 +18,8 @@ public class CRMRecordCreationStep : KernelProcessStep
[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 });
}
@@ -39,6 +39,8 @@ public class CompleteNewCustomerFormStep : KernelProcessStep<NewCustomerFormStat
- 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 class CompleteNewCustomerFormStep : KernelProcessStep<NewCustomerFormStat
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<IChatCompletionService>();
ChatMessageContent response = await chatService.GetChatMessageContentAsync(chatHistory, settings, kernel).ConfigureAwait(false);
var assistantResponse = "";
@@ -114,9 +116,10 @@ public class CompleteNewCustomerFormStep : KernelProcessStep<NewCustomerFormStat
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;
}
@@ -25,10 +25,16 @@ public class CreditScoreCheckStep : KernelProcessStep
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,
});
}
}
@@ -21,14 +21,17 @@ public class FraudDetectionStep : KernelProcessStep
// 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 });
}
}
@@ -33,6 +33,8 @@ public class NewAccountStep : KernelProcessStep
AccountType = AccountType.PrimeABC,
};
Console.WriteLine($"[ACCOUNT CREATION] New Account {accountId} created");
await context.EmitEventAsync(new()
{
Id = AccountOpeningEvents.NewMarketingRecordInfoReady,
@@ -18,6 +18,8 @@ public class NewMarketingEntryStep : KernelProcessStep
[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 });
}
@@ -0,0 +1,20 @@
// Copyright (c) Microsoft. All rights reserved.
using SharedSteps;
namespace Step02.Steps;
/// <summary>
/// <see cref="ScriptedUserInputStep"/> Step with interactions that makes the Process fail due credit score failure
/// </summary>
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 [email protected], what else do you need?");
}
}
@@ -0,0 +1,20 @@
// Copyright (c) Microsoft. All rights reserved.
using SharedSteps;
namespace Step02.Steps;
/// <summary>
/// <see cref="ScriptedUserInputStep"/> Step with interactions that makes the Process fail due fraud detection failure
/// </summary>
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 [email protected], what else do you need?");
}
}
@@ -0,0 +1,20 @@
// Copyright (c) Microsoft. All rights reserved.
using SharedSteps;
namespace Step02.Steps;
/// <summary>
/// <see cref="ScriptedUserInputStep"/> Step with interactions that makes the Process pass all steps and successfully open a new account
/// </summary>
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 [email protected], what else do you need?");
}
}
@@ -18,6 +18,8 @@ public class WelcomePacketStep : KernelProcessStep
[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 @@ public class WelcomePacketStep : KernelProcessStep
{
Id = AccountOpeningEvents.WelcomePacketCreated,
Data = mailMessage,
Visibility = KernelProcessEventVisibility.Public,
});
}
}
@@ -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 sealed class EchoStep : KernelProcessStep
}
/// <summary>
/// A step that repeats its input.
/// A step that repeats its input. Emits data internally AND publicly
/// </summary>
public sealed class RepeatStep : KernelProcessStep<StepState>
{
@@ -149,6 +150,62 @@ public sealed class RepeatStep : KernelProcessStep<StepState>
}
}
/// <summary>
/// A step that emits the input received internally OR publicly.
/// </summary>
public sealed class EmitterStep : KernelProcessStep<StepState>
{
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<StepState> 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 });
}
}
/// <summary>
/// A step that emits a startProcess event
/// </summary>
@@ -59,9 +59,7 @@ public sealed class ProcessTests : IClassFixture<ProcessTestFixture>
var processInfo = await processHandle.GetStateAsync();
// Assert
var repeatStepState = processInfo.Steps.Where(s => s.State.Name == nameof(RepeatStep)).FirstOrDefault()?.State as KernelProcessStepState<StepState>;
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)));
}
/// <summary>
@@ -101,9 +99,7 @@ public sealed class ProcessTests : IClassFixture<ProcessTestFixture>
// 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<StepState>;
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)));
}
/// <summary>
@@ -145,9 +141,7 @@ public sealed class ProcessTests : IClassFixture<ProcessTestFixture>
var processInfo = await processHandle.GetStateAsync();
// Assert
var repeatStepState = processInfo.Steps.Where(s => s.State.Name == nameof(RepeatStep)).FirstOrDefault()?.State as KernelProcessStepState<StepState>;
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)));
}
/// <summary>
@@ -189,9 +183,7 @@ public sealed class ProcessTests : IClassFixture<ProcessTestFixture>
var processInfo = await processHandle.GetStateAsync();
// Assert
var repeatStepState = processInfo.Steps.Where(s => s.State.Name == nameof(RepeatStep)).FirstOrDefault()?.State as KernelProcessStepState<StepState>;
Assert.NotNull(repeatStepState);
Assert.Null(repeatStepState.State?.LastMessage);
this.AssertStepStateLastMessage(processInfo, nameof(RepeatStep), expectedLastMessage: null);
}
/// <summary>
@@ -212,9 +204,7 @@ public sealed class ProcessTests : IClassFixture<ProcessTestFixture>
var processInfo = await processHandle.GetStateAsync();
// Assert
var outputStep = processInfo.Steps.Where(s => s.State.Name == nameof(FanInStep)).FirstOrDefault()?.State as KernelProcessStepState<StepState>;
Assert.NotNull(outputStep?.State);
Assert.Equal($"{testInput}-{testInput} {testInput}", outputStep.State.LastMessage);
this.AssertStepStateLastMessage(processInfo, nameof(FanInStep), expectedLastMessage: $"{testInput}-{testInput} {testInput}");
}
/// <summary>
@@ -234,13 +224,8 @@ public sealed class ProcessTests : IClassFixture<ProcessTestFixture>
var processInfo = await processHandle.GetStateAsync();
// Assert
var reportStep = processInfo.Steps.Where(s => s.State.Name == nameof(ReportStep)).FirstOrDefault()?.State as KernelProcessStepState<StepState>;
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<StepState>;
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);
}
/// <summary>
@@ -267,13 +252,134 @@ public sealed class ProcessTests : IClassFixture<ProcessTestFixture>
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<StepState>;
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}");
}
/// <summary>
/// Creates a simple linear process with two steps.
/// Process with multiple "long" nested sequential subprocesses and with multiple single step
/// output fan out only steps
/// <code>
/// ┌───────────────────────────────────────────────┐
/// │ ▼
/// ┌───────┐ │ ┌──────────────┐ ┌──────────────┐ ┌──────┐
/// │ 1st ├──┼──►│ 2nd-nested ├──┬─►│ 3rd-nested ├─┬─►│ last │
/// └───────┘ │ └──────────────┘ │ └──────────────┘ │ └──────┘
/// ▼ ▼ ▼
/// ┌─────────┐ ┌─────────┐ ┌─────────┐
/// │ output1 │ │ output2 │ │ output3 │
/// └─────────┘ └─────────┘ └─────────┘
/// </code>
/// </summary>
/// <returns><see cref="Task"/></returns>
[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<EmitterStep>("firstEmitterStep");
ProcessBuilder secondStep = processBuilder.AddStepFromProcess(this.CreateLongSequentialProcessWithFanInAsOutputStep("subprocess1"));
ProcessBuilder thirdStep = processBuilder.AddStepFromProcess(this.CreateLongSequentialProcessWithFanInAsOutputStep("subprocess2"));
ProcessStepBuilder outputStep1 = processBuilder.AddStepFromType<EmitterStep>(outputStepName1);
ProcessStepBuilder outputStep2 = processBuilder.AddStepFromType<EmitterStep>(outputStepName2);
ProcessStepBuilder outputStep3 = processBuilder.AddStepFromType<EmitterStep>(outputStepName3);
ProcessStepBuilder lastStep = processBuilder.AddStepFromType<FanInStep>(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
/// <summary>
/// Sample long sequential process, each step has a delay.<br/>
/// Input Event: <see cref="EmitterStep.InputEvent"/><br/>
/// Output Event: <see cref="ProcessTestsEvents.OutputReadyPublic"/><br/>
/// <code>
/// ┌───────────────────────────────────────────────┐
/// │ ▼
/// ┌───────┐ │ ┌───────┐ ┌───────┐ ┌────────┐ ┌──────┐
/// │ 1st ├──┴──►│ 2nd ├───►│ ... ├───►│ 10th ├───►│ last │
/// └───────┘ └───────┘ └───────┘ └────────┘ └──────┘
/// </code>
/// </summary>
/// <param name="name">name of the process</param>
/// <returns><see cref="ProcessBuilder"/></returns>
private ProcessBuilder CreateLongSequentialProcessWithFanInAsOutputStep(string name)
{
ProcessBuilder processBuilder = new(name);
ProcessStepBuilder firstNestedStep = processBuilder.AddStepFromType<EmitterStep>("firstNestedStep");
ProcessStepBuilder secondNestedStep = processBuilder.AddStepFromType<EmitterStep>("secondNestedStep");
ProcessStepBuilder thirdNestedStep = processBuilder.AddStepFromType<EmitterStep>("thirdNestedStep");
ProcessStepBuilder fourthNestedStep = processBuilder.AddStepFromType<EmitterStep>("fourthNestedStep");
ProcessStepBuilder fifthNestedStep = processBuilder.AddStepFromType<EmitterStep>("fifthNestedStep");
ProcessStepBuilder sixthNestedStep = processBuilder.AddStepFromType<EmitterStep>("sixthNestedStep");
ProcessStepBuilder seventhNestedStep = processBuilder.AddStepFromType<EmitterStep>("seventhNestedStep");
ProcessStepBuilder eighthNestedStep = processBuilder.AddStepFromType<EmitterStep>("eighthNestedStep");
ProcessStepBuilder ninthNestedStep = processBuilder.AddStepFromType<EmitterStep>("ninthNestedStep");
ProcessStepBuilder tenthNestedStep = processBuilder.AddStepFromType<EmitterStep>("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;
}
/// <summary>
/// Creates a simple linear process with two steps.<br/>
/// Input Event: <see cref="ProcessTestsEvents.StartProcess"/><br/>
/// Output Events: [<see cref="ProcessTestsEvents.OutputReadyInternal"/>, <see cref="ProcessTestsEvents.OutputReadyPublic"/>]<br/>
/// <code>
/// ┌────────┐ ┌────────┐
/// │ echo ├───►│ repeat │
/// └────────┘ └────────┘
/// </code>
/// </summary>
private ProcessBuilder CreateLinearProcess(string name)
{
@@ -290,13 +396,30 @@ public sealed class ProcessTests : IClassFixture<ProcessTestFixture>
return processBuilder;
}
/// <summary>
/// Simple process with fan in functionality.<br/>
/// Input Event: <see cref="ProcessTestsEvents.StartProcess"/><br/>
/// Output Events: <see cref="ProcessTestsEvents.OutputReadyPublic"/><br/>
/// <code>
/// ┌─────────┐
/// │ echoA ├──────┐
/// └─────────┘ ▼
/// ┌────────┐
/// │ fanInC │
/// └────────┘
/// ┌─────────┐ ▲
/// │ repeatB ├──────┘
/// └─────────┘
/// </code>
/// </summary>
/// <param name="name">name of the process</param>
/// <returns><see cref="ProcessBuilder"/></returns>
private ProcessBuilder CreateFanInProcess(string name)
{
var processBuilder = new ProcessBuilder(name);
var echoAStep = processBuilder.AddStepFromType<EchoStep>("EchoStepA");
var repeatBStep = processBuilder.AddStepFromType<RepeatStep>("RepeatStepB");
var fanInCStep = processBuilder.AddStepFromType<FanInStep>();
var echoDStep = processBuilder.AddStepFromType<EchoStep>();
processBuilder.OnInputEvent(ProcessTestsEvents.StartProcess).SendEventTo(new ProcessFunctionTargetBuilder(echoAStep));
processBuilder.OnInputEvent(ProcessTestsEvents.StartProcess).SendEventTo(new ProcessFunctionTargetBuilder(repeatBStep, parameterName: "message"));
@@ -307,6 +430,22 @@ public sealed class ProcessTests : IClassFixture<ProcessTestFixture>
return processBuilder;
}
/// <summary>
/// Creates a simple linear process with that emit error events.<br/>
/// Input Event: <see cref="ProcessTestsEvents.StartProcess"/><br/>
/// Output Events: <see cref="ProcessStepBuilder.OnFunctionError(string?)"/> <br/>
/// <code>
/// ┌────────┐
/// ┌───────►│ repeat │
/// │ └────────┘
/// ┌───┴───┐
/// │ error │
/// └───┬───┘
/// │ ┌────────┐
/// └───────►│ report │
/// └────────┘
/// </code>
/// </summary>
private ProcessBuilder CreateProcessWithError(string name)
{
var processBuilder = new ProcessBuilder(name);
@@ -320,4 +459,19 @@ public sealed class ProcessTests : IClassFixture<ProcessTestFixture>
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<StepState>;
Assert.NotNull(outputStepResult?.State);
Assert.Equal(expectedLastMessage, outputStepResult.State.LastMessage);
if (expectedInvocationCount.HasValue)
{
Assert.Equal(expectedInvocationCount.Value, outputStepResult.State.InvocationCount);
}
}
#endregion
}
@@ -293,12 +293,8 @@ internal sealed class LocalProcess : LocalStep, IDisposable
}
finally
{
if (this._processCancelSource?.IsCancellationRequested ?? false)
{
this._processCancelSource.Cancel();
}
this._processCancelSource?.Dispose();
this._processCancelSource = null;
}
return;