From 656159e492b5dc453cd39ed666633f52b198062e Mon Sep 17 00:00:00 2001 From: Ben Thomas Date: Mon, 21 Oct 2024 13:20:47 -0700 Subject: [PATCH] .Net: Processes with Dapr - Improving Demo with README (#9360) ### Description This PR adds the following: - A README file for the Processes with Dapr demo application to help explain how to run it and understand what is happening. - Cleans up naming of things within the demo and adds Console output to help follow execution of the process. - Improves tasks and launch config in VS Code and adds the Dapr extension as a recommended extension. ### Contribution Checklist - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone :smile: --------- Co-authored-by: Ben Thomas --- .vscode/extensions.json | 1 + .vscode/launch.json | 9 +- .vscode/tasks.json | 19 ++- ...tBotController.cs => ProcessController.cs} | 69 ++++++----- .../ProcessWithDapr/ProcessWithDapr.csproj | 5 +- .../samples/Demos/ProcessWithDapr/README.md | 110 ++++++++++++++++++ .../Process.Runtime.Dapr/Actors/StepActor.cs | 4 +- .../Processes/ProcessCycleTests.cs | 2 +- 8 files changed, 179 insertions(+), 40 deletions(-) rename dotnet/samples/Demos/ProcessWithDapr/Controllers/{ChatBotController.cs => ProcessController.cs} (69%) create mode 100644 dotnet/samples/Demos/ProcessWithDapr/README.md diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 997b474128..318937cc4e 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -10,5 +10,6 @@ "ms-semantic-kernel.semantic-kernel", "emeraldwalk.RunOnSave", "ms-java.vscode-java-pack", + "ms-azuretools.vscode-dapr" ] } \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 0b78c785ac..ae7d191a00 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -48,11 +48,14 @@ }, { "name": "C#: Process with Dapr", - "type": "dotnet", + "type": "coreclr", "request": "launch", - "projectPath": "${workspaceFolder}\\dotnet\\samples\\Demos\\ProcessWithDapr\\ProcessWithDapr.csproj", + "program": "${workspaceFolder}\\dotnet\\samples\\Demos\\ProcessWithDapr\\bin\\Debug\\net8.0\\ProcessWithDapr.dll", "preLaunchTask": "daprd-debug", - "postDebugTask": "dapr-down" + "postDebugTask": "dapr-down", + "env": { + "ASPNETCORE_ENVIRONMENT" : "Development" + } } ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index e3c11ac969..1cd9319c31 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -271,13 +271,28 @@ "group": "PR-Validate" } }, + { + "label": "build (ProcessWithDapr)", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/dotnet/samples/Demos/ProcessWithDapr/ProcessWithDapr.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary", + "/property:DebugType=portable" + ], + "problemMatcher": "$msCompile", + "group": "build" + }, { "label": "daprd-debug", "type": "daprd", "appId": "processwithdapr", "logLevel": "debug", - "appPort": 5231, - "httpPort": 3500 + "appPort": 5000, + "httpPort": 3500, + "dependsOn":["build (ProcessWithDapr)"] }, { "label": "dapr-down", diff --git a/dotnet/samples/Demos/ProcessWithDapr/Controllers/ChatBotController.cs b/dotnet/samples/Demos/ProcessWithDapr/Controllers/ProcessController.cs similarity index 69% rename from dotnet/samples/Demos/ProcessWithDapr/Controllers/ChatBotController.cs rename to dotnet/samples/Demos/ProcessWithDapr/Controllers/ProcessController.cs index a2da8f7c5a..b50cd8fba3 100644 --- a/dotnet/samples/Demos/ProcessWithDapr/Controllers/ChatBotController.cs +++ b/dotnet/samples/Demos/ProcessWithDapr/Controllers/ProcessController.cs @@ -10,80 +10,82 @@ namespace ProcessWithDapr.Controllers; /// A controller for chatbot. /// [ApiController] -public class ChatBotController : ControllerBase +public class ProcessController : ControllerBase { - private readonly Dictionary _processMap = []; private readonly Kernel _kernel; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// An instance of - public ChatBotController(Kernel kernel) + public ProcessController(Kernel kernel) { this._kernel = kernel; } /// - /// Post a message to a chat bot. + /// Start and run a process. /// - /// The Id of the chat bot. - /// The message to send. + /// The Id of the process. /// - [HttpGet("chatbots/{chatBotId}/{message}")] - public async Task PostAsync(string chatBotId, string message) + [HttpGet("processes/{processId}")] + public async Task PostAsync(string processId) { - var process = this.GetProcess(chatBotId); - var processContext = await process.StartAsync(this._kernel, new KernelProcessEvent() { Id = CommonEvents.StartProcess, Data = "foo" }, processId: chatBotId); + var process = this.GetProcess(); + var processContext = await process.StartAsync(this._kernel, new KernelProcessEvent() { Id = CommonEvents.StartProcess }, processId: processId); var finalState = await processContext.GetStateAsync(); - return this.Ok(chatBotId); + return this.Ok(processId); } - private KernelProcess GetProcess(string processId) + private KernelProcess GetProcess() { - if (this._processMap.TryGetValue(processId, out var process)) - { - return process; - } - - ProcessBuilder processBuilder = new("Test Process"); + // Create the process builder. + ProcessBuilder processBuilder = new("ProcessWithDapr"); + // Add some steps to the process. var kickoffStep = processBuilder.AddStepFromType(); var myAStep = processBuilder.AddStepFromType(); var myBStep = processBuilder.AddStepFromType(); + + // ########## 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 }); + // 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)); - - kickoffStep - .OnEvent(CommonEvents.StartBRequested) + .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 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(); - process = processBuilder.Build(); - this._processMap[processId] = process; + var process = processBuilder.Build(); return process; } @@ -104,8 +106,7 @@ public class ChatBotController : ControllerBase public async ValueTask PrintWelcomeMessageAsync(KernelProcessStepContext context) { Console.WriteLine("##### Kickoff ran."); - await context.EmitEventAsync(new() { Id = CommonEvents.StartARequested, Data = "Get Going A" }); - await context.EmitEventAsync(new() { Id = CommonEvents.StartBRequested, Data = "Get Going B" }); + await context.EmitEventAsync(new() { Id = CommonEvents.StartARequested, Data = "Get Going" }); } } @@ -138,15 +139,20 @@ public class ChatBotController : ControllerBase } /// - /// A step in the process. + /// A stateful step in the process. This step uses as the persisted + /// state object and overrides the ActivateAsync method to initialize the state when activated. /// private sealed class CStep : KernelProcessStep { - private CStepState _state = new(); + private CStepState? _state; + // ################ Using persisted state ################# + // CStep has state that we want to be persisted in the process. To ensure that the step always + // starts with the previously persisted or configured state, we need to override the ActivateAsync + // method and use the state object it provides. public override ValueTask ActivateAsync(KernelProcessStepState state) { - this._state = state.State ?? new CStepState(); + this._state = state.State!; Console.WriteLine($"##### CStep activated with Cycle = '{state.State?.CurrentCycle}'."); return base.ActivateAsync(state); } @@ -154,7 +160,8 @@ public class ChatBotController : ControllerBase [KernelFunction] public async ValueTask DoItAsync(KernelProcessStepContext context, string astepdata, string bstepdata) { - this._state.CurrentCycle++; + // ########### This method will restart the process in a loop until CurrentCycle >= 3 ########### + this._state!.CurrentCycle++; if (this._state.CurrentCycle >= 3) { // Exit the processes diff --git a/dotnet/samples/Demos/ProcessWithDapr/ProcessWithDapr.csproj b/dotnet/samples/Demos/ProcessWithDapr/ProcessWithDapr.csproj index c1a24b9d7c..69628bbacd 100644 --- a/dotnet/samples/Demos/ProcessWithDapr/ProcessWithDapr.csproj +++ b/dotnet/samples/Demos/ProcessWithDapr/ProcessWithDapr.csproj @@ -4,7 +4,8 @@ net8.0 enable enable - $(NoWarn);CA2007,CA1861,VSTHRD111,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0080,SKEXP0110 + + $(NoWarn);CA2007,CA1861,VSTHRD111,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0080,SKEXP0110 @@ -18,4 +19,4 @@ - + \ No newline at end of file diff --git a/dotnet/samples/Demos/ProcessWithDapr/README.md b/dotnet/samples/Demos/ProcessWithDapr/README.md new file mode 100644 index 0000000000..9718d5c6f1 --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithDapr/README.md @@ -0,0 +1,110 @@ +# 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. + +For more information about Semantic Kernel Processes and Dapr, 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 + Kickoff --> B + A --> C + B --> C + + C -->|Count < 3| Kickoff + C -->|Count == 3| End + + classDef kickoffClass fill:#f9f,stroke:#333,stroke-width:2px; + class Kickoff kickoffClass; + + 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. + +#### Invoking the process: + +1. Open a web browser and point it to [http://localhost:5000/processes/1234](http://localhost:5000/processes/1234) to invoke a new process with `Id = "1234"` +1. You should see console output from the running service with logs that match the following: + +```csharp +##### Kickoff ran. +##### AStep ran. +##### BStep ran. +##### CStep activated with Cycle = '1'. +##### CStep run cycle 2. +##### Kickoff ran. +##### AStep ran. +##### BStep ran. +##### CStep run cycle 3 - exiting. +``` + +Now refresh the page in your browser to run the same processes instance again. Now the logs should look like this: + +```csharp +##### Kickoff ran. +##### AStep ran. +##### BStep ran. +##### CStep run cycle 3 - exiting. +``` + +Notice that the logs from the two runs are not the same. In the first run, the processes has not been run before and so it's initial +state came from what we defined in the process: + +**_First Run_** + +- `CState` is initialized with `Cycle = 1` which is the initial state that we specified while building the process. +- `CState` is invoked a total of two times before the terminal condition of `Cycle >= 3` is reached. + +In the second run however, the process has persisted state from the first run: + +**_Second Run_** + +- `CState` is initialized with `Cycle = 3` which is the final state from the first run of the process. +- `CState` is invoked only once and is already in the terminal condition of `Cycle >= 3`. + +If you create a new instance of the process with `Id = "ABCD"` by pointing your browser to [http://localhost:5000/processes/ABCD](http://localhost:5000/processes/ABCD), you will see the it will start with the initial state as expected. + +## Understanding the Code + +Below are the key aspects of the code that show how Dapr and Semantic Kernel Processes can be integrated into an ASP.Net Core Web Api: + +- Create a new ASP.Net web API project. +- Add the required Semantic Kernel and Dapr packages to your project: + + **_Semantic Kernel Packages_** + + - `dotnet add package Microsoft.SemanticKernel --version 1.24.0` + - `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/src/Experimental/Process.Runtime.Dapr/Actors/StepActor.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/StepActor.cs index 45ed9fe288..5f18c3959e 100644 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/StepActor.cs +++ b/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/StepActor.cs @@ -102,7 +102,9 @@ internal class StepActor : Actor, IStep, IKernelProcessMessageChannel this._innerStepType = Type.GetType(stepInfo.InnerStepDotnetType); if (this._innerStepType is null) { - throw new KernelException($"Could not load the inner step type '{stepInfo.InnerStepDotnetType}'."); + var errorMessage = $"Could not load the inner step type '{stepInfo.InnerStepDotnetType}'."; + this._logger?.LogError("{ErrorMessage}", errorMessage); + throw new KernelException(errorMessage); } this.ParentProcessId = parentProcessId; diff --git a/dotnet/src/IntegrationTests/Processes/ProcessCycleTests.cs b/dotnet/src/IntegrationTests/Processes/ProcessCycleTests.cs index baccffb64a..bb01f01f56 100644 --- a/dotnet/src/IntegrationTests/Processes/ProcessCycleTests.cs +++ b/dotnet/src/IntegrationTests/Processes/ProcessCycleTests.cs @@ -1,13 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.SemanticKernel; using SemanticKernel.IntegrationTests.Agents; using SemanticKernel.IntegrationTests.TestSettings; using Xunit; -using System.Linq; namespace SemanticKernel.IntegrationTests.Processes;