chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Runtime.Serialization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.SemanticKernel;
|
||||
|
||||
namespace ProcessWithDapr.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// A controller for chatbot.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
public class ProcessController : ControllerBase
|
||||
{
|
||||
private readonly Kernel _kernel;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ProcessController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="kernel">An instance of <see cref="Kernel"/></param>
|
||||
public ProcessController(Kernel kernel)
|
||||
{
|
||||
this._kernel = kernel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start and run a process.
|
||||
/// </summary>
|
||||
/// <param name="processId">The Id of the process.</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("processes/{processId}")]
|
||||
public async Task<IActionResult> PostAsync(string processId)
|
||||
{
|
||||
var process = this.GetProcess();
|
||||
var processContext = await process.StartAsync(new KernelProcessEvent() { Id = CommonEvents.StartProcess }, processId: processId);
|
||||
var finalState = await processContext.GetStateAsync();
|
||||
|
||||
return this.Ok(processId);
|
||||
}
|
||||
|
||||
private KernelProcess GetProcess()
|
||||
{
|
||||
// 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))
|
||||
.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();
|
||||
|
||||
var process = processBuilder.Build();
|
||||
return process;
|
||||
}
|
||||
|
||||
#pragma warning disable CA1812 // Avoid uninstantiated internal classes
|
||||
// These classes are dynamically instantiated by the processes used in tests.
|
||||
|
||||
/// <summary>
|
||||
/// Kick off step for the process.
|
||||
/// </summary>
|
||||
private sealed class KickoffStep : KernelProcessStep
|
||||
{
|
||||
public static class Functions
|
||||
{
|
||||
public const string KickOff = nameof(KickOff);
|
||||
}
|
||||
|
||||
[KernelFunction(Functions.KickOff)]
|
||||
public async ValueTask PrintWelcomeMessageAsync(KernelProcessStepContext context)
|
||||
{
|
||||
Console.WriteLine("##### Kickoff ran.");
|
||||
await context.EmitEventAsync(new() { Id = CommonEvents.StartARequested, Data = "Get Going" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A step in the process.
|
||||
/// </summary>
|
||||
private sealed class AStep : KernelProcessStep
|
||||
{
|
||||
[KernelFunction]
|
||||
public async ValueTask DoItAsync(KernelProcessStepContext context)
|
||||
{
|
||||
Console.WriteLine("##### AStep ran.");
|
||||
await Task.Delay(TimeSpan.FromSeconds(1));
|
||||
await context.EmitEventAsync(CommonEvents.AStepDone, "I did A");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A step in the process.
|
||||
/// </summary>
|
||||
private sealed class BStep : KernelProcessStep
|
||||
{
|
||||
[KernelFunction]
|
||||
public async ValueTask DoItAsync(KernelProcessStepContext context)
|
||||
{
|
||||
Console.WriteLine("##### BStep ran.");
|
||||
await Task.Delay(TimeSpan.FromSeconds(2));
|
||||
await context.EmitEventAsync(new() { Id = CommonEvents.BStepDone, Data = "I did B" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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;
|
||||
|
||||
// ################ 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!;
|
||||
Console.WriteLine($"##### CStep activated with Cycle = '{state.State?.CurrentCycle}'.");
|
||||
return base.ActivateAsync(state);
|
||||
}
|
||||
|
||||
[KernelFunction]
|
||||
public async ValueTask DoItAsync(KernelProcessStepContext context, string astepdata, string bstepdata)
|
||||
{
|
||||
// ########### This method will restart the process in a loop until CurrentCycle >= 3 ###########
|
||||
this._state!.CurrentCycle++;
|
||||
if (this._state.CurrentCycle >= 3)
|
||||
{
|
||||
// Exit the processes
|
||||
Console.WriteLine("##### CStep run cycle 3 - exiting.");
|
||||
await context.EmitEventAsync(new() { Id = CommonEvents.ExitRequested });
|
||||
return;
|
||||
}
|
||||
|
||||
// Cycle back to the start
|
||||
Console.WriteLine($"##### CStep run cycle {this._state.CurrentCycle}.");
|
||||
await context.EmitEventAsync(new() { Id = CommonEvents.CStepDone });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A state object for the CStep.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
private sealed record CStepState
|
||||
{
|
||||
[DataMember]
|
||||
public int CurrentCycle { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Common Events used in the process.
|
||||
/// </summary>
|
||||
private static class CommonEvents
|
||||
{
|
||||
public const string UserInputReceived = nameof(UserInputReceived);
|
||||
public const string CompletionResponseGenerated = nameof(CompletionResponseGenerated);
|
||||
public const string WelcomeDone = nameof(WelcomeDone);
|
||||
public const string AStepDone = nameof(AStepDone);
|
||||
public const string BStepDone = nameof(BStepDone);
|
||||
public const string CStepDone = nameof(CStepDone);
|
||||
public const string StartARequested = nameof(StartARequested);
|
||||
public const string StartBRequested = nameof(StartBRequested);
|
||||
public const string ExitRequested = nameof(ExitRequested);
|
||||
public const string StartProcess = nameof(StartProcess);
|
||||
}
|
||||
#pragma warning restore CA1812 // Avoid uninstantiated internal classes
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<NoWarn>
|
||||
$(NoWarn);CA2007,CA1861,VSTHRD111,SKEXP0001,SKEXP0010,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0080,SKEXP0110</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Experimental\Process.Abstractions\Process.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Experimental\Process.Core\Process.Core.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Experimental\Process.Runtime.Dapr\Process.Runtime.Dapr.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapr.Actors" />
|
||||
<PackageReference Include="Dapr.Actors.AspNetCore" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel;
|
||||
|
||||
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 Dapr
|
||||
builder.Services.AddActors(static options =>
|
||||
{
|
||||
// Register the actors required to run Processes
|
||||
options.AddProcessActors();
|
||||
});
|
||||
|
||||
builder.Services.AddControllers();
|
||||
var app = builder.Build();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Configure the HTTP request pipeline.
|
||||
app.UseHttpsRedirection();
|
||||
app.UseAuthorization();
|
||||
}
|
||||
|
||||
app.MapControllers();
|
||||
app.MapActorsHandlers();
|
||||
app.Run();
|
||||
@@ -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).
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
Reference in New Issue
Block a user