From 213b5bbfaad0c9438d9545b670aaaf99cc52dfdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estefan=C3=ADa=20Tenorio?= <8483207+esttenorio@users.noreply.github.com> Date: Mon, 4 Aug 2025 15:10:40 -0700 Subject: [PATCH 1/4] .Net: Processes: Local Runtime persistence update - enabling rehydrating process if stopped/resumed (#12838) ### Description This is a long one, trying to break down the changes by commit for the most part: - Renaming `step.name` -> `step.Stepid`, `step.id` -> `step.RunId`. - Updating namespace from `SemanticKernel.xxxx` to `Microsoft.SemanticKernel.xxx` - Checkpointing logic to store LocalRuntime process state in a custom storage connector - main logic - tests update + addition - updating samples and demos - disabling Dapr related changes in the meantime in this feature branch ### Contribution Checklist - [ ] The code builds clean without any errors or warnings - [ ] 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 - [ ] All unit tests pass, and I have added new tests where possible - [ ] I didn't break anyone :smile: --- dotnet/SK-dotnet.slnx | 8 +- .../Program.cs | 4 +- .../.vscode/tasks.json | 18 +- .../ProcessWithCloudEvents.Client/src/App.tsx | 139 +++++- .../src/common/AppConstants.ts | 9 + .../src/components/ChatHeader.tsx | 46 ++ .../src/components/GenerateDocumentsChat.tsx | 19 +- .../TeacherStudentInteractionChat.tsx | 251 ++++++++++ .../src/main.tsx | 2 +- .../grpc/DocumentGenerationGrpcClient.ts | 21 - .../grpc/gen/documentGeneration.client.ts | 30 +- .../services/grpc/gen/documentGeneration.ts | 48 +- .../gen/teacherStudentInteraction.client.ts | 71 +++ .../grpc/gen/teacherStudentInteraction.ts | 171 +++++++ .../src/services/grpc/grpcClients.ts | 50 ++ .../grpc/proto/documentGeneration.proto | 3 +- .../proto/teacherStudentInteraction.proto | 26 + ...sWithCloudEvents.Grpc.LocalRuntime.csproj} | 18 +- .../Program.cs | 90 ++++ .../Services/DocumentGenerationService.cs | 38 +- .../TeacherStudentInteractionService.cs | 107 ++++ .../appsettings.json | 4 +- .../ProcessWithCloudEvents.Grpc/Program.cs | 65 --- .../ProcessWithCloudEvents.Grpc/README.md | 131 ----- .../DocumentGenerationProcess.cs | 27 +- .../ProcessWithCloudEvents.Processes.csproj | 1 + .../TeacherStudentProcess.cs | 99 ++++ .../Extensions/ConfigurationExtension.cs | 0 .../ExtensionsUtilities.props | 6 + .../Clients/DocumentGenerationGrpcClient.cs | 6 +- .../TeacherStudentInteractionGrpcClient.cs | 72 +++ .../Grpc/Proto}/documentationGenerator.proto | 3 +- .../Proto/teacherStudentInteraction.proto | 26 + .../GrpcComponents.props | 6 + .../Options/CosmosDBOptions.cs | 22 + .../Options/OpenAIOptions.cs | 2 +- .../CosmosDbContainerStorageConnector.cs | 108 ++++ .../Storage/JsonFileStorage.cs | 81 +++ .../StorageComponents.props | 5 + .../Demos/ProcessWithCloudEvents/README.md | 2 +- .../Controllers/ProcessController.cs | 30 +- .../ProcessWithLocalRuntime.csproj} | 9 +- .../Program.cs | 8 - .../README.md | 28 +- .../appsettings.json | 0 .../GettingStartedWithProcesses.csproj | 12 +- .../Step01/Step01_Processes.cs | 6 +- .../Processes/NewAccountCreationProcess.cs | 69 +-- .../NewAccountVerificationProcess.cs | 24 +- .../Step02/Step02a_AccountOpening.cs | 108 ++-- .../Step02/Step02b_AccountOpening.cs | 2 +- .../Step03/Processes/FishAndChipsProcess.cs | 35 +- .../FishSandwichStateProcessSuccess.json | 54 -- ...shSandwichStateProcessSuccessLowStock.json | 55 --- .../FriedFishProcessStateSuccess.json | 28 -- .../FriedFishProcessStateSuccessLowStock.json | 29 -- .../FriedFishProcessStateSuccessNoStock.json | 29 -- ...thStatefulStepsProcess.ProcessDetails.json | 20 + ...8d1.ExternalFriedFishStep.StepDetails.json | 13 + ...thStatefulStepsProcess.ProcessDetails.json | 19 + ...-218c78c8a023.CutFoodStep.StepDetails.json | 13 + ...-76640557fa40.FryFoodStep.StepDetails.json | 13 + ...hIngredientsWithStockStep.StepDetails.json | 19 + ...c3442.AddSpecialSauceStep.StepDetails.json | 13 + ...-5c964c89fb93.AddBunsStep.StepDetails.json | 13 + ...thStatefulStepsProcess.ProcessDetails.json | 20 + ...-d6d61a9c5fd9.AddBunsStep.StepDetails.json | 13 + ...901.ExternalFriedFishStep.StepDetails.json | 13 + ...97633.AddSpecialSauceStep.StepDetails.json | 13 + ...thStatefulStepsProcess.ProcessDetails.json | 19 + ...-a0ce695271e5.FryFoodStep.StepDetails.json | 13 + ...-2a22f1c09050.CutFoodStep.StepDetails.json | 13 + ...hIngredientsWithStockStep.StepDetails.json | 19 + ...thStatefulStepsProcess.ProcessDetails.json | 19 + ...-aa7db2b01c30.CutFoodStep.StepDetails.json | 13 + ...-2f21d90ec252.FryFoodStep.StepDetails.json | 13 + ...hIngredientsWithStockStep.StepDetails.json | 19 + ...thStatefulStepsProcess.ProcessDetails.json | 19 + ...hIngredientsWithStockStep.StepDetails.json | 19 + ...-b375940c432c.FryFoodStep.StepDetails.json | 13 + ...-545e18535c54.CutFoodStep.StepDetails.json | 13 + ...thStatefulStepsProcess.ProcessDetails.json | 19 + ...-c81247d6c75a.FryFoodStep.StepDetails.json | 13 + ...hIngredientsWithStockStep.StepDetails.json | 19 + ...-f84ccc944a3c.CutFoodStep.StepDetails.json | 13 + .../Step03/Step03a_FoodPreparation.cs | 169 ++----- .../Step04/Step04_AgentOrchestration.cs | 18 +- .../Utilities/FileStorageUtilities.cs | 22 + .../IProcessStepStorageOperations.cs | 64 +++ .../IProcessStorageConnector.cs | 48 ++ .../IProcessStorageOperations.cs | 92 ++++ .../Process.Abstractions/KernelProcess.cs | 4 +- .../Process.Abstractions/KernelProcessMap.cs | 6 +- .../KernelProcessProxy.cs | 4 +- .../Process.Abstractions/KernelProcessStep.cs | 5 + .../KernelProcessStepInfo.cs | 11 +- .../KernelProcessStepState.cs | 50 +- .../Models/Storage/StorageEntryBase.cs | 17 + .../Models/Storage/StorageProcessData.cs | 32 ++ .../Models/Storage/StorageProcessEvents.cs | 26 + .../Storage/StorageProcessExtensions.cs | 67 +++ .../Models/Storage/StorageProcessInfo.cs | 32 ++ .../Models/Storage/StorageProcessState.cs | 21 + .../Models/Storage/StorageStepData.cs | 29 ++ .../Models/Storage/StorageStepEdgesData.cs | 29 ++ .../Models/Storage/StorageStepEvents.cs | 20 + .../Models/Storage/StorageStepExtensions.cs | 93 ++++ .../Models/Storage/StorageStepInfo.cs | 29 ++ .../Models/Storage/StorageStepState.cs | 31 ++ .../ProcessStorageManager.cs | 340 +++++++++++++ .../Process.Core/Internal/EndStep.cs | 3 +- .../KernelProcessStateMetadataExtension.cs | 87 +--- .../Process.Core/ListenForBuilder.cs | 2 +- .../Process.Core/ListenForTargetBuilder.cs | 15 +- .../Process.Core/ProcessAgentBuilder.cs | 14 +- .../Process.Core/ProcessBuilder.cs | 27 +- .../ProcessFunctionTargetBuilder.cs | 26 +- .../Process.Core/ProcessMapBuilder.cs | 11 +- .../Process.Core/ProcessProxyBuilder.cs | 10 +- .../Process.Core/ProcessStepBuilder.cs | 106 ++-- .../Process.Core/ProcessStepEdgeBuilder.cs | 8 +- .../Tools/ProcessVisualizationExtensions.cs | 16 +- .../Process.Core/Workflow/WorkflowBuilder.cs | 18 +- .../Contracts/ProcessStartRequest.cs | 4 +- .../Controllers/ProcessTestController.cs | 4 +- .../HealthActor.cs | 2 +- .../IHealthActor.cs | 2 +- .../ProcessStateTypeResolver.cs | 3 +- .../Program.cs | 4 +- .../DaprTestProcessContext.cs | 8 +- .../ProcessTestFixture.cs | 9 +- .../ProcessCloudEventsResources.cs | 2 +- .../ProcessCycleTestResources.cs | 25 +- .../ProcessMapTestResources.cs | 3 +- .../ProcessCloudEventsTests.cs | 13 +- .../ProcessCycleTests.cs | 24 +- .../ProcessMapTests.cs | 8 +- .../ProcessTestFixture.cs | 7 +- .../ProcessTests.cs | 461 +++++++++++++++++- .../TestSettings/OpenAIConfiguration.cs | 2 +- .../TestSettings/ProcessTestGroup.cs | 2 +- .../Process.LocalRuntime/LocalAgentStep.cs | 2 +- .../LocalEdgeGroupProcessor.cs | 40 +- .../LocalKernelProcessContext.cs | 19 +- .../LocalKernelProcessFactory.cs | 13 +- .../Process.LocalRuntime/LocalMap.cs | 2 +- .../Process.LocalRuntime/LocalProcess.cs | 181 ++++++- .../Process.LocalRuntime/LocalProxy.cs | 13 +- .../Process.LocalRuntime/LocalStep.cs | 188 +++++-- .../Process.Runtime.Dapr/Actors/MapActor.cs | 8 +- .../Actors/ProcessActor.cs | 22 +- .../Process.Runtime.Dapr/Actors/StepActor.cs | 6 +- .../DaprKernelProcessContext.cs | 10 +- .../DaprKernelProcessFactory.cs | 6 +- .../Process.Runtime.Dapr/DaprMapInfo.cs | 2 +- .../Process.Runtime.Dapr/DaprProcessInfo.cs | 2 +- .../Process.Runtime.Dapr/DaprProxyInfo.cs | 2 +- .../Core/ProcessBuilderTests.cs | 8 +- .../Core/ProcessMapBuilderTests.cs | 18 +- .../Core/ProcessProxyBuilderTests.cs | 10 +- .../Core/ProcessStepBuilderTests.cs | 9 +- .../Core/ProcessStepEdgeBuilderTests.cs | 2 +- .../KernelProcessProxyTests.cs | 4 +- .../KernelProcessSerializationTests.cs | 24 +- .../KernelProcessStateTests.cs | 8 +- .../Runtime.Local/LocalMapTests.cs | 8 +- .../Runtime.Local/LocalProcessTests.cs | 363 +++++++++++++- .../Runtime.Local/LocalProxyTests.cs | 10 +- .../Process.Utilities.UnitTests/CloneTests.cs | 52 +- .../KernelProcessStateMetadataFactory.cs | 18 +- .../KernelProcessStepEdgesExtension.cs | 39 ++ .../process/Abstractions/MapExtensions.cs | 2 +- .../process/Abstractions/ProcessExtensions.cs | 2 +- .../process/Abstractions/StepExtensions.cs | 43 +- .../process/LocalStorageComponents.props | 5 + .../process/Runtime/MapExtensions.cs | 6 +- .../CloudEvents/MockCloudEventClient.cs | 5 +- .../CloudEvents/MockCloudEventData.cs | 3 +- .../TestsShared/Processes/CommonProcesses.cs | 338 +++++++++++++ .../TestsShared/Services/CounterService.cs | 11 +- .../TestsShared/Services/ICounterService.cs | 9 +- .../Services/Storage/JsonFileStorage.cs | 78 +++ .../Services/Storage/MockStorage.cs | 76 +++ .../process/TestsShared/Setup/KernelSetup.cs | 5 +- .../process/TestsShared/Steps/CommonSteps.cs | 137 +++++- 185 files changed, 5507 insertions(+), 1286 deletions(-) create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/components/ChatHeader.tsx create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/components/TeacherStudentInteractionChat.tsx delete mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/services/grpc/DocumentGenerationGrpcClient.ts create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/services/grpc/gen/teacherStudentInteraction.client.ts create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/services/grpc/gen/teacherStudentInteraction.ts create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/services/grpc/grpcClients.ts create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/services/grpc/proto/teacherStudentInteraction.proto rename dotnet/samples/Demos/ProcessWithCloudEvents/{ProcessWithCloudEvents.Grpc/ProcessWithCloudEvents.Grpc.csproj => ProcessWithCloudEvents.Grpc.LocalRuntime/ProcessWithCloudEvents.Grpc.LocalRuntime.csproj} (66%) create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc.LocalRuntime/Program.cs rename dotnet/samples/Demos/ProcessWithCloudEvents/{ProcessWithCloudEvents.Grpc => ProcessWithCloudEvents.Grpc.LocalRuntime}/Services/DocumentGenerationService.cs (85%) create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc.LocalRuntime/Services/TeacherStudentInteractionService.cs rename dotnet/samples/Demos/ProcessWithCloudEvents/{ProcessWithCloudEvents.Grpc => ProcessWithCloudEvents.Grpc.LocalRuntime}/appsettings.json (81%) delete mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc/Program.cs delete mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc/README.md create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/TeacherStudentProcess.cs rename dotnet/samples/Demos/ProcessWithCloudEvents/{ProcessWithCloudEvents.Grpc => ProcessWithCloudEvents.Utilities}/Extensions/ConfigurationExtension.cs (100%) create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/ExtensionsUtilities.props rename dotnet/samples/Demos/ProcessWithCloudEvents/{ProcessWithCloudEvents.Grpc => ProcessWithCloudEvents.Utilities/Grpc}/Clients/DocumentGenerationGrpcClient.cs (95%) create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/Grpc/Clients/TeacherStudentInteractionGrpcClient.cs rename dotnet/samples/Demos/ProcessWithCloudEvents/{ProcessWithCloudEvents.Grpc/Protos => ProcessWithCloudEvents.Utilities/Grpc/Proto}/documentationGenerator.proto (90%) create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/Grpc/Proto/teacherStudentInteraction.proto create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/GrpcComponents.props create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/Options/CosmosDBOptions.cs rename dotnet/samples/Demos/ProcessWithCloudEvents/{ProcessWithCloudEvents.Grpc => ProcessWithCloudEvents.Utilities}/Options/OpenAIOptions.cs (87%) create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/Storage/CosmosDbContainerStorageConnector.cs create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/Storage/JsonFileStorage.cs create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/StorageComponents.props rename dotnet/samples/Demos/{ProcessWithDapr => ProcessWithLocalRuntime}/Controllers/ProcessController.cs (86%) rename dotnet/samples/Demos/{ProcessWithDapr/ProcessWithDapr.csproj => ProcessWithLocalRuntime/ProcessWithLocalRuntime.csproj} (78%) rename dotnet/samples/Demos/{ProcessWithDapr => ProcessWithLocalRuntime}/Program.cs (78%) rename dotnet/samples/Demos/{ProcessWithDapr => ProcessWithLocalRuntime}/README.md (66%) rename dotnet/samples/Demos/{ProcessWithDapr => ProcessWithLocalRuntime}/appsettings.json (100%) delete mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/ProcessesStates/FishSandwichStateProcessSuccess.json delete mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/ProcessesStates/FishSandwichStateProcessSuccessLowStock.json delete mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/ProcessesStates/FriedFishProcessStateSuccess.json delete mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/ProcessesStates/FriedFishProcessStateSuccessLowStock.json delete mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/ProcessesStates/FriedFishProcessStateSuccessNoStock.json create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccess/myId.FishSandwichWithStatefulStepsProcess.ProcessDetails.json create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccess/myId_4ee7fb9c-7f48-4178-b072-c7d10188f8d1.ExternalFriedFishStep.StepDetails.json create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccess/myId_53747e21-83d9-4b99-b20a-6c401b59a0c0.FriedFishWithStatefulStepsProcess.ProcessDetails.json create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccess/myId_53747e21-83d9-4b99-b20a-6c401b59a0c0_3b5be7de-3283-4887-aa2a-218c78c8a023.CutFoodStep.StepDetails.json create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccess/myId_53747e21-83d9-4b99-b20a-6c401b59a0c0_68861295-9da6-422e-9ce5-76640557fa40.FryFoodStep.StepDetails.json create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccess/myId_53747e21-83d9-4b99-b20a-6c401b59a0c0_c4572c50-e762-4a7a-88f9-7f2f08c78936.GatherFriedFishIngredientsWithStockStep.StepDetails.json create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccess/myId_97f458fb-2ea4-4f19-a39f-2ebff68c3442.AddSpecialSauceStep.StepDetails.json create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccess/myId_ec377137-ae9d-49a9-a77f-5c964c89fb93.AddBunsStep.StepDetails.json create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccessLowStock/myId.FishSandwichWithStatefulStepsProcess.ProcessDetails.json create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccessLowStock/myId_47732a13-5877-4de3-a531-d6d61a9c5fd9.AddBunsStep.StepDetails.json create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccessLowStock/myId_688eb817-9792-405b-92a1-f9f44632c901.ExternalFriedFishStep.StepDetails.json create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccessLowStock/myId_c89f75c7-d570-4ccb-9be4-b8346b197633.AddSpecialSauceStep.StepDetails.json create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccessLowStock/myId_eee643c2-e689-41f8-aa10-92be0b882f22.FriedFishWithStatefulStepsProcess.ProcessDetails.json create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccessLowStock/myId_eee643c2-e689-41f8-aa10-92be0b882f22_35cd8ebd-db77-40cf-90b1-a0ce695271e5.FryFoodStep.StepDetails.json create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccessLowStock/myId_eee643c2-e689-41f8-aa10-92be0b882f22_85fb9efa-6c9b-4736-bf83-2a22f1c09050.CutFoodStep.StepDetails.json create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccessLowStock/myId_eee643c2-e689-41f8-aa10-92be0b882f22_e933a214-d0d4-418d-9384-036accbd9382.GatherFriedFishIngredientsWithStockStep.StepDetails.json create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccess/myId.FriedFishWithStatefulStepsProcess.ProcessDetails.json create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccess/myId_204e59a3-05c4-4186-8e1f-aa7db2b01c30.CutFoodStep.StepDetails.json create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccess/myId_6e850696-7132-4cc4-8495-2f21d90ec252.FryFoodStep.StepDetails.json create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccess/myId_d2c7e47c-792e-4092-8faf-6035150d4076.GatherFriedFishIngredientsWithStockStep.StepDetails.json create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccessLowStock/myId.FriedFishWithStatefulStepsProcess.ProcessDetails.json create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccessLowStock/myId_18022019-3ea4-433b-853a-9fcd80c00695.GatherFriedFishIngredientsWithStockStep.StepDetails.json create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccessLowStock/myId_6295dc68-9f08-40de-9cf3-b375940c432c.FryFoodStep.StepDetails.json create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccessLowStock/myId_c9b8d06a-ee3c-45c7-87cc-545e18535c54.CutFoodStep.StepDetails.json create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccessNoStock/myId.FriedFishWithStatefulStepsProcess.ProcessDetails.json create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccessNoStock/myId_9ca6e906-fa39-4107-b3ec-c81247d6c75a.FryFoodStep.StepDetails.json create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccessNoStock/myId_d66ccaba-1a6c-421a-b990-95129b25a16b.GatherFriedFishIngredientsWithStockStep.StepDetails.json create mode 100644 dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccessNoStock/myId_da066941-2a4f-4f45-bda2-f84ccc944a3c.CutFoodStep.StepDetails.json create mode 100644 dotnet/samples/GettingStartedWithProcesses/Utilities/FileStorageUtilities.cs create mode 100644 dotnet/src/Experimental/Process.Abstractions/IProcessStepStorageOperations.cs create mode 100644 dotnet/src/Experimental/Process.Abstractions/IProcessStorageConnector.cs create mode 100644 dotnet/src/Experimental/Process.Abstractions/IProcessStorageOperations.cs create mode 100644 dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageEntryBase.cs create mode 100644 dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageProcessData.cs create mode 100644 dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageProcessEvents.cs create mode 100644 dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageProcessExtensions.cs create mode 100644 dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageProcessInfo.cs create mode 100644 dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageProcessState.cs create mode 100644 dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageStepData.cs create mode 100644 dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageStepEdgesData.cs create mode 100644 dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageStepEvents.cs create mode 100644 dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageStepExtensions.cs create mode 100644 dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageStepInfo.cs create mode 100644 dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageStepState.cs create mode 100644 dotnet/src/Experimental/Process.Abstractions/ProcessStorageManager.cs create mode 100644 dotnet/src/InternalUtilities/process/Abstractions/KernelProcessStepEdgesExtension.cs create mode 100644 dotnet/src/InternalUtilities/process/LocalStorageComponents.props create mode 100644 dotnet/src/InternalUtilities/process/TestsShared/Processes/CommonProcesses.cs create mode 100644 dotnet/src/InternalUtilities/process/TestsShared/Services/Storage/JsonFileStorage.cs create mode 100644 dotnet/src/InternalUtilities/process/TestsShared/Services/Storage/MockStorage.cs diff --git a/dotnet/SK-dotnet.slnx b/dotnet/SK-dotnet.slnx index b662baf25562..e8f77612051c 100644 --- a/dotnet/SK-dotnet.slnx +++ b/dotnet/SK-dotnet.slnx @@ -42,7 +42,7 @@ - + @@ -80,7 +80,7 @@ - + @@ -159,14 +159,10 @@ - - - - diff --git a/dotnet/samples/Demos/ProcessFrameworkWithAspire/ProcessFramework.Aspire/ProcessFramework.Aspire.ProcessOrchestrator/Program.cs b/dotnet/samples/Demos/ProcessFrameworkWithAspire/ProcessFramework.Aspire/ProcessFramework.Aspire.ProcessOrchestrator/Program.cs index d200c9f05cb1..62743479c396 100644 --- a/dotnet/samples/Demos/ProcessFrameworkWithAspire/ProcessFramework.Aspire/ProcessFramework.Aspire.ProcessOrchestrator/Program.cs +++ b/dotnet/samples/Demos/ProcessFrameworkWithAspire/ProcessFramework.Aspire/ProcessFramework.Aspire.ProcessOrchestrator/Program.cs @@ -34,11 +34,11 @@ processBuilder .OnInputEvent(ProcessEvents.TranslateDocument) - .SendEventTo(new(translateDocumentStep, TranslateStep.ProcessFunctions.Translate, parameterName: "textToTranslate")); + .SendEventTo(new(translateDocumentStep, TranslateStep.ProcessFunctions.Translate)); translateDocumentStep .OnEvent(ProcessEvents.DocumentTranslated) - .SendEventTo(new ProcessFunctionTargetBuilder(summarizeDocumentStep, SummarizeStep.ProcessFunctions.Summarize, parameterName: "textToSummarize")); + .SendEventTo(new ProcessFunctionTargetBuilder(summarizeDocumentStep, SummarizeStep.ProcessFunctions.Summarize)); summarizeDocumentStep .OnEvent(ProcessEvents.DocumentSummarized) diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/.vscode/tasks.json b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/.vscode/tasks.json index 2daee70e790c..7fc65f97a796 100644 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/.vscode/tasks.json +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/.vscode/tasks.json @@ -20,8 +20,24 @@ "type": "npm", "script": "protoc --ts_out .\\src\\services\\grpc\\gen --ts_opt generate_dependencies --proto_path .\\src\\services\\grpc\\proto .\\src\\services\\grpc\\proto\\documentGeneration.proto", "problemMatcher": [], - "label": "protobuf: generate document generation proto files", + "label": "protobuf: generate 'document generation' proto files", "detail": "Generate necessary proto files for document generation" + }, + { + "type": "npm", + "script": "protoc --ts_out .\\src\\services\\grpc\\gen --ts_opt generate_dependencies --proto_path .\\src\\services\\grpc\\proto .\\src\\services\\grpc\\proto\\teacherStudentInteraction.proto", + "problemMatcher": [], + "label": "protobuf: generate 'teacher student interaction' proto files", + "detail": "Generate necessary proto files for teacher student interaction" + }, + { + "label": "protobuf: generate all proto files", + "type": "shell", + "dependsOn": [ + "protobuf: generate 'document generation' proto files", + "protobuf: generate 'teacher student interaction' proto files" + ], + "problemMatcher": [] } ] } \ No newline at end of file diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/App.tsx b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/App.tsx index 29e8ed528163..0775c0947993 100644 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/App.tsx +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/App.tsx @@ -28,6 +28,12 @@ import GenerateDocsChat, { NewDocument, } from "./components/GenerateDocumentsChat"; import { ExitIcon } from "./components/Icons"; +import TeacherStudentInteractionChat, { + StudentTeacherEntry, + TeacherStudentInteractionUser, +} from "./components/TeacherStudentInteractionChat"; +import { grpcTeacherStudentService } from "./services/grpc/grpcClients"; +import { User } from "./services/grpc/gen/teacherStudentInteraction"; interface AppProps { grpcDocClient?: GrpcDocumentationGenerationClient; @@ -77,12 +83,16 @@ const App: React.FC = ({ grpcDocClient }) => { const [selectedAppPage, setSelectedAppPage] = useState( AppPages.DocumentGeneration ); + // generated documents related const [generatedDocuments, setGeneratedDocuments] = useState( [] ); const [publishedDocuments, setPublishedDocuments] = useState( [] ); + // teacher student interaction related + const [studentAgentInteractionMessages, setStudentAgentInteractionMessages] = + useState([]); const [hasGrpcError, setHasGrpcError] = useState(false); @@ -196,7 +206,9 @@ const App: React.FC = ({ grpcDocClient }) => { } }; - const subscribeToSpecificProcessId = async (processId: string) => { + const subscribeToSpecificProcessIdForDocumentGeneration = async ( + processId: string + ) => { subscribeReceiveDocumentForReview(processId); subscribeToReceivePublishedDocument(processId); return Promise.all([ @@ -207,6 +219,109 @@ const App: React.FC = ({ grpcDocClient }) => { }); }; + // teacher student interaction related + + const onUserStartedTeacherInteractionProcess = ( + processId: string + ): Promise => { + if (selectedCloudTech == CloudTechnology.GRPC) { + if (grpcTeacherStudentService) { + return grpcTeacherStudentService + .startProcess({ + processId: processId, + }) + .then(() => { + console.log( + "[GRPC] User student interaction process started" + ); + setHasGrpcError(false); + return true; + }) + .catch((error) => { + console.error( + "[GRPC] Error starting student interaction process", + error + ); + setHasGrpcError(true); + return false; + }); + } + } + return new Promise((resolve) => resolve(false)); + }; + + const onSendTeacherQuestion = ( + teacherInteraction: StudentTeacherEntry + ): Promise => { + if (selectedCloudTech == CloudTechnology.GRPC) { + if (grpcTeacherStudentService) { + return grpcTeacherStudentService + .requestStudentAgentResponse({ + processId: teacherInteraction.processId, + content: teacherInteraction.content!, + user: User.TEACHER, + }) + .then(() => { + console.log( + "[GRPC] User teacher question sent to student agent" + ); + setHasGrpcError(false); + return true; + }) + .catch((error) => { + console.error( + "[GRPC] Error sending teacher question to student agent", + error + ); + setHasGrpcError(true); + return false; + }); + } + } + return new Promise((resolve) => resolve(false)); + }; + + const subscribeToStudentAgentResponses = async (processId: string) => { + if (selectedCloudTech == CloudTechnology.GRPC) { + if (grpcTeacherStudentService) { + // grpc stream for receiving published document + const studentResponseStream = + grpcTeacherStudentService.receiveStudentAgentResponse({ + processId: processId, + }); + for await (const message of studentResponseStream.responses) { + setStudentAgentInteractionMessages((prevMessages) => [ + ...prevMessages, + { + processId: message.processId, + content: message.content, + user: TeacherStudentInteractionUser.STUDENT, + }, + ]); + console.log( + "[GRPC] Student interaction received: ", + message + ); + } + } + } + }; + + const subscribeToSpecificProcessIdForTeacherStudentInteraction = async ( + processId: string + ) => { + subscribeReceiveDocumentForReview(processId); + subscribeToReceivePublishedDocument(processId); + return Promise.all([subscribeToStudentAgentResponses(processId)]).then( + () => { + return; + } + ); + }; + + const getCloudTechnologyName = () => + CloudTechnologiesDetails.get(selectedCloudTech)!.name; + return (
@@ -285,19 +400,31 @@ const App: React.FC = ({ grpcDocClient }) => { )} {selectedAppPage == AppPages.DocumentGeneration && ( )} + {selectedAppPage == AppPages.TeacherStudentInteraction && ( + + )}
); diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/common/AppConstants.ts b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/common/AppConstants.ts index 24c620056c93..52cd0bc9f801 100644 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/common/AppConstants.ts +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/common/AppConstants.ts @@ -7,6 +7,7 @@ // Additionally update the AppPagesDetails map to include the new process sample and its details. export enum AppPages { DocumentGeneration = "DocumentGeneration", + TeacherStudentInteraction = "TeacherStudentInteraction", } interface EnumDetails { @@ -23,6 +24,14 @@ export const AppPagesDetails = new Map([ "Demo used to show case document generation using different cloud technologies with SK Processes", }, ], + [ + AppPages.TeacherStudentInteraction, + { + name: "Teacher Student Interaction", + description: + "Demo used to show case teacher student interaction using different cloud technologies and declarative agents with SK Processes", + } + ] ]); // When more cloud technologies are added, add them to this enum and the CloudTechnologiesDetails map below. diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/components/ChatHeader.tsx b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/components/ChatHeader.tsx new file mode 100644 index 000000000000..eddab6d6a2cf --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/components/ChatHeader.tsx @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2025 Microsoft + * All rights reserved. + */ + +import React from "react"; +import { Title2, Label, makeStyles } from "@fluentui/react-components"; // Adjust imports based on your UI library + +interface ChatHeaderProps { + header: string; + processId?: string; +} + +const useStyles = makeStyles({ + processIdContainer: { + display: "flex", + flexDirection: "column", + rowGap: "8px", + alignItems: "flex-end", + }, + headerContainer: { + display: "flex", + justifyContent: "space-between", + }, +}); + +const ChatHeader: React.FC = ({ + header, + processId, +}) => { + const styles = useStyles(); + + return ( +
+ + {header} + +
+ + +
+
+ ); +}; + +export default ChatHeader; diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/components/GenerateDocumentsChat.tsx b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/components/GenerateDocumentsChat.tsx index 4bd8ea3726a7..27b14e093b7a 100644 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/components/GenerateDocumentsChat.tsx +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/components/GenerateDocumentsChat.tsx @@ -22,6 +22,7 @@ import Markdown from "react-markdown"; import { ChatMessageContent, ChatUser } from "../common/ChatConstants"; import SimpleChat from "./SimpleChat"; import { CheckIcon, RejectIcon } from "./Icons"; +import ChatHeader from "./ChatHeader"; export interface NewDocument { title?: string; @@ -51,12 +52,6 @@ const useStyles = makeStyles({ rowGap: "8px", width: "90%", }, - processIdContainer: { - display: "flex", - flexDirection: "column", - rowGap: "8px", - alignItems: "flex-end", - }, buttonsFamily: { display: "flex", columnGap: "40px", @@ -69,10 +64,6 @@ const useStyles = makeStyles({ newDocHeaderHeader: { marginTop: "0", }, - headerContainer: { - display: "flex", - justifyContent: "space-between", - }, }); const GenerateDocsChat: React.FC = ({ @@ -264,13 +255,7 @@ const GenerateDocsChat: React.FC = ({ return (
-
- Document Generation with {cloudTechnologyName} -
- - -
-
+
diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/components/TeacherStudentInteractionChat.tsx b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/components/TeacherStudentInteractionChat.tsx new file mode 100644 index 000000000000..37ca69ae5a42 --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/components/TeacherStudentInteractionChat.tsx @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2025 Microsoft + * All rights reserved. + */ +import { useEffect, useState } from "react"; +import { v4 as uuidv4 } from "uuid"; +import { + Button, + Dropdown, + makeStyles, + Option, + Spinner, +} from "@fluentui/react-components"; +import { ChatMessageContent, ChatUser } from "../common/ChatConstants"; +import SimpleChat from "./SimpleChat"; +import ChatHeader from "./ChatHeader"; + +export enum TeacherStudentInteractionUser { + TEACHER = "TEACHER", + STUDENT = "STUDENT", +} + +export interface StudentTeacherEntry { + processId: string; + content?: string; + user: TeacherStudentInteractionUser; +} + +interface TeacherStudentInteractionChatProps { + cloudTechnologyName: string; + newStudentAgentResponses: StudentTeacherEntry[]; + onStartNewProcess?: (processId: string) => Promise; + onSendTeacherQuestion?: ( + teacherInteraction: StudentTeacherEntry + ) => Promise; + subscribeToSpecificProcessId?: (processId: string) => Promise; +} + +const useStyles = makeStyles({ + root: { + display: "flex", + flexDirection: "column", + rowGap: "8px", + width: "90%", + }, + processIdContainer: { + display: "flex", + flexDirection: "column", + rowGap: "8px", + alignItems: "flex-end", + }, + buttonsFamily: { + display: "flex", + columnGap: "40px", + }, + headerContainer: { + display: "flex", + justifyContent: "space-between", + }, +}); + +const teacherMathQuestions = [ + "What is 2 + 2?", + "Can you explain the Pythagorean theorem?", + "What is the integral of sin(x)?", + "What is the area of a circle?", +]; + +const TeacherStudentInteractionChat: React.FC< + TeacherStudentInteractionChatProps +> = ({ + cloudTechnologyName, + newStudentAgentResponses, + onStartNewProcess, + onSendTeacherQuestion, + subscribeToSpecificProcessId +}) => { + const styles = useStyles(); + + const [messages, setMessages] = useState([]); + const [processId, setProcessId] = useState(); + const [startingNewProcess, setStartingNewProcess] = + useState(false); + const [selectedTeacherQuestion, setSelectedTeacherQuestion] = + useState(); + + useEffect(() => { + if (processId) { + subscribeToSpecificProcessId?.(processId); + } + }, [processId]); + + useEffect(() => { + if (processId) { + // subscribeToSpecificProcessId(processId); + } + }, [processId]); + + const formatStudentResponseString = (response: string) => { + return `Student Agent: ${response}`; + }; + + useEffect(() => { + if (newStudentAgentResponses.length > 0) { + const lastResponse = + newStudentAgentResponses[newStudentAgentResponses.length - 1]; + if (lastResponse.processId !== processId) { + console.warn( + `Received response for a different processId - ignoring ${lastResponse}` + ); + return; + } + + setMessages((prevMessages) => [ + ...prevMessages, + { + sender: ChatUser.ASSISTANT, + content: formatStudentResponseString( + lastResponse.content ?? "" + ), + timestamp: new Date().toLocaleString(), + }, + ]); + } + }, [newStudentAgentResponses]); + + const OnStartNewProcessClicked = () => { + // Need to know processId to be able to subscribe to incoming events from this process once it is running + // processId is used as identifier to start/resume process + const newProcessId = uuidv4(); + setProcessId(newProcessId); + + onStartNewProcess?.(newProcessId) + .then((result) => { + if (result) { + setMessages((prevMessages) => [...prevMessages]); + } + }) + .finally(() => { + setStartingNewProcess(false); + }); + + setMessages((prevMessages) => [ + ...prevMessages, + { + sender: ChatUser.USER, + action: `New process started - ${newProcessId}`, + timestamp: new Date().toLocaleString(), + }, + ]); + setStartingNewProcess(true); + }; + + const onSendTeacherQuestionClicked = () => { + if (!processId) { + alert("Process ID is not set. Please start a new process first."); + return; + } + if (!selectedTeacherQuestion) { + alert("Please select a teacher question first."); + return; + } + + onSendTeacherQuestion?.({ + processId, + user: TeacherStudentInteractionUser.TEACHER, + content: selectedTeacherQuestion, + }) + .then((result) => { + if (result) { + console.log("Successfully sent teacher question"); + } + }) + .catch((error) => { + console.error("Error sending teacher question:", error); + setMessages((prevMessages) => [ + ...prevMessages, + { + sender: ChatUser.ASSISTANT, + content: `Something went wrong and could not send question - ${error}`, + timestamp: new Date().toLocaleString(), + }, + ]); + }); + + setMessages((prevMessages) => [ + ...prevMessages, + { + sender: ChatUser.USER, + content: `User asked teacher question - ${selectedTeacherQuestion}`, + timestamp: new Date().toLocaleString(), + }, + ]); + }; + + const onClearChat = () => { + setMessages([]); + setProcessId(""); + }; + + return ( +
+ + +
+ + {/* {processId && ( */} + + setSelectedTeacherQuestion(data.optionValue) + } + > + {teacherMathQuestions.map((question) => ( + + ))} + + + +
+
+ ); +}; + +export default TeacherStudentInteractionChat; diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/main.tsx b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/main.tsx index b4b78c76abf2..4a73e004825e 100644 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/main.tsx +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/main.tsx @@ -3,7 +3,7 @@ * All rights reserved. */ import { createRoot } from "react-dom/client"; -import { grpcDocService } from "./services/grpc/DocumentGenerationGrpcClient.ts"; +import { grpcDocService } from "./services/grpc/grpcClients.ts"; import { FluentProvider, webLightTheme } from "@fluentui/react-components"; import "./index.css"; import App from "./App.tsx"; diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/services/grpc/DocumentGenerationGrpcClient.ts b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/services/grpc/DocumentGenerationGrpcClient.ts deleted file mode 100644 index 3ad067e2ef9f..000000000000 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/services/grpc/DocumentGenerationGrpcClient.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (c) 2025 Microsoft - * All rights reserved. - */ -import { GrpcWebFetchTransport } from "@protobuf-ts/grpcweb-transport"; -import { GrpcDocumentationGenerationClient } from "./gen/documentGeneration.client"; - -const createGrpcDocGenerationClient = () => { - try { - const transport = new GrpcWebFetchTransport({ - baseUrl: "http://localhost:58640", - format: "text", - }); - return new GrpcDocumentationGenerationClient(transport); - } catch (error) { - console.error("Could not create connection with gRPC server", error); - return undefined; - } -}; - -export const grpcDocService = createGrpcDocGenerationClient(); diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/services/grpc/gen/documentGeneration.client.ts b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/services/grpc/gen/documentGeneration.client.ts index accafe6f7a52..4831262d1262 100644 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/services/grpc/gen/documentGeneration.client.ts +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/services/grpc/gen/documentGeneration.client.ts @@ -1,5 +1,5 @@ // @generated by protobuf-ts 2.9.6 with parameter generate_dependencies -// @generated from protobuf file "documentGeneration.proto" (syntax proto3) +// @generated from protobuf file "documentGeneration.proto" (package "ProcessWithCloudEvents.Grpc.Contract", syntax proto3) // tslint:disable import type { RpcTransport } from "@protobuf-ts/runtime-rpc"; import type { ServiceInfo } from "@protobuf-ts/runtime-rpc"; @@ -14,36 +14,36 @@ import type { FeatureDocumentationRequest } from "./documentGeneration"; import type { UnaryCall } from "@protobuf-ts/runtime-rpc"; import type { RpcOptions } from "@protobuf-ts/runtime-rpc"; /** - * @generated from protobuf service GrpcDocumentationGeneration + * @generated from protobuf service ProcessWithCloudEvents.Grpc.Contract.GrpcDocumentationGeneration */ export interface IGrpcDocumentationGenerationClient { /** - * @generated from protobuf rpc: UserRequestFeatureDocumentation(FeatureDocumentationRequest) returns (ProcessData); + * @generated from protobuf rpc: UserRequestFeatureDocumentation(ProcessWithCloudEvents.Grpc.Contract.FeatureDocumentationRequest) returns (ProcessWithCloudEvents.Grpc.Contract.ProcessData); */ userRequestFeatureDocumentation(input: FeatureDocumentationRequest, options?: RpcOptions): UnaryCall; /** - * @generated from protobuf rpc: RequestUserReviewDocumentationFromProcess(DocumentationContentRequest) returns (Empty); + * @generated from protobuf rpc: RequestUserReviewDocumentationFromProcess(ProcessWithCloudEvents.Grpc.Contract.DocumentationContentRequest) returns (ProcessWithCloudEvents.Grpc.Contract.Empty); */ requestUserReviewDocumentationFromProcess(input: DocumentationContentRequest, options?: RpcOptions): UnaryCall; /** - * @generated from protobuf rpc: RequestUserReviewDocumentation(ProcessData) returns (stream DocumentationContentRequest); + * @generated from protobuf rpc: RequestUserReviewDocumentation(ProcessWithCloudEvents.Grpc.Contract.ProcessData) returns (stream ProcessWithCloudEvents.Grpc.Contract.DocumentationContentRequest); */ requestUserReviewDocumentation(input: ProcessData, options?: RpcOptions): ServerStreamingCall; /** - * @generated from protobuf rpc: UserReviewedDocumentation(DocumentationApprovalRequest) returns (Empty); + * @generated from protobuf rpc: UserReviewedDocumentation(ProcessWithCloudEvents.Grpc.Contract.DocumentationApprovalRequest) returns (ProcessWithCloudEvents.Grpc.Contract.Empty); */ userReviewedDocumentation(input: DocumentationApprovalRequest, options?: RpcOptions): UnaryCall; /** - * @generated from protobuf rpc: PublishDocumentation(DocumentationContentRequest) returns (Empty); + * @generated from protobuf rpc: PublishDocumentation(ProcessWithCloudEvents.Grpc.Contract.DocumentationContentRequest) returns (ProcessWithCloudEvents.Grpc.Contract.Empty); */ publishDocumentation(input: DocumentationContentRequest, options?: RpcOptions): UnaryCall; /** - * @generated from protobuf rpc: ReceivePublishedDocumentation(ProcessData) returns (stream DocumentationContentRequest); + * @generated from protobuf rpc: ReceivePublishedDocumentation(ProcessWithCloudEvents.Grpc.Contract.ProcessData) returns (stream ProcessWithCloudEvents.Grpc.Contract.DocumentationContentRequest); */ receivePublishedDocumentation(input: ProcessData, options?: RpcOptions): ServerStreamingCall; } /** - * @generated from protobuf service GrpcDocumentationGeneration + * @generated from protobuf service ProcessWithCloudEvents.Grpc.Contract.GrpcDocumentationGeneration */ export class GrpcDocumentationGenerationClient implements IGrpcDocumentationGenerationClient, ServiceInfo { typeName = GrpcDocumentationGeneration.typeName; @@ -52,42 +52,42 @@ export class GrpcDocumentationGenerationClient implements IGrpcDocumentationGene constructor(private readonly _transport: RpcTransport) { } /** - * @generated from protobuf rpc: UserRequestFeatureDocumentation(FeatureDocumentationRequest) returns (ProcessData); + * @generated from protobuf rpc: UserRequestFeatureDocumentation(ProcessWithCloudEvents.Grpc.Contract.FeatureDocumentationRequest) returns (ProcessWithCloudEvents.Grpc.Contract.ProcessData); */ userRequestFeatureDocumentation(input: FeatureDocumentationRequest, options?: RpcOptions): UnaryCall { const method = this.methods[0], opt = this._transport.mergeOptions(options); return stackIntercept("unary", this._transport, method, opt, input); } /** - * @generated from protobuf rpc: RequestUserReviewDocumentationFromProcess(DocumentationContentRequest) returns (Empty); + * @generated from protobuf rpc: RequestUserReviewDocumentationFromProcess(ProcessWithCloudEvents.Grpc.Contract.DocumentationContentRequest) returns (ProcessWithCloudEvents.Grpc.Contract.Empty); */ requestUserReviewDocumentationFromProcess(input: DocumentationContentRequest, options?: RpcOptions): UnaryCall { const method = this.methods[1], opt = this._transport.mergeOptions(options); return stackIntercept("unary", this._transport, method, opt, input); } /** - * @generated from protobuf rpc: RequestUserReviewDocumentation(ProcessData) returns (stream DocumentationContentRequest); + * @generated from protobuf rpc: RequestUserReviewDocumentation(ProcessWithCloudEvents.Grpc.Contract.ProcessData) returns (stream ProcessWithCloudEvents.Grpc.Contract.DocumentationContentRequest); */ requestUserReviewDocumentation(input: ProcessData, options?: RpcOptions): ServerStreamingCall { const method = this.methods[2], opt = this._transport.mergeOptions(options); return stackIntercept("serverStreaming", this._transport, method, opt, input); } /** - * @generated from protobuf rpc: UserReviewedDocumentation(DocumentationApprovalRequest) returns (Empty); + * @generated from protobuf rpc: UserReviewedDocumentation(ProcessWithCloudEvents.Grpc.Contract.DocumentationApprovalRequest) returns (ProcessWithCloudEvents.Grpc.Contract.Empty); */ userReviewedDocumentation(input: DocumentationApprovalRequest, options?: RpcOptions): UnaryCall { const method = this.methods[3], opt = this._transport.mergeOptions(options); return stackIntercept("unary", this._transport, method, opt, input); } /** - * @generated from protobuf rpc: PublishDocumentation(DocumentationContentRequest) returns (Empty); + * @generated from protobuf rpc: PublishDocumentation(ProcessWithCloudEvents.Grpc.Contract.DocumentationContentRequest) returns (ProcessWithCloudEvents.Grpc.Contract.Empty); */ publishDocumentation(input: DocumentationContentRequest, options?: RpcOptions): UnaryCall { const method = this.methods[4], opt = this._transport.mergeOptions(options); return stackIntercept("unary", this._transport, method, opt, input); } /** - * @generated from protobuf rpc: ReceivePublishedDocumentation(ProcessData) returns (stream DocumentationContentRequest); + * @generated from protobuf rpc: ReceivePublishedDocumentation(ProcessWithCloudEvents.Grpc.Contract.ProcessData) returns (stream ProcessWithCloudEvents.Grpc.Contract.DocumentationContentRequest); */ receivePublishedDocumentation(input: ProcessData, options?: RpcOptions): ServerStreamingCall { const method = this.methods[5], opt = this._transport.mergeOptions(options); diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/services/grpc/gen/documentGeneration.ts b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/services/grpc/gen/documentGeneration.ts index 754f9cfa6487..3ecdd396dea5 100644 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/services/grpc/gen/documentGeneration.ts +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/services/grpc/gen/documentGeneration.ts @@ -1,5 +1,5 @@ // @generated by protobuf-ts 2.9.6 with parameter generate_dependencies -// @generated from protobuf file "documentGeneration.proto" (syntax proto3) +// @generated from protobuf file "documentGeneration.proto" (package "ProcessWithCloudEvents.Grpc.Contract", syntax proto3) // tslint:disable import { ServiceType } from "@protobuf-ts/runtime-rpc"; import type { BinaryWriteOptions } from "@protobuf-ts/runtime"; @@ -12,7 +12,7 @@ import type { PartialMessage } from "@protobuf-ts/runtime"; import { reflectionMergePartial } from "@protobuf-ts/runtime"; import { MessageType } from "@protobuf-ts/runtime"; /** - * @generated from protobuf message FeatureDocumentationRequest + * @generated from protobuf message ProcessWithCloudEvents.Grpc.Contract.FeatureDocumentationRequest */ export interface FeatureDocumentationRequest { /** @@ -33,7 +33,7 @@ export interface FeatureDocumentationRequest { processId: string; } /** - * @generated from protobuf message DocumentationContentRequest + * @generated from protobuf message ProcessWithCloudEvents.Grpc.Contract.DocumentationContentRequest */ export interface DocumentationContentRequest { /** @@ -49,12 +49,12 @@ export interface DocumentationContentRequest { */ assistantMessage: string; /** - * @generated from protobuf field: ProcessData processData = 10; + * @generated from protobuf field: ProcessWithCloudEvents.Grpc.Contract.ProcessData processData = 10; */ processData?: ProcessData; } /** - * @generated from protobuf message DocumentationApprovalRequest + * @generated from protobuf message ProcessWithCloudEvents.Grpc.Contract.DocumentationApprovalRequest */ export interface DocumentationApprovalRequest { /** @@ -66,12 +66,12 @@ export interface DocumentationApprovalRequest { */ reason: string; /** - * @generated from protobuf field: ProcessData processData = 10; + * @generated from protobuf field: ProcessWithCloudEvents.Grpc.Contract.ProcessData processData = 10; */ processData?: ProcessData; } /** - * @generated from protobuf message ProcessData + * @generated from protobuf message ProcessWithCloudEvents.Grpc.Contract.ProcessData */ export interface ProcessData { /** @@ -80,14 +80,14 @@ export interface ProcessData { processId: string; } /** - * @generated from protobuf message Empty + * @generated from protobuf message ProcessWithCloudEvents.Grpc.Contract.Empty */ export interface Empty { } // @generated message type with reflection information, may provide speed optimized methods class FeatureDocumentationRequest$Type extends MessageType { constructor() { - super("FeatureDocumentationRequest", [ + super("ProcessWithCloudEvents.Grpc.Contract.FeatureDocumentationRequest", [ { no: 1, name: "title", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, { no: 2, name: "userDescription", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, { no: 3, name: "content", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, @@ -152,13 +152,13 @@ class FeatureDocumentationRequest$Type extends MessageType { constructor() { - super("DocumentationContentRequest", [ + super("ProcessWithCloudEvents.Grpc.Contract.DocumentationContentRequest", [ { no: 1, name: "title", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, { no: 2, name: "content", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, { no: 3, name: "assistantMessage", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, @@ -188,7 +188,7 @@ class DocumentationContentRequest$Type extends MessageType { constructor() { - super("DocumentationApprovalRequest", [ + super("ProcessWithCloudEvents.Grpc.Contract.DocumentationApprovalRequest", [ { no: 1, name: "documentationApproved", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }, { no: 2, name: "reason", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, { no: 10, name: "processData", kind: "message", T: () => ProcessData } @@ -253,7 +253,7 @@ class DocumentationApprovalRequest$Type extends MessageType { constructor() { - super("ProcessData", [ + super("ProcessWithCloudEvents.Grpc.Contract.ProcessData", [ { no: 1, name: "processId", kind: "scalar", T: 9 /*ScalarType.STRING*/ } ]); } @@ -331,13 +331,13 @@ class ProcessData$Type extends MessageType { } } /** - * @generated MessageType for protobuf message ProcessData + * @generated MessageType for protobuf message ProcessWithCloudEvents.Grpc.Contract.ProcessData */ export const ProcessData = new ProcessData$Type(); // @generated message type with reflection information, may provide speed optimized methods class Empty$Type extends MessageType { constructor() { - super("Empty", []); + super("ProcessWithCloudEvents.Grpc.Contract.Empty", []); } create(value?: PartialMessage): Empty { const message = globalThis.Object.create((this.messagePrototype!)); @@ -369,13 +369,13 @@ class Empty$Type extends MessageType { } } /** - * @generated MessageType for protobuf message Empty + * @generated MessageType for protobuf message ProcessWithCloudEvents.Grpc.Contract.Empty */ export const Empty = new Empty$Type(); /** - * @generated ServiceType for protobuf service GrpcDocumentationGeneration + * @generated ServiceType for protobuf service ProcessWithCloudEvents.Grpc.Contract.GrpcDocumentationGeneration */ -export const GrpcDocumentationGeneration = new ServiceType("GrpcDocumentationGeneration", [ +export const GrpcDocumentationGeneration = new ServiceType("ProcessWithCloudEvents.Grpc.Contract.GrpcDocumentationGeneration", [ { name: "UserRequestFeatureDocumentation", options: {}, I: FeatureDocumentationRequest, O: ProcessData }, { name: "RequestUserReviewDocumentationFromProcess", options: {}, I: DocumentationContentRequest, O: Empty }, { name: "RequestUserReviewDocumentation", serverStreaming: true, options: {}, I: ProcessData, O: DocumentationContentRequest }, diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/services/grpc/gen/teacherStudentInteraction.client.ts b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/services/grpc/gen/teacherStudentInteraction.client.ts new file mode 100644 index 000000000000..727c3bff03bd --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/services/grpc/gen/teacherStudentInteraction.client.ts @@ -0,0 +1,71 @@ +// @generated by protobuf-ts 2.9.6 with parameter generate_dependencies +// @generated from protobuf file "teacherStudentInteraction.proto" (package "ProcessWithCloudEvents.Grpc.Contract", syntax proto3) +// tslint:disable +import type { RpcTransport } from "@protobuf-ts/runtime-rpc"; +import type { ServiceInfo } from "@protobuf-ts/runtime-rpc"; +import { GrpcTeacherStudentInteraction } from "./teacherStudentInteraction"; +import type { ServerStreamingCall } from "@protobuf-ts/runtime-rpc"; +import type { MessageContent } from "./teacherStudentInteraction"; +import { stackIntercept } from "@protobuf-ts/runtime-rpc"; +import type { ProcessDetails } from "./teacherStudentInteraction"; +import type { UnaryCall } from "@protobuf-ts/runtime-rpc"; +import type { RpcOptions } from "@protobuf-ts/runtime-rpc"; +/** + * @generated from protobuf service ProcessWithCloudEvents.Grpc.Contract.GrpcTeacherStudentInteraction + */ +export interface IGrpcTeacherStudentInteractionClient { + /** + * @generated from protobuf rpc: StartProcess(ProcessWithCloudEvents.Grpc.Contract.ProcessDetails) returns (ProcessWithCloudEvents.Grpc.Contract.ProcessDetails); + */ + startProcess(input: ProcessDetails, options?: RpcOptions): UnaryCall; + /** + * @generated from protobuf rpc: RequestStudentAgentResponse(ProcessWithCloudEvents.Grpc.Contract.MessageContent) returns (ProcessWithCloudEvents.Grpc.Contract.MessageContent); + */ + requestStudentAgentResponse(input: MessageContent, options?: RpcOptions): UnaryCall; + /** + * @generated from protobuf rpc: ReceiveStudentAgentResponse(ProcessWithCloudEvents.Grpc.Contract.ProcessDetails) returns (stream ProcessWithCloudEvents.Grpc.Contract.MessageContent); + */ + receiveStudentAgentResponse(input: ProcessDetails, options?: RpcOptions): ServerStreamingCall; + /** + * @generated from protobuf rpc: PublishStudentAgentResponseFromProcess(ProcessWithCloudEvents.Grpc.Contract.MessageContent) returns (ProcessWithCloudEvents.Grpc.Contract.MessageContent); + */ + publishStudentAgentResponseFromProcess(input: MessageContent, options?: RpcOptions): UnaryCall; +} +/** + * @generated from protobuf service ProcessWithCloudEvents.Grpc.Contract.GrpcTeacherStudentInteraction + */ +export class GrpcTeacherStudentInteractionClient implements IGrpcTeacherStudentInteractionClient, ServiceInfo { + typeName = GrpcTeacherStudentInteraction.typeName; + methods = GrpcTeacherStudentInteraction.methods; + options = GrpcTeacherStudentInteraction.options; + constructor(private readonly _transport: RpcTransport) { + } + /** + * @generated from protobuf rpc: StartProcess(ProcessWithCloudEvents.Grpc.Contract.ProcessDetails) returns (ProcessWithCloudEvents.Grpc.Contract.ProcessDetails); + */ + startProcess(input: ProcessDetails, options?: RpcOptions): UnaryCall { + const method = this.methods[0], opt = this._transport.mergeOptions(options); + return stackIntercept("unary", this._transport, method, opt, input); + } + /** + * @generated from protobuf rpc: RequestStudentAgentResponse(ProcessWithCloudEvents.Grpc.Contract.MessageContent) returns (ProcessWithCloudEvents.Grpc.Contract.MessageContent); + */ + requestStudentAgentResponse(input: MessageContent, options?: RpcOptions): UnaryCall { + const method = this.methods[1], opt = this._transport.mergeOptions(options); + return stackIntercept("unary", this._transport, method, opt, input); + } + /** + * @generated from protobuf rpc: ReceiveStudentAgentResponse(ProcessWithCloudEvents.Grpc.Contract.ProcessDetails) returns (stream ProcessWithCloudEvents.Grpc.Contract.MessageContent); + */ + receiveStudentAgentResponse(input: ProcessDetails, options?: RpcOptions): ServerStreamingCall { + const method = this.methods[2], opt = this._transport.mergeOptions(options); + return stackIntercept("serverStreaming", this._transport, method, opt, input); + } + /** + * @generated from protobuf rpc: PublishStudentAgentResponseFromProcess(ProcessWithCloudEvents.Grpc.Contract.MessageContent) returns (ProcessWithCloudEvents.Grpc.Contract.MessageContent); + */ + publishStudentAgentResponseFromProcess(input: MessageContent, options?: RpcOptions): UnaryCall { + const method = this.methods[3], opt = this._transport.mergeOptions(options); + return stackIntercept("unary", this._transport, method, opt, input); + } +} diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/services/grpc/gen/teacherStudentInteraction.ts b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/services/grpc/gen/teacherStudentInteraction.ts new file mode 100644 index 000000000000..7f0cef52ec96 --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/services/grpc/gen/teacherStudentInteraction.ts @@ -0,0 +1,171 @@ +// @generated by protobuf-ts 2.9.6 with parameter generate_dependencies +// @generated from protobuf file "teacherStudentInteraction.proto" (package "ProcessWithCloudEvents.Grpc.Contract", syntax proto3) +// tslint:disable +import { ServiceType } from "@protobuf-ts/runtime-rpc"; +import type { BinaryWriteOptions } from "@protobuf-ts/runtime"; +import type { IBinaryWriter } from "@protobuf-ts/runtime"; +import { WireType } from "@protobuf-ts/runtime"; +import type { BinaryReadOptions } from "@protobuf-ts/runtime"; +import type { IBinaryReader } from "@protobuf-ts/runtime"; +import { UnknownFieldHandler } from "@protobuf-ts/runtime"; +import type { PartialMessage } from "@protobuf-ts/runtime"; +import { reflectionMergePartial } from "@protobuf-ts/runtime"; +import { MessageType } from "@protobuf-ts/runtime"; +/** + * @generated from protobuf message ProcessWithCloudEvents.Grpc.Contract.MessageContent + */ +export interface MessageContent { + /** + * @generated from protobuf field: ProcessWithCloudEvents.Grpc.Contract.User user = 1; + */ + user: User; + /** + * @generated from protobuf field: string content = 2; + */ + content: string; + /** + * @generated from protobuf field: string processId = 10; + */ + processId: string; +} +/** + * @generated from protobuf message ProcessWithCloudEvents.Grpc.Contract.ProcessDetails + */ +export interface ProcessDetails { + /** + * @generated from protobuf field: string processId = 1; + */ + processId: string; +} +/** + * @generated from protobuf enum ProcessWithCloudEvents.Grpc.Contract.User + */ +export enum User { + /** + * @generated from protobuf enum value: STUDENT = 0; + */ + STUDENT = 0, + /** + * @generated from protobuf enum value: TEACHER = 1; + */ + TEACHER = 1 +} +// @generated message type with reflection information, may provide speed optimized methods +class MessageContent$Type extends MessageType { + constructor() { + super("ProcessWithCloudEvents.Grpc.Contract.MessageContent", [ + { no: 1, name: "user", kind: "enum", T: () => ["ProcessWithCloudEvents.Grpc.Contract.User", User] }, + { no: 2, name: "content", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 10, name: "processId", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): MessageContent { + const message = globalThis.Object.create((this.messagePrototype!)); + message.user = 0; + message.content = ""; + message.processId = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: MessageContent): MessageContent { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* ProcessWithCloudEvents.Grpc.Contract.User user */ 1: + message.user = reader.int32(); + break; + case /* string content */ 2: + message.content = reader.string(); + break; + case /* string processId */ 10: + message.processId = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: MessageContent, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* ProcessWithCloudEvents.Grpc.Contract.User user = 1; */ + if (message.user !== 0) + writer.tag(1, WireType.Varint).int32(message.user); + /* string content = 2; */ + if (message.content !== "") + writer.tag(2, WireType.LengthDelimited).string(message.content); + /* string processId = 10; */ + if (message.processId !== "") + writer.tag(10, WireType.LengthDelimited).string(message.processId); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message ProcessWithCloudEvents.Grpc.Contract.MessageContent + */ +export const MessageContent = new MessageContent$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class ProcessDetails$Type extends MessageType { + constructor() { + super("ProcessWithCloudEvents.Grpc.Contract.ProcessDetails", [ + { no: 1, name: "processId", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): ProcessDetails { + const message = globalThis.Object.create((this.messagePrototype!)); + message.processId = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: ProcessDetails): ProcessDetails { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* string processId */ 1: + message.processId = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: ProcessDetails, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* string processId = 1; */ + if (message.processId !== "") + writer.tag(1, WireType.LengthDelimited).string(message.processId); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message ProcessWithCloudEvents.Grpc.Contract.ProcessDetails + */ +export const ProcessDetails = new ProcessDetails$Type(); +/** + * @generated ServiceType for protobuf service ProcessWithCloudEvents.Grpc.Contract.GrpcTeacherStudentInteraction + */ +export const GrpcTeacherStudentInteraction = new ServiceType("ProcessWithCloudEvents.Grpc.Contract.GrpcTeacherStudentInteraction", [ + { name: "StartProcess", options: {}, I: ProcessDetails, O: ProcessDetails }, + { name: "RequestStudentAgentResponse", options: {}, I: MessageContent, O: MessageContent }, + { name: "ReceiveStudentAgentResponse", serverStreaming: true, options: {}, I: ProcessDetails, O: MessageContent }, + { name: "PublishStudentAgentResponseFromProcess", options: {}, I: MessageContent, O: MessageContent } +]); diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/services/grpc/grpcClients.ts b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/services/grpc/grpcClients.ts new file mode 100644 index 000000000000..f8b98ba2ee41 --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/services/grpc/grpcClients.ts @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2025 Microsoft + * All rights reserved. + */ +import { GrpcWebFetchTransport } from "@protobuf-ts/grpcweb-transport"; +import { GrpcDocumentationGenerationClient } from "./gen/documentGeneration.client"; +import { GrpcTeacherStudentInteractionClient } from "./gen/teacherStudentInteraction.client"; + +// For this setup, both clients are using the same server URL - just different grpc services. +// In a real-world scenario, you might have different URLs for different services. +// This is just a placeholder URL. You should replace it with the actual URL of your gRPC server. +const SERVER_URL = "http://localhost:5640"; + +const getGrpcWebTransport = () => { + return new GrpcWebFetchTransport({ + baseUrl: SERVER_URL, + format: "text", + }); +}; + +const createGrpcDocGenerationClient = () => { + try { + const transport = getGrpcWebTransport(); + return new GrpcDocumentationGenerationClient(transport); + } catch (error) { + console.error( + "Could not create connection with gRPC server - document generation", + error + ); + return undefined; + } +}; + +export const grpcDocService = createGrpcDocGenerationClient(); + +const createGrpcTeacherStundentInteractionClient = () => { + try { + const transport = getGrpcWebTransport(); + return new GrpcTeacherStudentInteractionClient(transport); + } catch (error) { + console.error( + "Could not create connection with gRPC server - Teacher Student Interaction", + error + ); + return undefined; + } +}; + +export const grpcTeacherStudentService = + createGrpcTeacherStundentInteractionClient(); diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/services/grpc/proto/documentGeneration.proto b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/services/grpc/proto/documentGeneration.proto index 2bd2800daa08..0d213fd70a4a 100644 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/services/grpc/proto/documentGeneration.proto +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/services/grpc/proto/documentGeneration.proto @@ -1,6 +1,7 @@ syntax = "proto3"; -option csharp_namespace = "ProcessWithCloudEvents.Grpc.DocumentationGenerator"; +package ProcessWithCloudEvents.Grpc.Contract; +option csharp_namespace = "ProcessWithCloudEvents.Grpc.Contract"; service GrpcDocumentationGeneration { rpc UserRequestFeatureDocumentation (FeatureDocumentationRequest) returns (ProcessData); diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/services/grpc/proto/teacherStudentInteraction.proto b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/services/grpc/proto/teacherStudentInteraction.proto new file mode 100644 index 000000000000..4a864b38caad --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Client/src/services/grpc/proto/teacherStudentInteraction.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +package ProcessWithCloudEvents.Grpc.Contract; +option csharp_namespace = "ProcessWithCloudEvents.Grpc.Contract"; + +service GrpcTeacherStudentInteraction { + rpc StartProcess (ProcessDetails) returns (ProcessDetails); + rpc RequestStudentAgentResponse (MessageContent) returns (MessageContent); + rpc ReceiveStudentAgentResponse (ProcessDetails) returns (stream MessageContent); + rpc PublishStudentAgentResponseFromProcess (MessageContent) returns (MessageContent); +} + +enum User { + STUDENT = 0; + TEACHER = 1; +} + +message MessageContent { + User user = 1; + string content = 2; + string processId = 10; +} + +message ProcessDetails { + string processId = 1; +} \ No newline at end of file diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc/ProcessWithCloudEvents.Grpc.csproj b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc.LocalRuntime/ProcessWithCloudEvents.Grpc.LocalRuntime.csproj similarity index 66% rename from dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc/ProcessWithCloudEvents.Grpc.csproj rename to dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc.LocalRuntime/ProcessWithCloudEvents.Grpc.LocalRuntime.csproj index c8e87e203414..74fd15969591 100644 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc/ProcessWithCloudEvents.Grpc.csproj +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc.LocalRuntime/ProcessWithCloudEvents.Grpc.LocalRuntime.csproj @@ -9,22 +9,25 @@ 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0 - + - + + + + + + - + - - @@ -34,10 +37,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc.LocalRuntime/Program.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc.LocalRuntime/Program.cs new file mode 100644 index 000000000000..5f4ac1d2c603 --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc.LocalRuntime/Program.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ClientModel; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.OpenAI; +using OpenAI; +using ProcessWithCloudEvents.Grpc.Clients; +using ProcessWithCloudEvents.Grpc.Extensions; +using ProcessWithCloudEvents.Grpc.LocalRuntime.Services; +using ProcessWithCloudEvents.SharedComponents.Options; +using ProcessWithCloudEvents.SharedComponents.Storage; + +var builder = WebApplication.CreateBuilder(args); + +var config = new ConfigurationBuilder() + .AddUserSecrets() + .AddEnvironmentVariables() + .Build(); + +// Configure logging +builder.Services.AddLogging((logging) => +{ + logging.AddConsole(); + logging.AddDebug(); +}); + +var openAIOptions = config.GetValid(OpenAIOptions.SectionName); +var cosmosDbOptions = config.GetValid(CosmosDBOptions.SectionName); + +// Configure the Kernel with DI. This is required for dependency injection to work with processes. +builder.Services.AddKernel(); +builder.Services.AddOpenAIChatCompletion(openAIOptions.ChatModelId, openAIOptions.ApiKey); + +// Setup for using Agent Steps in SK Process +var openAIClient = OpenAIAssistantAgent.CreateOpenAIClient(new ApiKeyCredential(openAIOptions.ApiKey)); +builder.Services.AddSingleton(openAIClient); +builder.Services.AddTransient(); + +// Grpc setup +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +// Since we have multiple grpc clients, we need to register them as keyed singletons so then we can access them respetively in each service by key +builder.Services.AddKeyedSingleton(DocumentGenerationGrpcClient.Key, (sp, key) => +{ + return new DocumentGenerationGrpcClient(); +}); +builder.Services.AddKeyedSingleton(TeacherStudentInteractionGrpcClient.Key, (sp, key) => +{ + return new TeacherStudentInteractionGrpcClient(); +}); + +// Registering storage used for persisting process state with Local Runtime +string tempDirectoryPath = Path.Combine(Path.GetTempPath(), "MySKProcessStorage"); +var storageInstance = new JsonFileStorage(tempDirectoryPath); + +//var cloudStorageInstance = new CosmosDbProcessStorageConnector( +// cosmosDbOptions.ConnectionString, cosmosDbOptions.DatabaseName, cosmosDbOptions.ContainerName +//); + +builder.Services.AddSingleton(storageInstance); +//builder.Services.AddSingleton(cloudStorageInstance); + +// Enabling CORS for grpc-web when using webApp as client, remove if not needed +builder.Services.AddCors(o => o.AddPolicy("AllowAll", builder => +{ + builder.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); +})); + +// Add grpc related services. +builder.Services.AddGrpc(); +builder.Services.AddGrpcReflection(); + +var app = builder.Build(); + +app.UseCors(); + +// Enabling CORS for grpc-web, remove if not needed +app.MapGrpcReflectionService().RequireCors("AllowAll"); +app.MapGrpcService().EnableGrpcWeb().RequireCors("AllowAll"); +app.MapGrpcService().EnableGrpcWeb().RequireCors("AllowAll"); + +// Grpc services mapping +// Enabling grpc-web, remove if not needed +app.UseGrpcWeb(); + +app.Run(); diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc/Services/DocumentGenerationService.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc.LocalRuntime/Services/DocumentGenerationService.cs similarity index 85% rename from dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc/Services/DocumentGenerationService.cs rename to dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc.LocalRuntime/Services/DocumentGenerationService.cs index 75fb235784c1..165de9c5569f 100644 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc/Services/DocumentGenerationService.cs +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc.LocalRuntime/Services/DocumentGenerationService.cs @@ -1,16 +1,15 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Concurrent; -using Dapr.Actors.Client; using Grpc.Core; using Microsoft.SemanticKernel; using Microsoft.VisualStudio.Threading; using ProcessWithCloudEvents.Grpc.Clients; -using ProcessWithCloudEvents.Grpc.DocumentationGenerator; +using ProcessWithCloudEvents.Grpc.Contract; using ProcessWithCloudEvents.Processes; using ProcessWithCloudEvents.Processes.Models; -namespace ProcessWithCloudEvents.Grpc.Services; +namespace ProcessWithCloudEvents.Grpc.LocalRuntime.Services; /// /// This gRPC service handles the generation of documents using/invoking a SK Process @@ -19,7 +18,11 @@ public class DocumentGenerationService : GrpcDocumentationGeneration.GrpcDocumen { private readonly ILogger _logger; private readonly Kernel _kernel; - private readonly IActorProxyFactory _actorProxyFactory; + + private readonly KernelProcess _kernelProcess; + private readonly IExternalKernelProcessMessageChannel _externalMessageChannel; + private readonly IProcessStorageConnector _storageConnector; + private readonly ConcurrentDictionary>> _docReviewSubscribers; private readonly ConcurrentDictionary>> _publishDocumentSubscribers; /// @@ -27,14 +30,23 @@ public class DocumentGenerationService : GrpcDocumentationGeneration.GrpcDocumen /// /// /// - /// - public DocumentGenerationService(ILogger logger, Kernel kernel, IActorProxyFactory actorProxy) + /// + /// + public DocumentGenerationService( + ILogger logger, + Kernel kernel, + [FromKeyedServices("DocumentGenerationGrpcClient")] IExternalKernelProcessMessageChannel externalMessageChannel, + IProcessStorageConnector storageConnector) { this._logger = logger; this._kernel = kernel; - this._actorProxyFactory = actorProxy; this._docReviewSubscribers = new(); this._publishDocumentSubscribers = new(); + + this._kernelProcess = DocumentGenerationProcess.CreateProcessBuilder().Build(); + + this._externalMessageChannel = externalMessageChannel; + this._storageConnector = storageConnector; } /// @@ -48,16 +60,13 @@ public DocumentGenerationService(ILogger logger, Kern public override async Task UserRequestFeatureDocumentation(FeatureDocumentationRequest request, ServerCallContext context) { var processId = string.IsNullOrEmpty(request.ProcessId) ? Guid.NewGuid().ToString() : request.ProcessId; - var process = DocumentGenerationProcess.CreateProcessBuilder().Build(); - var processContext = await process.StartAsync(new KernelProcessEvent() + var processContext = await this._kernelProcess.StartAsync(this._kernel, new KernelProcessEvent() { Id = DocumentGenerationProcess.DocGenerationEvents.StartDocumentGeneration, // The object ProductInfo is sent because this is the type the GatherProductInfoStep is expecting Data = new ProductInfo() { Title = request.Title, Content = request.Content, UserInput = request.UserDescription }, - }, - processId, - this._actorProxyFactory); + }, processId, this._externalMessageChannel, this._storageConnector); return new ProcessData { ProcessId = processId }; } @@ -119,8 +128,6 @@ public override async Task RequestUserReviewDocumentation(ProcessData request, I /// public override async Task UserReviewedDocumentation(DocumentationApprovalRequest request, ServerCallContext context) { - var process = DocumentGenerationProcess.CreateProcessBuilder().Build(); - KernelProcessEvent processEvent; if (request.DocumentationApproved) { @@ -139,7 +146,8 @@ public override async Task UserReviewedDocumentation(DocumentationApprova }; } - var processContext = await process.StartAsync(processEvent, request.ProcessData.ProcessId); + var processId = request.ProcessData.ProcessId; + var processContext = await this._kernelProcess.StartAsync(this._kernel, processEvent, processId, this._externalMessageChannel, this._storageConnector); return new Empty(); } diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc.LocalRuntime/Services/TeacherStudentInteractionService.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc.LocalRuntime/Services/TeacherStudentInteractionService.cs new file mode 100644 index 000000000000..68dbd528b2c7 --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc.LocalRuntime/Services/TeacherStudentInteractionService.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Concurrent; +using Grpc.Core; +using Microsoft.SemanticKernel; +using Microsoft.VisualStudio.Threading; +using ProcessWithCloudEvents.Grpc.Contract; +using ProcessWithCloudEvents.Processes; + +namespace ProcessWithCloudEvents.Grpc.LocalRuntime.Services; + +/// +/// This gRPC service handles a teacher student interaction using/invoking a SK Process +/// +public class TeacherStudentInteractionService : GrpcTeacherStudentInteraction.GrpcTeacherStudentInteractionBase +{ + private readonly ILogger _logger; + private readonly Kernel _kernel; + private readonly KernelProcess _kernelProcess; + + private readonly IExternalKernelProcessMessageChannel _externalMessageChannel; + private readonly IProcessStorageConnector _storageConnector; + + private readonly ConcurrentDictionary>> _studentMessagesSubscribers; + /// + /// Constructor for the + /// + /// + /// + /// + /// + public TeacherStudentInteractionService( + ILogger logger, + Kernel kernel, + [FromKeyedServices("TeacherStudentInteractionGrpcClient")] IExternalKernelProcessMessageChannel externalMessageChannel, + IProcessStorageConnector storageConnector) + { + this._logger = logger; + this._kernel = kernel; + + this._studentMessagesSubscribers = new(); + + this._kernelProcess = TeacherStudentProcess.CreateProcessBuilder().Build(); + + this._externalMessageChannel = externalMessageChannel; + this._storageConnector = storageConnector; + } + + public override async Task StartProcess(ProcessDetails request, ServerCallContext context) + { + var processId = string.IsNullOrEmpty(request.ProcessId) ? Guid.NewGuid().ToString() : request.ProcessId; + + var processContext = await this._kernelProcess.StartAsync(this._kernel, new KernelProcessEvent() + { + Id = TeacherStudentProcess.ProcessEvents.StartProcess, + Data = "Give me a welcome message with a brief summary of what you can do", + }, processId, this._externalMessageChannel, this._storageConnector); + + return new ProcessDetails { ProcessId = processId }; + } + + public override async Task ReceiveStudentAgentResponse(ProcessDetails request, IServerStreamWriter responseStream, ServerCallContext context) + { + var subscribers = this._studentMessagesSubscribers.GetOrAdd(request.ProcessId, []); + subscribers.Add(responseStream); + + try + { + // Wait until the client disconnects + await context.CancellationToken.WaitHandle.ToTask(); + } + finally + { + // Remove the subscriber when client disconnects +#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. + subscribers.TryTake(out responseStream); +#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. + } + } + + public override async Task PublishStudentAgentResponseFromProcess(MessageContent request, ServerCallContext context) + { + if (this._studentMessagesSubscribers.TryGetValue(request.ProcessId, out var subscribers)) + { + foreach (var subscriber in subscribers) + { + await subscriber.WriteAsync(request).ConfigureAwait(false); + } + } + + return request; + } + + public override async Task RequestStudentAgentResponse(MessageContent request, ServerCallContext context) + { + var process = TeacherStudentProcess.CreateProcessBuilder().Build(); + var processId = request.ProcessId; + + var processContext = await this._kernelProcess.StartAsync(this._kernel, new KernelProcessEvent() + { + Id = TeacherStudentProcess.ProcessEvents.TeacherAskedQuestion, + Data = request.Content, + }, processId, this._externalMessageChannel, this._storageConnector); + + return request; + } +} diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc/appsettings.json b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc.LocalRuntime/appsettings.json similarity index 81% rename from dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc/appsettings.json rename to dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc.LocalRuntime/appsettings.json index b42f98334964..d5baf5ba6a05 100644 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc/appsettings.json +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc.LocalRuntime/appsettings.json @@ -9,11 +9,11 @@ "Kestrel": { "Endpoints": { "Http1": { - "Url": "http://*:58640", + "Url": "http://*:5640", "Protocols": "Http1" }, "Http2": { - "Url": "http://*:58641", + "Url": "http://*:5641", "Protocols": "Http2" } } diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc/Program.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc/Program.cs deleted file mode 100644 index af71ba85206b..000000000000 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc/Program.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.SemanticKernel; -using ProcessWithCloudEvents.Grpc.Clients; -using ProcessWithCloudEvents.Grpc.Extensions; -using ProcessWithCloudEvents.Grpc.Options; -using ProcessWithCloudEvents.Grpc.Services; - -var builder = WebApplication.CreateBuilder(args); - -var config = new ConfigurationBuilder() - .AddUserSecrets() - .AddEnvironmentVariables() - .Build(); - -// Configure logging -builder.Services.AddLogging((logging) => -{ - logging.AddConsole(); - logging.AddDebug(); -}); - -var openAIOptions = config.GetValid(OpenAIOptions.SectionName); - -// Configure the Kernel with DI. This is required for dependency injection to work with processes. -builder.Services.AddKernel(); -builder.Services.AddOpenAIChatCompletion(openAIOptions.ChatModelId, openAIOptions.ApiKey); - -builder.Services.AddSingleton(); -// Injecting SK Process custom grpc client IExternalKernelProcessMessageChannel implementation -builder.Services.AddSingleton(); - -// Configure Dapr -builder.Services.AddActors(static options => -{ - // Register the actors required to run Processes - options.AddProcessActors(); -}); - -// Enabling CORS for grpc-web when using webApp as client, remove if not needed -builder.Services.AddCors(o => o.AddPolicy("AllowAll", builder => -{ - builder.AllowAnyOrigin() - .AllowAnyMethod() - .AllowAnyHeader(); -})); - -// Add grpc related services. -builder.Services.AddGrpc(); -builder.Services.AddGrpcReflection(); - -var app = builder.Build(); - -app.UseCors(); - -// Grpc services mapping -// Enabling grpc-web, remove if not needed -app.UseGrpcWeb(); -// Enabling CORS for grpc-web, remove if not needed -app.MapGrpcReflectionService().RequireCors("AllowAll"); -app.MapGrpcService().EnableGrpcWeb().RequireCors("AllowAll"); - -// Dapr actors related mapping -app.MapActorsHandlers(); -app.Run(); diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc/README.md b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc/README.md deleted file mode 100644 index 69be4757b865..000000000000 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc/README.md +++ /dev/null @@ -1,131 +0,0 @@ -# Process With Cloud Events - using gRPC - -For using gRPC, this demo follows the guidelines suggested for any [gRPC ASP.NET Core App](https://learn.microsoft.com/en-us/aspnet/core/grpc/test-tools?view=aspnetcore-9.0). - -Which for this demo means: - -- Making use of `builder.Services.AddGrpcReflection()` and `app.MapGrpcReflectionService()` -- Making use of [`gRPCui`](https://github.com/fullstorydev/grpcui) for testing - -## Explanation - -This demo showcases how SK Process Framework could interact with a gRPC Server and clients. - -The main difference of this demo is the custom implementation of the gRPC Server and client used internally by the SK Process in the SK Proxy Step. - -Main gRPC components: - -- `documentationGenerator.proto`: `\dotnet\samples\Demos\ProcessWithCloudEvents\ProcessWithCloudEvents.Grpc\Protos\documentationGenerator.proto` -- gRPC Server: `\dotnet\samples\Demos\ProcessWithCloudEvents\ProcessWithCloudEvents.Grpc\Services\DocumentGenerationService.cs` -- gRPC Client/IExternalKernelProcessMessageChannel implementation: `\dotnet\samples\Demos\ProcessWithCloudEvents\ProcessWithCloudEvents.Grpc\Clients\DocumentGenerationGrpcClient.cs` - -### SK Process and gRPC Events - -``` mermaid - -sequenceDiagram - participant grpcClient as gRPC Client - box Server - participant grpcServer as gRPC Server - participant SKP as SK Process - end - - grpcClient->>grpcServer: UserRequestFeatureDocumentation
gRPC - grpcServer->>SKP: StartDocumentGeneration
SK event - SKP->>grpcServer: RequestUserReview (SK Topic)/
RequestUserReviewDocumentationFromProcess (gRPC) - grpcServer->>grpcClient: RequestUserReviewDocumentation
gRPC - grpcClient->>grpcServer: UserReviewedDocumentation
gRPC - grpcServer->>SKP: UserApprovedDocument/UserRejectedDocument
SK event - SKP->>grpcServer: PublishDocumentation (SK Topic)/
PublishDocumentation (gRPC) - grpcServer->>grpcClient: ReceivePublishedDocumentation
gRPC -``` -1. When the `UserRequestFeatureDocumentation` gRPC request is received from the gRPC client, the server initiates an SK Process and emits the `StartDocumentGeneration` SK event. -2. The `RequestUserReview` topic is emitted when the `DocumentationApproved` event is triggered during the `ProofReadDocumentationStep`. This event invokes the `RequestUserReviewDocumentationFromProcess` gRPC method to communicate with the server. -3. The `RequestUserReviewDocumentationFromProcess` method updates the shared stream, which is used to communicate with the subscribers of `RequestUserReviewDocumentation`. The gRPC client then receives the document for review and approval. -4. The gRPC client can approve or reject the document using the `UserReviewedDocumentation` method to communicate with the server. The server then sends the `UserApprovedDocument` or `UserRejectedDocument` SK event to the SK Process. -5. The SK Process resumes, and the `PublishDocumentationStep` now has all the necessary parameters to execute. Upon execution, the `PublishDocumentation` topic is triggered, invoking the `PublishDocumentation` method on the gRPC server. -6. The PublishDocumentation method updates the shared stream used by `ReceivePublishedDocumentation`, ensuring that all subscribers receive the update of the latest published document -## Demo -### Requirements - -- Have Dapr setup ready -- Build and Run the app -- Interact with the server by: - - Install and run `gRPCui` listening to the address `localhost:58641`: - ``` - ./grpcui.exe -plaintext localhost:58641 - ``` - - or - - - Use the `ProcessWithCloudEvents.Client` App and use it to interact with the server. This app uses gRPC Web, which interacts with the server through `localhost:58640`. - -### Usage without UI - -For interacting with the gRPC server, the - -1. Build and run the app -2. Open 2 windows of `gRPCui` with the following methods: - - Window 1: - - Method name: `UserRequestFeatureDocumentation` and `UserReviewedDocumentation` - - Window 2: - - Method name: `RequestUserReviewDocumentation` - - Window 3: - - Method name: `ReceivePublishedDocumentation` - -3. Select a process id to be used with all methods. Example: processId = "100" -4. Execute different methods in the following order: - 1. `RequestUserReviewDocumentation` with Request Data: - ```json - { - "processId": "100" - } - ``` - This will subscribe to any request for review done for the specific process id and a response will be received when the process emits a notification. - Set timeout to 30 seconds. - - 2. `UserRequestFeatureDocumentation` with Request Data: - ```json - { - "title": "some product title", - "userDescription": "some user description", - "content": "some product content", - "processId": "100", - } - ``` - This request will kickstart the creation of a new process with the specific processId passing an initial event to the SK process. - -5. Once the `RequestUserReviewDocumentation` is received, execute the following methods: - 1. `ReceivePublishedDocumentation` with Request Data: - ```json - { - "processId": "100" - } - ``` - This will subscribe to any request for review done for the specific process id and a response will be received when the process emits a notification. - Set timeout to 30 seconds. - - 2. `UserReviewedDocumentation` with Request Data: - ```json - { - "documentationApproved": true, - "reason": "", - "processData": - { - "processId": "100" - } - } - ``` - -### Debugging - -For debugging and be able to set breakpoints in different stages of the app, you can: - -- Install the [Visual Studio Dapr Extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vs-dapr) and make use of it by making use of the `\dotnet\dapr.yaml` file already in the repository. - -or - -- Set the `ProcessWithCloudEvents.Grpc` as startup app, run and attach the Visual Studio debugger: -``` -dapr run --app-id processwithcloudevents-grpc --app-port 58640 --app-protocol http -- dotnet run --no-build -``` \ No newline at end of file diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/DocumentGenerationProcess.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/DocumentGenerationProcess.cs index 6f319947c07a..ee84093bde10 100644 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/DocumentGenerationProcess.cs +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/DocumentGenerationProcess.cs @@ -10,6 +10,11 @@ namespace ProcessWithCloudEvents.Processes; ///
public static class DocumentGenerationProcess { + /// + /// The key that the process will be registered with in the SK process runtime. + /// + public static string Key => nameof(DocumentGenerationProcess); + /// /// SK Process events emitted by /// @@ -72,10 +77,6 @@ public static ProcessBuilder CreateProcessBuilder(string processName = "Document .OnInputEvent(DocGenerationEvents.UserRejectedDocument) .SendEventTo(new(docsGenerationStep, functionName: GenerateDocumentationStep.ProcessFunctions.ApplySuggestions)); - processBuilder - .OnInputEvent(DocGenerationEvents.UserApprovedDocument) - .SendEventTo(new(docsPublishStep, parameterName: "userApproval")); - // Hooking up the rest of the process steps infoGatheringStep .OnFunctionResult() @@ -93,8 +94,22 @@ public static ProcessBuilder CreateProcessBuilder(string processName = "Document // Additionally, the generated document is emitted externally for user approval using the pre-configured proxyStep docsProofreadStep .OnEvent(ProofReadDocumentationStep.OutputEvents.DocumentationApproved) - .EmitExternalEvent(proxyStep, DocGenerationTopics.RequestUserReview) - .SendEventTo(new ProcessFunctionTargetBuilder(docsPublishStep, parameterName: "document")); + .EmitExternalEvent(proxyStep, DocGenerationTopics.RequestUserReview); + //.SendEventTo(new ProcessFunctionTargetBuilder(docsPublishStep, parameterName: "document")); + + processBuilder + .ListenFor() + .AllOf([ + new(messageType: ProofReadDocumentationStep.OutputEvents.DocumentationApproved, docsProofreadStep), + new(messageType: DocGenerationEvents.UserApprovedDocument, processBuilder), + ]) + .SendEventTo(new ProcessStepTargetBuilder(docsPublishStep, inputMapping: (inputEvents) => + { + return new() { + { "document", inputEvents[docsProofreadStep.GetFullEventId(ProofReadDocumentationStep.OutputEvents.DocumentationApproved)] }, + { "userApproval", inputEvents[processBuilder.GetFullEventId(DocGenerationEvents.UserApprovedDocument)] }, + }; + })); // When event is approved by user, it gets published externally too docsPublishStep diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/ProcessWithCloudEvents.Processes.csproj b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/ProcessWithCloudEvents.Processes.csproj index e312860af9a7..e9e66ba9f02c 100644 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/ProcessWithCloudEvents.Processes.csproj +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/ProcessWithCloudEvents.Processes.csproj @@ -10,6 +10,7 @@ + diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/TeacherStudentProcess.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/TeacherStudentProcess.cs new file mode 100644 index 000000000000..8b951fafe3e3 --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/TeacherStudentProcess.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents.OpenAI; + +namespace ProcessWithCloudEvents.Processes; + +/// +/// Components related to the SK Process for generating documentation +/// +public static class TeacherStudentProcess +{ + /// + /// The key that the process will be registered with in the SK process runtime. + /// + public static string Key => nameof(TeacherStudentProcess); + + /// + /// SK Process events emitted by + /// + public static class ProcessEvents + { + /// + /// Event to start the document generation process + /// + public const string StartProcess = nameof(StartProcess); + /// + /// Event emitted when the user rejects the document + /// + public const string TeacherAskedQuestion = nameof(TeacherAskedQuestion); + } + + /// + /// SK Process topics emitted by + /// Topics are used to emit events to external systems + /// + public static class InteractionTopics + { + /// + /// Event emitted when the agent has a response + /// + public const string AgentResponseMessage = nameof(AgentResponseMessage); + + /// + /// Event emitted when the agent has an error + /// + public const string AgentErrorMessage = nameof(AgentErrorMessage); + } + + /// + /// Creates a process builder for the Document Generation SK Process + /// + /// name of the SK Process + /// instance of + public static ProcessBuilder CreateProcessBuilder(string processName = "TeacherStudentProcess") + { + // Create the process builder + ProcessBuilder processBuilder = new(processName); + + // Add the steps + var studentAgentStep = processBuilder.AddStepFromAgent( + new() + { + Name = "Student", + // On purpose not assigning AgentId, if not provided a new agent is created + Description = "Solves problem given", + Instructions = "Solve the problem given, if the question is repeated answer the question with a bit of humor emphasizing that the question was asked but still answering the question", + Model = new() + { + Id = "gpt-4o", + }, + Type = OpenAIAssistantAgentFactory.OpenAIAssistantAgentType, + }); + + var proxyStep = processBuilder.AddProxyStep(id: "proxy", [InteractionTopics.AgentResponseMessage, InteractionTopics.AgentErrorMessage]); + + // Orchestrate the external input events + processBuilder + .OnInputEvent(ProcessEvents.StartProcess) + //.SendEventTo(new(studentAgentStep, parameterName: "message")); + .SentToAgentStep(studentAgentStep); + + processBuilder + .OnInputEvent(ProcessEvents.TeacherAskedQuestion) + //.SendEventTo(new(studentAgentStep, parameterName: "message")); + .SentToAgentStep(studentAgentStep); + + studentAgentStep + .OnFunctionResult() + .EmitExternalEvent(proxyStep, InteractionTopics.AgentResponseMessage); + + studentAgentStep + .OnFunctionError() + .EmitExternalEvent(proxyStep, InteractionTopics.AgentErrorMessage) + .StopProcess(); + + return processBuilder; + } +} diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc/Extensions/ConfigurationExtension.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/Extensions/ConfigurationExtension.cs similarity index 100% rename from dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc/Extensions/ConfigurationExtension.cs rename to dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/Extensions/ConfigurationExtension.cs diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/ExtensionsUtilities.props b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/ExtensionsUtilities.props new file mode 100644 index 000000000000..58e2c5971538 --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/ExtensionsUtilities.props @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc/Clients/DocumentGenerationGrpcClient.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/Grpc/Clients/DocumentGenerationGrpcClient.cs similarity index 95% rename from dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc/Clients/DocumentGenerationGrpcClient.cs rename to dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/Grpc/Clients/DocumentGenerationGrpcClient.cs index c87355c5f78d..2f0e3d9c1da0 100644 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc/Clients/DocumentGenerationGrpcClient.cs +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/Grpc/Clients/DocumentGenerationGrpcClient.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using Grpc.Net.Client; using Microsoft.SemanticKernel; -using ProcessWithCloudEvents.Grpc.DocumentationGenerator; +using ProcessWithCloudEvents.Grpc.Contract; using ProcessWithCloudEvents.Processes; using ProcessWithCloudEvents.Processes.Models; @@ -14,13 +14,15 @@ namespace ProcessWithCloudEvents.Grpc.Clients; ///
public class DocumentGenerationGrpcClient : IExternalKernelProcessMessageChannel { + public static string Key => nameof(DocumentGenerationGrpcClient); + private GrpcChannel? _grpcChannel; private GrpcDocumentationGeneration.GrpcDocumentationGenerationClient? _grpcClient; /// public async ValueTask Initialize() { - this._grpcChannel = GrpcChannel.ForAddress("http://localhost:58641"); + this._grpcChannel = GrpcChannel.ForAddress("http://localhost:5641"); this._grpcClient = new GrpcDocumentationGeneration.GrpcDocumentationGenerationClient(this._grpcChannel); } diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/Grpc/Clients/TeacherStudentInteractionGrpcClient.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/Grpc/Clients/TeacherStudentInteractionGrpcClient.cs new file mode 100644 index 000000000000..f392b79cdff6 --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/Grpc/Clients/TeacherStudentInteractionGrpcClient.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft. All rights reserved. +using Grpc.Net.Client; +using Microsoft.SemanticKernel; +using ProcessWithCloudEvents.Grpc.Contract; +using ProcessWithCloudEvents.Processes; + +namespace ProcessWithCloudEvents.Grpc.Clients; + +/// +/// Client that implements the interface used internally by the SK process +/// to emit events to external systems.
+/// This implementation is an example of a gRPC client that emits events to a gRPC server +///
+public class TeacherStudentInteractionGrpcClient : IExternalKernelProcessMessageChannel +{ + public static string Key => nameof(TeacherStudentInteractionGrpcClient); + + private GrpcChannel? _grpcChannel; + private GrpcTeacherStudentInteraction.GrpcTeacherStudentInteractionClient? _grpcClient; + + /// + public async ValueTask Initialize() + { + this._grpcChannel = GrpcChannel.ForAddress("http://localhost:5641"); + this._grpcClient = new GrpcTeacherStudentInteraction.GrpcTeacherStudentInteractionClient(this._grpcChannel); + } + + /// + public async ValueTask Uninitialize() + { + if (this._grpcChannel != null) + { + await this._grpcChannel.ShutdownAsync().ConfigureAwait(false); + } + } + + /// + public async Task EmitExternalEventAsync(string externalTopicEvent, KernelProcessProxyMessage message) + { + if (this._grpcClient != null && message.EventData != null) + { + switch (externalTopicEvent) + { + case TeacherStudentProcess.InteractionTopics.AgentResponseMessage: + var agentResponse = message.EventData.ToObject() as ChatMessageContent; + if (agentResponse != null) + { + await this._grpcClient.PublishStudentAgentResponseFromProcessAsync(new() + { + User = User.Student, + Content = agentResponse.Content, + ProcessId = message.ProcessId, + }); + } + return; + + case TeacherStudentProcess.InteractionTopics.AgentErrorMessage: + var agentErrorResponse = message.EventData.ToObject() as string; + if (agentErrorResponse != null) + { + await this._grpcClient.PublishStudentAgentResponseFromProcessAsync(new() + { + User = User.Student, + Content = $"ERROR: {agentErrorResponse}", + ProcessId = message.ProcessId, + }); + } + return; + } + } + } +} diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc/Protos/documentationGenerator.proto b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/Grpc/Proto/documentationGenerator.proto similarity index 90% rename from dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc/Protos/documentationGenerator.proto rename to dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/Grpc/Proto/documentationGenerator.proto index 2bd2800daa08..0d213fd70a4a 100644 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc/Protos/documentationGenerator.proto +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/Grpc/Proto/documentationGenerator.proto @@ -1,6 +1,7 @@ syntax = "proto3"; -option csharp_namespace = "ProcessWithCloudEvents.Grpc.DocumentationGenerator"; +package ProcessWithCloudEvents.Grpc.Contract; +option csharp_namespace = "ProcessWithCloudEvents.Grpc.Contract"; service GrpcDocumentationGeneration { rpc UserRequestFeatureDocumentation (FeatureDocumentationRequest) returns (ProcessData); diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/Grpc/Proto/teacherStudentInteraction.proto b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/Grpc/Proto/teacherStudentInteraction.proto new file mode 100644 index 000000000000..4a864b38caad --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/Grpc/Proto/teacherStudentInteraction.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +package ProcessWithCloudEvents.Grpc.Contract; +option csharp_namespace = "ProcessWithCloudEvents.Grpc.Contract"; + +service GrpcTeacherStudentInteraction { + rpc StartProcess (ProcessDetails) returns (ProcessDetails); + rpc RequestStudentAgentResponse (MessageContent) returns (MessageContent); + rpc ReceiveStudentAgentResponse (ProcessDetails) returns (stream MessageContent); + rpc PublishStudentAgentResponseFromProcess (MessageContent) returns (MessageContent); +} + +enum User { + STUDENT = 0; + TEACHER = 1; +} + +message MessageContent { + User user = 1; + string content = 2; + string processId = 10; +} + +message ProcessDetails { + string processId = 1; +} \ No newline at end of file diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/GrpcComponents.props b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/GrpcComponents.props new file mode 100644 index 000000000000..c013057b12ad --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/GrpcComponents.props @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/Options/CosmosDBOptions.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/Options/CosmosDBOptions.cs new file mode 100644 index 000000000000..15da381f4a85 --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/Options/CosmosDBOptions.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ComponentModel.DataAnnotations; + +namespace ProcessWithCloudEvents.SharedComponents.Options; + +/// +/// Configuration for Cosmos DB. +/// +public class CosmosDBOptions +{ + public const string SectionName = "CosmosDB"; + + [Required] + public string ConnectionString { get; set; } = string.Empty; + + [Required] + public string DatabaseName { get; set; } = string.Empty; + + [Required] + public string ContainerName { get; set; } = string.Empty; +} diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc/Options/OpenAIOptions.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/Options/OpenAIOptions.cs similarity index 87% rename from dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc/Options/OpenAIOptions.cs rename to dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/Options/OpenAIOptions.cs index c6ef102d8555..eabe78412334 100644 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc/Options/OpenAIOptions.cs +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/Options/OpenAIOptions.cs @@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations; -namespace ProcessWithCloudEvents.Grpc.Options; +namespace ProcessWithCloudEvents.SharedComponents.Options; /// /// Configuration for OpenAI chat completion service. diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/Storage/CosmosDbContainerStorageConnector.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/Storage/CosmosDbContainerStorageConnector.cs new file mode 100644 index 000000000000..88a6cd5a81c8 --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/Storage/CosmosDbContainerStorageConnector.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft. All rights reserved. +#pragma warning disable IDE0005 // Using directive is unnecessary +using System.Collections.Concurrent; +using System.ComponentModel; +using System.Text.Json; +using Microsoft.Azure.Cosmos; +using Microsoft.SemanticKernel; +using Newtonsoft.Json; +#pragma warning restore IDE0005 // Using directive is unnecessary + +namespace ProcessWithCloudEvents.SharedComponents.Storage; + +internal sealed class CosmosDbProcessStorageConnector : IProcessStorageConnector, IDisposable +{ + // CosmosDB V3 has a dependency on Newtonsoft.Json, so need to add wrapper class for Cosmos DB entities: + // https://brettmckenzie.net/posts/the-input-content-is-invalid-because-the-required-properties-id-are-missing/ + internal sealed record CosmosDbEntity + { + [JsonProperty("id")] + public string Id { get; init; } = string.Empty; + + [JsonProperty("body")] + public T Body { get; init; } = default!; + + [JsonProperty("instanceId")] + public string PartitionKey { get; init; } = string.Empty; + } + + private readonly CosmosClient _cosmosClient; + private readonly Microsoft.Azure.Cosmos.Container _container; + private readonly string _databaseId; + private readonly string _containerId; + + public CosmosDbProcessStorageConnector(string connectionString, string databaseId, string containerId) + { + this._cosmosClient = new CosmosClient(connectionString); + this._databaseId = databaseId; + this._containerId = containerId; + this._container = this._cosmosClient.GetContainer(databaseId, containerId); + } + + public async ValueTask OpenConnectionAsync() + { + // Cosmos DB client handles connection pooling internally, so just ensure the client is initialized + await Task.CompletedTask; + } + + public async ValueTask CloseConnectionAsync() + { + // Dispose the CosmosClient to close connections + this._cosmosClient.Dispose(); + await Task.CompletedTask; + } + + public async Task GetEntryAsync(string id) where TEntry : class + { + try + { + var response = await this._container.ReadItemAsync>(id, new PartitionKey(id)); + return response.Resource.Body; + } + catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) + { + // Item not found + return null; + } + } + + public async Task SaveEntryAsync(string id, string type, TEntry entry) where TEntry : class + { + try + { + var wrappedEntry = new CosmosDbEntity + { + Id = id, + Body = entry, + PartitionKey = id + }; + await this._container.UpsertItemAsync(wrappedEntry, new PartitionKey(id)); + return true; + } + catch (CosmosException ex) + { + // Handle exceptions as needed, log them, etc. + Console.WriteLine($"Error saving entry: {ex.Message}"); + return false; + } + } + + public async Task DeleteEntryAsync(string id) + { + try + { + await this._container.DeleteItemAsync(id, new PartitionKey(id)); + return true; + } + catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) + { + // Item not found + return false; + } + } + + public void Dispose() + { + this._cosmosClient?.Dispose(); + } +} diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/Storage/JsonFileStorage.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/Storage/JsonFileStorage.cs new file mode 100644 index 000000000000..6ecdbe2b4c47 --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/Storage/JsonFileStorage.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft. All rights reserved. +#pragma warning disable IDE0005 // Using directive is unnecessary +using System.Text.Json; +using Microsoft.SemanticKernel; +#pragma warning restore IDE0005 // Using directive is unnecessary + +namespace ProcessWithCloudEvents.SharedComponents.Storage; + +#pragma warning disable CA1812 // Avoid uninstantiated internal classes +internal sealed class JsonFileStorage : IProcessStorageConnector +#pragma warning restore CA1812 // Avoid uninstantiated internal classes +{ + private readonly string _storageDirectory; + + private readonly JsonSerializerOptions _jsonSerializerOptions = new() + { + WriteIndented = true, + }; + + public JsonFileStorage(string storageDirectory) + { + if (string.IsNullOrWhiteSpace(storageDirectory)) + { + throw new ArgumentException("Storage directory cannot be null or empty.", nameof(storageDirectory)); + } + + this._storageDirectory = storageDirectory; + Directory.CreateDirectory(this._storageDirectory); + } + + public async ValueTask OpenConnectionAsync() + { + // For file-based storage, there's no real "connection" to open. + // This method might be used to validate if the storage directory is accessible. + await Task.CompletedTask; + } + + public async ValueTask CloseConnectionAsync() + { + // For file-based storage, there's no real "connection" to close. + await Task.CompletedTask; + } + + private string GetFilePath(string id) + { + return Path.Combine(this._storageDirectory, $"{id}.json"); + } + + public async Task GetEntryAsync(string id) where TEntry : class + { + string filePath = this.GetFilePath(id); + if (!File.Exists(filePath)) + { + return null; + } + + string jsonData = await File.ReadAllTextAsync(filePath); + return JsonSerializer.Deserialize(jsonData); + } + + public async Task SaveEntryAsync(string id, string type, TEntry entry) where TEntry : class + { + string filePath = this.GetFilePath(id); + string jsonData = JsonSerializer.Serialize(entry, this._jsonSerializerOptions); + + await File.WriteAllTextAsync(filePath, jsonData); + return true; + } + + public Task DeleteEntryAsync(string id) + { + string filePath = this.GetFilePath(id); + if (File.Exists(filePath)) + { + File.Delete(filePath); + return Task.FromResult(true); + } + + return Task.FromResult(false); + } +} diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/StorageComponents.props b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/StorageComponents.props new file mode 100644 index 000000000000..318a6c02708a --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Utilities/StorageComponents.props @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/README.md b/dotnet/samples/Demos/ProcessWithCloudEvents/README.md index b445393ad0c4..3da645091944 100644 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/README.md +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/README.md @@ -5,7 +5,7 @@ The following demos describe how to use the SK Process Framework to emit and rec | Project | Description | | --- | --- | | ProcessWithCloudEvents.Processes | Project that contains Process Builders definitions, related steps, models and structures independent of runtime | -| ProcessWithCloudEvents.Grpc | Project that contains a gRPC server using DAPR, that interacts with processes defined in the Processes project using gRPC | +| ProcessWithCloudEvents.Grpc.LocalRuntime | Project that contains a gRPC server using LocalRuntime, that interacts with processes defined in the Processes project using gRPC | | ProcessWithCloudEvents.Client | Project that contains a ReactJS App to showcase sending and receiving cloud events to and from a running SK Process in a server | ## Processes diff --git a/dotnet/samples/Demos/ProcessWithDapr/Controllers/ProcessController.cs b/dotnet/samples/Demos/ProcessWithLocalRuntime/Controllers/ProcessController.cs similarity index 86% rename from dotnet/samples/Demos/ProcessWithDapr/Controllers/ProcessController.cs rename to dotnet/samples/Demos/ProcessWithLocalRuntime/Controllers/ProcessController.cs index efbd990cb692..a1dfc528b863 100644 --- a/dotnet/samples/Demos/ProcessWithDapr/Controllers/ProcessController.cs +++ b/dotnet/samples/Demos/ProcessWithLocalRuntime/Controllers/ProcessController.cs @@ -32,7 +32,7 @@ public ProcessController(Kernel kernel) public async Task PostAsync(string processId) { var process = this.GetProcess(); - var processContext = await process.StartAsync(new KernelProcessEvent() { Id = CommonEvents.StartProcess }, processId: processId); + var processContext = await process.StartAsync(this._kernel, new KernelProcessEvent() { Id = CommonEvents.StartProcess }, processId: processId); var finalState = await processContext.GetStateAsync(); return this.Ok(processId); @@ -65,15 +65,25 @@ private KernelProcess GetProcess() .SendEventTo(new ProcessFunctionTargetBuilder(myAStep)) .SendEventTo(new ProcessFunctionTargetBuilder(myBStep)); - // When AStep finishes, send its output to CStep. - myAStep - .OnEvent(CommonEvents.AStepDone) - .SendEventTo(new ProcessFunctionTargetBuilder(myCStep, parameterName: "astepdata")); - - // When BStep finishes, send its output to CStep also. - myBStep - .OnEvent(CommonEvents.BStepDone) - .SendEventTo(new ProcessFunctionTargetBuilder(myCStep, parameterName: "bstepdata")); + // When step A and step B have finished, trigger the CStep. + processBuilder + .ListenFor() + .AllOf(new() + { + new(messageType: CommonEvents.AStepDone, source: myAStep), + new(messageType: CommonEvents.BStepDone, source: myBStep) + }) + .SendEventTo(new ProcessStepTargetBuilder(myCStep, inputMapping: (inputEvents) => + { + // Map the input events to the CStep's input parameters. + // In this case, we are mapping the output of AStep to the first input parameter of CStep + // and the output of BStep to the second input parameter of CStep. + return new() + { + { "astepdata", inputEvents[$"{nameof(AStep)}.{CommonEvents.AStepDone}"] }, + { "bstepdata", inputEvents[$"{nameof(BStep)}.{CommonEvents.BStepDone}"] } + }; + })); // When CStep has finished without requesting an exit, activate the Kickoff step to start again. myCStep diff --git a/dotnet/samples/Demos/ProcessWithDapr/ProcessWithDapr.csproj b/dotnet/samples/Demos/ProcessWithLocalRuntime/ProcessWithLocalRuntime.csproj similarity index 78% rename from dotnet/samples/Demos/ProcessWithDapr/ProcessWithDapr.csproj rename to dotnet/samples/Demos/ProcessWithLocalRuntime/ProcessWithLocalRuntime.csproj index 2912992ce565..d4e67fa4eb68 100644 --- a/dotnet/samples/Demos/ProcessWithDapr/ProcessWithDapr.csproj +++ b/dotnet/samples/Demos/ProcessWithLocalRuntime/ProcessWithLocalRuntime.csproj @@ -8,15 +8,12 @@ $(NoWarn);CA2007,CA1861,VSTHRD111,SKEXP0001,SKEXP0010,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0080,SKEXP0110 + + - - - - - - + \ No newline at end of file diff --git a/dotnet/samples/Demos/ProcessWithDapr/Program.cs b/dotnet/samples/Demos/ProcessWithLocalRuntime/Program.cs similarity index 78% rename from dotnet/samples/Demos/ProcessWithDapr/Program.cs rename to dotnet/samples/Demos/ProcessWithLocalRuntime/Program.cs index 4b2c0bdd2daf..635153194907 100644 --- a/dotnet/samples/Demos/ProcessWithDapr/Program.cs +++ b/dotnet/samples/Demos/ProcessWithLocalRuntime/Program.cs @@ -14,13 +14,6 @@ // Configure the Kernel with DI. This is required for dependency injection to work with processes. builder.Services.AddKernel(); -// Configure Dapr -builder.Services.AddActors(static options => -{ - // Register the actors required to run Processes - options.AddProcessActors(); -}); - builder.Services.AddControllers(); var app = builder.Build(); @@ -36,5 +29,4 @@ } app.MapControllers(); -app.MapActorsHandlers(); app.Run(); diff --git a/dotnet/samples/Demos/ProcessWithDapr/README.md b/dotnet/samples/Demos/ProcessWithLocalRuntime/README.md similarity index 66% rename from dotnet/samples/Demos/ProcessWithDapr/README.md rename to dotnet/samples/Demos/ProcessWithLocalRuntime/README.md index 9718d5c6f1f1..3607a17bede7 100644 --- a/dotnet/samples/Demos/ProcessWithDapr/README.md +++ b/dotnet/samples/Demos/ProcessWithLocalRuntime/README.md @@ -1,24 +1,17 @@ # Semantic Kernel Processes in Dapr -This demo contains an ASP.NET core API that uses Dapr to run a Semantic Kernel Process. Dapr is a portable, event-driven runtime that can simplify the process of building resilient, stateful application that run in the cloud and/or edge. Dapr is a natural fit for hosting Semantic Kernel Processes and allows you to scale your processes in size and quantity without sacrificing performance, or reliability. +This demo contains an ASP.NET core API that uses Local Runtime to run a Semantic Kernel Process. Local Runtime is a portable, event-driven runtime that can simplify the process of building resilient, stateful application that run in the cloud and/or edge. Local Runtime is a natural fit for hosting Semantic Kernel Processes and allows you to scale your processes in size and quantity without sacrificing performance, or reliability. -For more information about Semantic Kernel Processes and Dapr, see the following documentation: +For more information about Semantic Kernel Processes, see the following documentation: #### Semantic Kernel Processes - [Overview of the Process Framework (docs)](https://learn.microsoft.com/semantic-kernel/frameworks/process/process-framework) - [Getting Started with Processes (samples)](../../GettingStartedWithProcesses/) -#### Dapr - -- [Dapr documentation](https://docs.dapr.io/) -- [Dapr Actor documentation](https://v1-10.docs.dapr.io/developing-applications/building-blocks/actors/) -- [Dapr local development](https://docs.dapr.io/getting-started/install-dapr-selfhost/) ## Running the Demo -Before running this Demo, make sure to configure Dapr for local development following the links above. The Dapr containers must be running for this demo application to run. - ```mermaid flowchart LR Kickoff --> A @@ -35,8 +28,8 @@ flowchart LR End((End)) ``` -1. Build and run the sample. Running the Dapr service locally can be done using the Dapr Cli or with the Dapr VS Code extension. The VS Code extension is the recommended approach if you want to debug the code as it runs. -1. When the service is up and running, it will expose a single API in localhost port 5000. +1. Build and run the sample. +2. When the service is up and running, it will expose a single API in localhost port 5000. #### Invoking the process: @@ -94,17 +87,4 @@ Below are the key aspects of the code that show how Dapr and Semantic Kernel Pro - `dotnet add package Microsoft.SemanticKernel.Process.Core --version 1.24.0-alpha` - `dotnet add package Microsoft.SemanticKernel.Process.Runtime.Dapr --version 1.24.0-alpha` - **_Dapr Packages_** - - - `dotnet add package Dapr.Actors.AspNetCore --version 1.14.0` - -- Configure `program.cs` to use Dapr and the Process framework: - ```csharp - // Configure Dapr - builder.Services.AddActors(static options => - { - // Register the actors required to run Processes - options.AddProcessActors(); - }); - ``` - Build and run a Process as you normally would. For this Demo we run a simple example process from with a Controller's action method in response to a GET request. [See Controller here](./Controllers/ProcessController.cs). diff --git a/dotnet/samples/Demos/ProcessWithDapr/appsettings.json b/dotnet/samples/Demos/ProcessWithLocalRuntime/appsettings.json similarity index 100% rename from dotnet/samples/Demos/ProcessWithDapr/appsettings.json rename to dotnet/samples/Demos/ProcessWithLocalRuntime/appsettings.json diff --git a/dotnet/samples/GettingStartedWithProcesses/GettingStartedWithProcesses.csproj b/dotnet/samples/GettingStartedWithProcesses/GettingStartedWithProcesses.csproj index 7244d2cb967b..5457637d722a 100644 --- a/dotnet/samples/GettingStartedWithProcesses/GettingStartedWithProcesses.csproj +++ b/dotnet/samples/GettingStartedWithProcesses/GettingStartedWithProcesses.csproj @@ -10,8 +10,8 @@ - $(NoWarn);CS8618,IDE0009,IDE1006,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0080,SKEXP0081,SKEXP0101,SKEXP0110,OPENAI001 - + $(NoWarn);CS8618,IDE0009,IDE1006,CA1051,CA1050,CA1707,CA1812,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0080,SKEXP0081,SKEXP0101,SKEXP0110,OPENAI001 + Library 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0 @@ -38,6 +38,7 @@ + @@ -66,4 +67,11 @@ + + + + + + + \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step01/Step01_Processes.cs b/dotnet/samples/GettingStartedWithProcesses/Step01/Step01_Processes.cs index 0c6f072ad03c..c77839e227dc 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step01/Step01_Processes.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step01/Step01_Processes.cs @@ -56,7 +56,7 @@ public async Task UseSimpleProcessAsync() // When the userInput step emits a user input event, send it to the assistantResponse step userInputStep .OnEvent(CommonEvents.UserInputReceived) - .SendEventTo(new ProcessFunctionTargetBuilder(responseStep, parameterName: "userMessage")); + .SendEventTo(new ProcessFunctionTargetBuilder(responseStep)); // When the assistantResponse step emits a response, send it to the userInput step responseStep @@ -68,9 +68,9 @@ public async Task UseSimpleProcessAsync() // Generate a Mermaid diagram for the process and print it to the console string mermaidGraph = kernelProcess.ToMermaid(); - Console.WriteLine($"=== Start - Mermaid Diagram for '{process.Name}' ==="); + Console.WriteLine($"=== Start - Mermaid Diagram for '{process.StepId}' ==="); Console.WriteLine(mermaidGraph); - Console.WriteLine($"=== End - Mermaid Diagram for '{process.Name}' ==="); + Console.WriteLine($"=== End - Mermaid Diagram for '{process.StepId}' ==="); // Generate an image from the Mermaid diagram string generatedImagePath = await MermaidRenderer.GenerateMermaidImageAsync(mermaidGraph, "ChatBotProcess.png"); diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Processes/NewAccountCreationProcess.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Processes/NewAccountCreationProcess.cs index 74095b93d81f..86d970106d13 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step02/Processes/NewAccountCreationProcess.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Processes/NewAccountCreationProcess.cs @@ -25,45 +25,54 @@ public static ProcessBuilder CreateProcess() // 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.ProcessStepFunctions.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.ProcessStepFunctions.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.ProcessStepFunctions.CreateNewAccount, parameterName: "previousCheckSucceeded")); + .ListenFor() + .AllOf( + [ + // When the newCustomerForm is completed, the new user form is passed to the core system record creation step + new(AccountOpeningEvents.NewCustomerFormCompleted, process), + // When the newCustomerForm is completed, the user interaction transcript with the user is passed to the core system record creation step + new(AccountOpeningEvents.CustomerInteractionTranscriptReady, process), + // When the fraudDetectionCheck step passes, the information gets to core system record creation step to kickstart this step + new(AccountOpeningEvents.NewAccountVerificationCheckPassed, process) + ]).SendEventTo(new ProcessStepTargetBuilder(coreSystemRecordCreationStep, inputMapping: (inputEvents) => + { + return new() + { + { "customerDetails", inputEvents[process.GetFullEventId(AccountOpeningEvents.NewCustomerFormCompleted)] }, + { "interactionTranscript", inputEvents[process.GetFullEventId(AccountOpeningEvents.CustomerInteractionTranscriptReady)] }, + { "previousCheckSucceeded", inputEvents[process.GetFullEventId(AccountOpeningEvents.NewAccountVerificationCheckPassed)] }, + }; + })); // 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.ProcessStepFunctions.CreateNewMarketingEntry, parameterName: "userDetails")); + .SendEventTo(new ProcessFunctionTargetBuilder(marketingRecordCreationStep)); // 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.ProcessStepFunctions.CreateCRMEntry, parameterName: "userInteractionDetails")); + .SendEventTo(new ProcessFunctionTargetBuilder(crmRecordStep)); - // 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")); + process + .ListenFor() + .AllOf([ + // When the coreSystemRecordCreation step successfully creates a new accountId, it will pass the account information details to the welcomePacket step + new(AccountOpeningEvents.NewAccountDetailsReady, coreSystemRecordCreationStep), + // When the marketingRecordCreation step successfully creates a new marketing entry, it will notify the welcomePacket step it is ready + new(AccountOpeningEvents.NewMarketingEntryCreated, marketingRecordCreationStep), + // When the crmRecord step successfully creates a new CRM entry, it will notify the welcomePacket step it is ready + new(AccountOpeningEvents.CRMRecordInfoEntryCreated, crmRecordStep) + ]) + .SendEventTo(new ProcessStepTargetBuilder(welcomePacketStep, inputMapping: (inputEvents) => + { + return new() + { + { "accountDetails", inputEvents[coreSystemRecordCreationStep.GetFullEventId(AccountOpeningEvents.NewAccountDetailsReady)] }, + { "marketingEntryCreated", inputEvents[marketingRecordCreationStep.GetFullEventId(AccountOpeningEvents.NewMarketingEntryCreated)] }, + { "crmRecordCreated", inputEvents[crmRecordStep.GetFullEventId(AccountOpeningEvents.CRMRecordInfoEntryCreated)] }, + }; + })); return process; } diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Processes/NewAccountVerificationProcess.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Processes/NewAccountVerificationProcess.cs index 41490e1a69b7..72a9c3964e44 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step02/Processes/NewAccountVerificationProcess.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Processes/NewAccountVerificationProcess.cs @@ -25,14 +25,24 @@ public static ProcessBuilder CreateProcess() process .OnInputEvent(AccountOpeningEvents.NewCustomerFormCompleted) // The information gets passed to the core system record creation step - .SendEventTo(new ProcessFunctionTargetBuilder(customerCreditCheckStep, functionName: CreditScoreCheckStep.ProcessStepFunctions.DetermineCreditScore, parameterName: "customerDetails")) - // The information gets passed to the fraud detection step for validation - .SendEventTo(new ProcessFunctionTargetBuilder(fraudDetectionCheckStep, functionName: FraudDetectionStep.ProcessStepFunctions.FraudDetectionCheck, parameterName: "customerDetails")); + .SendEventTo(new ProcessFunctionTargetBuilder(customerCreditCheckStep, functionName: CreditScoreCheckStep.ProcessStepFunctions.DetermineCreditScore)); - // 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.ProcessStepFunctions.FraudDetectionCheck, parameterName: "previousCheckSucceeded")); + process.ListenFor().AllOf( + [ + // When the newCustomerForm is completed the information gets passed to the fraud detection step for validation + new(AccountOpeningEvents.NewCustomerFormCompleted, process), + // When the creditScoreCheck step results in Approval, the information gets to the fraudDetection step to kickstart this step + new(AccountOpeningEvents.CreditScoreCheckApproved, customerCreditCheckStep) + ]) + .SendEventTo(new ProcessStepTargetBuilder(fraudDetectionCheckStep, inputMapping: (inputEvents) => + { + // The fraud detection step needs the customer details and the credit score check result + return new() + { + { "customerDetails", inputEvents[process.GetFullEventId(AccountOpeningEvents.NewCustomerFormCompleted)] }, + { "previousCheckSucceeded", inputEvents[customerCreditCheckStep.GetFullEventId(AccountOpeningEvents.CreditScoreCheckApproved)] } + }; + })); return process; } diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Step02a_AccountOpening.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Step02a_AccountOpening.cs index e8dc106928b9..fd33e11ca0f3 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step02/Step02a_AccountOpening.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Step02a_AccountOpening.cs @@ -45,7 +45,7 @@ private KernelProcess SetupAccountOpeningProcess() where TUserIn // 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.ProcessStepFunctions.NewAccountProcessUserInfo, "userMessage")); + .SendEventTo(new ProcessFunctionTargetBuilder(newCustomerFormStep, CompleteNewCustomerFormStep.ProcessStepFunctions.NewAccountProcessUserInfo)); userInputStep .OnEvent(CommonEvents.Exit) @@ -64,68 +64,90 @@ private KernelProcess SetupAccountOpeningProcess() where TUserIn // When the newCustomerForm is completed... newCustomerFormStep .OnEvent(AccountOpeningEvents.NewCustomerFormCompleted) - // The information gets passed to the core system record creation step - .SendEventTo(new ProcessFunctionTargetBuilder(customerCreditCheckStep, functionName: CreditScoreCheckStep.ProcessStepFunctions.DetermineCreditScore, parameterName: "customerDetails")) - // The information gets passed to the fraud detection step for validation - .SendEventTo(new ProcessFunctionTargetBuilder(fraudDetectionCheckStep, functionName: FraudDetectionStep.ProcessStepFunctions.FraudDetectionCheck, parameterName: "customerDetails")) - // The information gets passed to the core system record creation step - .SendEventTo(new ProcessFunctionTargetBuilder(coreSystemRecordCreationStep, functionName: NewAccountStep.ProcessStepFunctions.CreateNewAccount, parameterName: "customerDetails")); - - // 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(new ProcessFunctionTargetBuilder(coreSystemRecordCreationStep, functionName: NewAccountStep.ProcessStepFunctions.CreateNewAccount, parameterName: "interactionTranscript")); + // The information gets passed to the credit check step record creation step + .SendEventTo(new ProcessFunctionTargetBuilder(customerCreditCheckStep, functionName: CreditScoreCheckStep.ProcessStepFunctions.DetermineCreditScore)); // 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 customerCreditCheckStep .OnEvent(AccountOpeningEvents.CreditScoreCheckRejected) - .SendEventTo(new ProcessFunctionTargetBuilder(mailServiceStep, functionName: MailServiceStep.ProcessStepFunctions.SendMailToUserWithDetails, parameterName: "message")); - - // 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.ProcessStepFunctions.FraudDetectionCheck, parameterName: "previousCheckSucceeded")); + .SendEventTo(new ProcessFunctionTargetBuilder(mailServiceStep)); + + process + .ListenFor() + .AllOf([ + // When the newCustomerForm is completed the information gets passed to the fraud detection step for validation + new(AccountOpeningEvents.NewCustomerFormCompleted, newCustomerFormStep), + // When the creditScoreCheck step results in Approval, the information gets to the fraudDetection step to kickstart this step + new(AccountOpeningEvents.CreditScoreCheckApproved, customerCreditCheckStep) + ]) + .SendEventTo(new ProcessStepTargetBuilder(fraudDetectionCheckStep, inputMapping: (inputEvents) => + { + return new() + { + { "customerDetails", inputEvents[newCustomerFormStep.GetFullEventId(AccountOpeningEvents.NewCustomerFormCompleted)] }, + { "previousCheckSucceeded", inputEvents[customerCreditCheckStep.GetFullEventId(AccountOpeningEvents.CreditScoreCheckApproved)] }, + }; + })); // 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 fraudDetectionCheckStep .OnEvent(AccountOpeningEvents.FraudDetectionCheckFailed) - .SendEventTo(new ProcessFunctionTargetBuilder(mailServiceStep, functionName: MailServiceStep.ProcessStepFunctions.SendMailToUserWithDetails, parameterName: "message")); - - // When the fraudDetectionCheck step passes, the information gets to core system record creation step to kickstart this step - fraudDetectionCheckStep - .OnEvent(AccountOpeningEvents.FraudDetectionCheckPassed) - .SendEventTo(new ProcessFunctionTargetBuilder(coreSystemRecordCreationStep, functionName: NewAccountStep.ProcessStepFunctions.CreateNewAccount, parameterName: "previousCheckSucceeded")); + .SendEventTo(new ProcessFunctionTargetBuilder(mailServiceStep)); + + process + .ListenFor() + .AllOf( + [ + // When the newCustomerForm is completed, the information gets passed to the core system record creation step + new(AccountOpeningEvents.NewCustomerFormCompleted, newCustomerFormStep), + // When the newCustomerForm is completed, the user interaction transcript with the user is passed to the core system record creation step + new(AccountOpeningEvents.CustomerInteractionTranscriptReady, newCustomerFormStep), + // When the fraudDetectionCheck step passes, the information gets to core system record creation step to kickstart this step + new(AccountOpeningEvents.FraudDetectionCheckPassed, fraudDetectionCheckStep), + ]).SendEventTo(new ProcessStepTargetBuilder(coreSystemRecordCreationStep, inputMapping: (inputEvents) => + { + return new() + { + { "customerDetails", inputEvents[newCustomerFormStep.GetFullEventId(AccountOpeningEvents.NewCustomerFormCompleted)] }, + { "interactionTranscript", inputEvents[newCustomerFormStep.GetFullEventId(AccountOpeningEvents.CustomerInteractionTranscriptReady)] }, + { "previousCheckSucceeded", inputEvents[fraudDetectionCheckStep.GetFullEventId(AccountOpeningEvents.FraudDetectionCheckPassed)] }, + }; + })); // 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.ProcessStepFunctions.CreateNewMarketingEntry, parameterName: "userDetails")); + .SendEventTo(new ProcessFunctionTargetBuilder(marketingRecordCreationStep)); // 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.ProcessStepFunctions.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")); + .SendEventTo(new ProcessFunctionTargetBuilder(crmRecordStep)); + + process + .ListenFor() + .AllOf([ + // When the coreSystemRecordCreation step successfully creates a new accountId, it will pass the account information details to the welcomePacket step + new(AccountOpeningEvents.NewAccountDetailsReady, coreSystemRecordCreationStep), + // When the marketingRecordCreation step successfully creates a new marketing entry, it will notify the welcomePacket step it is ready + new(AccountOpeningEvents.NewMarketingEntryCreated, marketingRecordCreationStep), + // When the crmRecord step successfully creates a new CRM entry, it will notify the welcomePacket step it is ready + new(AccountOpeningEvents.CRMRecordInfoEntryCreated, crmRecordStep) + ]) + .SendEventTo(new ProcessStepTargetBuilder(welcomePacketStep, inputMapping: (inputEvents) => + { + return new() + { + { "accountDetails", inputEvents[coreSystemRecordCreationStep.GetFullEventId(AccountOpeningEvents.NewAccountDetailsReady)] }, + { "marketingEntryCreated", inputEvents[marketingRecordCreationStep.GetFullEventId(AccountOpeningEvents.NewMarketingEntryCreated)] }, + { "crmRecordCreated", inputEvents[crmRecordStep.GetFullEventId(AccountOpeningEvents.CRMRecordInfoEntryCreated)] }, + }; + })); // After crmRecord and marketing gets created, a welcome packet is created to then send information to the user with the mailService step welcomePacketStep .OnEvent(AccountOpeningEvents.WelcomePacketCreated) - .SendEventTo(new ProcessFunctionTargetBuilder(mailServiceStep, functionName: MailServiceStep.ProcessStepFunctions.SendMailToUserWithDetails, parameterName: "message")); + .SendEventTo(new ProcessFunctionTargetBuilder(mailServiceStep)); // All possible paths end up with the user being notified about the account creation decision throw the mailServiceStep completion mailServiceStep diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Step02b_AccountOpening.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Step02b_AccountOpening.cs index 15e6fc692701..61a6e8c41748 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step02/Step02b_AccountOpening.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Step02b_AccountOpening.cs @@ -45,7 +45,7 @@ private KernelProcess SetupAccountOpeningProcess() where TUserIn // 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.ProcessStepFunctions.NewAccountProcessUserInfo, "userMessage")); + .SendEventTo(new ProcessFunctionTargetBuilder(newCustomerFormStep, CompleteNewCustomerFormStep.ProcessStepFunctions.NewAccountProcessUserInfo)); userInputStep .OnEvent(CommonEvents.Exit) diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FishAndChipsProcess.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FishAndChipsProcess.cs index 67a7fbc863ef..5979fba627b4 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FishAndChipsProcess.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FishAndChipsProcess.cs @@ -34,13 +34,16 @@ public static ProcessBuilder CreateProcess(string processName = "FishAndChipsPro .SendEventTo(makeFriedFishStep.WhereInputEventIs(FriedFishProcess.ProcessEvents.PrepareFriedFish)) .SendEventTo(makePotatoFriesStep.WhereInputEventIs(PotatoFriesProcess.ProcessEvents.PreparePotatoFries)); - makeFriedFishStep - .OnEvent(FriedFishProcess.ProcessEvents.FriedFishReady) - .SendEventTo(new ProcessFunctionTargetBuilder(addCondimentsStep, parameterName: "fishActions")); - - makePotatoFriesStep - .OnEvent(PotatoFriesProcess.ProcessEvents.PotatoFriesReady) - .SendEventTo(new ProcessFunctionTargetBuilder(addCondimentsStep, parameterName: "potatoActions")); + processBuilder.ListenFor().AllOf([ + new(FriedFishProcess.ProcessEvents.FriedFishReady, makeFriedFishStep), + new(PotatoFriesProcess.ProcessEvents.PotatoFriesReady, makePotatoFriesStep), + ]).SendEventTo(new ProcessStepTargetBuilder(addCondimentsStep, inputMapping: inputEvents => + { + return new() { + { "fishActions", inputEvents[makeFriedFishStep.GetFullEventId(FriedFishProcess.ProcessEvents.FriedFishReady)] }, + { "potatoActions", inputEvents[makePotatoFriesStep.GetFullEventId(PotatoFriesProcess.ProcessEvents.PotatoFriesReady)] }, + }; + })); addCondimentsStep .OnEvent(AddFishAndChipsCondimentsStep.OutputEvents.CondimentsAdded) @@ -63,13 +66,17 @@ public static ProcessBuilder CreateProcessWithStatefulSteps(string processName = .SendEventTo(makeFriedFishStep.WhereInputEventIs(FriedFishProcess.ProcessEvents.PrepareFriedFish)) .SendEventTo(makePotatoFriesStep.WhereInputEventIs(PotatoFriesProcess.ProcessEvents.PreparePotatoFries)); - makeFriedFishStep - .OnEvent(FriedFishProcess.ProcessEvents.FriedFishReady) - .SendEventTo(new ProcessFunctionTargetBuilder(addCondimentsStep, parameterName: "fishActions")); - - makePotatoFriesStep - .OnEvent(PotatoFriesProcess.ProcessEvents.PotatoFriesReady) - .SendEventTo(new ProcessFunctionTargetBuilder(addCondimentsStep, parameterName: "potatoActions")); + processBuilder.ListenFor().AllOf( + [ + new(FriedFishProcess.ProcessEvents.FriedFishReady, makeFriedFishStep), + new(PotatoFriesProcess.ProcessEvents.PotatoFriesReady, makePotatoFriesStep), + ]).SendEventTo(new ProcessStepTargetBuilder(addCondimentsStep, inputMapping: inputEvents => + { + return new() { + { "fishActions", inputEvents[makeFriedFishStep.GetFullEventId(FriedFishProcess.ProcessEvents.FriedFishReady)] }, + { "potatoActions", inputEvents[makePotatoFriesStep.GetFullEventId(PotatoFriesProcess.ProcessEvents.PotatoFriesReady)] }, + }; + })); addCondimentsStep .OnEvent(AddFishAndChipsCondimentsStep.OutputEvents.CondimentsAdded) diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/ProcessesStates/FishSandwichStateProcessSuccess.json b/dotnet/samples/GettingStartedWithProcesses/Step03/ProcessesStates/FishSandwichStateProcessSuccess.json deleted file mode 100644 index b675988eda82..000000000000 --- a/dotnet/samples/GettingStartedWithProcesses/Step03/ProcessesStates/FishSandwichStateProcessSuccess.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "stepsState": { - "FriedFishWithStatefulStepsProcess": { - "$type": "Process", - "stepsState": { - "GatherFriedFishIngredientsWithStockStep": { - "$type": "Step", - "id": "7d4c02d000a744f490f2b5f9bad721fb", - "name": "GatherFriedFishIngredientsWithStockStep", - "versionInfo": "GatherFishIngredient.V2", - "state": { - "IngredientsStock": 2 - } - }, - "CutFoodStep": { - "$type": "Step", - "id": "f147010a57d34587a3dc1ed4677e5163", - "name": "CutFoodStep", - "versionInfo": "CutFoodStep.V1" - }, - "FryFoodStep": { - "$type": "Step", - "id": "78cc5af4106549afb74d7a6813016f87", - "name": "FryFoodStep", - "versionInfo": "FryFoodStep.V1" - } - }, - "id": "282717158b9f49e5b1acce81429610e0", - "name": "FriedFishWithStatefulStepsProcess", - "versionInfo": "FriedFishProcess.v1" - }, - "AddBunsStep": { - "$type": "Step", - "id": "31e953154e574470911d168a39588ed8", - "name": "AddBunsStep", - "versionInfo": "v1" - }, - "AddSpecialSauceStep": { - "$type": "Step", - "id": "67ee29ff28e4446d8046417675ec21e8", - "name": "AddSpecialSauceStep", - "versionInfo": "v1" - }, - "ExternalFriedFishStep": { - "$type": "Step", - "id": "873b1c8dee45412e975a5e8db2ed0b43", - "name": "ExternalFriedFishStep", - "versionInfo": "v1" - } - }, - "id": "af40089f-e57b-46d1-a15b-40c0d7f3800f", - "name": "FishSandwichWithStatefulStepsProcess", - "versionInfo": "FishSandwich.V1" -} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/ProcessesStates/FishSandwichStateProcessSuccessLowStock.json b/dotnet/samples/GettingStartedWithProcesses/Step03/ProcessesStates/FishSandwichStateProcessSuccessLowStock.json deleted file mode 100644 index 9af1d9c1763b..000000000000 --- a/dotnet/samples/GettingStartedWithProcesses/Step03/ProcessesStates/FishSandwichStateProcessSuccessLowStock.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "$type": "Process", - "stepsState": { - "FriedFishWithStatefulStepsProcess": { - "$type": "Process", - "stepsState": { - "GatherFriedFishIngredientsWithStockStep": { - "$type": "Step", - "id": "2908f8c88cf0476a8e0075c3a8020d5d", - "name": "GatherFriedFishIngredientsWithStockStep", - "versionInfo": "GatherFishIngredient.V2", - "state": { - "IngredientsStock": 1 - } - }, - "CutFoodStep": { - "$type": "Step", - "id": "014388cf0bbd41119b8730dfc4b0b459", - "name": "CutFoodStep", - "versionInfo": "CutFoodStep.V1" - }, - "FryFoodStep": { - "$type": "Step", - "id": "c55af0425d864c4e97b6ae67bd715480", - "name": "FryFoodStep", - "versionInfo": "FryFoodStep.V1" - } - }, - "id": "cab89a17aeae4b9a97568967dbf1ea47", - "name": "FriedFishWithStatefulStepsProcess", - "versionInfo": "FriedFishProcess.v1" - }, - "AddBunsStep": { - "$type": "Step", - "id": "35d09b83dea24ddf8e0c24fbe6a3746c", - "name": "AddBunsStep", - "versionInfo": "v1" - }, - "AddSpecialSauceStep": { - "$type": "Step", - "id": "aa0d408976574afea94387e3da7ca111", - "name": "AddSpecialSauceStep", - "versionInfo": "v1" - }, - "ExternalFriedFishStep": { - "$type": "Step", - "id": "2eda38b8ee8745a4ab8b21f4fa01d173", - "name": "ExternalFriedFishStep", - "versionInfo": "v1" - } - }, - "id": "973b06f1-a522-4d2d-9e1c-ec45a07e275c", - "name": "FishSandwichWithStatefulStepsProcess", - "versionInfo": "FishSandwich.V1" -} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/ProcessesStates/FriedFishProcessStateSuccess.json b/dotnet/samples/GettingStartedWithProcesses/Step03/ProcessesStates/FriedFishProcessStateSuccess.json deleted file mode 100644 index d926c2db3cd1..000000000000 --- a/dotnet/samples/GettingStartedWithProcesses/Step03/ProcessesStates/FriedFishProcessStateSuccess.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "stepsState": { - "GatherFriedFishIngredientsWithStockStep": { - "$type": "Step", - "id": "77c2f967cd354e66a51828e9755d2f07", - "name": "GatherFriedFishIngredientsWithStockStep", - "versionInfo": "GatherFishIngredient.V2", - "state": { - "IngredientsStock": 4 - } - }, - "CutFoodStep": { - "$type": "Step", - "id": "9276d03e64c44a6792d5fd81bd0dc143", - "name": "CutFoodStep", - "versionInfo": "CutFoodStep.V1" - }, - "FryFoodStep": { - "$type": "Step", - "id": "af2a00be4fe2408181ab5654318ed56b", - "name": "FryFoodStep", - "versionInfo": "FryFoodStep.V1" - } - }, - "id": "2050a24b-3e9d-418a-8413-74cadf4f6b4c", - "name": "FriedFishWithStatefulStepsProcess", - "versionInfo": "FriedFishProcess.v1" -} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/ProcessesStates/FriedFishProcessStateSuccessLowStock.json b/dotnet/samples/GettingStartedWithProcesses/Step03/ProcessesStates/FriedFishProcessStateSuccessLowStock.json deleted file mode 100644 index 10372bc65077..000000000000 --- a/dotnet/samples/GettingStartedWithProcesses/Step03/ProcessesStates/FriedFishProcessStateSuccessLowStock.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "$type": "Process", - "stepsState": { - "GatherFriedFishIngredientsWithStockStep": { - "$type": "Step", - "id": "92a4cda38c7248648b0aa7ffaaa57f21", - "name": "GatherFriedFishIngredientsWithStockStep", - "versionInfo": "GatherFishIngredient.V2", - "state": { - "IngredientsStock": 1 - } - }, - "CutFoodStep": { - "$type": "Step", - "id": "7ace89e38e1c48b0b3a700b40d160c68", - "name": "CutFoodStep", - "versionInfo": "CutFoodStep.V1" - }, - "FryFoodStep": { - "$type": "Step", - "id": "09bc39ba6d9745439c7c792b8dac0af7", - "name": "FryFoodStep", - "versionInfo": "FryFoodStep.V1" - } - }, - "id": "669c5850-9efc-4585-b3f0-9291a4471887", - "name": "FriedFishWithStatefulStepsProcess", - "versionInfo": "FriedFishProcess.v1" -} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/ProcessesStates/FriedFishProcessStateSuccessNoStock.json b/dotnet/samples/GettingStartedWithProcesses/Step03/ProcessesStates/FriedFishProcessStateSuccessNoStock.json deleted file mode 100644 index 5c745ebe233f..000000000000 --- a/dotnet/samples/GettingStartedWithProcesses/Step03/ProcessesStates/FriedFishProcessStateSuccessNoStock.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "$type": "Process", - "stepsState": { - "GatherFriedFishIngredientsWithStockStep": { - "$type": "Step", - "id": "92a4cda38c7248648b0aa7ffaaa57f21", - "name": "GatherFriedFishIngredientsWithStockStep", - "versionInfo": "GatherFishIngredient.V2", - "state": { - "IngredientsStock": 0 - } - }, - "CutFoodStep": { - "$type": "Step", - "id": "7ace89e38e1c48b0b3a700b40d160c68", - "name": "CutFoodStep", - "versionInfo": "CutFoodStep.V1" - }, - "FryFoodStep": { - "$type": "Step", - "id": "09bc39ba6d9745439c7c792b8dac0af7", - "name": "FryFoodStep", - "versionInfo": "FryFoodStep.V1" - } - }, - "id": "669c5850-9efc-4585-b3f0-9291a4471887", - "name": "FriedFishWithStatefulStepsProcess", - "versionInfo": "FriedFishProcess.v1" -} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccess/myId.FishSandwichWithStatefulStepsProcess.ProcessDetails.json b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccess/myId.FishSandwichWithStatefulStepsProcess.ProcessDetails.json new file mode 100644 index 000000000000..4d9c02e0098a --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccess/myId.FishSandwichWithStatefulStepsProcess.ProcessDetails.json @@ -0,0 +1,20 @@ +{ + "processInfo": { + "processName": "FishSandwichWithStatefulStepsProcess", + "parentId": null, + "steps": { + "FriedFishWithStatefulStepsProcess": "myId_53747e21-83d9-4b99-b20a-6c401b59a0c0", + "AddBunsStep": "myId_ec377137-ae9d-49a9-a77f-5c964c89fb93", + "AddSpecialSauceStep": "myId_97f458fb-2ea4-4f19-a39f-2ebff68c3442", + "ExternalFriedFishStep": "myId_4ee7fb9c-7f48-4178-b072-c7d10188f8d1" + }, + "instanceId": "myId" + }, + "processEvents": { + "externalPendingMessages": [] + }, + "processState": { + "sharedVariables": {} + }, + "instanceId": "myId" +} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccess/myId_4ee7fb9c-7f48-4178-b072-c7d10188f8d1.ExternalFriedFishStep.StepDetails.json b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccess/myId_4ee7fb9c-7f48-4178-b072-c7d10188f8d1.ExternalFriedFishStep.StepDetails.json new file mode 100644 index 000000000000..d48d475a369b --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccess/myId_4ee7fb9c-7f48-4178-b072-c7d10188f8d1.ExternalFriedFishStep.StepDetails.json @@ -0,0 +1,13 @@ +{ + "stepInfo": { + "stepName": "ExternalFriedFishStep", + "parentId": null, + "version": "v1", + "instanceId": "myId_4ee7fb9c-7f48-4178-b072-c7d10188f8d1" + }, + "stepEvents": { + "edgeGroupEvents": {} + }, + "stepState": null, + "instanceId": "myId_4ee7fb9c-7f48-4178-b072-c7d10188f8d1" +} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccess/myId_53747e21-83d9-4b99-b20a-6c401b59a0c0.FriedFishWithStatefulStepsProcess.ProcessDetails.json b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccess/myId_53747e21-83d9-4b99-b20a-6c401b59a0c0.FriedFishWithStatefulStepsProcess.ProcessDetails.json new file mode 100644 index 000000000000..7334f4310a26 --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccess/myId_53747e21-83d9-4b99-b20a-6c401b59a0c0.FriedFishWithStatefulStepsProcess.ProcessDetails.json @@ -0,0 +1,19 @@ +{ + "processInfo": { + "processName": "FriedFishWithStatefulStepsProcess", + "parentId": null, + "steps": { + "GatherFriedFishIngredientsWithStockStep": "myId_53747e21-83d9-4b99-b20a-6c401b59a0c0_c4572c50-e762-4a7a-88f9-7f2f08c78936", + "CutFoodStep": "myId_53747e21-83d9-4b99-b20a-6c401b59a0c0_3b5be7de-3283-4887-aa2a-218c78c8a023", + "FryFoodStep": "myId_53747e21-83d9-4b99-b20a-6c401b59a0c0_68861295-9da6-422e-9ce5-76640557fa40" + }, + "instanceId": "myId_53747e21-83d9-4b99-b20a-6c401b59a0c0" + }, + "processEvents": { + "externalPendingMessages": [] + }, + "processState": { + "sharedVariables": {} + }, + "instanceId": "myId_53747e21-83d9-4b99-b20a-6c401b59a0c0" +} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccess/myId_53747e21-83d9-4b99-b20a-6c401b59a0c0_3b5be7de-3283-4887-aa2a-218c78c8a023.CutFoodStep.StepDetails.json b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccess/myId_53747e21-83d9-4b99-b20a-6c401b59a0c0_3b5be7de-3283-4887-aa2a-218c78c8a023.CutFoodStep.StepDetails.json new file mode 100644 index 000000000000..864e766e5361 --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccess/myId_53747e21-83d9-4b99-b20a-6c401b59a0c0_3b5be7de-3283-4887-aa2a-218c78c8a023.CutFoodStep.StepDetails.json @@ -0,0 +1,13 @@ +{ + "stepInfo": { + "stepName": "CutFoodStep", + "parentId": null, + "version": "CutFoodStep.V1", + "instanceId": "myId_53747e21-83d9-4b99-b20a-6c401b59a0c0_3b5be7de-3283-4887-aa2a-218c78c8a023" + }, + "stepEvents": { + "edgeGroupEvents": {} + }, + "stepState": null, + "instanceId": "myId_53747e21-83d9-4b99-b20a-6c401b59a0c0_3b5be7de-3283-4887-aa2a-218c78c8a023" +} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccess/myId_53747e21-83d9-4b99-b20a-6c401b59a0c0_68861295-9da6-422e-9ce5-76640557fa40.FryFoodStep.StepDetails.json b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccess/myId_53747e21-83d9-4b99-b20a-6c401b59a0c0_68861295-9da6-422e-9ce5-76640557fa40.FryFoodStep.StepDetails.json new file mode 100644 index 000000000000..42151efc57db --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccess/myId_53747e21-83d9-4b99-b20a-6c401b59a0c0_68861295-9da6-422e-9ce5-76640557fa40.FryFoodStep.StepDetails.json @@ -0,0 +1,13 @@ +{ + "stepInfo": { + "stepName": "FryFoodStep", + "parentId": null, + "version": "FryFoodStep.V1", + "instanceId": "myId_53747e21-83d9-4b99-b20a-6c401b59a0c0_68861295-9da6-422e-9ce5-76640557fa40" + }, + "stepEvents": { + "edgeGroupEvents": {} + }, + "stepState": null, + "instanceId": "myId_53747e21-83d9-4b99-b20a-6c401b59a0c0_68861295-9da6-422e-9ce5-76640557fa40" +} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccess/myId_53747e21-83d9-4b99-b20a-6c401b59a0c0_c4572c50-e762-4a7a-88f9-7f2f08c78936.GatherFriedFishIngredientsWithStockStep.StepDetails.json b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccess/myId_53747e21-83d9-4b99-b20a-6c401b59a0c0_c4572c50-e762-4a7a-88f9-7f2f08c78936.GatherFriedFishIngredientsWithStockStep.StepDetails.json new file mode 100644 index 000000000000..0a12f24dd4ec --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccess/myId_53747e21-83d9-4b99-b20a-6c401b59a0c0_c4572c50-e762-4a7a-88f9-7f2f08c78936.GatherFriedFishIngredientsWithStockStep.StepDetails.json @@ -0,0 +1,19 @@ +{ + "stepInfo": { + "stepName": "GatherFriedFishIngredientsWithStockStep", + "parentId": null, + "version": "GatherFishIngredient.V2", + "instanceId": "myId_53747e21-83d9-4b99-b20a-6c401b59a0c0_c4572c50-e762-4a7a-88f9-7f2f08c78936" + }, + "stepEvents": { + "edgeGroupEvents": {} + }, + "stepState": { + "stateType": "Step03.Steps.GatherIngredientsState, GettingStartedWithProcesses, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", + "state": { + "ObjectType": "Step03.Steps.GatherIngredientsState, GettingStartedWithProcesses, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", + "Content": "{\u0022IngredientsStock\u0022:4}" + } + }, + "instanceId": "myId_53747e21-83d9-4b99-b20a-6c401b59a0c0_c4572c50-e762-4a7a-88f9-7f2f08c78936" +} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccess/myId_97f458fb-2ea4-4f19-a39f-2ebff68c3442.AddSpecialSauceStep.StepDetails.json b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccess/myId_97f458fb-2ea4-4f19-a39f-2ebff68c3442.AddSpecialSauceStep.StepDetails.json new file mode 100644 index 000000000000..ef4d4cee10e8 --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccess/myId_97f458fb-2ea4-4f19-a39f-2ebff68c3442.AddSpecialSauceStep.StepDetails.json @@ -0,0 +1,13 @@ +{ + "stepInfo": { + "stepName": "AddSpecialSauceStep", + "parentId": null, + "version": "v1", + "instanceId": "myId_97f458fb-2ea4-4f19-a39f-2ebff68c3442" + }, + "stepEvents": { + "edgeGroupEvents": {} + }, + "stepState": null, + "instanceId": "myId_97f458fb-2ea4-4f19-a39f-2ebff68c3442" +} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccess/myId_ec377137-ae9d-49a9-a77f-5c964c89fb93.AddBunsStep.StepDetails.json b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccess/myId_ec377137-ae9d-49a9-a77f-5c964c89fb93.AddBunsStep.StepDetails.json new file mode 100644 index 000000000000..19af3d438572 --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccess/myId_ec377137-ae9d-49a9-a77f-5c964c89fb93.AddBunsStep.StepDetails.json @@ -0,0 +1,13 @@ +{ + "stepInfo": { + "stepName": "AddBunsStep", + "parentId": null, + "version": "v1", + "instanceId": "myId_ec377137-ae9d-49a9-a77f-5c964c89fb93" + }, + "stepEvents": { + "edgeGroupEvents": {} + }, + "stepState": null, + "instanceId": "myId_ec377137-ae9d-49a9-a77f-5c964c89fb93" +} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccessLowStock/myId.FishSandwichWithStatefulStepsProcess.ProcessDetails.json b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccessLowStock/myId.FishSandwichWithStatefulStepsProcess.ProcessDetails.json new file mode 100644 index 000000000000..77ee600853fd --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccessLowStock/myId.FishSandwichWithStatefulStepsProcess.ProcessDetails.json @@ -0,0 +1,20 @@ +{ + "processInfo": { + "processName": "FishSandwichWithStatefulStepsProcess", + "parentId": null, + "steps": { + "FriedFishWithStatefulStepsProcess": "myId_eee643c2-e689-41f8-aa10-92be0b882f22", + "AddBunsStep": "myId_47732a13-5877-4de3-a531-d6d61a9c5fd9", + "AddSpecialSauceStep": "myId_c89f75c7-d570-4ccb-9be4-b8346b197633", + "ExternalFriedFishStep": "myId_688eb817-9792-405b-92a1-f9f44632c901" + }, + "instanceId": "myId" + }, + "processEvents": { + "externalPendingMessages": [] + }, + "processState": { + "sharedVariables": {} + }, + "instanceId": "myId" +} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccessLowStock/myId_47732a13-5877-4de3-a531-d6d61a9c5fd9.AddBunsStep.StepDetails.json b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccessLowStock/myId_47732a13-5877-4de3-a531-d6d61a9c5fd9.AddBunsStep.StepDetails.json new file mode 100644 index 000000000000..84261ad7f716 --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccessLowStock/myId_47732a13-5877-4de3-a531-d6d61a9c5fd9.AddBunsStep.StepDetails.json @@ -0,0 +1,13 @@ +{ + "stepInfo": { + "stepName": "AddBunsStep", + "parentId": null, + "version": "v1", + "instanceId": "myId_47732a13-5877-4de3-a531-d6d61a9c5fd9" + }, + "stepEvents": { + "edgeGroupEvents": {} + }, + "stepState": null, + "instanceId": "myId_47732a13-5877-4de3-a531-d6d61a9c5fd9" +} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccessLowStock/myId_688eb817-9792-405b-92a1-f9f44632c901.ExternalFriedFishStep.StepDetails.json b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccessLowStock/myId_688eb817-9792-405b-92a1-f9f44632c901.ExternalFriedFishStep.StepDetails.json new file mode 100644 index 000000000000..884839bf4a26 --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccessLowStock/myId_688eb817-9792-405b-92a1-f9f44632c901.ExternalFriedFishStep.StepDetails.json @@ -0,0 +1,13 @@ +{ + "stepInfo": { + "stepName": "ExternalFriedFishStep", + "parentId": null, + "version": "v1", + "instanceId": "myId_688eb817-9792-405b-92a1-f9f44632c901" + }, + "stepEvents": { + "edgeGroupEvents": {} + }, + "stepState": null, + "instanceId": "myId_688eb817-9792-405b-92a1-f9f44632c901" +} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccessLowStock/myId_c89f75c7-d570-4ccb-9be4-b8346b197633.AddSpecialSauceStep.StepDetails.json b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccessLowStock/myId_c89f75c7-d570-4ccb-9be4-b8346b197633.AddSpecialSauceStep.StepDetails.json new file mode 100644 index 000000000000..3446d725518e --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccessLowStock/myId_c89f75c7-d570-4ccb-9be4-b8346b197633.AddSpecialSauceStep.StepDetails.json @@ -0,0 +1,13 @@ +{ + "stepInfo": { + "stepName": "AddSpecialSauceStep", + "parentId": null, + "version": "v1", + "instanceId": "myId_c89f75c7-d570-4ccb-9be4-b8346b197633" + }, + "stepEvents": { + "edgeGroupEvents": {} + }, + "stepState": null, + "instanceId": "myId_c89f75c7-d570-4ccb-9be4-b8346b197633" +} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccessLowStock/myId_eee643c2-e689-41f8-aa10-92be0b882f22.FriedFishWithStatefulStepsProcess.ProcessDetails.json b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccessLowStock/myId_eee643c2-e689-41f8-aa10-92be0b882f22.FriedFishWithStatefulStepsProcess.ProcessDetails.json new file mode 100644 index 000000000000..8e29da2b114e --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccessLowStock/myId_eee643c2-e689-41f8-aa10-92be0b882f22.FriedFishWithStatefulStepsProcess.ProcessDetails.json @@ -0,0 +1,19 @@ +{ + "processInfo": { + "processName": "FriedFishWithStatefulStepsProcess", + "parentId": null, + "steps": { + "GatherFriedFishIngredientsWithStockStep": "myId_eee643c2-e689-41f8-aa10-92be0b882f22_e933a214-d0d4-418d-9384-036accbd9382", + "CutFoodStep": "myId_eee643c2-e689-41f8-aa10-92be0b882f22_85fb9efa-6c9b-4736-bf83-2a22f1c09050", + "FryFoodStep": "myId_eee643c2-e689-41f8-aa10-92be0b882f22_35cd8ebd-db77-40cf-90b1-a0ce695271e5" + }, + "instanceId": "myId_eee643c2-e689-41f8-aa10-92be0b882f22" + }, + "processEvents": { + "externalPendingMessages": [] + }, + "processState": { + "sharedVariables": {} + }, + "instanceId": "myId_eee643c2-e689-41f8-aa10-92be0b882f22" +} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccessLowStock/myId_eee643c2-e689-41f8-aa10-92be0b882f22_35cd8ebd-db77-40cf-90b1-a0ce695271e5.FryFoodStep.StepDetails.json b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccessLowStock/myId_eee643c2-e689-41f8-aa10-92be0b882f22_35cd8ebd-db77-40cf-90b1-a0ce695271e5.FryFoodStep.StepDetails.json new file mode 100644 index 000000000000..2a31fab1b76b --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccessLowStock/myId_eee643c2-e689-41f8-aa10-92be0b882f22_35cd8ebd-db77-40cf-90b1-a0ce695271e5.FryFoodStep.StepDetails.json @@ -0,0 +1,13 @@ +{ + "stepInfo": { + "stepName": "FryFoodStep", + "parentId": null, + "version": "FryFoodStep.V1", + "instanceId": "myId_eee643c2-e689-41f8-aa10-92be0b882f22_35cd8ebd-db77-40cf-90b1-a0ce695271e5" + }, + "stepEvents": { + "edgeGroupEvents": {} + }, + "stepState": null, + "instanceId": "myId_eee643c2-e689-41f8-aa10-92be0b882f22_35cd8ebd-db77-40cf-90b1-a0ce695271e5" +} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccessLowStock/myId_eee643c2-e689-41f8-aa10-92be0b882f22_85fb9efa-6c9b-4736-bf83-2a22f1c09050.CutFoodStep.StepDetails.json b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccessLowStock/myId_eee643c2-e689-41f8-aa10-92be0b882f22_85fb9efa-6c9b-4736-bf83-2a22f1c09050.CutFoodStep.StepDetails.json new file mode 100644 index 000000000000..542bb4a34535 --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccessLowStock/myId_eee643c2-e689-41f8-aa10-92be0b882f22_85fb9efa-6c9b-4736-bf83-2a22f1c09050.CutFoodStep.StepDetails.json @@ -0,0 +1,13 @@ +{ + "stepInfo": { + "stepName": "CutFoodStep", + "parentId": null, + "version": "CutFoodStep.V1", + "instanceId": "myId_eee643c2-e689-41f8-aa10-92be0b882f22_85fb9efa-6c9b-4736-bf83-2a22f1c09050" + }, + "stepEvents": { + "edgeGroupEvents": {} + }, + "stepState": null, + "instanceId": "myId_eee643c2-e689-41f8-aa10-92be0b882f22_85fb9efa-6c9b-4736-bf83-2a22f1c09050" +} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccessLowStock/myId_eee643c2-e689-41f8-aa10-92be0b882f22_e933a214-d0d4-418d-9384-036accbd9382.GatherFriedFishIngredientsWithStockStep.StepDetails.json b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccessLowStock/myId_eee643c2-e689-41f8-aa10-92be0b882f22_e933a214-d0d4-418d-9384-036accbd9382.GatherFriedFishIngredientsWithStockStep.StepDetails.json new file mode 100644 index 000000000000..b2bf88a055da --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FishSandwichSuccessLowStock/myId_eee643c2-e689-41f8-aa10-92be0b882f22_e933a214-d0d4-418d-9384-036accbd9382.GatherFriedFishIngredientsWithStockStep.StepDetails.json @@ -0,0 +1,19 @@ +{ + "stepInfo": { + "stepName": "GatherFriedFishIngredientsWithStockStep", + "parentId": null, + "version": "GatherFishIngredient.V2", + "instanceId": "myId_eee643c2-e689-41f8-aa10-92be0b882f22_e933a214-d0d4-418d-9384-036accbd9382" + }, + "stepEvents": { + "edgeGroupEvents": {} + }, + "stepState": { + "stateType": "Step03.Steps.GatherIngredientsState, GettingStartedWithProcesses, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", + "state": { + "ObjectType": "Step03.Steps.GatherIngredientsState, GettingStartedWithProcesses, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", + "Content": "{\u0022IngredientsStock\u0022:1}" + } + }, + "instanceId": "myId_eee643c2-e689-41f8-aa10-92be0b882f22_e933a214-d0d4-418d-9384-036accbd9382" +} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccess/myId.FriedFishWithStatefulStepsProcess.ProcessDetails.json b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccess/myId.FriedFishWithStatefulStepsProcess.ProcessDetails.json new file mode 100644 index 000000000000..5f18951ddd9c --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccess/myId.FriedFishWithStatefulStepsProcess.ProcessDetails.json @@ -0,0 +1,19 @@ +{ + "processInfo": { + "processName": "FriedFishWithStatefulStepsProcess", + "parentId": null, + "steps": { + "GatherFriedFishIngredientsWithStockStep": "myId_d2c7e47c-792e-4092-8faf-6035150d4076", + "CutFoodStep": "myId_204e59a3-05c4-4186-8e1f-aa7db2b01c30", + "FryFoodStep": "myId_6e850696-7132-4cc4-8495-2f21d90ec252" + }, + "instanceId": "myId" + }, + "processEvents": { + "externalPendingMessages": [] + }, + "processState": { + "sharedVariables": {} + }, + "instanceId": "myId" +} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccess/myId_204e59a3-05c4-4186-8e1f-aa7db2b01c30.CutFoodStep.StepDetails.json b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccess/myId_204e59a3-05c4-4186-8e1f-aa7db2b01c30.CutFoodStep.StepDetails.json new file mode 100644 index 000000000000..a9a9287f5834 --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccess/myId_204e59a3-05c4-4186-8e1f-aa7db2b01c30.CutFoodStep.StepDetails.json @@ -0,0 +1,13 @@ +{ + "stepInfo": { + "stepName": "CutFoodStep", + "parentId": null, + "version": "CutFoodStep.V1", + "instanceId": "myId_204e59a3-05c4-4186-8e1f-aa7db2b01c30" + }, + "stepEvents": { + "edgeGroupEvents": {} + }, + "stepState": null, + "instanceId": "myId_204e59a3-05c4-4186-8e1f-aa7db2b01c30" +} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccess/myId_6e850696-7132-4cc4-8495-2f21d90ec252.FryFoodStep.StepDetails.json b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccess/myId_6e850696-7132-4cc4-8495-2f21d90ec252.FryFoodStep.StepDetails.json new file mode 100644 index 000000000000..be439e6dd145 --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccess/myId_6e850696-7132-4cc4-8495-2f21d90ec252.FryFoodStep.StepDetails.json @@ -0,0 +1,13 @@ +{ + "stepInfo": { + "stepName": "FryFoodStep", + "parentId": null, + "version": "FryFoodStep.V1", + "instanceId": "myId_6e850696-7132-4cc4-8495-2f21d90ec252" + }, + "stepEvents": { + "edgeGroupEvents": {} + }, + "stepState": null, + "instanceId": "myId_6e850696-7132-4cc4-8495-2f21d90ec252" +} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccess/myId_d2c7e47c-792e-4092-8faf-6035150d4076.GatherFriedFishIngredientsWithStockStep.StepDetails.json b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccess/myId_d2c7e47c-792e-4092-8faf-6035150d4076.GatherFriedFishIngredientsWithStockStep.StepDetails.json new file mode 100644 index 000000000000..951f7f255aef --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccess/myId_d2c7e47c-792e-4092-8faf-6035150d4076.GatherFriedFishIngredientsWithStockStep.StepDetails.json @@ -0,0 +1,19 @@ +{ + "stepInfo": { + "stepName": "GatherFriedFishIngredientsWithStockStep", + "parentId": null, + "version": "GatherFishIngredient.V2", + "instanceId": "myId_d2c7e47c-792e-4092-8faf-6035150d4076" + }, + "stepEvents": { + "edgeGroupEvents": {} + }, + "stepState": { + "stateType": "Step03.Steps.GatherIngredientsState, GettingStartedWithProcesses, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", + "state": { + "ObjectType": "Step03.Steps.GatherIngredientsState, GettingStartedWithProcesses, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", + "Content": "{\u0022IngredientsStock\u0022:3}" + } + }, + "instanceId": "myId_d2c7e47c-792e-4092-8faf-6035150d4076" +} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccessLowStock/myId.FriedFishWithStatefulStepsProcess.ProcessDetails.json b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccessLowStock/myId.FriedFishWithStatefulStepsProcess.ProcessDetails.json new file mode 100644 index 000000000000..194f70a1b042 --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccessLowStock/myId.FriedFishWithStatefulStepsProcess.ProcessDetails.json @@ -0,0 +1,19 @@ +{ + "processInfo": { + "processName": "FriedFishWithStatefulStepsProcess", + "parentId": null, + "steps": { + "GatherFriedFishIngredientsWithStockStep": "myId_18022019-3ea4-433b-853a-9fcd80c00695", + "CutFoodStep": "myId_c9b8d06a-ee3c-45c7-87cc-545e18535c54", + "FryFoodStep": "myId_6295dc68-9f08-40de-9cf3-b375940c432c" + }, + "instanceId": "myId" + }, + "processEvents": { + "externalPendingMessages": [] + }, + "processState": { + "sharedVariables": {} + }, + "instanceId": "myId" +} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccessLowStock/myId_18022019-3ea4-433b-853a-9fcd80c00695.GatherFriedFishIngredientsWithStockStep.StepDetails.json b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccessLowStock/myId_18022019-3ea4-433b-853a-9fcd80c00695.GatherFriedFishIngredientsWithStockStep.StepDetails.json new file mode 100644 index 000000000000..067c3f8877d2 --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccessLowStock/myId_18022019-3ea4-433b-853a-9fcd80c00695.GatherFriedFishIngredientsWithStockStep.StepDetails.json @@ -0,0 +1,19 @@ +{ + "stepInfo": { + "stepName": "GatherFriedFishIngredientsWithStockStep", + "parentId": null, + "version": "GatherFishIngredient.V2", + "instanceId": "myId_18022019-3ea4-433b-853a-9fcd80c00695" + }, + "stepEvents": { + "edgeGroupEvents": {} + }, + "stepState": { + "stateType": "Step03.Steps.GatherIngredientsState, GettingStartedWithProcesses, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", + "state": { + "ObjectType": "Step03.Steps.GatherIngredientsState, GettingStartedWithProcesses, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", + "Content": "{\u0022IngredientsStock\u0022:1}" + } + }, + "instanceId": "myId_18022019-3ea4-433b-853a-9fcd80c00695" +} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccessLowStock/myId_6295dc68-9f08-40de-9cf3-b375940c432c.FryFoodStep.StepDetails.json b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccessLowStock/myId_6295dc68-9f08-40de-9cf3-b375940c432c.FryFoodStep.StepDetails.json new file mode 100644 index 000000000000..0ad9db24827b --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccessLowStock/myId_6295dc68-9f08-40de-9cf3-b375940c432c.FryFoodStep.StepDetails.json @@ -0,0 +1,13 @@ +{ + "stepInfo": { + "stepName": "FryFoodStep", + "parentId": null, + "version": "FryFoodStep.V1", + "instanceId": "myId_6295dc68-9f08-40de-9cf3-b375940c432c" + }, + "stepEvents": { + "edgeGroupEvents": {} + }, + "stepState": null, + "instanceId": "myId_6295dc68-9f08-40de-9cf3-b375940c432c" +} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccessLowStock/myId_c9b8d06a-ee3c-45c7-87cc-545e18535c54.CutFoodStep.StepDetails.json b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccessLowStock/myId_c9b8d06a-ee3c-45c7-87cc-545e18535c54.CutFoodStep.StepDetails.json new file mode 100644 index 000000000000..287ea56f6f8b --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccessLowStock/myId_c9b8d06a-ee3c-45c7-87cc-545e18535c54.CutFoodStep.StepDetails.json @@ -0,0 +1,13 @@ +{ + "stepInfo": { + "stepName": "CutFoodStep", + "parentId": null, + "version": "CutFoodStep.V1", + "instanceId": "myId_c9b8d06a-ee3c-45c7-87cc-545e18535c54" + }, + "stepEvents": { + "edgeGroupEvents": {} + }, + "stepState": null, + "instanceId": "myId_c9b8d06a-ee3c-45c7-87cc-545e18535c54" +} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccessNoStock/myId.FriedFishWithStatefulStepsProcess.ProcessDetails.json b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccessNoStock/myId.FriedFishWithStatefulStepsProcess.ProcessDetails.json new file mode 100644 index 000000000000..f7dd5f040dab --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccessNoStock/myId.FriedFishWithStatefulStepsProcess.ProcessDetails.json @@ -0,0 +1,19 @@ +{ + "processInfo": { + "processName": "FriedFishWithStatefulStepsProcess", + "parentId": null, + "steps": { + "GatherFriedFishIngredientsWithStockStep": "myId_d66ccaba-1a6c-421a-b990-95129b25a16b", + "CutFoodStep": "myId_da066941-2a4f-4f45-bda2-f84ccc944a3c", + "FryFoodStep": "myId_9ca6e906-fa39-4107-b3ec-c81247d6c75a" + }, + "instanceId": "myId" + }, + "processEvents": { + "externalPendingMessages": [] + }, + "processState": { + "sharedVariables": {} + }, + "instanceId": "myId" +} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccessNoStock/myId_9ca6e906-fa39-4107-b3ec-c81247d6c75a.FryFoodStep.StepDetails.json b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccessNoStock/myId_9ca6e906-fa39-4107-b3ec-c81247d6c75a.FryFoodStep.StepDetails.json new file mode 100644 index 000000000000..57b7f8f39e1c --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccessNoStock/myId_9ca6e906-fa39-4107-b3ec-c81247d6c75a.FryFoodStep.StepDetails.json @@ -0,0 +1,13 @@ +{ + "stepInfo": { + "stepName": "FryFoodStep", + "parentId": null, + "version": "FryFoodStep.V1", + "instanceId": "myId_9ca6e906-fa39-4107-b3ec-c81247d6c75a" + }, + "stepEvents": { + "edgeGroupEvents": {} + }, + "stepState": null, + "instanceId": "myId_9ca6e906-fa39-4107-b3ec-c81247d6c75a" +} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccessNoStock/myId_d66ccaba-1a6c-421a-b990-95129b25a16b.GatherFriedFishIngredientsWithStockStep.StepDetails.json b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccessNoStock/myId_d66ccaba-1a6c-421a-b990-95129b25a16b.GatherFriedFishIngredientsWithStockStep.StepDetails.json new file mode 100644 index 000000000000..8bf4ad1e21d3 --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccessNoStock/myId_d66ccaba-1a6c-421a-b990-95129b25a16b.GatherFriedFishIngredientsWithStockStep.StepDetails.json @@ -0,0 +1,19 @@ +{ + "stepInfo": { + "stepName": "GatherFriedFishIngredientsWithStockStep", + "parentId": null, + "version": "GatherFishIngredient.V2", + "instanceId": "myId_d66ccaba-1a6c-421a-b990-95129b25a16b" + }, + "stepEvents": { + "edgeGroupEvents": {} + }, + "stepState": { + "stateType": "Step03.Steps.GatherIngredientsState, GettingStartedWithProcesses, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", + "state": { + "ObjectType": "Step03.Steps.GatherIngredientsState, GettingStartedWithProcesses, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", + "Content": "{\u0022IngredientsStock\u0022:0}" + } + }, + "instanceId": "myId_d66ccaba-1a6c-421a-b990-95129b25a16b" +} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccessNoStock/myId_da066941-2a4f-4f45-bda2-f84ccc944a3c.CutFoodStep.StepDetails.json b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccessNoStock/myId_da066941-2a4f-4f45-bda2-f84ccc944a3c.CutFoodStep.StepDetails.json new file mode 100644 index 000000000000..d4a6149bf15e --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/States/FriedFishSuccessNoStock/myId_da066941-2a4f-4f45-bda2-f84ccc944a3c.CutFoodStep.StepDetails.json @@ -0,0 +1,13 @@ +{ + "stepInfo": { + "stepName": "CutFoodStep", + "parentId": null, + "version": "CutFoodStep.V1", + "instanceId": "myId_da066941-2a4f-4f45-bda2-f84ccc944a3c" + }, + "stepEvents": { + "edgeGroupEvents": {} + }, + "stepState": null, + "instanceId": "myId_da066941-2a4f-4f45-bda2-f84ccc944a3c" +} \ No newline at end of file diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Step03a_FoodPreparation.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Step03a_FoodPreparation.cs index 99d2f2f4e122..058222a99ba2 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step03/Step03a_FoodPreparation.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Step03a_FoodPreparation.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Process.Models; +using Microsoft.SemanticKernel.Process.TestsShared.Services.Storage; using Microsoft.SemanticKernel.Process.Tools; using Step03.Processes; using Utilities; @@ -39,9 +39,9 @@ public async Task UsePrepareFishSandwichProcessAsync() var process = FishSandwichProcess.CreateProcess(); string mermaidGraph = process.ToMermaid(1); - Console.WriteLine($"=== Start - Mermaid Diagram for '{process.Name}' ==="); + Console.WriteLine($"=== Start - Mermaid Diagram for '{process.StepId}' ==="); Console.WriteLine(mermaidGraph); - Console.WriteLine($"=== End - Mermaid Diagram for '{process.Name}' ==="); + Console.WriteLine($"=== End - Mermaid Diagram for '{process.StepId}' ==="); await UsePrepareSpecificProductAsync(process, FishSandwichProcess.ProcessEvents.PrepareFishSandwich); } @@ -67,10 +67,10 @@ public async Task UsePrepareStatefulFriedFishProcessNoSharedStateAsync() Kernel kernel = CreateKernelWithChatCompletion(); // Assert - Console.WriteLine($"=== Start SK Process '{processBuilder.Name}' ==="); - await ExecuteProcessWithStateAsync(processBuilder.Build(), kernel, externalTriggerEvent, "Order 1"); - await ExecuteProcessWithStateAsync(processBuilder.Build(), kernel, externalTriggerEvent, "Order 2"); - Console.WriteLine($"=== End SK Process '{processBuilder.Name}' ==="); + Console.WriteLine($"=== Start SK Process '{processBuilder.StepId}' ==="); + await ExecuteProcessWithStateAsync(processBuilder.Build(), kernel, null, externalTriggerEvent, "Order 1"); + await ExecuteProcessWithStateAsync(processBuilder.Build(), kernel, null, externalTriggerEvent, "Order 2"); + Console.WriteLine($"=== End SK Process '{processBuilder.StepId}' ==="); } /// @@ -87,11 +87,11 @@ public async Task UsePrepareStatefulFriedFishProcessSharedStateAsync() Kernel kernel = CreateKernelWithChatCompletion(); KernelProcess kernelProcess = processBuilder.Build(); - Console.WriteLine($"=== Start SK Process '{processBuilder.Name}' ==="); - await ExecuteProcessWithStateAsync(kernelProcess, kernel, externalTriggerEvent, "Order 1"); - await ExecuteProcessWithStateAsync(kernelProcess, kernel, externalTriggerEvent, "Order 2"); - await ExecuteProcessWithStateAsync(kernelProcess, kernel, externalTriggerEvent, "Order 3"); - Console.WriteLine($"=== End SK Process '{processBuilder.Name}' ==="); + Console.WriteLine($"=== Start SK Process '{processBuilder.StepId}' ==="); + await ExecuteProcessWithStateAsync(kernelProcess, kernel, null, externalTriggerEvent, "Order 1"); + await ExecuteProcessWithStateAsync(kernelProcess, kernel, null, externalTriggerEvent, "Order 2"); + await ExecuteProcessWithStateAsync(kernelProcess, kernel, null, externalTriggerEvent, "Order 3"); + Console.WriteLine($"=== End SK Process '{processBuilder.StepId}' ==="); } [Fact] @@ -103,143 +103,90 @@ public async Task UsePrepareStatefulPotatoFriesProcessSharedStateAsync() Kernel kernel = CreateKernelWithChatCompletion(); KernelProcess kernelProcess = processBuilder.Build(); - Console.WriteLine($"=== Start SK Process '{processBuilder.Name}' ==="); - await ExecuteProcessWithStateAsync(kernelProcess, kernel, externalTriggerEvent, "Order 1"); - await ExecuteProcessWithStateAsync(kernelProcess, kernel, externalTriggerEvent, "Order 2"); - await ExecuteProcessWithStateAsync(kernelProcess, kernel, externalTriggerEvent, "Order 3"); - Console.WriteLine($"=== End SK Process '{processBuilder.Name}' ==="); + Console.WriteLine($"=== Start SK Process '{processBuilder.StepId}' ==="); + await ExecuteProcessWithStateAsync(kernelProcess, kernel, null, externalTriggerEvent, "Order 1"); + await ExecuteProcessWithStateAsync(kernelProcess, kernel, null, externalTriggerEvent, "Order 2"); + await ExecuteProcessWithStateAsync(kernelProcess, kernel, null, externalTriggerEvent, "Order 3"); + Console.WriteLine($"=== End SK Process '{processBuilder.StepId}' ==="); } - private async Task ExecuteProcessWithStateAsync(KernelProcess process, Kernel kernel, string externalTriggerEvent, string orderLabel = "Order 1") + private async Task ExecuteProcessWithStateAsync(KernelProcess process, Kernel kernel, IProcessStorageConnector? storageConnector, string externalTriggerEvent, string orderLabel = "Order 1", string? processId = null) { Console.WriteLine($"=== {orderLabel} ==="); var runningProcess = await process.StartAsync(kernel, new KernelProcessEvent() { Id = externalTriggerEvent, Data = new List() - }); + }, processId, storageConnector: storageConnector); return await runningProcess.GetStateAsync(); } - #region Running processes and saving Process State Metadata in a file locally - [Fact] - public async Task RunAndStoreStatefulFriedFishProcessStateAsync() - { - Kernel kernel = CreateKernelWithChatCompletion(); - ProcessBuilder builder = FriedFishProcess.CreateProcessWithStatefulStepsV1(); - KernelProcess friedFishProcess = builder.Build(); - - var executedProcess = await ExecuteProcessWithStateAsync(friedFishProcess, kernel, externalTriggerEvent: FriedFishProcess.ProcessEvents.PrepareFriedFish); - var processState = executedProcess.ToProcessStateMetadata(); - DumpProcessStateMetadataLocally(processState, _statefulFriedFishProcessFilename); - } - - [Fact] - public async Task RunAndStoreStatefulFishSandwichProcessStateAsync() - { - Kernel kernel = CreateKernelWithChatCompletion(); - ProcessBuilder builder = FishSandwichProcess.CreateProcessWithStatefulStepsV1(); - KernelProcess friedFishProcess = builder.Build(); - - var executedProcess = await ExecuteProcessWithStateAsync(friedFishProcess, kernel, externalTriggerEvent: FishSandwichProcess.ProcessEvents.PrepareFishSandwich); - var processState = executedProcess.ToProcessStateMetadata(); - DumpProcessStateMetadataLocally(processState, _statefulFishSandwichProcessFilename); - } - #endregion - #region Reading State from local file and apply to existing ProcessBuilder [Fact] public async Task RunStatefulFriedFishProcessFromFileAsync() { - var processState = LoadProcessStateMetadata(this._statefulFriedFishProcessFilename); - Assert.NotNull(processState); + var processStatePath = GetSampleStep03DirPath(this._statefulFriedFishProcessFoldername); + var stateFileStorage = new JsonFileStorage(processStatePath); Kernel kernel = CreateKernelWithChatCompletion(); ProcessBuilder processBuilder = FriedFishProcess.CreateProcessWithStatefulStepsV1(); - KernelProcess processFromFile = processBuilder.Build(processState); + KernelProcess processFromFile = processBuilder.Build(); - await ExecuteProcessWithStateAsync(processFromFile, kernel, externalTriggerEvent: FriedFishProcess.ProcessEvents.PrepareFriedFish); + await ExecuteProcessWithStateAsync(processFromFile, kernel, stateFileStorage, FriedFishProcess.ProcessEvents.PrepareFriedFish, processId: this._processId); } [Fact] public async Task RunStatefulFriedFishProcessWithLowStockFromFileAsync() { - var processState = LoadProcessStateMetadata(this._statefulFriedFishLowStockProcessFilename); - Assert.NotNull(processState); + var processStatePath = GetSampleStep03DirPath(this._statefulFriedFishLowStockProcessFoldername); + var stateFileStorage = new JsonFileStorage(processStatePath); Kernel kernel = CreateKernelWithChatCompletion(); ProcessBuilder processBuilder = FriedFishProcess.CreateProcessWithStatefulStepsV1(); - KernelProcess processFromFile = processBuilder.Build(processState); + KernelProcess processFromFile = processBuilder.Build(); - await ExecuteProcessWithStateAsync(processFromFile, kernel, externalTriggerEvent: FriedFishProcess.ProcessEvents.PrepareFriedFish); + await ExecuteProcessWithStateAsync(processFromFile, kernel, stateFileStorage, FriedFishProcess.ProcessEvents.PrepareFriedFish, processId: this._processId); } [Fact] public async Task RunStatefulFriedFishProcessWithNoStockFromFileAsync() { - var processState = LoadProcessStateMetadata(this._statefulFriedFishNoStockProcessFilename); - Assert.NotNull(processState); + var processStatePath = GetSampleStep03DirPath(this._statefulFriedFishNoStockProcessFoldername); + var stateFileStorage = new JsonFileStorage(processStatePath); Kernel kernel = CreateKernelWithChatCompletion(); ProcessBuilder processBuilder = FriedFishProcess.CreateProcessWithStatefulStepsV1(); - KernelProcess processFromFile = processBuilder.Build(processState); + KernelProcess processFromFile = processBuilder.Build(); - await ExecuteProcessWithStateAsync(processFromFile, kernel, externalTriggerEvent: FriedFishProcess.ProcessEvents.PrepareFriedFish); + await ExecuteProcessWithStateAsync(processFromFile, kernel, stateFileStorage, FriedFishProcess.ProcessEvents.PrepareFriedFish, processId: this._processId); } [Fact] public async Task RunStatefulFishSandwichProcessFromFileAsync() { - var processState = LoadProcessStateMetadata(this._statefulFishSandwichProcessFilename); - Assert.NotNull(processState); + var processStatePath = GetSampleStep03DirPath(this._statefulFishSandwichProcessFoldername); + var stateFileStorage = new JsonFileStorage(processStatePath); Kernel kernel = CreateKernelWithChatCompletion(); ProcessBuilder processBuilder = FishSandwichProcess.CreateProcessWithStatefulStepsV1(); - KernelProcess processFromFile = processBuilder.Build(processState); + KernelProcess processFromFile = processBuilder.Build(); - await ExecuteProcessWithStateAsync(processFromFile, kernel, externalTriggerEvent: FishSandwichProcess.ProcessEvents.PrepareFishSandwich); + await ExecuteProcessWithStateAsync(processFromFile, kernel, stateFileStorage, FishSandwichProcess.ProcessEvents.PrepareFishSandwich, processId: this._processId); } [Fact] public async Task RunStatefulFishSandwichProcessWithLowStockFromFileAsync() { - var processState = LoadProcessStateMetadata(this._statefulFishSandwichLowStockProcessFilename); - Assert.NotNull(processState); + var processStatePath = GetSampleStep03DirPath(this._statefulFishSandwichLowStockProcessFoldername); + var stateFileStorage = new JsonFileStorage(processStatePath); Kernel kernel = CreateKernelWithChatCompletion(); ProcessBuilder processBuilder = FishSandwichProcess.CreateProcessWithStatefulStepsV1(); - KernelProcess processFromFile = processBuilder.Build(processState); + KernelProcess processFromFile = processBuilder.Build(); - await ExecuteProcessWithStateAsync(processFromFile, kernel, externalTriggerEvent: FishSandwichProcess.ProcessEvents.PrepareFishSandwich); + await ExecuteProcessWithStateAsync(processFromFile, kernel, stateFileStorage, FishSandwichProcess.ProcessEvents.PrepareFishSandwich, processId: this._processId); } - #region Versioning compatibiily scenarios: Loading State generated with previous version of process - [Fact] - public async Task RunStatefulFriedFishV2ProcessWithLowStockV1StateFromFileAsync() - { - var processState = LoadProcessStateMetadata(this._statefulFriedFishLowStockProcessFilename); - Assert.NotNull(processState); - - Kernel kernel = CreateKernelWithChatCompletion(); - ProcessBuilder processBuilder = FriedFishProcess.CreateProcessWithStatefulStepsV2(); - KernelProcess processFromFile = processBuilder.Build(processState); - - await ExecuteProcessWithStateAsync(processFromFile, kernel, externalTriggerEvent: FriedFishProcess.ProcessEvents.PrepareFriedFish); - } - - [Fact] - public async Task RunStatefulFishSandwichV2ProcessWithLowStockV1StateFromFileAsync() - { - var processState = LoadProcessStateMetadata(this._statefulFishSandwichLowStockProcessFilename); - Assert.NotNull(processState); - - Kernel kernel = CreateKernelWithChatCompletion(); - ProcessBuilder processBuilder = FishSandwichProcess.CreateProcessWithStatefulStepsV2(); - KernelProcess processFromFile = processBuilder.Build(processState); - - await ExecuteProcessWithStateAsync(processFromFile, kernel, externalTriggerEvent: FishSandwichProcess.ProcessEvents.PrepareFishSandwich); - } - #endregion #endregion #endregion protected async Task UsePrepareSpecificProductAsync(ProcessBuilder processBuilder, string externalTriggerEvent) @@ -251,36 +198,26 @@ protected async Task UsePrepareSpecificProductAsync(ProcessBuilder processBuilde KernelProcess kernelProcess = processBuilder.Build(); // Assert - Console.WriteLine($"=== Start SK Process '{processBuilder.Name}' ==="); + Console.WriteLine($"=== Start SK Process '{processBuilder.StepId}' ==="); await using var runningProcess = await kernelProcess.StartAsync(kernel, new KernelProcessEvent() { Id = externalTriggerEvent, Data = new List() }); - Console.WriteLine($"=== End SK Process '{processBuilder.Name}' ==="); + Console.WriteLine($"=== End SK Process '{processBuilder.StepId}' ==="); } // Step03a Utils for saving and loading SK Processes from/to repository - private readonly string _step03RelativePath = Path.Combine("Step03", "ProcessesStates"); - private readonly string _statefulFriedFishProcessFilename = "FriedFishProcessStateSuccess.json"; - private readonly string _statefulFriedFishLowStockProcessFilename = "FriedFishProcessStateSuccessLowStock.json"; - private readonly string _statefulFriedFishNoStockProcessFilename = "FriedFishProcessStateSuccessNoStock.json"; - private readonly string _statefulFishSandwichProcessFilename = "FishSandwichStateProcessSuccess.json"; - private readonly string _statefulFishSandwichLowStockProcessFilename = "FishSandwichStateProcessSuccessLowStock.json"; - - private void DumpProcessStateMetadataLocally(KernelProcessStateMetadata processStateInfo, string jsonFilename) - { - var sampleRelativePath = GetSampleStep03Filepath(jsonFilename); - ProcessStateMetadataUtilities.DumpProcessStateMetadataLocally(processStateInfo, sampleRelativePath); - } - - private KernelProcessStateMetadata? LoadProcessStateMetadata(string jsonFilename) - { - var sampleRelativePath = GetSampleStep03Filepath(jsonFilename); - return ProcessStateMetadataUtilities.LoadProcessStateMetadata(sampleRelativePath); - } - - private string GetSampleStep03Filepath(string jsonFilename) - { - return Path.Combine(this._step03RelativePath, jsonFilename); + private readonly string _processId = "myId"; + private readonly string _step03RelativePath = Path.Combine("Step03", "States"); + private readonly string _statefulFriedFishProcessFoldername = "FriedFishSuccess"; + private readonly string _statefulFriedFishLowStockProcessFoldername = "FriedFishSuccessLowStock"; + private readonly string _statefulFriedFishNoStockProcessFoldername = "FriedFishSuccessNoStock"; + private readonly string _statefulFishSandwichProcessFoldername = "FishSandwichSuccess"; + private readonly string _statefulFishSandwichLowStockProcessFoldername = "FishSandwichSuccessLowStock"; + + private string GetSampleStep03DirPath(string dir) + { + var relativeDir = Path.Combine(this._step03RelativePath, dir); + return FileStorageUtilities.GetRepositoryProcessStateFilepath(relativeDir, checkFilepathExists: true); } } diff --git a/dotnet/samples/GettingStartedWithProcesses/Step04/Step04_AgentOrchestration.cs b/dotnet/samples/GettingStartedWithProcesses/Step04/Step04_AgentOrchestration.cs index 3bcf8758956a..e4c0bb1d3ddf 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step04/Step04_AgentOrchestration.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step04/Step04_AgentOrchestration.cs @@ -74,7 +74,7 @@ await process.StartAsync( // Cleaning up created agents var processState = await localProcess.GetStateAsync(); - var agentState = (KernelProcessStepState)processState.Steps.Where(step => step.State.Id == "Student").FirstOrDefault()!.State; + var agentState = (KernelProcessStepState)processState.Steps.Where(step => step.State.StepId == "Student").FirstOrDefault()!.State; var agentId = agentState?.State?.AgentId; if (agentId != null) { @@ -168,7 +168,7 @@ private KernelProcess SetupSingleAgentProcess(string processName // Pass user input to primary agent userInputStep .OnEvent(CommonEvents.UserInputReceived) - .SendEventTo(new ProcessFunctionTargetBuilder(agentStep, parameterName: "message")) + .SendEventTo(new ProcessFunctionTargetBuilder(agentStep)) .SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.ProcessStepFunctions.RenderUserText)); agentStep @@ -178,7 +178,7 @@ private KernelProcess SetupSingleAgentProcess(string processName agentStep .OnFunctionError() - .SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.ProcessStepFunctions.RenderError, "error")) + .SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.ProcessStepFunctions.RenderError)) .StopProcess(); return process.Build(); @@ -215,7 +215,7 @@ private KernelProcess SetupAgentProcess(string processName) wher userInputStep .OnEvent(CommonEvents.UserInputReceived) .SendEventTo(new ProcessFunctionTargetBuilder(managerAgentStep, ManagerAgentStep.ProcessStepFunctions.InvokeAgent)) - .SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.ProcessStepFunctions.RenderUserText, parameterName: "message")); + .SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.ProcessStepFunctions.RenderUserText)); // Process completed userInputStep @@ -226,7 +226,7 @@ private KernelProcess SetupAgentProcess(string processName) wher // Render response from primary agent managerAgentStep .OnEvent(AgentOrchestrationEvents.AgentResponse) - .SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.ProcessStepFunctions.RenderMessage, parameterName: "message")); + .SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.ProcessStepFunctions.RenderMessage)); // Request is complete managerAgentStep @@ -247,17 +247,17 @@ private KernelProcess SetupAgentProcess(string processName) wher // Provide input to inner agents managerAgentStep .OnEvent(AgentOrchestrationEvents.GroupInput) - .SendEventTo(new ProcessFunctionTargetBuilder(agentGroupStep, parameterName: "input")); + .SendEventTo(new ProcessFunctionTargetBuilder(agentGroupStep)); // Render response from inner chat (for visibility) agentGroupStep .OnEvent(AgentOrchestrationEvents.GroupMessage) - .SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.ProcessStepFunctions.RenderInnerMessage, parameterName: "message")); + .SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.ProcessStepFunctions.RenderInnerMessage)); // Provide inner response to primary agent agentGroupStep .OnEvent(AgentOrchestrationEvents.GroupCompleted) - .SendEventTo(new ProcessFunctionTargetBuilder(managerAgentStep, ManagerAgentStep.ProcessStepFunctions.ReceiveResponse, parameterName: "response")); + .SendEventTo(new ProcessFunctionTargetBuilder(managerAgentStep, ManagerAgentStep.ProcessStepFunctions.ReceiveResponse)); KernelProcess kernelProcess = process.Build(); @@ -269,7 +269,7 @@ void AttachErrorStep(ProcessStepBuilder step, params string[] functionNames) { step .OnFunctionError(functionName) - .SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.ProcessStepFunctions.RenderError, "error")) + .SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.ProcessStepFunctions.RenderError)) .StopProcess(); } } diff --git a/dotnet/samples/GettingStartedWithProcesses/Utilities/FileStorageUtilities.cs b/dotnet/samples/GettingStartedWithProcesses/Utilities/FileStorageUtilities.cs new file mode 100644 index 000000000000..768da6b3290c --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Utilities/FileStorageUtilities.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel; + +namespace Utilities; +public static class FileStorageUtilities +{ + // Path used for storing json processes samples in repository + private static readonly string s_currentSourceDir = Path.Combine( + Directory.GetCurrentDirectory(), "..", "..", ".."); + + public static string GetRepositoryProcessStateFilepath(string relativePath, bool checkFilepathExists = false) + { + string fullPath = Path.Combine(s_currentSourceDir, relativePath); + if (checkFilepathExists && !Directory.Exists(fullPath)) + { + throw new KernelException($"Path {fullPath} does not exist"); + } + + return fullPath; + } +} diff --git a/dotnet/src/Experimental/Process.Abstractions/IProcessStepStorageOperations.cs b/dotnet/src/Experimental/Process.Abstractions/IProcessStepStorageOperations.cs new file mode 100644 index 000000000000..3ceec92fc1ab --- /dev/null +++ b/dotnet/src/Experimental/Process.Abstractions/IProcessStepStorageOperations.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Process.Models.Storage; + +namespace Microsoft.SemanticKernel; + +/// +/// Defines operations for managing the storage of process step information, state, and events. +/// +public interface IProcessStepStorageOperations +{ + // For now step data is fetched by parent process only. + // Task FetchStepDataAsync(KernelProcessStepInfo step); + + /// + /// Retrieves detailed information about a specific process step: parentId, version, etc. + /// + /// The step for which information is to be retrieved. This parameter cannot be null. + /// A task that represents the asynchronous operation. The task result contains a + /// object with the step details, or if the step information is not available. + Task GetStepInfoAsync(KernelProcessStepInfo stepInfo); + + /// + /// Saves detailed information about a specific process step, such as parentId, version, etc. + /// + /// + /// + Task SaveStepInfoAsync(KernelProcessStepInfo stepInfo); + + /// + /// Retrieves the current state of a process step. + /// + /// + /// A task that represents the asynchronous operation. The task result contains the current state of the specified + /// step, or if the step does not exist. + Task GetStepStateAsync(KernelProcessStepInfo stepInfo); + + /// + /// Saves the current state of a process step. + /// + /// + /// + Task SaveStepStateAsync(KernelProcessStepInfo stepInfo); + + /// + /// Retrieves the events associated with a specific process step. + /// + /// + /// + Task GetStepEventsAsync(KernelProcessStepInfo stepInfo); + + /// + /// Saves the events associated with a specific process step. + /// + /// + /// + /// + Task SaveStepEventsAsync(KernelProcessStepInfo stepInfo, Dictionary>? edgeGroups = null); + // For now step data can be saved to storage only by parent process only. + // This is to only save data to storage at the end of the process super step execution. + //Task SaveStepDataToStorageAsync(KernelProcessStepInfo step); +} diff --git a/dotnet/src/Experimental/Process.Abstractions/IProcessStorageConnector.cs b/dotnet/src/Experimental/Process.Abstractions/IProcessStorageConnector.cs new file mode 100644 index 000000000000..4ec030ccc845 --- /dev/null +++ b/dotnet/src/Experimental/Process.Abstractions/IProcessStorageConnector.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; + +namespace Microsoft.SemanticKernel; + +/// +/// An interface that provides a channel interacting with custom storage +/// +public interface IProcessStorageConnector +{ + /// + /// Logic executed when creating a new storage connection + /// + /// A + abstract ValueTask OpenConnectionAsync(); + + /// + /// Logic executed when closing opened storage connection + /// + /// A + abstract ValueTask CloseConnectionAsync(); + + /// + /// Get specific entry type by id + /// + /// class that defines the entry type to be extracted from storage + /// id of entry used storage + /// + abstract Task GetEntryAsync(string id) where TEntry : class; + + /// + /// Save specific entry type with assigned id + /// + /// + /// id of entry used storage + /// type of entry used in storage + /// data to be stored in storage + /// + abstract Task SaveEntryAsync(string id, string type, TEntry entry) where TEntry : class; + + /// + /// Delete specific entry from storage + /// + /// id of entry used storage + /// + abstract Task DeleteEntryAsync(string id); +} diff --git a/dotnet/src/Experimental/Process.Abstractions/IProcessStorageOperations.cs b/dotnet/src/Experimental/Process.Abstractions/IProcessStorageOperations.cs new file mode 100644 index 000000000000..32c3efd6455e --- /dev/null +++ b/dotnet/src/Experimental/Process.Abstractions/IProcessStorageOperations.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Process.Models.Storage; + +namespace Microsoft.SemanticKernel; + +internal interface IProcessStorageOperations +{ + /// + /// Initializes the storage connection to be used by the process. + /// + /// + Task InitializeAsync(); + /// + /// Closes storage connection and cleans up resources. + /// + /// + Task CloseAsync(); + /// + /// Fetches from storage the process data for a specific process. + /// + /// + /// + Task FetchProcessDataAsync(KernelProcess process); + /// + /// Get the process information already retrieved from storage, including parentId, version, mapping of steps and running ids. + /// + /// + /// + Task GetProcessInfoAsync(KernelProcess process); + /// + /// Saves the process information to storage, including parentId, version, mapping of steps and running ids. + /// + /// + /// + Task SaveProcessInfoAsync(KernelProcess process); + + /// + /// Retrieves a list of external events associated with the specified kernel process. + /// + /// + /// + Task?> GetProcessExternalEventsAsync(KernelProcess process); + + /// + /// Save process events to storage, including pending external events. + /// + /// + /// + /// + Task SaveProcessEventsAsync(KernelProcess process, List? pendingExternalEvents = null); + + /// + /// Retrieve process shared variables + /// + /// + /// + Task?> GetProcessStateVariablesAsync(KernelProcess process); + + /// + /// Save process state related components + /// + /// + /// + /// + Task SaveProcessStateAsync(KernelProcess process, Dictionary sharedVariables); + + /// + /// Saves all process related data to storage, including process info, events, and step data. + /// + /// + /// + Task SaveProcessDataToStorageAsync(KernelProcess process); + + // Step related operations to be applied to process children steps only + + /// + /// Fetches from storage the step data for a specific process step. + /// + /// + /// + Task FetchStepDataAsync(KernelProcessStepInfo stepInfo); + + /// + /// Saves the step data to storage. + /// + /// + /// + Task SaveStepDataToStorageAsync(KernelProcessStepInfo stepInfo); +} diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcess.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcess.cs index d35c29ad6a78..355765de5c32 100644 --- a/dotnet/src/Experimental/Process.Abstractions/KernelProcess.cs +++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcess.cs @@ -15,7 +15,7 @@ public sealed record KernelProcess : KernelProcessStepInfo /// /// The collection of Steps in the Process. /// - public IList Steps { get; } + public IList Steps { get; init; } /// /// The collection of Threads in the Process. @@ -47,7 +47,7 @@ public KernelProcess(KernelProcessState state, IList step : base(typeof(KernelProcess), state, edges ?? []) { Verify.NotNull(steps); - Verify.NotNullOrWhiteSpace(state.Name); + Verify.NotNullOrWhiteSpace(state.StepId); this.Steps = [.. steps]; } diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessMap.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessMap.cs index f171b9527c77..58f009ba67fa 100644 --- a/dotnet/src/Experimental/Process.Abstractions/KernelProcessMap.cs +++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessMap.cs @@ -11,7 +11,7 @@ public sealed record KernelProcessMap : KernelProcessStepInfo /// /// The map operation. /// - public KernelProcessStepInfo Operation { get; } + public KernelProcessStepInfo Operation { get; init; } /// /// Creates a new instance of the class. @@ -23,8 +23,8 @@ public KernelProcessMap(KernelProcessMapState state, KernelProcessStepInfo opera : base(typeof(KernelProcessMap), state, edges) { Verify.NotNull(operation, nameof(operation)); - Verify.NotNullOrWhiteSpace(state.Name, $"{nameof(state)}.{nameof(KernelProcessMapState.Name)}"); - Verify.NotNullOrWhiteSpace(state.Id, $"{nameof(state)}.{nameof(KernelProcessMapState.Id)}"); + Verify.NotNullOrWhiteSpace(state.StepId, $"{nameof(state)}.{nameof(KernelProcessMapState.StepId)}"); + Verify.NotNullOrWhiteSpace(state.RunId, $"{nameof(state)}.{nameof(KernelProcessMapState.RunId)}"); this.Operation = operation; } diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessProxy.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessProxy.cs index de0cac9b221b..ec9ba3c9aabd 100644 --- a/dotnet/src/Experimental/Process.Abstractions/KernelProcessProxy.cs +++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessProxy.cs @@ -23,7 +23,7 @@ public sealed record KernelProcessProxy : KernelProcessStepInfo public KernelProcessProxy(KernelProcessStepState state, Dictionary> edges) : base(typeof(KernelProxyStep), state, edges) { - Verify.NotNullOrWhiteSpace(state.Name, $"{nameof(state)}.{nameof(KernelProcessStepState.Name)}"); - Verify.NotNullOrWhiteSpace(state.Id, $"{nameof(state)}.{nameof(KernelProcessStepState.Id)}"); + Verify.NotNullOrWhiteSpace(state.StepId, $"{nameof(state)}.{nameof(KernelProcessStepState.StepId)}"); + Verify.NotNullOrWhiteSpace(state.RunId, $"{nameof(state)}.{nameof(KernelProcessStepState.RunId)}"); } } diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessStep.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessStep.cs index c3162340bb35..81d434c92c84 100644 --- a/dotnet/src/Experimental/Process.Abstractions/KernelProcessStep.cs +++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessStep.cs @@ -9,6 +9,11 @@ namespace Microsoft.SemanticKernel; /// public class KernelProcessStep { + /// + /// Name of the step given by the StepBuilder id. + /// + public string? StepName { get; init; } + /// public virtual ValueTask ActivateAsync(KernelProcessStepState state) { diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepInfo.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepInfo.cs index 92f5016b2e46..317fd1741ff5 100644 --- a/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepInfo.cs +++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepInfo.cs @@ -31,6 +31,15 @@ public KernelProcessStepState State } } + /// + public string? RunId => this.State.RunId; + + /// + public string? StepId => this.State.StepId; + + /// + public string? ParentId => this.State.ParentId; + /// /// The semantic description of the Step. This is intended to be human and AI readable and is not required to be unique. /// @@ -39,7 +48,7 @@ public KernelProcessStepState State /// /// A read-only dictionary of output edges from the Step. /// - public IReadOnlyDictionary> Edges { get; } + public IReadOnlyDictionary> Edges { get; init; } /// /// A dictionary of input mappings for the grouped edges. diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepState.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepState.cs index e4e2b816cb8c..066f8c736b69 100644 --- a/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepState.cs +++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepState.cs @@ -4,6 +4,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Runtime.Serialization; +using System.Text.Json.Serialization; namespace Microsoft.SemanticKernel; @@ -40,34 +41,44 @@ internal static void RegisterDerivedType(Type derivedType) /// This may be null until a process containing this step has been invoked. /// [DataMember] - public string? Id { get; init; } + [JsonPropertyName("runId")] + public string? RunId { get; set; } + + /// + /// Gets or sets the identifier of the step parent. + /// + [DataMember] + [JsonPropertyName("parentId")] + public string? ParentId { get; set; } /// /// The name of the Step. This is intended to be human readable and is not required to be unique. If /// not provided, the name will be derived from the steps .NET type. /// [DataMember] - public string Name { get; init; } + [JsonPropertyName("stepId")] + public string StepId { get; init; } /// /// Version of the state /// [DataMember] + [JsonPropertyName("version")] public string Version { get; init; } /// /// Initializes a new instance of the class. /// - /// The name of the associated + /// The name of the associated /// version id of the process step state - /// The Id of the associated - public KernelProcessStepState(string name, string version, string? id = null) + /// The Id of the associated running instance + public KernelProcessStepState(string stepId, string version, string? runId = null) { - Verify.NotNullOrWhiteSpace(name, nameof(name)); + Verify.NotNullOrWhiteSpace(stepId, nameof(stepId)); Verify.NotNullOrWhiteSpace(version, nameof(version)); - this.Id = id; - this.Name = name; + this.RunId = runId; + this.StepId = stepId; this.Version = version; } } @@ -88,16 +99,25 @@ public KernelProcessStepState(string name, string version, string? id = null) /// /// Initializes a new instance of the class. /// - /// The name of the associated + /// The name of the associated /// version id of the process step state - /// The Id of the associated - public KernelProcessStepState(string name, string version, string? id = null) - : base(name, version, id) + /// The Id of the associated + public KernelProcessStepState(string stepId, string version, string? runId = null) + : base(stepId, version, runId) { - Verify.NotNullOrWhiteSpace(name); + Verify.NotNullOrWhiteSpace(stepId); - this.Id = id; - this.Name = name; + this.RunId = runId; + this.StepId = stepId; this.Version = version; } + + /// + /// Initializes a new instance of the class. + /// + /// + public KernelProcessStepState(KernelProcessStepState stepState) + : base(stepState.StepId, stepState.Version, stepState.RunId) + { + } } diff --git a/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageEntryBase.cs b/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageEntryBase.cs new file mode 100644 index 000000000000..02514fe198b5 --- /dev/null +++ b/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageEntryBase.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.Process.Models.Storage; + +/// +/// Base class for storage entries. +/// +public abstract record StorageEntryBase +{ + /// + /// Unique identifier of the storage entry. + /// + [JsonPropertyName("instanceId")] + public string InstanceId { get; set; } = string.Empty; +} diff --git a/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageProcessData.cs b/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageProcessData.cs new file mode 100644 index 000000000000..5c30d216a29c --- /dev/null +++ b/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageProcessData.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.Process.Models.Storage; + +// it seems all properties needed are already in KernelProcessStepStateMetadata +// using new class for now in case there some extra props needed while +// plumbing +/// +/// Data class for the parent of a step. +/// +public record StorageProcessData : StorageEntryBase +{ + /// + /// Process runtime details like id, parent id, step id mapping, etc. + /// + [JsonPropertyName("processInfo")] + public StorageProcessInfo ProcessInfo { get; set; } = new(); + + /// + /// Process runtime unprocessed events + /// + [JsonPropertyName("processEvents")] + public StorageProcessEvents ProcessEvents { get; set; } = new(); + + /// + /// Process state/variables related data + /// + [JsonPropertyName("processState")] + public StorageProcessState ProcessState { get; set; } = new(); +} diff --git a/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageProcessEvents.cs b/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageProcessEvents.cs new file mode 100644 index 000000000000..a4f4846a0b85 --- /dev/null +++ b/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageProcessEvents.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.Process.Models.Storage; + +/// +/// Storage representation of a process state. +/// +public record StorageProcessEvents +{ + /// + /// Gets or sets the list of external pending messages associated with the process. + /// + [JsonPropertyName("externalPendingMessages")] + public List ExternalPendingMessages { get; set; } = []; + + /* TODO: + * Hypothetically step edgeGroup pending messages logic could be moved to the process and save it here + * All "pending messages" that cannot be processed by a step yet (waiting on some condition) + * could be moved to the process layer instead. + */ + //[JsonPropertyName("internalPendingMessages")] + //public List InternalProcessEvents { get; set; } = []; +} diff --git a/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageProcessExtensions.cs b/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageProcessExtensions.cs new file mode 100644 index 000000000000..3338107f0675 --- /dev/null +++ b/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageProcessExtensions.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.SemanticKernel.Process.Models.Storage; +/// +/// Extension methods for converting between StorageProcessState and KernelProcess. +/// +public static class StorageProcessExtension +{ + /// + /// Converts a to a . + /// + /// instance of + /// + public static StorageProcessInfo ToKernelStorageProcessInfo(this KernelProcess kernelProcess) + { + return new StorageProcessInfo + { + ProcessName = kernelProcess.StepId ?? string.Empty, + InstanceId = kernelProcess.RunId ?? string.Empty, + Steps = kernelProcess.Steps.ToList().ToDictionary(step => step.State.StepId, step => step.State.RunId ?? string.Empty) ?? [], + ParentId = kernelProcess.ParentId, + }; + } + + /// + /// Converts a to a . + /// + /// list of ending external events to be processed + /// + public static StorageProcessEvents ToKernelStorageProcessEvents(List? pendingExternalEvents = null) + { + return new StorageProcessEvents + { + ExternalPendingMessages = pendingExternalEvents ?? new List(), + }; + } + + /// + /// Converts a to a dictionary of shared variables for the kernel process. + /// + /// + /// + public static Dictionary ToKernelProcessSharedVariables(this StorageProcessState storageProcessState) + { + return storageProcessState.SharedVariables.ToDictionary( + kvp => kvp.Key, + kvp => kvp.Value?.ToObject()); + } + + /// + /// Converts a to a . + /// + /// + /// + public static StorageProcessState ToKernelStorageProcessState(Dictionary? processSharedVariables = null) + { + return new StorageProcessState + { + SharedVariables = processSharedVariables?.ToDictionary( + kvp => kvp.Key, + kvp => KernelProcessEventData.FromObject(kvp.Value)) ?? [] + }; + } +} diff --git a/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageProcessInfo.cs b/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageProcessInfo.cs new file mode 100644 index 000000000000..13d6672703e0 --- /dev/null +++ b/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageProcessInfo.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.Process.Models.Storage; + +/// +/// Storage representation of a process state. +/// +public record StorageProcessInfo : StorageEntryBase +{ + /// + /// Name of the process builder used to create the process instance + /// + [JsonPropertyName("processName")] + public string ProcessName { get; set; } = string.Empty; + + /// + /// Id of the parent process, if null the process is a root process. + /// + [JsonPropertyName("parentId")] + public string? ParentId { get; set; } = null; + + /// + /// Map containing Step Names and their respective step instance + /// + [JsonPropertyName("steps")] + public Dictionary Steps { get; set; } = []; + + // TODO: Add running state here: RUNNING, COMPLETED (EndStep was reached), IDLE (it ran and no more events are in queue to be processed) +} diff --git a/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageProcessState.cs b/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageProcessState.cs new file mode 100644 index 000000000000..a69d2b0ccd65 --- /dev/null +++ b/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageProcessState.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.Process.Models.Storage; + +/// +/// Storage representation of a process state. +/// +public record StorageProcessState +{ + // Properties here should match properties used by LocalUserStateStore + + /// + /// Collection of shared variables used by the process and process steps. + /// Saving values as KernelProcessEventData to allow serialization and deserialization of custom objects. + /// + [JsonPropertyName("sharedVariables")] + public Dictionary SharedVariables { get; init; } = []; +} diff --git a/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageStepData.cs b/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageStepData.cs new file mode 100644 index 000000000000..b7151add2c80 --- /dev/null +++ b/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageStepData.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.Process.Models.Storage; + +/// +/// Data class for the parent of a step. +/// +public record StorageStepData : StorageEntryBase +{ + /// + /// Process runtime details like id, parent id, step id mapping, etc. + /// + [JsonPropertyName("stepInfo")] + public StorageStepInfo? StepInfo { get; set; } = null; + + /// + /// Process runtime unprocessed events + /// + [JsonPropertyName("stepEvents")] + public StorageStepEvents? StepEvents { get; set; } = null; + + /// + /// Step state if any. + /// + [JsonPropertyName("stepState")] + public StorageStepState? StepState { get; set; } = null; +} diff --git a/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageStepEdgesData.cs b/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageStepEdgesData.cs new file mode 100644 index 000000000000..7a4aae72b81f --- /dev/null +++ b/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageStepEdgesData.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.Process.Models.Storage; + +/// +/// Storage representation of step edges data. +/// +public record StorageStepEdgesData +{ + // TODO: potentially also add versioning/snapshot info to allow "going back in time" + + /// + /// Data received by step edges + /// + [DataMember] + [JsonPropertyName("edgesData")] + public Dictionary> EdgesData { get; set; } = []; + + /// + /// Indicates if the edge is a group edge + /// + [DataMember] + [JsonPropertyName("isGroupEdge")] + public bool IsGroupEdge { get; set; } = false; +} diff --git a/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageStepEvents.cs b/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageStepEvents.cs new file mode 100644 index 000000000000..d634ac206f6e --- /dev/null +++ b/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageStepEvents.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.Process.Models.Storage; + +/// +/// Storage representation of step edges data. +/// +public record StorageStepEvents +{ + /// + /// Data received by step edges, used when step function has multiple parameters + /// + [JsonPropertyName("edgeGroupEvents")] + public Dictionary>? EdgesData { get; set; } = null; + + //[JsonPropertyName("lastEventReceived")] +} diff --git a/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageStepExtensions.cs b/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageStepExtensions.cs new file mode 100644 index 000000000000..cc8b86b1f88a --- /dev/null +++ b/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageStepExtensions.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using Microsoft.SemanticKernel.Process.Internal; + +namespace Microsoft.SemanticKernel.Process.Models.Storage; +/// +/// Extension methods for converting between StorageStepState and KernelProcessStepStateMetadata. +/// +public static class StorageStepExtensions +{ + /// + /// Converts a KernelProcessStepInfo to a StorageStepInfo. + /// + /// + /// + public static StorageStepInfo ToStorageStepInfo(this KernelProcessStepInfo step) + { + return new StorageStepInfo + { + StepName = step.StepId!, + InstanceId = step.RunId!, + ParentId = step.ParentId!, + // There should be a distinction between stepName version and stepState version + // A stepName could be compatible with a previous stepState version + Version = step.State.Version, + }; + } + + /// + /// Converts a StorageStepData to a KernelProcessStepState. + /// + /// + /// + public static KernelProcessStepState? ToKernelProcessStepState(this StorageStepData storageData) + { + var stepState = new KernelProcessStepState(stepId: storageData.StepInfo!.StepName, version: storageData.StepInfo.Version, runId: storageData.InstanceId) + { + ParentId = storageData.StepInfo.ParentId, + }; + + if (storageData.StepState != null && !string.IsNullOrEmpty(storageData.StepState.StateType)) + { + var userStateType = Type.GetType(storageData.StepState.StateType); + if (userStateType != null && storageData.StepState.State != null) + { + var stateType = typeof(KernelProcessStepState<>).MakeGenericType(userStateType); + stepState = (KernelProcessStepState?)Activator.CreateInstance(stateType, stepState); + stateType.GetProperty(nameof(KernelProcessStepState.State))?.SetValue(stepState, storageData.StepState.State.ToObject()); + } + } + + return stepState; + } + + /// + /// Converts a KernelProcessStepInfo to a StorageStepState. + /// + /// + /// + public static StorageStepState? ToStorageStepState(this KernelProcessStepInfo step) + { + object? stepState = null; + var stateType = step.State.GetType(); + if (stateType.IsGenericType) + { + // it is a step with a custom state + stepState = stateType.GetProperty(nameof(KernelProcessStepState.State))?.GetValue(step.State); + + return new StorageStepState + { + State = KernelProcessEventData.FromObject(stepState), + StateType = stateType.GetGenericArguments()[0].AssemblyQualifiedName! + }; + } + + return null; + } + + /// + /// Converts edge group data to a StorageStepEvents. + /// + /// + /// + public static StorageStepEvents ToStorageStepEvents(Dictionary?>? edgeGroups = null) + { + return new StorageStepEvents + { + EdgesData = edgeGroups?.PackStepEdgesValues() + }; + } +} diff --git a/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageStepInfo.cs b/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageStepInfo.cs new file mode 100644 index 000000000000..5c45e191fcd3 --- /dev/null +++ b/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageStepInfo.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.Process.Models.Storage; + +/// +/// Storage representation of a process state. +/// +public record StorageStepInfo : StorageEntryBase +{ + /// + /// Name of the process builder used to create the process instance + /// + [JsonPropertyName("stepName")] + public string StepName { get; set; } = string.Empty; + + /// + /// Id of the step parent process + /// + [JsonPropertyName("parentId")] + public string ParentId { get; set; } = string.Empty; + + /// + /// version of step + /// + [JsonPropertyName("version")] + public string Version { get; set; } = string.Empty; +} diff --git a/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageStepState.cs b/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageStepState.cs new file mode 100644 index 000000000000..c1d08b5f5423 --- /dev/null +++ b/dotnet/src/Experimental/Process.Abstractions/Models/Storage/StorageStepState.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.Process.Models.Storage; + +/// +/// Storage representation of a step state. +/// +public record StorageStepState +{ + /// + /// Custom step type assembly name + /// + [JsonPropertyName("stateType")] + public string StateType { get; init; } = string.Empty; + + // Not needed if using KernelProcessEventData + ///// + ///// Version of the state that is stored. Used for validation and versioning + ///// purposes when reading a state and applying it to a ProcessStepBuilder/ProcessBuilder + ///// + //[JsonPropertyName("versionInfo")] + //public string VersionInfo { get; init; } = string.Empty; + + /// + /// The user-defined state object associated with the Step (if the step is stateful) -> Original object comes from StepStateMetadata + /// + [JsonPropertyName("state")] + public KernelProcessEventData? State { get; set; } = null; +} diff --git a/dotnet/src/Experimental/Process.Abstractions/ProcessStorageManager.cs b/dotnet/src/Experimental/Process.Abstractions/ProcessStorageManager.cs new file mode 100644 index 000000000000..ba35b430f5e6 --- /dev/null +++ b/dotnet/src/Experimental/Process.Abstractions/ProcessStorageManager.cs @@ -0,0 +1,340 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Process.Models.Storage; + +namespace Microsoft.SemanticKernel.Process; + +/// +/// Storage manager for storing step and process related data using the implementation of . +/// +public class ProcessStorageManager : IProcessStepStorageOperations, IProcessStorageOperations +{ + internal static class StorageKeywords + { + // Types + /// + /// To be used for storing process children data, parent info and external events + /// + public const string ProcessDetails = nameof(ProcessDetails); + + public const string StepDetails = nameof(StepDetails); + } + + private readonly IProcessStorageConnector _storageConnector; + + private bool _isInitialized = false; + + private readonly ConcurrentDictionary _softSaveStorage = []; + + /// + /// Constructor for the class. + /// + /// + public ProcessStorageManager(IProcessStorageConnector storageConnector) + { + this._storageConnector = storageConnector; + } + + /// + /// Initialize the storage connection. + /// + /// + public async Task InitializeAsync() + { + if (this._isInitialized) + { + return true; + } + await this._storageConnector.OpenConnectionAsync().ConfigureAwait(false); + this._isInitialized = true; + + return this._isInitialized; + } + + /// + /// Close the storage connection. + /// + /// + public async Task CloseAsync() + { + if (!this._isInitialized) + { + return true; + } + await this._storageConnector.CloseConnectionAsync().ConfigureAwait(false); + this._isInitialized = false; + + return true; + } + + private string GetEntryId(string componentName, string componentId) + { + return $"{componentId}.{componentName}"; + } + + #region Process related methods + private string GetProcessEntryId(KernelProcess process) + { + Verify.NotNullOrWhiteSpace(process.StepId); + Verify.NotNullOrWhiteSpace(process.RunId); + + return $"{this.GetEntryId(process.StepId, process.RunId)}.{StorageKeywords.ProcessDetails}"; + //return process.RunId; + } + + private string GetStepEntryId(KernelProcessStepInfo stepInfo) + { + Verify.NotNullOrWhiteSpace(stepInfo.StepId); + Verify.NotNullOrWhiteSpace(stepInfo.RunId); + + return $"{this.GetEntryId(stepInfo.StepId, stepInfo.RunId)}.{StorageKeywords.StepDetails}"; + //return step.RunId; + } + + /// + public async Task FetchProcessDataAsync(KernelProcess process) + { + var entryId = this.GetProcessEntryId(process); + + var storageState = await this._storageConnector.GetEntryAsync(entryId).ConfigureAwait(false); + if (storageState != null) + { + this._softSaveStorage[entryId] = storageState; + } + } + + /// + public Task GetProcessInfoAsync(KernelProcess process) + { + var entryId = this.GetProcessEntryId(process); + if (this._softSaveStorage.TryGetValue(entryId, out var softSaveProcessData) && softSaveProcessData is StorageProcessData processData) + { + return Task.FromResult(processData.ProcessInfo); + } + return Task.FromResult(null); + } + + /// + public Task SaveProcessInfoAsync(KernelProcess process) + { + Verify.NotNullOrWhiteSpace(process.RunId); + + var entryId = this.GetProcessEntryId(process); + if (!this._softSaveStorage.TryGetValue(entryId, out var processSavedData)) + { + processSavedData = new StorageProcessData() { InstanceId = process.RunId }; + this._softSaveStorage.TryAdd(entryId, processSavedData); + } + + if (processSavedData is StorageProcessData processData) + { + processData.ProcessInfo = process.ToKernelStorageProcessInfo(); + } + + return Task.FromResult(true); + } + + /// + public Task?> GetProcessExternalEventsAsync(KernelProcess process) + { + var entryId = this.GetProcessEntryId(process); + if (this._softSaveStorage.TryGetValue(entryId, out var softSaveProcessData) && softSaveProcessData is StorageProcessData processData) + { + return Task.FromResult?>(processData.ProcessEvents?.ExternalPendingMessages); + } + + return Task.FromResult?>(null); + } + + /// + public Task SaveProcessEventsAsync(KernelProcess process, List? pendingExternalEvents = null) + { + Verify.NotNullOrWhiteSpace(process.RunId); + + var entryId = this.GetProcessEntryId(process); + if (!this._softSaveStorage.TryGetValue(entryId, out var processSavedData)) + { + processSavedData = new StorageProcessData() { InstanceId = process.RunId }; + this._softSaveStorage.TryAdd(entryId, processSavedData); + } + + if (processSavedData is StorageProcessData processData) + { + processData.ProcessEvents = StorageProcessExtension.ToKernelStorageProcessEvents(pendingExternalEvents); + } + + return Task.FromResult(true); + } + + /// + public Task?> GetProcessStateVariablesAsync(KernelProcess process) + { + var entryId = this.GetProcessEntryId(process); + if (this._softSaveStorage.TryGetValue(entryId, out var softSaveStepData) && softSaveStepData is StorageProcessData processData) + { + var processState = processData.ProcessState.ToKernelProcessSharedVariables(); + return Task.FromResult?>(processState); + } + + return Task.FromResult?>(null); + } + + /// + public Task SaveProcessStateAsync(KernelProcess process, Dictionary sharedVariables) + { + Verify.NotNullOrWhiteSpace(process.RunId); + + var entryId = this.GetProcessEntryId(process); + + if (!this._softSaveStorage.TryGetValue(entryId, out var processSavedData)) + { + processSavedData = new StorageProcessData() { InstanceId = process.RunId }; + this._softSaveStorage.TryAdd(entryId, processSavedData); + } + + if (processSavedData is StorageProcessData processData) + { + processData.ProcessState = StorageProcessExtension.ToKernelStorageProcessState(sharedVariables); + } + + return Task.FromResult(true); + } + + /// + public async Task SaveProcessDataToStorageAsync(KernelProcess process) + { + var entryId = this.GetProcessEntryId(process); + if (this._softSaveStorage.TryGetValue(entryId, out var softSaveProcessData) && softSaveProcessData is StorageProcessData processData) + { + // for now process only has one entry - in the future the process state may be saved in a separate entity -> 2 storage calls + return await this._storageConnector.SaveEntryAsync(entryId, StorageKeywords.ProcessDetails, processData).ConfigureAwait(false); + } + + return false; + } + + #endregion + #region Step related methods + + /// + public async Task FetchStepDataAsync(KernelProcessStepInfo stepInfo) + { + var entryId = this.GetStepEntryId(stepInfo); + var storageState = await this._storageConnector.GetEntryAsync(entryId).ConfigureAwait(false); + + if (storageState != null) + { + this._softSaveStorage[entryId] = storageState; + } + } + + /// + public Task GetStepInfoAsync(KernelProcessStepInfo stepInfo) + { + var entryId = this.GetStepEntryId(stepInfo); + if (this._softSaveStorage.TryGetValue(entryId, out var softSaveStepData) && softSaveStepData is StorageStepData stepData) + { + return Task.FromResult(stepData.StepInfo); + } + return Task.FromResult(null); + } + + /// + public Task SaveStepInfoAsync(KernelProcessStepInfo stepInfo) + { + Verify.NotNullOrWhiteSpace(stepInfo.RunId); + + var entryId = this.GetStepEntryId(stepInfo); + if (!this._softSaveStorage.TryGetValue(entryId, out var stepSavedData)) + { + stepSavedData = new StorageStepData() { InstanceId = stepInfo.RunId }; + this._softSaveStorage.TryAdd(entryId, stepSavedData); + } + + if (stepSavedData is StorageStepData stepData) + { + stepData.StepInfo = stepInfo.ToStorageStepInfo(); + } + + return Task.FromResult(true); + } + + /// + public Task GetStepStateAsync(KernelProcessStepInfo stepInfo) + { + var entryId = this.GetStepEntryId(stepInfo); + if (this._softSaveStorage.TryGetValue(entryId, out var softSaveStepData) && softSaveStepData is StorageStepData stepData) + { + return Task.FromResult(stepData.ToKernelProcessStepState()); + } + + return Task.FromResult(null); + } + + /// + public Task SaveStepStateAsync(KernelProcessStepInfo stepInfo) + { + Verify.NotNullOrWhiteSpace(stepInfo.RunId); + + var entryId = this.GetStepEntryId(stepInfo); + + if (!this._softSaveStorage.TryGetValue(entryId, out var stepSavedData)) + { + stepSavedData = new StorageStepData() { InstanceId = stepInfo.RunId }; + this._softSaveStorage.TryAdd(entryId, stepSavedData); + } + + if (stepSavedData is StorageStepData stepData) + { + stepData.StepState = stepInfo.ToStorageStepState(); + } + + return Task.FromResult(true); + } + + /// + public Task GetStepEventsAsync(KernelProcessStepInfo stepInfo) + { + var entryId = this.GetStepEntryId(stepInfo); + if (this._softSaveStorage.TryGetValue(entryId, out var softSaveStepData) && softSaveStepData is StorageStepData stepData) + { + return Task.FromResult(stepData.StepEvents); + } + + return Task.FromResult(null); + } + + /// + public Task SaveStepEventsAsync(KernelProcessStepInfo stepInfo, Dictionary>? edgeGroups = null) + { + Verify.NotNullOrWhiteSpace(stepInfo.RunId); + var entryId = this.GetStepEntryId(stepInfo); + + if (!this._softSaveStorage.TryGetValue(entryId, out var stepSavedData)) + { + stepSavedData = new StorageStepData() { InstanceId = stepInfo.RunId }; + this._softSaveStorage.TryAdd(entryId, stepSavedData); + } + if (stepSavedData is StorageStepData stepData && edgeGroups != null) + { + stepData.StepEvents = StorageStepExtensions.ToStorageStepEvents(edgeGroups!); + } + return Task.FromResult(true); + } + + /// + public async Task SaveStepDataToStorageAsync(KernelProcessStepInfo stepInfo) + { + var entryId = this.GetStepEntryId(stepInfo); + if (this._softSaveStorage.TryGetValue(entryId, out var softSaveStepData) && softSaveStepData is StorageStepData stepData) + { + return await this._storageConnector.SaveEntryAsync(entryId, StorageKeywords.StepDetails, stepData).ConfigureAwait(false); + } + + return false; + } + #endregion +} diff --git a/dotnet/src/Experimental/Process.Core/Internal/EndStep.cs b/dotnet/src/Experimental/Process.Core/Internal/EndStep.cs index 7e11c8247800..72627b5cafe7 100644 --- a/dotnet/src/Experimental/Process.Core/Internal/EndStep.cs +++ b/dotnet/src/Experimental/Process.Core/Internal/EndStep.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using Microsoft.SemanticKernel.Process.Internal; -using Microsoft.SemanticKernel.Process.Models; namespace Microsoft.SemanticKernel; @@ -30,7 +29,7 @@ internal override Dictionary GetFunctionMetadata return []; } - internal override KernelProcessStepInfo BuildStep(ProcessBuilder processBuilder, KernelProcessStepStateMetadata? stateMetadata = null) + internal override KernelProcessStepInfo BuildStep(ProcessBuilder processBuilder) { // The end step has no state. return new KernelProcessStepInfo(typeof(KernelProcessStepState), new KernelProcessStepState(ProcessConstants.EndStepName, version: ProcessConstants.InternalStepsVersion), []); diff --git a/dotnet/src/Experimental/Process.Core/Internal/KernelProcessStateMetadataExtension.cs b/dotnet/src/Experimental/Process.Core/Internal/KernelProcessStateMetadataExtension.cs index 59c66ac9d0e8..cb1c5805b5b0 100644 --- a/dotnet/src/Experimental/Process.Core/Internal/KernelProcessStateMetadataExtension.cs +++ b/dotnet/src/Experimental/Process.Core/Internal/KernelProcessStateMetadataExtension.cs @@ -1,105 +1,20 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using System.Linq; -using Microsoft.SemanticKernel.Process.Models; namespace Microsoft.SemanticKernel.Process.Internal; internal static class KernelProcessStateMetadataExtension { - public static List BuildWithStateMetadata(this ProcessBuilder processBuilder, KernelProcessStateMetadata? stateMetadata) + public static List BuildWithChildren(this ProcessBuilder processBuilder) { List builtSteps = []; - // 1- Validate StateMetadata: Migrate previous state versions if needed + sanitize state - KernelProcessStateMetadata? sanitizedMetadata = null; - if (stateMetadata != null) - { - sanitizedMetadata = SanitizeProcessStateMetadata(processBuilder, stateMetadata, processBuilder.Steps); - } - // 2- Build steps info with validated stateMetadata foreach (ProcessStepBuilder step in processBuilder.Steps) { - if (sanitizedMetadata != null && sanitizedMetadata.StepsState != null && sanitizedMetadata.StepsState.TryGetValue(step.Name, out var stepStateObject) && stepStateObject != null) - { - builtSteps.Add(step.BuildStep(processBuilder, stepStateObject)); - continue; - } - builtSteps.Add(step.BuildStep(processBuilder)); } return builtSteps; } - - private static KernelProcessStateMetadata SanitizeProcessStateMetadata(ProcessBuilder processBuilder, KernelProcessStateMetadata stateMetadata, IReadOnlyList stepBuilders) - { - KernelProcessStateMetadata sanitizedStateMetadata = stateMetadata; - foreach (ProcessStepBuilder step in stepBuilders) - { - // 1- find matching key name with exact match or by alias match - string? stepKey = null; - - if (sanitizedStateMetadata.StepsState != null && sanitizedStateMetadata.StepsState.ContainsKey(step.Name)) - { - stepKey = step.Name; - } - else - { - stepKey = step.Aliases - .Where(alias => sanitizedStateMetadata.StepsState != null && sanitizedStateMetadata.StepsState.ContainsKey(alias)) - .FirstOrDefault(); - } - - // 2- stepKey match found - if (stepKey != null) - { - var currentVersionStateMetadata = step.BuildStep(processBuilder).ToProcessStateMetadata(); - if (sanitizedStateMetadata.StepsState!.TryGetValue(stepKey, out var savedStateMetadata)) - { - if (stepKey != step.Name) - { - if (savedStateMetadata.VersionInfo == currentVersionStateMetadata.VersionInfo) - { - // key mismatch only, but same version - sanitizedStateMetadata.StepsState[step.Name] = savedStateMetadata; - // TODO: Should there be state formatting check too? - } - else - { - // version mismatch - check if migration logic in place - if (step is ProcessBuilder subprocessBuilder) - { - KernelProcessStateMetadata sanitizedStepState = SanitizeProcessStateMetadata(processBuilder, (KernelProcessStateMetadata)savedStateMetadata, subprocessBuilder.Steps); - sanitizedStateMetadata.StepsState[step.Name] = sanitizedStepState; - } - else if (step is ProcessMapBuilder mapBuilder) - { - KernelProcessStateMetadata sanitizedStepState = SanitizeProcessStateMetadata(processBuilder, (KernelProcessStateMetadata)savedStateMetadata, [mapBuilder.MapOperation]); - sanitizedStateMetadata.StepsState[step.Name] = sanitizedStepState; - } - else if (false) - { - // TODO: Improvements for support on advance versioning scenarios process M:N steps differences https://github.com/microsoft/semantic-kernel/issues/9555 - } - else - { - // no compatible state found, migrating id only - sanitizedStateMetadata.StepsState[step.Name] = new KernelProcessStepStateMetadata() - { - Name = step.Name, - Id = step.Id, - }; - } - } - sanitizedStateMetadata.StepsState[step.Name].Name = step.Name; - sanitizedStateMetadata.StepsState.Remove(stepKey); - } - } - } - } - - return sanitizedStateMetadata; - } } diff --git a/dotnet/src/Experimental/Process.Core/ListenForBuilder.cs b/dotnet/src/Experimental/Process.Core/ListenForBuilder.cs index 6694701f18c7..f8cd2aa8c319 100644 --- a/dotnet/src/Experimental/Process.Core/ListenForBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/ListenForBuilder.cs @@ -84,7 +84,7 @@ public ListenForTargetBuilder AllOf(List messageSources) private string GetGroupId(List messageSources) { var sortedKeys = messageSources - .Select(source => $"{source.Source.Id}.{source.MessageType}") + .Select(source => $"{source.Source.StepId}.{source.MessageType}") .OrderBy(id => id, StringComparer.OrdinalIgnoreCase) .ToList(); diff --git a/dotnet/src/Experimental/Process.Core/ListenForTargetBuilder.cs b/dotnet/src/Experimental/Process.Core/ListenForTargetBuilder.cs index afd99ea21456..df9570fbb01a 100644 --- a/dotnet/src/Experimental/Process.Core/ListenForTargetBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/ListenForTargetBuilder.cs @@ -88,7 +88,20 @@ internal override ProcessStepEdgeBuilder SendEventTo_Internal(ProcessTargetBuild } // Link all the source steps to the event listener - var onEventBuilder = messageSource.Source.OnEvent(messageSource.MessageType); + ProcessStepEdgeBuilder? onEventBuilder = null; + + if (messageSource.Source is ProcessBuilder processSource && !processSource.HasParentProcess) + { + // process has no parent, it is root process an only output event from root is external input events + onEventBuilder = processSource.OnInputEvent(messageSource.MessageType); + } + else + { + // if it is a process and has parent process, process is seen as step by other steps. + // if it is a step it seen as step by other steps + onEventBuilder = messageSource.Source.OnEvent(messageSource.MessageType); + } + onEventBuilder.EdgeGroupBuilder = this.EdgeGroupBuilder; if (messageSource.Condition != null) diff --git a/dotnet/src/Experimental/Process.Core/ProcessAgentBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessAgentBuilder.cs index 3a4b39f82751..92258f6d5a50 100644 --- a/dotnet/src/Experimental/Process.Core/ProcessAgentBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/ProcessAgentBuilder.cs @@ -10,7 +10,6 @@ using Json.Schema; using Json.Schema.Generation; using Microsoft.SemanticKernel.Agents; -using Microsoft.SemanticKernel.Process.Models; namespace Microsoft.SemanticKernel; @@ -208,10 +207,8 @@ public ProcessAgentBuilder WithUserStateInput(Expressi #endregion - internal override KernelProcessStepInfo BuildStep(ProcessBuilder processBuilder, KernelProcessStepStateMetadata? stateMetadata = null) + internal override KernelProcessStepInfo BuildStep(ProcessBuilder processBuilder) { - KernelProcessMapStateMetadata? mapMetadata = stateMetadata as KernelProcessMapStateMetadata; - // Build the edges first var builtEdges = this.Edges.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Select(e => e.Build()).ToList()); var agentActions = new ProcessAgentActions( @@ -226,14 +223,19 @@ internal override KernelProcessStepInfo BuildStep(ProcessBuilder processBuilder, OnError = this.OnErrorBuilder?.Build() }); - var state = new KernelProcessStepState(this.Name, "1.0", this.Id); + var state = new KernelProcessStepState(this.StepId, "1.0"); return new KernelProcessAgentStep(this._agentDefinition, agentActions, state, builtEdges, this.DefaultThreadName, this.Inputs) { AgentIdResolver = this.AgentIdResolver, HumanInLoopMode = this.HumanInLoopMode }; } internal ProcessFunctionTargetBuilder GetInvokeAgentFunctionTargetBuilder() { - return new ProcessFunctionTargetBuilder(this, functionName: KernelProcessAgentExecutor.ProcessFunctions.Invoke, parameterName: "message"); + return new ProcessFunctionTargetBuilder(this, functionName: KernelProcessAgentExecutor.ProcessFunctions.Invoke, parameterName: Constants.MessageParameterName); + } + + internal static class Constants + { + public const string MessageParameterName = "message"; } } diff --git a/dotnet/src/Experimental/Process.Core/ProcessBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessBuilder.cs index 830ed59f141c..41b74ce95a5f 100644 --- a/dotnet/src/Experimental/Process.Core/ProcessBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/ProcessBuilder.cs @@ -8,7 +8,6 @@ using Microsoft.SemanticKernel.Agents.AzureAI; using Microsoft.SemanticKernel.Process; using Microsoft.SemanticKernel.Process.Internal; -using Microsoft.SemanticKernel.Process.Models; using Microsoft.SemanticKernel.Process.Tools; namespace Microsoft.SemanticKernel; @@ -130,12 +129,11 @@ internal override Dictionary GetFunctionMetadata /// Builds the step. /// /// ProcessBuilder to build the step for - /// State to apply to the step on the build process /// - internal override KernelProcessStepInfo BuildStep(ProcessBuilder processBuilder, KernelProcessStepStateMetadata? stateMetadata = null) + internal override KernelProcessStepInfo BuildStep(ProcessBuilder processBuilder) { // The step is a, process so we can return the step info directly. - return this.Build(stateMetadata as KernelProcessStateMetadata); + return this.Build(); } /// @@ -154,7 +152,7 @@ internal void AddStepFromBuilder(ProcessStepBuilder stepBuilder) /// private bool StepNameAlreadyExists(string stepName) { - return this._steps.Select(step => step.Name).Contains(stepName); + return this._steps.Select(step => step.StepId).Contains(stepName); } /// @@ -162,9 +160,9 @@ private bool StepNameAlreadyExists(string stepName) /// private TBuilder AddStep(TBuilder builder, IReadOnlyList? aliases) where TBuilder : ProcessStepBuilder { - if (this.StepNameAlreadyExists(builder.Name)) + if (this.StepNameAlreadyExists(builder.StepId)) { - throw new InvalidOperationException($"Step name {builder.Name} is already used, assign a different name for step"); + throw new InvalidOperationException($"Step name {builder.StepId} is already used, assign a different name for step"); } if (aliases != null && aliases.Count > 0) @@ -480,7 +478,7 @@ public ProcessEdgeBuilder OnError() /// Creates a instance to define a listener for incoming messages. /// /// - internal ListenForBuilder ListenFor() + public ListenForBuilder ListenFor() { return new ListenForBuilder(this); } @@ -497,12 +495,12 @@ public ProcessFunctionTargetBuilder WhereInputEventIs(string eventId) if (!this._externalEventTargetMap.TryGetValue(eventId, out var target)) { - throw new KernelException($"The process named '{this.Name}' does not expose an event with Id '{eventId}'."); + throw new KernelException($"The process named '{this.StepId}' does not expose an event with Id '{eventId}'."); } if (target is not ProcessFunctionTargetBuilder functionTargetBuilder) { - throw new KernelException($"The process named '{this.Name}' does not expose an event with Id '{eventId}'."); + throw new KernelException($"The process named '{this.StepId}' does not expose an event with Id '{eventId}'."); } // Targets for external events on a process should be scoped to the process itself rather than the step inside the process. @@ -514,16 +512,17 @@ public ProcessFunctionTargetBuilder WhereInputEventIs(string eventId) /// Builds the process. /// /// An instance of - public KernelProcess Build(KernelProcessStateMetadata? stateMetadata = null) + /// + public KernelProcess Build() { // Build the edges first var builtEdges = this.Edges.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Select(e => e.Build()).ToList()); - // Build the steps and injecting initial state if any is provided - var builtSteps = this.BuildWithStateMetadata(stateMetadata); + // Build the steps + var builtSteps = this.BuildWithChildren(); // Create the process - KernelProcessState state = new(this.Name, version: this.Version, id: this.Id); + KernelProcessState state = new(this.StepId, version: this.Version); KernelProcess process = new(state, builtSteps, builtEdges) { Threads = this._threads, UserStateType = this.StateType, Description = this.Description }; return process; diff --git a/dotnet/src/Experimental/Process.Core/ProcessFunctionTargetBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessFunctionTargetBuilder.cs index d407e227eeca..58e0ae5fe479 100644 --- a/dotnet/src/Experimental/Process.Core/ProcessFunctionTargetBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/ProcessFunctionTargetBuilder.cs @@ -136,7 +136,7 @@ public ProcessAgentInvokeTargetBuilder(ProcessStepBuilder step, string? threadEv internal override KernelProcessTarget Build(ProcessBuilder? processBuilder = null) { - return new KernelProcessAgentInvokeTarget(this.Step.Id, this.ThreadEval, this.MessagesInEval, this.InputEvals); + return new KernelProcessAgentInvokeTarget(this.Step.StepId, this.ThreadEval, this.MessagesInEval, this.InputEvals); } } @@ -146,12 +146,14 @@ internal override KernelProcessTarget Build(ProcessBuilder? processBuilder = nul public record ProcessFunctionTargetBuilder : ProcessTargetBuilder { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class.
+ /// Constructor only meant to be used by internal SK Builders components to allow piping specific parameters if needed.
+ /// For external use, with should be used for mapping multiple parameters to a step function ///
/// The step to target. /// The function to target. /// The parameter to target. - public ProcessFunctionTargetBuilder(ProcessStepBuilder step, string? functionName = null, string? parameterName = null) : base(ProcessTargetType.KernelFunction) + internal ProcessFunctionTargetBuilder(ProcessStepBuilder step, string? functionName, string? parameterName = null) : base(ProcessTargetType.KernelFunction) { Verify.NotNull(step, nameof(step)); @@ -169,21 +171,30 @@ public ProcessFunctionTargetBuilder(ProcessStepBuilder step, string? functionNam var target = step.ResolveFunctionTarget(functionName, parameterName); if (target == null) { - throw new InvalidOperationException($"Failed to resolve function target for {step.GetType().Name}, {step.Name}: Function - {functionName ?? "any"} / Parameter - {parameterName ?? "any"}"); + throw new InvalidOperationException($"Failed to resolve function target for {step.GetType().Name}, {step.StepId}: Function - {functionName ?? "any"} / Parameter - {parameterName ?? "any"}"); } this.FunctionName = target.FunctionName!; this.ParameterName = target.ParameterName; } + /// + /// Initializes a new instance of the class. + /// + /// The step to target. + /// The function to target. + public ProcessFunctionTargetBuilder(ProcessStepBuilder step, string? functionName = null) : this(step, functionName, step is ProcessAgentBuilder ? ProcessAgentBuilder.Constants.MessageParameterName : null) + { + } + /// /// Builds the function target. /// /// An instance of internal override KernelProcessTarget Build(ProcessBuilder? processBuilder = null) { - Verify.NotNull(this.Step.Id); - return new KernelProcessFunctionTarget(this.Step.Id, this.FunctionName, this.ParameterName, this.TargetEventId); + Verify.NotNull(this.Step.StepId); + return new KernelProcessFunctionTarget(this.Step.StepId, this.FunctionName, this.ParameterName, this.TargetEventId); } /// @@ -216,8 +227,9 @@ public sealed record ProcessStepTargetBuilder : ProcessFunctionTargetBuilder /// Initializes a new instance of the class. /// /// + /// /// - public ProcessStepTargetBuilder(ProcessStepBuilder stepBuilder, Func, Dictionary>? inputMapping = null) : base(stepBuilder) + public ProcessStepTargetBuilder(ProcessStepBuilder stepBuilder, string? functionName = null, Func, Dictionary>? inputMapping = null) : base(stepBuilder, functionName, null) { this.InputMapping = inputMapping ?? new Func, Dictionary>((input) => input); } diff --git a/dotnet/src/Experimental/Process.Core/ProcessMapBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessMapBuilder.cs index 93a25529fb38..5e6ab1bf108c 100644 --- a/dotnet/src/Experimental/Process.Core/ProcessMapBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/ProcessMapBuilder.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Microsoft.SemanticKernel.Process.Models; namespace Microsoft.SemanticKernel; @@ -18,7 +17,7 @@ public sealed class ProcessMapBuilder : ProcessStepBuilder /// /// The target of the map operation. May target a step or process internal ProcessMapBuilder(ProcessStepBuilder mapOperation) - : base($"Map{mapOperation.Name}", mapOperation.ProcessBuilder) + : base($"Map{mapOperation.StepId}", mapOperation.ProcessBuilder) { this.MapOperation = mapOperation; } @@ -74,16 +73,14 @@ internal override KernelProcessFunctionTarget ResolveFunctionTarget(string? func } /// - internal override KernelProcessStepInfo BuildStep(ProcessBuilder processBuilder, KernelProcessStepStateMetadata? stateMetadata = null) + internal override KernelProcessStepInfo BuildStep(ProcessBuilder processBuilder) { - KernelProcessMapStateMetadata? mapMetadata = stateMetadata as KernelProcessMapStateMetadata; - // Build the edges first var builtEdges = this.Edges.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Select(e => e.Build()).ToList()); // Define the map state - KernelProcessMapState state = new(this.Name, this.Version, this.Id); + KernelProcessMapState state = new(this.StepId, this.Version, this.StepId); - return new KernelProcessMap(state, this.MapOperation.BuildStep(processBuilder, mapMetadata?.OperationState), builtEdges); + return new KernelProcessMap(state, this.MapOperation.BuildStep(processBuilder), builtEdges); } } diff --git a/dotnet/src/Experimental/Process.Core/ProcessProxyBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessProxyBuilder.cs index 28a5387cb510..301fddf0781f 100644 --- a/dotnet/src/Experimental/Process.Core/ProcessProxyBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/ProcessProxyBuilder.cs @@ -45,7 +45,7 @@ internal ProcessProxyBuilder(IReadOnlyList externalTopics, string name, internal ProcessFunctionTargetBuilder GetExternalFunctionTargetBuilder() { - return new ProcessFunctionTargetBuilder(this, functionName: KernelProxyStep.ProcessFunctions.EmitExternalEvent, parameterName: "proxyEvent"); + return new ProcessFunctionTargetBuilder(this, functionName: KernelProxyStep.ProcessFunctions.EmitExternalEvent); } internal void LinkTopicToStepEdgeInfo(string topicName, ProcessStepBuilder sourceStep, ProcessEventData eventData) @@ -65,7 +65,7 @@ internal void LinkTopicToStepEdgeInfo(string topicName, ProcessStepBuilder sourc } /// - internal override KernelProcessStepInfo BuildStep(ProcessBuilder processBuilder, KernelProcessStepStateMetadata? stateMetadata = null) + internal override KernelProcessStepInfo BuildStep(ProcessBuilder processBuilder) { if (this._externalTopicUsage.All(topic => !topic.Value)) { @@ -74,8 +74,8 @@ internal override KernelProcessStepInfo BuildStep(ProcessBuilder processBuilder, KernelProcessProxyStateMetadata proxyMetadata = new() { - Name = this.Name, - Id = this.Id, + Name = this.StepId, + Id = this.StepId, EventMetadata = this._eventMetadata, PublishTopics = this._externalTopicUsage.ToList().Select(topic => topic.Key).ToList(), }; @@ -83,7 +83,7 @@ internal override KernelProcessStepInfo BuildStep(ProcessBuilder processBuilder, // Build the edges first var builtEdges = this.Edges.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Select(e => e.Build()).ToList()); - KernelProcessStepState state = new(this.Name, this.Version, this.Id); + KernelProcessStepState state = new(this.StepId, this.Version, this.StepId); return new KernelProcessProxy(state, builtEdges) { diff --git a/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs index d0a63da1a274..85096dbe4464 100644 --- a/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs @@ -3,10 +3,8 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text.Json; using Microsoft.SemanticKernel.Process; using Microsoft.SemanticKernel.Process.Internal; -using Microsoft.SemanticKernel.Process.Models; namespace Microsoft.SemanticKernel; @@ -18,14 +16,10 @@ public abstract class ProcessStepBuilder #region Public Interface /// - /// The unique identifier for the step. This may be null until the step is run within a process. + /// The unique identifier for the step within a process. A process cannot have two steps with the same stepId. + /// This can be human-readable but is required to be unique within the process. /// - public string Id { get; } - - /// - /// The name of the step. This is intended to be a human-readable name and is not required to be unique. - /// - public string Name { get; } + public string StepId { get; } /// /// Alternative names that have been used to previous versions of the step @@ -49,6 +43,38 @@ public ProcessStepEdgeBuilder OnEvent(string eventId) return new ProcessStepEdgeBuilder(this, scopedEventId, eventId); } + /// + /// Returns the event Id that is used to identify the result of a function. + /// + /// Optional: name of the step function the result is expected from + /// + public string GetFunctionResultEventId(string? functionName = null) + { + // TODO: Add a check to see if the function name is valid if provided + if (string.IsNullOrWhiteSpace(functionName)) + { + functionName = this.ResolveFunctionName(); + } + return $"{functionName}.OnResult"; + } + + /// + /// Returns the event Id that is used to identify the step specific event. + /// + /// used for custom events emitted by step + /// used for return objects from specific function, if step has only 1 function no need to provide functionName + /// + public string GetFullEventId(string? eventName = null, string? functionName = null) + { + if (eventName == null) + { + // default function result are used + eventName = this.GetFunctionResultEventId(functionName); + } + + return $"{this.StepId}.{eventName}"; + } + /// /// Define the behavior of the step when the specified function has been successfully invoked. /// @@ -57,11 +83,8 @@ public ProcessStepEdgeBuilder OnEvent(string eventId) /// An instance of . public ProcessStepEdgeBuilder OnFunctionResult(string? functionName = null) { - if (string.IsNullOrWhiteSpace(functionName)) - { - functionName = this.ResolveFunctionName(); - } - return this.OnEvent($"{functionName}.OnResult"); + var eventId = this.GetFunctionResultEventId(functionName); + return this.OnEvent(eventId); } /// @@ -103,7 +126,7 @@ public ProcessStepEdgeBuilder OnFunctionError(string? functionName = null) /// Builds the step with step state /// /// an instance of . - internal abstract KernelProcessStepInfo BuildStep(ProcessBuilder processBuilder, KernelProcessStepStateMetadata? stateMetadata = null); + internal abstract KernelProcessStepInfo BuildStep(ProcessBuilder processBuilder); /// /// Registers a group input mapping for the step. @@ -130,11 +153,11 @@ private string ResolveFunctionName() { if (this.FunctionsDict.Count == 0) { - throw new KernelException($"The step {this.Name} has no functions."); + throw new KernelException($"The step {this.StepId} has no functions."); } else if (this.FunctionsDict.Count > 1) { - throw new KernelException($"The step {this.Name} has more than one function, so a function name must be provided."); + throw new KernelException($"The step {this.StepId} has more than one function, so a function name must be provided."); } return this.FunctionsDict.Keys.First(); @@ -156,6 +179,17 @@ internal virtual void LinkTo(string eventId, ProcessStepEdgeBuilder edgeBuilder) edges.Add(edgeBuilder); } + internal static bool FilterSupportedParameterTypes(Type? parameterType, bool hasDefaultValue = false) + { + if (parameterType != typeof(KernelProcessStepContext) && + parameterType != typeof(KernelProcessStepExternalContext)) + { + return !hasDefaultValue; + } + + return false; + } + /// /// Used to resolve the target function and parameter for a given optional function name and parameter name. /// This is used to simplify the process of creating a by making it possible @@ -172,7 +206,7 @@ internal virtual KernelProcessFunctionTarget ResolveFunctionTarget(string? funct if (this.FunctionsDict.Count == 0) { - throw new KernelException($"The target step {this.Name} has no functions."); + throw new KernelException($"The target step {this.StepId} has no functions."); } // If the function name is null or whitespace, then there can only one function on the step @@ -180,7 +214,7 @@ internal virtual KernelProcessFunctionTarget ResolveFunctionTarget(string? funct { if (this.FunctionsDict.Count > 1) { - throw new KernelException("The target step has more than one function, so a function name must be provided."); + throw new KernelException($"The target step {this.StepId} has more than one function, so a function name must be provided."); } verifiedFunctionName = this.FunctionsDict.Keys.First(); @@ -189,13 +223,13 @@ internal virtual KernelProcessFunctionTarget ResolveFunctionTarget(string? funct // Verify that the target function exists if (!this.FunctionsDict.TryGetValue(verifiedFunctionName!, out var kernelFunctionMetadata) || kernelFunctionMetadata is null) { - throw new KernelException($"The function {functionName} does not exist on step {this.Name}"); + throw new KernelException($"The function {functionName} does not exist on step {this.StepId}"); } // If the parameter name is null or whitespace, then the function must have 0 or 1 parameters if (string.IsNullOrWhiteSpace(verifiedParameterName)) { - var undeterminedParameters = kernelFunctionMetadata.Parameters.Where(p => p.ParameterType != typeof(KernelProcessStepContext)).ToList(); + var undeterminedParameters = kernelFunctionMetadata.Parameters.Where(p => FilterSupportedParameterTypes(p.ParameterType, hasDefaultValue: p.DefaultValue != null)).ToList(); if (undeterminedParameters.Count > 1) { @@ -214,7 +248,7 @@ internal virtual KernelProcessFunctionTarget ResolveFunctionTarget(string? funct Verify.NotNull(verifiedFunctionName); return new KernelProcessFunctionTarget( - stepId: this.Id!, + stepId: this.StepId!, functionName: verifiedFunctionName, parameterName: verifiedParameterName ); @@ -246,10 +280,10 @@ protected ProcessStepBuilder(string id, ProcessBuilder? processBuilder) { Verify.NotNullOrWhiteSpace(id, nameof(id)); - this.Id ??= id; - this.Name = id; + this.StepId ??= id; + this.StepId = id; this.FunctionsDict = []; - this._eventNamespace = this.Id; + this._eventNamespace = this.StepId; this.Edges = new Dictionary>(StringComparer.OrdinalIgnoreCase); this.ProcessBuilder = processBuilder; } @@ -263,7 +297,7 @@ public class ProcessStepBuilderTyped : ProcessStepBuilder /// /// The initial state of the step. This may be null if the step does not have any state. /// - private object? _initialState; + private readonly object? _initialState; private readonly Type _stepType; @@ -288,7 +322,7 @@ internal ProcessStepBuilderTyped(Type stepType, string id, ProcessBuilder? proce /// Builds the step with a state if provided /// /// An instance of - internal override KernelProcessStepInfo BuildStep(ProcessBuilder processBuilder, KernelProcessStepStateMetadata? stateMetadata = null) + internal override KernelProcessStepInfo BuildStep(ProcessBuilder processBuilder) { KernelProcessStepState? stateObject = null; KernelProcessStepMetadataAttribute stepMetadataAttributes = KernelProcessStepMetadataFactory.ExtractProcessStepMetadataFromType(this._stepType); @@ -303,32 +337,20 @@ internal override KernelProcessStepInfo BuildStep(ProcessBuilder processBuilder, var stateType = typeof(KernelProcessStepState<>).MakeGenericType(userStateType); Verify.NotNull(stateType); - if (stateMetadata != null && stateMetadata.State != null && stateMetadata.State is JsonElement jsonState) - { - try - { - this._initialState = jsonState.Deserialize(userStateType); - } - catch (JsonException) - { - throw new KernelException($"The initial state provided for step {this.Name} is not of the correct type. The expected type is {userStateType.Name}."); - } - } - // If the step has a user-defined state then we need to validate that the initial state is of the correct type. if (this._initialState is not null && this._initialState.GetType() != userStateType) { - throw new KernelException($"The initial state provided for step {this.Name} is not of the correct type. The expected type is {userStateType.Name}."); + throw new KernelException($"The initial state provided for step {this.StepId} is not of the correct type. The expected type is {userStateType.Name}."); } var initialState = this._initialState ?? Activator.CreateInstance(userStateType); - stateObject = (KernelProcessStepState?)Activator.CreateInstance(stateType, this.Name, stepMetadataAttributes.Version, this.Id); + stateObject = (KernelProcessStepState?)Activator.CreateInstance(stateType, this.StepId, stepMetadataAttributes.Version, null); stateType.GetProperty(nameof(KernelProcessStepState.State))?.SetValue(stateObject, initialState); } else { // The step is a KernelProcessStep with no user-defined state, so we can use the base KernelProcessStepState. - stateObject = new KernelProcessStepState(this.Name, stepMetadataAttributes.Version, this.Id); + stateObject = new KernelProcessStepState(this.StepId, stepMetadataAttributes.Version); } Verify.NotNull(stateObject); diff --git a/dotnet/src/Experimental/Process.Core/ProcessStepEdgeBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessStepEdgeBuilder.cs index d37350743b23..27be7eb24e35 100644 --- a/dotnet/src/Experimental/Process.Core/ProcessStepEdgeBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/ProcessStepEdgeBuilder.cs @@ -62,9 +62,9 @@ internal ProcessStepEdgeBuilder(ProcessStepBuilder source, string eventId, strin /// internal KernelProcessEdge Build(ProcessBuilder? processBuilder = null) { - Verify.NotNull(this.Source?.Id); + Verify.NotNull(this.Source?.StepId); - if (this.Target is null || this.Source?.Id is null) + if (this.Target is null || this.Source?.StepId is null) { throw new InvalidOperationException("A target and Source must be specified before building the edge."); } @@ -73,13 +73,13 @@ internal KernelProcessEdge Build(ProcessBuilder? processBuilder = null) { if (this.EdgeGroupBuilder is not null && this.Target is ProcessStepTargetBuilder stepTargetBuilder) { - var messageSources = this.EdgeGroupBuilder.MessageSources.Select(e => new KernelProcessMessageSource(e.MessageType, e.Source.Id)).ToList(); + var messageSources = this.EdgeGroupBuilder.MessageSources.Select(e => new KernelProcessMessageSource(e.MessageType, e.Source.StepId)).ToList(); var edgeGroup = new KernelProcessEdgeGroup(this.EdgeGroupBuilder.GroupId, messageSources, stepTargetBuilder.InputMapping); functionTargetBuilder.Step.RegisterGroupInputMapping(edgeGroup); } } - return new KernelProcessEdge(this.Source.Id, this.Target.Build(processBuilder), groupId: this.EdgeGroupBuilder?.GroupId, this.Condition, this.VariableUpdate); + return new KernelProcessEdge(this.Source.StepId, this.Target.Build(processBuilder), groupId: this.EdgeGroupBuilder?.GroupId, this.Condition, this.VariableUpdate); } /// diff --git a/dotnet/src/Experimental/Process.Core/Tools/ProcessVisualizationExtensions.cs b/dotnet/src/Experimental/Process.Core/Tools/ProcessVisualizationExtensions.cs index acaed115a092..3be6f1101213 100644 --- a/dotnet/src/Experimental/Process.Core/Tools/ProcessVisualizationExtensions.cs +++ b/dotnet/src/Experimental/Process.Core/Tools/ProcessVisualizationExtensions.cs @@ -64,10 +64,10 @@ private static string RenderProcess(KernelProcess process, int level, bool isSub // Dictionary to map step IDs to step names var stepNames = process.Steps - .Where(step => step.State.Id != null && step.State.Name != null) + .Where(step => step.State.RunId != null && step.State.StepId != null) .ToDictionary( - step => step.State.Id!, - step => step.State.Name! + step => step.State.RunId!, + step => step.State.StepId! ); // Add Start and End nodes only if this is not a sub-process @@ -80,8 +80,8 @@ private static string RenderProcess(KernelProcess process, int level, bool isSub // Process each step foreach (var step in process.Steps) { - var stepId = step.State.Id; - var stepName = step.State.Name; + var stepId = step.State.RunId; + var stepName = step.State.StepId; // Check if the step is a nested process (sub-process) if (step is KernelProcess nestedProcess && level < maxLevel) @@ -115,7 +115,7 @@ private static string RenderProcess(KernelProcess process, int level, bool isSub var stepEdges = kvp.Value; // Skip drawing edges that point to a nested process as an entry point - if (stepNames.ContainsKey(eventId) && process.Steps.Any(s => s.State.Name == eventId && s is KernelProcess)) + if (stepNames.ContainsKey(eventId) && process.Steps.Any(s => s.State.StepId == eventId && s is KernelProcess)) { continue; } @@ -156,8 +156,8 @@ private static string RenderProcess(KernelProcess process, int level, bool isSub // Connect Start to the first step and the last step to End (only for the main process) if (!isSubProcess && process.Steps.Count > 0) { - var firstStepName = process.Steps.First().State.Name; - var lastStepName = process.Steps.Last().State.Name; + var firstStepName = process.Steps.First().State.StepId; + var lastStepName = process.Steps.Last().State.StepId; sb.AppendLine($"{indentation}Start --> {firstStepName}[\"{firstStepName}\"]"); sb.AppendLine($"{indentation}{lastStepName}[\"{lastStepName}\"] --> End"); diff --git a/dotnet/src/Experimental/Process.Core/Workflow/WorkflowBuilder.cs b/dotnet/src/Experimental/Process.Core/Workflow/WorkflowBuilder.cs index 38d9d2b90a00..7ceebb0e4c49 100644 --- a/dotnet/src/Experimental/Process.Core/Workflow/WorkflowBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/Workflow/WorkflowBuilder.cs @@ -325,10 +325,10 @@ public static Task BuildWorkflow(KernelProcess process) Workflow workflow = new() { - Id = process.State.Id ?? throw new KernelException("The process must have an Id set"), + Id = process.State.StepId ?? throw new KernelException("The process must have an Id set"), Description = process.Description, FormatVersion = "1.0", - Name = process.State.Name, + Name = process.State.StepId, Nodes = [new Node { Id = "End", Type = "declarative", Version = "1.0", Description = "Terminal state" }], Variables = [], }; @@ -403,7 +403,7 @@ public static Task BuildWorkflow(KernelProcess process) private static Node BuildNode(KernelProcessStepInfo step, List orchestrationSteps) { - Verify.NotNullOrWhiteSpace(step?.State?.Id, nameof(step.State.Id)); + Verify.NotNullOrWhiteSpace(step?.State?.StepId, nameof(step.State.StepId)); if (step is KernelProcessAgentStep agentStep) { @@ -418,12 +418,12 @@ private static Node BuildNode(KernelProcessStepInfo step, List /// Represents the body of a POST request to start a process in the test host. diff --git a/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/Controllers/ProcessTestController.cs b/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/Controllers/ProcessTestController.cs index ff90188e1de7..28bb1d7a57f3 100644 --- a/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/Controllers/ProcessTestController.cs +++ b/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/Controllers/ProcessTestController.cs @@ -5,9 +5,9 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Process.Serialization; -using SemanticKernel.Process.TestsShared.CloudEvents; +using Microsoft.SemanticKernel.Process.TestsShared.CloudEvents; -namespace SemanticKernel.Process.IntegrationTests.Controllers; +namespace Microsoft.SemanticKernel.Process.IntegrationTests.Controllers; /// /// A controller for starting and managing processes. diff --git a/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/HealthActor.cs b/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/HealthActor.cs index 01766daa1c95..3c975673548c 100644 --- a/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/HealthActor.cs +++ b/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/HealthActor.cs @@ -2,7 +2,7 @@ using Dapr.Actors.Runtime; -namespace SemanticKernel.Process.IntegrationTests; +namespace Microsoft.SemanticKernel.Process.IntegrationTests; /// /// An implementation of the health actor that is only used for testing the health of the Dapr runtime. diff --git a/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/IHealthActor.cs b/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/IHealthActor.cs index 818824575777..0293a747c3d9 100644 --- a/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/IHealthActor.cs +++ b/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/IHealthActor.cs @@ -2,7 +2,7 @@ using Dapr.Actors; -namespace SemanticKernel.Process.IntegrationTests; +namespace Microsoft.SemanticKernel.Process.IntegrationTests; /// /// An interface for a health actor that is only used for testing the health of the Dapr runtime. diff --git a/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/ProcessStateTypeResolver.cs b/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/ProcessStateTypeResolver.cs index 3069dd47087e..d0b1809fa47a 100644 --- a/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/ProcessStateTypeResolver.cs +++ b/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/ProcessStateTypeResolver.cs @@ -3,9 +3,8 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; -using Microsoft.SemanticKernel; -namespace SemanticKernel.Process.IntegrationTests; +namespace Microsoft.SemanticKernel.Process.IntegrationTests; /// /// An implementation of that resolves the type information for . diff --git a/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/Program.cs b/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/Program.cs index 76b4d862f197..7ab9bd668653 100644 --- a/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/Program.cs +++ b/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/Program.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. using Microsoft.SemanticKernel; -using SemanticKernel.Process.IntegrationTests; -using SemanticKernel.Process.TestsShared.CloudEvents; +using Microsoft.SemanticKernel.Process.IntegrationTests; +using Microsoft.SemanticKernel.Process.TestsShared.CloudEvents; var builder = WebApplication.CreateBuilder(args); diff --git a/dotnet/src/Experimental/Process.IntegrationTestRunner.Dapr/DaprTestProcessContext.cs b/dotnet/src/Experimental/Process.IntegrationTestRunner.Dapr/DaprTestProcessContext.cs index 5b2e74d6f027..a9557aba1f08 100644 --- a/dotnet/src/Experimental/Process.IntegrationTestRunner.Dapr/DaprTestProcessContext.cs +++ b/dotnet/src/Experimental/Process.IntegrationTestRunner.Dapr/DaprTestProcessContext.cs @@ -17,13 +17,13 @@ internal sealed class DaprTestProcessContext : KernelProcessContext internal DaprTestProcessContext(KernelProcess process, HttpClient httpClient) { - if (string.IsNullOrWhiteSpace(process.State.Id)) + if (string.IsNullOrWhiteSpace(process.State.RunId)) { - process = process with { State = process.State with { Id = Guid.NewGuid().ToString() } }; + process = process with { State = process.State with { RunId = Guid.NewGuid().ToString() } }; } this._process = process; - this._processId = process.State.Id; + this._processId = process.State.RunId; this._httpClient = httpClient; this._serializerOptions = new JsonSerializerOptions() @@ -80,5 +80,5 @@ public override Task StopAsync() }; } - public override Task GetProcessIdAsync() => Task.FromResult(this._process.State.Id!); + public override Task GetProcessIdAsync() => Task.FromResult(this._process.State.RunId!); } diff --git a/dotnet/src/Experimental/Process.IntegrationTestRunner.Local/ProcessTestFixture.cs b/dotnet/src/Experimental/Process.IntegrationTestRunner.Local/ProcessTestFixture.cs index fc9b16e4ad50..3cbdafa8679e 100644 --- a/dotnet/src/Experimental/Process.IntegrationTestRunner.Local/ProcessTestFixture.cs +++ b/dotnet/src/Experimental/Process.IntegrationTestRunner.Local/ProcessTestFixture.cs @@ -2,10 +2,8 @@ using System; using System.Threading.Tasks; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Process; -namespace SemanticKernel.Process.IntegrationTests; +namespace Microsoft.SemanticKernel.Process.IntegrationTests; /// /// A test fixture for running shared process tests across multiple runtimes. @@ -19,10 +17,11 @@ public class ProcessTestFixture /// An instance of /// An optional initial event. /// channel used for external messages + /// The Id of the run. /// A - public async Task StartProcessAsync(KernelProcess process, Kernel kernel, KernelProcessEvent initialEvent, IExternalKernelProcessMessageChannel? externalMessageChannel = null) + public async Task StartProcessAsync(KernelProcess process, Kernel kernel, KernelProcessEvent initialEvent, IExternalKernelProcessMessageChannel? externalMessageChannel = null, string? runId = null) { - return await process.StartAsync(kernel, initialEvent, externalMessageChannel); + return await process.StartAsync(kernel, initialEvent, processId: runId, externalMessageChannel: externalMessageChannel); } /// diff --git a/dotnet/src/Experimental/Process.IntegrationTests.Resources/ProcessCloudEventsResources.cs b/dotnet/src/Experimental/Process.IntegrationTests.Resources/ProcessCloudEventsResources.cs index a7cccb8cd33a..5d684329cdf1 100644 --- a/dotnet/src/Experimental/Process.IntegrationTests.Resources/ProcessCloudEventsResources.cs +++ b/dotnet/src/Experimental/Process.IntegrationTests.Resources/ProcessCloudEventsResources.cs @@ -1,6 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. -namespace SemanticKernel.Process.IntegrationTests; +namespace Microsoft.SemanticKernel.Process.IntegrationTests; #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member diff --git a/dotnet/src/Experimental/Process.IntegrationTests.Resources/ProcessCycleTestResources.cs b/dotnet/src/Experimental/Process.IntegrationTests.Resources/ProcessCycleTestResources.cs index bc8a23ebe61a..20faec5c2d00 100644 --- a/dotnet/src/Experimental/Process.IntegrationTests.Resources/ProcessCycleTestResources.cs +++ b/dotnet/src/Experimental/Process.IntegrationTests.Resources/ProcessCycleTestResources.cs @@ -3,11 +3,11 @@ using System; using System.Linq; using System.Runtime.Serialization; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; -using Microsoft.SemanticKernel; -namespace SemanticKernel.Process.IntegrationTests; +namespace Microsoft.SemanticKernel.Process.IntegrationTests; #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member @@ -91,6 +91,7 @@ public async ValueTask DoItAsync(KernelProcessStepContext context, string astepd public sealed record CStepState { [DataMember] + [JsonPropertyName("currentCycle")] public int CurrentCycle { get; set; } } @@ -150,6 +151,7 @@ public sealed class EmitterStep : KernelProcessStep public const string InternalEventFunction = "SomeInternalFunctionName"; public const string PublicEventFunction = "SomePublicFunctionName"; public const string DualInputPublicEventFunction = "SomeDualInputPublicEventFunctionName"; + public const string QuadInputPublicEventFunction = "SomeQuadInputPublicEventFunctionName"; private readonly int _sleepDurationMs = 150; @@ -166,7 +168,7 @@ public async Task InternalTestFunctionAsync(KernelProcessStepContext context, st { Thread.Sleep(this._sleepDurationMs); - Console.WriteLine($"[EMIT_INTERNAL] {data}"); + Console.WriteLine($"[EMIT_INTERNAL-{this.StepName}] {data}"); this._state!.LastMessage = data; await context.EmitEventAsync(new() { Id = EventId, Data = data }); } @@ -176,7 +178,7 @@ public async Task PublicTestFunctionAsync(KernelProcessStepContext context, stri { Thread.Sleep(this._sleepDurationMs); - Console.WriteLine($"[EMIT_PUBLIC] {data}"); + Console.WriteLine($"[EMIT_PUBLIC-{this.StepName}] {data}"); this._state!.LastMessage = data; await context.EmitEventAsync(new() { Id = PublicEventId, Data = data, Visibility = KernelProcessEventVisibility.Public }); } @@ -187,10 +189,21 @@ public async Task DualInputPublicTestFunctionAsync(KernelProcessStepContext cont Thread.Sleep(this._sleepDurationMs); string outputText = $"{firstInput}-{secondInput}"; - Console.WriteLine($"[EMIT_PUBLIC_DUAL] {outputText}"); + Console.WriteLine($"[EMIT_PUBLIC_DUAL-{this.StepName}] {outputText}"); this._state!.LastMessage = outputText; await context.EmitEventAsync(new() { Id = ProcessTestsEvents.OutputReadyPublic, Data = outputText, Visibility = KernelProcessEventVisibility.Public }); } + + [KernelFunction(QuadInputPublicEventFunction)] + public async Task QuadInputPublicEventFunctionAsync(KernelProcessStepContext context, string firstInput, string secondInput, string thirdInput = "thirdInput", string fourthInput = "fourthInput") + { + Thread.Sleep(this._sleepDurationMs * 2); + + string outputText = $"{firstInput}-{secondInput}-{thirdInput}-{fourthInput}"; + Console.WriteLine($"[EMIT_PUBLIC_QUAD-{this.StepName}] {outputText}"); + this._state!.LastMessage = outputText; + await context.EmitEventAsync(new() { Id = ProcessTestsEvents.OutputReadySecondaryPublic, Data = outputText, Visibility = KernelProcessEventVisibility.Public }); + } } /// @@ -320,8 +333,10 @@ public sealed record StepState public static class ProcessTestsEvents { public const string StartProcess = "StartProcess"; + public const string SecondaryStartProcess = "SecondaryStartProcess"; public const string StartInnerProcess = "StartInnerProcess"; public const string OutputReadyPublic = "OutputReadyPublic"; + public const string OutputReadySecondaryPublic = "OutputReadySecondaryPublic"; public const string OutputReadyInternal = "OutputReadyInternal"; public const string ErrorStepSuccess = "ErrorStepSuccess"; } diff --git a/dotnet/src/Experimental/Process.IntegrationTests.Resources/ProcessMapTestResources.cs b/dotnet/src/Experimental/Process.IntegrationTests.Resources/ProcessMapTestResources.cs index 758bd75b1d2d..e159792b3f9d 100644 --- a/dotnet/src/Experimental/Process.IntegrationTests.Resources/ProcessMapTestResources.cs +++ b/dotnet/src/Experimental/Process.IntegrationTests.Resources/ProcessMapTestResources.cs @@ -4,9 +4,8 @@ using System.IO; using System.Linq; using System.Threading.Tasks; -using Microsoft.SemanticKernel; -namespace SemanticKernel.Process.IntegrationTests; +namespace Microsoft.SemanticKernel.Process.IntegrationTests; #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member diff --git a/dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessCloudEventsTests.cs b/dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessCloudEventsTests.cs index 818b90acfdf4..d58e08c93562 100644 --- a/dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessCloudEventsTests.cs +++ b/dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessCloudEventsTests.cs @@ -9,13 +9,14 @@ using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.SemanticKernel; -using SemanticKernel.IntegrationTests.TestSettings; -using SemanticKernel.Process.TestsShared.CloudEvents; -using SemanticKernel.Process.TestsShared.Steps; +using Microsoft.SemanticKernel.IntegrationTests.TestSettings; +using Microsoft.SemanticKernel.Process.IntegrationTests; +using Microsoft.SemanticKernel.Process.TestsShared.CloudEvents; +using Microsoft.SemanticKernel.Process.TestsShared.Steps; using Xunit; #pragma warning restore IDE0005 // Using directive is unnecessary. -namespace SemanticKernel.Process.IntegrationTests; +namespace Microsoft.SemanticKernel.Process.IntegrationTests; /// /// Integration tests for processes. @@ -187,8 +188,8 @@ private ProcessBuilder CreateLinearProcessWithEmitTopic(string name) .SendEventTo(new ProcessFunctionTargetBuilder(echoStep)); echoStep - .OnFunctionResult(nameof(CommonSteps.EchoStep.Echo)) - .SendEventTo(new ProcessFunctionTargetBuilder(repeatStep, parameterName: "message")); + .OnFunctionResult() + .SendEventTo(new ProcessFunctionTargetBuilder(repeatStep)); echoStep .OnFunctionResult() diff --git a/dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessCycleTests.cs b/dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessCycleTests.cs index f12079a24a33..43fd3ee46868 100644 --- a/dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessCycleTests.cs +++ b/dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessCycleTests.cs @@ -7,7 +7,7 @@ using Xunit; #pragma warning restore IDE0005 // Using directive is unnecessary. -namespace SemanticKernel.Process.IntegrationTests; +namespace Microsoft.SemanticKernel.Process.IntegrationTests; /// /// Integration test focusing on cycles in a process. @@ -54,13 +54,19 @@ public async Task TestCycleAndExitWithFanInAsync() .OnEvent(CommonEvents.StartBRequested) .SendEventTo(new ProcessFunctionTargetBuilder(myBStep)); - myAStep - .OnEvent(CommonEvents.AStepDone) - .SendEventTo(new ProcessFunctionTargetBuilder(myCStep, parameterName: "astepdata")); - - myBStep - .OnEvent(CommonEvents.BStepDone) - .SendEventTo(new ProcessFunctionTargetBuilder(myCStep, parameterName: "bstepdata")); + process.ListenFor().AllOf( + [ + new(CommonEvents.AStepDone, myAStep), + new(CommonEvents.BStepDone, myBStep) + ]) + .SendEventTo(new ProcessStepTargetBuilder(myCStep, inputMapping: (inputEvents) => + { + return new() + { + { "astepdata", inputEvents[myAStep.GetFullEventId(CommonEvents.AStepDone)] }, + { "bstepdata", inputEvents[myBStep.GetFullEventId(CommonEvents.BStepDone)] } + }; + })); myCStep .OnEvent(CommonEvents.CStepDone) @@ -74,7 +80,7 @@ public async Task TestCycleAndExitWithFanInAsync() var processContext = await this._fixture.StartProcessAsync(kernelProcess, kernel, new KernelProcessEvent() { Id = CommonEvents.StartProcess, Data = "foo" }); var processState = await processContext.GetStateAsync(); - var cStepState = processState.Steps.Where(s => s.State.Name == "CStep").FirstOrDefault()?.State as KernelProcessStepState; + var cStepState = processState.Steps.Where(s => s.State.StepId == "CStep").FirstOrDefault()?.State as KernelProcessStepState; Assert.NotNull(cStepState?.State); Assert.Equal(3, cStepState.State.CurrentCycle); diff --git a/dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessMapTests.cs b/dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessMapTests.cs index 3213ad57b237..7e4752eac85b 100644 --- a/dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessMapTests.cs +++ b/dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessMapTests.cs @@ -9,7 +9,7 @@ using Xunit; #pragma warning restore IDE0005 // Using directive is unnecessary. -namespace SemanticKernel.Process.IntegrationTests; +namespace Microsoft.SemanticKernel.Process.IntegrationTests; /// /// Integration test focusing on . @@ -65,7 +65,7 @@ await this._fixture.StartProcessAsync( // Assert KernelProcess processState = await processContext.GetStateAsync(); - KernelProcessStepState unionState = (KernelProcessStepState)processState.Steps.Where(s => s.State.Name == "Union").Single().State; + KernelProcessStepState unionState = (KernelProcessStepState)processState.Steps.Where(s => s.State.StepId == "Union").Single().State; Assert.NotNull(unionState?.State); Assert.Equal(55L, unionState.State.SquareResult); @@ -106,7 +106,7 @@ public async Task TestMapWithProcessAsync() // Act KernelProcessContext processContext = await this._fixture.StartProcessAsync( - processInstance with { State = processInstance.State with { Id = Guid.NewGuid().ToString() } }, + processInstance with { State = processInstance.State with { RunId = Guid.NewGuid().ToString() } }, kernel, new KernelProcessEvent() { @@ -116,7 +116,7 @@ await this._fixture.StartProcessAsync( // Assert KernelProcess processState = await processContext.GetStateAsync(); - KernelProcessStepState unionState = (KernelProcessStepState)processState.Steps.Where(s => s.State.Name == "Union").Single().State; + KernelProcessStepState unionState = (KernelProcessStepState)processState.Steps.Where(s => s.State.StepId == "Union").Single().State; Assert.NotNull(unionState?.State); Assert.Equal(55L, unionState.State.SquareResult); diff --git a/dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessTestFixture.cs b/dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessTestFixture.cs index e7e37b4149c5..ccf74466f49e 100644 --- a/dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessTestFixture.cs +++ b/dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessTestFixture.cs @@ -1,10 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. using System.Threading.Tasks; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Process; -namespace SemanticKernel.Process.IntegrationTests; +namespace Microsoft.SemanticKernel.Process.IntegrationTests; /// /// A test fixture for running shared process tests across multiple runtimes. @@ -18,8 +16,9 @@ public abstract class ProcessTestFixture /// An instance of /// An optional initial event. /// channel used for external messages + /// An optional run Id. /// A - public abstract Task StartProcessAsync(KernelProcess process, Kernel kernel, KernelProcessEvent initialEvent, IExternalKernelProcessMessageChannel? externalMessageChannel = null); + public abstract Task StartProcessAsync(KernelProcess process, Kernel kernel, KernelProcessEvent initialEvent, IExternalKernelProcessMessageChannel? externalMessageChannel = null, string? runId = null); /// /// Starts the specified process. diff --git a/dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessTests.cs b/dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessTests.cs index 24932e1e72e6..1f1f16544ced 100644 --- a/dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessTests.cs +++ b/dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessTests.cs @@ -1,16 +1,18 @@ // Copyright (c) Microsoft. All rights reserved. #pragma warning disable IDE0005 // Using directive is unnecessary. +using System; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.SemanticKernel; -using SemanticKernel.IntegrationTests.TestSettings; -using SemanticKernel.Process.TestsShared.Steps; +using Microsoft.SemanticKernel.Process.TestsShared.Steps; +using Microsoft.SemanticKernel.IntegrationTests.TestSettings; using Xunit; #pragma warning restore IDE0005 // Using directive is unnecessary. -namespace SemanticKernel.Process.IntegrationTests; +namespace Microsoft.SemanticKernel.Process.IntegrationTests; /// /// Integration tests for processes. @@ -61,6 +63,113 @@ public async Task LinearProcessAsync() this.AssertStepStateLastMessage(processInfo, nameof(RepeatStep), expectedLastMessage: string.Join(" ", Enumerable.Repeat(testInput, 2))); } + /// + /// Tests a simple process with a WhenAll event listener + /// + /// + [Fact] + public async Task ProcessWithWhenAllListenerAsync() + { + // Arrange + OpenAIConfiguration configuration = this._configuration.GetSection("OpenAI").Get()!; + this._kernelBuilder.AddOpenAIChatCompletion( + modelId: configuration.ModelId!, + apiKey: configuration.ApiKey); + + Kernel kernel = this._kernelBuilder.Build(); + var process = this.GetProcess().Build(); + + // Act + string testInput = "Test"; + var processHandle = await this._fixture.StartProcessAsync(process, new(), new() { Id = ProcessTestsEvents.StartProcess, Data = testInput }, runId: Guid.NewGuid().ToString()); + + var processInfo = await processHandle.GetStateAsync(); + + // Assert + this.AssertStepState(processInfo, "cStep", (KernelProcessStepState state) => state.State?.CurrentCycle == 3); + } + + /// + /// Tests a process with a WhenAll listener and a step that has multiple functions and parameters. + /// + /// A + [Fact] + public async Task ProcessWithWhenAllListenerAndStepWithMultipleFunctionsAndParametersUsingOnlyOneMultiParamFunctionAsync() + { + // Arrange + OpenAIConfiguration configuration = this._configuration.GetSection("OpenAI").Get()!; + this._kernelBuilder.AddOpenAIChatCompletion( + modelId: configuration.ModelId!, + apiKey: configuration.ApiKey); + + Kernel kernel = this._kernelBuilder.Build(); + + var processBuilder = this.CreateProcessWithFanInUsingStepWithMultipleFunctionsAndParameters("testProcess"); + var process = processBuilder.Build(); + + // Act + string testInput = "Test"; + var processHandle = await this._fixture.StartProcessAsync(process, kernel, new() { Id = ProcessTestsEvents.StartProcess, Data = testInput }); + + // Assert + var processInfo = await processHandle.GetStateAsync(); + this.AssertStepStateLastMessage(processInfo, "emitterStep", expectedLastMessage: $"{testInput} {testInput}-{testInput}"); + } + + /// + /// Tests a process with a WhenAll listener and a step that has multiple functions and parameters. + /// + /// A + [Fact] + public async Task ProcessWithWhenAllListenerAndStepWithMultipleFunctionsAndParametersUsingOnlyTwoMultiParamFunctionsFromStepsAndInputEventsAsync() + { + // Arrange + OpenAIConfiguration configuration = this._configuration.GetSection("OpenAI").Get()!; + this._kernelBuilder.AddOpenAIChatCompletion( + modelId: configuration.ModelId!, + apiKey: configuration.ApiKey); + + Kernel kernel = this._kernelBuilder.Build(); + + var processBuilder = this.CreateProcessWithFanInUsingStepWithMultipleFunctionsAndParametersSimultaneouslyFromStepsAndInputEvents("testProcess"); + var process = processBuilder.Build(); + + // Act + string testInput = "Test"; + var processHandle = await this._fixture.StartProcessAsync(process, kernel, new() { Id = ProcessTestsEvents.StartProcess, Data = testInput }); + + // Assert + var processInfo = await processHandle.GetStateAsync(); + this.AssertStepStateLastMessage(processInfo, "fanInStep", expectedLastMessage: $"{testInput}-{testInput}-{testInput} {testInput}-{testInput}-thirdInput-someFixedInputFromProcessDefinition"); + } + + /// + /// Tests a process with a WhenAll listener and a step that has multiple functions and parameters. + /// + /// A + [Fact] + public async Task ProcessWithWhenAllListenerAndStepWithMultipleFunctionsAndParametersUsingOnlyTwoMultiParamFunctionsFromStepsOnlyAsync() + { + // Arrange + OpenAIConfiguration configuration = this._configuration.GetSection("OpenAI").Get()!; + this._kernelBuilder.AddOpenAIChatCompletion( + modelId: configuration.ModelId!, + apiKey: configuration.ApiKey); + + Kernel kernel = this._kernelBuilder.Build(); + + var processBuilder = this.CreateProcessWithFanInUsingStepWithMultipleFunctionsAndParametersSimultaneouslyFromStepsOnly("testProcess"); + var process = processBuilder.Build(); + + // Act + string testInput = "Test"; + var processHandle = await this._fixture.StartProcessAsync(process, kernel, new() { Id = ProcessTestsEvents.StartProcess, Data = testInput }); + + // Assert + var processInfo = await processHandle.GetStateAsync(); + this.AssertStepStateLastMessage(processInfo, "fanInStep", expectedLastMessage: $"{testInput} {testInput}-{testInput}-{testInput} {testInput}-{testInput}-thirdInput-someFixedInputFromProcessDefinition"); + } + /// /// Tests a process with three steps where the third step is a nested process. Ev/ts from the outer process /// are routed to the inner process. @@ -96,7 +205,7 @@ public async Task NestedProcessOuterToInnerWorksAsync() var processInfo = await processHandle.GetStateAsync(); // Assert - var innerProcess = processInfo.Steps.Where(s => s.State.Name == "Inner").Single() as KernelProcess; + var innerProcess = processInfo.Steps.Where(s => s.State.StepId == "Inner").Single() as KernelProcess; Assert.NotNull(innerProcess); this.AssertStepStateLastMessage(innerProcess, nameof(RepeatStep), expectedLastMessage: string.Join(" ", Enumerable.Repeat(testInput, 4))); } @@ -251,7 +360,7 @@ public async Task StepAndFanInProcessAsync() var processInfo = await processHandle.GetStateAsync(); // Assert - var subprocessStepInfo = processInfo.Steps.Where(s => s.State.Name == fanInStepName)?.FirstOrDefault() as KernelProcess; + var subprocessStepInfo = processInfo.Steps.Where(s => s.State.StepId == fanInStepName)?.FirstOrDefault() as KernelProcess; Assert.NotNull(subprocessStepInfo); this.AssertStepStateLastMessage(subprocessStepInfo, nameof(FanInStep), expectedLastMessage: $"{testInput}-{testInput} {testInput}"); } @@ -304,12 +413,22 @@ public async Task ProcessWith2NestedSubprocessSequentiallyAndMultipleOutputSteps .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")); + processBuilder.ListenFor().AllOf( + [ + new(EmitterStep.EventId, firstStep), + new(ProcessTestsEvents.OutputReadyPublic, thirdStep) + ]) + .SendEventTo(new ProcessStepTargetBuilder(lastStep, inputMapping: (inputEvents) => + { + // Map the inputs to the last step. + return new() + { + { "firstInput", inputEvents[firstStep.GetFullEventId(EmitterStep.EventId)] }, + { "secondInput", inputEvents[thirdStep.GetFullEventId(ProcessTestsEvents.OutputReadyPublic)] } + }; + })); KernelProcess process = processBuilder.Build(); @@ -327,6 +446,275 @@ public async Task ProcessWith2NestedSubprocessSequentiallyAndMultipleOutputSteps #region Predefined ProcessBuilders for testing + private ProcessBuilder GetProcess() + { + // Create the process builder. + ProcessBuilder processBuilder = new("ProcessWithDapr"); + + // Add some steps to the process. + var kickoffStep = processBuilder.AddStepFromType(id: "kickoffStep"); + var myAStep = processBuilder.AddStepFromType(id: "aStep"); + var myBStep = processBuilder.AddStepFromType(id: "bStep"); + + // ########## Configuring initial state on steps in a process ########### + // For demonstration purposes, we add the CStep and configure its initial state with a CurrentCycle of 1. + // Initializing state in a step can be useful for when you need a step to start out with a predetermines + // configuration that is not easily accomplished with dependency injection. + var myCStep = processBuilder.AddStepFromType(initialState: new() { CurrentCycle = 1 }, id: "cStep"); + + // Setup the input event that can trigger the process to run and specify which step and function it should be routed to. + processBuilder + .OnInputEvent(CommonEvents.StartProcess) + .SendEventTo(new ProcessFunctionTargetBuilder(kickoffStep)); + + // When the kickoff step is finished, trigger both AStep and BStep. + kickoffStep + .OnEvent(CommonEvents.StartARequested) + .SendEventTo(new ProcessFunctionTargetBuilder(myAStep)) + .SendEventTo(new ProcessFunctionTargetBuilder(myBStep)); + + // When step A and step B have finished, trigger the CStep. + processBuilder + .ListenFor() + .AllOf(new() + { + new(messageType: CommonEvents.AStepDone, source: myAStep), + new(messageType: CommonEvents.BStepDone, source: myBStep) + }) + .SendEventTo(new ProcessStepTargetBuilder(myCStep, inputMapping: (inputEvents) => + { + // Map the input events to the CStep's input parameters. + // In this case, we are mapping the output of AStep to the first input parameter of CStep + // and the output of BStep to the second input parameter of CStep. + return new() + { + { "astepdata", inputEvents[$"aStep.{CommonEvents.AStepDone}"] }, + { "bstepdata", inputEvents[$"bStep.{CommonEvents.BStepDone}"] } + }; + })); + + // When CStep has finished without requesting an exit, activate the Kickoff step to start again. + myCStep + .OnEvent(CommonEvents.CStepDone) + .SendEventTo(new ProcessFunctionTargetBuilder(kickoffStep)); + + // When the CStep has finished by requesting an exit, stop the process. + myCStep + .OnEvent(CommonEvents.ExitRequested) + .StopProcess(); + + return processBuilder; + } + + /// + /// Sample process with a fan in step that takes the output of two steps and combines them.
+ /// Fan in step - emitter - has multiple functions and some have multiple parameters.
+ /// + /// ┌────────┐ + /// │ repeat ├───┐ + /// └────────┘ │ ┌─────────┐ + /// └──►│ │ + /// │ emitter │ + /// ┌──►│ │ + /// ┌────────┐ │ └─────────┘ + /// │ echo ├───┘ + /// └────────┘ + /// + ///
+ /// + /// + private ProcessBuilder CreateProcessWithFanInUsingStepWithMultipleFunctionsAndParameters(string name) + { + ProcessBuilder processBuilder = new(name); + ProcessStepBuilder repeatStep = processBuilder.AddStepFromType("repeatStep"); + ProcessStepBuilder echoStep = processBuilder.AddStepFromType("echoStep"); + ProcessStepBuilder emitterStep = processBuilder.AddStepFromType("emitterStep"); + + processBuilder + .OnInputEvent(ProcessTestsEvents.StartProcess) + .SendEventTo(new ProcessFunctionTargetBuilder(repeatStep)) + .SendEventTo(new ProcessFunctionTargetBuilder(echoStep)); + + processBuilder.ListenFor().AllOf( + [ + new(ProcessTestsEvents.OutputReadyInternal, repeatStep), + new(echoStep.GetFunctionResultEventId(), echoStep) + ]) + .SendEventTo(new ProcessStepTargetBuilder(emitterStep, functionName: EmitterStep.DualInputPublicEventFunction, inputMapping: inputEvents => + { + return new() + { + { "firstInput", inputEvents[repeatStep.GetFullEventId(ProcessTestsEvents.OutputReadyInternal)] }, + { "secondInput", inputEvents[echoStep.GetFullEventId()] } + }; + })); + + return processBuilder; + } + + /// + /// Sample process with a fan in step that takes the output of two steps from same step and combines them.
+ /// Fan in step - emitter - has multiple functions and some have multiple parameters.
+ /// This test is meant to test the ability to create multiple internal edgeGroups in the same step.
+ /// + /// ┌────────┐ + /// │ repeat ├───┐ + /// └────────┘ │ ┌────────┐ ┌─────────┐ ┌─────────┐ + /// └►│ r_echo ├─►│ ├────►│ │ + /// └────────┘ │ emitter │ │ fanIn │ + /// ┌-───────────►│ ├────►│ │ + /// ┌────────┐ │ └─────────┘ └─────────┘ + /// │ echo ├───┘ + /// └────────┘ + /// + ///
+ /// + /// + private ProcessBuilder CreateProcessWithFanInUsingStepWithMultipleFunctionsAndParametersSimultaneouslyFromStepsAndInputEvents(string name) + { + ProcessBuilder processBuilder = new(name); + ProcessStepBuilder repeatStep = processBuilder.AddStepFromType("repeatStep"); + ProcessStepBuilder repeatEchoStep = processBuilder.AddStepFromType("repeatEchoStep"); + ProcessStepBuilder echoStep = processBuilder.AddStepFromType("echoStep"); + ProcessStepBuilder emitterStep = processBuilder.AddStepFromType("emitterStep"); + ProcessStepBuilder fanInStep = processBuilder.AddStepFromType("fanInStep"); + + processBuilder + .OnInputEvent(ProcessTestsEvents.StartProcess) + .SendEventTo(new ProcessFunctionTargetBuilder(repeatStep)) + .SendEventTo(new ProcessFunctionTargetBuilder(echoStep)); + + repeatStep + .OnEvent(ProcessTestsEvents.OutputReadyInternal) + .SendEventTo(new ProcessFunctionTargetBuilder(repeatEchoStep)); + + processBuilder.ListenFor().AllOf( + [ + new(ProcessTestsEvents.StartProcess, processBuilder), + ]) + .SendEventTo(new ProcessStepTargetBuilder(emitterStep, functionName: EmitterStep.DualInputPublicEventFunction, inputMapping: inputEvents => + { + return new() + { + { "firstInput", inputEvents[processBuilder.GetFullEventId(ProcessTestsEvents.StartProcess)] }, + { "secondInput", inputEvents[processBuilder.GetFullEventId(ProcessTestsEvents.StartProcess)] } + }; + })); + + processBuilder.ListenFor().AllOf( + [ + new(ProcessTestsEvents.OutputReadyInternal, repeatStep), + new(echoStep.GetFunctionResultEventId(), echoStep) + ]) + .SendEventTo(new ProcessStepTargetBuilder(emitterStep, functionName: EmitterStep.QuadInputPublicEventFunction, inputMapping: inputEvents => + { + return new() + { + { "firstInput", inputEvents[repeatStep.GetFullEventId(ProcessTestsEvents.OutputReadyInternal)] }, + { "secondInput", inputEvents[echoStep.GetFullEventId()] }, + { "fourthInput", "someFixedInputFromProcessDefinition" }, + }; + })); + + processBuilder.ListenFor().AllOf( + [ + new(ProcessTestsEvents.OutputReadyPublic, emitterStep), + new(ProcessTestsEvents.OutputReadySecondaryPublic, emitterStep), + ]) + .SendEventTo(new ProcessStepTargetBuilder(fanInStep, inputMapping: inputEvents => + { + return new() + { + { "firstInput", inputEvents[emitterStep.GetFullEventId(ProcessTestsEvents.OutputReadyPublic)] }, + { "secondInput", inputEvents[emitterStep.GetFullEventId(ProcessTestsEvents.OutputReadySecondaryPublic)] } + }; + })); + + return processBuilder; + } + + /// + /// Sample process with a fan in step that takes the output of two steps from same step and combines them.
+ /// Fan in step - emitter - has multiple functions and some have multiple parameters.
+ /// This test is meant to test the ability to create multiple internal edgeGroups in the same step.
+ /// + /// ┌────────┐ + /// │ repeat ├───┐ + /// └────────┘ │ ┌────────┐ ┌─────────┐ ┌─────────┐ + /// └►│ r_echo ├─►│ ├────►│ │ + /// └────────┘ │ emitter │ │ fanIn │ + /// ┌-───────────►│ ├────►│ │ + /// ┌────────┐ │ └─────────┘ └─────────┘ + /// │ echo ├───┘ + /// └────────┘ + /// + ///
+ /// + /// + private ProcessBuilder CreateProcessWithFanInUsingStepWithMultipleFunctionsAndParametersSimultaneouslyFromStepsOnly(string name) + { + ProcessBuilder processBuilder = new(name); + ProcessStepBuilder repeatStep = processBuilder.AddStepFromType("repeatStep"); + ProcessStepBuilder repeatEchoStep = processBuilder.AddStepFromType("repeatEchoStep"); + ProcessStepBuilder echoStep = processBuilder.AddStepFromType("echoStep"); + ProcessStepBuilder emitterStep = processBuilder.AddStepFromType("emitterStep"); + ProcessStepBuilder fanInStep = processBuilder.AddStepFromType("fanInStep"); + + processBuilder + .OnInputEvent(ProcessTestsEvents.StartProcess) + .SendEventTo(new ProcessFunctionTargetBuilder(repeatStep)) + .SendEventTo(new ProcessFunctionTargetBuilder(echoStep)); + + repeatStep + .OnEvent(ProcessTestsEvents.OutputReadyInternal) + .SendEventTo(new ProcessFunctionTargetBuilder(repeatEchoStep)); + + processBuilder.ListenFor().AllOf( + [ + new(ProcessTestsEvents.StartProcess, processBuilder), + new(repeatEchoStep.GetFunctionResultEventId(), repeatEchoStep), + ]) + .SendEventTo(new ProcessStepTargetBuilder(emitterStep, functionName: EmitterStep.DualInputPublicEventFunction, inputMapping: inputEvents => + { + return new() + { + { "firstInput", inputEvents[repeatEchoStep.GetFullEventId()] }, + { "secondInput", inputEvents[processBuilder.GetFullEventId(ProcessTestsEvents.StartProcess)] } + }; + })); + + processBuilder.ListenFor().AllOf( + [ + new(ProcessTestsEvents.OutputReadyInternal, repeatStep), + new(echoStep.GetFunctionResultEventId(), echoStep) + ]) + .SendEventTo(new ProcessStepTargetBuilder(emitterStep, functionName: EmitterStep.QuadInputPublicEventFunction, inputMapping: inputEvents => + { + return new() + { + { "firstInput", inputEvents[repeatStep.GetFullEventId(ProcessTestsEvents.OutputReadyInternal)] }, + { "secondInput", inputEvents[echoStep.GetFullEventId()] }, + { "fourthInput", "someFixedInputFromProcessDefinition" }, + }; + })); + + processBuilder.ListenFor().AllOf( + [ + new(ProcessTestsEvents.OutputReadyPublic, emitterStep), + new(ProcessTestsEvents.OutputReadySecondaryPublic, emitterStep), + ]) + .SendEventTo(new ProcessStepTargetBuilder(fanInStep, inputMapping: inputEvents => + { + return new() + { + { "firstInput", inputEvents[emitterStep.GetFullEventId(ProcessTestsEvents.OutputReadyPublic)] }, + { "secondInput", inputEvents[emitterStep.GetFullEventId(ProcessTestsEvents.OutputReadySecondaryPublic)] } + }; + })); + + return processBuilder; + } + /// /// Sample long sequential process, each step has a delay.
/// Input Event:
@@ -364,9 +752,21 @@ private ProcessBuilder CreateLongSequentialProcessWithFanInAsOutputStep(string n 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")); + processBuilder.ListenFor().AllOf( + [ + new(EmitterStep.EventId, firstNestedStep), + new(EmitterStep.EventId, ninthNestedStep), + ]) + .SendEventTo(new ProcessStepTargetBuilder(tenthNestedStep, functionName: EmitterStep.DualInputPublicEventFunction, inputMapping: (inputEvents) => + { + // Map the input events to the parameters of the tenth step. + return new() + { + { "firstInput", inputEvents[firstNestedStep.GetFullEventId(EmitterStep.EventId)] }, + { "secondInput", inputEvents[ninthNestedStep.GetFullEventId(EmitterStep.EventId)] } + }; + })); return processBuilder; } @@ -387,11 +787,13 @@ private ProcessBuilder CreateLinearProcess(string name) var echoStep = processBuilder.AddStepFromType(id: nameof(CommonSteps.EchoStep)); var repeatStep = processBuilder.AddStepFromType(id: nameof(RepeatStep)); - processBuilder.OnInputEvent(ProcessTestsEvents.StartProcess) + processBuilder + .OnInputEvent(ProcessTestsEvents.StartProcess) .SendEventTo(new ProcessFunctionTargetBuilder(echoStep)); - echoStep.OnFunctionResult(nameof(CommonSteps.EchoStep.Echo)) - .SendEventTo(new ProcessFunctionTargetBuilder(repeatStep, parameterName: "message")); + echoStep + .OnFunctionResult() + .SendEventTo(new ProcessFunctionTargetBuilder(repeatStep)); return processBuilder; } @@ -422,10 +824,22 @@ private ProcessBuilder CreateFanInProcess(string name) var fanInCStep = processBuilder.AddStepFromType(id: nameof(FanInStep)); processBuilder.OnInputEvent(ProcessTestsEvents.StartProcess).SendEventTo(new ProcessFunctionTargetBuilder(echoAStep)); - processBuilder.OnInputEvent(ProcessTestsEvents.StartProcess).SendEventTo(new ProcessFunctionTargetBuilder(repeatBStep, parameterName: "message")); - - echoAStep.OnFunctionResult(nameof(CommonSteps.EchoStep.Echo)).SendEventTo(new ProcessFunctionTargetBuilder(fanInCStep, parameterName: "firstInput")); - repeatBStep.OnEvent(ProcessTestsEvents.OutputReadyPublic).SendEventTo(new ProcessFunctionTargetBuilder(fanInCStep, parameterName: "secondInput")); + processBuilder.OnInputEvent(ProcessTestsEvents.StartProcess).SendEventTo(new ProcessFunctionTargetBuilder(repeatBStep)); + + processBuilder.ListenFor().AllOf( + [ + new(echoAStep.GetFunctionResultEventId(), echoAStep), + new(ProcessTestsEvents.OutputReadyPublic, repeatBStep) + ]) + .SendEventTo(new ProcessStepTargetBuilder(fanInCStep, inputMapping: (inputEvents) => + { + // Map the input events to the parameters of the fan-in step. + return new() + { + { "firstInput", inputEvents[echoAStep.GetFullEventId(echoAStep.GetFunctionResultEventId())] }, + { "secondInput", inputEvents[repeatBStep.GetFullEventId(ProcessTestsEvents.OutputReadyPublic)] } + }; + })); return processBuilder; } @@ -454,7 +868,7 @@ private ProcessBuilder CreateProcessWithError(string name) var reportStep = processBuilder.AddStepFromType("ReportStep"); processBuilder.OnInputEvent(ProcessTestsEvents.StartProcess).SendEventTo(new ProcessFunctionTargetBuilder(errorStep)); - errorStep.OnEvent(ProcessTestsEvents.ErrorStepSuccess).SendEventTo(new ProcessFunctionTargetBuilder(repeatStep, parameterName: "message")); + errorStep.OnEvent(ProcessTestsEvents.ErrorStepSuccess).SendEventTo(new ProcessFunctionTargetBuilder(repeatStep)); errorStep.OnFunctionError("ErrorWhenTrue").SendEventTo(new ProcessFunctionTargetBuilder(reportStep)); return processBuilder; @@ -464,7 +878,7 @@ private ProcessBuilder CreateProcessWithError(string name) #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); + KernelProcessStepInfo? stepInfo = processInfo.Steps.FirstOrDefault(s => s.State.StepId == stepName); Assert.NotNull(stepInfo); var outputStepResult = stepInfo.State as KernelProcessStepState; Assert.NotNull(outputStepResult?.State); @@ -474,16 +888,13 @@ private void AssertStepStateLastMessage(KernelProcess processInfo, string stepNa Assert.Equal(expectedInvocationCount.Value, outputStepResult.State.InvocationCount); } } - -#if !NET - private void AssertStepState(KernelProcess processInfo, string stepName, Predicate> predicate) where T : class, new() + private void AssertStepState(KernelProcess processInfo, string stepName, Func, bool> predicate) where T : class, new() { - KernelProcessStepInfo? stepInfo = processInfo.Steps.FirstOrDefault(s => s.State.Name == stepName); + KernelProcessStepInfo? stepInfo = processInfo.Steps.FirstOrDefault(s => s.State.StepId == stepName); Assert.NotNull(stepInfo); var outputStepResult = stepInfo.State as KernelProcessStepState; Assert.NotNull(outputStepResult?.State); Assert.True(predicate(outputStepResult)); } -#endif #endregion } diff --git a/dotnet/src/Experimental/Process.IntegrationTests.Shared/TestSettings/OpenAIConfiguration.cs b/dotnet/src/Experimental/Process.IntegrationTests.Shared/TestSettings/OpenAIConfiguration.cs index cb3884e3bdfc..daa907fa97f2 100644 --- a/dotnet/src/Experimental/Process.IntegrationTests.Shared/TestSettings/OpenAIConfiguration.cs +++ b/dotnet/src/Experimental/Process.IntegrationTests.Shared/TestSettings/OpenAIConfiguration.cs @@ -2,7 +2,7 @@ using System.Diagnostics.CodeAnalysis; -namespace SemanticKernel.IntegrationTests.TestSettings; +namespace Microsoft.SemanticKernel.IntegrationTests.TestSettings; [SuppressMessage("Performance", "CA1812:Internal class that is apparently never instantiated", Justification = "Configuration classes are instantiated through IConfiguration.")] diff --git a/dotnet/src/Experimental/Process.IntegrationTests.Shared/TestSettings/ProcessTestGroup.cs b/dotnet/src/Experimental/Process.IntegrationTests.Shared/TestSettings/ProcessTestGroup.cs index 1bcc6894f7e4..45d185d97f2d 100644 --- a/dotnet/src/Experimental/Process.IntegrationTests.Shared/TestSettings/ProcessTestGroup.cs +++ b/dotnet/src/Experimental/Process.IntegrationTests.Shared/TestSettings/ProcessTestGroup.cs @@ -4,7 +4,7 @@ using Xunit; #pragma warning restore IDE0005 // Using directive is unnecessary. -namespace SemanticKernel.Process.IntegrationTests; +namespace Microsoft.SemanticKernel.Process.IntegrationTests; /// /// A collection definition for shared process tests. diff --git a/dotnet/src/Experimental/Process.LocalRuntime/LocalAgentStep.cs b/dotnet/src/Experimental/Process.LocalRuntime/LocalAgentStep.cs index 6ce3fc248b06..a0d496a7d7fc 100644 --- a/dotnet/src/Experimental/Process.LocalRuntime/LocalAgentStep.cs +++ b/dotnet/src/Experimental/Process.LocalRuntime/LocalAgentStep.cs @@ -27,7 +27,7 @@ public LocalAgentStep(KernelProcessAgentStep stepInfo, Kernel kernel, KernelProc protected override ValueTask InitializeStepAsync() { this._stepInstance = new KernelProcessAgentExecutorInternal(this._stepInfo, this._agentThread, this._processStateManager); - var kernelPlugin = KernelPluginFactory.CreateFromObject(this._stepInstance, pluginName: this._stepInfo.State.Name); + var kernelPlugin = KernelPluginFactory.CreateFromObject(this._stepInstance, pluginName: this._stepInfo.State.StepId); // Load the kernel functions foreach (KernelFunction f in kernelPlugin) diff --git a/dotnet/src/Experimental/Process.LocalRuntime/LocalEdgeGroupProcessor.cs b/dotnet/src/Experimental/Process.LocalRuntime/LocalEdgeGroupProcessor.cs index 887a32511421..1503a3a842a2 100644 --- a/dotnet/src/Experimental/Process.LocalRuntime/LocalEdgeGroupProcessor.cs +++ b/dotnet/src/Experimental/Process.LocalRuntime/LocalEdgeGroupProcessor.cs @@ -7,10 +7,11 @@ namespace Microsoft.SemanticKernel; internal class LocalEdgeGroupProcessor { private readonly KernelProcessEdgeGroup _edgeGroup; - private readonly Dictionary _messageData = []; private HashSet _requiredMessages = new(); private HashSet _absentMessages = new(); + public Dictionary MessageData { get; private set; } = []; + public LocalEdgeGroupProcessor(KernelProcessEdgeGroup edgeGroup) { Verify.NotNull(edgeGroup, nameof(edgeGroup)); @@ -19,6 +20,30 @@ public LocalEdgeGroupProcessor(KernelProcessEdgeGroup edgeGroup) this.InitializeEventTracking(); } + public void ClearMessageData() + { + this.MessageData.Clear(); + this.InitializeEventTracking(); + } + + public bool RehydrateMessageData(Dictionary cachedMessageData) + { + if (cachedMessageData == null || cachedMessageData.Count == 0) + { + return false; + } + + // Add check to ensure message data values have supported types + + foreach (var message in cachedMessageData) + { + this.MessageData[message.Key] = message.Value; + } + this._absentMessages.RemoveWhere(message => cachedMessageData.ContainsKey(message)); + + return true; + } + public bool TryGetResult(ProcessMessage message, out Dictionary? result) { string messageKey = this.GetKeyForMessageSource(message); @@ -27,13 +52,22 @@ public bool TryGetResult(ProcessMessage message, out Dictionary throw new KernelException($"Message {messageKey} is not expected for edge group {this._edgeGroup.GroupId}."); } - this._messageData[messageKey] = (message.TargetEventData as KernelProcessEventData)!.ToObject(); + if (message.TargetEventData is KernelProcessEventData processEventData) + { + // used by events from steps + this.MessageData[messageKey] = processEventData.ToObject(); + } + else + { + // used by events that are process input events + this.MessageData[messageKey] = message.TargetEventData; + } this._absentMessages.Remove(messageKey); if (this._absentMessages.Count == 0) { // We have received all required events so forward them to the target - result = (Dictionary?)this._edgeGroup.InputMapping(this._messageData); + result = (Dictionary?)this._edgeGroup.InputMapping(this.MessageData); // TODO: Reset state according to configured logic i.e. reset after first message or after all messages are received. this.InitializeEventTracking(); diff --git a/dotnet/src/Experimental/Process.LocalRuntime/LocalKernelProcessContext.cs b/dotnet/src/Experimental/Process.LocalRuntime/LocalKernelProcessContext.cs index f4ee96daac1a..4c8689e7dc1b 100644 --- a/dotnet/src/Experimental/Process.LocalRuntime/LocalKernelProcessContext.cs +++ b/dotnet/src/Experimental/Process.LocalRuntime/LocalKernelProcessContext.cs @@ -12,28 +12,31 @@ public sealed class LocalKernelProcessContext : KernelProcessContext, System.IAs private readonly LocalProcess _localProcess; private readonly Kernel _kernel; - internal LocalKernelProcessContext(KernelProcess process, Kernel kernel, ProcessEventProxy? eventProxy = null, IExternalKernelProcessMessageChannel? externalMessageChannel = null) + private readonly ProcessStorageManager? _storageConnector; + + internal LocalKernelProcessContext(KernelProcess process, Kernel kernel, ProcessEventProxy? eventProxy = null, IExternalKernelProcessMessageChannel? externalMessageChannel = null, IProcessStorageConnector? storageConnector = null, string? instanceId = null) { Verify.NotNull(process, nameof(process)); Verify.NotNull(kernel, nameof(kernel)); - Verify.NotNullOrWhiteSpace(process.State?.Name); + Verify.NotNullOrWhiteSpace(process.State?.StepId); + + if (storageConnector != null) + { + this._storageConnector = new(storageConnector); + } this._kernel = kernel; - this._localProcess = new LocalProcess(process, kernel) + this._localProcess = new LocalProcess(process, kernel, instanceId) { EventProxy = eventProxy, ExternalMessageChannel = externalMessageChannel, + StorageManager = this._storageConnector, }; } internal Task StartWithEventAsync(KernelProcessEvent initialEvent, Kernel? kernel = null) => this._localProcess.RunOnceAsync(initialEvent, kernel); - //internal RunUntilEndAsync(KernelProcessEvent initialEvent, Kernel? kernel = null, TimeSpan? timeout = null) - //{ - - //} - /// /// Sends a message to the process. /// diff --git a/dotnet/src/Experimental/Process.LocalRuntime/LocalKernelProcessFactory.cs b/dotnet/src/Experimental/Process.LocalRuntime/LocalKernelProcessFactory.cs index 125f22dcface..ea955d78d50e 100644 --- a/dotnet/src/Experimental/Process.LocalRuntime/LocalKernelProcessFactory.cs +++ b/dotnet/src/Experimental/Process.LocalRuntime/LocalKernelProcessFactory.cs @@ -16,13 +16,22 @@ public static class LocalKernelProcessFactory /// Required: The to start running. /// Required: An instance of /// Required: The initial event to start the process. + /// Optional: id to be assigined to the running process, if null it will be assigned during runtime /// Optional: an instance of . + /// Optional: an instance of . /// An instance of that can be used to interrogate or stop the running process. - public static async Task StartAsync(this KernelProcess process, Kernel kernel, KernelProcessEvent initialEvent, IExternalKernelProcessMessageChannel? externalMessageChannel = null) + public static async Task StartAsync( + this KernelProcess process, + Kernel kernel, + KernelProcessEvent initialEvent, + string? processId = null, + IExternalKernelProcessMessageChannel? externalMessageChannel = null, + IProcessStorageConnector? storageConnector = null) { Verify.NotNull(initialEvent, nameof(initialEvent)); + Verify.NotNullOrWhiteSpace(initialEvent.Id, nameof(initialEvent.Id)); - LocalKernelProcessContext processContext = new(process, kernel, null, externalMessageChannel); + LocalKernelProcessContext processContext = new(process, kernel, null, externalMessageChannel, storageConnector, instanceId: processId); await processContext.StartWithEventAsync(initialEvent).ConfigureAwait(false); return processContext; } diff --git a/dotnet/src/Experimental/Process.LocalRuntime/LocalMap.cs b/dotnet/src/Experimental/Process.LocalRuntime/LocalMap.cs index a46d7f3a9663..706b8b3032c3 100644 --- a/dotnet/src/Experimental/Process.LocalRuntime/LocalMap.cs +++ b/dotnet/src/Experimental/Process.LocalRuntime/LocalMap.cs @@ -27,7 +27,7 @@ internal LocalMap(KernelProcessMap map, Kernel kernel) : base(map, kernel) { this._map = map; - this._logger = this._kernel.LoggerFactory?.CreateLogger(this._map.State.Name) ?? new NullLogger(); + this._logger = this._kernel.LoggerFactory?.CreateLogger(this._map.State.StepId) ?? new NullLogger(); this._mapEvents = [.. map.Edges.Keys.Select(key => key.Split(ProcessConstants.EventIdSeparator).Last())]; } diff --git a/dotnet/src/Experimental/Process.LocalRuntime/LocalProcess.cs b/dotnet/src/Experimental/Process.LocalRuntime/LocalProcess.cs index 77fa480c3b4e..b212bc158626 100644 --- a/dotnet/src/Experimental/Process.LocalRuntime/LocalProcess.cs +++ b/dotnet/src/Experimental/Process.LocalRuntime/LocalProcess.cs @@ -37,13 +37,16 @@ internal sealed class LocalProcess : LocalStep, System.IAsyncDisposable private CancellationTokenSource? _processCancelSource; private ProcessStateManager? _processStateManager; + private List _runningStepIds = []; + /// /// Initializes a new instance of the class. /// /// The instance. /// An instance of - internal LocalProcess(KernelProcess process, Kernel kernel) - : base(process, kernel) + /// id to be used for LocalProcess as unique identifier + internal LocalProcess(KernelProcess process, Kernel kernel, string? instanceId = null) + : base(process, kernel, instanceId: instanceId) { Verify.NotNull(process.Steps); @@ -55,7 +58,15 @@ internal LocalProcess(KernelProcess process, Kernel kernel) this._joinableTaskFactory = new JoinableTaskFactory(this._joinableTaskContext); this._logger = this._kernel.LoggerFactory?.CreateLogger(this.Name) ?? new NullLogger(); // if parent id is null this is the root process + // if parent id is null this is the root process this.RootProcessId = this.ParentProcessId == null ? this.Id : null; + this._stepState.ParentId = this.ParentProcessId; + } + + internal KernelProcess Process + { + // Special get to ensure returning most updated process data + get => this._process with { State = this._stepState, Steps = this._steps.Select(step => step._stepInfo).ToList() }; } /// @@ -63,6 +74,8 @@ internal LocalProcess(KernelProcess process, Kernel kernel) /// internal string? RootProcessId { get; init; } + internal new ProcessStorageManager? StorageManager { get; init; } + /// /// Starts the process with an initial event and an optional kernel. /// @@ -206,6 +219,22 @@ private async ValueTask InitializeProcessAsync() { // Initialize the input and output edges for the process this._outputEdges = this._process.Edges.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToList()); + Dictionary processInfoInstanceMap = []; + + if (this.StorageManager != null) + { + await this.StorageManager.InitializeAsync().ConfigureAwait(false); + await this.FetchSavedProcessDataAsync().ConfigureAwait(false); + + // Trying to restore step running ids instances from storage before creation of steps + var processInfo = await this.StorageManager.GetProcessInfoAsync(this.Process).ConfigureAwait(false); + if (processInfo != null && processInfo.Steps.Count > 0) + { + processInfoInstanceMap = processInfo.Steps; + } + } + + bool usingExistingProcessInstances = processInfoInstanceMap.Count > 0; // TODO: Pull user state from persisted state on resume. this._processStateManager = new ProcessStateManager(this._process.UserStateType, null); @@ -247,44 +276,49 @@ private async ValueTask InitializeProcessAsync() LocalStep? localStep = null; // The current step should already have a name. - Verify.NotNull(step.State?.Name); + Verify.NotNull(step.State?.StepId); - if (step is KernelProcess processStep) + // Assign id to kernelStepInfo if any before creation of Local components + if (!processInfoInstanceMap.TryGetValue(step.State.StepId, out string? stepId) && stepId == null) { - // The process will only have an Id if its already been executed. - if (string.IsNullOrWhiteSpace(processStep.State.Id)) - { - processStep = processStep with { State = processStep.State with { Id = Guid.NewGuid().ToString() } }; - } + stepId = this.GetChildStepId(); + } + KernelProcessStepInfo stepInfo = step.CloneWithIdAndEdges(stepId, this._logger); + + if (stepInfo is KernelProcess processStep) + { + // Subprocess should be created with an assigned id, only root process can be without the id + Verify.NotNullOrWhiteSpace(processStep.State.RunId); localStep = - new LocalProcess(processStep, this._kernel) + new LocalProcess(processStep, this._kernel, stepId) { ParentProcessId = this.Id, RootProcessId = this.RootProcessId, EventProxy = this.EventProxy, ExternalMessageChannel = this.ExternalMessageChannel, + StorageManager = this.StorageManager, }; } - else if (step is KernelProcessMap mapStep) + else if (stepInfo is KernelProcessMap mapStep) { + mapStep = mapStep with { Operation = mapStep.Operation with { State = mapStep.Operation.State with { RunId = mapStep.Operation.State.StepId } } }; localStep = new LocalMap(mapStep, this._kernel) { ParentProcessId = this.Id, }; } - else if (step is KernelProcessProxy proxyStep) + else if (stepInfo is KernelProcessProxy proxyStep) { localStep = - new LocalProxy(proxyStep, this._kernel) + new LocalProxy(proxyStep, this._kernel, this.ExternalMessageChannel) { ParentProcessId = this.RootProcessId, EventProxy = this.EventProxy, - ExternalMessageChannel = this.ExternalMessageChannel }; } - else if (step is KernelProcessAgentStep agentStep) + else if (stepInfo is KernelProcessAgentStep agentStep) { if (!this._threads.TryGetValue(agentStep.ThreadName, out KernelProcessAgentThread? thread) || thread is null) { @@ -296,19 +330,100 @@ private async ValueTask InitializeProcessAsync() else { // The current step should already have an Id. - Verify.NotNull(step.State?.Id); + Verify.NotNull(stepInfo.State?.RunId); localStep = - new LocalStep(step, this._kernel) + new LocalStep(stepInfo, this._kernel, parentProcessId: this.Id) { - ParentProcessId = this.Id, - EventProxy = this.EventProxy + EventProxy = this.EventProxy, + StorageManager = this.StorageManager, }; } this._steps.Add(localStep); } + + // Now that children ids are fetched from storage if any + if (usingExistingProcessInstances) + { + await this.FetchProcessChildrenDataAsync().ConfigureAwait(false); + } + + // Process steps local instances have been created, saving process info + await this.SaveProcessInfoAsync().ConfigureAwait(false); + } + + #region Storage related methods + private string GetChildStepId() + { + return $"{this.Id}_{Guid.NewGuid()}"; + } + + /// + /// Fetches process data from storage and children data if child is a step. + /// For subprocesses, subprocess children data is fetched by the subprocess itself when it initializes. + /// + /// + private async Task FetchSavedProcessDataAsync() + { + if (this.StorageManager != null) + { + await this.StorageManager.FetchProcessDataAsync(this.Process).ConfigureAwait(false); + } + } + + private async Task FetchProcessChildrenDataAsync() + { + if (this.StorageManager != null) + { + foreach (var step in this._steps) + { + if (step is not LocalProcess) + { + await this.StorageManager.FetchStepDataAsync(step._stepInfo).ConfigureAwait(false); + } + } + } + } + + private async Task SaveProcessInfoAsync() + { + if (this.StorageManager != null) + { + await this.StorageManager.SaveProcessInfoAsync(this.Process).ConfigureAwait(false); + } + } + + private async Task SaveProcessDataAsync() + { + if (this.StorageManager != null) + { + await this.StorageManager.SaveProcessInfoAsync(this.Process).ConfigureAwait(false); + // TODO: Add other process related components to be persisted: shared state, external undelivered messages, etc + } + } + + private async Task SaveProcessAndChildrenDataToStorageAsync() + { + if (this.StorageManager != null) + { + await this.StorageManager.SaveProcessDataToStorageAsync(this.Process).ConfigureAwait(false); + + // Filtering saving only steps that ran in last superstep execution loop + var executedSteps = this._steps.Where(step => this._runningStepIds.Contains(step.Id)) ?? []; + + foreach (var step in executedSteps) + { + if (step is not LocalProcess subprocessStep) + { + // Local Process when they run they do their own checkpointing, no need to do it again. + await this.StorageManager.SaveStepDataToStorageAsync(step.StepInfo).ConfigureAwait(false); + } + } + this._runningStepIds = []; + } } + #endregion /// /// Initializes this process as a step within another process. @@ -360,6 +475,8 @@ private async Task Internal_ExecuteAsync(Kernel? kernel = null, int maxSuperstep } List messageTasks = []; + this._runningStepIds = []; + foreach (var message in messagesToProcess) { // Check for end condition @@ -369,14 +486,28 @@ private async Task Internal_ExecuteAsync(Kernel? kernel = null, int maxSuperstep break; } - var destinationStep = this._steps.First(v => v.Id == message.DestinationId); + var destinationStep = this._steps.FirstOrDefault(v => v.Name == message.DestinationId); - // Send a message to the step - messageTasks.Add(destinationStep.HandleMessageAsync(message)); - finalStep = destinationStep; + if (destinationStep != null) + { + this._runningStepIds.Add(destinationStep.Id); + // Send a message to the step + messageTasks.Add(destinationStep.HandleMessageAsync(message)); + finalStep = destinationStep; + } + else + { + this._logger?.LogWarning("Could not find destination for event {0} from step {1}", message.SourceEventId, message.SourceId); + } } await Task.WhenAll(messageTasks).ConfigureAwait(false); + + // All steps have executed saving process data: state, events, etc + await this.SaveProcessDataAsync().ConfigureAwait(false); + + // Now saving checkpoint of process to storage + await this.SaveProcessAndChildrenDataToStorageAsync().ConfigureAwait(false); } } catch (Exception ex) @@ -453,7 +584,7 @@ private async Task EnqueueEdgesAsync(IEnumerable edges, Queue { foreach (KernelProcessEdge edge in defaultConditionedEdges) { - ProcessMessage message = ProcessMessageFactory.CreateFromEdge(edge, this._process.State.Id!, null, null); + ProcessMessage message = ProcessMessageFactory.CreateFromEdge(edge, this._process.State.RunId!, null, null); messageChannel.Enqueue(message); // TODO: Handle state here as well @@ -482,7 +613,7 @@ private async Task EnqueueOnEnterMessagesAsync(Queue messageChan var processEvent = new ProcessEvent { Namespace = this.Name, - SourceId = this._process.State.Id!, + SourceId = this._process.State.RunId!, Data = null, Visibility = KernelProcessEventVisibility.Internal }; diff --git a/dotnet/src/Experimental/Process.LocalRuntime/LocalProxy.cs b/dotnet/src/Experimental/Process.LocalRuntime/LocalProxy.cs index 221ffcd2b371..46a9a37f462b 100644 --- a/dotnet/src/Experimental/Process.LocalRuntime/LocalProxy.cs +++ b/dotnet/src/Experimental/Process.LocalRuntime/LocalProxy.cs @@ -23,11 +23,20 @@ internal sealed class LocalProxy : LocalStep /// /// an instance of /// An instance of - internal LocalProxy(KernelProcessProxy proxy, Kernel kernel) + /// An instance of + internal LocalProxy(KernelProcessProxy proxy, Kernel kernel, IExternalKernelProcessMessageChannel? externalMessageChannel) : base(proxy, kernel) { this._proxy = proxy; - this._logger = this._kernel.LoggerFactory?.CreateLogger(this._proxy.State.Name) ?? new NullLogger(); + this._logger = this._kernel.LoggerFactory?.CreateLogger(this._proxy.State.StepId) ?? new NullLogger(); + this.ExternalMessageChannel = externalMessageChannel; + + this.InitializeStepInitialInputs(); + } + + internal override void PopulateInitialInputs() + { + this._initialInputs = this.FindInputChannels(this._functions, this._logger, this.ExternalMessageChannel); } internal override void AssignStepFunctionParameterValues(ProcessMessage message) diff --git a/dotnet/src/Experimental/Process.LocalRuntime/LocalStep.cs b/dotnet/src/Experimental/Process.LocalRuntime/LocalStep.cs index 286ab272d3d7..50357c62bcdc 100644 --- a/dotnet/src/Experimental/Process.LocalRuntime/LocalStep.cs +++ b/dotnet/src/Experimental/Process.LocalRuntime/LocalStep.cs @@ -40,25 +40,35 @@ internal class LocalStep : IKernelProcessMessageChannel /// An instance of /// Required. An instance of . /// Optional. The Id of the parent process if one exists. - public LocalStep(KernelProcessStepInfo stepInfo, Kernel kernel, string? parentProcessId = null) + /// Optional: Id of the process if given + public LocalStep(KernelProcessStepInfo stepInfo, Kernel kernel, string? parentProcessId = null, string? instanceId = null) { Verify.NotNull(kernel, nameof(kernel)); Verify.NotNull(stepInfo, nameof(stepInfo)); - // This special handling will be removed with the refactoring of KernelProcessState - if (string.IsNullOrEmpty(stepInfo.State.Id) && stepInfo is KernelProcess) + if (stepInfo is KernelProcess) { - stepInfo = stepInfo with { State = stepInfo.State with { Id = Guid.NewGuid().ToString() } }; + // Only KernelProcess can have a null Id if it is the root process + stepInfo = stepInfo with { State = stepInfo.State with { RunId = instanceId ?? Guid.NewGuid().ToString() } }; } + // For any step that is not a process, step id must already be assigned from parent process in the step state + Verify.NotNullOrWhiteSpace(stepInfo.State.RunId); - Verify.NotNull(stepInfo.State.Id); - - this.ParentProcessId = parentProcessId; this._kernel = kernel; this._stepInfo = stepInfo; - this._stepState = stepInfo.State; - this._initializeTask = new Lazy(this.InitializeStepAsync); this._logger = this._kernel.LoggerFactory?.CreateLogger(this._stepInfo.InnerStepType) ?? new NullLogger(); + + if (stepInfo is not KernelProcess and not KernelProcessMap and not KernelProcessProxy and not KernelProcessAgentStep) + { + // Special initialization occurs with specific substep types + this.InitializeStepInitialInputs(); + } + + Verify.NotNull(stepInfo.State.RunId); + + this.ParentProcessId = parentProcessId; + this._stepState = stepInfo.State with { ParentId = parentProcessId }; + this._initializeTask = new Lazy(this.InitializeStepAsync); this._outputEdges = this._stepInfo.Edges.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToList()); this._eventNamespace = this.Id; this._edgeGroupProcessors = this._stepInfo.IncomingEdgeGroups?.ToDictionary(kvp => kvp.Key, kvp => new LocalEdgeGroupProcessor(kvp.Value)) ?? []; @@ -72,12 +82,12 @@ public LocalStep(KernelProcessStepInfo stepInfo, Kernel kernel, string? parentPr /// /// The name of the step. /// - internal string Name => this._stepInfo.State.Name!; + internal string Name => this._stepInfo.State.StepId!; /// /// The Id of the step. /// - internal string Id => this._stepInfo.State.Id!; + internal string Id => this._stepInfo.State.RunId!; /// /// An event proxy that can be used to intercept events emitted by the step. @@ -86,6 +96,43 @@ public LocalStep(KernelProcessStepInfo stepInfo, Kernel kernel, string? parentPr internal IExternalKernelProcessMessageChannel? ExternalMessageChannel { get; init; } + internal IProcessStepStorageOperations? StorageManager { get; init; } + + internal KernelProcessStepInfo StepInfo + { + get => this._stepInfo with { State = this._stepState }; + } + + internal void InitializeStepInitialInputs() + { + // Instantiate an instance of the inner step object + this._stepInstance = this.CreateStepInstance(); + + var kernelPlugin = KernelPluginFactory.CreateFromObject(this._stepInstance, pluginName: this._stepInfo.State.StepId); + + // Load the kernel functions + foreach (KernelFunction f in kernelPlugin) + { + this._functions.Add(f.Name, f); + } + + // Initialize the input channels + this.PopulateInitialInputs(); + } + + internal virtual KernelProcessStep CreateStepInstance() + { + var stepInstance = (KernelProcessStep)ActivatorUtilities.CreateInstance(this._kernel.Services, this._stepInfo.InnerStepType); + typeof(KernelProcessStep).GetProperty(nameof(KernelProcessStep.StepName))?.SetValue(stepInstance, this._stepInfo.State.StepId); + + return stepInstance; + } + + internal virtual void PopulateInitialInputs() + { + this._initialInputs = this.FindInputChannels(this._functions, this._logger, this.ExternalMessageChannel); + } + /// /// Retrieves all events that have been emitted by this step in the previous superstep. /// @@ -198,16 +245,26 @@ internal virtual async Task HandleMessageAsync(ProcessMessage message) if (!edgeGroupProcessor.TryGetResult(message, out Dictionary? result)) { - // The edge group processor has not received all required messages yet. + await this.SaveStepEventsAsync().ConfigureAwait(false); return; } + // Saving values with updated new edge value + await this.SaveStepEventsAsync().ConfigureAwait(false); // The edge group processor has received all required messages and has produced a result. message = message with { Values = result ?? [] }; + + // Add the message values to the inputs for the function + this.AssignStepFunctionParameterValues(message); } + else + { + // Add the message values to the inputs for the function + this.AssignStepFunctionParameterValues(message); - // Add the message values to the inputs for the function - this.AssignStepFunctionParameterValues(message); + // TODO: Add saving last message received when no edge groups are used + //await this.SaveStepEventsAsync(message).ConfigureAwait(false); + } // If we're still waiting for inputs on all of our functions then don't do anything. List invocableFunctions = this._inputs.Where(i => i.Value != null && i.Value.All(v => v.Value != null)).Select(i => i.Key).ToList(); @@ -264,6 +321,18 @@ internal virtual async Task HandleMessageAsync(ProcessMessage message) { // Reset the inputs for the function that was just executed this._inputs[targetFunction] = new(this._initialInputs[targetFunction] ?? []); + + if (!string.IsNullOrEmpty(message.GroupId) && this._edgeGroupProcessors != null) + { + // Only clearing out edge processor with most recent group id received + if (this._edgeGroupProcessors.TryGetValue(message.GroupId, out LocalEdgeGroupProcessor? edgeGroupProcessor) && edgeGroupProcessor != null) + { + edgeGroupProcessor.ClearMessageData(); + } + } + + await this.SaveStepStateAsync().ConfigureAwait(false); + await this.SaveStepEventsAsync().ConfigureAwait(false); } #pragma warning restore CA1031 // Do not catch general exception types } @@ -275,31 +344,30 @@ internal virtual async Task HandleMessageAsync(ProcessMessage message) /// protected virtual async ValueTask InitializeStepAsync() { - // Instantiate an instance of the inner step object - this._stepInstance = (KernelProcessStep)ActivatorUtilities.CreateInstance(this._kernel.Services, this._stepInfo.InnerStepType); - var kernelPlugin = KernelPluginFactory.CreateFromObject(this._stepInstance, pluginName: this._stepInfo.State.Name); - - // Load the kernel functions - foreach (KernelFunction f in kernelPlugin) + if (this._initialInputs == null || this._stepInstance == null) { - this._functions.Add(f.Name, f); + throw new KernelException("Initial Inputs have not been initialize, cannot initialize step properly"); } - // Initialize the input channels - if (this._stepInfo is KernelProcessAgentStep agentStep) + // Populating step function inputs + this._inputs = this._initialInputs.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)); + + // Activate the step with user-defined state if needed + Type stateType = this._stepInfo.InnerStepType.ExtractStateType(out Type? userStateType, this._logger); + KernelProcessStepState? stateObject = null; + + if (this.StorageManager != null) { - this._initialInputs = this.FindInputChannels(this._functions, this._logger, this.ExternalMessageChannel, agentStep.AgentDefinition); + stateObject = await this.StorageManager.GetStepStateAsync(this.StepInfo).ConfigureAwait(false); + await this.TryRestoreCachedUnprocessedInputValuesAsync().ConfigureAwait(false); } - else + + if (stateObject == null) { - this._initialInputs = this.FindInputChannels(this._functions, this._logger, this.ExternalMessageChannel); + // no previous state in storage found, try using the default state instead + stateObject = this._stepInfo.State; } - this._inputs = this._initialInputs.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)); - - // Activate the step with user-defined state if needed - Type stateType = this._stepInfo.InnerStepType.ExtractStateType(out Type? userStateType, this._logger); - KernelProcessStepState stateObject = this._stepInfo.State; stateObject.InitializeUserState(stateType, userStateType); if (stateObject is null) @@ -319,6 +387,8 @@ protected virtual async ValueTask InitializeStepAsync() await this._stepInstance.ActivateAsync(stateObject).ConfigureAwait(false); await activateTask.ConfigureAwait(false); + + await this.SaveStepInfoAsync().ConfigureAwait(false); } /// @@ -330,6 +400,62 @@ public virtual Task DeinitializeStepAsync() return Task.CompletedTask; } + #region Storage related functions + + internal virtual async Task SaveStepStateAsync() + { + if (this.StorageManager != null) + { + await this.StorageManager.SaveStepStateAsync(this.StepInfo).ConfigureAwait(false); + } + } + + internal async Task SaveStepInfoAsync() + { + if (this.StorageManager != null) + { + await this.StorageManager.SaveStepInfoAsync(this.StepInfo).ConfigureAwait(false); + } + } + + /// + /// Helper function that isolate use of Local Runtime specific objects when calling StorageManager + /// + /// + internal async Task SaveStepEventsAsync(ProcessMessage? lastMessageReceived = null) + { + var edgeGroupValues = this._edgeGroupProcessors.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.MessageData); + if (this.StorageManager != null) + { + await this.StorageManager.SaveStepEventsAsync(this.StepInfo, edgeGroupValues).ConfigureAwait(false); + } + } + + private async Task TryRestoreCachedUnprocessedInputValuesAsync() + { + if (this._initialInputs == null) + { + throw new KernelException("Initial Inputs have not been initialize, cannot initialize step properly"); + } + + if (this.StorageManager != null) + { + var stepEventData = await this.StorageManager.GetStepEventsAsync(this.StepInfo).ConfigureAwait(false); + if (this._edgeGroupProcessors != null && stepEventData?.EdgesData != null) + { + foreach (var edgeGroup in this._edgeGroupProcessors) + { + if (stepEventData.EdgesData.TryGetValue(edgeGroup.Key, out Dictionary? edgeGroupData) && edgeGroupData != null) + { + edgeGroup.Value.RehydrateMessageData(edgeGroupData.ToDictionary(edgeGroupData => edgeGroupData.Key, edgeGroupData => edgeGroupData.Value?.ToObject())); + } + } + } + } + } + + #endregion + /// /// Invokes the provides function with the provided kernel and arguments. /// diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/MapActor.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/MapActor.cs index 5892bd0783d9..dedbc35f5419 100644 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/MapActor.cs +++ b/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/MapActor.cs @@ -75,7 +75,7 @@ protected override async Task OnActivateAsync() /// /// The name of the step. /// - protected override string Name => this._mapInfo?.State.Name ?? throw new KernelException("The Map must be initialized before accessing the Name property."); + protected override string Name => this._mapInfo?.State.StepId ?? throw new KernelException("The Map must be initialized before accessing the Name property."); #endregion @@ -91,7 +91,7 @@ internal override async Task HandleMessageAsync(ProcessMessage message) List mapOperations = []; foreach (var value in inputValues) { - KernelProcess mapProcess = mapOperation with { State = mapOperation.State with { Id = $"{this.Name}-{mapOperations.Count}-{Guid.NewGuid():N}" } }; + KernelProcess mapProcess = mapOperation with { State = mapOperation.State with { RunId = $"{this.Name}-{mapOperations.Count}-{Guid.NewGuid():N}" } }; DaprKernelProcessContext processContext = new(mapProcess); Task processTask = processContext.StartWithEventAsync( @@ -172,9 +172,9 @@ private void InitializeMapActor(DaprMapInfo mapInfo, string? parentProcessId) this._mapInfo = mapInfo; this._map = mapInfo.ToKernelProcessMap(); this.ParentProcessId = parentProcessId; - this._logger = this._kernel.LoggerFactory?.CreateLogger(this._mapInfo.State.Name) ?? new NullLogger(); + this._logger = this._kernel.LoggerFactory?.CreateLogger(this._mapInfo.State.StepId) ?? new NullLogger(); this._outputEdges = this._mapInfo.Edges.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToList()); - this._eventNamespace = $"{this._mapInfo.State.Name}_{this._mapInfo.State.Id}"; + this._eventNamespace = $"{this._mapInfo.State.StepId}_{this._mapInfo.State.RunId}"; // Capture the events that the map is interested in as hashtable for performant lookup this._mapEvents = [.. this._mapInfo.Edges.Keys.Select(key => key.Split(ProcessConstants.EventIdSeparator).Last())]; diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ProcessActor.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ProcessActor.cs index f28aba6783bc..8ac0b2f8a49a 100644 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ProcessActor.cs +++ b/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ProcessActor.cs @@ -184,7 +184,7 @@ protected override async Task OnActivateAsync() /// /// The name of the step. /// - protected override string Name => this._process?.State.Name ?? throw new KernelException("The Process must be initialized before accessing the Name property.").Log(this._logger); + protected override string Name => this._process?.State.StepId ?? throw new KernelException("The Process must be initialized before accessing the Name property.").Log(this._logger); #endregion @@ -238,7 +238,7 @@ private async Task InitializeProcessActorAsync(DaprProcessInfo processInfo, stri this.ParentProcessId = parentProcessId; this._process = processInfo; this._stepsInfos = [.. this._process.Steps]; - this._logger = this._kernel.LoggerFactory?.CreateLogger(this._process.State.Name) ?? new NullLogger(); + this._logger = this._kernel.LoggerFactory?.CreateLogger(this._process.State.StepId) ?? new NullLogger(); if (!string.IsNullOrWhiteSpace(eventProxyStepId)) { this.EventProxyStepId = new ActorId(eventProxyStepId); @@ -253,18 +253,18 @@ private async Task InitializeProcessActorAsync(DaprProcessInfo processInfo, stri IStep? stepActor = null; // The current step should already have a name. - Verify.NotNull(step.State?.Name); + Verify.NotNull(step.State?.StepId); if (step is DaprProcessInfo processStep) { // The process will only have an Id if its already been executed. - if (string.IsNullOrWhiteSpace(processStep.State.Id)) + if (string.IsNullOrWhiteSpace(processStep.State.RunId)) { - processStep = processStep with { State = processStep.State with { Id = Guid.NewGuid().ToString() } }; + processStep = processStep with { State = processStep.State with { RunId = Guid.NewGuid().ToString() } }; } // Initialize the step as a process. - var scopedProcessId = this.ScopedActorId(new ActorId(processStep.State.Id!)); + var scopedProcessId = this.ScopedActorId(new ActorId(processStep.State.RunId!)); var processActor = this.ProxyFactory.CreateActorProxy(scopedProcessId, nameof(ProcessActor)); await processActor.InitializeProcessAsync(processStep, this.Id.GetId(), eventProxyStepId).ConfigureAwait(false); stepActor = this.ProxyFactory.CreateActorProxy(scopedProcessId, nameof(ProcessActor)); @@ -272,7 +272,7 @@ private async Task InitializeProcessActorAsync(DaprProcessInfo processInfo, stri else if (step is DaprMapInfo mapStep) { // Initialize the step as a map. - ActorId scopedMapId = this.ScopedActorId(new ActorId(mapStep.State.Id!)); + ActorId scopedMapId = this.ScopedActorId(new ActorId(mapStep.State.RunId!)); IMap mapActor = this.ProxyFactory.CreateActorProxy(scopedMapId, nameof(MapActor)); await mapActor.InitializeMapAsync(mapStep, this.Id.GetId()).ConfigureAwait(false); stepActor = this.ProxyFactory.CreateActorProxy(scopedMapId, nameof(MapActor)); @@ -280,7 +280,7 @@ private async Task InitializeProcessActorAsync(DaprProcessInfo processInfo, stri else if (step is DaprProxyInfo proxyStep) { // Initialize the step as a proxy - ActorId scopedProxyId = this.ScopedActorId(new ActorId(proxyStep.State.Id!)); + ActorId scopedProxyId = this.ScopedActorId(new ActorId(proxyStep.State.RunId!)); IProxy proxyActor = this.ProxyFactory.CreateActorProxy(scopedProxyId, nameof(ProxyActor)); await proxyActor.InitializeProxyAsync(proxyStep, this.Id.GetId()).ConfigureAwait(false); stepActor = this.ProxyFactory.CreateActorProxy(scopedProxyId, nameof(ProxyActor)); @@ -288,9 +288,9 @@ private async Task InitializeProcessActorAsync(DaprProcessInfo processInfo, stri else { // The current step should already have an Id. - Verify.NotNull(step.State?.Id); + Verify.NotNull(step.State?.RunId); - var scopedStepId = this.ScopedActorId(new ActorId(step.State.Id!)); + var scopedStepId = this.ScopedActorId(new ActorId(step.State.RunId!)); stepActor = this.ProxyFactory.CreateActorProxy(scopedStepId, nameof(StepActor)); await stepActor.InitializeStepAsync(step, this.Id.GetId(), eventProxyStepId).ConfigureAwait(false); } @@ -516,7 +516,7 @@ private ActorId ScopedActorId(ActorId actorId, bool scopeToParent = false) private ProcessEvent ScopedEvent(ProcessEvent daprEvent) { Verify.NotNull(daprEvent); - return daprEvent with { Namespace = $"{this.Name}_{this._process!.State.Id}" }; + return daprEvent with { Namespace = $"{this.Name}_{this._process!.State.RunId}" }; } #endregion diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/StepActor.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/StepActor.cs index 27bda37176fb..8fc9bbd1ecfc 100644 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/StepActor.cs +++ b/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/StepActor.cs @@ -107,7 +107,7 @@ private void InitializeStep(DaprStepInfo stepInfo, string? parentProcessId, stri this._stepState = this._stepInfo.State; this._logger = this._kernel.LoggerFactory?.CreateLogger(this._innerStepType) ?? new NullLogger(); this._outputEdges = this._stepInfo.Edges.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToList()); - this._eventNamespace = $"{this._stepInfo.State.Name}_{this._stepInfo.State.Id}"; + this._eventNamespace = $"{this._stepInfo.State.StepId}_{this._stepInfo.State.RunId}"; if (!string.IsNullOrWhiteSpace(eventProxyStepId)) { @@ -203,7 +203,7 @@ protected override async Task OnActivateAsync() /// /// The name of the step. /// - protected virtual string Name => this._stepInfo?.State.Name ?? throw new KernelException("The Step must be initialized before accessing the Name property.").Log(this._logger); + protected virtual string Name => this._stepInfo?.State.StepId ?? throw new KernelException("The Step must be initialized before accessing the Name property.").Log(this._logger); /// /// Emits an event from the step. @@ -352,7 +352,7 @@ protected virtual async ValueTask ActivateStepAsync() // Instantiate an instance of the inner step object KernelProcessStep stepInstance = (KernelProcessStep)ActivatorUtilities.CreateInstance(this._kernel.Services, this._innerStepType!); - var kernelPlugin = KernelPluginFactory.CreateFromObject(stepInstance, pluginName: this._stepInfo.State.Name); + var kernelPlugin = KernelPluginFactory.CreateFromObject(stepInstance, pluginName: this._stepInfo.State.StepId); // Load the kernel functions foreach (KernelFunction f in kernelPlugin) diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/DaprKernelProcessContext.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/DaprKernelProcessContext.cs index 64ed08b63820..3b3ba0a0ecb6 100644 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/DaprKernelProcessContext.cs +++ b/dotnet/src/Experimental/Process.Runtime.Dapr/DaprKernelProcessContext.cs @@ -20,15 +20,15 @@ public class DaprKernelProcessContext : KernelProcessContext internal DaprKernelProcessContext(KernelProcess process, IActorProxyFactory? actorProxyFactory = null) { Verify.NotNull(process); - Verify.NotNullOrWhiteSpace(process.State?.Name); + Verify.NotNullOrWhiteSpace(process.State?.StepId); - if (string.IsNullOrWhiteSpace(process.State.Id)) + if (string.IsNullOrWhiteSpace(process.State.RunId)) { - process = process with { State = process.State with { Id = Guid.NewGuid().ToString() } }; + process = process with { State = process.State with { RunId = Guid.NewGuid().ToString() } }; } this._process = process; - var processId = new ActorId(process.State.Id); + var processId = new ActorId(process.State.RunId); // For a non-dependency-injected application, the static methods on ActorProxy are used. // Since the ActorProxy methods are error prone, try to avoid using them when using @@ -89,6 +89,6 @@ public override async Task GetStateAsync() public override async Task GetProcessIdAsync() { var processInfo = await this._daprProcess.GetProcessInfoAsync().ConfigureAwait(false); - return processInfo.State.Id!; + return processInfo.State.RunId!; } } diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/DaprKernelProcessFactory.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/DaprKernelProcessFactory.cs index 9d2155ae8955..52cda5391c77 100644 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/DaprKernelProcessFactory.cs +++ b/dotnet/src/Experimental/Process.Runtime.Dapr/DaprKernelProcessFactory.cs @@ -20,13 +20,13 @@ public static class DaprKernelProcessFactory public static async Task StartAsync(this KernelProcess process, KernelProcessEvent initialEvent, string? processId = null, IActorProxyFactory? actorProxyFactory = null) { Verify.NotNull(process); - Verify.NotNullOrWhiteSpace(process.State?.Name); + Verify.NotNullOrWhiteSpace(process.State?.StepId); Verify.NotNull(initialEvent); // Assign the process Id if one is provided and the processes does not already have an Id. - if (!string.IsNullOrWhiteSpace(processId) && string.IsNullOrWhiteSpace(process.State.Id)) + if (!string.IsNullOrWhiteSpace(processId) && string.IsNullOrWhiteSpace(process.State.RunId)) { - process = process with { State = process.State with { Id = processId } }; + process = process with { State = process.State with { RunId = processId } }; } DaprKernelProcessContext processContext = new(process, actorProxyFactory); diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/DaprMapInfo.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/DaprMapInfo.cs index 06b3193f6691..f5f090b7ff2b 100644 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/DaprMapInfo.cs +++ b/dotnet/src/Experimental/Process.Runtime.Dapr/DaprMapInfo.cs @@ -27,7 +27,7 @@ public KernelProcessMap ToKernelProcessMap() KernelProcessStepInfo processStepInfo = this.ToKernelProcessStepInfo(); if (this.State is not KernelProcessMapState state) { - throw new KernelException($"Unable to read state from map with name '{this.State.Name}' and Id '{this.State.Id}'."); + throw new KernelException($"Unable to read state from map with name '{this.State.StepId}' and Id '{this.State.RunId}'."); } KernelProcessStepInfo operationStep = diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/DaprProcessInfo.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/DaprProcessInfo.cs index 53553e2ece16..b5154a3803b5 100644 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/DaprProcessInfo.cs +++ b/dotnet/src/Experimental/Process.Runtime.Dapr/DaprProcessInfo.cs @@ -30,7 +30,7 @@ public KernelProcess ToKernelProcess() var processStepInfo = this.ToKernelProcessStepInfo(); if (this.State is not KernelProcessState state) { - throw new KernelException($"Unable to read state from process with name '{this.State.Name}' and Id '{this.State.Id}'."); + throw new KernelException($"Unable to read state from process with name '{this.State.StepId}' and Id '{this.State.RunId}'."); } List steps = []; diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/DaprProxyInfo.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/DaprProxyInfo.cs index 8e7dc7583b6e..173a159cc0f0 100644 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/DaprProxyInfo.cs +++ b/dotnet/src/Experimental/Process.Runtime.Dapr/DaprProxyInfo.cs @@ -27,7 +27,7 @@ public KernelProcessProxy ToKernelProcessProxy() KernelProcessStepInfo processStepInfo = this.ToKernelProcessStepInfo(); if (this.State is not KernelProcessStepState state) { - throw new KernelException($"Unable to read state from proxy with name '{this.State.Name}', Id '{this.State.Id}' and type {this.State.GetType()}."); + throw new KernelException($"Unable to read state from proxy with name '{this.State.StepId}', Id '{this.State.RunId}' and type {this.State.GetType()}."); } return new KernelProcessProxy(state, this.Edges) diff --git a/dotnet/src/Experimental/Process.UnitTests/Core/ProcessBuilderTests.cs b/dotnet/src/Experimental/Process.UnitTests/Core/ProcessBuilderTests.cs index 26c2fc9b0e7e..0fca9e58243f 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Core/ProcessBuilderTests.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Core/ProcessBuilderTests.cs @@ -25,7 +25,7 @@ public void ProcessBuilderInitialization() var processBuilder = new ProcessBuilder(ProcessName); // Assert - Assert.Equal(ProcessName, processBuilder.Name); + Assert.Equal(ProcessName, processBuilder.StepId); Assert.Empty(processBuilder.Steps); } @@ -43,7 +43,7 @@ public void AddStepFromTypeAddsStep() // Assert Assert.Single(processBuilder.Steps); - Assert.Equal(StepName, stepBuilder.Name); + Assert.Equal(StepName, stepBuilder.StepId); } /// @@ -84,7 +84,7 @@ public void AddStepFromProcessAddsSubProcess() // Assert Assert.Single(processBuilder.Steps); - Assert.Equal(SubProcessName, stepBuilder.Name); + Assert.Equal(SubProcessName, stepBuilder.StepId); } /// @@ -143,7 +143,7 @@ public void BuildCreatesKernelProcess() // Assert Assert.NotNull(kernelProcess); - Assert.Equal(ProcessName, kernelProcess.State.Name); + Assert.Equal(ProcessName, kernelProcess.State.StepId); Assert.Single(kernelProcess.Steps); } diff --git a/dotnet/src/Experimental/Process.UnitTests/Core/ProcessMapBuilderTests.cs b/dotnet/src/Experimental/Process.UnitTests/Core/ProcessMapBuilderTests.cs index d62b54646bcd..b883ba7f96cc 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Core/ProcessMapBuilderTests.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Core/ProcessMapBuilderTests.cs @@ -24,9 +24,9 @@ public void ProcessMapBuilderFromStep() ProcessMapBuilder map = new(step); // Assert - Assert.NotNull(map.Id); - Assert.NotNull(map.Name); - Assert.Contains(nameof(SimpleTestStep), map.Name); + Assert.NotNull(map.StepId); + Assert.NotNull(map.StepId); + Assert.Contains(nameof(SimpleTestStep), map.StepId); Assert.NotNull(map.MapOperation); Assert.Equal(step, map.MapOperation); } @@ -61,9 +61,9 @@ public void ProcessMapBuilderFromProcess() ProcessMapBuilder map = new(process); // Assert - Assert.NotNull(map.Id); - Assert.NotNull(map.Name); - Assert.Contains(process.Name, map.Name); + Assert.NotNull(map.StepId); + Assert.NotNull(map.StepId); + Assert.Contains(process.StepId, map.StepId); Assert.NotNull(map.MapOperation); Assert.Equal(process, map.MapOperation); } @@ -95,7 +95,7 @@ public void ProcessMapBuilderCanDefineTarget() Assert.NotNull(processMap); Assert.Equal(processMap.Edges.Count, map.Edges.Count); Assert.Equal(processMap.Edges.Single().Value.Count, map.Edges.First().Value.Count); - Assert.Equal((processMap.Edges.Single().Value.Single().OutputTarget as KernelProcessFunctionTarget)!.StepId, (map.Edges.Single().Value[0].Target as ProcessFunctionTargetBuilder)!.Step.Id); + Assert.Equal((processMap.Edges.Single().Value.Single().OutputTarget as KernelProcessFunctionTarget)!.StepId, (map.Edges.Single().Value[0].Target as ProcessFunctionTargetBuilder)!.Step.StepId); } /// @@ -129,8 +129,8 @@ public void ProcessMapBuilderWillBuild() // Assert Assert.NotNull(processMap); Assert.IsType(processMap); - Assert.Equal(map.Name, processMap.State.Name); - Assert.Equal(map.Id, processMap.State.Id); + Assert.Equal(map.StepId, processMap.State.StepId); + Assert.Equal(map.StepId, processMap.State.RunId); } /// diff --git a/dotnet/src/Experimental/Process.UnitTests/Core/ProcessProxyBuilderTests.cs b/dotnet/src/Experimental/Process.UnitTests/Core/ProcessProxyBuilderTests.cs index e8f78b9daeaf..9a9418204975 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Core/ProcessProxyBuilderTests.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Core/ProcessProxyBuilderTests.cs @@ -26,9 +26,9 @@ public void ProcessProxyBuilderInitialization() ProcessProxyBuilder proxy = new([this._topicName1, this._topicName2, this._topicName3], this._proxyName, null); // Assert - Assert.NotNull(proxy.Id); - Assert.NotNull(proxy.Name); - Assert.Equal(this._proxyName, proxy.Name); + Assert.NotNull(proxy.StepId); + Assert.NotNull(proxy.StepId); + Assert.Equal(this._proxyName, proxy.StepId); Assert.True(proxy._externalTopicUsage.Count > 0); } @@ -78,8 +78,8 @@ public void ProcessProxyBuilderWillBuild() // Assert Assert.NotNull(proxyInfo); Assert.IsType(proxyInfo); - Assert.Equal(proxy.Name, proxyInfo.State.Name); - Assert.Equal(proxy.Id, proxyInfo.State.Id); + Assert.Equal(proxy.StepId, proxyInfo.State.StepId); + Assert.Equal(proxy.StepId, proxyInfo.State.RunId); var processProxy = (KernelProcessProxy)proxyInfo; Assert.NotNull(processProxy?.ProxyMetadata); Assert.Equal(proxy._eventMetadata, processProxy.ProxyMetadata.EventMetadata); diff --git a/dotnet/src/Experimental/Process.UnitTests/Core/ProcessStepBuilderTests.cs b/dotnet/src/Experimental/Process.UnitTests/Core/ProcessStepBuilderTests.cs index 8131d345ce2a..2518491b99f0 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Core/ProcessStepBuilderTests.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Core/ProcessStepBuilderTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using Microsoft.SemanticKernel.Process.Models; using Xunit; namespace Microsoft.SemanticKernel.Process.Core.UnitTests; @@ -24,8 +23,8 @@ public void ConstructorShouldInitializeProperties() var stepBuilder = new TestProcessStepBuilder(name); // Assert - Assert.Equal(name, stepBuilder.Name); - Assert.NotNull(stepBuilder.Id); + Assert.Equal(name, stepBuilder.StepId); + Assert.NotNull(stepBuilder.StepId); Assert.NotNull(stepBuilder.FunctionsDict); Assert.NotNull(stepBuilder.Edges); } @@ -235,9 +234,9 @@ private sealed class TestProcessStepBuilder : ProcessStepBuilder { public TestProcessStepBuilder(string name) : base(name, null) { } - internal override KernelProcessStepInfo BuildStep(ProcessBuilder processBuilder, KernelProcessStepStateMetadata? stateMetadata = null) + internal override KernelProcessStepInfo BuildStep(ProcessBuilder processBuilder) { - return new KernelProcessStepInfo(typeof(TestProcessStepBuilder), new KernelProcessStepState(this.Name, version: "v1", id: this.Id), []); + return new KernelProcessStepInfo(typeof(TestProcessStepBuilder), new KernelProcessStepState(this.StepId, version: "v1", runId: this.StepId), []); } internal override Dictionary GetFunctionMetadataMap() diff --git a/dotnet/src/Experimental/Process.UnitTests/Core/ProcessStepEdgeBuilderTests.cs b/dotnet/src/Experimental/Process.UnitTests/Core/ProcessStepEdgeBuilderTests.cs index fb854d873625..7c2db619d417 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Core/ProcessStepEdgeBuilderTests.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Core/ProcessStepEdgeBuilderTests.cs @@ -139,7 +139,7 @@ public void BuildShouldReturnKernelProcessEdge() // Assert Assert.NotNull(edge); - Assert.Equal(source.Id, edge.SourceStepId); + Assert.Equal(source.StepId, edge.SourceStepId); } /// diff --git a/dotnet/src/Experimental/Process.UnitTests/KernelProcessProxyTests.cs b/dotnet/src/Experimental/Process.UnitTests/KernelProcessProxyTests.cs index 5c5589ec60bb..5a5bed1cd64a 100644 --- a/dotnet/src/Experimental/Process.UnitTests/KernelProcessProxyTests.cs +++ b/dotnet/src/Experimental/Process.UnitTests/KernelProcessProxyTests.cs @@ -33,8 +33,8 @@ public void KernelProcessProxyStateInitialization() public void KernelProcessProxyStateRequiredProperties() { // Act & Assert - Assert.Throws(() => new KernelProcessStepState(name: null!, "vTest", "testid")); - Assert.Throws(() => new KernelProcessStepState(name: "testname", null!, "testid")); + Assert.Throws(() => new KernelProcessStepState(stepId: null!, "vTest", "testid")); + Assert.Throws(() => new KernelProcessStepState(stepId: "testname", null!, "testid")); Assert.Throws(() => new KernelProcessProxy(new KernelProcessStepState("testname", "vTest", null!), [])); } } diff --git a/dotnet/src/Experimental/Process.UnitTests/KernelProcessSerializationTests.cs b/dotnet/src/Experimental/Process.UnitTests/KernelProcessSerializationTests.cs index dbb21073ca00..4d5bb3f3cea6 100644 --- a/dotnet/src/Experimental/Process.UnitTests/KernelProcessSerializationTests.cs +++ b/dotnet/src/Experimental/Process.UnitTests/KernelProcessSerializationTests.cs @@ -46,7 +46,7 @@ public void KernelProcessSerialization() ProcessBuilder anotherBuilder = new(nameof(KernelProcessSerialization)); anotherBuilder.AddStepFromType("SimpleStep"); anotherBuilder.AddStepFromType("StatefulStep"); - KernelProcess another = anotherBuilder.Build(copyState); + KernelProcess another = anotherBuilder.Build(); AssertProcess(process, another); } @@ -85,7 +85,7 @@ public void KernelSubProcessSerialization() anotherSubBuilder.AddStepFromType("SimpleStep"); anotherSubBuilder.AddStepFromType("StatefulStep"); anotherBuilder.AddStepFromProcess(anotherSubBuilder); - KernelProcess another = anotherBuilder.Build(copyState); + KernelProcess another = anotherBuilder.Build(); AssertProcess(process, another); } @@ -117,14 +117,14 @@ public void KernelProcessMapSerialization() // Arrange ProcessBuilder anotherBuilder = new(nameof(KernelProcessSerialization)); anotherBuilder.AddMapStepFromType("StatefulStep"); - KernelProcess another = anotherBuilder.Build(copyState); + KernelProcess another = anotherBuilder.Build(); AssertProcess(process, another); } private static void AssertProcess(KernelProcess expectedProcess, KernelProcess anotherProcess) { - Assert.Equal(expectedProcess.State.Name, anotherProcess.State.Name); + Assert.Equal(expectedProcess.State.StepId, anotherProcess.State.StepId); Assert.Equal(expectedProcess.State.Version, anotherProcess.State.Version); Assert.Equal(expectedProcess.Steps.Count, anotherProcess.Steps.Count); @@ -137,7 +137,7 @@ private static void AssertProcess(KernelProcess expectedProcess, KernelProcess a private static void AssertStep(KernelProcessStepInfo expectedStep, KernelProcessStepInfo actualStep) { Assert.Equal(expectedStep.InnerStepType, actualStep.InnerStepType); - Assert.Equal(expectedStep.State.Name, actualStep.State.Name); + Assert.Equal(expectedStep.State.StepId, actualStep.State.StepId); Assert.Equal(expectedStep.State.Version, actualStep.State.Version); if (expectedStep is KernelProcessMap mapStep) @@ -156,15 +156,15 @@ private static void AssertStep(KernelProcessStepInfo expectedStep, KernelProcess KernelProcessStepState actualState = (KernelProcessStepState)actualStep.State; Assert.NotNull(stepState.State); Assert.NotNull(actualState.State); - Assert.Equal(stepState.State.Id, actualState.State.Id); + //Assert.Equal(stepState.State.Id, actualState.State.Id); } } private static void AssertProcessState(KernelProcess process, KernelProcessStateMetadata? savedProcess) { Assert.NotNull(savedProcess); - Assert.Equal(process.State.Id, savedProcess.Id); - Assert.Equal(process.State.Name, savedProcess.Name); + Assert.Equal(process.State.RunId, savedProcess.Id); + Assert.Equal(process.State.StepId, savedProcess.Name); Assert.Equal(process.State.Version, savedProcess.VersionInfo); Assert.NotNull(savedProcess.StepsState); Assert.Equal(process.Steps.Count, savedProcess.StepsState.Count); @@ -177,10 +177,10 @@ private static void AssertProcessState(KernelProcess process, KernelProcessState private static void AssertStepState(KernelProcessStepInfo step, Dictionary savedSteps) { - Assert.True(savedSteps.ContainsKey(step.State.Name)); - KernelProcessStepStateMetadata savedStep = savedSteps[step.State.Name]; - Assert.Equal(step.State.Id, savedStep.Id); - Assert.Equal(step.State.Name, savedStep.Name); + Assert.True(savedSteps.ContainsKey(step.State.StepId)); + KernelProcessStepStateMetadata savedStep = savedSteps[step.State.StepId]; + Assert.Equal(step.State.RunId, savedStep.Id); + Assert.Equal(step.State.StepId, savedStep.Name); Assert.Equal(step.State.Version, savedStep.VersionInfo); if (step is KernelProcessMap mapStep) diff --git a/dotnet/src/Experimental/Process.UnitTests/KernelProcessStateTests.cs b/dotnet/src/Experimental/Process.UnitTests/KernelProcessStateTests.cs index c20509037a99..9fde929fca0a 100644 --- a/dotnet/src/Experimental/Process.UnitTests/KernelProcessStateTests.cs +++ b/dotnet/src/Experimental/Process.UnitTests/KernelProcessStateTests.cs @@ -24,8 +24,8 @@ public void KernelProcessStateInitializationSetsPropertiesCorrectly() KernelProcessState state = new(name, "v1", id); // Assert - Assert.Equal(name, state.Name); - Assert.Equal(id, state.Id); + Assert.Equal(name, state.StepId); + Assert.Equal(id, state.RunId); } /// @@ -41,8 +41,8 @@ public void KernelProcessStateInitializationWithNullIdSucceeds() KernelProcessState state = new(name, version: "v1"); // Assert - Assert.Equal(name, state.Name); - Assert.Null(state.Id); + Assert.Equal(name, state.StepId); + Assert.Null(state.RunId); } /// diff --git a/dotnet/src/Experimental/Process.UnitTests/Runtime.Local/LocalMapTests.cs b/dotnet/src/Experimental/Process.UnitTests/Runtime.Local/LocalMapTests.cs index 634ec8a08f48..9e8042f6d926 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Runtime.Local/LocalMapTests.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Runtime.Local/LocalMapTests.cs @@ -4,9 +4,9 @@ using System.IO; using System.Linq; using System.Threading.Tasks; -using SemanticKernel.Process.TestsShared.Services; -using SemanticKernel.Process.TestsShared.Setup; -using SemanticKernel.Process.TestsShared.Steps; +using Microsoft.SemanticKernel.Process.TestsShared.Services; +using Microsoft.SemanticKernel.Process.TestsShared.Setup; +using Microsoft.SemanticKernel.Process.TestsShared.Steps; using Xunit; namespace Microsoft.SemanticKernel.Process.Runtime.Local.UnitTests; @@ -387,7 +387,7 @@ await process.StartAsync( private static async Task GetUnionStateAsync(LocalKernelProcessContext processContext) { KernelProcess processState = await processContext.GetStateAsync(); - KernelProcessStepState unionState = (KernelProcessStepState)processState.Steps.Single(s => s.State.Name == "Union").State; + KernelProcessStepState unionState = (KernelProcessStepState)processState.Steps.Single(s => s.State.StepId == "Union").State; Assert.NotNull(unionState); Assert.NotNull(unionState.State); return unionState.State; diff --git a/dotnet/src/Experimental/Process.UnitTests/Runtime.Local/LocalProcessTests.cs b/dotnet/src/Experimental/Process.UnitTests/Runtime.Local/LocalProcessTests.cs index 770eab991394..0365b6fca1a2 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Runtime.Local/LocalProcessTests.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Runtime.Local/LocalProcessTests.cs @@ -1,7 +1,14 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Linq; +using System.Text.Json; using System.Threading.Tasks; +using Microsoft.SemanticKernel.Process.Models.Storage; +using Microsoft.SemanticKernel.Process.TestsShared.Services; +using Microsoft.SemanticKernel.Process.TestsShared.Services.Storage; +using Microsoft.SemanticKernel.Process.TestsShared.Setup; +using Microsoft.SemanticKernel.Process.TestsShared.Steps; using Xunit; namespace Microsoft.SemanticKernel.Process.Runtime.Local.UnitTests; @@ -66,15 +73,16 @@ public async Task ProcessWithAssignedIdIsNotOverwrittenIdAsync() { // Arrange var mockKernel = new Kernel(); - var processState = new KernelProcessState(name: "TestProcess", version: "v1", id: "AlreadySet"); + var processId = "AlreadySet"; + var processState = new KernelProcessState(name: "TestProcess", version: "v1"); var mockKernelProcess = new KernelProcess(processState, [ - new(typeof(TestStep), new KernelProcessState(name: "Step1", version: "v1", id: "1"), []), - new(typeof(TestStep), new KernelProcessState(name: "Step2", version: "v1", id: "2"), []) + new(typeof(TestStep), new KernelProcessState(name: "Step1", version: "v1"), []), + new(typeof(TestStep), new KernelProcessState(name: "Step2", version: "v1"), []) ], []); // Act - await using var localProcess = new LocalProcess(mockKernelProcess, mockKernel); + await using var localProcess = new LocalProcess(mockKernelProcess, mockKernel, instanceId: processId); // Assert Assert.NotEmpty(localProcess.Id); @@ -188,6 +196,353 @@ public void ProcessWithSubprocessAndInvalidTargetThrows() Kernel kernel = new(); } + #region Local Storage Connector related test using MockStorage Connector + /// + /// Verify that the process runs correctly when using the context factory with process key and a storage manager. + /// + /// + [Fact] + public async Task StartProcessWithKeyedProcessDictAndStoreManagerAsync() + { + // Arrange + var processId = "myProcessId"; + var kernelProcess = CommonProcesses.GetCounterProcess(); + var counterName = "counterStep"; + + var processStorage = new MockStorage(); + + CounterService counterService = new(); + Kernel kernel = KernelSetup.SetupKernelWithCounterService(counterService); + + // Act - 1 + await using LocalKernelProcessContext runningProcess = await kernelProcess.StartAsync( + kernel, new KernelProcessEvent() + { + Id = CommonProcesses.ProcessEvents.StartProcess, + }, processId: processId, storageConnector: processStorage); + + // Assert - 1 + var processState = await runningProcess.GetStateAsync(); + Assert.NotNull(processState); + var counterState = processState.Steps.Where(s => s.State.StepId == counterName).FirstOrDefault(); + Assert.NotNull(counterState); + Assert.Equal(1, ((KernelProcessStepState)counterState.State).State?.Count); + + Assert.Equal(kernelProcess.StepId, processState.StepId); + Assert.Equal(processId, processState.RunId); + + // Act - 2 + counterService.SetCount(0); + await using LocalKernelProcessContext runningProcess2 = await kernelProcess.StartAsync( + kernel, new KernelProcessEvent() + { + Id = CommonProcesses.ProcessEvents.StartProcess, + }, processId: processId, storageConnector: processStorage); + + // Assert - 2 + var processState2 = await runningProcess2.GetStateAsync(); + Assert.NotNull(processState2); + var counterState2 = processState2.Steps.Where(s => s.State.StepId == counterName).FirstOrDefault(); + Assert.NotNull(counterState2); + Assert.Equal(2, ((KernelProcessStepState)counterState2.State).State?.Count); + + Assert.Equal(kernelProcess.StepId, processState2.State.StepId); + Assert.Equal(processId, processState2.State.RunId); + } + + /// + /// Verify that the process runs correctly when using the context factory with process key and a storage manager. + /// Running same process twice to verify step parameters get persisted. + /// + /// + [Fact] + public async Task StartProcessWithKeyedProcessAndUseOfAllOfAsync() + { + // Arrange + var processId = "myProcessId"; + var mergeStepStorageEntry = "{0}.MergeStringsStep.StepDetails"; + var kernelProcess = CommonProcesses.GetDelayedMergeProcess(); + + var processStorage = new MockStorage(); + // To use local storage, comment line above and uncomment line below + replacing with existing directory path + //var processStorage = new JsonFileStorage(""); + + CounterService counterService = new(); + Kernel kernel = KernelSetup.SetupKernelWithCounterService(counterService); + + // Act - 1 + await using LocalKernelProcessContext runningProcess = await kernelProcess.StartAsync( + kernel, new KernelProcessEvent() + { + Id = CommonProcesses.ProcessEvents.StartProcess, + Data = "Hello", + }, processId: processId, storageConnector: processStorage); + + // Assert - 1 + var processState = await runningProcess.GetStateAsync(); + Assert.NotNull(processState); + Assert.Equal(processId, processState.State.RunId); + var mergeStepId = processState.Steps.Where(s => s.State.StepId == "MergeStringsStep").FirstOrDefault()?.RunId; + Assert.NotNull(mergeStepId); + var mergeStepFullEntry = string.Format(mergeStepStorageEntry, mergeStepId); + processStorage._dbMock.TryGetValue(mergeStepFullEntry, out var entry); + Assert.NotNull(entry?.Content); + + var edgeData = JsonSerializer.Deserialize(entry.Content); + Assert.NotNull(edgeData?.StepEvents?.EdgesData); + Assert.Single(edgeData.StepEvents.EdgesData); + // Only 2/3 events in merge step should have arrived and persisted in stepEdgesData + Assert.Equal(2, edgeData.StepEvents.EdgesData.First().Value?.Count); + Assert.True(edgeData.StepEvents.EdgesData.First().Value?.ContainsKey("DelayedEchoStep22.DelayedEcho")); + Assert.True(edgeData.StepEvents.EdgesData.First().Value?.ContainsKey("DelayedEchoStep33.DelayedEcho")); + + // Act - 2 + await using LocalKernelProcessContext runningProcess2 = await kernelProcess.StartAsync( + kernel, new KernelProcessEvent() + { + Id = CommonProcesses.ProcessEvents.OtherEvent, + Data = "World", + }, processId: processId, storageConnector: processStorage); + + // Assert - 2 + var processState2 = await runningProcess2.GetStateAsync(); + Assert.NotNull(processState2); + Assert.Equal(processId, processState2.State.RunId); + processStorage._dbMock.TryGetValue(mergeStepFullEntry, out var entry2); + Assert.NotNull(entry2?.Content); + Assert.IsType(entry2?.Content); + + var edgeData2 = JsonSerializer.Deserialize(entry2.Content); + Assert.NotNull(edgeData2?.StepEvents?.EdgesData); + Assert.Single(edgeData2.StepEvents.EdgesData); + // All parameters in merge step should have been processed and edge data should be empty + Assert.Empty(edgeData2.StepEvents.EdgesData.First().Value!); + } + + /// + /// Verify that the process runs correctly when using the context factory with process key and a storage manager. + /// Running same process twice to verify step parameters get persisted. + /// Making use of process input events directly to validate AllOf plumbing with process events + /// + /// + [Fact] + public async Task StartProcessWithKeyedProcessUseOfAllOfAndAllEventsAreProcessInputsAsync() + { + // Arrange + var processId = "myProcessId"; + var mergeStepStorageEntry = "{0}.MergeStringsStep.StepDetails"; + var kernelProcess = CommonProcesses.GetSimpleMergeProcess(); + + var processStorage = new MockStorage(); + // To use local storage, comment line above and uncomment line below + replacing with existing directory path + //var processStorage = new JsonFileStorage(""); + + Kernel kernel = new(); + + // Act - 1 + await using LocalKernelProcessContext runningProcess = await kernelProcess.StartAsync( + kernel, new KernelProcessEvent() + { + Id = CommonProcesses.ProcessEvents.StartProcess, + Data = "Hello", + }, processId: processId, storageConnector: processStorage); + + // Assert - 1 + var processState = await runningProcess.GetStateAsync(); + Assert.NotNull(processState); + Assert.Equal(processId, processState.State.RunId); + var mergeStepId = processState.Steps.Where(s => s.State.StepId == "MergeStringsStep").FirstOrDefault()?.RunId; + Assert.NotNull(mergeStepId); + var mergeStepFullEntry = string.Format(mergeStepStorageEntry, mergeStepId); + processStorage._dbMock.TryGetValue(mergeStepFullEntry, out var entry); + Assert.NotNull(entry?.Content); + Assert.IsType(entry?.Content); + + var edgeData = JsonSerializer.Deserialize(entry.Content); + Assert.NotNull(edgeData?.StepEvents?.EdgesData); + Assert.Single(edgeData.StepEvents.EdgesData); + // Only 1/2 events in merge step should have arrived and persisted in stepEdgesData + Assert.Single(edgeData.StepEvents.EdgesData.First().Value); + Assert.True(edgeData.StepEvents.EdgesData.First().Value?.ContainsKey("SimpleMergeProcess.StartProcess")); + + // Act - 2 + await using LocalKernelProcessContext runningProcess2 = await kernelProcess.StartAsync( + kernel, new KernelProcessEvent() + { + Id = CommonProcesses.ProcessEvents.OtherEvent, + Data = "World", + }, processId: processId, storageConnector: processStorage); + + // Assert - 2 + var processState2 = await runningProcess2.GetStateAsync(); + Assert.NotNull(processState2); + processStorage._dbMock.TryGetValue(mergeStepFullEntry, out var entry2); + Assert.NotNull(entry2?.Content); + Assert.IsType(entry2?.Content); + + var edgeData2 = JsonSerializer.Deserialize(entry2.Content); + Assert.NotNull(edgeData2?.StepEvents?.EdgesData); + Assert.Single(edgeData2.StepEvents.EdgesData); + // All parameters in merge step should have been processed and edge data should be empty + Assert.Empty(edgeData2.StepEvents.EdgesData.First().Value!); + } + + [Fact] + public async Task StartProcessWithKeyedProcessUseOfNestedStatefulStepsAndAllOfAsync() + { + // Arrange + var processId = "myProcessId"; + var mergeStepStorageEntry = "{0}.MergeStringsStep.StepDetails"; + var outerCounterStorageEntry = "{0}.outerCounterStep.StepDetails"; + var innerCounterStorageEntry = "{0}.counterStep.StepDetails"; + var kernelProcess = CommonProcesses.GetNestedCounterWithEvenDetectionAndMergeProcess().Build(); + + var processStorage = new MockStorage(); + // To use local storage, comment line above and uncomment line below + replacing with existing directory path + //var processStorage = new JsonFileStorage(""); + + Kernel kernel = new(); + var iterationCount = 4; + string? outerCounterStepFullEntry = null; + string? innerCounterStepFullEntry = null; + string? mergeStepFullEntry = null; + + for (int i = 1; i < iterationCount; i++) + { + // Act - 1,2,3 + await using LocalKernelProcessContext runningProcess = await kernelProcess.StartAsync( + kernel, new KernelProcessEvent() + { + Id = CommonProcesses.ProcessEvents.StartProcess, + }, processId: processId, storageConnector: processStorage); + + // Assert - 1,2,3 + var processState = await runningProcess.GetStateAsync(); + Assert.NotNull(processState); + Assert.Equal(processId, processState.State.RunId); + + outerCounterStepFullEntry ??= string.Format(outerCounterStorageEntry, processState.Steps.Where(s => s.State.StepId == "outerCounterStep").FirstOrDefault()?.State.RunId); + this.AssertCounterState(processStorage, outerCounterStepFullEntry, i); + + var innerCounterStepId = (processState.Steps.Where(s => s.State.StepId == "innerCounterProcess").FirstOrDefault() as KernelProcess)?.Steps.Where(s => s.State.StepId == "counterStep").FirstOrDefault()?.State.RunId; + if (i == 1) + { + Assert.Null(innerCounterStepId); + } + else + { + innerCounterStepFullEntry ??= string.Format(innerCounterStorageEntry, innerCounterStepId); + this.AssertCounterState(processStorage, innerCounterStepFullEntry, i / 2); + } + + // Merge Step entry should have parameter entries pending until iteration 4 + mergeStepFullEntry ??= string.Format(mergeStepStorageEntry, processState.Steps.Where(s => s.State.StepId == "MergeStringsStep").FirstOrDefault()?.State.RunId); + processStorage._dbMock.TryGetValue(mergeStepFullEntry, out var mergeStorageEntry); + Assert.NotNull(mergeStorageEntry?.Content); + + var mergeEdgeData = JsonSerializer.Deserialize(mergeStorageEntry.Content); + Assert.NotNull(mergeEdgeData?.StepEvents?.EdgesData); + Assert.Single(mergeEdgeData.StepEvents.EdgesData); + Assert.NotEmpty(mergeEdgeData.StepEvents.EdgesData.Values); + } + + // Act - 4 + await using LocalKernelProcessContext runningProcess2 = await kernelProcess.StartAsync( + kernel, new KernelProcessEvent() + { + Id = CommonProcesses.ProcessEvents.StartProcess, + }, processId: processId, storageConnector: processStorage); + + // Assert - 4 + var processState2 = await runningProcess2.GetStateAsync(); + Assert.NotNull(processState2); + Assert.Equal(processId, processState2.State.RunId); + + Assert.NotNull(outerCounterStepFullEntry); + this.AssertCounterState(processStorage, outerCounterStepFullEntry, 4); + + Assert.NotNull(innerCounterStepFullEntry); + this.AssertCounterState(processStorage, innerCounterStepFullEntry, 2); + + Assert.NotNull(mergeStepFullEntry); + processStorage._dbMock.TryGetValue(mergeStepFullEntry, out var mergeStorageEntry2); + Assert.NotNull(mergeStorageEntry2?.Content); + + var mergeEdgeData2 = JsonSerializer.Deserialize(mergeStorageEntry2.Content); + Assert.NotNull(mergeEdgeData2?.StepEvents?.EdgesData); + Assert.Single(mergeEdgeData2.StepEvents.EdgesData); + Assert.Empty(mergeEdgeData2.StepEvents.EdgesData.Values.First()); + } + + private void AssertCounterState(MockStorage processStorage, string stepStorageEntry, int expectedCount) + { + processStorage._dbMock.TryGetValue(stepStorageEntry, out var outerCounterEntry); + Assert.NotNull(outerCounterEntry?.Content); + var outerCounterData = JsonSerializer.Deserialize(outerCounterEntry?.Content!); + Assert.NotNull(outerCounterData?.StepState?.State); + var counterStateData = outerCounterData.StepState.State.ToObject(); + Assert.NotNull(counterStateData); + Assert.IsType(counterStateData); + Assert.Equal(expectedCount, ((CommonSteps.CounterState)counterStateData).Count); + } + + [Fact] + public async Task StartProcessWithKeyedProcessUseOfInternalNestedStatefulStepsAndAllOfInternallyAndExternallyAsync() + { + // Arrange + var processId = "myProcessId"; + var mergeStepStorageEntry = "{0}.MergeStringsStep.StepDetails"; + var kernelProcess = CommonProcesses.GetInternalNestedCounterWithEvenDetectionAndMergeProcess(); + + var processStorage = new MockStorage(); + // To use local storage, comment line above and uncomment line below + replacing with existing directory path + //var processStorage = new JsonFileStorage(""); + + Kernel kernel = new(); + var iterationCount = 4; + + string? mergeStepFullEntry = null; + + for (int i = 1; i <= iterationCount; i++) + { + // Act - 1,2,3,4 + await using LocalKernelProcessContext runningProcess = await kernelProcess.StartAsync( + kernel, new KernelProcessEvent() + { + Id = CommonProcesses.ProcessEvents.StartProcess, + Data = i.ToString(), + }, processId: processId, storageConnector: processStorage); + + // Assert - 1,2,3,4 + var processState = await runningProcess.GetStateAsync(); + Assert.NotNull(processState); + Assert.Equal(processId, processState.State.RunId); + + mergeStepFullEntry ??= string.Format(mergeStepStorageEntry, processState.Steps.Where(s => s.State.StepId == "MergeStringsStep").FirstOrDefault()?.State.RunId); + processStorage._dbMock.TryGetValue(mergeStepFullEntry, out var mergeStorageEntry); + Assert.NotNull(mergeStorageEntry?.Content); + + var mergeEdgeData = JsonSerializer.Deserialize(mergeStorageEntry.Content); + Assert.NotNull(mergeEdgeData?.StepEvents?.EdgesData); + Assert.Single(mergeEdgeData.StepEvents.EdgesData); + Assert.Single(mergeEdgeData.StepEvents.EdgesData.Values); + if (i < 4) + { + // outer merge is waiting on missing parameters pending from internal nested subprocess + // in the meantime on each iteration, the only piped event/parameter keeps changing + Assert.Single(mergeEdgeData.StepEvents.EdgesData.Values.First().Values); + var firstEventValue = mergeEdgeData.StepEvents.EdgesData.Values.First().Values.First()?.ToObject(); + Assert.Equal(i.ToString(), firstEventValue?.ToString()); + } + else + { + // finally the missing event, that is piped for 2 parameters, arrived and now the merge edge data is empty since it was processed + Assert.Empty(mergeEdgeData.StepEvents.EdgesData.Values.First().Values); + } + } + } + #endregion + /// /// A class that represents a step for testing. /// diff --git a/dotnet/src/Experimental/Process.UnitTests/Runtime.Local/LocalProxyTests.cs b/dotnet/src/Experimental/Process.UnitTests/Runtime.Local/LocalProxyTests.cs index 51ec8fbd739e..67779c5e0d1e 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Runtime.Local/LocalProxyTests.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Runtime.Local/LocalProxyTests.cs @@ -1,10 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. using System; using System.Threading.Tasks; -using SemanticKernel.Process.TestsShared.CloudEvents; -using SemanticKernel.Process.TestsShared.Services; -using SemanticKernel.Process.TestsShared.Setup; -using SemanticKernel.Process.TestsShared.Steps; +using Microsoft.SemanticKernel.Process.TestsShared.CloudEvents; +using Microsoft.SemanticKernel.Process.TestsShared.Services; +using Microsoft.SemanticKernel.Process.TestsShared.Setup; +using Microsoft.SemanticKernel.Process.TestsShared.Steps; using Xunit; namespace Microsoft.SemanticKernel.Process.Runtime.Local.UnitTests; @@ -269,6 +269,6 @@ await process.StartAsync( Id = inputEvent, Data = input, }, - externalMessageChannel); + externalMessageChannel: externalMessageChannel); } } diff --git a/dotnet/src/Experimental/Process.Utilities.UnitTests/CloneTests.cs b/dotnet/src/Experimental/Process.Utilities.UnitTests/CloneTests.cs index fb0c4764b081..9ed3f4d1f31c 100644 --- a/dotnet/src/Experimental/Process.Utilities.UnitTests/CloneTests.cs +++ b/dotnet/src/Experimental/Process.Utilities.UnitTests/CloneTests.cs @@ -137,10 +137,44 @@ public void VerifyCloneMapStepTest() VerifyProcess(source, copy); } + /// + /// Verify that cloning a with a new run ID and edges works correctly. + /// + [Fact] + public void VerifyCloneWithIdAndEdgesTest() + { + // Arrange + string runningId = Guid.NewGuid().ToString(); + string stepName = nameof(VerifyCloneWithIdAndEdgesTest); + Dictionary> originalStepEdges = new() + { + { $"{stepName}.SomeOutputEvent1", CreateTestProcessEdges(1) }, + { $"{stepName}.{stepName}", CreateTestProcessEdges(3) }, + }; + KernelProcessStepInfo step = new(typeof(KernelProcessStep), new(stepName, "v1"), originalStepEdges); + + // Act + KernelProcessStepInfo copy = step.CloneWithIdAndEdges(runningId, NullLogger.Instance); + + // Assert + Assert.NotNull(copy); + Assert.Equal(stepName, copy.State.StepId); + Assert.Equal(runningId, copy.State.RunId); + foreach (var (expectedEdges, actualEdges) in step.Edges.Zip(copy.Edges)) + { + Assert.Contains(runningId, actualEdges.Key); + + var runningIdCountInEvent = actualEdges.Key.Split(runningId).Length - 1; + Assert.Equal(1, runningIdCountInEvent); + + Assert.Equivalent(expectedEdges.Value, actualEdges.Value); + } + } + private static void VerifyProcess(KernelProcess expected, KernelProcess actual) { - Assert.Equal(expected.State.Id, actual.State.Id); - Assert.Equal(expected.State.Name, actual.State.Name); + Assert.Equal(expected.State.RunId, actual.State.RunId); + Assert.Equal(expected.State.StepId, actual.State.StepId); Assert.Equal(expected.State.Version, actual.State.Version); Assert.Equal(expected.InnerStepType, actual.InnerStepType); Assert.Equivalent(expected.Edges, actual.Edges); @@ -163,12 +197,20 @@ private static Dictionary> CreateTestEdges() => { { "sourceId", - [ - new KernelProcessEdge("sourceId", new KernelProcessFunctionTarget("sourceId", "targetFunction", "targetParameter", "targetEventId")), - ] + CreateTestProcessEdges(1) } }; + private static List CreateTestProcessEdges(int count = 1) + { + var edges = new List(); + for (int i = 0; i < count; i++) + { + edges.Add(new KernelProcessEdge($"sourceId{i}", new KernelProcessFunctionTarget($"sourceId{i}", "targetFunction", "targetParameter", $"targetEventId{i}"))); + } + return edges; + } + private sealed record TestState { public Guid Value { get; set; } diff --git a/dotnet/src/InternalUtilities/process/Abstractions/KernelProcessStateMetadataFactory.cs b/dotnet/src/InternalUtilities/process/Abstractions/KernelProcessStateMetadataFactory.cs index 2c9be1a1c03e..2540732f76bd 100644 --- a/dotnet/src/InternalUtilities/process/Abstractions/KernelProcessStateMetadataFactory.cs +++ b/dotnet/src/InternalUtilities/process/Abstractions/KernelProcessStateMetadataFactory.cs @@ -15,15 +15,15 @@ public static KernelProcessStateMetadata KernelProcessToProcessStateMetadata(Ker { KernelProcessStateMetadata metadata = new() { - Name = kernelProcess.State.Name, - Id = kernelProcess.State.Id, + Name = kernelProcess.State.StepId, + Id = kernelProcess.State.RunId, VersionInfo = kernelProcess.State.Version, StepsState = [], }; foreach (KernelProcessStepInfo step in kernelProcess.Steps) { - metadata.StepsState.Add(step.State.Name, step.ToProcessStateMetadata()); + metadata.StepsState.Add(step.State.StepId, step.ToProcessStateMetadata()); } return metadata; @@ -52,8 +52,8 @@ private static KernelProcessMapStateMetadata KernelProcessMapToProcessStateMetad return new() { - Name = stepMap.State.Name, - Id = stepMap.State.Id, + Name = stepMap.State.StepId, + Id = stepMap.State.RunId, VersionInfo = stepMap.State.Version, OperationState = ToProcessStateMetadata(stepMap.Operation), }; @@ -63,8 +63,8 @@ private static KernelProcessProxyStateMetadata KernelProcessProxyToProcessStateM { return new() { - Name = stepProxy.State.Name, - Id = stepProxy.State.Id, + Name = stepProxy.State.StepId, + Id = stepProxy.State.RunId, VersionInfo = stepProxy.State.Version, PublishTopics = stepProxy.ProxyMetadata?.PublishTopics ?? [], EventMetadata = stepProxy.ProxyMetadata?.EventMetadata ?? [], @@ -79,8 +79,8 @@ private static KernelProcessStepStateMetadata StepInfoToProcessStateMetadata(Ker { KernelProcessStepStateMetadata metadata = new() { - Name = stepInfo.State.Name, - Id = stepInfo.State.Id, + Name = stepInfo.State.StepId, + Id = stepInfo.State.RunId, VersionInfo = stepInfo.State.Version }; diff --git a/dotnet/src/InternalUtilities/process/Abstractions/KernelProcessStepEdgesExtension.cs b/dotnet/src/InternalUtilities/process/Abstractions/KernelProcessStepEdgesExtension.cs new file mode 100644 index 000000000000..9c5e7c48401d --- /dev/null +++ b/dotnet/src/InternalUtilities/process/Abstractions/KernelProcessStepEdgesExtension.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using Microsoft.SemanticKernel.Agents; + +namespace Microsoft.SemanticKernel.Process.Internal; + +internal static class KernelProcessStepEdgesExtensions +{ + public static Dictionary> PackStepEdgesValues(this Dictionary?> functionsParameters) + { + Dictionary> stepFunctionParamsEventData = []; + foreach (var function in functionsParameters) + { + var functionName = function.Key; + stepFunctionParamsEventData[functionName] = []; + foreach (var parameterData in function.Value ?? []) + { + var parameterName = parameterData.Key; + if (parameterData.Value is null) + { + stepFunctionParamsEventData[functionName][parameterName] = null; + continue; + } + + // Avoid storing parameters that are automatically injected by the step + // Must match cases in StepExtensions.FindInputChannels + if (parameterData.Value is Kernel or KernelProcessStepContext or KernelProcessStepExternalContext or AgentDefinition) + { + continue; + } + + stepFunctionParamsEventData[functionName][parameterName] = KernelProcessEventData.FromObject(parameterData.Value); + } + } + + return stepFunctionParamsEventData; + } +} diff --git a/dotnet/src/InternalUtilities/process/Abstractions/MapExtensions.cs b/dotnet/src/InternalUtilities/process/Abstractions/MapExtensions.cs index b42be3c87a9e..30bdb073e8cb 100644 --- a/dotnet/src/InternalUtilities/process/Abstractions/MapExtensions.cs +++ b/dotnet/src/InternalUtilities/process/Abstractions/MapExtensions.cs @@ -8,7 +8,7 @@ internal static class MapExtensions { public static KernelProcessMap CloneMap(this KernelProcessMap map, ILogger logger) { - KernelProcessMapState newState = new(map.State.Name, map.State.Version, map.State.Id!); + KernelProcessMapState newState = new(map.State.StepId, map.State.Version, map.State.RunId!); KernelProcessMap copy = new( diff --git a/dotnet/src/InternalUtilities/process/Abstractions/ProcessExtensions.cs b/dotnet/src/InternalUtilities/process/Abstractions/ProcessExtensions.cs index 84143151da95..cdf958c6c431 100644 --- a/dotnet/src/InternalUtilities/process/Abstractions/ProcessExtensions.cs +++ b/dotnet/src/InternalUtilities/process/Abstractions/ProcessExtensions.cs @@ -11,7 +11,7 @@ public static KernelProcess CloneProcess(this KernelProcess process, ILogger log { KernelProcess copy = new( - new KernelProcessState(process.State.Name, process.State.Version, process.State.Id), + new KernelProcessState(process.State.StepId, process.State.Version, process.State.RunId), process.Steps.Select(s => s.Clone(logger)).ToArray(), process.Edges.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToList())); diff --git a/dotnet/src/InternalUtilities/process/Abstractions/StepExtensions.cs b/dotnet/src/InternalUtilities/process/Abstractions/StepExtensions.cs index ecada9c6abc9..6820ec920f6c 100644 --- a/dotnet/src/InternalUtilities/process/Abstractions/StepExtensions.cs +++ b/dotnet/src/InternalUtilities/process/Abstractions/StepExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Agents; @@ -38,10 +39,10 @@ public static KernelProcessStepInfo Clone(this KernelProcessStepInfo step, ILogg // Exposed for testing public static KernelProcessStepState Clone(this KernelProcessStepState sourceState, Type stateType, Type? userStateType, ILogger logger) { - KernelProcessStepState? newState = (KernelProcessStepState?)Activator.CreateInstance(stateType, sourceState.Name, sourceState.Version, sourceState.Id); + KernelProcessStepState? newState = (KernelProcessStepState?)Activator.CreateInstance(stateType, sourceState.StepId, sourceState.Version, sourceState.RunId); if (newState == null) { - throw new KernelException($"Failed to instantiate state: {stateType.Name} [{sourceState.Id}].").Log(logger); + throw new KernelException($"Failed to instantiate state: {stateType.Name} [{sourceState.RunId}].").Log(logger); } if (userStateType != null) @@ -153,4 +154,42 @@ public static void InitializeUserState(this KernelProcessStepState stateObject, return inputs; } + + private static IReadOnlyDictionary> ReplaceEdgeSourceNames(IReadOnlyDictionary> edges, string originalSourceName, string newSourceName) + { + if (edges.Count == 0) + { + return edges; + } + + var updatedEdges = new Dictionary>(); + + foreach (var kvp in edges) + { + // Ensuring only replacing the first occurrence of the original source name in case it is also used in event name or other parts of the event name. + var regex = new Regex($"^{originalSourceName}"); + var newKey = regex.Replace(kvp.Key, newSourceName, 1); + + updatedEdges[newKey] = kvp.Value; + } + + return updatedEdges; + } + + /// + /// Creates a new instance of the class with a new step ID and updated edges. + /// + /// instance of + /// id to be assigned to the updated step info + /// instance of + /// + public static KernelProcessStepInfo CloneWithIdAndEdges(this KernelProcessStepInfo step, string stepId, ILogger logger) + { + if (string.IsNullOrWhiteSpace(stepId)) + { + throw new KernelException("Internal Error: The step needs a non-empty id").Log(logger); + } + + return step with { State = step.State with { RunId = stepId }, Edges = ReplaceEdgeSourceNames(step.Edges, step.State.StepId, stepId) }; + } } diff --git a/dotnet/src/InternalUtilities/process/LocalStorageComponents.props b/dotnet/src/InternalUtilities/process/LocalStorageComponents.props new file mode 100644 index 000000000000..69f1bf256ada --- /dev/null +++ b/dotnet/src/InternalUtilities/process/LocalStorageComponents.props @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/dotnet/src/InternalUtilities/process/Runtime/MapExtensions.cs b/dotnet/src/InternalUtilities/process/Runtime/MapExtensions.cs index 45118c93840f..c1d8edab41f9 100644 --- a/dotnet/src/InternalUtilities/process/Runtime/MapExtensions.cs +++ b/dotnet/src/InternalUtilities/process/Runtime/MapExtensions.cs @@ -37,9 +37,9 @@ public static (IEnumerable, KernelProcess, string) Initialize(this KernelProcess string proxyId = Guid.NewGuid().ToString("N"); mapOperation = new KernelProcess( - new KernelProcessState($"Map{map.Operation.State.Name}", map.Operation.State.Version, proxyId), + new KernelProcessState($"Map{map.Operation.State.StepId}", map.Operation.State.Version, proxyId), [map.Operation], - new() { { ProcessConstants.MapEventId, [new KernelProcessEdge(proxyId, new KernelProcessFunctionTarget(map.Operation.State.Id!, message.FunctionName, parameterName))] } }); + new() { { ProcessConstants.MapEventId, [new KernelProcessEdge(proxyId, new KernelProcessFunctionTarget(map.Operation.State.RunId!, message.FunctionName, parameterName))] } }); } return (inputValues, mapOperation, startEventId); @@ -110,7 +110,7 @@ private static string DefineOperationEventId(KernelProcess mapOperation, Process // Fails when zero or multiple candidate edges exist. No reason a map-operation should be irrational. return mapOperation.Edges.SingleOrDefault(kvp => kvp.Value.Any(e => (e.OutputTarget as KernelProcessFunctionTarget)!.FunctionName == message.FunctionName)).Key ?? - throw new InvalidOperationException($"The map operation does not have an input edge that matches the message destination: {mapOperation.State.Name}/{mapOperation.State.Id}."); + throw new InvalidOperationException($"The map operation does not have an input edge that matches the message destination: {mapOperation.State.StepId}/{mapOperation.State.RunId}."); } private static bool IsEqual(IEnumerable targetData, object? possibleValue) diff --git a/dotnet/src/InternalUtilities/process/TestsShared/CloudEvents/MockCloudEventClient.cs b/dotnet/src/InternalUtilities/process/TestsShared/CloudEvents/MockCloudEventClient.cs index adf83577cfcf..4f719b4e07bc 100644 --- a/dotnet/src/InternalUtilities/process/TestsShared/CloudEvents/MockCloudEventClient.cs +++ b/dotnet/src/InternalUtilities/process/TestsShared/CloudEvents/MockCloudEventClient.cs @@ -1,10 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. +#pragma warning disable IDE0005 // Using directive is unnecessary using System.Collections.Generic; using System.Threading.Tasks; -using Microsoft.SemanticKernel; +#pragma warning restore IDE0005 // Using directive is unnecessary -namespace SemanticKernel.Process.TestsShared.CloudEvents; +namespace Microsoft.SemanticKernel.Process.TestsShared.CloudEvents; /// /// Class used for testing purposes to mock emitting external cloud events /// diff --git a/dotnet/src/InternalUtilities/process/TestsShared/CloudEvents/MockCloudEventData.cs b/dotnet/src/InternalUtilities/process/TestsShared/CloudEvents/MockCloudEventData.cs index 8ac34795c061..0e20dc17fa46 100644 --- a/dotnet/src/InternalUtilities/process/TestsShared/CloudEvents/MockCloudEventData.cs +++ b/dotnet/src/InternalUtilities/process/TestsShared/CloudEvents/MockCloudEventData.cs @@ -2,9 +2,8 @@ using System.Runtime.Serialization; using System.Text.Json.Serialization; -using Microsoft.SemanticKernel; -namespace SemanticKernel.Process.TestsShared.CloudEvents; +namespace Microsoft.SemanticKernel.Process.TestsShared.CloudEvents; /// /// Mock cloud event data used for testing purposes only diff --git a/dotnet/src/InternalUtilities/process/TestsShared/Processes/CommonProcesses.cs b/dotnet/src/InternalUtilities/process/TestsShared/Processes/CommonProcesses.cs new file mode 100644 index 000000000000..f9aaae2e2ced --- /dev/null +++ b/dotnet/src/InternalUtilities/process/TestsShared/Processes/CommonProcesses.cs @@ -0,0 +1,338 @@ +// Copyright (c) Microsoft. All rights reserved. +#pragma warning disable IDE0005 // Using directive is unnecessary +using System.Collections.Generic; +using Microsoft.SemanticKernel; +#pragma warning restore IDE0005 // Using directive is unnecessary + +namespace Microsoft.SemanticKernel.Process.TestsShared.Steps; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +/// +/// Collection of common steps used by UnitTests and IntegrationUnitTests +/// +public static class CommonProcesses +{ + public static class ProcessEvents + { + /// + /// Start Process Event + /// + public const string StartProcess = nameof(StartProcess); + public const string OtherEvent = nameof(OtherEvent); + } + + public static class ProcessKeys + { + public const string CounterProcess = nameof(CounterProcess); + public const string CounterWithEvenNumberDetectionProcess = nameof(CounterWithEvenNumberDetectionProcess); + public const string NestedCounterWithEvenDetectionAndMergeProcess = nameof(NestedCounterWithEvenDetectionAndMergeProcess); + public const string InternalNestedCounterWithEvenDetectionAndMergeProcess = nameof(InternalNestedCounterWithEvenDetectionAndMergeProcess); + public const string DelayedMergeProcess = nameof(DelayedMergeProcess); + public const string SimpleMergeProcess = nameof(SimpleMergeProcess); + } + + public static KernelProcess GetCounterProcess(string processName = ProcessKeys.CounterProcess) + { + ProcessBuilder process = new(processName); + + var counterStep = process.AddStepFromType(id: "counterStep"); + var echoStep = process.AddStepFromType(id: nameof(CommonSteps.EchoStep)); + + process + .OnInputEvent(ProcessEvents.StartProcess) + .SendEventTo(new(counterStep)); + + counterStep + .OnFunctionResult() + .SendEventTo(new ProcessFunctionTargetBuilder(echoStep)); + + return process.Build(); + } + + public static ProcessBuilder GetCounterWithEvenDetectionProcess(string processName = ProcessKeys.CounterWithEvenNumberDetectionProcess) + { + ProcessBuilder process = new(processName); + + var counterStep = process.AddStepFromType(id: "counterStep"); + var evenNumberStep = process.AddStepFromType(id: nameof(CommonSteps.EvenNumberDetectorStep)); + + process + .OnInputEvent(ProcessEvents.StartProcess) + .SendEventTo(new ProcessFunctionTargetBuilder(counterStep)); + + counterStep + .OnFunctionResult() + .SendEventTo(new ProcessFunctionTargetBuilder(evenNumberStep)); + + return process; + } + + /// + /// + /// ┌───────┐ + /// │ │ + /// ┌───────────────────────────────────────────────────────────────────────►│ │ + /// │ │ │ + /// │ EvenNumber Event │ │ ┌───────────┐ + /// │ ┌─────────────────────────────────────────────────►│ merge ├──►│ finalEcho │ + /// │ │ │ │ └───────────┘ + /// │ │ ┌─────────────────────────────────────┐ │ │ + /// │ │ │ innerCounterProcess │ ┌──►│ │ + /// Start ┌───────────┐ │ ┌──────────────┐ │ │ ┌───────────┐ ┌──────────────┐ │ │ │ │ + /// Process ────►│ outer ├─┴─►│ outer ├───┴──►│ │ counter ├───►│ evenDetector ├──┼────┘ └───────┘ + /// Event │ counter │ │ evenDetector │ │ └───────────┘ └──────────────┘ │ EvenNumber + /// └───────────┘ └──────────────┘ └─────────────────────────────────────┘ Event + /// + /// + /// + /// + public static ProcessBuilder GetNestedCounterWithEvenDetectionAndMergeProcess(string processName = ProcessKeys.NestedCounterWithEvenDetectionAndMergeProcess) + { + ProcessBuilder process = new(processName); + + var outerCounterStep = process.AddStepFromType(id: "outerCounterStep"); + var outerEvenNumberStep = process.AddStepFromType(id: $"outer{nameof(CommonSteps.EvenNumberDetectorStep)}"); + var innerCounterProcess = process.AddStepFromProcess(GetCounterWithEvenDetectionProcess("innerCounterProcess")); + var mergeStep = process.AddStepFromType(id: nameof(CommonSteps.MergeStringsStep)); + var finalEchoStep = process.AddStepFromType(id: nameof(CommonSteps.EchoStep)); + + process + .OnInputEvent(ProcessEvents.StartProcess) + .SendEventTo(new ProcessFunctionTargetBuilder(outerCounterStep)); + + outerCounterStep + .OnFunctionResult() + .SendEventTo(new ProcessFunctionTargetBuilder(outerEvenNumberStep)); + + outerEvenNumberStep + .OnEvent(CommonSteps.EvenNumberDetectorStep.OutputEvents.EvenNumber) + .SendEventTo(innerCounterProcess.WhereInputEventIs(ProcessEvents.StartProcess)); + + // merging inputs + process + .ListenFor() + .AllOf([ + new(messageType: outerCounterStep.GetFunctionResultEventId(), outerCounterStep), + new(messageType: CommonSteps.EvenNumberDetectorStep.OutputEvents.EvenNumber, outerEvenNumberStep), + new(messageType: CommonSteps.EvenNumberDetectorStep.OutputEvents.EvenNumber, innerCounterProcess), + ]) + .SendEventTo(new ProcessStepTargetBuilder(mergeStep, inputMapping: (inputEvents) => + { + return new() + { + { "str1", inputEvents[outerCounterStep.GetFullEventId()] }, + { "str2", inputEvents[outerEvenNumberStep.GetFullEventId(CommonSteps.EvenNumberDetectorStep.OutputEvents.EvenNumber)] }, + { "str3", inputEvents[innerCounterProcess.GetFullEventId(CommonSteps.EvenNumberDetectorStep.OutputEvents.EvenNumber)] }, + }; + })); + + mergeStep + .OnFunctionResult() + .SendEventTo(new ProcessFunctionTargetBuilder(finalEchoStep)); + + return process; + } + + /// + /// Process that merges string from process onInput and nested AllOf triggered events from a nested process. + /// + /// ┌───────┐ + /// │ │ + /// ┌───────────────────────────────►│ │ + /// │ │ │ + /// │ │ │ ┌───────────┐ + /// │ ┌──►│ merge ├──►│ finalEcho │ + /// │ │ │ │ └───────────┘ + /// │ │ │ │ + /// │ ┌───────────────┐ ├──►│ │ + /// Start │ │ Nested │ │ │ │ + /// Process ───┴─►│ Counter ├─────────┘ └───────┘ + /// Event │ Process │ Echo + /// └───────────────┘ Event + /// + /// + /// + /// + public static KernelProcess GetInternalNestedCounterWithEvenDetectionAndMergeProcess(string processName = ProcessKeys.InternalNestedCounterWithEvenDetectionAndMergeProcess) + { + ProcessBuilder process = new(processName); + + var internalNestedCounterProcess = process.AddStepFromProcess(GetNestedCounterWithEvenDetectionAndMergeProcess("internalNestedCounterProcess")); + var mergeStep = process.AddStepFromType(id: nameof(CommonSteps.MergeStringsStep)); + var finalEchoStep = process.AddStepFromType(id: nameof(CommonSteps.EchoStep)); + + process + .OnInputEvent(ProcessEvents.StartProcess) + .SendEventTo(internalNestedCounterProcess.WhereInputEventIs(ProcessEvents.StartProcess)); + + // merging inputs + process.ListenFor() + .AllOf([ + new(messageType: ProcessEvents.StartProcess, process), + new(messageType: CommonSteps.EchoStep.OutputEvents.EchoMessage, internalNestedCounterProcess), + ]) + .SendEventTo(new ProcessStepTargetBuilder(mergeStep, inputMapping: (inputEvents) => + { + return new() + { + { "str1", inputEvents[process.GetFullEventId(ProcessEvents.StartProcess)] }, + { "str2", inputEvents[internalNestedCounterProcess.GetFullEventId(CommonSteps.EchoStep.OutputEvents.EchoMessage)] }, + { "str3", inputEvents[internalNestedCounterProcess.GetFullEventId(CommonSteps.EchoStep.OutputEvents.EchoMessage)] }, + }; + })); + + mergeStep + .OnFunctionResult() + .SendEventTo(new ProcessFunctionTargetBuilder(finalEchoStep)); + + return process.Build(); + } + + /// + /// Process that merges string from process onInput events when they are all available. + /// Helps test make use of ListenFor() and AllOf() methods with events from process. + /// + /// Other ───┐ ┌───────┐ + /// │ │ │ + /// └──►│ │ + /// │ │ + /// │ │ ┌───────────┐ + /// ┌────────►│ merge ├───►│ finalEcho │ + /// │ │ │ └───────────┘ + /// Start │ │ + /// Process ┌──►│ │ + /// │ │ │ │ + /// └─────┘ └───────┘ + /// + /// + /// + /// + public static KernelProcess GetSimpleMergeProcess(string processName = ProcessKeys.SimpleMergeProcess) + { + ProcessBuilder process = new(processName); + + var mergeStep = process.AddStepFromType(id: nameof(CommonSteps.MergeStringsStep)); + + var finalEchoStep = process.AddStepFromType(id: nameof(CommonSteps.EchoStep)); + + // merging inputs + process + .ListenFor() + .AllOf([ + new(messageType: ProcessEvents.StartProcess, process), + new(messageType: ProcessEvents.StartProcess, process), + new(messageType: ProcessEvents.OtherEvent, process), + ]) + .SendEventTo(new ProcessStepTargetBuilder(mergeStep, inputMapping: (inputEvents) => + { + return new() + { + { "str1", inputEvents[process.GetFullEventId(ProcessEvents.StartProcess)] }, + { "str2", inputEvents[process.GetFullEventId(ProcessEvents.StartProcess)] }, + { "str3", inputEvents[process.GetFullEventId(ProcessEvents.OtherEvent)] }, + }; + })); + + mergeStep + .OnFunctionResult() + .SendEventTo(new ProcessFunctionTargetBuilder(finalEchoStep)); + + return process.Build(); + } + + /// + /// Process that delays the merge of three strings until all three are available. + /// Helps test make use of ListenFor() and AllOf() methods with events from steps. + /// + /// ┌────────┐ + /// Other ─►│ echo1 ├───────────────────────────────┐ ┌───────┐ + /// └────────┘ │ │ │ + /// └──►│ │ + /// │ │ + /// ┌────────┐ ┌────────┐ │ │ ┌───────────┐ + /// ┌──► │ echo21 ├────►│ echo22 ├───────────────────►│ merge ├───►│ finalEcho │ + /// │ └────────┘ └────────┘ │ │ └───────────┘ + /// Start │ │ + /// Process ┌──►│ │ + /// │ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │ + /// └──► │ echo31 ├────►│ echo32 ├───►│ echo33 ├──┘ └───────┘ + /// └────────┘ └────────┘ └────────┘ + /// + /// + /// + /// + public static KernelProcess GetDelayedMergeProcess(string processName = ProcessKeys.DelayedMergeProcess) + { + ProcessBuilder process = new(processName); + + var echoStep1 = process.AddStepFromType(id: $"{nameof(CommonSteps.DelayedEchoStep)}1"); + var echoStep21 = process.AddStepFromType(id: $"{nameof(CommonSteps.DelayedEchoStep)}21"); + var echoStep22 = process.AddStepFromType(id: $"{nameof(CommonSteps.DelayedEchoStep)}22"); + var echoStep31 = process.AddStepFromType(id: $"{nameof(CommonSteps.DelayedEchoStep)}31"); + var echoStep32 = process.AddStepFromType(id: $"{nameof(CommonSteps.DelayedEchoStep)}32"); + var echoStep33 = process.AddStepFromType(id: $"{nameof(CommonSteps.DelayedEchoStep)}33"); + + var mergeStep = process.AddStepFromType(id: nameof(CommonSteps.MergeStringsStep)); + + var finalEchoStep = process.AddStepFromType(id: nameof(CommonSteps.EchoStep)); + + process + .OnInputEvent(ProcessEvents.StartProcess) + //.SendEventTo(new(echoStep1)) + .SendEventTo(new(echoStep21)) + .SendEventTo(new(echoStep31)); + + process + .OnInputEvent(ProcessEvents.OtherEvent) + .SendEventTo(new ProcessFunctionTargetBuilder(echoStep1)); + + echoStep21 + .OnFunctionResult() + .SendEventTo(new ProcessFunctionTargetBuilder(echoStep22)); + + echoStep31 + .OnFunctionResult() + .SendEventTo(new ProcessFunctionTargetBuilder(echoStep32)); + + echoStep32 + .OnFunctionResult() + .SendEventTo(new ProcessFunctionTargetBuilder(echoStep33)); + + // merging inputs + process + .ListenFor() + .AllOf([ + new(messageType: CommonSteps.DelayedEchoStep.OutputEvents.DelayedEcho, echoStep1), + new(messageType: CommonSteps.DelayedEchoStep.OutputEvents.DelayedEcho, echoStep22), + new(messageType: CommonSteps.DelayedEchoStep.OutputEvents.DelayedEcho, echoStep33), + ]) + .SendEventTo(new ProcessStepTargetBuilder(mergeStep, inputMapping: (inputEvents) => + { + return new() + { + { "str1", inputEvents[echoStep1.GetFullEventId(CommonSteps.DelayedEchoStep.OutputEvents.DelayedEcho)] }, + { "str2", inputEvents[echoStep22.GetFullEventId(CommonSteps.DelayedEchoStep.OutputEvents.DelayedEcho)] }, + { "str3", inputEvents[echoStep33.GetFullEventId(CommonSteps.DelayedEchoStep.OutputEvents.DelayedEcho)] }, + }; + })); + + mergeStep + .OnFunctionResult() + .SendEventTo(new ProcessFunctionTargetBuilder(finalEchoStep)); + + return process.Build(); + } + + public static IReadOnlyDictionary GetCommonProcessesKeyedDictionary() + { + return new Dictionary() { + { ProcessKeys.CounterProcess, GetCounterProcess() }, + { ProcessKeys.CounterWithEvenNumberDetectionProcess, GetCounterWithEvenDetectionProcess().Build() }, + { ProcessKeys.NestedCounterWithEvenDetectionAndMergeProcess, GetNestedCounterWithEvenDetectionAndMergeProcess().Build() }, + { ProcessKeys.InternalNestedCounterWithEvenDetectionAndMergeProcess, GetInternalNestedCounterWithEvenDetectionAndMergeProcess() }, + { ProcessKeys.DelayedMergeProcess, GetDelayedMergeProcess() }, + { ProcessKeys.SimpleMergeProcess, GetSimpleMergeProcess() }, + }; + } +} diff --git a/dotnet/src/InternalUtilities/process/TestsShared/Services/CounterService.cs b/dotnet/src/InternalUtilities/process/TestsShared/Services/CounterService.cs index a393de8f6169..7bd80c88dc93 100644 --- a/dotnet/src/InternalUtilities/process/TestsShared/Services/CounterService.cs +++ b/dotnet/src/InternalUtilities/process/TestsShared/Services/CounterService.cs @@ -1,10 +1,14 @@ // Copyright (c) Microsoft. All rights reserved. +#pragma warning disable IDE0005 // Using directive is unnecessary using System.Threading; +#pragma warning restore IDE0005 // Using directive is unnecessary -namespace SemanticKernel.Process.TestsShared.Services; +namespace Microsoft.SemanticKernel.Process.TestsShared.Services; +#pragma warning disable CA1812 // Avoid uninstantiated internal classes internal sealed class CounterService : ICounterService +#pragma warning restore CA1812 // Avoid uninstantiated internal classes { internal int _counter = 0; public int GetCount() @@ -17,4 +21,9 @@ public int IncreaseCount() Interlocked.Increment(ref this._counter); return this._counter; } + + public void SetCount(int count) + { + this._counter = count; + } } diff --git a/dotnet/src/InternalUtilities/process/TestsShared/Services/ICounterService.cs b/dotnet/src/InternalUtilities/process/TestsShared/Services/ICounterService.cs index caf6063fdea1..db995b67fd14 100644 --- a/dotnet/src/InternalUtilities/process/TestsShared/Services/ICounterService.cs +++ b/dotnet/src/InternalUtilities/process/TestsShared/Services/ICounterService.cs @@ -1,5 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -namespace SemanticKernel.Process.TestsShared.Services; +namespace Microsoft.SemanticKernel.Process.TestsShared.Services; /// /// Interface for Counter Service used by @@ -17,4 +17,11 @@ public interface ICounterService /// /// int GetCount(); + + /// + /// Set counter to specific value + /// + /// + /// + void SetCount(int count); } diff --git a/dotnet/src/InternalUtilities/process/TestsShared/Services/Storage/JsonFileStorage.cs b/dotnet/src/InternalUtilities/process/TestsShared/Services/Storage/JsonFileStorage.cs new file mode 100644 index 000000000000..94f3bef0ed4d --- /dev/null +++ b/dotnet/src/InternalUtilities/process/TestsShared/Services/Storage/JsonFileStorage.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft. All rights reserved. + +#pragma warning disable IDE0005 // Using directive is unnecessary +using System.IO; +using System.Text.Json; +using System.Threading.Tasks; +#pragma warning restore IDE0005 // Using directive is unnecessary + +namespace Microsoft.SemanticKernel.Process.TestsShared.Services.Storage; + +#pragma warning disable CA1812 // Avoid uninstantiated internal classes +internal sealed class JsonFileStorage : IProcessStorageConnector +#pragma warning restore CA1812 // Avoid uninstantiated internal classes +{ + private readonly string _storageDirectory; + + private readonly JsonSerializerOptions _jsonSerializerOptions = new() + { + WriteIndented = true, + }; + + public JsonFileStorage(string storageDirectory) + { + this._storageDirectory = storageDirectory; + Directory.CreateDirectory(this._storageDirectory); + } + + public async ValueTask OpenConnectionAsync() + { + // For file-based storage, there's no real "connection" to open. + // This method might be used to validate if the storage directory is accessible. + await Task.CompletedTask; + } + + public async ValueTask CloseConnectionAsync() + { + // For file-based storage, there's no real "connection" to close. + await Task.CompletedTask; + } + + private string GetFilePath(string id) + { + return Path.Combine(this._storageDirectory, $"{id}.json"); + } + + public async Task GetEntryAsync(string id) where TEntry : class + { + string filePath = this.GetFilePath(id); + if (!File.Exists(filePath)) + { + return null; + } + + string jsonData = await File.ReadAllTextAsync(filePath); + return JsonSerializer.Deserialize(jsonData); + } + + public async Task SaveEntryAsync(string id, string type, TEntry entry) where TEntry : class + { + string filePath = this.GetFilePath(id); + string jsonData = JsonSerializer.Serialize(entry, this._jsonSerializerOptions); + + await File.WriteAllTextAsync(filePath, jsonData); + return true; + } + + public Task DeleteEntryAsync(string id) + { + string filePath = this.GetFilePath(id); + if (File.Exists(filePath)) + { + File.Delete(filePath); + return Task.FromResult(true); + } + + return Task.FromResult(false); + } +} diff --git a/dotnet/src/InternalUtilities/process/TestsShared/Services/Storage/MockStorage.cs b/dotnet/src/InternalUtilities/process/TestsShared/Services/Storage/MockStorage.cs new file mode 100644 index 000000000000..01073cf057a8 --- /dev/null +++ b/dotnet/src/InternalUtilities/process/TestsShared/Services/Storage/MockStorage.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft. All rights reserved. +#pragma warning disable IDE0005 // Using directive is unnecessary +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading.Tasks; +#pragma warning restore IDE0005 // Using directive is unnecessary + +namespace Microsoft.SemanticKernel.Process.TestsShared.Services.Storage; + +#pragma warning disable CA1812 // Avoid uninstantiated internal classes +internal sealed class MockStorage : IProcessStorageConnector +#pragma warning restore CA1812 // Avoid uninstantiated internal classes +{ + internal readonly Dictionary _dbMock = []; + + public bool ConnectionOpened { get; private set; } = false; + public bool ConnectionClosed { get; private set; } = false; + + public ValueTask OpenConnectionAsync() + { + Console.WriteLine("Mock opening connection"); + this.ConnectionOpened = true; + return ValueTask.CompletedTask; + } + public ValueTask CloseConnectionAsync() + { + Console.WriteLine("Mock closing connection"); + this.ConnectionClosed = true; + return ValueTask.CompletedTask; + } + + public Task DeleteEntryAsync(string id) + { + throw new System.NotImplementedException(); + } + + public Task GetEntryAsync(string id) where TEntry : class + { + if (this._dbMock.TryGetValue(id, out var entry) && entry != null) + { + var deserializedEntry = JsonSerializer.Deserialize(entry.Content); + return Task.FromResult(deserializedEntry); + } + + return Task.FromResult(null); + } + + public Task SaveEntryAsync(string id, string type, TEntry entry) where TEntry : class + { + this._dbMock[id] = new() { Id = id, Type = type, Content = JsonSerializer.Serialize(entry) }; + + return Task.FromResult(true); + } +} + +/// +/// Mock storage entry used for testing purposes. +/// +public record MockStorageEntry +{ + /// + /// Unique identifier for the entry. + /// + public string Id { get; set; } = string.Empty; + + /// + /// Type of the entry. + /// + public string Type { get; set; } = string.Empty; + + /// + /// Serialized content of the entry. + /// + public string Content { get; set; } = string.Empty; +} diff --git a/dotnet/src/InternalUtilities/process/TestsShared/Setup/KernelSetup.cs b/dotnet/src/InternalUtilities/process/TestsShared/Setup/KernelSetup.cs index b8307019edb9..a70dc96db5c8 100644 --- a/dotnet/src/InternalUtilities/process/TestsShared/Setup/KernelSetup.cs +++ b/dotnet/src/InternalUtilities/process/TestsShared/Setup/KernelSetup.cs @@ -1,9 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. using Microsoft.Extensions.DependencyInjection; -using Microsoft.SemanticKernel; -using SemanticKernel.Process.TestsShared.Services; +using Microsoft.SemanticKernel.Process.TestsShared.Services; -namespace SemanticKernel.Process.TestsShared.Setup; +namespace Microsoft.SemanticKernel.Process.TestsShared.Setup; internal static class KernelSetup { diff --git a/dotnet/src/InternalUtilities/process/TestsShared/Steps/CommonSteps.cs b/dotnet/src/InternalUtilities/process/TestsShared/Steps/CommonSteps.cs index 5cf2fbcfd8e1..29e0728497dc 100644 --- a/dotnet/src/InternalUtilities/process/TestsShared/Steps/CommonSteps.cs +++ b/dotnet/src/InternalUtilities/process/TestsShared/Steps/CommonSteps.cs @@ -1,10 +1,12 @@ // Copyright (c) Microsoft. All rights reserved. +#pragma warning disable IDE0005 // Using directive is unnecessary using System; +using System.Threading; using System.Threading.Tasks; -using Microsoft.SemanticKernel; -using SemanticKernel.Process.TestsShared.Services; +#pragma warning restore IDE0005 // Using directive is unnecessary +using Microsoft.SemanticKernel.Process.TestsShared.Services; -namespace SemanticKernel.Process.TestsShared.Steps; +namespace Microsoft.SemanticKernel.Process.TestsShared.Steps; #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member @@ -16,25 +18,65 @@ public static class CommonSteps /// /// The step that counts how many times it has been invoked. /// - public sealed class CountStep : KernelProcessStep + public sealed class CountStep : KernelProcessStep { public const string CountFunction = nameof(Count); private readonly ICounterService _counter; + + private CounterState? _state; public CountStep(ICounterService counterService) { this._counter = counterService; } + public override ValueTask ActivateAsync(KernelProcessStepState state) + { + this._state = state.State ?? new(); + this._counter.SetCount(this._state.Count); + Console.WriteLine($"Activating counter with value {this._state.Count}"); + return base.ActivateAsync(state); + } + [KernelFunction] public string Count() { int count = this._counter.IncreaseCount(); - + this._state!.Count = count; return count.ToString(); } } + public class CounterState + { + public int Count { get; set; } = 0; + } + + /// + /// The step that counts how many times it has been invoked. + /// + public sealed class SimpleCountStep : KernelProcessStep + { + public const string CountFunction = nameof(Count); + + private CounterState? _state; + + public override ValueTask ActivateAsync(KernelProcessStepState state) + { + this._state = state.State ?? new(); + Console.WriteLine($"Activating counter with value {this._state.Count}"); + return base.ActivateAsync(state); + } + + [KernelFunction] + public string Count() + { + this._state!.Count++; + Console.WriteLine($"[COUNTER-{this.StepName}] {this._state.Count}"); + return this._state!.Count.ToString(); + } + } + /// /// The step that counts how many times it has been invoked. /// @@ -46,11 +88,11 @@ public sealed class EvenNumberDetectorStep : KernelProcessStep public static class OutputEvents { /// - /// Event number event name + /// Even number event name /// public const string EvenNumber = nameof(EvenNumber); /// - /// Event number event name + /// Odd number event name /// public const string OddNumber = nameof(OddNumber); } @@ -67,11 +109,13 @@ public async Task DetectEvenNumberAsync(string numberString, KernelProcessStepCo var number = int.Parse(numberString); if (number % 2 == 0) { - await context.EmitEventAsync(OutputEvents.EvenNumber, numberString); + Console.WriteLine($"[EVEN_NUMBER-{this.StepName}] {number}"); + await context.EmitEventAsync(OutputEvents.EvenNumber, numberString, KernelProcessEventVisibility.Public); return; } - await context.EmitEventAsync(OutputEvents.OddNumber, numberString); + Console.WriteLine($"[ODD_NUMBER-{this.StepName}] {number}"); + await context.EmitEventAsync(OutputEvents.OddNumber, numberString, KernelProcessEventVisibility.Public); } } @@ -80,11 +124,82 @@ public async Task DetectEvenNumberAsync(string numberString, KernelProcessStepCo /// public sealed class EchoStep : KernelProcessStep { + /// + /// Output events emitted by + /// + public static class OutputEvents + { + /// + /// Echo message event name + /// + public const string EchoMessage = nameof(EchoMessage); + } + + [KernelFunction] + public async Task EchoAsync(KernelProcessStepContext context, string message) + { + Console.WriteLine($"[ECHO-{this.StepName}] {message}"); + await context.EmitEventAsync(OutputEvents.EchoMessage, data: message, KernelProcessEventVisibility.Public); + return message; + } + } + + /// + /// A step that echos its input. + /// + public sealed class DualEchoStep : KernelProcessStep + { + /// + /// Output events emitted by + /// + public static class OutputEvents + { + /// + /// Echo message event name + /// + public const string EchoMessage = nameof(EchoMessage); + } + [KernelFunction] - public string Echo(string message) + public async Task EchoAsync(KernelProcessStepContext context, string message1, string message2) { - Console.WriteLine($"[ECHO] {message}"); + var message = $"{message1} {message2}"; + Console.WriteLine($"[DUAL-ECHO-{this.StepName}] {message}"); + await context.EmitEventAsync(OutputEvents.EchoMessage, data: message, KernelProcessEventVisibility.Public); return message; } } + + /// + /// A step that echos its input. Delays input for a specified amount of time. + /// + public sealed class DelayedEchoStep : KernelProcessStep + { + public static class OutputEvents + { + public const string DelayedEcho = nameof(DelayedEcho); + } + + private readonly int _delayInMs = 1000; + + [KernelFunction] + public async Task EchoAsync(KernelProcessStepContext context, string message) + { + // Simulate a delay + Thread.Sleep(this._delayInMs); + Console.WriteLine($"[DELAYED_ECHO-{this.StepName}]: {message}"); + await context.EmitEventAsync(OutputEvents.DelayedEcho, data: message); + return message; + } + } + + public sealed class MergeStringsStep : KernelProcessStep + { + [KernelFunction] + public string MergeStrings(string str1, string str2, string str3) + { + Console.WriteLine($"[MERGE_STRINGS-{this.StepName}] {str1} {str2} {str3}"); + return string.Join(",", [str1, str2, str3]); + } + } } From f1f36e2d49281d83b437843feb32f9b8f7dabe8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estefan=C3=ADa=20Tenorio?= <8483207+esttenorio@users.noreply.github.com> Date: Mon, 4 Aug 2025 16:07:44 -0700 Subject: [PATCH 2/4] .Net: Process Framework Removing DAPR runtime related files (#12854) ### Motivation and Context ### Description - Removing DAPR related files - Migrating fully existing dapr sample to use local runtime instead - Fixing build/fomrating/ut errors ### Contribution Checklist - [ ] The code builds clean without any errors or warnings - [ ] 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 - [ ] All unit tests pass, and I have added new tests where possible - [ ] I didn't break anyone :smile: --- .../Controllers/ProcessController.cs | 7 +- .../ProcessWithLocalRuntime.http | 5 + .../Demos/ProcessWithLocalRuntime/Program.cs | 8 + .../Contracts/ProcessStartRequest.cs | 19 - .../Controllers/ProcessTestController.cs | 107 ---- .../HealthActor.cs | 25 - .../IHealthActor.cs | 17 - .../Process.IntegrationTestHost.Dapr.csproj | 27 - .../ProcessStateTypeResolver.cs | 108 ---- .../Program.cs | 40 -- .../appsettings.json | 9 - .../DaprTestProcessContext.cs | 84 --- .../Process.IntegrationTestRunner.Dapr.csproj | 58 -- .../ProcessTestFixture.cs | 157 ------ .../README.md | 5 - .../KernelProcessEventSerializationTests.cs | 114 ---- .../Process.Runtime.Dapr.UnitTests.csproj | 36 -- .../ProcessEventSerializationTests.cs | 114 ---- .../ProcessMessageSerializationTests.cs | 143 ----- .../TestSerializer.cs | 25 - .../Actors/ActorStateKeys.cs | 35 -- .../Actors/EventBufferActor.cs | 67 --- .../Actors/ExternalEventBufferActor.cs | 67 --- .../Actors/ExternalMessageBufferActor.cs | 39 -- .../ExternalMessageBufferActorWrapper.cs | 45 -- .../Process.Runtime.Dapr/Actors/MapActor.cs | 186 ------ .../Actors/MessageBufferActor.cs | 67 --- .../Actors/ProcessActor.cs | 531 ------------------ .../Process.Runtime.Dapr/Actors/ProxyActor.cs | 80 --- .../Process.Runtime.Dapr/Actors/StepActor.cs | 497 ---------------- .../Process.Runtime.Dapr/AssemblyInfo.cs | 6 - .../DaprKernelProcessContext.cs | 94 ---- .../DaprKernelProcessFactory.cs | 36 -- .../Process.Runtime.Dapr/DaprMapInfo.cs | 64 --- .../Process.Runtime.Dapr/DaprProcessInfo.cs | 100 ---- .../Process.Runtime.Dapr/DaprProxyInfo.cs | 58 -- .../Process.Runtime.Dapr/DaprStepInfo.cs | 71 --- .../Interfaces/IEventBuffer.cs | 27 - .../Interfaces/IExternalEventBuffer.cs | 26 - .../Interfaces/IExternalMessageBuffer.cs | 25 - .../Process.Runtime.Dapr/Interfaces/IMap.cs | 18 - .../Interfaces/IMessageBuffer.cs | 27 - .../Interfaces/IProcess.cs | 56 -- .../Process.Runtime.Dapr/Interfaces/IProxy.cs | 18 - .../Process.Runtime.Dapr/Interfaces/IStep.cs | 37 -- .../KernelProcessDaprExtensions.cs | 28 - .../Process.Runtime.Dapr.csproj | 47 -- .../KernelProcessEventSerializer.cs | 53 -- .../Serialization/ProcessEventSerializer.cs | 45 -- .../Serialization/ProcessMessageSerializer.cs | 66 --- .../Serialization/TypeContainers.cs | 21 - .../Serialization/TypeInfo.cs | 44 -- .../Runtime.Local/LocalMapTests.cs | 1 - .../process/TestsShared/Setup/KernelSetup.cs | 2 + 54 files changed, 20 insertions(+), 3672 deletions(-) create mode 100644 dotnet/samples/Demos/ProcessWithLocalRuntime/ProcessWithLocalRuntime.http delete mode 100644 dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/Contracts/ProcessStartRequest.cs delete mode 100644 dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/Controllers/ProcessTestController.cs delete mode 100644 dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/HealthActor.cs delete mode 100644 dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/IHealthActor.cs delete mode 100644 dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/Process.IntegrationTestHost.Dapr.csproj delete mode 100644 dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/ProcessStateTypeResolver.cs delete mode 100644 dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/Program.cs delete mode 100644 dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/appsettings.json delete mode 100644 dotnet/src/Experimental/Process.IntegrationTestRunner.Dapr/DaprTestProcessContext.cs delete mode 100644 dotnet/src/Experimental/Process.IntegrationTestRunner.Dapr/Process.IntegrationTestRunner.Dapr.csproj delete mode 100644 dotnet/src/Experimental/Process.IntegrationTestRunner.Dapr/ProcessTestFixture.cs delete mode 100644 dotnet/src/Experimental/Process.IntegrationTestRunner.Dapr/README.md delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/KernelProcessEventSerializationTests.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/Process.Runtime.Dapr.UnitTests.csproj delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/ProcessEventSerializationTests.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/ProcessMessageSerializationTests.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/TestSerializer.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ActorStateKeys.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr/Actors/EventBufferActor.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ExternalEventBufferActor.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ExternalMessageBufferActor.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ExternalMessageBufferActorWrapper.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr/Actors/MapActor.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr/Actors/MessageBufferActor.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ProcessActor.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ProxyActor.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr/Actors/StepActor.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr/AssemblyInfo.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr/DaprKernelProcessContext.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr/DaprKernelProcessFactory.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr/DaprMapInfo.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr/DaprProcessInfo.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr/DaprProxyInfo.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr/DaprStepInfo.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr/Interfaces/IEventBuffer.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr/Interfaces/IExternalEventBuffer.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr/Interfaces/IExternalMessageBuffer.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr/Interfaces/IMap.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr/Interfaces/IMessageBuffer.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr/Interfaces/IProcess.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr/Interfaces/IProxy.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr/Interfaces/IStep.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr/KernelProcessDaprExtensions.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr/Process.Runtime.Dapr.csproj delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr/Serialization/KernelProcessEventSerializer.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr/Serialization/ProcessEventSerializer.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr/Serialization/ProcessMessageSerializer.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr/Serialization/TypeContainers.cs delete mode 100644 dotnet/src/Experimental/Process.Runtime.Dapr/Serialization/TypeInfo.cs diff --git a/dotnet/samples/Demos/ProcessWithLocalRuntime/Controllers/ProcessController.cs b/dotnet/samples/Demos/ProcessWithLocalRuntime/Controllers/ProcessController.cs index a1dfc528b863..bce4788fefa5 100644 --- a/dotnet/samples/Demos/ProcessWithLocalRuntime/Controllers/ProcessController.cs +++ b/dotnet/samples/Demos/ProcessWithLocalRuntime/Controllers/ProcessController.cs @@ -13,14 +13,17 @@ namespace ProcessWithDapr.Controllers; public class ProcessController : ControllerBase { private readonly Kernel _kernel; + private readonly IProcessStorageConnector _storageConnector; /// /// Initializes a new instance of the class. /// /// An instance of - public ProcessController(Kernel kernel) + /// An implementation of instance of + public ProcessController(Kernel kernel, IProcessStorageConnector storageConnector) { this._kernel = kernel; + this._storageConnector = storageConnector; } /// @@ -32,7 +35,7 @@ public ProcessController(Kernel kernel) public async Task PostAsync(string processId) { var process = this.GetProcess(); - var processContext = await process.StartAsync(this._kernel, new KernelProcessEvent() { Id = CommonEvents.StartProcess }, processId: processId); + var processContext = await process.StartAsync(this._kernel, new KernelProcessEvent() { Id = CommonEvents.StartProcess }, processId: processId, storageConnector: this._storageConnector); var finalState = await processContext.GetStateAsync(); return this.Ok(processId); diff --git a/dotnet/samples/Demos/ProcessWithLocalRuntime/ProcessWithLocalRuntime.http b/dotnet/samples/Demos/ProcessWithLocalRuntime/ProcessWithLocalRuntime.http new file mode 100644 index 000000000000..c709a38d790f --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithLocalRuntime/ProcessWithLocalRuntime.http @@ -0,0 +1,5 @@ +# For more info on HTTP files go to https://aka.ms/vs/httpfile +@host = https://localhost:5000 +@customProcessId = myProcessId123 + +GET {{host}}/processes/{{customProcessId}} \ No newline at end of file diff --git a/dotnet/samples/Demos/ProcessWithLocalRuntime/Program.cs b/dotnet/samples/Demos/ProcessWithLocalRuntime/Program.cs index 635153194907..ecaa98f6ecbb 100644 --- a/dotnet/samples/Demos/ProcessWithLocalRuntime/Program.cs +++ b/dotnet/samples/Demos/ProcessWithLocalRuntime/Program.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Process.TestsShared.Services.Storage; var builder = WebApplication.CreateBuilder(args); @@ -15,6 +16,13 @@ builder.Services.AddKernel(); builder.Services.AddControllers(); + +// Registering storage used for persisting process state with Local Runtime +string tempDirectoryPath = Path.Combine(Path.GetTempPath(), "ProcessWithLocalRuntimeStorage"); +var storageInstance = new JsonFileStorage(tempDirectoryPath); + +builder.Services.AddSingleton(storageInstance); + var app = builder.Build(); if (app.Environment.IsDevelopment()) diff --git a/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/Contracts/ProcessStartRequest.cs b/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/Contracts/ProcessStartRequest.cs deleted file mode 100644 index 4c32fb1c6681..000000000000 --- a/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/Contracts/ProcessStartRequest.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -namespace Microsoft.SemanticKernel.Process.IntegrationTests; - -/// -/// Represents the body of a POST request to start a process in the test host. -/// -public record ProcessStartRequest -{ - /// - /// The process to start. - /// - public required DaprProcessInfo Process { get; set; } - - /// - /// The initial event to send to the process. - /// - public required string InitialEvent { get; set; } -} diff --git a/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/Controllers/ProcessTestController.cs b/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/Controllers/ProcessTestController.cs deleted file mode 100644 index 28bb1d7a57f3..000000000000 --- a/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/Controllers/ProcessTestController.cs +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json; -using Dapr.Actors.Client; -using Microsoft.AspNetCore.Mvc; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Process.Serialization; -using Microsoft.SemanticKernel.Process.TestsShared.CloudEvents; - -namespace Microsoft.SemanticKernel.Process.IntegrationTests.Controllers; - -/// -/// A controller for starting and managing processes. -/// -[ApiController] -[Route("/")] -[Produces("application/json")] -public class ProcessTestController : Controller -{ - private static readonly Dictionary s_processes = new(); - private readonly Kernel _kernel; - - /// - /// Initializes a new instance of the class. - /// - /// - public ProcessTestController(Kernel kernel) - { - this._kernel = kernel; - } - - /// - /// Starts a process. - /// - /// The Id of the process - /// The request - /// - [HttpPost("processes/{processId}")] - public async Task StartProcessAsync(string processId, [FromBody] ProcessStartRequest request) - { - if (s_processes.ContainsKey(processId)) - { - return this.BadRequest("Process already started"); - } - - KernelProcessEvent initialEvent = request.InitialEvent.ToKernelProcessEvent(); - - var kernelProcess = request.Process.ToKernelProcess(); - var context = await kernelProcess.StartAsync(initialEvent); - s_processes.Add(processId, context); - - return this.Ok(); - } - - /// - /// Retrieves information about a process. - /// - /// The Id of the process. - /// - [HttpGet("processes/{processId}")] - public async Task GetProcessAsync(string processId) - { - if (!s_processes.TryGetValue(processId, out DaprKernelProcessContext? context)) - { - return this.NotFound(); - } - - var process = await context.GetStateAsync(); - var daprProcess = DaprProcessInfo.FromKernelProcess(process); - - var serialized = JsonSerializer.Serialize(daprProcess); - - return this.Ok(daprProcess); - } - - /// - /// Retrieves current state of the MockCloudEventClient used in the running process - /// - /// The Id of the process. - /// Mock Cloud client ingested via dependency injection - /// - [HttpGet("processes/{processId}/mockCloudClient")] - public Task GetMockCloudClient(string processId, MockCloudEventClient cloudClient) - { - if (!s_processes.TryGetValue(processId, out DaprKernelProcessContext? context)) - { - return Task.FromResult(this.NotFound()); - } - - var cloudClientCopy = JsonSerializer.Deserialize(JsonSerializer.Serialize(cloudClient)); - cloudClient.Reset(); - - return Task.FromResult(this.Ok(cloudClientCopy)); - } - - /// - /// Checks the health of the Dapr runtime by attempting to send a message to a health actor. - /// - /// - [HttpGet("daprHealth")] - public async Task HealthCheckAsync() - { - var healthActor = ActorProxy.Create(new Dapr.Actors.ActorId(Guid.NewGuid().ToString("n")), nameof(HealthActor)); - await healthActor.HealthCheckAsync(); - return this.Ok(); - } -} diff --git a/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/HealthActor.cs b/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/HealthActor.cs deleted file mode 100644 index 3c975673548c..000000000000 --- a/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/HealthActor.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Dapr.Actors.Runtime; - -namespace Microsoft.SemanticKernel.Process.IntegrationTests; - -/// -/// An implementation of the health actor that is only used for testing the health of the Dapr runtime. -/// -public class HealthActor : Actor, IHealthActor -{ - /// - /// Initializes a new instance of the class. - /// - /// - public HealthActor(ActorHost host) : base(host) - { - } - - /// - public Task HealthCheckAsync() - { - return Task.CompletedTask; - } -} diff --git a/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/IHealthActor.cs b/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/IHealthActor.cs deleted file mode 100644 index 0293a747c3d9..000000000000 --- a/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/IHealthActor.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Dapr.Actors; - -namespace Microsoft.SemanticKernel.Process.IntegrationTests; - -/// -/// An interface for a health actor that is only used for testing the health of the Dapr runtime. -/// -public interface IHealthActor : IActor -{ - /// - /// An empty method used to determine if Dapr runtime is up and reachable. - /// - /// - Task HealthCheckAsync(); -} diff --git a/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/Process.IntegrationTestHost.Dapr.csproj b/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/Process.IntegrationTestHost.Dapr.csproj deleted file mode 100644 index c5d8ba4378cb..000000000000 --- a/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/Process.IntegrationTestHost.Dapr.csproj +++ /dev/null @@ -1,27 +0,0 @@ - - - - SemanticKernel.Process.IntegrationTests - SemanticKernel.Process.IntegrationTestHost.Dapr - net8.0 - enable - enable - false - $(NoWarn);CA2007,CA1861,VSTHRD111,SKEXP0001,SKEXP0010,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0080,SKEXP0110 - b7762d10-e29b-4bb1-8b74-b6d69a667dd4 - true - - - - - - - - - - - - - - - diff --git a/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/ProcessStateTypeResolver.cs b/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/ProcessStateTypeResolver.cs deleted file mode 100644 index d0b1809fa47a..000000000000 --- a/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/ProcessStateTypeResolver.cs +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; - -namespace Microsoft.SemanticKernel.Process.IntegrationTests; - -/// -/// An implementation of that resolves the type information for . -/// -public class ProcessStateTypeResolver : DefaultJsonTypeInfoResolver where T : KernelProcessStep -{ - private static readonly Type s_genericType = typeof(KernelProcessStep<>); - private readonly Dictionary _types = - new() - { - { "process", typeof(KernelProcessState) }, - { "map", typeof(KernelProcessMapState) }, - }; - - /// - /// Initializes a new instance of the class. - /// - public ProcessStateTypeResolver() - { - // Load all types from the resources assembly that derive from KernelProcessStep - var assembly = typeof(T).Assembly; - var stepTypes = assembly.GetTypes().Where(t => t.IsSubclassOf(typeof(KernelProcessStep))); - - foreach (var type in stepTypes) - { - if (TryGetSubtypeOfStatefulStep(type, out Type? genericStepType) && genericStepType is not null) - { - var userStateType = genericStepType.GetGenericArguments()[0]; - var stateType = typeof(KernelProcessStepState<>).MakeGenericType(userStateType); - this._types.TryAdd(userStateType.Name, stateType); - } - } - } - - /// - public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options) - { - JsonTypeInfo jsonTypeInfo = base.GetTypeInfo(type, options); - - Type baseType = typeof(KernelProcessStepState); - if (jsonTypeInfo.Type == baseType) - { - var jsonDerivedTypes = this._types.Select(t => new JsonDerivedType(t.Value, t.Key)).ToList(); - - jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions - { - TypeDiscriminatorPropertyName = "$state-type", - IgnoreUnrecognizedTypeDiscriminators = true, - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization - }; - - // Add the known derived types to the collection - var derivedTypesCollection = jsonTypeInfo.PolymorphismOptions.DerivedTypes; - if (derivedTypesCollection is List list) - { - list.AddRange(jsonDerivedTypes); - } - else - { - foreach (var item in jsonDerivedTypes!) - { - derivedTypesCollection!.Add(item); - } - } - } - else if (jsonTypeInfo.Type == typeof(DaprStepInfo)) - { - jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions - { - TypeDiscriminatorPropertyName = "$state-type", - IgnoreUnrecognizedTypeDiscriminators = true, - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization, - DerivedTypes = - { - new JsonDerivedType(typeof(DaprProcessInfo), nameof(DaprProcessInfo)), - new JsonDerivedType(typeof(DaprMapInfo), nameof(DaprMapInfo)), - new JsonDerivedType(typeof(DaprProxyInfo), nameof(DaprProxyInfo)), - } - }; - } - - return jsonTypeInfo; - } - - private static bool TryGetSubtypeOfStatefulStep(Type? type, out Type? genericStateType) - { - while (type != null && type != typeof(object)) - { - if (type.IsGenericType && type.GetGenericTypeDefinition() == s_genericType) - { - genericStateType = type; - return true; - } - - type = type.BaseType; - } - - genericStateType = null; - return false; - } -} diff --git a/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/Program.cs b/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/Program.cs deleted file mode 100644 index 7ab9bd668653..000000000000 --- a/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/Program.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Process.IntegrationTests; -using Microsoft.SemanticKernel.Process.TestsShared.CloudEvents; - -var builder = WebApplication.CreateBuilder(args); - -// Configure logging -builder.Services.AddLogging((logging) => -{ - logging.AddConsole(); - logging.AddDebug(); -}); - -// Configure the Kernel with DI. This is required for dependency injection to work with processes. -builder.Services.AddKernel(); - -// Configure IExternalKernelProcessMessageChannel used for testing purposes -builder.Services.AddSingleton(MockCloudEventClient.Instance); -builder.Services.AddSingleton(MockCloudEventClient.Instance); - -// Configure Dapr -builder.Services.AddActors(static options => -{ - // Register the actors required to run Processes - options.AddProcessActors(); - options.Actors.RegisterActor(); -}); - -builder.Services.AddControllers().AddJsonOptions(options => -{ - options.JsonSerializerOptions.TypeInfoResolver = new ProcessStateTypeResolver(); -}); - -var app = builder.Build(); - -app.MapControllers(); -app.MapActorsHandlers(); -app.Run(); diff --git a/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/appsettings.json b/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/appsettings.json deleted file mode 100644 index 10f68b8c8b4f..000000000000 --- a/dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/appsettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" -} diff --git a/dotnet/src/Experimental/Process.IntegrationTestRunner.Dapr/DaprTestProcessContext.cs b/dotnet/src/Experimental/Process.IntegrationTestRunner.Dapr/DaprTestProcessContext.cs deleted file mode 100644 index a9557aba1f08..000000000000 --- a/dotnet/src/Experimental/Process.IntegrationTestRunner.Dapr/DaprTestProcessContext.cs +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Net.Http.Json; -using System.Text.Json; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Process; -using Microsoft.SemanticKernel.Process.Serialization; -using SemanticKernel.Process.TestsShared.CloudEvents; - -namespace SemanticKernel.Process.IntegrationTests; -internal sealed class DaprTestProcessContext : KernelProcessContext -{ - private readonly HttpClient _httpClient; - private readonly KernelProcess _process; - private readonly string _processId; - private readonly JsonSerializerOptions _serializerOptions; - - internal DaprTestProcessContext(KernelProcess process, HttpClient httpClient) - { - if (string.IsNullOrWhiteSpace(process.State.RunId)) - { - process = process with { State = process.State with { RunId = Guid.NewGuid().ToString() } }; - } - - this._process = process; - this._processId = process.State.RunId; - this._httpClient = httpClient; - - this._serializerOptions = new JsonSerializerOptions() - { - TypeInfoResolver = new ProcessStateTypeResolver(), - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; - } - - /// - /// Starts the process with an initial event. - /// - /// The initial event. - /// - internal async Task StartWithEventAsync(KernelProcessEvent initialEvent) - { - var daprProcess = DaprProcessInfo.FromKernelProcess(this._process); - var request = new ProcessStartRequest { Process = daprProcess, InitialEvent = initialEvent.ToJson() }; - - var response = await this._httpClient.PostAsJsonAsync($"http://localhost:5200/processes/{this._processId}", request, options: this._serializerOptions).ConfigureAwait(false); - if (!response.IsSuccessStatusCode) - { - throw new InvalidOperationException("Failed to start process"); - } - } - - public override async Task GetStateAsync() - { - var response = await this._httpClient.GetFromJsonAsync($"http://localhost:5200/processes/{this._processId}", options: this._serializerOptions); - return response switch - { - null => throw new InvalidOperationException("Process not found"), - _ => response.ToKernelProcess() - }; - } - - public override Task SendEventAsync(KernelProcessEvent processEvent) - { - throw new NotImplementedException(); - } - - public override Task StopAsync() - { - throw new NotImplementedException(); - } - - public override async Task GetExternalMessageChannelAsync() - { - var response = await this._httpClient.GetFromJsonAsync($"http://localhost:5200/processes/{this._processId}/mockCloudClient", options: this._serializerOptions); - return response switch - { - null => throw new InvalidOperationException("Process not found"), - _ => response - }; - } - - public override Task GetProcessIdAsync() => Task.FromResult(this._process.State.RunId!); -} diff --git a/dotnet/src/Experimental/Process.IntegrationTestRunner.Dapr/Process.IntegrationTestRunner.Dapr.csproj b/dotnet/src/Experimental/Process.IntegrationTestRunner.Dapr/Process.IntegrationTestRunner.Dapr.csproj deleted file mode 100644 index 8029ae2ff601..000000000000 --- a/dotnet/src/Experimental/Process.IntegrationTestRunner.Dapr/Process.IntegrationTestRunner.Dapr.csproj +++ /dev/null @@ -1,58 +0,0 @@ - - - - SemanticKernel.Process.IntegrationTests - SemanticKernel.Process.IntegrationTestRunner.Dapr - net8.0 - enable - enable - false - $(NoWarn);CA2007,CA1861,VSTHRD111,SKEXP0001,SKEXP0010,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0080,SKEXP0110 - b7762d10-e29b-4bb1-8b74-b6d69a667dd4 - true - - - - - - - PreserveNewest - - - - - - - - - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - - - - - - diff --git a/dotnet/src/Experimental/Process.IntegrationTestRunner.Dapr/ProcessTestFixture.cs b/dotnet/src/Experimental/Process.IntegrationTestRunner.Dapr/ProcessTestFixture.cs deleted file mode 100644 index c6f55eb95f69..000000000000 --- a/dotnet/src/Experimental/Process.IntegrationTestRunner.Dapr/ProcessTestFixture.cs +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Diagnostics; -using System.Net; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Process; -using Xunit; - -namespace SemanticKernel.Process.IntegrationTests; - -/// -/// A test fixture for running shared process tests across multiple runtimes. -/// -public sealed class ProcessTestFixture : IDisposable, IAsyncLifetime -{ - private System.Diagnostics.Process? _process; - private HttpClient? _httpClient; - - /// - /// Called by xUnit before the test is run. - /// - /// - public async Task InitializeAsync() - { - this._httpClient = new HttpClient(); - await this.StartTestHostAsync(); - } - - /// - /// Starts the test host by creating a new process with the Dapr cli. The startup process can take 30 seconds or more and so we wait for this to complete before returning. - /// - /// - private async Task StartTestHostAsync() - { - try - { - string workingDirectory = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), @"..\..\..\..\Process.IntegrationTestHost.Dapr")); - var processStartInfo = new ProcessStartInfo - { - FileName = "dapr", - Arguments = "run --app-id daprprocesstests --app-port 5200 --dapr-http-port 3500 -- dotnet run --urls http://localhost:5200", - WorkingDirectory = workingDirectory, - RedirectStandardOutput = false, - RedirectStandardError = false, - UseShellExecute = true, - CreateNoWindow = false - }; - - this._process = new System.Diagnostics.Process - { - StartInfo = processStartInfo - }; - - this._process.Start(); - await this.WaitForHostStartupAsync(); - } - catch (Exception) - { - throw; - } - } - - private async Task ShutdownTestHostAsync() - { - var processStartInfo = new ProcessStartInfo - { - FileName = "dapr", - Arguments = "stop --app-id daprprocesstests", - RedirectStandardOutput = false, - RedirectStandardError = false, - UseShellExecute = true, - CreateNoWindow = false - }; - - using var shutdownProcess = new System.Diagnostics.Process - { - StartInfo = processStartInfo - }; - - shutdownProcess.Start(); - await shutdownProcess.WaitForExitAsync(); - } - - /// - /// Waits for the test host to be ready to accept requests. This is determined by making a request to the health endpoint. - /// - /// - /// - private async Task WaitForHostStartupAsync() - { - // Wait for the process to start - var now = DateTime.Now; - while (DateTime.Now - now < TimeSpan.FromSeconds(120)) - { - if (this._process!.HasExited) - { - break; - } - - try - { - var healthResponse = await this._httpClient!.GetAsync(new Uri("http://localhost:5200/daprHealth")); - if (healthResponse.StatusCode == HttpStatusCode.OK) - { - await Task.Delay(TimeSpan.FromSeconds(10)); - return; - } - } - catch (HttpRequestException) - { - // Do nothing, just wait - } - } - - throw new InvalidProgramException("Dapr Test Host did not start"); - } - - /// - /// Starts a process. - /// - /// The process to start. - /// An instance of - /// An optional initial event. - /// channel used for external messages - /// A - public async Task StartProcessAsync(KernelProcess process, Kernel kernel, KernelProcessEvent initialEvent, IExternalKernelProcessMessageChannel? externalMessageChannel = null) - { - // Actual Kernel injection of Kernel and ExternalKernelProcessMessageChannel is in dotnet\src\Experimental\Process.IntegrationTestHost.Dapr\Program.cs - var context = new DaprTestProcessContext(process, this._httpClient!); - await context.StartWithEventAsync(initialEvent); - return context; - } - - /// - /// Disposes of the test fixture. - /// - public void Dispose() - { - if (this._process is not null && this._process.HasExited) - { - this._process?.Kill(); - this._process?.WaitForExit(); - } - - this._process?.Dispose(); - this._httpClient?.Dispose(); - } - - /// - /// Called by xUnit after the test is run. - /// - /// - public Task DisposeAsync() - { - return this.ShutdownTestHostAsync(); - } -} diff --git a/dotnet/src/Experimental/Process.IntegrationTestRunner.Dapr/README.md b/dotnet/src/Experimental/Process.IntegrationTestRunner.Dapr/README.md deleted file mode 100644 index 5341a344bade..000000000000 --- a/dotnet/src/Experimental/Process.IntegrationTestRunner.Dapr/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Dapr Process Integration Tests Runner - -**_ Dapr must be running on the machine to run these tests _** - -Make sure you setup Dapr for local development before running these tests. Follow this guide: [Dapr local development](https://docs.dapr.io/getting-started/install-dapr-selfhost/) diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/KernelProcessEventSerializationTests.cs b/dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/KernelProcessEventSerializationTests.cs deleted file mode 100644 index b4b2774f8328..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/KernelProcessEventSerializationTests.cs +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System; -using System.Collections.Generic; -using System.IO; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Process.Runtime; -using Microsoft.SemanticKernel.Process.Serialization; -using Xunit; - -namespace SemanticKernel.Process.Dapr.Runtime.UnitTests; - -/// -/// Unit tests for the class. -/// -public class KernelProcessEventSerializationTests -{ - /// - /// Validates that a can be serialized and deserialized correctly - /// with out an explicit type definition for - /// - [Fact] - public void VerifySerializeEventSingleTest() - { - // Arrange, Act & Assert - VerifyContainerSerialization([new() { Id = "Test", Data = 3 }]); - VerifyContainerSerialization([new() { Id = "Test", Data = "test" }]); - VerifyContainerSerialization([new() { Id = "Test", Data = Guid.NewGuid() }]); - VerifyContainerSerialization([new() { Id = "Test", Data = new int[] { 1, 2, 3, 4 } }]); - VerifyContainerSerialization([new() { Id = "Test", Data = new ComplexData { Id = "test", Value = 3 } }]); - VerifyContainerSerialization([new() { Id = "testid", Data = KernelProcessError.FromException(new InvalidOperationException()) }]); - } - - /// - /// Validates that a list can be serialized and deserialized correctly - /// with out varying types assigned to for - /// - [Fact] - public void VerifySerializeEventMixedTest() - { - // Arrange, Act & Assert - VerifyContainerSerialization( - [ - new() { Id = "Test", Data = 3 }, - new() { Id = "Test", Data = "test" }, - new() { Id = "Test", Data = Guid.NewGuid() }, - new() { Id = "Test", Data = new int[] { 1, 2, 3, 4 } }, - new() { Id = "Test", Data = new ComplexData { Id = "test", Value = 3 } }, - new() { Id = "testid", Data = KernelProcessError.FromException(new InvalidOperationException()) }, - ]); - } - - /// - /// Validates that a list can be serialized and deserialized correctly - /// with out varying types assigned to for - /// - [Fact] - public void VerifyDataContractSerializationTest() - { - // Arrange - KernelProcessEvent[] processEvents = - [ - new() { Id = "Test", Data = 3 }, - new() { Id = "Test", Data = "test" }, - new() { Id = "Test", Data = Guid.NewGuid() }, - new() { Id = "Test", Data = new int[] { 1, 2, 3, 4 } }, - new() { Id = "Test", Data = new ComplexData { Id = "test", Value = 3 } }, - new() { Id = "testid", Data = KernelProcessError.FromException(new InvalidOperationException()) }, - ]; - List jsonEvents = []; - foreach (KernelProcessEvent processEvent in processEvents) - { - jsonEvents.Add(KernelProcessEventSerializer.ToJson(processEvent)); - } - - // Act - using MemoryStream stream = new(); - jsonEvents.Serialize(stream); - stream.Position = 0; - - List? copy = stream.Deserialize>(); - - // Assert - Assert.NotNull(copy); - - // Act - IList copiedEvents = KernelProcessEventSerializer.ToKernelProcessEvents(jsonEvents); - - // Assert - Assert.Equivalent(processEvents, copiedEvents); - } - - private static void VerifyContainerSerialization(KernelProcessEvent[] processEvents) - { - // Arrange - List jsonEvents = []; - foreach (KernelProcessEvent processEvent in processEvents) - { - jsonEvents.Add(KernelProcessEventSerializer.ToJson(processEvent)); - } - - // Act - IList copiedEvents = KernelProcessEventSerializer.ToKernelProcessEvents(jsonEvents); - - // Assert - Assert.Equivalent(processEvents, copiedEvents); - } - - internal sealed class ComplexData - { - public string Id { get; init; } = string.Empty; - - public int Value { get; init; } - } -} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/Process.Runtime.Dapr.UnitTests.csproj b/dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/Process.Runtime.Dapr.UnitTests.csproj deleted file mode 100644 index 6dd3594f5ea8..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/Process.Runtime.Dapr.UnitTests.csproj +++ /dev/null @@ -1,36 +0,0 @@ - - - - SemanticKernel.Process.Runtime.Dapr.UnitTests - SemanticKernel.Process.Runtime.Dapr.UnitTests - net8.0 - - LatestMajor - true - false - 12 - - $(NoWarn);CA2007,CA1812,CA1861,CA1063,VSTHRD111,SKEXP0001,SKEXP0050,SKEXP0080,SKEXP0110;OPENAI001 - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/ProcessEventSerializationTests.cs b/dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/ProcessEventSerializationTests.cs deleted file mode 100644 index e4c127897dbd..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/ProcessEventSerializationTests.cs +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System; -using System.Collections.Generic; -using System.IO; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Process.Runtime; -using Microsoft.SemanticKernel.Process.Serialization; -using Xunit; - -namespace SemanticKernel.Process.Dapr.Runtime.UnitTests; - -/// -/// Unit tests for the class. -/// -public class ProcessEventSerializationTests -{ - /// - /// Validates that a can be serialized and deserialized correctly - /// with out an explicit type definition for - /// - [Fact] - public void VerifySerializeEventSingleTest() - { - // Arrange, Act & Assert - VerifyContainerSerialization([new() { Namespace = "testname", SourceId = "testid", Data = 3 }]); - VerifyContainerSerialization([new() { Namespace = "testname", SourceId = "testid", Data = "test" }]); - VerifyContainerSerialization([new() { Namespace = "testname", SourceId = "testid", Data = Guid.NewGuid() }]); - VerifyContainerSerialization([new() { Namespace = "testname", SourceId = "testid", Data = new int[] { 1, 2, 3, 4 } }]); - VerifyContainerSerialization([new() { Namespace = "testname", SourceId = "testid", Data = new ComplexData { Value = 3 } }]); - VerifyContainerSerialization([new() { Namespace = "testname", SourceId = "testid", Data = KernelProcessError.FromException(new InvalidOperationException()) }]); - } - - /// - /// Validates that a list can be serialized and deserialized correctly - /// with out varying types assigned to for - /// - [Fact] - public void VerifySerializeEventMixedTest() - { - // Arrange, Act & Assert - VerifyContainerSerialization( - [ - new() { Namespace = "testname", SourceId = "testid", Data = 3 }, - new() { Namespace = "testname", SourceId = "testid", Data = "test" }, - new() { Namespace = "testname", SourceId = "testid", Data = Guid.NewGuid() }, - new() { Namespace = "testname", SourceId = "testid", Data = new int[] { 1, 2, 3, 4 } }, - new() { Namespace = "testname", SourceId = "testid", Data = new ComplexData { Value = 3 } }, - new() { Namespace = "testname", SourceId = "testid", Data = KernelProcessError.FromException(new InvalidOperationException()) }, - ]); - } - - /// - /// Validates that a list can be serialized and deserialized correctly - /// with out varying types assigned to for - /// - [Fact] - public void VerifyDataContractSerializationTest() - { - // Arrange - ProcessEvent[] processEvents = - [ - new() { Namespace = "testname", SourceId = "testid", Data = 3 }, - new() { Namespace = "testname", SourceId = "testid", Data = "test" }, - new() { Namespace = "testname", SourceId = "testid", Data = Guid.NewGuid() }, - new() { Namespace = "testname", SourceId = "testid", Data = new int[] { 1, 2, 3, 4 } }, - new() { Namespace = "testname", SourceId = "testid", Data = new ComplexData { Value = 3 } }, - new() { Namespace = "testname", SourceId = "testid", Data = KernelProcessError.FromException(new InvalidOperationException()) }, - ]; - List jsonEvents = []; - foreach (ProcessEvent processEvent in processEvents) - { - jsonEvents.Add(ProcessEventSerializer.ToJson(processEvent)); - } - - // Act - using MemoryStream stream = new(); - jsonEvents.Serialize(stream); - stream.Position = 0; - - List? copy = stream.Deserialize>(); - - // Assert - Assert.NotNull(copy); - - // Act - IList copiedEvents = ProcessEventSerializer.ToProcessEvents(jsonEvents); - - // Assert - Assert.Equivalent(processEvents, copiedEvents); - } - - private static void VerifyContainerSerialization(ProcessEvent[] processEvents) - { - // Arrange - List jsonEvents = []; - foreach (ProcessEvent processEvent in processEvents) - { - jsonEvents.Add(ProcessEventSerializer.ToJson(processEvent)); - } - - // Act - IList copiedEvents = ProcessEventSerializer.ToProcessEvents(jsonEvents); - - // Assert - Assert.Equivalent(processEvents, copiedEvents); - } - - internal sealed class ComplexData - { - public string Id { get; init; } = string.Empty; - - public int Value { get; init; } - } -} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/ProcessMessageSerializationTests.cs b/dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/ProcessMessageSerializationTests.cs deleted file mode 100644 index 3ed0e0c42cfb..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/ProcessMessageSerializationTests.cs +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System; -using System.Collections.Generic; -using System.IO; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Process.Runtime; -using Microsoft.SemanticKernel.Process.Serialization; -using Xunit; - -namespace SemanticKernel.Process.Dapr.Runtime.UnitTests; - -/// -/// Unit tests for the class. -/// -public class ProcessMessageSerializationTests -{ - /// - /// Validates that a can be serialized and deserialized correctly - /// with out an explicit type definition for - /// - [Fact] - public void VerifySerializeMessageSingleTest() - { - // Arrange, Act & Assert - VerifyContainerSerialization([CreateMessage(new() { { "Data", 3 } })]); - VerifyContainerSerialization([CreateMessage(new() { { "Data", "test" } })]); - VerifyContainerSerialization([CreateMessage(new() { { "Data", Guid.NewGuid() } })]); - VerifyContainerSerialization([CreateMessage(new() { { "Data", new int[] { 1, 2, 3, 4 } } })]); - VerifyContainerSerialization([CreateMessage(new() { { "Data", new ComplexData { Value = 3 } } })]); - VerifyContainerSerialization([CreateMessage(new() { { "Data", KernelProcessError.FromException(new InvalidOperationException()) } })]); - } - - /// - /// Validates that a list can be serialized and deserialized correctly - /// with out varying types assigned to for - /// - [Fact] - public void VerifySerializeMessageMixedTest() - { - // Arrange, Act & Assert - VerifyContainerSerialization( - [ - CreateMessage(new() { { "Data", 3 } }), - CreateMessage(new() { { "Data", "test" } }), - CreateMessage(new() { { "Data", Guid.NewGuid() } }), - CreateMessage(new() { { "Data", new int[] { 1, 2, 3, 4 } } }), - CreateMessage(new() { { "Data", new ComplexData { Value = 3 } } }), - CreateMessage(new() { { "Data", KernelProcessError.FromException(new InvalidOperationException()) } }), - ]); - } - - /// - /// Validates that a list can be serialized and deserialized correctly - /// with out varying types assigned to for - /// - [Fact] - public void VerifySerializeMessageManyTest() - { - // Arrange, Act & Assert - VerifyContainerSerialization( - [ - CreateMessage(new() { - { "Data1", 3 }, - { "Data2", "test" }, - { "Data3", Guid.NewGuid() }, - { "Data4", new int[] { 1, 2, 3, 4 } }, - { "Data5", new ComplexData { Value = 3 } }, - { "Data6", KernelProcessError.FromException(new InvalidOperationException()) } }) - ]); - } - - /// - /// Validates that a list can be serialized and deserialized correctly - /// with out varying types assigned to for - /// - [Fact] - public void VerifyDataContractSerializationTest() - { - // Arrange - ProcessMessage[] processMessages = - [ - CreateMessage(new() { { "Data", 3 } }), - CreateMessage(new() { { "Data", "test" } }), - CreateMessage(new() { { "Data", Guid.NewGuid() } }), - CreateMessage(new() { { "Data", new int[] { 1, 2, 3, 4 } } }), - CreateMessage(new() { { "Data", new ComplexData { Value = 3 } } }), - CreateMessage(new() { { "Data", KernelProcessError.FromException(new InvalidOperationException()) } }), - ]; - List jsonEvents = []; - foreach (ProcessMessage processMessage in processMessages) - { - jsonEvents.Add(ProcessMessageSerializer.ToJson(processMessage)); - } - - // Act - using MemoryStream stream = new(); - jsonEvents.Serialize(stream); - stream.Position = 0; - - List? copy = stream.Deserialize>(); - - // Assert - Assert.NotNull(copy); - - // Act - IList copiedEvents = ProcessMessageSerializer.ToProcessMessages(jsonEvents); - - // Assert - Assert.Equivalent(processMessages, copiedEvents); - } - - private static void VerifyContainerSerialization(ProcessMessage[] processMessages) - { - // Arrange - List jsonEvents = []; - foreach (ProcessMessage processMessage in processMessages) - { - jsonEvents.Add(ProcessMessageSerializer.ToJson(processMessage)); - } - - // Act - IList copiedEvents = ProcessMessageSerializer.ToProcessMessages(jsonEvents); - - // Assert - Assert.Equivalent(processMessages, copiedEvents); - } - - private static ProcessMessage CreateMessage(Dictionary values) - { - return new ProcessMessage("test-source", "test-destination", "test-function", values) - { - TargetEventData = "testdata", - TargetEventId = "targetevent", - }; - } - - internal sealed class ComplexData - { - public string Id { get; init; } = string.Empty; - - public int Value { get; init; } - } -} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/TestSerializer.cs b/dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/TestSerializer.cs deleted file mode 100644 index 07864c02358e..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/TestSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System.IO; -using System.Runtime.Serialization; -using System.Text; -using System.Xml; - -namespace SemanticKernel.Process.Dapr.Runtime.UnitTests; - -internal static class TestSerializer -{ - public static void Serialize(this T obj, Stream stream) where T : class - { - DataContractSerializer serializer = new(obj.GetType()); - using XmlDictionaryWriter writer = XmlDictionaryWriter.CreateTextWriter(stream, Encoding.Default, ownsStream: false); - serializer.WriteObject(writer, obj); - writer.Flush(); - } - - public static T? Deserialize(this Stream stream) - { - DataContractSerializer serializer = new(typeof(T)); - stream.Position = 0; - return (T?)serializer.ReadObject(stream); - } -} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ActorStateKeys.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ActorStateKeys.cs deleted file mode 100644 index 8aa73d3566ac..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ActorStateKeys.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -namespace Microsoft.SemanticKernel; - -/// -/// State keys utilized by DAPR actor classes. -/// -internal static class ActorStateKeys -{ - // Shared Actor keys - public const string StepParentProcessId = "parentProcessId"; - - // StepActor keys - public const string StepInfoState = nameof(DaprStepInfo); - public const string StepStateJson = "kernelStepStateJson"; - public const string StepStateType = "kernelStepStateType"; - public const string StepIncomingMessagesState = "incomingMessagesState"; - - // MapActor keys - public const string MapInfoState = nameof(DaprMapInfo); - - // ProcessActor keys - public const string ProcessInfoState = nameof(DaprProcessInfo); - public const string EventProxyStepId = "processEventProxyId"; - public const string StepActivatedState = "kernelStepActivated"; - - // MessageBufferActor keys - public const string MessageQueueState = "DaprMessageBufferState"; - - // ExternalEventBufferActor keys - public const string ExternalEventQueueState = "DaprExternalEventBufferState"; - - // EventBufferActor keys - public const string EventQueueState = "DaprEventBufferState"; -} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/EventBufferActor.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/EventBufferActor.cs deleted file mode 100644 index 762ef31647d3..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/EventBufferActor.cs +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Threading.Tasks; -using Dapr.Actors.Runtime; -using Microsoft.SemanticKernel.Process.Runtime; - -namespace Microsoft.SemanticKernel; - -/// -/// An actor that represents an event queue. -/// -internal class EventBufferActor : Actor, IEventBuffer -{ - private List _queue = []; - - /// - /// Required constructor for Dapr Actor. - /// - /// The actor host. - public EventBufferActor(ActorHost host) : base(host) - { - } - - /// - /// Dequeues an event. - /// - /// A where T is - public async Task> DequeueAllAsync() - { - // Dequeue and clear the queue. - string[] items = [.. this._queue]; - this._queue.Clear(); - - // Save the state. - await this.StateManager.SetStateAsync(ActorStateKeys.EventQueueState, this._queue).ConfigureAwait(false); - await this.StateManager.SaveStateAsync().ConfigureAwait(false); - - return items; - } - - public async Task EnqueueAsync(string stepEvent) - { - this._queue.Add(stepEvent); - - // Save the state. - await this.StateManager.SetStateAsync(ActorStateKeys.EventQueueState, this._queue).ConfigureAwait(false); - await this.StateManager.SaveStateAsync().ConfigureAwait(false); - } - - /// - /// Called when the actor is activated. Used to initialize the state of the actor. - /// - /// A - protected override async Task OnActivateAsync() - { - var eventQueueState = await this.StateManager.TryGetStateAsync>(ActorStateKeys.EventQueueState).ConfigureAwait(false); - if (eventQueueState.HasValue) - { - this._queue = eventQueueState.Value; - } - else - { - this._queue = []; - } - } -} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ExternalEventBufferActor.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ExternalEventBufferActor.cs deleted file mode 100644 index 2411daf6b3b6..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ExternalEventBufferActor.cs +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Threading.Tasks; -using Dapr.Actors.Runtime; -using Microsoft.SemanticKernel.Process.Runtime; - -namespace Microsoft.SemanticKernel; - -/// -/// An actor that represents an external event queue. -/// -internal class ExternalEventBufferActor : Actor, IExternalEventBuffer -{ - private List _queue = []; - - /// - /// Required constructor for Dapr Actor. - /// - /// The actor host. - public ExternalEventBufferActor(ActorHost host) : base(host) - { - } - - /// - /// Dequeues an event. - /// - /// A where T is - public async Task> DequeueAllAsync() - { - // Dequeue and clear the queue. - string[] items = [.. this._queue]; - this._queue!.Clear(); - - // Save the state. - await this.StateManager.SetStateAsync(ActorStateKeys.ExternalEventQueueState, this._queue).ConfigureAwait(false); - await this.StateManager.SaveStateAsync().ConfigureAwait(false); - - return items; - } - - public async Task EnqueueAsync(string externalEvent) - { - this._queue.Add(externalEvent); - - // Save the state. - await this.StateManager.SetStateAsync(ActorStateKeys.ExternalEventQueueState, this._queue).ConfigureAwait(false); - await this.StateManager.SaveStateAsync().ConfigureAwait(false); - } - - /// - /// Called when the actor is activated. Used to initialize the state of the actor. - /// - /// A - protected override async Task OnActivateAsync() - { - var eventQueueState = await this.StateManager.TryGetStateAsync>(ActorStateKeys.ExternalEventQueueState).ConfigureAwait(false); - if (eventQueueState.HasValue) - { - this._queue = [.. eventQueueState.Value]; - } - else - { - this._queue = []; - } - } -} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ExternalMessageBufferActor.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ExternalMessageBufferActor.cs deleted file mode 100644 index d3961967beef..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ExternalMessageBufferActor.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Threading.Tasks; -using Dapr.Actors.Runtime; - -namespace Microsoft.SemanticKernel; - -/// -/// An actor that represents en external event messaging buffer. -/// -internal sealed class ExternalMessageBufferActor : Actor, IExternalMessageBuffer -{ - private readonly IExternalKernelProcessMessageChannel _externalMessageChannel; - - /// - /// Required constructor for Dapr Actor. - /// - /// The actor host. - /// Instance of - public ExternalMessageBufferActor(ActorHost host, IExternalKernelProcessMessageChannel externalMessageChannel) : base(host) - { - this._externalMessageChannel = externalMessageChannel; - } - - public async Task EmitExternalEventAsync(string externalTopicEvent, KernelProcessProxyMessage eventData) - { - await this._externalMessageChannel.EmitExternalEventAsync(externalTopicEvent, eventData).ConfigureAwait(false); - } - - protected override async Task OnDeactivateAsync() - { - await this._externalMessageChannel.Uninitialize().ConfigureAwait(false); - } - - protected override async Task OnActivateAsync() - { - await this._externalMessageChannel.Initialize().ConfigureAwait(false); - } -} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ExternalMessageBufferActorWrapper.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ExternalMessageBufferActorWrapper.cs deleted file mode 100644 index b8e8622e225a..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ExternalMessageBufferActorWrapper.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Runtime.Serialization; -using System.Threading.Tasks; - -namespace Microsoft.SemanticKernel; - -/// -/// Class used to allow using as -/// in SK Process shared abstractions -/// -[KnownType(typeof(KernelProcessProxyMessage))] -public class ExternalMessageBufferActorWrapper : IExternalKernelProcessMessageChannel -{ - private readonly IExternalMessageBuffer _actor; - - /// - /// Constructor to wrap as - /// - /// The actor host. - public ExternalMessageBufferActorWrapper(IExternalMessageBuffer actor) - { - this._actor = actor; - } - - /// - public async Task EmitExternalEventAsync(string externalTopicEvent, KernelProcessProxyMessage message) - { - await this._actor.EmitExternalEventAsync(externalTopicEvent, message).ConfigureAwait(false); - } - - /// - public ValueTask Initialize() - { - // When using Dapr initialization is already taken care of by Dapr Actors - throw new System.NotImplementedException(); - } - - /// - public ValueTask Uninitialize() - { - // When using Dapr uninitialization is already taken care of by Dapr Actors - throw new System.NotImplementedException(); - } -} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/MapActor.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/MapActor.cs deleted file mode 100644 index dedbc35f5419..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/MapActor.cs +++ /dev/null @@ -1,186 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Dapr.Actors.Runtime; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.SemanticKernel.Process.Internal; -using Microsoft.SemanticKernel.Process.Runtime; -using Microsoft.SemanticKernel.Process.Serialization; - -namespace Microsoft.SemanticKernel; - -internal sealed class MapActor : StepActor, IMap -{ - private const string DaprProcessMapStateName = nameof(DaprMapInfo); - - private bool _isInitialized; - private HashSet _mapEvents = []; - private ILogger? _logger; - private KernelProcessMap? _map; - - internal DaprMapInfo? _mapInfo; - - /// - /// Initializes a new instance of the class. - /// - /// The Dapr host actor - /// An instance of - public MapActor(ActorHost host, Kernel kernel) - : base(host, kernel) - { - } - - #region Public Actor Methods - - public async Task InitializeMapAsync(DaprMapInfo mapInfo, string? parentProcessId) - { - // Only initialize once. This check is required as the actor can be re-activated from persisted state and - // this should not result in multiple initializations. - if (this._isInitialized) - { - return; - } - - this.InitializeMapActor(mapInfo, parentProcessId); - - this._isInitialized = true; - - // Save the state - await this.StateManager.AddStateAsync(DaprProcessMapStateName, mapInfo).ConfigureAwait(false); - await this.StateManager.AddStateAsync(ActorStateKeys.StepParentProcessId, parentProcessId).ConfigureAwait(false); - await this.StateManager.SaveStateAsync().ConfigureAwait(false); - } - - /// - /// When the process is used as a step within another process, this method will be called - /// rather than ToKernelProcessAsync when extracting the state. - /// - /// A where T is - public override Task ToDaprStepInfoAsync() => Task.FromResult(this._mapInfo!); - - protected override async Task OnActivateAsync() - { - var existingMapInfo = await this.StateManager.TryGetStateAsync(DaprProcessMapStateName).ConfigureAwait(false); - if (existingMapInfo.HasValue) - { - this.ParentProcessId = await this.StateManager.GetStateAsync(ActorStateKeys.StepParentProcessId).ConfigureAwait(false); - this.InitializeMapActor(existingMapInfo.Value, this.ParentProcessId); - } - } - - /// - /// The name of the step. - /// - protected override string Name => this._mapInfo?.State.StepId ?? throw new KernelException("The Map must be initialized before accessing the Name property."); - - #endregion - - /// - /// Handles a that has been sent to the map. - /// - /// The message to map. - internal override async Task HandleMessageAsync(ProcessMessage message) - { - // Initialize the current operation - (IEnumerable inputValues, KernelProcess mapOperation, string startEventId) = this._map!.Initialize(message, this._logger); - - List mapOperations = []; - foreach (var value in inputValues) - { - KernelProcess mapProcess = mapOperation with { State = mapOperation.State with { RunId = $"{this.Name}-{mapOperations.Count}-{Guid.NewGuid():N}" } }; - DaprKernelProcessContext processContext = new(mapProcess); - Task processTask = - processContext.StartWithEventAsync( - new KernelProcessEvent - { - Id = startEventId, - Data = value - }, - eventProxyStepId: this.Id); - - mapOperations.Add(processTask); - } - - // Wait for all the map operations to complete - await Task.WhenAll(mapOperations).ConfigureAwait(false); - - // Retrieve all proxied events from the map operations - IEventBuffer proxyBuffer = this.ProxyFactory.CreateActorProxy(this.Id, nameof(EventBufferActor)); - IList proxyEvents = await proxyBuffer.DequeueAllAsync().ConfigureAwait(false); - IList processEvents = proxyEvents.ToProcessEvents(); - - // Survey the events to determine the type of the results associated with each event proxied by the map - Dictionary capturedEvents = []; - foreach (ProcessEvent processEvent in processEvents) - { - string eventName = processEvent.SourceId; - if (this._mapEvents.Contains(eventName)) - { - capturedEvents.TryGetValue(eventName, out Type? resultType); - if (resultType is null || resultType == typeof(object)) - { - capturedEvents[eventName] = processEvent.Data?.GetType() ?? typeof(object); - } - } - } - - // Correlate the operation results to emit as the map result - Dictionary resultMap = []; - Dictionary resultCounts = []; - - foreach (ProcessEvent processEvent in processEvents) - { - string eventName = processEvent.SourceId; - if (capturedEvents.TryGetValue(eventName, out Type? resultType)) - { - var eventData = processEvent.Data; - if (resultType == typeof(KernelProcessEventData) && eventData is KernelProcessEventData kernelProcessData) - { - eventData = kernelProcessData.ToObject(); - resultType = Type.GetType(kernelProcessData.ObjectType); - } - - if (!resultMap.TryGetValue(eventName, out Array? results)) - { - results = Array.CreateInstance(resultType!, mapOperations.Count); - resultMap[eventName] = results; - } - - resultCounts.TryGetValue(eventName, out int resultIndex); // resultIndex defaults to 0 when not found - results.SetValue(eventData, resultIndex); - resultCounts[eventName] = resultIndex + 1; - } - } - - // Emit map results - foreach (string eventName in capturedEvents.Keys) - { - Array eventResult = resultMap[eventName]; - await this.EmitEventAsync(new KernelProcessEvent() { Id = eventName, Data = eventResult }).ConfigureAwait(false); - } - } - - private void InitializeMapActor(DaprMapInfo mapInfo, string? parentProcessId) - { - Verify.NotNull(mapInfo); - Verify.NotNull(mapInfo.Operation); - - this._mapInfo = mapInfo; - this._map = mapInfo.ToKernelProcessMap(); - this.ParentProcessId = parentProcessId; - this._logger = this._kernel.LoggerFactory?.CreateLogger(this._mapInfo.State.StepId) ?? new NullLogger(); - this._outputEdges = this._mapInfo.Edges.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToList()); - this._eventNamespace = $"{this._mapInfo.State.StepId}_{this._mapInfo.State.RunId}"; - - // Capture the events that the map is interested in as hashtable for performant lookup - this._mapEvents = [.. this._mapInfo.Edges.Keys.Select(key => key.Split(ProcessConstants.EventIdSeparator).Last())]; - - this._isInitialized = true; - } - - private sealed record TypedResult(Type ResultType, Array Results); -} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/MessageBufferActor.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/MessageBufferActor.cs deleted file mode 100644 index 5022a9d014ac..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/MessageBufferActor.cs +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Threading.Tasks; -using Dapr.Actors.Runtime; -using Microsoft.SemanticKernel.Process.Runtime; - -namespace Microsoft.SemanticKernel; - -/// -/// An actor that represents an external event queue. -/// -internal class MessageBufferActor : Actor, IMessageBuffer -{ - private List _queue = []; - - /// - /// Required constructor for Dapr Actor. - /// - /// The actor host. - public MessageBufferActor(ActorHost host) : base(host) - { - } - - /// - /// Dequeues an event. - /// - /// A where T is - public async Task> DequeueAllAsync() - { - // Dequeue and clear the queue. - string[] items = [.. this._queue]; - this._queue.Clear(); - - // Save the state. - await this.StateManager.SetStateAsync(ActorStateKeys.MessageQueueState, this._queue).ConfigureAwait(false); - await this.StateManager.SaveStateAsync().ConfigureAwait(false); - - return items; - } - - public async Task EnqueueAsync(string message) - { - this._queue.Add(message); - - // Save the state. - await this.StateManager.SetStateAsync(ActorStateKeys.MessageQueueState, this._queue).ConfigureAwait(false); - await this.StateManager.SaveStateAsync().ConfigureAwait(false); - } - - /// - /// Called when the actor is activated. Used to initialize the state of the actor. - /// - /// A - protected override async Task OnActivateAsync() - { - var eventQueueState = await this.StateManager.TryGetStateAsync>(ActorStateKeys.MessageQueueState).ConfigureAwait(false); - if (eventQueueState.HasValue) - { - this._queue = eventQueueState.Value; - } - else - { - this._queue = []; - } - } -} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ProcessActor.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ProcessActor.cs deleted file mode 100644 index 8ac0b2f8a49a..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ProcessActor.cs +++ /dev/null @@ -1,531 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using Dapr.Actors; -using Dapr.Actors.Runtime; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.SemanticKernel.Process.Internal; -using Microsoft.SemanticKernel.Process.Runtime; -using Microsoft.SemanticKernel.Process.Serialization; -using Microsoft.VisualStudio.Threading; - -namespace Microsoft.SemanticKernel; - -internal sealed class ProcessActor : StepActor, IProcess, IDisposable -{ - private readonly JoinableTaskFactory _joinableTaskFactory; - private readonly JoinableTaskContext _joinableTaskContext; - private readonly Channel _externalEventChannel; - - internal readonly List _steps = []; - - internal IList? _stepsInfos; - internal DaprProcessInfo? _process; - private JoinableTask? _processTask; - private CancellationTokenSource? _processCancelSource; - private bool _isInitialized; - private ILogger? _logger; - - /// - /// Initializes a new instance of the class. - /// - /// The Dapr host actor - /// An instance of - public ProcessActor(ActorHost host, Kernel kernel) - : base(host, kernel) - { - this._externalEventChannel = Channel.CreateUnbounded(); - this._joinableTaskContext = new JoinableTaskContext(); - this._joinableTaskFactory = new JoinableTaskFactory(this._joinableTaskContext); - } - - #region Public Actor Methods - - public async Task InitializeProcessAsync(DaprProcessInfo processInfo, string? parentProcessId, string? eventProxyStepId = null) - { - Verify.NotNull(processInfo); - Verify.NotNull(processInfo.Steps); - - // Only initialize once. This check is required as the actor can be re-activated from persisted state and - // this should not result in multiple initializations. - if (this._isInitialized) - { - return; - } - - // Initialize the process - await this.InitializeProcessActorAsync(processInfo, parentProcessId, eventProxyStepId).ConfigureAwait(false); - - // Save the state - await this.StateManager.AddStateAsync(ActorStateKeys.ProcessInfoState, processInfo).ConfigureAwait(false); - await this.StateManager.AddStateAsync(ActorStateKeys.StepParentProcessId, parentProcessId).ConfigureAwait(false); - await this.StateManager.AddStateAsync(ActorStateKeys.StepActivatedState, true).ConfigureAwait(false); - if (!string.IsNullOrWhiteSpace(eventProxyStepId)) - { - await this.StateManager.AddStateAsync(ActorStateKeys.EventProxyStepId, eventProxyStepId).ConfigureAwait(false); - } - await this.StateManager.SaveStateAsync().ConfigureAwait(false); - } - - /// - /// Starts the process with an initial event and an optional kernel. - /// - /// Indicates if the process should wait for external events after it's finished processing. - /// - public Task StartAsync(bool keepAlive) - { - if (!this._isInitialized) - { - throw new InvalidOperationException("The process cannot be started before it has been initialized.").Log(this._logger); - } - - this._processCancelSource = new CancellationTokenSource(); - this._processTask = this._joinableTaskFactory.RunAsync(() - => this.Internal_ExecuteAsync(keepAlive: keepAlive, cancellationToken: this._processCancelSource.Token)); - - return Task.CompletedTask; - } - - /// - /// Starts the process with an initial event and then waits for the process to finish. In this case the process will not - /// keep alive waiting for external events after the internal messages have stopped. - /// - /// Required. The to start the process with. - /// A - public async Task RunOnceAsync(string processEvent) - { - Verify.NotNull(processEvent, nameof(processEvent)); - IExternalEventBuffer externalEventQueue = this.ProxyFactory.CreateActorProxy(new ActorId(this.Id.GetId()), nameof(ExternalEventBufferActor)); - await externalEventQueue.EnqueueAsync(processEvent).ConfigureAwait(false); - await this.StartAsync(keepAlive: false).ConfigureAwait(false); - await this._processTask!.JoinAsync().ConfigureAwait(false); - } - - /// - /// Stops a running process. This will cancel the process and wait for it to complete before returning. - /// - /// A - public async Task StopAsync() - { - if (this._processTask is null || this._processCancelSource is null || this._processTask.IsCompleted) - { - return; - } - - // Cancel the process and wait for it to complete. - this._processCancelSource.Cancel(); - - try - { - await this._processTask; - } - catch (OperationCanceledException) - { - // The task was cancelled, so we can ignore this exception. - } - finally - { - this._processCancelSource.Dispose(); - } - } - - /// - /// Sends a message to the process. This does not start the process if it's not already running, in - /// this case the message will remain queued until the process is started. - /// - /// Required. The to start the process with. - /// A - public async Task SendMessageAsync(string processEvent) - { - Verify.NotNull(processEvent, nameof(processEvent)); - await this._externalEventChannel.Writer.WriteAsync(processEvent.ToKernelProcessEvent()).ConfigureAwait(false); - } - - /// - /// Gets the process information. - /// - /// An instance of - public async Task GetProcessInfoAsync() - { - return await this.ToDaprProcessInfoAsync().ConfigureAwait(false); - } - - /// - /// When the process is used as a step within another process, this method will be called - /// rather than ToKernelProcessAsync when extracting the state. - /// - /// A - public override async Task ToDaprStepInfoAsync() - { - return await this.ToDaprProcessInfoAsync().ConfigureAwait(false); - } - - protected override async Task OnActivateAsync() - { - var existingProcessInfo = await this.StateManager.TryGetStateAsync(ActorStateKeys.ProcessInfoState).ConfigureAwait(false); - if (existingProcessInfo.HasValue) - { - this.ParentProcessId = await this.StateManager.GetStateAsync(ActorStateKeys.StepParentProcessId).ConfigureAwait(false); - string? eventProxyStepId = null; - if (await this.StateManager.ContainsStateAsync(ActorStateKeys.EventProxyStepId).ConfigureAwait(false)) - { - eventProxyStepId = await this.StateManager.GetStateAsync(ActorStateKeys.EventProxyStepId).ConfigureAwait(false); - } - await this.InitializeProcessActorAsync(existingProcessInfo.Value, this.ParentProcessId, eventProxyStepId).ConfigureAwait(false); - } - } - - /// - /// The name of the step. - /// - protected override string Name => this._process?.State.StepId ?? throw new KernelException("The Process must be initialized before accessing the Name property.").Log(this._logger); - - #endregion - - /// - /// Handles a that has been sent to the process. This happens only in the case - /// of a process (this one) running as a step within another process (this one's parent). In this case the - /// entire sub-process should be executed within a single superstep. - /// - /// The message to process. - internal override async Task HandleMessageAsync(ProcessMessage message) - { - if (string.IsNullOrWhiteSpace(message.TargetEventId)) - { - throw new KernelException("Internal Process Error: The target event id must be specified when sending a message to a step.").Log(this._logger); - } - - string eventId = message.TargetEventId!; - if (this._outputEdges!.TryGetValue(eventId, out List? edges) && edges is not null) - { - foreach (var edge in edges) - { - // Create the external event that will be used to start the nested process. Since this event came - // from outside this processes, we set the visibility to internal so that it's not emitted back out again. - KernelProcessEvent nestedEvent = new() { Id = eventId, Data = message.TargetEventData }; - - // Run the nested process completely within a single superstep. - await this.RunOnceAsync(nestedEvent.ToJson()).ConfigureAwait(false); - } - } - } - - internal static ActorId GetScopedGlobalErrorEventBufferId(string processId) => new($"{ProcessConstants.GlobalErrorEventId}_{processId}"); - - #region Private Methods - - /// - /// Initializes this process as a step within another process. - /// - protected override ValueTask ActivateStepAsync() - { - // The process does not need any further initialization as it's already been initialized. - // Override the base method to prevent it from being called. - return default; - } - - private async Task InitializeProcessActorAsync(DaprProcessInfo processInfo, string? parentProcessId, string? eventProxyStepId) - { - Verify.NotNull(processInfo, nameof(processInfo)); - Verify.NotNull(processInfo.Steps); - - this.ParentProcessId = parentProcessId; - this._process = processInfo; - this._stepsInfos = [.. this._process.Steps]; - this._logger = this._kernel.LoggerFactory?.CreateLogger(this._process.State.StepId) ?? new NullLogger(); - if (!string.IsNullOrWhiteSpace(eventProxyStepId)) - { - this.EventProxyStepId = new ActorId(eventProxyStepId); - } - - // Initialize the input and output edges for the process - this._outputEdges = this._process.Edges.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToList()); - - // Initialize the steps within this process - foreach (var step in this._stepsInfos) - { - IStep? stepActor = null; - - // The current step should already have a name. - Verify.NotNull(step.State?.StepId); - - if (step is DaprProcessInfo processStep) - { - // The process will only have an Id if its already been executed. - if (string.IsNullOrWhiteSpace(processStep.State.RunId)) - { - processStep = processStep with { State = processStep.State with { RunId = Guid.NewGuid().ToString() } }; - } - - // Initialize the step as a process. - var scopedProcessId = this.ScopedActorId(new ActorId(processStep.State.RunId!)); - var processActor = this.ProxyFactory.CreateActorProxy(scopedProcessId, nameof(ProcessActor)); - await processActor.InitializeProcessAsync(processStep, this.Id.GetId(), eventProxyStepId).ConfigureAwait(false); - stepActor = this.ProxyFactory.CreateActorProxy(scopedProcessId, nameof(ProcessActor)); - } - else if (step is DaprMapInfo mapStep) - { - // Initialize the step as a map. - ActorId scopedMapId = this.ScopedActorId(new ActorId(mapStep.State.RunId!)); - IMap mapActor = this.ProxyFactory.CreateActorProxy(scopedMapId, nameof(MapActor)); - await mapActor.InitializeMapAsync(mapStep, this.Id.GetId()).ConfigureAwait(false); - stepActor = this.ProxyFactory.CreateActorProxy(scopedMapId, nameof(MapActor)); - } - else if (step is DaprProxyInfo proxyStep) - { - // Initialize the step as a proxy - ActorId scopedProxyId = this.ScopedActorId(new ActorId(proxyStep.State.RunId!)); - IProxy proxyActor = this.ProxyFactory.CreateActorProxy(scopedProxyId, nameof(ProxyActor)); - await proxyActor.InitializeProxyAsync(proxyStep, this.Id.GetId()).ConfigureAwait(false); - stepActor = this.ProxyFactory.CreateActorProxy(scopedProxyId, nameof(ProxyActor)); - } - else - { - // The current step should already have an Id. - Verify.NotNull(step.State?.RunId); - - var scopedStepId = this.ScopedActorId(new ActorId(step.State.RunId!)); - stepActor = this.ProxyFactory.CreateActorProxy(scopedStepId, nameof(StepActor)); - await stepActor.InitializeStepAsync(step, this.Id.GetId(), eventProxyStepId).ConfigureAwait(false); - } - - this._steps.Add(stepActor); - } - - this._isInitialized = true; - } - - private async Task Internal_ExecuteAsync(int maxSupersteps = 100, bool keepAlive = true, CancellationToken cancellationToken = default) - { - try - { - // Run the Pregel algorithm until there are no more messages being sent. - for (int superstep = 0; superstep < maxSupersteps; superstep++) - { - // Check for EndStep messages. If there are any then cancel the process. - if (await this.IsEndMessageSentAsync().ConfigureAwait(false)) - { - this._processCancelSource?.Cancel(); - break; - } - - // Translate any global error events into an message that targets the appropriate step, when one exists. - await this.HandleGlobalErrorMessageAsync().ConfigureAwait(false); - - // Check for external events - await this.EnqueueExternalMessagesAsync().ConfigureAwait(false); - - // Reach out to all of the steps in the process and instruct them to retrieve their pending messages from their associated queues. - var stepPreparationTasks = this._steps.Select(step => step.PrepareIncomingMessagesAsync()).ToArray(); - var messageCounts = await Task.WhenAll(stepPreparationTasks).ConfigureAwait(false); - - // If there are no messages to process, wait for an external event or finish. - if (messageCounts.Sum() == 0) - { - if (!keepAlive || !await this._externalEventChannel.Reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false)) - { - this._processCancelSource?.Cancel(); - break; - } - } - - // Process the incoming messages for each step. - var stepProcessingTasks = this._steps.Select(step => step.ProcessIncomingMessagesAsync()).ToArray(); - await Task.WhenAll(stepProcessingTasks).ConfigureAwait(false); - - // Handle public events that need to be bubbled out of the process. - await this.SendOutgoingPublicEventsAsync().ConfigureAwait(false); - } - } - catch (Exception ex) - { - this._logger?.LogError(ex, "An error occurred while running the process: {ErrorMessage}.", ex.Message); - throw; - } - finally - { - if (this._processCancelSource?.IsCancellationRequested ?? false) - { - this._processCancelSource.Cancel(); - } - - this._processCancelSource?.Dispose(); - } - - return; - } - - /// - /// Processes external events that have been sent to the process, translates them to s, and enqueues - /// them to the provided message channel so that they can be processed in the next superstep. - /// - private async Task EnqueueExternalMessagesAsync() - { - IExternalEventBuffer externalEventQueue = this.ProxyFactory.CreateActorProxy(new ActorId(this.Id.GetId()), nameof(ExternalEventBufferActor)); - IList dequeuedEvents = await externalEventQueue.DequeueAllAsync().ConfigureAwait(false); - IList externalEvents = dequeuedEvents.ToKernelProcessEvents(); - - foreach (KernelProcessEvent externalEvent in externalEvents) - { - if (this._outputEdges!.TryGetValue(externalEvent.Id!, out List? edges) && edges is not null) - { - foreach (KernelProcessEdge edge in edges) - { - if (edge.OutputTarget is not KernelProcessFunctionTarget functionTarget) - { - throw new KernelException("The target for the edge is not a function target.").Log(this._logger); - } - - ProcessMessage message = ProcessMessageFactory.CreateFromEdge(edge, externalEvent.Id, externalEvent.Data); - var scopedMessageBufferId = this.ScopedActorId(new ActorId(functionTarget.StepId)); - var messageQueue = this.ProxyFactory.CreateActorProxy(scopedMessageBufferId, nameof(MessageBufferActor)); - await messageQueue.EnqueueAsync(message.ToJson()).ConfigureAwait(false); - } - } - } - } - - /// - /// Check for the presence of an global-error event and any edges defined for processing it. - /// When both exist, the error event is processed and sent to the appropriate targets. - /// - private async Task HandleGlobalErrorMessageAsync() - { - var errorEventQueue = this.ProxyFactory.CreateActorProxy(ProcessActor.GetScopedGlobalErrorEventBufferId(this.Id.GetId()), nameof(EventBufferActor)); - - IList errorEvents = await errorEventQueue.DequeueAllAsync().ConfigureAwait(false); - if (errorEvents.Count == 0) - { - // No error events in queue. - return; - } - - var errorEdges = this.GetEdgeForEvent(ProcessConstants.GlobalErrorEventId).ToArray(); - if (errorEdges.Length == 0) - { - // No further action is required when there are no targetes defined for processing the error. - return; - } - - IList processErrorEvents = errorEvents.ToProcessEvents(); - foreach (var errorEdge in errorEdges) - { - foreach (ProcessEvent errorEvent in processErrorEvents) - { - if (errorEdge.OutputTarget is not KernelProcessFunctionTarget functionTarget) - { - throw new KernelException("The target for the edge is not a function target.").Log(this._logger); - } - var errorMessage = ProcessMessageFactory.CreateFromEdge(errorEdge, errorEvent.SourceId, errorEvent.Data); - var scopedErrorMessageBufferId = this.ScopedActorId(new ActorId(functionTarget.StepId)); - var errorStepQueue = this.ProxyFactory.CreateActorProxy(scopedErrorMessageBufferId, nameof(MessageBufferActor)); - await errorStepQueue.EnqueueAsync(errorMessage.ToJson()).ConfigureAwait(false); - } - } - } - - /// - /// Public events that are produced inside of this process need to be sent to the parent process. This method reads - /// all of the public events from the event buffer and sends them to the targeted step in the parent process. - /// - private async Task SendOutgoingPublicEventsAsync() - { - // Loop through all steps that are processes and call a function requesting their outgoing events, then queue them up. - if (!string.IsNullOrWhiteSpace(this.ParentProcessId)) - { - // Handle public events that need to be bubbled out of the process. - IEventBuffer eventQueue = this.ProxyFactory.CreateActorProxy(new ActorId(this.Id.GetId()), nameof(EventBufferActor)); - IList allEvents = await eventQueue.DequeueAllAsync().ConfigureAwait(false); - IList processEvents = allEvents.ToProcessEvents(); - - foreach (ProcessEvent processEvent in processEvents) - { - ProcessEvent scopedEvent = this.ScopedEvent(processEvent); - if (this._outputEdges!.TryGetValue(scopedEvent.QualifiedId, out List? edges) && edges is not null) - { - foreach (var edge in edges) - { - if (edge.OutputTarget is not KernelProcessFunctionTarget functionTarget) - { - throw new KernelException("The target for the edge is not a function target.").Log(this._logger); - } - - ProcessMessage message = ProcessMessageFactory.CreateFromEdge(edge, scopedEvent.SourceId, scopedEvent.Data); - var scopedMessageBufferId = this.ScopedActorId(new ActorId(functionTarget.StepId), scopeToParent: true); - var messageQueue = this.ProxyFactory.CreateActorProxy(scopedMessageBufferId, nameof(MessageBufferActor)); - await messageQueue.EnqueueAsync(message.ToJson()).ConfigureAwait(false); - } - } - } - } - } - - /// - /// Determines is the end message has been sent to the process. - /// - /// True if the end message has been sent, otherwise false. - private async Task IsEndMessageSentAsync() - { - var scopedMessageBufferId = this.ScopedActorId(new ActorId(ProcessConstants.EndStepName)); - var endMessageQueue = this.ProxyFactory.CreateActorProxy(scopedMessageBufferId, nameof(MessageBufferActor)); - var messages = await endMessageQueue.DequeueAllAsync().ConfigureAwait(false); - return messages.Count > 0; - } - - /// - /// Builds a from the current . - /// - /// An instance of - /// - private async Task ToDaprProcessInfoAsync() - { - var processState = new KernelProcessState(this.Name, this._process!.State.Version, this.Id.GetId()); - var stepTasks = this._steps.Select(step => step.ToDaprStepInfoAsync()).ToList(); - var steps = await Task.WhenAll(stepTasks).ConfigureAwait(false); - return new DaprProcessInfo { InnerStepDotnetType = this._process!.InnerStepDotnetType, Edges = this._process!.Edges, State = processState, Steps = [.. steps] }; - } - - /// - /// Scopes the Id of a step within the process to the process. - /// - /// The actor Id to scope. - /// Indicates if the Id should be scoped to the parent process. - /// A new which is scoped to the process. - private ActorId ScopedActorId(ActorId actorId, bool scopeToParent = false) - { - if (scopeToParent && string.IsNullOrWhiteSpace(this.ParentProcessId)) - { - throw new InvalidOperationException("The parent process Id must be set before scoping to the parent process."); - } - - string id = scopeToParent ? this.ParentProcessId! : this.Id.GetId(); - return new ActorId($"{id}.{actorId.GetId()}"); - } - - /// - /// Generates a scoped event for the step. - /// - /// The event. - /// A with the correctly scoped namespace. - private ProcessEvent ScopedEvent(ProcessEvent daprEvent) - { - Verify.NotNull(daprEvent); - return daprEvent with { Namespace = $"{this.Name}_{this._process!.State.RunId}" }; - } - - #endregion - - public void Dispose() - { - this._externalEventChannel.Writer.Complete(); - this._joinableTaskContext.Dispose(); - this._joinableTaskContext.Dispose(); - this._processCancelSource?.Dispose(); - } -} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ProxyActor.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ProxyActor.cs deleted file mode 100644 index 5d64b0fbdd6f..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ProxyActor.cs +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Dapr.Actors; -using Dapr.Actors.Runtime; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.SemanticKernel.Process; -using Microsoft.SemanticKernel.Process.Internal; -using Microsoft.SemanticKernel.Process.Runtime; - -namespace Microsoft.SemanticKernel; - -internal sealed class ProxyActor : StepActor, IProxy -{ - private readonly ILogger? _logger; - - internal DaprProxyInfo? _daprProxyInfo; - - /// - /// Initializes a new instance of the class. - /// - /// The Dapr host actor - /// An instance of - public ProxyActor(ActorHost host, Kernel kernel) - : base(host, kernel) - { - this._logger = this._kernel.LoggerFactory?.CreateLogger(typeof(KernelProxyStep)) ?? new NullLogger(); - } - - internal override void AssignStepFunctionParameterValues(ProcessMessage message) - { - if (this._functions is null || this._inputs is null || this._initialInputs is null) - { - throw new KernelException("The step has not been initialized.").Log(this._logger); - } - - if (message.Values.Count != 1) - { - throw new KernelException("The proxy step can only handle 1 parameter object").Log(this._logger); - } - - // Add the message values to the inputs for the function - var kvp = message.Values.Single(); - if (this._inputs.TryGetValue(message.FunctionName, out Dictionary? functionName) && functionName != null && functionName.TryGetValue(kvp.Key, out object? parameterName) && parameterName != null) - { - this._logger?.LogWarning("Step {StepName} already has input for {FunctionName}.{Key}, it is being overwritten with a message from Step named '{SourceId}'.", this.Name, message.FunctionName, kvp.Key, message.SourceId); - } - - if (!this._inputs.TryGetValue(message.FunctionName, out Dictionary? functionParameters)) - { - this._inputs[message.FunctionName] = []; - functionParameters = this._inputs[message.FunctionName]; - } - - if (this._daprProxyInfo?.ProxyMetadata != null && message.SourceEventId != null && this._daprProxyInfo.ProxyMetadata.EventMetadata.TryGetValue(message.SourceEventId, out var metadata) && metadata != null) - { - functionParameters![kvp.Key] = KernelProcessProxyMessageFactory.CreateProxyMessage(this.ParentProcessId!, message.SourceEventId, metadata.TopicName, kvp.Value); - } - } - - internal override Dictionary?> GenerateInitialInputs() - { - // Creating external process channel actor to be used for only by proxy step actor - IExternalKernelProcessMessageChannel? externalMessageChannelActor = null; - var scopedExternalMessageBufferId = this.ScopedActorId(new ActorId(this.Id.GetId())); - IExternalMessageBuffer actor = this.ProxyFactory.CreateActorProxy(scopedExternalMessageBufferId, nameof(ExternalMessageBufferActor)); - externalMessageChannelActor = new ExternalMessageBufferActorWrapper(actor); - - return this.FindInputChannels(this._functions, this._logger, externalMessageChannelActor); - } - - public async Task InitializeProxyAsync(DaprProxyInfo proxyInfo, string? parentProcessId) - { - this._daprProxyInfo = proxyInfo; - - await base.InitializeStepAsync(proxyInfo, parentProcessId).ConfigureAwait(false); - } -} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/StepActor.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/StepActor.cs deleted file mode 100644 index 8fc9bbd1ecfc..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/StepActor.cs +++ /dev/null @@ -1,497 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text.Json; -using System.Threading.Tasks; -using Dapr.Actors; -using Dapr.Actors.Runtime; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.SemanticKernel.Process.Internal; -using Microsoft.SemanticKernel.Process.Runtime; -using Microsoft.SemanticKernel.Process.Serialization; - -namespace Microsoft.SemanticKernel; - -internal class StepActor : Actor, IStep, IKernelProcessMessageChannel -{ - private readonly Lazy _activateTask; - - private DaprStepInfo? _stepInfo; - private ILogger? _logger; - private Type? _innerStepType; - - private bool _isInitialized; - - protected readonly Kernel _kernel; - protected string? _eventNamespace; - - internal Queue _incomingMessages = new(); - internal KernelProcessStepState? _stepState; - internal Type? _stepStateType; - internal Dictionary>? _outputEdges; - internal readonly Dictionary _functions = []; - internal Dictionary?>? _inputs = []; - internal Dictionary?>? _initialInputs = []; - - internal string? ParentProcessId; - internal ActorId? EventProxyStepId; - - /// - /// Represents a step in a process that is running in-process. - /// - /// The host. - /// Required. An instance of . - public StepActor(ActorHost host, Kernel kernel) - : base(host) - { - this._kernel = kernel; - this._activateTask = new Lazy(this.ActivateStepAsync); - } - - #region Public Actor Methods - - /// - /// Initializes the step with the provided step information. - /// - /// The instance describing the step. - /// The Id of the parent process if one exists. - /// An optional identifier of an actor requesting to proxy events. - /// A - public async Task InitializeStepAsync(DaprStepInfo stepInfo, string? parentProcessId, string? eventProxyStepId = null) - { - Verify.NotNull(stepInfo, nameof(stepInfo)); - - // Only initialize once. This check is required as the actor can be re-activated from persisted state and - // this should not result in multiple initializations. - if (this._isInitialized) - { - return; - } - - this.InitializeStep(stepInfo, parentProcessId, eventProxyStepId); - - // Save initial state - await this.StateManager.AddStateAsync(ActorStateKeys.StepInfoState, stepInfo).ConfigureAwait(false); - await this.StateManager.AddStateAsync(ActorStateKeys.StepParentProcessId, parentProcessId).ConfigureAwait(false); - if (!string.IsNullOrWhiteSpace(eventProxyStepId)) - { - await this.StateManager.AddStateAsync(ActorStateKeys.EventProxyStepId, eventProxyStepId).ConfigureAwait(false); - } - await this.StateManager.SaveStateAsync().ConfigureAwait(false); - } - - /// - /// Initializes the step with the provided step information. - /// - /// The instance describing the step. - /// The Id of the parent process if one exists. - /// An optional identifier of an actor requesting to proxy events. - private void InitializeStep(DaprStepInfo stepInfo, string? parentProcessId, string? eventProxyStepId = null) - { - Verify.NotNull(stepInfo, nameof(stepInfo)); - - // Attempt to load the inner step type - this._innerStepType = Type.GetType(stepInfo.InnerStepDotnetType); - if (this._innerStepType is null) - { - throw new KernelException($"Could not load the inner step type '{stepInfo.InnerStepDotnetType}'.").Log(this._logger); - } - - this.ParentProcessId = parentProcessId; - this._stepInfo = stepInfo; - this._stepState = this._stepInfo.State; - this._logger = this._kernel.LoggerFactory?.CreateLogger(this._innerStepType) ?? new NullLogger(); - this._outputEdges = this._stepInfo.Edges.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToList()); - this._eventNamespace = $"{this._stepInfo.State.StepId}_{this._stepInfo.State.RunId}"; - - if (!string.IsNullOrWhiteSpace(eventProxyStepId)) - { - this.EventProxyStepId = new ActorId(eventProxyStepId); - } - - this._isInitialized = true; - } - - /// - /// Triggers the step to dequeue all pending messages and prepare for processing. - /// - /// A where T is an indicating the number of messages that are prepared for processing. - public async Task PrepareIncomingMessagesAsync() - { - IMessageBuffer messageQueue = this.ProxyFactory.CreateActorProxy(new ActorId(this.Id.GetId()), nameof(MessageBufferActor)); - IList incoming = await messageQueue.DequeueAllAsync().ConfigureAwait(false); - IList messages = incoming.ToProcessMessages(); - - foreach (ProcessMessage message in messages) - { - this._incomingMessages.Enqueue(message); - } - - // Save the incoming messages to state - await this.StateManager.SetStateAsync(ActorStateKeys.StepIncomingMessagesState, this._incomingMessages).ConfigureAwait(false); - await this.StateManager.SaveStateAsync().ConfigureAwait(false); - - return this._incomingMessages.Count; - } - - /// - /// Triggers the step to process all prepared messages. - /// - /// A - public async Task ProcessIncomingMessagesAsync() - { - // Handle all the incoming messages one at a time - while (this._incomingMessages.Count > 0) - { - var message = this._incomingMessages.Dequeue(); - await this.HandleMessageAsync(message).ConfigureAwait(false); - - // Save the incoming messages to state - await this.StateManager.SetStateAsync(ActorStateKeys.StepIncomingMessagesState, this._incomingMessages).ConfigureAwait(false); - await this.StateManager.SaveStateAsync().ConfigureAwait(false); - } - } - - /// - /// Extracts the current state of the step and returns it as a . - /// - /// An instance of - public virtual async Task ToDaprStepInfoAsync() - { - // Lazy one-time initialization of the step before extracting state information. - // This allows state information to be extracted even if the step has not been activated. - await this._activateTask.Value.ConfigureAwait(false); - - var stepInfo = new DaprStepInfo { InnerStepDotnetType = this._stepInfo!.InnerStepDotnetType!, State = this._stepInfo.State, Edges = this._stepInfo.Edges! }; - return stepInfo; - } - - /// - /// Overrides the base method to initialize the step from persisted state. - /// - /// A - protected override async Task OnActivateAsync() - { - var existingStepInfo = await this.StateManager.TryGetStateAsync(ActorStateKeys.StepInfoState).ConfigureAwait(false); - if (existingStepInfo.HasValue) - { - // Initialize the step from persisted state - string? parentProcessId = await this.StateManager.GetStateAsync(ActorStateKeys.StepParentProcessId).ConfigureAwait(false); - string? eventProxyStepId = null; - if (await this.StateManager.ContainsStateAsync(ActorStateKeys.EventProxyStepId).ConfigureAwait(false)) - { - eventProxyStepId = await this.StateManager.GetStateAsync(ActorStateKeys.EventProxyStepId).ConfigureAwait(false); - } - this.InitializeStep(existingStepInfo.Value, parentProcessId, eventProxyStepId); - - // Load the persisted incoming messages - var incomingMessages = await this.StateManager.TryGetStateAsync>(ActorStateKeys.StepIncomingMessagesState).ConfigureAwait(false); - if (incomingMessages.HasValue) - { - this._incomingMessages = incomingMessages.Value; - } - } - } - - #endregion - - /// - /// The name of the step. - /// - protected virtual string Name => this._stepInfo?.State.StepId ?? throw new KernelException("The Step must be initialized before accessing the Name property.").Log(this._logger); - - /// - /// Emits an event from the step. - /// - /// The event to emit. - /// A - public ValueTask EmitEventAsync(KernelProcessEvent processEvent) => this.EmitEventAsync(ProcessEvent.Create(processEvent, this._eventNamespace!)); - - // TODO: this can be moved to shared runtime code, looks almost/same to localRuntime implementation - internal virtual void AssignStepFunctionParameterValues(ProcessMessage message) - { - if (this._functions is null || this._inputs is null || this._initialInputs is null) - { - throw new KernelException("The step has not been initialized.").Log(this._logger); - } - - // Add the message values to the inputs for the function - foreach (var kvp in message.Values) - { - if (this._inputs.TryGetValue(message.FunctionName, out Dictionary? functionName) && functionName != null && functionName.TryGetValue(kvp.Key, out object? parameterName) && parameterName != null) - { - this._logger?.LogWarning("Step {StepName} already has input for {FunctionName}.{Key}, it is being overwritten with a message from Step named '{SourceId}'.", this.Name, message.FunctionName, kvp.Key, message.SourceId); - } - - if (!this._inputs.TryGetValue(message.FunctionName, out Dictionary? functionParameters)) - { - this._inputs[message.FunctionName] = []; - functionParameters = this._inputs[message.FunctionName]; - } - - if (kvp.Value is KernelProcessEventData proxyData) - { - functionParameters![kvp.Key] = proxyData.ToObject(); - } - else - { - functionParameters![kvp.Key] = kvp.Value; - } - } - } - - /// - /// Handles a that has been sent to the step. - /// - /// The message to process. - /// A - /// - internal virtual async Task HandleMessageAsync(ProcessMessage message) - { - Verify.NotNull(message, nameof(message)); - - // Lazy one-time initialization of the step before processing a message - await this._activateTask.Value.ConfigureAwait(false); - - if (this._functions is null || this._inputs is null || this._initialInputs is null) - { - throw new KernelException("The step has not been initialized.").Log(this._logger); - } - - string messageLogParameters = string.Join(", ", message.Values.Select(kvp => $"{kvp.Key}: {kvp.Value}")); - this._logger?.LogDebug("Received message from '{SourceId}' targeting function '{FunctionName}' and parameters '{Parameters}'.", message.SourceId, message.FunctionName, messageLogParameters); - - // Add the message values to the inputs for the function - this.AssignStepFunctionParameterValues(message); - - // If we're still waiting for inputs on all of our functions then don't do anything. - List invocableFunctions = this._inputs.Where(i => i.Value != null && i.Value.All(v => v.Value != null)).Select(i => i.Key).ToList(); - var missingKeys = this._inputs.Where(i => i.Value is null || i.Value.Any(v => v.Value is null)); - - if (invocableFunctions.Count == 0) - { - string missingKeysLog() => string.Join(", ", missingKeys.Select(k => $"{k.Key}: {string.Join(", ", k.Value?.Where(v => v.Value == null).Select(v => v.Key) ?? [])}")); - this._logger?.LogInformation("No invocable functions, missing keys: {MissingKeys}", missingKeysLog()); - return; - } - - // A message can only target one function and should not result in a different function being invoked. - var targetFunction = invocableFunctions.FirstOrDefault((name) => name == message.FunctionName) ?? - throw new InvalidOperationException($"A message targeting function '{message.FunctionName}' has resulted in a function named '{invocableFunctions.First()}' becoming invocable. Are the function names configured correctly?").Log(this._logger); - - this._logger?.LogInformation("Step with Id `{StepId}` received all required input for function [{TargetFunction}] and is executing.", this.Name, targetFunction); - - // Concat all the inputs and run the function - KernelArguments arguments = new(this._inputs[targetFunction]!); - if (!this._functions.TryGetValue(targetFunction, out KernelFunction? function) || function == null) - { - throw new InvalidOperationException($"Function {targetFunction} not found in plugin {this.Name}").Log(this._logger); - } - - // Invoke the function, catching all exceptions that it may throw, and then post the appropriate event. -#pragma warning disable CA1031 // Do not catch general exception types - try - { - this?._logger?.LogInformation("Invoking function {FunctionName} with arguments {Arguments}", targetFunction, arguments); - FunctionResult invokeResult = await this.InvokeFunction(function, this._kernel, arguments).ConfigureAwait(false); - - this?.Logger?.LogInformation("Function {FunctionName} returned {Result}", targetFunction, invokeResult); - - // Persist the state after the function has been executed - var stateJson = JsonSerializer.Serialize(this._stepState, this._stepStateType!); - await this.StateManager.SetStateAsync(ActorStateKeys.StepStateJson, stateJson).ConfigureAwait(false); - await this.StateManager.SaveStateAsync().ConfigureAwait(false); - - await this.EmitEventAsync( - ProcessEvent.Create( - invokeResult.GetValue(), - this._eventNamespace!, - sourceId: $"{targetFunction}.OnResult", - eventVisibility: KernelProcessEventVisibility.Public)).ConfigureAwait(false); - } - catch (Exception ex) - { - this._logger?.LogError(ex, "Error in Step {StepName}: {ErrorMessage}", this.Name, ex.Message); - await this.EmitEventAsync( - ProcessEvent.Create( - KernelProcessError.FromException(ex), - this._eventNamespace!, - sourceId: $"{targetFunction}.OnError", - eventVisibility: KernelProcessEventVisibility.Public, - isError: true)).ConfigureAwait(false); - } - finally - { - // Reset the inputs for the function that was just executed - this._inputs[targetFunction] = new(this._initialInputs[targetFunction] ?? []); - } -#pragma warning restore CA1031 // Do not catch general exception types - } - - internal virtual Dictionary?> GenerateInitialInputs() - { - return this.FindInputChannels(this._functions, this._logger); - } - - /// - /// Initializes the step with the provided step information. - /// - /// A - /// - protected virtual async ValueTask ActivateStepAsync() - { - if (this._stepInfo is null) - { - throw new KernelException("A step cannot be activated before it has been initialized.").Log(this._logger); - } - - // Instantiate an instance of the inner step object - KernelProcessStep stepInstance = (KernelProcessStep)ActivatorUtilities.CreateInstance(this._kernel.Services, this._innerStepType!); - var kernelPlugin = KernelPluginFactory.CreateFromObject(stepInstance, pluginName: this._stepInfo.State.StepId); - - // Load the kernel functions - foreach (KernelFunction f in kernelPlugin) - { - this._functions.Add(f.Name, f); - } - - // Initialize the input channels - this._initialInputs = this.GenerateInitialInputs(); - this._inputs = this._initialInputs.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)); - - // Activate the step with user-defined state if needed - KernelProcessStepState? stateObject = null; - Type? stateType = null; - - // Check if the state has already been persisted - var stepStateType = await this.StateManager.TryGetStateAsync(ActorStateKeys.StepStateType).ConfigureAwait(false); - if (stepStateType.HasValue) - { - stateType = Type.GetType(stepStateType.Value); - var stateObjectJson = await this.StateManager.GetStateAsync(ActorStateKeys.StepStateJson).ConfigureAwait(false); - stateObject = JsonSerializer.Deserialize(stateObjectJson, stateType!) as KernelProcessStepState; - } - else - { - stateType = this._innerStepType.ExtractStateType(out Type? userStateType, this._logger); - stateObject = this._stepInfo.State; - - // Persist the state type and type object. - await this.StateManager.AddStateAsync(ActorStateKeys.StepStateType, stateType.AssemblyQualifiedName).ConfigureAwait(false); - await this.StateManager.AddStateAsync(ActorStateKeys.StepStateJson, JsonSerializer.Serialize(stateObject)).ConfigureAwait(false); - await this.StateManager.SaveStateAsync().ConfigureAwait(false); - } - - if (stateType is null || stateObject is null) - { - throw new KernelException("The state object for the KernelProcessStep could not be created.").Log(this._logger); - } - - MethodInfo? methodInfo = - this._innerStepType!.GetMethod(nameof(KernelProcessStep.ActivateAsync), [stateType]) ?? - throw new KernelException("The ActivateAsync method for the KernelProcessStep could not be found.").Log(this._logger); - - this._stepState = stateObject; - this._stepStateType = stateType; - - ValueTask activateTask = - (ValueTask?)methodInfo.Invoke(stepInstance, [stateObject]) ?? - throw new KernelException("The ActivateAsync method failed to complete.").Log(this._logger); - - await stepInstance.ActivateAsync(stateObject).ConfigureAwait(false); - await activateTask.ConfigureAwait(false); - } - - /// - /// Invokes the provides function with the provided kernel and arguments. - /// - /// The function to invoke. - /// The kernel to use for invocation. - /// The arguments to invoke with. - /// A containing the result of the function invocation. - private Task InvokeFunction(KernelFunction function, Kernel kernel, KernelArguments arguments) - { - return kernel.InvokeAsync(function, arguments: arguments); - } - - /// - /// Emits an event from the step. - /// - /// The event to emit. - internal async ValueTask EmitEventAsync(ProcessEvent daprEvent) - { - // Emit the event out of the process (this one) if it's visibility is public. - if (daprEvent.Visibility == KernelProcessEventVisibility.Public) - { - if (this.ParentProcessId is not null) - { - // Emit the event to the parent process - IEventBuffer parentProcess = this.ProxyFactory.CreateActorProxy(new ActorId(this.ParentProcessId), nameof(EventBufferActor)); - await parentProcess.EnqueueAsync(daprEvent.ToJson()).ConfigureAwait(false); - } - } - - if (this.EventProxyStepId != null) - { - IEventBuffer proxyBuffer = this.ProxyFactory.CreateActorProxy(this.EventProxyStepId, nameof(EventBufferActor)); - await proxyBuffer.EnqueueAsync(daprEvent.ToJson()).ConfigureAwait(false); - } - - // Get the edges for the event and queue up the messages to be sent to the next steps. - bool foundEdge = false; - foreach (KernelProcessEdge edge in this.GetEdgeForEvent(daprEvent.QualifiedId)) - { - if (edge.OutputTarget is not KernelProcessFunctionTarget functionTarget) - { - throw new KernelException("The target for the edge is not a function target.").Log(this._logger); - } - ProcessMessage message = ProcessMessageFactory.CreateFromEdge(edge, daprEvent.SourceId, daprEvent.Data); - ActorId scopedStepId = this.ScopedActorId(new ActorId(functionTarget.StepId)); - IMessageBuffer targetStep = this.ProxyFactory.CreateActorProxy(scopedStepId, nameof(MessageBufferActor)); - await targetStep.EnqueueAsync(message.ToJson()).ConfigureAwait(false); - foundEdge = true; - } - - // Error event was raised with no edge to handle it, send it to the global error event buffer. - if (!foundEdge && daprEvent.IsError && this.ParentProcessId != null) - { - IEventBuffer parentProcess1 = this.ProxyFactory.CreateActorProxy(ProcessActor.GetScopedGlobalErrorEventBufferId(this.ParentProcessId), nameof(EventBufferActor)); - await parentProcess1.EnqueueAsync(daprEvent.ToJson()).ConfigureAwait(false); - } - } - - /// - /// Scopes the Id of a step within the process to the process. - /// - /// The actor Id to scope. - /// A new which is scoped to the process. - internal ActorId ScopedActorId(ActorId actorId) - { - return new ActorId($"{this.ParentProcessId}.{actorId.GetId()}"); - } - - /// - /// Retrieves all edges that are associated with the provided event Id. - /// - /// The event Id of interest. - /// A where T is - internal IEnumerable GetEdgeForEvent(string eventId) - { - if (this._outputEdges is null) - { - return []; - } - - if (this._outputEdges.TryGetValue(eventId, out List? edges) && edges is not null) - { - return edges; - } - - return []; - } -} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/AssemblyInfo.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/AssemblyInfo.cs deleted file mode 100644 index ba9a54f203e2..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/AssemblyInfo.cs +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Diagnostics.CodeAnalysis; - -// This assembly is currently experimental. -[assembly: Experimental("SKEXP0080")] diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/DaprKernelProcessContext.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/DaprKernelProcessContext.cs deleted file mode 100644 index 3b3ba0a0ecb6..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/DaprKernelProcessContext.cs +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Threading.Tasks; -using Dapr.Actors; -using Dapr.Actors.Client; -using Microsoft.SemanticKernel.Process; -using Microsoft.SemanticKernel.Process.Serialization; - -namespace Microsoft.SemanticKernel; - -/// -/// A context for a Dapr kernel process. -/// -public class DaprKernelProcessContext : KernelProcessContext -{ - private readonly IProcess _daprProcess; - private readonly KernelProcess _process; - - internal DaprKernelProcessContext(KernelProcess process, IActorProxyFactory? actorProxyFactory = null) - { - Verify.NotNull(process); - Verify.NotNullOrWhiteSpace(process.State?.StepId); - - if (string.IsNullOrWhiteSpace(process.State.RunId)) - { - process = process with { State = process.State with { RunId = Guid.NewGuid().ToString() } }; - } - - this._process = process; - var processId = new ActorId(process.State.RunId); - - // For a non-dependency-injected application, the static methods on ActorProxy are used. - // Since the ActorProxy methods are error prone, try to avoid using them when using - // dependency-injected applications - if (actorProxyFactory != null) - { - this._daprProcess = actorProxyFactory.CreateActorProxy(processId, nameof(ProcessActor)); - } - else - { - this._daprProcess = ActorProxy.Create(processId, nameof(ProcessActor)); - } - } - - /// - /// Starts the process with an initial event. - /// - /// The initial event. - /// An optional identifier of an actor requesting to proxy events. - internal async Task StartWithEventAsync(KernelProcessEvent initialEvent, ActorId? eventProxyStepId = null) - { - var daprProcess = DaprProcessInfo.FromKernelProcess(this._process); - await this._daprProcess.InitializeProcessAsync(daprProcess, null, eventProxyStepId?.GetId()).ConfigureAwait(false); - await this._daprProcess.RunOnceAsync(initialEvent.ToJson()).ConfigureAwait(false); - } - - /// - /// Sends a message to the process. - /// - /// The event to sent to the process. - /// A - public override async Task SendEventAsync(KernelProcessEvent processEvent) => - await this._daprProcess.SendMessageAsync(processEvent.ToJson()).ConfigureAwait(false); - - /// - /// Stops the process. - /// - /// A - public override async Task StopAsync() => await this._daprProcess.StopAsync().ConfigureAwait(false); - - /// - /// Gets a snapshot of the current state of the process. - /// - /// A where T is - public override async Task GetStateAsync() - { - var daprProcessInfo = await this._daprProcess.GetProcessInfoAsync().ConfigureAwait(false); - return daprProcessInfo.ToKernelProcess(); - } - - /// - public override Task GetExternalMessageChannelAsync() - { - throw new NotImplementedException(); - } - - /// - public override async Task GetProcessIdAsync() - { - var processInfo = await this._daprProcess.GetProcessInfoAsync().ConfigureAwait(false); - return processInfo.State.RunId!; - } -} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/DaprKernelProcessFactory.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/DaprKernelProcessFactory.cs deleted file mode 100644 index 52cda5391c77..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/DaprKernelProcessFactory.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Threading.Tasks; -using Dapr.Actors.Client; - -namespace Microsoft.SemanticKernel; -/// -/// A class that can run a process locally or in-process. -/// -public static class DaprKernelProcessFactory -{ - /// - /// Starts the specified process. - /// - /// Required: The to start running. - /// Required: The initial event to start the process. - /// Optional: Used to specify the unique Id of the process. If the process already has an Id, it will not be overwritten and this parameter has no effect. - /// Optional: when using in application with dependency injection it is recommended to pass the - /// An instance of that can be used to interrogate or stop the running process. - public static async Task StartAsync(this KernelProcess process, KernelProcessEvent initialEvent, string? processId = null, IActorProxyFactory? actorProxyFactory = null) - { - Verify.NotNull(process); - Verify.NotNullOrWhiteSpace(process.State?.StepId); - Verify.NotNull(initialEvent); - - // Assign the process Id if one is provided and the processes does not already have an Id. - if (!string.IsNullOrWhiteSpace(processId) && string.IsNullOrWhiteSpace(process.State.RunId)) - { - process = process with { State = process.State with { RunId = processId } }; - } - - DaprKernelProcessContext processContext = new(process, actorProxyFactory); - await processContext.StartWithEventAsync(initialEvent).ConfigureAwait(false); - return processContext; - } -} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/DaprMapInfo.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/DaprMapInfo.cs deleted file mode 100644 index f5f090b7ff2b..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/DaprMapInfo.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System.Runtime.Serialization; - -namespace Microsoft.SemanticKernel; - -/// -/// A serializable representation of a Dapr Map. -/// -[KnownType(typeof(KernelProcessEdge))] -[KnownType(typeof(KernelProcessMapState))] -[KnownType(typeof(KernelProcessStepState))] -[KnownType(typeof(KernelProcessStepState<>))] -public sealed record DaprMapInfo : DaprStepInfo -{ - /// - /// The map operation - /// - public required DaprStepInfo Operation { get; init; } - - /// - /// Initializes a new instance of the class from this instance of . - /// - /// An instance of - /// - public KernelProcessMap ToKernelProcessMap() - { - KernelProcessStepInfo processStepInfo = this.ToKernelProcessStepInfo(); - if (this.State is not KernelProcessMapState state) - { - throw new KernelException($"Unable to read state from map with name '{this.State.StepId}' and Id '{this.State.RunId}'."); - } - - KernelProcessStepInfo operationStep = - this.Operation is DaprProcessInfo processInfo - ? processInfo.ToKernelProcess() - : this.Operation.ToKernelProcessStepInfo(); - - return new KernelProcessMap(state, operationStep, this.Edges); - } - - /// - /// Initializes a new instance of the class from an instance of . - /// - /// The used to build the - /// An instance of - public static DaprMapInfo FromKernelProcessMap(KernelProcessMap processMap) - { - Verify.NotNull(processMap); - - DaprStepInfo operationInfo = - processMap.Operation is KernelProcess processOperation - ? DaprProcessInfo.FromKernelProcess(processOperation) - : DaprStepInfo.FromKernelStepInfo(processMap.Operation); - DaprStepInfo mapStepInfo = DaprStepInfo.FromKernelStepInfo(processMap); - - return new DaprMapInfo - { - InnerStepDotnetType = mapStepInfo.InnerStepDotnetType, - State = mapStepInfo.State, - Edges = mapStepInfo.Edges, - Operation = operationInfo, - }; - } -} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/DaprProcessInfo.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/DaprProcessInfo.cs deleted file mode 100644 index b5154a3803b5..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/DaprProcessInfo.cs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Runtime.Serialization; - -namespace Microsoft.SemanticKernel; - -/// -/// A serializable representation of a Dapr Process. -/// -[KnownType(typeof(KernelProcessEdge))] -[KnownType(typeof(KernelProcessState))] -[KnownType(typeof(KernelProcessMapState))] -[KnownType(typeof(KernelProcessStepState))] -[KnownType(typeof(KernelProcessStepState<>))] -public sealed record DaprProcessInfo : DaprStepInfo -{ - /// - /// The collection of Steps in the Process. - /// - public required IList Steps { get; init; } - - /// - /// Initializes a new instance of the class from this instance of . - /// - /// An instance of - /// - public KernelProcess ToKernelProcess() - { - var processStepInfo = this.ToKernelProcessStepInfo(); - if (this.State is not KernelProcessState state) - { - throw new KernelException($"Unable to read state from process with name '{this.State.StepId}' and Id '{this.State.RunId}'."); - } - - List steps = []; - foreach (var step in this.Steps) - { - if (step is DaprProcessInfo processStep) - { - steps.Add(processStep.ToKernelProcess()); - } - else if (step is DaprMapInfo mapStep) - { - steps.Add(mapStep.ToKernelProcessMap()); - } - else if (step is DaprProxyInfo proxyStep) - { - steps.Add(proxyStep.ToKernelProcessProxy()); - } - else - { - steps.Add(step.ToKernelProcessStepInfo()); - } - } - - return new KernelProcess(state, steps, this.Edges); - } - - /// - /// Initializes a new instance of the class from an instance of . - /// - /// The used to build the - /// An instance of - public static DaprProcessInfo FromKernelProcess(KernelProcess kernelProcess) - { - Verify.NotNull(kernelProcess); - - DaprStepInfo daprStepInfo = DaprStepInfo.FromKernelStepInfo(kernelProcess); - List daprSteps = []; - - foreach (var step in kernelProcess.Steps) - { - if (step is KernelProcess processStep) - { - daprSteps.Add(DaprProcessInfo.FromKernelProcess(processStep)); - } - else if (step is KernelProcessMap mapStep) - { - daprSteps.Add(DaprMapInfo.FromKernelProcessMap(mapStep)); - } - else if (step is KernelProcessProxy proxyStep) - { - daprSteps.Add(DaprProxyInfo.FromKernelProxyInfo(proxyStep)); - } - else - { - daprSteps.Add(DaprStepInfo.FromKernelStepInfo(step)); - } - } - - return new DaprProcessInfo - { - InnerStepDotnetType = daprStepInfo.InnerStepDotnetType, - State = daprStepInfo.State, - Edges = daprStepInfo.Edges, - Steps = daprSteps, - }; - } -} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/DaprProxyInfo.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/DaprProxyInfo.cs deleted file mode 100644 index 173a159cc0f0..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/DaprProxyInfo.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System.Runtime.Serialization; -using Microsoft.SemanticKernel.Process.Models; - -namespace Microsoft.SemanticKernel; - -/// -/// A serializable representation of a Dapr Proxy. -/// -[KnownType(typeof(KernelProcessEdge))] -[KnownType(typeof(KernelProcessStepState))] -[KnownType(typeof(KernelProcessStepState<>))] -public sealed record DaprProxyInfo : DaprStepInfo -{ - /// - /// Proxy related data to be able to emit the events externally - /// - public required KernelProcessProxyStateMetadata? ProxyMetadata { get; init; } - - /// - /// Initializes a new instance of the class from this instance of . - /// - /// An instance of - /// - public KernelProcessProxy ToKernelProcessProxy() - { - KernelProcessStepInfo processStepInfo = this.ToKernelProcessStepInfo(); - if (this.State is not KernelProcessStepState state) - { - throw new KernelException($"Unable to read state from proxy with name '{this.State.StepId}', Id '{this.State.RunId}' and type {this.State.GetType()}."); - } - - return new KernelProcessProxy(state, this.Edges) - { - ProxyMetadata = this.ProxyMetadata, - }; - } - - /// - /// Initializes a new instance of the class from an instance of . - /// - /// The used to build the - /// - public static DaprProxyInfo FromKernelProxyInfo(KernelProcessProxy kernelProxyInfo) - { - Verify.NotNull(kernelProxyInfo, nameof(kernelProxyInfo)); - - DaprStepInfo proxyStepInfo = DaprStepInfo.FromKernelStepInfo(kernelProxyInfo); - - return new DaprProxyInfo - { - InnerStepDotnetType = proxyStepInfo.InnerStepDotnetType, - State = proxyStepInfo.State, - Edges = proxyStepInfo.Edges, - ProxyMetadata = kernelProxyInfo.ProxyMetadata, - }; - } -} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/DaprStepInfo.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/DaprStepInfo.cs deleted file mode 100644 index 15b72d76744d..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/DaprStepInfo.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel; - -/// -/// Contains information about a Step in a Dapr Process including it's state and edges. -/// -[KnownType(typeof(KernelProcessEdge))] -[KnownType(typeof(KernelProcessStepState))] -[KnownType(typeof(KernelProcessProxyMessage))] -[KnownType(typeof(DaprProcessInfo))] -[KnownType(typeof(DaprMapInfo))] -[KnownType(typeof(DaprProxyInfo))] -[JsonDerivedType(typeof(DaprProcessInfo))] -[JsonDerivedType(typeof(DaprMapInfo))] -[JsonDerivedType(typeof(DaprProxyInfo))] -public record DaprStepInfo -{ - /// - /// The .Net type of the inner step. - /// - public required string InnerStepDotnetType { get; init; } - - /// - /// The state of the Step. - /// - public required KernelProcessStepState State { get; init; } - - /// - /// A read-only dictionary of output edges from the Step. - /// - public required Dictionary> Edges { get; init; } - - /// - /// Builds an instance of from the current object. - /// - /// An instance of - /// - public KernelProcessStepInfo ToKernelProcessStepInfo() - { - Type? innerStepType = Type.GetType(this.InnerStepDotnetType); - if (innerStepType is null) - { - throw new KernelException($"Unable to create inner step type from assembly qualified name `{this.InnerStepDotnetType}`"); - } - - return new KernelProcessStepInfo(innerStepType, this.State, this.Edges); - } - - /// - /// Initializes a new instance of the class from an instance of . - /// - /// An instance of - public static DaprStepInfo FromKernelStepInfo(KernelProcessStepInfo kernelStepInfo) - { - Verify.NotNull(kernelStepInfo, nameof(kernelStepInfo)); - - return new DaprStepInfo - { - InnerStepDotnetType = kernelStepInfo.InnerStepType.AssemblyQualifiedName!, - State = kernelStepInfo.State, - Edges = kernelStepInfo.Edges.ToDictionary(kvp => kvp.Key, kvp => new List(kvp.Value)), - }; - } -} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/Interfaces/IEventBuffer.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/Interfaces/IEventBuffer.cs deleted file mode 100644 index cdaf2f84d349..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Interfaces/IEventBuffer.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Threading.Tasks; -using Dapr.Actors; -using Microsoft.SemanticKernel.Process.Runtime; - -namespace Microsoft.SemanticKernel; - -/// -/// An interface for a buffer of s. -/// -public interface IEventBuffer : IActor -{ - /// - /// Enqueues an external event. - /// - /// The event to enqueue as JSON. - /// A - Task EnqueueAsync(string stepEvent); - - /// - /// Dequeues all external events. - /// - /// A where T is the JSON representation of a - Task> DequeueAllAsync(); -} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/Interfaces/IExternalEventBuffer.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/Interfaces/IExternalEventBuffer.cs deleted file mode 100644 index 50e296db54a7..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Interfaces/IExternalEventBuffer.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Threading.Tasks; -using Dapr.Actors; - -namespace Microsoft.SemanticKernel; - -/// -/// An interface for a buffer of s. -/// -public interface IExternalEventBuffer : IActor -{ - /// - /// Enqueues an external event. - /// - /// The external event to enqueue as JSON. - /// A - Task EnqueueAsync(string externalEvent); - - /// - /// Dequeues all external events. - /// - /// A where T is the JSON representation of a - Task> DequeueAllAsync(); -} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/Interfaces/IExternalMessageBuffer.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/Interfaces/IExternalMessageBuffer.cs deleted file mode 100644 index 11f5907217dc..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Interfaces/IExternalMessageBuffer.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Threading.Tasks; -using Dapr.Actors; - -namespace Microsoft.SemanticKernel; - -// estenori-note: -// for some reason dapr doesn't like if instead public interface IExternalMessageBuffer : IActor, IExternalKernelProcessMessageChannelBase -// instead defining the interface component is necessary. To make it compatible with shared components a "casting" to IExternalKernelProcessMessageChannelEmitter -// is added in StepActor logic to make use of FindInputChannels - -/// -/// An interface for -/// -public interface IExternalMessageBuffer : IActor -{ - /// - /// Emits external events outside of the SK process - /// - /// - /// - /// - abstract Task EmitExternalEventAsync(string externalTopicEvent, KernelProcessProxyMessage eventData); -} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/Interfaces/IMap.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/Interfaces/IMap.cs deleted file mode 100644 index 483a6bc3502a..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Interfaces/IMap.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System.Threading.Tasks; -using Dapr.Actors; - -namespace Microsoft.SemanticKernel; - -/// -/// An interface that represents a step in a process. -/// -public interface IMap : IActor -{ - /// - /// Initializes the step with the provided step information. - /// - /// A - /// - Task InitializeMapAsync(DaprMapInfo mapInfo, string? parentProcessId); -} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/Interfaces/IMessageBuffer.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/Interfaces/IMessageBuffer.cs deleted file mode 100644 index 5760d0585454..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Interfaces/IMessageBuffer.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Threading.Tasks; -using Dapr.Actors; -using Microsoft.SemanticKernel.Process.Runtime; - -namespace Microsoft.SemanticKernel; - -/// -/// An interface for a buffer of s. -/// -public interface IMessageBuffer : IActor -{ - /// - /// Enqueues an external event. - /// - /// The message to enqueue as JSON. - /// A - Task EnqueueAsync(string message); - - /// - /// Dequeues all external events. - /// - /// A where T is the JSON representation of a - Task> DequeueAllAsync(); -} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/Interfaces/IProcess.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/Interfaces/IProcess.cs deleted file mode 100644 index 8a4ce788b51d..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Interfaces/IProcess.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Threading.Tasks; -using Dapr.Actors; - -namespace Microsoft.SemanticKernel; - -/// -/// An interface that represents a process. -/// -public interface IProcess : IActor, IStep -{ - /// - /// Initializes the process with the specified instance of . - /// - /// Used to initialize the process. - /// The parent Id of the process if one exists. - /// An optional identifier of an actor requesting to proxy events. - /// A - Task InitializeProcessAsync(DaprProcessInfo processInfo, string? parentProcessId, string? eventProxyStepId); - - /// - /// Starts an initialized process. - /// - /// Indicates if the process should wait for external events after it's finished processing. - /// - Task StartAsync(bool keepAlive); - - /// - /// Starts the process with an initial event and then waits for the process to finish. In this case the process will not - /// keep alive waiting for external events after the internal messages have stopped. - /// - /// Required. The to start the process with. - /// A - Task RunOnceAsync(string processEvent); - - /// - /// Stops a running process. This will cancel the process and wait for it to complete before returning. - /// - /// A - Task StopAsync(); - - /// - /// Sends a message to the process. This does not start the process if it's not already running, in - /// this case the message will remain queued until the process is started. - /// - /// Required. The to start the process with. - /// A - Task SendMessageAsync(string processEvent); - - /// - /// Gets the process information. - /// - /// An instance of - Task GetProcessInfoAsync(); -} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/Interfaces/IProxy.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/Interfaces/IProxy.cs deleted file mode 100644 index 786fe7db49b2..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Interfaces/IProxy.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System.Threading.Tasks; -using Dapr.Actors; - -namespace Microsoft.SemanticKernel; - -/// -/// An interface that represents a step in a process. -/// -public interface IProxy : IActor -{ - /// - /// Initializes the step with the provided step information. - /// - /// A - /// - Task InitializeProxyAsync(DaprProxyInfo proxyInfo, string? parentProcessId); -} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/Interfaces/IStep.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/Interfaces/IStep.cs deleted file mode 100644 index 1cac1d945217..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Interfaces/IStep.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Threading.Tasks; -using Dapr.Actors; - -namespace Microsoft.SemanticKernel; - -/// -/// An interface that represents a step in a process. -/// -public interface IStep : IActor -{ - /// - /// Initializes the step with the provided step information. - /// - /// A - /// - Task InitializeStepAsync(DaprStepInfo stepInfo, string? parentProcessId, string? eventProxyStepId); - - /// - /// Triggers the step to dequeue all pending messages and prepare for processing. - /// - /// A where T is an indicating the number of messages that are prepared for processing. - Task PrepareIncomingMessagesAsync(); - - /// - /// Triggers the step to process all prepared messages. - /// - /// A - Task ProcessIncomingMessagesAsync(); - - /// - /// Builds the current state of the step into a . - /// - /// An instance of - Task ToDaprStepInfoAsync(); -} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/KernelProcessDaprExtensions.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/KernelProcessDaprExtensions.cs deleted file mode 100644 index 504895e463fd..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/KernelProcessDaprExtensions.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Dapr.Actors.Runtime; - -namespace Microsoft.SemanticKernel; - -/// -/// Extension methods for configuring Dapr actors for the process runtime. -/// -public static class KernelProcessDaprExtensions -{ - /// - /// Adds the process runtime actors to the actor runtime options. - /// - /// The instance of - public static void AddProcessActors(this ActorRuntimeOptions actorOptions) - { - // Register actor types and configure actor settings - actorOptions.Actors.RegisterActor(); - actorOptions.Actors.RegisterActor(); - actorOptions.Actors.RegisterActor(); - actorOptions.Actors.RegisterActor(); - actorOptions.Actors.RegisterActor(); - actorOptions.Actors.RegisterActor(); - actorOptions.Actors.RegisterActor(); - actorOptions.Actors.RegisterActor(); - } -} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/Process.Runtime.Dapr.csproj b/dotnet/src/Experimental/Process.Runtime.Dapr/Process.Runtime.Dapr.csproj deleted file mode 100644 index 8ba7b61f4c8e..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Process.Runtime.Dapr.csproj +++ /dev/null @@ -1,47 +0,0 @@ - - - - - Microsoft.SemanticKernel.Process.Runtime.Dapr - Microsoft.SemanticKernel.Process - net8.0 - false - alpha - - - - - - - - - - Semantic Kernel Process - Dapr Runtime - Semantic Kernel Process Dapr Runtime. This package is automatically installed by Semantic Kernel Process packages if needed. - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/Serialization/KernelProcessEventSerializer.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/Serialization/KernelProcessEventSerializer.cs deleted file mode 100644 index c77becb9f6f1..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Serialization/KernelProcessEventSerializer.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; - -namespace Microsoft.SemanticKernel.Process.Serialization; - -/// -/// Serializer for objects. -/// -/// -/// Includes type info for . -/// -internal static class KernelProcessEventSerializer -{ - /// - /// Serialize to JSON with type information. - /// - public static string ToJson(this KernelProcessEvent processEvent) - { - EventContainer containedEvents = new(TypeInfo.GetAssemblyQualifiedType(processEvent.Data), processEvent); - return JsonSerializer.Serialize(containedEvents); - } - - /// - /// Deserialize a list of JSON events into a list of objects. - /// - /// If any event fails deserialization - public static IList ToKernelProcessEvents(this IEnumerable jsonEvents) - { - return Deserialize().ToArray(); - - IEnumerable Deserialize() - { - foreach (string json in jsonEvents) - { - yield return json.ToKernelProcessEvent(); - } - } - } - - /// - /// Deserialize a list of JSON events into a list of objects. - /// - /// If any event fails deserialization - public static KernelProcessEvent ToKernelProcessEvent(this string jsonEvent) - { - EventContainer eventContainer = - JsonSerializer.Deserialize>(jsonEvent) ?? - throw new KernelException($"Unable to deserialize {nameof(KernelProcessEvent)} queue."); - return eventContainer.Payload with { Data = TypeInfo.ConvertValue(eventContainer.DataTypeName, eventContainer.Payload.Data) }; - } -} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/Serialization/ProcessEventSerializer.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/Serialization/ProcessEventSerializer.cs deleted file mode 100644 index ab87ae00180c..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Serialization/ProcessEventSerializer.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using Microsoft.SemanticKernel.Process.Runtime; - -namespace Microsoft.SemanticKernel.Process.Serialization; - -/// -/// Serializer for objects. -/// -/// -/// Includes type info for . -/// -internal static class ProcessEventSerializer -{ - /// - /// Serialize to JSON with type information. - /// - public static string ToJson(this ProcessEvent processEvent) - { - EventContainer containedEvent = new(TypeInfo.GetAssemblyQualifiedType(processEvent.Data), processEvent); - return JsonSerializer.Serialize(containedEvent); - } - - /// - /// Deserialize a list of JSON events into a list of objects. - /// - /// If any event fails deserialization - public static IList ToProcessEvents(this IEnumerable jsonEvents) - { - return Deserialize().ToArray(); - - IEnumerable Deserialize() - { - foreach (string json in jsonEvents) - { - EventContainer eventContainer = - JsonSerializer.Deserialize>(json) ?? - throw new KernelException($"Unable to deserialize {nameof(ProcessEvent)} queue."); - yield return eventContainer.Payload with { Data = TypeInfo.ConvertValue(eventContainer.DataTypeName, eventContainer.Payload.Data) }; - } - } - } -} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/Serialization/ProcessMessageSerializer.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/Serialization/ProcessMessageSerializer.cs deleted file mode 100644 index 9532afc284bd..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Serialization/ProcessMessageSerializer.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using Microsoft.SemanticKernel.Process.Runtime; - -namespace Microsoft.SemanticKernel.Process.Serialization; - -/// -/// Serializer for objects. -/// -/// -/// Includes type info for and . -/// -internal static class ProcessMessageSerializer -{ - /// - /// Serialize to JSON with type information. - /// - public static string ToJson(this ProcessMessage processMessage) - { - Dictionary typeMap = processMessage.Values.ToDictionary(kvp => kvp.Key, kvp => TypeInfo.GetAssemblyQualifiedType(kvp.Value)); - MessageContainer containedMessage = new(TypeInfo.GetAssemblyQualifiedType(processMessage.TargetEventData), typeMap, processMessage); - return JsonSerializer.Serialize(containedMessage); - } - - /// - /// Deserialize a list of JSON messages into a list of objects. - /// - /// If any message fails deserialization - public static IList ToProcessMessages(this IEnumerable jsonMessages) - { - return Deserialize().ToArray(); - - IEnumerable Deserialize() - { - foreach (string json in jsonMessages) - { - MessageContainer containedMessage = - JsonSerializer.Deserialize(json) ?? - throw new KernelException($"Unable to deserialize {nameof(ProcessMessage)} queue."); - - yield return Process(containedMessage); - } - } - } - - private static ProcessMessage Process(MessageContainer messageContainer) - { - ProcessMessage processMessage = messageContainer.Message; - - if (processMessage.Values.Count == 0) - { - return processMessage; - } - - processMessage = - processMessage with - { - TargetEventData = TypeInfo.ConvertValue(messageContainer.DataTypeName, processMessage.TargetEventData), - Values = messageContainer.ValueTypeNames.ToDictionary(kvp => kvp.Key, kvp => TypeInfo.ConvertValue(kvp.Value, processMessage.Values[kvp.Key])) - }; - - return processMessage; - } -} diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/Serialization/TypeContainers.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/Serialization/TypeContainers.cs deleted file mode 100644 index 4e24b1709b43..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Serialization/TypeContainers.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System.Collections.Generic; -using Microsoft.SemanticKernel.Process.Runtime; - -namespace Microsoft.SemanticKernel.Process.Serialization; - -/// -/// Container for an event with type information. -/// -/// The type of event -/// The typeof the Data property -/// The source event -internal sealed record EventContainer(string? DataTypeName, TValue Payload); - -/// -/// Container for an message with type information. -/// -/// The type of . -/// A type map for . -/// The source message -internal sealed record MessageContainer(string? DataTypeName, Dictionary ValueTypeNames, ProcessMessage Message); diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/Serialization/TypeInfo.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/Serialization/TypeInfo.cs deleted file mode 100644 index ad64a1e1a53c..000000000000 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Serialization/TypeInfo.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System; -using System.Text.Json; - -namespace Microsoft.SemanticKernel.Process.Serialization; - -/// -/// Extension methods for capturing and restoring an object's type. -/// -internal static class TypeInfo -{ - /// - /// Retrieves the assembly qualified type-name of the provided value (null when null). - /// - public static string? GetAssemblyQualifiedType(object? value) - { - if (value == null) - { - return null; - } - - return value.GetType().AssemblyQualifiedName; - } - - /// - /// Restore the object's type from the provided assembly qualified type-name, but - /// only if it is a . Otherwise, return the original value. - /// - public static object? ConvertValue(string? assemblyQualifiedTypeName, object? value) - { - if (value == null || value.GetType() != typeof(JsonElement)) - { - return value; - } - - if (assemblyQualifiedTypeName == null) - { - throw new KernelException("Data persisted without type information."); - } - - Type? valueType = Type.GetType(assemblyQualifiedTypeName); - return ((JsonElement)value).Deserialize(valueType!); - } -} diff --git a/dotnet/src/Experimental/Process.UnitTests/Runtime.Local/LocalMapTests.cs b/dotnet/src/Experimental/Process.UnitTests/Runtime.Local/LocalMapTests.cs index 9e8042f6d926..57bd052fd44f 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Runtime.Local/LocalMapTests.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Runtime.Local/LocalMapTests.cs @@ -322,7 +322,6 @@ public async Task ProcessMapResultWithTargetExtraAsync() // Assert UnionState unionState = await GetUnionStateAsync(processContext); Assert.Equal(55L, unionState.SquareResult); - Assert.Equal(5, counterService.GetCount()); } /// diff --git a/dotnet/src/InternalUtilities/process/TestsShared/Setup/KernelSetup.cs b/dotnet/src/InternalUtilities/process/TestsShared/Setup/KernelSetup.cs index a70dc96db5c8..cd0f9eae3a1a 100644 --- a/dotnet/src/InternalUtilities/process/TestsShared/Setup/KernelSetup.cs +++ b/dotnet/src/InternalUtilities/process/TestsShared/Setup/KernelSetup.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. +#pragma warning disable IDE0005 // Using directive is unnecessary using Microsoft.Extensions.DependencyInjection; using Microsoft.SemanticKernel.Process.TestsShared.Services; +#pragma warning restore IDE0005 // Using directive is unnecessary namespace Microsoft.SemanticKernel.Process.TestsShared.Setup; From 69963c19c3d99282e947b2c3d57fcdb9a34be537 Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Tue, 5 Aug 2025 10:24:00 -0700 Subject: [PATCH 3/4] Update dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessTests.cs --- .../Experimental/Process.IntegrationTests.Shared/ProcessTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessTests.cs b/dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessTests.cs index 1f1f16544ced..f8376467c868 100644 --- a/dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessTests.cs +++ b/dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessTests.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.IntegrationTests.TestSettings; using Microsoft.SemanticKernel.Process.TestsShared.Steps; using Microsoft.SemanticKernel.IntegrationTests.TestSettings; using Xunit; From ce6e570361edc8fa1391751c384ca7ab83238432 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estefan=C3=ADa=20Tenorio?= <8483207+esttenorio@users.noreply.github.com> Date: Tue, 5 Aug 2025 10:28:43 -0700 Subject: [PATCH 4/4] fixing formatting error --- .../Experimental/Process.IntegrationTests.Shared/ProcessTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessTests.cs b/dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessTests.cs index f8376467c868..1ffc20699b94 100644 --- a/dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessTests.cs +++ b/dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessTests.cs @@ -9,7 +9,6 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.IntegrationTests.TestSettings; using Microsoft.SemanticKernel.Process.TestsShared.Steps; -using Microsoft.SemanticKernel.IntegrationTests.TestSettings; using Xunit; #pragma warning restore IDE0005 // Using directive is unnecessary.