From 5c047c9fc6daf3bb3c956e86e52ff93c64f677e9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Estefan=C3=ADa=20Tenorio?=
<8483207+esttenorio@users.noreply.github.com>
Date: Wed, 9 Jul 2025 15:54:53 -0700
Subject: [PATCH 1/2] changes needed for in out typing using event descriptor
---
.../KernelProcessEventDescriptor.cs | 40 +++
.../KernelProcessOptions.cs | 13 +
.../Process.Abstractions/KernelProcessStep.cs | 15 ++
.../KernelProcessStepContext.cs | 40 ++-
.../KernelProcessStepInfo.cs | 5 +
.../Models/KernelEventTypeData.cs | 88 +++++++
.../ProcessStepEventData.cs | 38 +++
.../KernelProcessAbstractionsJsonContext.cs | 39 +++
.../Process.Core/ListenForBuilder.cs | 24 ++
.../Process.Core/MessageSourceBuilder.cs | 18 +-
.../Process.Core/ProcessBuilder.cs | 82 +++++-
.../Process.Core/ProcessEdgeBuilder.cs | 44 ++++
.../Process.Core/ProcessStepBuilder.cs | 233 ++++++++++++++++--
.../Process.Core/ProcessStepEdgeBuilder.cs | 53 +++-
.../Process.LocalRuntime/LocalAgentStep.cs | 9 +-
.../Process.LocalRuntime/LocalProxy.cs | 3 +-
.../Process.LocalRuntime/LocalStep.cs | 2 +-
.../Actors/AgentStepActor.cs | 2 +-
.../Process.Runtime.Dapr/Actors/ProxyActor.cs | 3 +-
.../Process.Runtime.Dapr/Actors/StepActor.cs | 2 +-
.../process/Abstractions/StepExtensions.cs | 4 +-
.../KernelJsonSerializationOptions.cs | 31 +++
.../KernelReturnParameterMetadataFactory.cs | 23 ++
23 files changed, 772 insertions(+), 39 deletions(-)
create mode 100644 dotnet/src/Experimental/Process.Abstractions/KernelProcessEventDescriptor.cs
create mode 100644 dotnet/src/Experimental/Process.Abstractions/KernelProcessOptions.cs
create mode 100644 dotnet/src/Experimental/Process.Abstractions/Models/KernelEventTypeData.cs
create mode 100644 dotnet/src/Experimental/Process.Abstractions/ProcessStepEventData.cs
create mode 100644 dotnet/src/Experimental/Process.Abstractions/Serialization/KernelProcessAbstractionsJsonContext.cs
create mode 100644 dotnet/src/SemanticKernel.Abstractions/Serialization/KernelJsonSerializationOptions.cs
create mode 100644 dotnet/src/SemanticKernel.Core/Functions/KernelReturnParameterMetadataFactory.cs
diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventDescriptor.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventDescriptor.cs
new file mode 100644
index 000000000000..cba0a190fbf7
--- /dev/null
+++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventDescriptor.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+
+namespace Microsoft.SemanticKernel;
+
+///
+/// Represents a descriptor for a kernel process event, including its name and associated type.
+/// It allows ensuring that events are uniquely defined before runtime and also determines the type of data that the event carries.
+///
+/// This class provides metadata about a kernel process event, including its name and the type of data it
+/// carries. It is commonly used to define and identify events in a kernel process event system.
+/// The type of the event data associated with the kernel process event data.
+public class KernelProcessEventDescriptor
+{
+ ///
+ /// Name of the event emitted by the Process Step.
+ ///
+ public string EventName { get; }
+ ///
+ /// Type of the event data associated with the event.
+ ///
+ public Type EventType { get; }
+
+ ///
+ /// Constructor for the KernelProcessEventDescriptor class.
+ ///
+ /// The name of the event emitted by the Process Step.
+ /// Thrown when the event name is null or empty.
+ public KernelProcessEventDescriptor(string eventName)
+ {
+ if (string.IsNullOrWhiteSpace(eventName))
+ {
+ throw new ArgumentException("Event name cannot be null or empty.", nameof(eventName));
+ }
+
+ this.EventName = eventName;
+ this.EventType = typeof(T);
+ }
+}
diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessOptions.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessOptions.cs
new file mode 100644
index 000000000000..6818179f5c49
--- /dev/null
+++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessOptions.cs
@@ -0,0 +1,13 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.SemanticKernel;
+
+///
+/// Options for configuring the Kernel process
+///
+public record KernelProcessOptions
+{
+ public JsonSerializerContext[] JsonSerializerAdditionalContexts { get; init; } = [];
+}
diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessStep.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessStep.cs
index 81d434c92c84..83f45e685391 100644
--- a/dotnet/src/Experimental/Process.Abstractions/KernelProcessStep.cs
+++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessStep.cs
@@ -9,6 +9,21 @@ namespace Microsoft.SemanticKernel;
///
public class KernelProcessStep
{
+ ///
+ /// Class containing events related to the step. This class is used to define events that can be raised during the execution of a step in a process.
+ /// This class should contain only public static readonly components.
+ /// When using in a custom step, it should be overwritten with:
+ ///
+ /// public static new class StepEvents
+ /// {
+ /// ...custom step events described with KernelProcessEventDescriptor...
+ /// }
+ ///
+ ///
+ public static class StepEvents
+ {
+ }
+
///
/// Name of the step given by the StepBuilder id.
///
diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepContext.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepContext.cs
index e652e0adb367..1de1365951ce 100644
--- a/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepContext.cs
+++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepContext.cs
@@ -1,5 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.Threading.Tasks;
namespace Microsoft.SemanticKernel;
@@ -10,14 +12,17 @@ namespace Microsoft.SemanticKernel;
public sealed class KernelProcessStepContext
{
private readonly IKernelProcessMessageChannel _stepMessageChannel;
+ private readonly IReadOnlyDictionary _outputEventsData;
///
/// Initializes a new instance of the class.
///
/// An instance of .
- public KernelProcessStepContext(IKernelProcessMessageChannel channel)
+ /// event data of output events emitted by step
+ public KernelProcessStepContext(IKernelProcessMessageChannel channel, IReadOnlyDictionary? outputEventsData)
{
this._stepMessageChannel = channel;
+ this._outputEventsData = outputEventsData ?? new ReadOnlyDictionary(new Dictionary());
}
///
@@ -44,12 +49,43 @@ public ValueTask EmitEventAsync(
{
Verify.NotNullOrWhiteSpace(eventId, nameof(eventId));
+ var eventVisibility = KernelProcessEventVisibility.Internal;
+ if (this._outputEventsData.TryGetValue(eventId, out var eventData))
+ {
+ if (eventData.IsPublic)
+ {
+ eventVisibility = KernelProcessEventVisibility.Public;
+ }
+ }
+ else
+ {
+ // TODO: Log a warning that the event is not registered in the step metadata -> event mismatch between step implementation and step builder event usage
+ var t = "Event with ID '" + eventId + "' is not registered in the step metadata.";
+ }
+
return this._stepMessageChannel.EmitEventAsync(
new KernelProcessEvent
{
Id = eventId,
Data = data,
- Visibility = visibility
+ Visibility = eventVisibility,
});
}
+
+ public ValueTask EmitEventAsync(KernelProcessEventDescriptor eventIdData, object? data = null)
+ {
+ Verify.NotNull(eventIdData, nameof(eventIdData));
+ var eventVisibility = KernelProcessEventVisibility.Internal;
+ if (this._outputEventsData.TryGetValue(eventIdData.EventName, out var eventData))
+ {
+ if (eventData.IsPublic)
+ {
+ eventVisibility = KernelProcessEventVisibility.Public;
+ }
+ }
+
+ // TODO: Runtime check/warning can be added were to validate that the data type matches the expected type of the event.
+
+ return this._stepMessageChannel.EmitEventAsync(new KernelProcessEvent { Id = eventIdData.EventName, Data = data, Visibility = eventVisibility });
+ }
}
diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepInfo.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepInfo.cs
index a1f5d0cef358..443edcba02e5 100644
--- a/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepInfo.cs
+++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepInfo.cs
@@ -46,6 +46,11 @@ public KernelProcessStepState State
///
public IReadOnlyDictionary? IncomingEdgeGroups { get; }
+ ///
+ /// A dictionary of output events type data for the step.
+ ///
+ public IReadOnlyDictionary? OutputEventsData { get; init; }
+
///
/// Initializes a new instance of the class.
///
diff --git a/dotnet/src/Experimental/Process.Abstractions/Models/KernelEventTypeData.cs b/dotnet/src/Experimental/Process.Abstractions/Models/KernelEventTypeData.cs
new file mode 100644
index 000000000000..2b4d2479b170
--- /dev/null
+++ b/dotnet/src/Experimental/Process.Abstractions/Models/KernelEventTypeData.cs
@@ -0,0 +1,88 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Text.Json;
+
+namespace Microsoft.SemanticKernel.Process.Models;
+
+///
+/// Properties to describe a serializable object as input or output event of a SK Process
+///
+public record KernelEventTypeData
+{
+ ///
+ /// Dotnet type of the object
+ ///
+ public Type? DataType { get; init; } = null;
+
+ ///
+ /// Gets or sets the short description of the data type.
+ ///
+ public string? Description { get; set; }
+
+ ///
+ /// Gets or sets whether the event data is considered required (rather than optional).
+ ///
+ ///
+ /// The default is false.
+ ///
+ public bool Required { get; set; } = false;
+
+ ///
+ /// Gets or sets JSON Schema describing this data type.
+ ///
+ public string? JsonSchema { get; set; }
+}
+
+///
+/// Methods to create from Semantic Kernel function specific objects
+///
+public static class KernelEventTypeDataExtensions
+{
+ ///
+ /// Converts the specified instance to a
+ /// object.
+ ///
+ /// The metadata describing the kernel parameter to be converted. Cannot be .
+ /// A object containing the schema, data type, description, and required status
+ /// derived from the specified .
+ public static KernelEventTypeData ToKernelEventTypeData(this KernelParameterMetadata kernelParameterMetadata)
+ {
+ Verify.NotNull(kernelParameterMetadata);
+ return new KernelEventTypeData
+ {
+ JsonSchema = kernelParameterMetadata.Schema?.ToString(),
+ DataType = kernelParameterMetadata.ParameterType,
+ Description = kernelParameterMetadata.Description,
+ Required = kernelParameterMetadata.IsRequired,
+ };
+ }
+
+ ///
+ /// Converts the specified instance to a object.
+ ///
+ /// The instance containing metadata about the kernel parameter.
+ /// A object populated with the corresponding data from the provided .
+ public static KernelEventTypeData ToKernelEventTypeData(this KernelReturnParameterMetadata kernelParameterMetadata)
+ {
+ Verify.NotNull(kernelParameterMetadata);
+ return new KernelEventTypeData
+ {
+ JsonSchema = kernelParameterMetadata.Schema?.ToString(),
+ DataType = kernelParameterMetadata.ParameterType,
+ Description = kernelParameterMetadata.Description,
+ Required = false,
+ };
+ }
+
+ public static KernelEventTypeData FromObjectType(Type objectType, JsonSerializerOptions jsonSerializerOptions)
+ {
+ Verify.NotNull(objectType, nameof(objectType));
+ // TODO: this is the centralized SK way to generate json schemas
+ var returnType = KernelReturnParameterMetadataFactory.CreateFromType(objectType, jsonSerializerOptions);
+
+ return returnType.ToKernelEventTypeData();
+ }
+}
diff --git a/dotnet/src/Experimental/Process.Abstractions/ProcessStepEventData.cs b/dotnet/src/Experimental/Process.Abstractions/ProcessStepEventData.cs
new file mode 100644
index 000000000000..520536467dec
--- /dev/null
+++ b/dotnet/src/Experimental/Process.Abstractions/ProcessStepEventData.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.SemanticKernel.Process.Models;
+
+namespace Microsoft.SemanticKernel;
+
+///
+/// Represents data associated with a process step event.
+///
+public record ProcessStepEventData
+{
+ ///
+ /// Gets the unique identifier for the event. Recommended to be human readable and unique within the process.
+ ///
+ public string EventId { get; init; } = string.Empty;
+
+ ///
+ /// Determines whether the event is public outside the step parent process or not.
+ ///
+ public bool IsPublic { get; set; } = false;
+
+ ///
+ /// Gets the event type data associated with the process event.
+ ///
+ public KernelEventTypeData? EventTypeData { get; init; } = null;
+
+ ///
+ /// Initializes a new instance of the class with the specified event ID and
+ /// optional event type data.
+ ///
+ /// The unique identifier for the event. This value cannot be null or empty.
+ /// Optional data describing the type of the event. If not provided, the event type data will be null.
+ public ProcessStepEventData(string eventId, KernelEventTypeData? eventTypeData = null)
+ {
+ this.EventId = eventId;
+ this.EventTypeData = eventTypeData;
+ }
+}
diff --git a/dotnet/src/Experimental/Process.Abstractions/Serialization/KernelProcessAbstractionsJsonContext.cs b/dotnet/src/Experimental/Process.Abstractions/Serialization/KernelProcessAbstractionsJsonContext.cs
new file mode 100644
index 000000000000..5e9c637c25b2
--- /dev/null
+++ b/dotnet/src/Experimental/Process.Abstractions/Serialization/KernelProcessAbstractionsJsonContext.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.SemanticKernel.Process.Serialization;
+
+// Internal Process Framework objects used in Process Framework should be added to the serialization context
+[JsonSerializable(typeof(KernelProcessStepContext))]
+[JsonSerializable(typeof(KernelProcessStepExternalContext))]
+[JsonSerializable(typeof(KernelProcessProxyMessage))]
+internal partial class KernelProcessAbstractionsJsonContext : JsonSerializerContext
+{
+}
+
+///
+/// Extensions for Semantic Kernel process abstractions JSON serialization.
+///
+public static class KernelProcessJsonSerializationExtensions
+{
+ ///
+ /// Method that allows adding additional serialization contexts to the default options used within Semantic Kernel for process abstractions.
+ ///
+ /// List of additional serializations contexts to be added
+ ///
+ public static JsonSerializerOptions GetKernelProcessCustomJsonSerializationOptions(JsonSerializerContext[] additionalContexts)
+ {
+ List predefinedContexts = new() { KernelProcessAbstractionsJsonContext.Default };
+ if (additionalContexts != null)
+ {
+ predefinedContexts.AddRange(additionalContexts);
+ }
+
+ var jsonOptions = KernelJsonSerializationOptions.GetKernelCustomJsonSerializationOptions(predefinedContexts.ToArray());
+
+ return jsonOptions;
+ }
+}
diff --git a/dotnet/src/Experimental/Process.Core/ListenForBuilder.cs b/dotnet/src/Experimental/Process.Core/ListenForBuilder.cs
index f8cd2aa8c319..8bee056898aa 100644
--- a/dotnet/src/Experimental/Process.Core/ListenForBuilder.cs
+++ b/dotnet/src/Experimental/Process.Core/ListenForBuilder.cs
@@ -5,6 +5,7 @@
using System.Linq;
using System.Security.Cryptography;
using System.Text;
+using Microsoft.SemanticKernel.Process.Models;
namespace Microsoft.SemanticKernel;
@@ -76,6 +77,29 @@ public ListenForTargetBuilder AllOf(List messageSources)
{
Verify.NotNullOrEmpty(messageSources, nameof(messageSources));
+ // verify mapped message sources output events do exist
+ foreach (var source in messageSources)
+ {
+ var messageSourceType = source.GetType();
+ Type? eventTypeData = null;
+ if (messageSourceType.IsGenericType && messageSourceType.GetGenericTypeDefinition() == typeof(TypedMessageSourceBuilder<>))
+ {
+ eventTypeData = messageSourceType.GenericTypeArguments.FirstOrDefault();
+ }
+
+ if (source.Source is ProcessBuilder sourceProcessBuilder)
+ {
+ sourceProcessBuilder.AddInputEventToProcess(source.MessageType, eventTypeData);
+ }
+ else
+ {
+ if (!source.Source.OutputStepEvents.ContainsKey(source.MessageType))
+ {
+ throw new InvalidOperationException($"Output Event {source.MessageType} is not emitted by {source.Source.StepId}");
+ }
+ }
+ }
+
var edgeGroup = new KernelProcessEdgeGroupBuilder(this.GetGroupId(messageSources), messageSources);
this._targetBuilder = new ListenForTargetBuilder(messageSources, this._processBuilder, edgeGroup: edgeGroup);
return this._targetBuilder;
diff --git a/dotnet/src/Experimental/Process.Core/MessageSourceBuilder.cs b/dotnet/src/Experimental/Process.Core/MessageSourceBuilder.cs
index 91723bc542bf..5215c6f632a8 100644
--- a/dotnet/src/Experimental/Process.Core/MessageSourceBuilder.cs
+++ b/dotnet/src/Experimental/Process.Core/MessageSourceBuilder.cs
@@ -7,7 +7,7 @@ namespace Microsoft.SemanticKernel;
///
/// Represents a builder for defining the source of a message in a process.
///
-public sealed class MessageSourceBuilder
+public class MessageSourceBuilder
{
///
/// Initializes a new instance of the class.
@@ -37,3 +37,19 @@ public MessageSourceBuilder(string messageType, ProcessStepBuilder source, Kerne
///
public KernelProcessEdgeCondition Condition { get; }
}
+
+public sealed class TypedMessageSourceBuilder : MessageSourceBuilder
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The source step builder
+ /// Condition that must be met for the message to be processed
+ public TypedMessageSourceBuilder(KernelProcessEventDescriptor messageType, ProcessStepBuilder source, KernelProcessEdgeCondition? condition = null)
+ : base(messageType.EventName, source, condition)
+ {
+ this.MessageDescriptor = messageType;
+ }
+
+ public KernelProcessEventDescriptor MessageDescriptor { get; }
+}
diff --git a/dotnet/src/Experimental/Process.Core/ProcessBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessBuilder.cs
index 2b72d156ac4c..4588f6b428db 100644
--- a/dotnet/src/Experimental/Process.Core/ProcessBuilder.cs
+++ b/dotnet/src/Experimental/Process.Core/ProcessBuilder.cs
@@ -2,12 +2,15 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.AzureAI;
using Microsoft.SemanticKernel.Process;
using Microsoft.SemanticKernel.Process.Internal;
+using Microsoft.SemanticKernel.Process.Models;
+using Microsoft.SemanticKernel.Process.Serialization;
using Microsoft.SemanticKernel.Process.Tools;
namespace Microsoft.SemanticKernel;
@@ -36,6 +39,8 @@ public sealed partial class ProcessBuilder : ProcessStepBuilder
///
internal bool HasParentProcess { get; set; }
+ internal Dictionary InputProcessEvents { get; init; } = [];
+
///
/// Version of the process, used when saving the state of the process
///
@@ -51,6 +56,8 @@ public sealed partial class ProcessBuilder : ProcessStepBuilder
///
public string Description { get; init; } = string.Empty;
+ public KernelProcessOptions Options { get; init; } = new KernelProcessOptions();
+
///
/// Initializes a new instance of the class.
///
@@ -58,12 +65,19 @@ public sealed partial class ProcessBuilder : ProcessStepBuilder
/// The semantic description of the Process being built.
/// ProcessBuilder to copy from
/// The type of the state. This is optional.
- public ProcessBuilder(string id, string? description = null, ProcessBuilder? processBuilder = null, Type? stateType = null)
+ ///
+ public ProcessBuilder(string id, string? description = null, ProcessBuilder? processBuilder = null, Type? stateType = null, KernelProcessOptions? processOptions = null)
: base(id, processBuilder)
{
Verify.NotNullOrWhiteSpace(id, nameof(id));
this.StateType = stateType;
this.Description = description ?? string.Empty;
+ this.Options = processOptions ?? new KernelProcessOptions();
+
+ if (this.JsonSerializerOptions == null)
+ {
+ this.JsonSerializerOptions = KernelProcessJsonSerializationExtensions.GetKernelProcessCustomJsonSerializationOptions(processOptions?.JsonSerializerAdditionalContexts ?? []);
+ }
}
///
@@ -125,6 +139,29 @@ internal override Dictionary GetFunctionMetadata
.ToDictionary(pair => pair.Key, pair => pair.Value);
}
+ internal void AddInputEventToProcess(string eventName, Type? inputType)
+ {
+ var processStepEventData = inputType != null ? KernelEventTypeDataExtensions.FromObjectType(inputType, this.JsonSerializerOptions!) : new();
+ this.AddInputEventToProcess(eventName, processStepEventData);
+ }
+
+ internal void AddInputEventToProcess(string eventName, KernelEventTypeData inputTypeData)
+ {
+ // If the event is already registered, we can skip adding it.
+ if (this.InputProcessEvents.ContainsKey(eventName))
+ {
+ // TODO: verify that the event type data is the same as the one already registered.
+ return;
+ }
+ // Add the input event to the process.
+ this.InputProcessEvents.Add(eventName, inputTypeData);
+ }
+
+ internal void AddOutputEventToProcess(ProcessStepEventData outputEvent)
+ {
+ this.OutputStepEvents.Add(outputEvent.EventId, outputEvent);
+ }
+
///
/// Builds the step.
///
@@ -191,7 +228,7 @@ private TBuilder AddStep(TBuilder builder, IReadOnlyList? alia
/// An instance of
public ProcessStepBuilder AddStepFromType(string? id = null, IReadOnlyList? aliases = null) where TStep : KernelProcessStep
{
- ProcessStepBuilder stepBuilder = new(id: id ?? typeof(TStep).Name, this.ProcessBuilder);
+ ProcessStepBuilder stepBuilder = new(id: id ?? typeof(TStep).Name, this, jsonSerializerOptions: this.JsonSerializerOptions);
return this.AddStep(stepBuilder, aliases);
}
@@ -205,7 +242,7 @@ public ProcessStepBuilder AddStepFromType(string? id = null, IReadOnlyLis
/// An instance of
public ProcessStepBuilder AddStepFromType(Type stepType, string? id = null, IReadOnlyList? aliases = null)
{
- ProcessStepBuilderTyped stepBuilder = new(stepType: stepType, id: id ?? stepType.Name, this.ProcessBuilder);
+ ProcessStepBuilderTyped stepBuilder = new(stepType: stepType, id: id ?? stepType.Name, this, jsonSerializerOptions: this.JsonSerializerOptions);
return this.AddStep(stepBuilder, aliases);
}
@@ -221,7 +258,7 @@ public ProcessStepBuilder AddStepFromType(Type stepType, string? id = null, IRea
/// An instance of
public ProcessStepBuilder AddStepFromType(TState initialState, string? id = null, IReadOnlyList? aliases = null) where TStep : KernelProcessStep where TState : class, new()
{
- ProcessStepBuilder stepBuilder = new(id ?? typeof(TStep).Name, this.ProcessBuilder, initialState: initialState);
+ ProcessStepBuilder stepBuilder = new(id ?? typeof(TStep).Name, this, initialState: initialState) { JsonSerializerOptions = this.JsonSerializerOptions };
return this.AddStep(stepBuilder, aliases);
}
@@ -250,7 +287,7 @@ public ProcessStepBuilder AddStepFromType(Type stepType, string? id = null, IRea
threadName = agentDefinition.Name;
}
- var stepBuilder = new ProcessAgentBuilder(agentDefinition, threadName: threadName, [], this.ProcessBuilder, id) { HumanInLoopMode = humanInLoopMode }; // TODO: Add inputs to the agent
+ var stepBuilder = new ProcessAgentBuilder(agentDefinition, threadName: threadName, [], this, id) { HumanInLoopMode = humanInLoopMode, JsonSerializerOptions = this.JsonSerializerOptions }; // TODO: Add inputs to the agent
return this.AddStep(stepBuilder, aliases);
}
@@ -278,7 +315,7 @@ public ProcessAgentBuilder AddStepFromAgent(AgentDefinition agentDefinition, str
threadName = agentDefinition.Name;
}
- var stepBuilder = new ProcessAgentBuilder(agentDefinition, threadName: threadName, [], this.ProcessBuilder, id) { HumanInLoopMode = humanInLoopMode };
+ var stepBuilder = new ProcessAgentBuilder(agentDefinition, threadName: threadName, [], this, id) { HumanInLoopMode = humanInLoopMode, JsonSerializerOptions = this.JsonSerializerOptions };
return this.AddStep(stepBuilder, aliases);
}
@@ -320,7 +357,7 @@ public ProcessAgentBuilder AddStepFromAgent(AgentDefinition agentDefinition, str
return Task.FromResult(result);
});
- var stepBuilder = new ProcessAgentBuilder(agentDefinition, threadName: threadName, [], this.ProcessBuilder, stepId) { AgentIdResolver = agentIdResolver, HumanInLoopMode = humanInLoopMode }; // TODO: Add inputs to the agent
+ var stepBuilder = new ProcessAgentBuilder(agentDefinition, threadName: threadName, [], this, stepId) { AgentIdResolver = agentIdResolver, HumanInLoopMode = humanInLoopMode, JsonSerializerOptions = this.JsonSerializerOptions }; // TODO: Add inputs to the agent
return this.AddStep(stepBuilder, aliases);
}
@@ -342,7 +379,9 @@ public ProcessStepBuilder AddEndStep()
/// An instance of
public ProcessBuilder AddStepFromProcess(ProcessBuilder kernelProcess, IReadOnlyList? aliases = null)
{
+ kernelProcess.ProcessBuilder = this;
kernelProcess.HasParentProcess = true;
+ kernelProcess.JsonSerializerOptions = this.JsonSerializerOptions;
return this.AddStep(kernelProcess, aliases);
}
@@ -356,9 +395,9 @@ public ProcessBuilder AddStepFromProcess(ProcessBuilder kernelProcess, IReadOnly
/// An instance of
public ProcessMapBuilder AddMapStepFromType(string? id = null, IReadOnlyList? aliases = null) where TStep : KernelProcessStep
{
- ProcessStepBuilder stepBuilder = new(id ?? typeof(TStep).Name, this.ProcessBuilder);
+ ProcessStepBuilder stepBuilder = new(id ?? typeof(TStep).Name, this.ProcessBuilder) { JsonSerializerOptions = this.JsonSerializerOptions };
- ProcessMapBuilder mapBuilder = new(stepBuilder);
+ ProcessMapBuilder mapBuilder = new(stepBuilder) { JsonSerializerOptions = this.JsonSerializerOptions };
return this.AddStep(mapBuilder, aliases);
}
@@ -374,9 +413,9 @@ public ProcessMapBuilder AddMapStepFromType(string? id = null, IReadOnlyL
/// An instance of
public ProcessMapBuilder AddMapStepFromType(TState initialState, string id, IReadOnlyList? aliases = null) where TStep : KernelProcessStep where TState : class, new()
{
- ProcessStepBuilder stepBuilder = new(id, this.ProcessBuilder, initialState: initialState);
+ ProcessStepBuilder stepBuilder = new(id, this.ProcessBuilder, initialState: initialState) { JsonSerializerOptions = this.JsonSerializerOptions };
- ProcessMapBuilder mapBuilder = new(stepBuilder);
+ ProcessMapBuilder mapBuilder = new(stepBuilder) { JsonSerializerOptions = this.JsonSerializerOptions };
return this.AddStep(mapBuilder, aliases);
}
@@ -393,7 +432,7 @@ public ProcessMapBuilder AddMapStepFromProcess(ProcessBuilder process, IReadOnly
{
process.HasParentProcess = true;
- ProcessMapBuilder mapBuilder = new(process);
+ ProcessMapBuilder mapBuilder = new(process) { JsonSerializerOptions = this.JsonSerializerOptions };
return this.AddStep(mapBuilder, aliases);
}
@@ -461,6 +500,25 @@ public ProcessEdgeBuilder OnInputEvent(string eventId)
return new ProcessEdgeBuilder(this, eventId);
}
+
+ public ProcessEdgeBuilder OnInputEvent(KernelProcessEventDescriptor eventDescriptor)
+ {
+ this.AddInputEventToProcess(eventDescriptor.EventName, eventDescriptor.EventType);
+
+ return this.OnInputEvent(eventDescriptor.EventName);
+ }
+
+ ///
+ public override ProcessStepEdgeBuilder OnEvent(string eventId, bool isPublic = false)
+ {
+ if (!this.OutputStepEvents.ContainsKey(eventId))
+ {
+ throw new InvalidOperationException($"Output Event {eventId} is not emitted publicly by {this.StepId}");
+ }
+
+ return base.OnEvent(eventId, isPublic);
+ }
+
///
/// Provides an instance of for defining an edge to a
/// step that responds to an unhandled process error.
diff --git a/dotnet/src/Experimental/Process.Core/ProcessEdgeBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessEdgeBuilder.cs
index f1f61469332d..8d725c608037 100644
--- a/dotnet/src/Experimental/Process.Core/ProcessEdgeBuilder.cs
+++ b/dotnet/src/Experimental/Process.Core/ProcessEdgeBuilder.cs
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
+using System.Linq;
+using Microsoft.SemanticKernel.Process.Models;
namespace Microsoft.SemanticKernel;
@@ -29,9 +31,51 @@ internal ProcessEdgeBuilder(ProcessBuilder source, string eventId) : base(source
///
public ProcessEdgeBuilder SendEventTo(ProcessFunctionTargetBuilder target)
{
+ if (!target.Step.InputParametersTypeData.TryGetValue(target.FunctionName, out var targetFunctionParameters))
+ {
+ throw new InvalidOperationException($"Target function {target.FunctionName} not found");
+ }
+
+ KernelEventTypeData targetParameterData = new();
+ if (target.ParameterName != null)
+ {
+ if (!targetFunctionParameters.TryGetValue(target.ParameterName, out var parameterData))
+ {
+ throw new InvalidOperationException($"Target function {target.FunctionName} has no parameter named {target.ParameterName}");
+ }
+
+ targetParameterData = parameterData;
+ }
+ this.Source.AddInputEventToProcess(this.EventData.EventId, targetParameterData);
+
return this.SendEventTo_Int(target as ProcessTargetBuilder);
}
+ public ProcessStepEdgeBuilder SendEventTo(ProcessFunctionTargetBuilder target) where TParameterDataType : class, new()
+ {
+ if (!target.Step.InputParametersTypeData.TryGetValue(target.FunctionName, out var targetFunctionInput))
+ {
+ throw new InvalidOperationException($"Step {target.Step.StepId} does not have input parameter schemas for function {target.FunctionName}");
+ }
+
+ var matchingParameterTypeValues = targetFunctionInput.Where(p => p.Value.DataType == typeof(TParameterDataType)).ToList();
+ if (matchingParameterTypeValues.Count == 0)
+ {
+ throw new InvalidOperationException($"No matching parameters of type {typeof(TParameterDataType).Name} found for function {target.FunctionName} in step {target.Step.StepId}.");
+ }
+
+ KernelEventTypeData targetParameterData = matchingParameterTypeValues.FirstOrDefault().Value;
+
+ if (targetParameterData == null)
+ {
+ throw new InvalidOperationException("No matching parameters found.");
+ }
+
+ this.Source.AddInputEventToProcess(this.EventData.EventId, targetParameterData);
+
+ return this.SendEventTo(target);
+ }
+
///
/// Sends the output of the source step to the specified target when the associated event fires.
///
diff --git a/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs
index 85096dbe4464..d2388ab8f9b9 100644
--- a/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs
+++ b/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs
@@ -3,8 +3,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Reflection;
+using System.Text.Json;
+using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Process;
using Microsoft.SemanticKernel.Process.Internal;
+using Microsoft.SemanticKernel.Process.Models;
namespace Microsoft.SemanticKernel;
@@ -35,20 +39,30 @@ public abstract class ProcessStepBuilder
/// Define the behavior of the step when the event with the specified Id is fired.
///
/// The Id of the event of interest.
+ /// Determins if the event is accessible outside of the parent process
/// An instance of .
- public ProcessStepEdgeBuilder OnEvent(string eventId)
+ public virtual ProcessStepEdgeBuilder OnEvent(string eventId, bool isPublic = false)
{
+ if (isPublic)
+ {
+ this.MarkPublicEvent(eventId);
+ }
// scope the event to this instance of this step
var scopedEventId = this.GetScopedEventId(eventId);
return new ProcessStepEdgeBuilder(this, scopedEventId, eventId);
}
- ///
- /// Returns the event Id that is used to identify the result of a function.
- ///
- /// Optional: name of the step function the result is expected from
- ///
- public string GetFunctionResultEventId(string? functionName = null)
+ public virtual ProcessStepEdgeBuilder OnEvent(KernelProcessEventDescriptor eventDescriptor, bool isPublic = false)
+ {
+ return this.OnEvent(eventDescriptor.EventName, isPublic);
+ }
+
+ ///
+ /// Returns the event Id that is used to identify the result of a function.
+ ///
+ /// Optional: name of the step function the result is expected from
+ ///
+ public string GetFunctionResultEventId(string? functionName = null)
{
// TODO: Add a check to see if the function name is valid if provided
if (string.IsNullOrWhiteSpace(functionName))
@@ -75,6 +89,11 @@ public string GetFullEventId(string? eventName = null, string? functionName = nu
return $"{this.StepId}.{eventName}";
}
+ public string GetFullEventId(KernelProcessEventDescriptor eventDescriptor, string? functionName = null)
+ {
+ return this.GetFullEventId(eventDescriptor.EventName, functionName);
+ }
+
///
/// Define the behavior of the step when the specified function has been successfully invoked.
///
@@ -107,11 +126,16 @@ public ProcessStepEdgeBuilder OnFunctionError(string? functionName = null)
/// The namespace for events that are scoped to this step.
private readonly string _eventNamespace;
+ internal JsonSerializerOptions? JsonSerializerOptions { get; set; } = null;
+
///
/// A mapping of function names to the functions themselves.
///
internal Dictionary FunctionsDict { get; set; }
+ internal Dictionary> InputParametersTypeData { get; init; } = [];
+ internal Dictionary OutputStepEvents { get; init; } = [];
+
///
/// A mapping of event Ids to the edges that are triggered by those events.
///
@@ -120,7 +144,7 @@ public ProcessStepEdgeBuilder OnFunctionError(string? functionName = null)
///
/// The process builder that this step is a part of. This may be null if the step is itself a process.
///
- internal ProcessBuilder? ProcessBuilder { get; }
+ internal ProcessBuilder? ProcessBuilder { get; set; }
///
/// Builds the step with step state
@@ -163,6 +187,21 @@ private string ResolveFunctionName()
return this.FunctionsDict.Keys.First();
}
+ internal void MarkPublicEvent(string eventId)
+ {
+ if (this.OutputStepEvents.TryGetValue(eventId, out var stepEventData) && stepEventData != null)
+ {
+ stepEventData.IsPublic = true;
+
+ // Update parent process as well since this step event is an output edge of the process
+ this.ProcessBuilder?.AddOutputEventToProcess(stepEventData with { IsPublic = false });
+
+ return;
+ }
+
+ throw new KernelException($"The event {eventId} does not exist on step {this.StepId}.");
+ }
+
///
/// Links the output of the current step to the an input of another step via the specified event type.
///
@@ -181,8 +220,11 @@ internal virtual void LinkTo(string eventId, ProcessStepEdgeBuilder edgeBuilder)
internal static bool FilterSupportedParameterTypes(Type? parameterType, bool hasDefaultValue = false)
{
- if (parameterType != typeof(KernelProcessStepContext) &&
- parameterType != typeof(KernelProcessStepExternalContext))
+ // Should match parameters piped in in StepExtensions.FindInputChannels
+ if (parameterType != typeof(Kernel) &&
+ parameterType != typeof(KernelProcessStepContext) &&
+ parameterType != typeof(KernelProcessStepExternalContext) &&
+ parameterType != typeof(AgentDefinition))
{
return !hasDefaultValue;
}
@@ -308,14 +350,173 @@ public class ProcessStepBuilderTyped : ProcessStepBuilder
/// The unique id of the step.
/// The process builder that this step is a part of.
/// Initial state of the step to be used on the step building stage
- internal ProcessStepBuilderTyped(Type stepType, string id, ProcessBuilder? processBuilder, object? initialState = default)
+ internal ProcessStepBuilderTyped(Type stepType, string id, ProcessBuilder? processBuilder, object? initialState = default, JsonSerializerOptions? jsonSerializerOptions = null)
: base(id, processBuilder)
{
Verify.NotNull(stepType);
+ this.JsonSerializerOptions ??= jsonSerializerOptions;
+
this._stepType = stepType;
this.FunctionsDict = this.GetFunctionMetadataMap();
this._initialState = initialState;
+
+ this.InputParametersTypeData = this.GetInputParameterFunctionDataMap(this.FunctionsDict);
+ this.OutputStepEvents = this.ExtractStepOutputEvents(stepType, this.FunctionsDict);
+ }
+
+ internal Dictionary> GetInputParameterFunctionDataMap(IDictionary functionDict)
+ {
+ var inputDataDict = new Dictionary>();
+
+ foreach (var kvp in functionDict)
+ {
+ var parameters = kvp.Value.Parameters;
+ if (!inputDataDict.TryGetValue(kvp.Key, out Dictionary? value))
+ {
+ value = new Dictionary();
+ inputDataDict[kvp.Key] = value;
+ }
+
+ foreach (var parameter in parameters)
+ {
+ if (FilterSupportedParameterTypes(parameter.ParameterType, hasDefaultValue: parameter.DefaultValue != null))
+ {
+ value[parameter.Name] = parameter.ToKernelEventTypeData();
+ }
+ }
+ }
+
+ return inputDataDict;
+ }
+
+ ///
+ /// Inspects default function return data if any, since this could be used with the method
+ ///
+ ///
+ ///
+ internal Dictionary ExtractOnFunctionResultEventsData(IDictionary functionDict)
+ {
+ var eventDataDict = new Dictionary();
+
+ foreach (var kvp in functionDict)
+ {
+ var eventId = this.GetFunctionResultEventId(kvp.Key);
+ if (kvp.Value.ReturnParameter.Schema != null)
+ {
+ var outputEventData = kvp.Value.ReturnParameter.ToKernelEventTypeData();
+ eventDataDict[eventId] = new(eventId, outputEventData);
+ }
+ }
+
+ return eventDataDict;
+ }
+
+ ///
+ /// Inspects Custom step implementation to extract custom output events declared in the class if any.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ internal Dictionary ExtractCustomOutputEventsData(Type stepType)
+ {
+ if (stepType == null)
+ {
+ throw new ArgumentNullException(nameof(stepType));
+ }
+
+ if (!typeof(KernelProcessStep).IsAssignableFrom(stepType))
+ {
+ throw new ArgumentException($"The type {stepType.Name} must derive from {nameof(KernelProcessStep)}.", nameof(stepType));
+ }
+
+ var stepEventsType = stepType.GetNestedType(nameof(KernelProcessStep.StepEvents), BindingFlags.Public | BindingFlags.Static);
+ if (stepEventsType == null)
+ {
+ // since there is no StepEvents found, attepting to using base class instead in case there is one
+ stepEventsType = stepType.BaseType?.GetNestedType(nameof(KernelProcessStep.StepEvents), BindingFlags.Public | BindingFlags.Static);
+ if (stepEventsType == null)
+ {
+ // no StepEvents found in base class either, log warning, maybe user is only relying on FunctionResult events only
+ //throw new ArgumentException($"No static {nameof(KernelProcessStep.StepEvents)} class found in {stepType.Name}");
+ return [];
+ }
+ }
+
+ var eventClassFields = stepEventsType.GetFields(BindingFlags.Public | BindingFlags.Static);
+ var processEventDescriptors = eventClassFields.Where(field => field.FieldType.IsGenericType && field.FieldType.GetGenericTypeDefinition() == typeof(KernelProcessEventDescriptor<>)).ToList();
+ if (processEventDescriptors.Count == 0)
+ {
+ // no StepEvents found in base class either, log warning, maybe user is only relying on FunctionResult events only
+ //throw new ArgumentException($"No public static fields of type {nameof(KernelProcessEventDescriptor