diff --git a/.vscode/settings.json b/.vscode/settings.json index 3dc48d0f6e75..2372e2822947 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,6 @@ { "prettier.enable": true, - "css.lint.validProperties": [ - "composes" - ], + "css.lint.validProperties": ["composes"], "editor.formatOnType": true, "editor.formatOnSave": true, "editor.formatOnPaste": true, @@ -15,13 +13,9 @@ "editor.bracketPairColorization.enabled": true, "editor.guides.bracketPairs": "active", "python.formatting.provider": "autopep8", - "python.formatting.autopep8Args": [ - "--max-line-length=120" - ], + "python.formatting.autopep8Args": ["--max-line-length=120"], "notebook.output.textLineLimit": 500, - "python.analysis.extraPaths": [ - "./python/src" - ], + "python.analysis.extraPaths": ["./python/src"], "javascript.updateImportsOnFileMove.enabled": "always", "search.exclude": { "**/node_modules": true, @@ -71,31 +65,34 @@ "**/Thumbs.db": true }, "cSpell.words": [ + "Dapr", "Partitioner", + "Pregel", "Prompty", - "SKEXP" + "SKEXP", + "superstep", + "Supersteps", + "typeref" ], "[java]": { "editor.formatOnSave": false, "editor.tabSize": 4, "editor.codeActionsOnSave": { "source.fixAll": "never" - }, + } }, "emeraldwalk.runonsave": { "commands": [ { "match": "\\.java$", "cmd": "java -Xmx128m -jar ${workspaceFolder}/java/utilities/google-java-format-1.17.0-all-deps.jar --replace --aosp ${file}" - }, - ], + } + ] }, "java.debug.settings.onBuildFailureProceed": true, "java.compile.nullAnalysis.mode": "disabled", "dotnet.defaultSolution": "dotnet\\SK-dotnet.sln", - "python.testing.pytestArgs": [ - "python/tests" - ], + "python.testing.pytestArgs": ["python/tests"], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true -} \ No newline at end of file +} diff --git a/dotnet/samples/GettingStartedWithProcesses/SharedSteps/ScriptedUserInputStep.cs b/dotnet/samples/GettingStartedWithProcesses/SharedSteps/ScriptedUserInputStep.cs index c1908b8e8cdf..8ece418f3070 100644 --- a/dotnet/samples/GettingStartedWithProcesses/SharedSteps/ScriptedUserInputStep.cs +++ b/dotnet/samples/GettingStartedWithProcesses/SharedSteps/ScriptedUserInputStep.cs @@ -41,10 +41,9 @@ public virtual void PopulateUserInputs(UserInputState state) /// A public override ValueTask ActivateAsync(KernelProcessStepState state) { - state.State ??= new(); _state = state.State; - PopulateUserInputs(_state); + PopulateUserInputs(_state!); return ValueTask.CompletedTask; } diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessEdge.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessEdge.cs index e474df9658c6..224d5b67bb56 100644 --- a/dotnet/src/Experimental/Process.Abstractions/KernelProcessEdge.cs +++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessEdge.cs @@ -1,31 +1,37 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Runtime.Serialization; + namespace Microsoft.SemanticKernel; /// /// A serializable representation of an edge between a source and a . /// +[DataContract] +[KnownType(typeof(KernelProcessFunctionTarget))] public sealed class KernelProcessEdge { /// /// The unique identifier of the source Step. /// - public string SourceStepId { get; } + [DataMember] + public string SourceStepId { get; init; } /// /// The collection of s that are the output of the source Step. /// - public KernelProcessFunctionTarget OutputTarget { get; } + [DataMember] + public KernelProcessFunctionTarget OutputTarget { get; init; } /// /// Creates a new instance of the class. /// - public KernelProcessEdge(string sourceStepId, KernelProcessFunctionTarget outputTargets) + public KernelProcessEdge(string sourceStepId, KernelProcessFunctionTarget outputTarget) { Verify.NotNullOrWhiteSpace(sourceStepId); - Verify.NotNull(outputTargets); + Verify.NotNull(outputTarget); this.SourceStepId = sourceStepId; - this.OutputTarget = outputTargets; + this.OutputTarget = outputTarget; } } diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessFunctionTarget.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessFunctionTarget.cs index 8a388347f35c..025382352709 100644 --- a/dotnet/src/Experimental/Process.Abstractions/KernelProcessFunctionTarget.cs +++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessFunctionTarget.cs @@ -1,10 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Runtime.Serialization; + namespace Microsoft.SemanticKernel; /// /// A serializable representation of a specific parameter of a specific function of a specific Step. /// +[DataContract] public record KernelProcessFunctionTarget { /// @@ -24,20 +27,24 @@ public KernelProcessFunctionTarget(string stepId, string functionName, string? p /// /// The unique identifier of the Step being targeted. /// + [DataMember] public string StepId { get; init; } /// /// The name if the Kernel Function to target. /// + [DataMember] public string FunctionName { get; init; } /// /// The name of the parameter to target. This may be null if the function has no parameters. /// + [DataMember] public string? ParameterName { get; init; } /// /// The unique identifier for the event to target. This may be null if the target is not a sub-process. /// + [DataMember] public string? TargetEventId { get; init; } } diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessState.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessState.cs index bb09b2068195..dc0641c24030 100644 --- a/dotnet/src/Experimental/Process.Abstractions/KernelProcessState.cs +++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessState.cs @@ -1,14 +1,17 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Runtime.Serialization; + namespace Microsoft.SemanticKernel; /// /// Represents the state of a process. /// +[DataContract] public sealed record KernelProcessState : KernelProcessStepState { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The name of the associated /// The Id of the associated diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepInfo.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepInfo.cs index 7273ccf875ea..7665a5bdcc58 100644 --- a/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepInfo.cs +++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepInfo.cs @@ -54,5 +54,8 @@ public KernelProcessStepInfo(Type innerStepType, KernelProcessStepState state, D this.InnerStepType = innerStepType; this._outputEdges = edges; this._state = state; + + // Register the state as a know type for the DataContractSerialization used by Dapr. + KernelProcessState.RegisterDerivedType(state.GetType()); } } diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepState.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepState.cs index fb90d70f8d2a..f38eb23da397 100644 --- a/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepState.cs +++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepState.cs @@ -1,22 +1,51 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + namespace Microsoft.SemanticKernel; /// /// Represents the state of an individual step in a process. /// +[DataContract] +[KnownType(nameof(GetKnownTypes))] public record KernelProcessStepState { + /// + /// A set of known types that may be used in serialization. + /// + private readonly static HashSet s_knownTypes = []; + + /// + /// Used to dynamically provide the set of known types for serialization. + /// + /// + private static HashSet GetKnownTypes() => s_knownTypes; + + /// + /// Registers a derived type for serialization. Types registered here are used by the KnownType attribute + /// to support DataContractSerialization of derived types as required to support Dapr. + /// + /// A Type that derives from + internal static void RegisterDerivedType(Type derivedType) + { + s_knownTypes.Add(derivedType); + } + /// /// The identifier of the Step which is required to be unique within an instance of a Process. /// This may be null until a process containing this step has been invoked. /// + [DataMember] public string? Id { get; init; } /// - /// The name of the Step. This is itended to be human readable and is not required to be unique. If + /// 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; } /// @@ -37,12 +66,14 @@ public KernelProcessStepState(string name, string? id = null) /// Represents the state of an individual step in a process that includes a user-defined state object. /// /// The type of the user-defined state. +[DataContract] public sealed record KernelProcessStepState : KernelProcessStepState where TState : class, new() { /// /// The user-defined state object associated with the Step. /// - public TState? State { get; set; } + [DataMember] + public TState? State { get; init; } /// /// Initializes a new instance of the class. diff --git a/dotnet/src/Experimental/Process.Core/Internal/EndStep.cs b/dotnet/src/Experimental/Process.Core/Internal/EndStep.cs index 287a2b488f1d..1eefc586bdc2 100644 --- a/dotnet/src/Experimental/Process.Core/Internal/EndStep.cs +++ b/dotnet/src/Experimental/Process.Core/Internal/EndStep.cs @@ -9,7 +9,7 @@ namespace Microsoft.SemanticKernel; /// internal sealed class EndStep : ProcessStepBuilder { - private const string EndStepValue = "END"; + private const string EndStepValue = "Microsoft.SemanticKernel.Process.EndStep"; /// /// The name of the end step. diff --git a/dotnet/src/Experimental/Process.Core/ProcessBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessBuilder.cs index fa509daebfd5..13c020150bc2 100644 --- a/dotnet/src/Experimental/Process.Core/ProcessBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/ProcessBuilder.cs @@ -115,6 +115,22 @@ public ProcessStepBuilder AddStepFromType(string? name = null) where TSte return stepBuilder; } + /// + /// Adds a step to the process and define it's initial user-defined state. + /// + /// The step Type. + /// The state Type. + /// The initial state of the step. + /// The name of the step. This parameter is optional. + /// An instance of + public ProcessStepBuilder AddStepFromType(TState initialState, string? name = null) where TStep : KernelProcessStep where TState : class, new() + { + var stepBuilder = new ProcessStepBuilder(name, initialState); + this._steps.Add(stepBuilder); + + return stepBuilder; + } + /// /// Adds a sub process to the process. /// diff --git a/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs index 4434a81c76f8..d9dc5098429f 100644 --- a/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs @@ -194,13 +194,21 @@ protected ProcessStepBuilder(string name) /// public sealed class ProcessStepBuilder : ProcessStepBuilder where TStep : KernelProcessStep { + /// + /// The initial state of the step. This may be null if the step does not have any state. + /// + private readonly object? _initialState; + /// /// Creates a new instance of the class. If a name is not provided, the name will be derived from the type of the step. /// - public ProcessStepBuilder(string? name = null) + /// Optional: The name of the step. + /// Optional: The initial state of the step. + internal ProcessStepBuilder(string? name = null, object? initialState = default) : base(name ?? typeof(TStep).Name) { this.FunctionsDict = this.GetFunctionMetadataMap(); + this._initialState = initialState; } /// @@ -221,7 +229,14 @@ internal override KernelProcessStepInfo BuildStep() var stateType = typeof(KernelProcessStepState<>).MakeGenericType(userStateType); Verify.NotNull(stateType); + // 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}."); + } + stateObject = (KernelProcessStepState?)Activator.CreateInstance(stateType, this.Name, this.Id); + stateType.GetProperty(nameof(KernelProcessStepState.State))?.SetValue(stateObject, this._initialState); } else { diff --git a/dotnet/src/Experimental/Process.LocalRuntime/LocalProcess.cs b/dotnet/src/Experimental/Process.LocalRuntime/LocalProcess.cs index cab0d77e81f1..9b284d697b7d 100644 --- a/dotnet/src/Experimental/Process.LocalRuntime/LocalProcess.cs +++ b/dotnet/src/Experimental/Process.LocalRuntime/LocalProcess.cs @@ -15,7 +15,7 @@ namespace Microsoft.SemanticKernel; internal sealed class LocalProcess : LocalStep, IDisposable { - private const string EndProcessId = "END"; + private const string EndProcessId = "Microsoft.SemanticKernel.Process.EndStep"; private readonly JoinableTaskFactory _joinableTaskFactory; private readonly JoinableTaskContext _joinableTaskContext; private readonly Channel _externalEventChannel; @@ -304,7 +304,7 @@ private async Task Internal_ExecuteAsync(Kernel? kernel = null, int maxSuperstep /// /// 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 processesed in the next superstep. + /// them to the provided message channel so that they can be processed in the next superstep. /// /// The message channel where messages should be enqueued. private void EnqueueExternalMessages(Queue messageChannel) @@ -324,7 +324,7 @@ private void EnqueueExternalMessages(Queue messageChannel) /// /// Processes events emitted by the given step in the last superstep, translates them to s, and enqueues - /// them to the provided message channel so that they can be processesed in the next superstep. + /// them to the provided message channel so that they can be processed in the next superstep. /// /// The step containing outgoing events to process. /// The message channel where messages should be enqueued. diff --git a/dotnet/src/Experimental/Process.LocalRuntime/LocalStep.cs b/dotnet/src/Experimental/Process.LocalRuntime/LocalStep.cs index 3d6ee8b21cf7..acdd66c556a1 100644 --- a/dotnet/src/Experimental/Process.LocalRuntime/LocalStep.cs +++ b/dotnet/src/Experimental/Process.LocalRuntime/LocalStep.cs @@ -231,7 +231,7 @@ protected virtual async ValueTask InitializeStepAsync() 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; + KernelProcessStepState stateObject = this._stepInfo.State; Type? stateType = null; if (TryGetSubtypeOfStatefulStep(this._stepInfo.InnerStepType, out Type? genericStepType) && genericStepType is not null) @@ -254,13 +254,16 @@ protected virtual async ValueTask InitializeStepAsync() throw new KernelException(errorMessage); } - stateObject = (KernelProcessStepState?)Activator.CreateInstance(stateType, this.Name, this.Id); + var userState = stateType.GetProperty(nameof(KernelProcessStepState.State))?.GetValue(stateObject); + if (userState is null) + { + stateType.GetProperty(nameof(KernelProcessStepState.State))?.SetValue(stateObject, Activator.CreateInstance(userStateType)); + } } else { // The step is a KernelProcessStep with no user-defined state, so we can use the base KernelProcessStepState. stateType = typeof(KernelProcessStepState); - stateObject = new KernelProcessStepState(this.Name, this.Id); } if (stateObject is null) diff --git a/dotnet/src/IntegrationTests/Processes/ProcessTests.cs b/dotnet/src/IntegrationTests/Processes/ProcessTests.cs index c1c426f66679..df8847d89620 100644 --- a/dotnet/src/IntegrationTests/Processes/ProcessTests.cs +++ b/dotnet/src/IntegrationTests/Processes/ProcessTests.cs @@ -285,12 +285,11 @@ public string Echo(string message) /// private sealed class RepeatStep : KernelProcessStep { - private readonly StepState _state = new(); + private StepState? _state; public override ValueTask ActivateAsync(KernelProcessStepState state) { - state.State ??= this._state; - + this._state = state.State; return default; } @@ -299,7 +298,7 @@ public async Task RepeatAsync(string message, KernelProcessStepContext context, { var output = string.Join(" ", Enumerable.Repeat(message, count)); Console.WriteLine($"[REPEAT] {output}"); - this._state.LastMessage = output; + this._state!.LastMessage = output; // Emit the OnReady event with a public visibility and an internal visibility to aid in testing await context.EmitEventAsync(new() { Id = ProcessTestsEvents.OutputReadyPublic, Data = output, Visibility = KernelProcessEventVisibility.Public }); @@ -330,12 +329,11 @@ await context.EmitEventAsync(new() /// private sealed class FanInStep : KernelProcessStep { - private readonly StepState _state = new(); + private StepState? _state; public override ValueTask ActivateAsync(KernelProcessStepState state) { - state.State ??= this._state; - + this._state = state.State; return default; } @@ -344,7 +342,7 @@ public async Task EmitCombinedMessageAsync(KernelProcessStepContext context, str { var output = $"{firstInput}-{secondInput}"; Console.WriteLine($"[EMIT_COMBINED] {output}"); - this._state.LastMessage = output; + this._state!.LastMessage = output; await context.EmitEventAsync(new() {