Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

.Net: Signal R exploration #10056

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dotnet/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<PackageVersion Include="Dapr.Actors.AspNetCore" Version="1.14.0" />
<PackageVersion Include="Dapr.AspNetCore" Version="1.14.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.ML.Tokenizers.Data.Cl100kBase" Version="1.0.0" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.5.1" />
Expand Down
6 changes: 6 additions & 0 deletions dotnet/SK-dotnet.sln
Original file line number Diff line number Diff line change
Expand Up @@ -1164,6 +1164,12 @@ Global
{78785CB1-66CF-4895-D7E5-A440DD84BE86}.Publish|Any CPU.Build.0 = Debug|Any CPU
{78785CB1-66CF-4895-D7E5-A440DD84BE86}.Release|Any CPU.ActiveCfg = Release|Any CPU
{78785CB1-66CF-4895-D7E5-A440DD84BE86}.Release|Any CPU.Build.0 = Release|Any CPU
{D1786E2B-CAA0-4B2D-A974-9845EB9E420F}.Debug|Any CPU.ActiveCfg = Debug
{D1786E2B-CAA0-4B2D-A974-9845EB9E420F}.Debug|Any CPU.Build.0 = Debug
{D1786E2B-CAA0-4B2D-A974-9845EB9E420F}.Publish|Any CPU.ActiveCfg = Debug
{D1786E2B-CAA0-4B2D-A974-9845EB9E420F}.Publish|Any CPU.Build.0 = Debug
{D1786E2B-CAA0-4B2D-A974-9845EB9E420F}.Release|Any CPU.ActiveCfg = Release
{D1786E2B-CAA0-4B2D-A974-9845EB9E420F}.Release|Any CPU.Build.0 = Release
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
8 changes: 8 additions & 0 deletions dotnet/dapr.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apps:
- appDirPath: samples\Demos\ProcessWithDapr
appID: processwithdapr
appPort: 58848
command:
- dotnet
- run
version: 1
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Runtime.Serialization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.SemanticKernel;
using ProcessWithDapr.Processes;

namespace ProcessWithDapr.Controllers;

Expand Down Expand Up @@ -31,176 +31,10 @@ public ProcessController(Kernel kernel)
[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 processContext = await ProcessManager.StartFormFillingProcessAsync(processId);
//var processContext = await ProcessManager.StartProcessAsync(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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) Microsoft. All rights reserved.

using System.ComponentModel.DataAnnotations;

namespace ProcessWithDapr.Extensions;

/// <summary>
/// Class with extension methods for app configuration.
/// </summary>
public static class ConfigurationExtensions
{
/// <summary>
/// Returns <typeparamref name="TOptions"/> if it's valid or throws <see cref="ValidationException"/>.
/// </summary>
public static TOptions GetValid<TOptions>(this IConfigurationRoot configurationRoot, string sectionName)
{
var options = configurationRoot.GetSection(sectionName).Get<TOptions>()!;

Validator.ValidateObject(options, new(options));

return options;
}
}
56 changes: 56 additions & 0 deletions dotnet/samples/Demos/ProcessWithDapr/Hubs/ChatHub.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.SignalR;
using Microsoft.SemanticKernel;

namespace ProcessWithDapr.Hubs;

record ChatMessage
{
[JsonPropertyName("senderId")]
public string SenderId { get; set; }

[JsonPropertyName("content")]
public string Content { get; set; }
}


[EnableCors("CorsPolicy")]
public class ChatHub : Hub
{
private int _counter = 0;
private List<ChatMessage> _messages = [];

public async Task SendUserMessageAsync(string user, string message)
{
this._counter++;
// emiting user message

Check warning on line 30 in dotnet/samples/Demos/ProcessWithDapr/Hubs/ChatHub.cs

View workflow job for this annotation

GitHub Actions / Spell Check with Typos

"emiting" should be "emitting".
ChatMessage userMessage = new() { SenderId = user, Content = message };
this._messages.Add(userMessage);
await this.Clients.All.SendAsync("ReceiveMessage", userMessage.SenderId, userMessage.Content);
await this.Clients.All.SendAsync("onUserMessage", userMessage.Content);
}

public async Task SendAssistantMessageAsync(string message)
{
this._counter++;
// emiting assistant message

Check warning on line 40 in dotnet/samples/Demos/ProcessWithDapr/Hubs/ChatHub.cs

View workflow job for this annotation

GitHub Actions / Spell Check with Typos

"emiting" should be "emitting".
ChatMessage userMessage = new() { SenderId = "Assistant", Content = message };
this._messages.Add(userMessage);
await this.Clients.All.SendAsync("ReceiveMessage", userMessage.SenderId, userMessage.Content);
}

public async Task JoinConversationAsync(string conversationId)
{
await this.Clients.All.SendAsync("ConversationHistory", this._messages);
}

public async Task ResetConversationAsync(string conversationId)
{
this._messages.Clear();
await this.Clients.All.SendAsync("ConversationReset");
}
}
19 changes: 19 additions & 0 deletions dotnet/samples/Demos/ProcessWithDapr/Options/OpenAIOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft. All rights reserved.

using System.ComponentModel.DataAnnotations;

namespace ProcessWithDapr.Options;

/// <summary>
/// Configuration for OpenAI chat completion service.
/// </summary>
public class OpenAIOptions
{
public const string SectionName = "OpenAI";

[Required]
public string ChatModelId { get; set; }

[Required]
public string ApiKey { get; set; }
}
3 changes: 3 additions & 0 deletions dotnet/samples/Demos/ProcessWithDapr/ProcessWithDapr.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>
$(NoWarn);CA2007,CA1861,VSTHRD111,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0080,SKEXP0110</NoWarn>
<UserSecretsId>5ee045b0-aea3-4f08-8d31-32d1a6f8fed0</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\src\Connectors\Connectors.OpenAI\Connectors.OpenAI.csproj" />
<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" />
Expand All @@ -17,6 +19,7 @@
<ItemGroup>
<PackageReference Include="Dapr.Actors" />
<PackageReference Include="Dapr.Actors.AspNetCore" />
<PackageReference Include="Dapr.AspNetCore" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) Microsoft. All rights reserved.

using Microsoft.SemanticKernel;

namespace ProcessWithDapr.Processes;

public interface IProcessManager
{
Task<DaprKernelProcessContext> StartProcessAsync(string processId);
DaprKernelProcessContext? GetProcessContext(string processId);
}
Loading
Loading