mirror of
https://github.com/microsoft/semantic-kernel.git
synced 2026-10-12 02:19:06 +00:00
.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 <!-- Before submitting this PR, please make sure: --> - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone 😄 --------- Co-authored-by: Ben Thomas <[email protected]>
This commit is contained in:
Vendored
+1
@@ -10,5 +10,6 @@
|
||||
"ms-semantic-kernel.semantic-kernel",
|
||||
"emeraldwalk.RunOnSave",
|
||||
"ms-java.vscode-java-pack",
|
||||
"ms-azuretools.vscode-dapr"
|
||||
]
|
||||
}
|
||||
Vendored
+6
-3
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+17
-2
@@ -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",
|
||||
|
||||
+38
-31
@@ -10,80 +10,82 @@ namespace ProcessWithDapr.Controllers;
|
||||
/// A controller for chatbot.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
public class ChatBotController : ControllerBase
|
||||
public class ProcessController : ControllerBase
|
||||
{
|
||||
private readonly Dictionary<string, KernelProcess> _processMap = [];
|
||||
private readonly Kernel _kernel;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatBotController"/> class.
|
||||
/// Initializes a new instance of the <see cref="ProcessController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="kernel">An instance of <see cref="Kernel"/></param>
|
||||
public ChatBotController(Kernel kernel)
|
||||
public ProcessController(Kernel kernel)
|
||||
{
|
||||
this._kernel = kernel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Post a message to a chat bot.
|
||||
/// Start and run a process.
|
||||
/// </summary>
|
||||
/// <param name="chatBotId">The Id of the chat bot.</param>
|
||||
/// <param name="message">The message to send.</param>
|
||||
/// <param name="processId">The Id of the process.</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("chatbots/{chatBotId}/{message}")]
|
||||
public async Task<IActionResult> PostAsync(string chatBotId, string message)
|
||||
[HttpGet("processes/{processId}")]
|
||||
public async Task<IActionResult> 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<KickoffStep>();
|
||||
var myAStep = processBuilder.AddStepFromType<AStep>();
|
||||
var myBStep = processBuilder.AddStepFromType<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<CStep, CStepState>(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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A step in the process.
|
||||
/// A stateful step in the process. This step uses <see cref="CStepState"/> as the persisted
|
||||
/// state object and overrides the ActivateAsync method to initialize the state when activated.
|
||||
/// </summary>
|
||||
private sealed class CStep : KernelProcessStep<CStepState>
|
||||
{
|
||||
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<CStepState> 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
|
||||
@@ -4,7 +4,8 @@
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<NoWarn>$(NoWarn);CA2007,CA1861,VSTHRD111,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0080,SKEXP0110</NoWarn>
|
||||
<NoWarn>
|
||||
$(NoWarn);CA2007,CA1861,VSTHRD111,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0080,SKEXP0110</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -18,4 +19,4 @@
|
||||
<PackageReference Include="Dapr.Actors.AspNetCore" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -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).
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user