Skip to content

Commit 15b94c1

Browse files
alliscodeBen Thomas
andauthored
.NET: Processes support for sub-processes (microsoft#9095)
### Description #### Enables use of sub-processes by adding a process to another process as a step. ##### Event bubbling and visibility: This PR adds the concept of visibility to events within a process. There are two options, `Internal` which keeps the event within the process it's fired in, and `Public` which allows the event to bubble out of the process to parent processes or external systems. Events default to `Internal`. This allows a parent process to receive events that originate from within a sub-process. ##### Targets for process' external events: Processes now expose targets for their external events. When a processes defines a route for an external event by calling `processBuilder.OnExternalEvent(...)...`, the target for this event can now be retrieved by calling `process.GetTargetForExternalEvent(...)`. This allows a parent process to route a step event to the specified entry event of the sub-process. #### Retrieve state of a running or completed process The `*KernelProcessContext` now exposes a `GetStateAsync` method that allows the state of the process to be retrieved on a started process. Closes microsoft#9097 ### 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]>
1 parent 81953f2 commit 15b94c1

20 files changed

+652
-127
lines changed

dotnet/samples/GettingStartedWithProcesses/Step01/Step01_Processes.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,17 @@ public async Task UseSimpleProcessAsync()
3838

3939
// Define the behavior when the process receives an external event
4040
process
41-
.OnExternalEvent(ChatBotEvents.StartProcess)
41+
.OnInputEvent(ChatBotEvents.StartProcess)
4242
.SendEventTo(new ProcessFunctionTargetBuilder(introStep));
4343

4444
// When the intro is complete, notify the userInput step
4545
introStep
4646
.OnFunctionResult(nameof(IntroStep.PrintIntroMessage))
4747
.SendEventTo(new ProcessFunctionTargetBuilder(userInputStep));
4848

49-
// When the userInput step emits an exit event, send it to the end steprt
49+
// When the userInput step emits an exit event, send it to the end step
5050
userInputStep
51-
.OnFunctionResult("GetUserInput")
51+
.OnEvent(ChatBotEvents.Exit)
5252
.StopProcess();
5353

5454
// When the userInput step emits a user input event, send it to the assistantResponse step

dotnet/samples/GettingStartedWithProcesses/Step02/Step02_AccountOpening.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ private KernelProcess SetupAccountOpeningProcess<TUserInputStep>() where TUserIn
3030
var crmRecordStep = process.AddStepFromType<CRMRecordCreationStep>();
3131
var welcomePacketStep = process.AddStepFromType<WelcomePacketStep>();
3232

33-
process.OnExternalEvent(AccountOpeningEvents.StartProcess)
33+
process.OnInputEvent(AccountOpeningEvents.StartProcess)
3434
.SendEventTo(new ProcessFunctionTargetBuilder(newCustomerFormStep, CompleteNewCustomerFormStep.Functions.NewAccountWelcome));
3535

3636
// When the welcome message is generated, send message to displayAssistantMessageStep

dotnet/src/Experimental/Process.Abstractions/KernelProcessEvent.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,9 @@ public sealed record KernelProcessEvent
1616
/// An optional data payload associated with the event.
1717
/// </summary>
1818
public object? Data { get; set; }
19+
20+
/// <summary>
21+
/// The visibility of the event. Defaults to <see cref="KernelProcessEventVisibility.Internal"/>.
22+
/// </summary>
23+
public KernelProcessEventVisibility Visibility { get; set; } = KernelProcessEventVisibility.Internal;
1924
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
namespace Microsoft.SemanticKernel;
4+
5+
/// <summary>
6+
/// An enumeration representing the visibility of a <see cref="KernelProcessEvent"/>. This is used to determine
7+
/// if the event is kept within the process it's emitted in, or exposed to external processes and systems.
8+
/// </summary>
9+
public enum KernelProcessEventVisibility
10+
{
11+
/// <summary>
12+
/// The event is only visible to steps within the same process.
13+
/// </summary>
14+
Internal,
15+
16+
/// <summary>
17+
/// The event is visible inside the process as well as outside the process. This is useful
18+
/// when the event is intended to be consumed by other processes or external systems.
19+
/// </summary>
20+
Public
21+
}

dotnet/src/Experimental/Process.Abstractions/KernelProcessFunctionTarget.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ public record KernelProcessFunctionTarget
1010
/// <summary>
1111
/// Creates an instance of the <see cref="KernelProcessFunctionTarget"/> class.
1212
/// </summary>
13-
public KernelProcessFunctionTarget(string stepId, string functionName, string? parameterName = null)
13+
public KernelProcessFunctionTarget(string stepId, string functionName, string? parameterName = null, string? targetEventId = null)
1414
{
1515
Verify.NotNullOrWhiteSpace(stepId);
1616
Verify.NotNullOrWhiteSpace(functionName);
1717

1818
this.StepId = stepId;
1919
this.FunctionName = functionName;
2020
this.ParameterName = parameterName;
21+
this.TargetEventId = targetEventId;
2122
}
2223

2324
/// <summary>
@@ -34,4 +35,9 @@ public KernelProcessFunctionTarget(string stepId, string functionName, string? p
3435
/// The name of the parameter to target. This may be null if the function has no parameters.
3536
/// </summary>
3637
public string? ParameterName { get; init; }
38+
39+
/// <summary>
40+
/// The unique identifier for the event to target. This may be null if the target is not a sub-process.
41+
/// </summary>
42+
public string? TargetEventId { get; init; }
3743
}

dotnet/src/Experimental/Process.Core/Internal/EndStep.cs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,6 @@ internal EndStep()
3434
{
3535
}
3636

37-
internal override string GetScopedEventId(string eventId)
38-
{
39-
// No event scoping for the end step.
40-
return eventId;
41-
}
42-
4337
internal override Dictionary<string, KernelFunctionMetadata> GetFunctionMetadataMap()
4438
{
4539
// The end step has no functions.

dotnet/src/Experimental/Process.Core/ProcessBuilder.cs

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,19 @@ namespace Microsoft.SemanticKernel;
1111
/// </summary>
1212
public sealed class ProcessBuilder : ProcessStepBuilder
1313
{
14-
private readonly List<ProcessStepBuilder> _steps;
15-
private readonly List<ProcessStepBuilder> _entrySteps;
16-
private readonly Dictionary<string, ProcessStepBuilder> _stepsMap;
14+
/// <summary>The collection of steps within this process.</summary>
15+
private readonly List<ProcessStepBuilder> _steps = [];
16+
17+
/// <summary>The collection of entry steps within this process.</summary>
18+
private readonly List<ProcessStepBuilder> _entrySteps = [];
19+
20+
/// <summary>Maps external event Ids to the target entry step for the event.</summary>
21+
private readonly Dictionary<string, ProcessFunctionTargetBuilder> _externalEventTargetMap = [];
22+
23+
/// <summary>
24+
/// A boolean indicating if the current process is a step within another process.
25+
/// </summary>
26+
internal bool HasParentProcess { get; set; }
1727

1828
/// <summary>
1929
/// Used to resolve the target function and parameter for a given optional function name and parameter name.
@@ -56,18 +66,15 @@ internal override KernelProcessFunctionTarget ResolveFunctionTarget(string? func
5666
/// <inheritdoc/>
5767
internal override void LinkTo(string eventId, ProcessStepEdgeBuilder edgeBuilder)
5868
{
69+
Verify.NotNull(edgeBuilder?.Source, nameof(edgeBuilder.Source));
70+
Verify.NotNull(edgeBuilder?.Target, nameof(edgeBuilder.Target));
71+
5972
// Keep track of the entry point steps
6073
this._entrySteps.Add(edgeBuilder.Source);
74+
this._externalEventTargetMap[eventId] = edgeBuilder.Target;
6175
base.LinkTo(eventId, edgeBuilder);
6276
}
6377

64-
/// <inheritdoc/>
65-
internal override string GetScopedEventId(string eventId)
66-
{
67-
// The event id is scoped to the process name
68-
return $"{this.Name}.{eventId}";
69-
}
70-
7178
/// <inheritdoc/>
7279
internal override Dictionary<string, KernelFunctionMetadata> GetFunctionMetadataMap()
7380
{
@@ -104,7 +111,6 @@ public ProcessStepBuilder AddStepFromType<TStep>(string? name = null) where TSte
104111
{
105112
var stepBuilder = new ProcessStepBuilder<TStep>(name);
106113
this._steps.Add(stepBuilder);
107-
this._stepsMap[stepBuilder.Name] = stepBuilder;
108114

109115
return stepBuilder;
110116
}
@@ -114,15 +120,10 @@ public ProcessStepBuilder AddStepFromType<TStep>(string? name = null) where TSte
114120
/// </summary>
115121
/// <param name="kernelProcess">The process to add as a step.</param>
116122
/// <returns>An instance of <see cref="ProcessStepBuilder"/></returns>
117-
public ProcessStepBuilder AddStepFromProcess(ProcessBuilder kernelProcess)
123+
public ProcessBuilder AddStepFromProcess(ProcessBuilder kernelProcess)
118124
{
119-
// TODO: Could this method be converted to an "AddStepFromObject" method takes an
120-
// instance of ProcessStepBase and adds it to the process?
121-
// This would work for processes.
122-
// This would benefit steps because the initial value of state could be captured?
123-
125+
kernelProcess.HasParentProcess = true;
124126
this._steps.Add(kernelProcess);
125-
this._stepsMap[kernelProcess.Name] = kernelProcess;
126127
return kernelProcess;
127128
}
128129

@@ -132,11 +133,31 @@ public ProcessStepBuilder AddStepFromProcess(ProcessBuilder kernelProcess)
132133
/// </summary>
133134
/// <param name="eventId">The Id of the external event.</param>
134135
/// <returns>An instance of <see cref="ProcessStepEdgeBuilder"/></returns>
135-
public ProcessEdgeBuilder OnExternalEvent(string eventId)
136+
public ProcessEdgeBuilder OnInputEvent(string eventId)
136137
{
137138
return new ProcessEdgeBuilder(this, eventId);
138139
}
139140

141+
/// <summary>
142+
/// Retrieves the target for a given external event. The step associated with the target is the process itself (this).
143+
/// </summary>
144+
/// <param name="eventId">The Id of the event</param>
145+
/// <returns>An instance of <see cref="ProcessFunctionTargetBuilder"/></returns>
146+
/// <exception cref="KernelException"></exception>
147+
public ProcessFunctionTargetBuilder WhereInputEventIs(string eventId)
148+
{
149+
Verify.NotNullOrWhiteSpace(eventId);
150+
151+
if (!this._externalEventTargetMap.TryGetValue(eventId, out var target))
152+
{
153+
throw new KernelException($"The process named '{this.Name}' does not expose an event with Id '{eventId}'.");
154+
}
155+
156+
// Targets for external events on a process should be scoped to the process itself rather than the step inside the process.
157+
var processTarget = target with { Step = this, TargetEventId = eventId };
158+
return processTarget;
159+
}
160+
140161
/// <summary>
141162
/// Builds the process.
142163
/// </summary>
@@ -151,7 +172,7 @@ public KernelProcess Build()
151172
var builtSteps = this._steps.Select(step => step.BuildStep()).ToList();
152173

153174
// Create the process
154-
var state = new KernelProcessState(this.Name);
175+
var state = new KernelProcessState(this.Name, id: this.HasParentProcess ? this.Id : null);
155176
var process = new KernelProcess(state, builtSteps, builtEdges);
156177
return process;
157178
}
@@ -163,9 +184,6 @@ public KernelProcess Build()
163184
public ProcessBuilder(string name)
164185
: base(name)
165186
{
166-
this._steps = [];
167-
this._entrySteps = [];
168-
this._stepsMap = [];
169187
}
170188

171189
#endregion

dotnet/src/Experimental/Process.Core/ProcessFunctionTargetBuilder.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace Microsoft.SemanticKernel;
55
/// <summary>
66
/// Provides functionality for incrementally defining a process function target.
77
/// </summary>
8-
public sealed class ProcessFunctionTargetBuilder
8+
public sealed record ProcessFunctionTargetBuilder
99
{
1010
/// <summary>
1111
/// Initializes a new instance of the <see cref="ProcessFunctionTargetBuilder"/> class.
@@ -41,7 +41,7 @@ public ProcessFunctionTargetBuilder(ProcessStepBuilder step, string? functionNam
4141
internal KernelProcessFunctionTarget Build()
4242
{
4343
Verify.NotNull(this.Step.Id);
44-
return new KernelProcessFunctionTarget(this.Step.Id, this.FunctionName, this.ParameterName);
44+
return new KernelProcessFunctionTarget(this.Step.Id, this.FunctionName, this.ParameterName, this.TargetEventId);
4545
}
4646

4747
/// <summary>
@@ -58,4 +58,9 @@ internal KernelProcessFunctionTarget Build()
5858
/// The name of the parameter to target. This may be null if the function has no parameters.
5959
/// </summary>
6060
public string? ParameterName { get; init; }
61+
62+
/// <summary>
63+
/// The unique identifier for the event to target. This may be null if the target is not a sub-process.
64+
/// </summary>
65+
public string? TargetEventId { get; init; }
6166
}

dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ public virtual ProcessStepEdgeBuilder OnFunctionResult(string functionName)
4848

4949
#endregion
5050

51+
/// <summary>The namespace for events that are scoped to this step.</summary>
52+
private readonly string _eventNamespace;
53+
5154
/// <summary>
5255
/// A mapping of function names to the functions themselves.
5356
/// </summary>
@@ -143,19 +146,23 @@ internal virtual KernelProcessFunctionTarget ResolveFunctionTarget(string? funct
143146
);
144147
}
145148

146-
/// <summary>
147-
/// Given an event Id, returns a scoped event Id that is unique to this instance of the step.
148-
/// </summary>
149-
/// <param name="eventId">The Id of the event.</param>
150-
/// <returns>An Id that represents the provided event Id scoped to this step instance.</returns>
151-
internal abstract string GetScopedEventId(string eventId);
152-
153149
/// <summary>
154150
/// Loads a mapping of function names to the associated functions metadata.
155151
/// </summary>
156152
/// <returns>A <see cref="Dictionary{TKey, TValue}"/> where TKey is <see cref="string"/> and TValue is <see cref="KernelFunctionMetadata"/></returns>
157153
internal abstract Dictionary<string, KernelFunctionMetadata> GetFunctionMetadataMap();
158154

155+
/// <summary>
156+
/// Given an event Id, returns a scoped event Id that is unique to this instance of the step.
157+
/// </summary>
158+
/// <param name="eventId">The Id of the event.</param>
159+
/// <returns>An Id that represents the provided event Id scoped to this step instance.</returns>
160+
protected string GetScopedEventId(string eventId)
161+
{
162+
// Scope the event to this instance of this step by prefixing the event Id with the step's namespace.
163+
return $"{this._eventNamespace}.{eventId}";
164+
}
165+
159166
/// <summary>
160167
/// Initializes a new instance of the <see cref="ProcessStepBuilder"/> class.
161168
/// </summary>
@@ -167,6 +174,7 @@ protected ProcessStepBuilder(string name)
167174

168175
this.FunctionsDict = [];
169176
this.Id = Guid.NewGuid().ToString("n");
177+
this._eventNamespace = $"{this.Name}_{this.Id}";
170178
this.Edges = new Dictionary<string, List<ProcessStepEdgeBuilder>>(StringComparer.OrdinalIgnoreCase);
171179
}
172180
}
@@ -176,16 +184,12 @@ protected ProcessStepBuilder(string name)
176184
/// </summary>
177185
public sealed class ProcessStepBuilder<TStep> : ProcessStepBuilder where TStep : KernelProcessStep
178186
{
179-
/// <summary>The namespace for events that are scoped to this step.</summary>
180-
private readonly string _eventNamespace;
181-
182187
/// <summary>
183188
/// 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.
184189
/// </summary>
185190
public ProcessStepBuilder(string? name = null)
186191
: base(name ?? typeof(TStep).Name)
187192
{
188-
this._eventNamespace = $"{this.Name}_{this.Id}";
189193
this.FunctionsDict = this.GetFunctionMetadataMap();
190194
}
191195

@@ -225,13 +229,6 @@ internal override KernelProcessStepInfo BuildStep()
225229
return builtStep;
226230
}
227231

228-
/// <inheritdoc/>
229-
internal override string GetScopedEventId(string eventId)
230-
{
231-
// Scope the event to this instance of this step by prefixing the event Id with the step's namespace.
232-
return $"{this._eventNamespace}.{eventId}";
233-
}
234-
235232
/// <inheritdoc/>
236233
internal override Dictionary<string, KernelFunctionMetadata> GetFunctionMetadataMap()
237234
{

dotnet/src/Experimental/Process.Core/ProcessStepEdgeBuilder.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,15 @@ internal KernelProcessEdge Build()
4949
/// <summary>
5050
/// Signals that the output of the source step should be sent to the specified target when the associated event fires.
5151
/// </summary>
52-
/// <param name="outputTarget">The output target.</param>
53-
public void SendEventTo(ProcessFunctionTargetBuilder outputTarget)
52+
/// <param name="target">The output target.</param>
53+
public void SendEventTo(ProcessFunctionTargetBuilder target)
5454
{
5555
if (this.Target is not null)
5656
{
5757
throw new InvalidOperationException("An output target has already been set.");
5858
}
5959

60-
this.Target = outputTarget;
60+
this.Target = target;
6161
this.Source.LinkTo(this.EventId, this);
6262
}
6363

0 commit comments

Comments
 (0)