mirror of
https://github.com/microsoft/semantic-kernel.git
synced 2026-10-12 02:19:06 +00:00
.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:
co-authored by
Estefania Tenorio
parent
30b67ec715
commit
3c13912c9d
@@ -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);
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
{
|
||||
|
||||
+1
-1
@@ -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
|
||||
{
|
||||
|
||||
+70
@@ -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;
|
||||
}
|
||||
}
|
||||
+39
@@ -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;
|
||||
}
|
||||
}
|
||||
+5
-41
@@ -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 });
|
||||
}
|
||||
|
||||
+6
-3
@@ -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 });
|
||||
}
|
||||
|
||||
+20
@@ -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?");
|
||||
}
|
||||
}
|
||||
+20
@@ -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?");
|
||||
}
|
||||
}
|
||||
+20
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+58
-1
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user