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 Processes: Updated to process abstractions and core builders in preparation for Dapr runtime #9285

Merged
merged 8 commits into from
Oct 16, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ public virtual void PopulateUserInputs(UserInputState state)
/// <returns>A <see cref="ValueTask"/></returns>
public override ValueTask ActivateAsync(KernelProcessStepState<UserInputState> state)
{
state.State ??= new();
_state = state.State;
_state = state.State ?? new();
alliscode marked this conversation as resolved.
Show resolved Hide resolved

PopulateUserInputs(_state);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,37 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Runtime.Serialization;

namespace Microsoft.SemanticKernel;

/// <summary>
/// A serializable representation of an edge between a source <see cref="KernelProcessStep"/> and a <see cref="KernelProcessFunctionTarget"/>.
/// </summary>
[DataContract]
alliscode marked this conversation as resolved.
Show resolved Hide resolved
[KnownType(typeof(KernelProcessFunctionTarget))]
public sealed class KernelProcessEdge
{
/// <summary>
/// The unique identifier of the source Step.
/// </summary>
public string SourceStepId { get; }
[DataMember(Name = "sourceStepId")]
public string SourceStepId { get; init; }

/// <summary>
/// The collection of <see cref="KernelProcessFunctionTarget"/>s that are the output of the source Step.
/// </summary>
public KernelProcessFunctionTarget OutputTarget { get; }
[DataMember(Name = "outputTarget")]
public KernelProcessFunctionTarget OutputTarget { get; init; }

/// <summary>
/// Creates a new instance of the <see cref="KernelProcessEdge"/> class.
/// </summary>
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;
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Runtime.Serialization;

namespace Microsoft.SemanticKernel;

/// <summary>
/// A serializable representation of a specific parameter of a specific function of a specific Step.
/// </summary>
[DataContract]
public record KernelProcessFunctionTarget
{
/// <summary>
Expand All @@ -24,20 +27,24 @@ public KernelProcessFunctionTarget(string stepId, string functionName, string? p
/// <summary>
/// The unique identifier of the Step being targeted.
/// </summary>
[DataMember(Name = "stepId")]
public string StepId { get; init; }

/// <summary>
/// The name if the Kernel Function to target.
/// </summary>
[DataMember(Name = "functionName")]
public string FunctionName { get; init; }

/// <summary>
/// The name of the parameter to target. This may be null if the function has no parameters.
/// </summary>
[DataMember(Name = "parameterName")]
public string? ParameterName { get; init; }

/// <summary>
/// The unique identifier for the event to target. This may be null if the target is not a sub-process.
/// </summary>
[DataMember(Name = "targetEventId")]
public string? TargetEventId { get; init; }
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Runtime.Serialization;

namespace Microsoft.SemanticKernel;

/// <summary>
/// Represents the state of a process.
/// </summary>
[DataContract]
public sealed record KernelProcessState : KernelProcessStepState
{
/// <summary>
/// Initializes a new instance of the <see cref="KernelProcessStepState"/> class.
/// Initializes a new instance of the <see cref="KernelProcessState"/> class.
/// </summary>
/// <param name="name">The name of the associated <see cref="KernelProcessStep"/></param>
/// <param name="id">The Id of the associated <see cref="KernelProcessStep"/></param>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,41 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Collections.Generic;
using System.Runtime.Serialization;

namespace Microsoft.SemanticKernel;

/// <summary>
/// Represents the state of an individual step in a process.
/// </summary>
[DataContract]
[KnownType(nameof(GetKnownTypes))]
public record KernelProcessStepState
{
/// <summary>
/// A set of known types that may be used in serialization.
/// </summary>
private readonly static HashSet<Type> s_knownTypes = [];

/// <summary>
/// Used to dynamically provide the set of known types for serialization.
/// </summary>
/// <returns></returns>
private static HashSet<Type> GetKnownTypes() => s_knownTypes;
alliscode marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// 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.
/// </summary>
[DataMember(Name = "id")]
public string? Id { get; init; }

/// <summary>
/// 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.
/// </summary>
[DataMember(Name = "name")]
public string Name { get; init; }

/// <summary>
Expand All @@ -31,18 +50,30 @@ public KernelProcessStepState(string name, string? id = null)
this.Id = id;
this.Name = name;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="derivedType">A Type that derives from <typeref name="KernelProcessStepState"/></param>
public static void RegisterDerivedType(Type derivedType)
alliscode marked this conversation as resolved.
Show resolved Hide resolved
{
s_knownTypes.Add(derivedType);
}
}

/// <summary>
/// Represents the state of an individual step in a process that includes a user-defined state object.
/// </summary>
/// <typeparam name="TState">The type of the user-defined state.</typeparam>
[DataContract]
public sealed record KernelProcessStepState<TState> : KernelProcessStepState where TState : class, new()
{
/// <summary>
/// The user-defined state object associated with the Step.
/// </summary>
public TState? State { get; set; }
[DataMember]
public TState? State { get; init; }
alliscode marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Initializes a new instance of the <see cref="KernelProcessStepState"/> class.
Expand Down
2 changes: 1 addition & 1 deletion dotnet/src/Experimental/Process.Core/Internal/EndStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace Microsoft.SemanticKernel;
/// </summary>
internal sealed class EndStep : ProcessStepBuilder
{
private const string EndStepValue = "END";
private const string EndStepValue = "Microsoft.SemanticKernel.Process.EndStep";

/// <summary>
/// The name of the end step.
Expand Down
33 changes: 33 additions & 0 deletions dotnet/src/Experimental/Process.Core/ProcessBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.SemanticKernel.Process;

namespace Microsoft.SemanticKernel;

Expand Down Expand Up @@ -109,12 +110,44 @@ internal override KernelProcessStepInfo BuildStep()
/// <returns>An instance of <see cref="ProcessStepBuilder"/></returns>
public ProcessStepBuilder AddStepFromType<TStep>(string? name = null) where TStep : KernelProcessStep
{
// If the step has a user-defined state then we need to register it as a know type for the DataContractSerialization used by Dapr.
if (typeof(TStep).TryGetSubtypeOfStatefulStep(out Type? genericStepType) && genericStepType is not null)
{
// The step is a subclass of KernelProcessStep<>, so we need to extract the generic type argument
// and create an instance of the corresponding KernelProcessStepState<>.
var userStateType = genericStepType.GetGenericArguments()[0];
Verify.NotNull(userStateType);

var stateType = typeof(KernelProcessStepState<>).MakeGenericType(userStateType);
KernelProcessState.RegisterDerivedType(stateType);
}

var stepBuilder = new ProcessStepBuilder<TStep>(name);
this._steps.Add(stepBuilder);

return stepBuilder;
}

/// <summary>
/// Adds a step to the process and define it's initial user-defined state.
/// </summary>
/// <typeparam name="TStep">The step Type.</typeparam>
/// <typeparam name="TState">The state Type.</typeparam>
/// <param name="initialState">The initial state of the step.</param>
/// <param name="name">The name of the step. This parameter is optional.</param>
/// <returns>An instance of <see cref="ProcessStepBuilder"/></returns>
public ProcessStepBuilder AddStepFromType<TStep, TState>(TState initialState, string? name = null) where TStep : KernelProcessStep<TState> where TState : class, new()
{
// The step has a user-defined state so we need to register it as a know type for the DataContractSerialization used by Dapr.
var stateType = typeof(KernelProcessStepState<TState>);
KernelProcessState.RegisterDerivedType(stateType);

var stepBuilder = new ProcessStepBuilder<TStep>(name, initialState);
this._steps.Add(stepBuilder);

return stepBuilder;
}

/// <summary>
/// Adds a sub process to the process.
/// </summary>
Expand Down
17 changes: 16 additions & 1 deletion dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,13 +194,21 @@ protected ProcessStepBuilder(string name)
/// </summary>
public sealed class ProcessStepBuilder<TStep> : ProcessStepBuilder where TStep : KernelProcessStep
{
/// <summary>
/// The initial state of the step. This may be null if the step does not have any state.
/// </summary>
private readonly object? _initialState;

/// <summary>
/// Creates a new instance of the <see cref="ProcessStepBuilder"/> class. If a name is not provided, the name will be derived from the type of the step.
/// </summary>
public ProcessStepBuilder(string? name = null)
/// <param name="name">Optional: The name of the step.</param>
/// <param name="initialState">Optional: The initial state of the step.</param>
public ProcessStepBuilder(string? name = null, object? initialState = default)
alliscode marked this conversation as resolved.
Show resolved Hide resolved
: base(name ?? typeof(TStep).Name)
{
this.FunctionsDict = this.GetFunctionMetadataMap();
this._initialState = initialState;
}

/// <summary>
Expand All @@ -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<object>.State))?.SetValue(stateObject, this._initialState);
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<KernelProcessEvent> _externalEventChannel;
Expand Down
10 changes: 7 additions & 3 deletions dotnet/src/Experimental/Process.LocalRuntime/LocalStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -254,13 +254,17 @@ protected virtual async ValueTask InitializeStepAsync()
throw new KernelException(errorMessage);
}

stateObject = (KernelProcessStepState?)Activator.CreateInstance(stateType, this.Name, this.Id);
var userState = stateType.GetProperty(nameof(KernelProcessStepState<object>.State))?.GetValue(stateObject);
if (userState is null)
{
stateType.GetProperty(nameof(KernelProcessStepState<object>.State))?.SetValue(stateObject, Activator.CreateInstance(userStateType));
alliscode marked this conversation as resolved.
Show resolved Hide resolved
}
}
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);
//stateObject = new KernelProcessStepState(this.Name, this.Id);
alliscode marked this conversation as resolved.
Show resolved Hide resolved
}

if (stateObject is null)
Expand Down
14 changes: 6 additions & 8 deletions dotnet/src/IntegrationTests/Processes/ProcessTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -285,12 +285,11 @@ public string Echo(string message)
/// </summary>
private sealed class RepeatStep : KernelProcessStep<StepState>
{
private readonly StepState _state = new();
private StepState? _state;

public override ValueTask ActivateAsync(KernelProcessStepState<StepState> state)
{
state.State ??= this._state;

this._state = state.State;
return default;
}

Expand All @@ -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 });
Expand Down Expand Up @@ -330,12 +329,11 @@ await context.EmitEventAsync(new()
/// </summary>
private sealed class FanInStep : KernelProcessStep<StepState>
{
private readonly StepState _state = new();
private StepState? _state;

public override ValueTask ActivateAsync(KernelProcessStepState<StepState> state)
{
state.State ??= this._state;

this._state = state.State;
return default;
}

Expand All @@ -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()
{
Expand Down
Loading