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)} found in {stepEventsType.Name}"); + return []; + } + + // TODO: Add logger warning saying that anything that is not a KernelProcessEventDescriptor will be ignored + + var outputEvents = new Dictionary(); + foreach (var field in processEventDescriptors) + { + var eventData = field.GetValue(null); + var eventDescriptorType = eventData?.GetType(); + if (eventDescriptorType != null) + { + var eventTypeValue = eventDescriptorType.GetProperty(nameof(KernelProcessEventDescriptor.EventType))?.GetValue(eventData); + var eventNameValue = eventDescriptorType.GetProperty(nameof(KernelProcessEventDescriptor.EventName))?.GetValue(eventData); + + if (eventTypeValue is Type eventType && eventNameValue is string eventName) + { + // Create a new ProcessStepEventData instance + var processStepEventData = KernelEventTypeDataExtensions.FromObjectType((Type)eventTypeValue, this.JsonSerializerOptions!); + if (outputEvents.TryGetValue(eventName, out ProcessStepEventData? value)) + { + var assignedDataType = value.EventTypeData?.DataType; + if (assignedDataType != processStepEventData.DataType) + { + throw new InvalidOperationException($"Event {eventName} has already been assigned with data type {assignedDataType?.Name}, cannot assign multiple types to same event name"); + } + } + else + { + outputEvents.Add(eventName, new(eventName, processStepEventData)); + } + } + else + { + throw new ArgumentException($"The field {field.Name} does not have valid EventType or EventName properties."); + } + } + } + + return outputEvents; + } + + internal Dictionary ExtractStepOutputEvents(Type stepType, IDictionary functionDict) + { + var stepOutputEvents = new Dictionary(); + + var defaultOutputEvents = this.ExtractOnFunctionResultEventsData(functionDict); + if (defaultOutputEvents != null) + { + foreach (var kvp in defaultOutputEvents) + { + stepOutputEvents[kvp.Key] = kvp.Value; + } + } + + var customOutputEvents = this.ExtractCustomOutputEventsData(stepType); + foreach (var eventKvp in customOutputEvents) + { + if (stepOutputEvents.ContainsKey(eventKvp.Key)) + { + throw new KernelException($"The event {eventKvp.Key} has already been defined. All event in a step must be unique."); + } + stepOutputEvents[eventKvp.Key] = eventKvp.Value; + } + + return stepOutputEvents; } /// @@ -359,13 +560,17 @@ internal override KernelProcessStepInfo BuildStep(ProcessBuilder processBuilder) var builtEdges = this.Edges.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Select(e => e.Build()).ToList()); // Then build the step with the edges and state. - var builtStep = new KernelProcessStepInfo(this._stepType, stateObject, builtEdges, this.IncomingEdgeGroups); + var builtStep = new KernelProcessStepInfo(this._stepType, stateObject, builtEdges, this.IncomingEdgeGroups) + { + OutputEventsData = this.OutputStepEvents, + }; return builtStep; } /// internal override Dictionary GetFunctionMetadataMap() { + // TODO: !!!! var metadata = KernelFunctionMetadataFactory.CreateFromType(this._stepType); return metadata.ToDictionary(m => m.Name, m => m); } @@ -382,8 +587,8 @@ public class ProcessStepBuilder : ProcessStepBuilderTyped where TStep : K /// 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 ProcessStepBuilder(string id, ProcessBuilder? processBuilder = null, object? initialState = default) - : base(typeof(TStep), id, processBuilder, initialState) + internal ProcessStepBuilder(string id, ProcessBuilder? processBuilder = null, object? initialState = default, JsonSerializerOptions? jsonSerializerOptions = null) + : base(typeof(TStep), id, processBuilder, initialState, jsonSerializerOptions) { } } diff --git a/dotnet/src/Experimental/Process.Core/ProcessStepEdgeBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessStepEdgeBuilder.cs index 27be7eb24e35..ac9965cd2047 100644 --- a/dotnet/src/Experimental/Process.Core/ProcessStepEdgeBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/ProcessStepEdgeBuilder.cs @@ -1,8 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Collections.Generic; using System.Linq; using Microsoft.SemanticKernel.Process.Internal; +using Microsoft.SemanticKernel.Process.Models; namespace Microsoft.SemanticKernel; @@ -89,6 +91,45 @@ internal KernelProcessEdge Build(ProcessBuilder? processBuilder = null) /// A fresh builder instance for fluid definition public ProcessStepEdgeBuilder SendEventTo(ProcessTargetBuilder target) { + if (target is ProcessFunctionTargetBuilder functionTarget) + { + Dictionary? targetFunctionInput = null; + KernelEventTypeData? targetParameter = null; + // Some step may not have input parameters, they may just need event trigger, so we need to check if the step has input parameters defined + if (functionTarget.Step.InputParametersTypeData.Count > 0 && !functionTarget.Step.InputParametersTypeData.TryGetValue(functionTarget.FunctionName, out targetFunctionInput)) + { + throw new InvalidOperationException($"Step {functionTarget.Step.StepId} does not have input parameter schemas for function {functionTarget.FunctionName}"); + } + + if (targetFunctionInput != null && functionTarget.ParameterName != null && !targetFunctionInput.TryGetValue(functionTarget.ParameterName, out targetParameter)) + { + throw new InvalidOperationException($"Step {functionTarget.Step.StepId} does not have input parameter schemas for function {functionTarget.FunctionName} and parameter {functionTarget.ParameterName}"); + } + + // TODO: Typing comparison only works if process uses KernelProcessStepEventMetadata Attribute or OnEvent + if (targetParameter != null && this.Source.OutputStepEvents.TryGetValue(this.EventData.EventName, out var outputEventData) && outputEventData != null) + { + // If the output event data is not null, we can check if the types match + if (outputEventData.EventTypeData?.DataType != targetParameter?.DataType) + { + var outputType = outputEventData.EventTypeData?.DataType; + var inputType = targetParameter?.DataType; + if (outputType is null || inputType is null) + { + throw new InvalidOperationException($"Failed to resolve types for output schema {outputType?.Name} or input schema {inputType?.Name}."); + } + + // TODO: Need to add any additional internal types to skip types compatibility checks + if (inputType != typeof(KernelProcessProxyMessage) && !outputType.IsAssignableFrom(inputType) && !inputType.IsAssignableFrom(outputType)) + { + throw new InvalidOperationException($"Output type {outputType.Name} is not assignable to input type {inputType.Name} for event {this.EventData.EventName} in step {this.Source.StepId}."); + } + + // TODO: Add default schema check + } + } + } + return this.SendEventTo_Internal(target); } @@ -129,7 +170,7 @@ internal virtual ProcessStepEdgeBuilder SendEventTo_Internal(ProcessTargetBuilde this.Target = target; this.Source.LinkTo(this.EventData.EventId, this); - return new ProcessStepEdgeBuilder(this.Source, this.EventData.EventId, this.EventData.EventName, this.EdgeGroupBuilder, this.Condition); + return new ProcessStepEdgeBuilder(this.Source, this.EventData.EventId, this.EventData.EventName, edgeGroupBuilder: this.EdgeGroupBuilder, condition: this.Condition); } /// @@ -158,6 +199,16 @@ public ProcessStepEdgeBuilder SentToAgentStep(ProcessAgentBuilder agentStep) return this.SendEventTo(targetBuilder); } + public void EmitAsPublicEvent() + { + if (this.Source is null) + { + throw new InvalidOperationException("An source must be set before marking the event as public."); + } + + this.Source.MarkPublicEvent(this.EventData.EventName); + } + /// /// Signals that the process should be stopped. /// diff --git a/dotnet/src/Experimental/Process.LocalRuntime/LocalAgentStep.cs b/dotnet/src/Experimental/Process.LocalRuntime/LocalAgentStep.cs index 18a8e630abd7..58fe9d2fd1ab 100644 --- a/dotnet/src/Experimental/Process.LocalRuntime/LocalAgentStep.cs +++ b/dotnet/src/Experimental/Process.LocalRuntime/LocalAgentStep.cs @@ -41,7 +41,8 @@ internal override void PopulateInitialInputs() { if (this._stepInfo is KernelProcessAgentStep agentStep) { - this._initialInputs = this.FindInputChannels(this._functions, this._logger, this.ExternalMessageChannel, agentStep.AgentDefinition); + // TODO: custom logic may be needed to extract output events data mapping from declarative definition - placeholder null for now + this._initialInputs = this.FindInputChannels(this._functions, null, this._logger, this.ExternalMessageChannel, agentStep.AgentDefinition); } else { @@ -97,7 +98,8 @@ internal override async Task HandleMessageAsync(ProcessMessage message) } if (this._stepInfo.Actions.CodeActions?.OnError is not null) { - this._stepInfo.Actions.CodeActions?.OnError(processError, new KernelProcessStepContext(this)); + // TODO: Properly pass context output events mapping data + this._stepInfo.Actions.CodeActions?.OnError(processError, new KernelProcessStepContext(this, null)); } return; @@ -111,7 +113,8 @@ internal override async Task HandleMessageAsync(ProcessMessage message) } if (this._stepInfo.Actions.CodeActions?.OnComplete is not null) { - this._stepInfo.Actions.CodeActions?.OnComplete(result, new KernelProcessStepContext(this)); + // TODO: Properly pass context output events mapping data + this._stepInfo.Actions.CodeActions?.OnComplete(result, new KernelProcessStepContext(this, null)); } } diff --git a/dotnet/src/Experimental/Process.LocalRuntime/LocalProxy.cs b/dotnet/src/Experimental/Process.LocalRuntime/LocalProxy.cs index 46a9a37f462b..f9b4142ce62b 100644 --- a/dotnet/src/Experimental/Process.LocalRuntime/LocalProxy.cs +++ b/dotnet/src/Experimental/Process.LocalRuntime/LocalProxy.cs @@ -36,7 +36,8 @@ internal LocalProxy(KernelProcessProxy proxy, Kernel kernel, IExternalKernelProc internal override void PopulateInitialInputs() { - this._initialInputs = this.FindInputChannels(this._functions, this._logger, this.ExternalMessageChannel); + // Proxy step does not emit output events, no output event data is needed + this._initialInputs = this.FindInputChannels(this._functions, null, this._logger, this.ExternalMessageChannel); } internal override void AssignStepFunctionParameterValues(ProcessMessage message) diff --git a/dotnet/src/Experimental/Process.LocalRuntime/LocalStep.cs b/dotnet/src/Experimental/Process.LocalRuntime/LocalStep.cs index 246e29c55ca4..37b2567c109c 100644 --- a/dotnet/src/Experimental/Process.LocalRuntime/LocalStep.cs +++ b/dotnet/src/Experimental/Process.LocalRuntime/LocalStep.cs @@ -101,7 +101,7 @@ internal virtual KernelProcessStep CreateStepInstance() internal virtual void PopulateInitialInputs() { - this._initialInputs = this.FindInputChannels(this._functions, this._logger, this.ExternalMessageChannel); + this._initialInputs = this.FindInputChannels(this._functions, this._stepInfo.OutputEventsData, this._logger, this.ExternalMessageChannel); } /// diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/AgentStepActor.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/AgentStepActor.cs index 794895e59074..052a6184aecb 100644 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/AgentStepActor.cs +++ b/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/AgentStepActor.cs @@ -41,7 +41,7 @@ internal override KernelProcessStep GetStepInstance() internal override Dictionary?> GenerateInitialInputs() { - return this.FindInputChannels(this._functions, this._logger, agentDefinition: this._daprAgentStepInfo!.AgentDefinition); + return this.FindInputChannels(this._functions, null, this._logger, agentDefinition: this._daprAgentStepInfo!.AgentDefinition); } public async Task InitializeAgentStepAsync(string processId, string stepId, string? parentProcessId) diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ProxyActor.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ProxyActor.cs index 6644cd30b43d..807d16105cb9 100644 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ProxyActor.cs +++ b/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/ProxyActor.cs @@ -70,7 +70,8 @@ internal override void AssignStepFunctionParameterValues(ProcessMessage message) IExternalMessageBuffer actor = this.ProxyFactory.CreateActorProxy(scopedExternalMessageBufferId, nameof(ExternalMessageBufferActor)); externalMessageChannelActor = new ExternalMessageBufferActorWrapper(actor); - return this.FindInputChannels(this._functions, this._logger, externalMessageChannelActor); + // Proxy step does not emit output events, no output event data is needed + return this.FindInputChannels(this._functions, null, this._logger, externalMessageChannelActor); } public async Task InitializeProxyAsync(string processId, string stepId, string? parentProcessId) diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/StepActor.cs b/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/StepActor.cs index 87e0c4ff4c11..29ee74fc2f18 100644 --- a/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/StepActor.cs +++ b/dotnet/src/Experimental/Process.Runtime.Dapr/Actors/StepActor.cs @@ -371,7 +371,7 @@ await this.EmitEventAsync( internal virtual Dictionary?> GenerateInitialInputs() { - return this.FindInputChannels(this._functions, this._logger); + return this.FindInputChannels(this._functions, this._stepInfo?.OutputEventsData, this._logger); } internal virtual KernelProcessStep GetStepInstance() diff --git a/dotnet/src/InternalUtilities/process/Abstractions/StepExtensions.cs b/dotnet/src/InternalUtilities/process/Abstractions/StepExtensions.cs index 524928dc6128..a6b833ef3491 100644 --- a/dotnet/src/InternalUtilities/process/Abstractions/StepExtensions.cs +++ b/dotnet/src/InternalUtilities/process/Abstractions/StepExtensions.cs @@ -102,6 +102,7 @@ public static void InitializeUserState(this KernelProcessStepState stateObject, /// /// The source channel to evaluate /// A dictionary of KernelFunction instances. + /// A dictionary with step output event data /// An instance of . /// An instance of /// An instance of @@ -110,6 +111,7 @@ public static void InitializeUserState(this KernelProcessStepState stateObject, public static Dictionary?> FindInputChannels( this IKernelProcessMessageChannel channel, Dictionary functions, + IReadOnlyDictionary? outputEventsData, ILogger? logger, IExternalKernelProcessMessageChannel? externalMessageChannel = null, AgentDefinition? agentDefinition = null) @@ -135,7 +137,7 @@ public static void InitializeUserState(this KernelProcessStepState stateObject, // and are instantiated here. if (param.ParameterType == typeof(KernelProcessStepContext)) { - inputs[kvp.Key]![param.Name] = new KernelProcessStepContext(channel); + inputs[kvp.Key]![param.Name] = new KernelProcessStepContext(channel, outputEventsData); } else if (param.ParameterType == typeof(KernelProcessStepExternalContext)) { diff --git a/dotnet/src/SemanticKernel.Abstractions/Serialization/KernelJsonSerializationOptions.cs b/dotnet/src/SemanticKernel.Abstractions/Serialization/KernelJsonSerializationOptions.cs new file mode 100644 index 000000000000..c3ac171f4ff8 --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/Serialization/KernelJsonSerializationOptions.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; + +namespace Microsoft.SemanticKernel; + +public static class KernelJsonSerializationOptions +{ + /// + /// Method that allows adding additional serialization contexts to the default options using within Semantic Kernel. + /// + /// List of additional serializations contexts to be added + /// + public static JsonSerializerOptions GetKernelCustomJsonSerializationOptions(JsonSerializerContext[] additionalContexts) + { + // Default options are compatible with internal SK components + var jsonOptions = new JsonSerializerOptions(AbstractionsJsonContext.Default.Options); + if (additionalContexts != null) + { + List predefinedContexts = new() { AbstractionsJsonContext.Default }; + predefinedContexts.AddRange(additionalContexts); + + jsonOptions.TypeInfoResolver = JsonTypeInfoResolver.Combine(predefinedContexts.ToArray()); + } + + return jsonOptions; + } +} diff --git a/dotnet/src/SemanticKernel.Core/Functions/KernelReturnParameterMetadataFactory.cs b/dotnet/src/SemanticKernel.Core/Functions/KernelReturnParameterMetadataFactory.cs new file mode 100644 index 000000000000..1c0f059999bf --- /dev/null +++ b/dotnet/src/SemanticKernel.Core/Functions/KernelReturnParameterMetadataFactory.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Text.Json; + +namespace Microsoft.SemanticKernel; + +/// +/// Provides factory methods for creating instances. +/// +public static class KernelReturnParameterMetadataFactory +{ + /// + /// Creates a from a .NET type. + /// + /// The .NET type from which to create the metadata. + /// json serialization options + /// A instance representing the type. + public static KernelReturnParameterMetadata CreateFromType(Type returnType, JsonSerializerOptions jsonSerializerOptions) + { + return new KernelReturnParameterMetadata(jsonSerializerOptions) { ParameterType = returnType }; + } +} From c81125714c433ee5f85ec3eea5b9fcf3a09e9eb6 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 16:01:04 -0700 Subject: [PATCH 2/2] updating samples used for validation --- dotnet/SK-dotnet.slnx | 2 + .../DocumentGenerationProcess.cs | 39 +++++++++----- .../Steps/GenerateDocumentationStep.cs | 8 +-- .../Steps/ProofreadDocumentationStep.cs | 15 +++--- ...sWithCloudEvents.Tests.LocalRuntime.csproj | 53 +++++++++++++++++++ ...thCloudEvents_DocumentGenerationProcess.cs | 18 +++++++ .../Step03/Processes/FishAndChipsProcess.cs | 19 +++++-- .../Step03/Processes/FishSandwichProcess.cs | 8 +++ .../Step03/Processes/FriedFishProcess.cs | 14 ++--- .../Step03/Processes/PotatoFriesProcess.cs | 12 +++-- .../Step03/Steps/CutFoodStep.cs | 10 ++-- .../Step03/Steps/ExternalStep.cs | 2 +- .../Step03/Steps/FryFoodStep.cs | 10 ++-- .../Step03/Steps/GatherIngredientsStep.cs | 6 +-- 14 files changed, 161 insertions(+), 55 deletions(-) create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Tests.LocalRuntime/ProcessWithCloudEvents.Tests.LocalRuntime.csproj create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Tests.LocalRuntime/ProcessWithCloudEvents_DocumentGenerationProcess.cs diff --git a/dotnet/SK-dotnet.slnx b/dotnet/SK-dotnet.slnx index 495a570d6941..e3826a91ff8d 100644 --- a/dotnet/SK-dotnet.slnx +++ b/dotnet/SK-dotnet.slnx @@ -79,8 +79,10 @@ + + diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/DocumentGenerationProcess.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/DocumentGenerationProcess.cs index fdfc8c7497fe..c1030ffa3ba3 100644 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/DocumentGenerationProcess.cs +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/DocumentGenerationProcess.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Text.Json.Serialization; using Microsoft.SemanticKernel; +using ProcessWithCloudEvents.Processes.Models; using ProcessWithCloudEvents.Processes.Steps; namespace ProcessWithCloudEvents.Processes; @@ -34,6 +36,13 @@ public static class DocGenerationEvents public const string UserApprovedDocument = nameof(UserApprovedDocument); } + public static class ProcessInputEvents + { + public static readonly KernelProcessEventDescriptor StartDocumentGeneration = new(nameof(StartDocumentGeneration)); + public static readonly KernelProcessEventDescriptor UserApprovedDocument = new(nameof(UserApprovedDocument)); + public static readonly KernelProcessEventDescriptor UserRejectedDocument = new(nameof(UserRejectedDocument)); + } + /// /// SK Process topics emitted by /// Topics are used to emit events to external systems @@ -57,8 +66,8 @@ public static class DocGenerationTopics /// instance of public static ProcessBuilder CreateProcessBuilder(string processName = "DocumentationGeneration") { - // Create the process builder - ProcessBuilder processBuilder = new(processName); + // Create the process builder + ProcessBuilder processBuilder = new(processName, processOptions: new() { JsonSerializerAdditionalContexts = [DocumentJsonSerializerContext.Default] }); // Add the steps var infoGatheringStep = processBuilder.AddStepFromType(); @@ -70,7 +79,7 @@ public static ProcessBuilder CreateProcessBuilder(string processName = "Document // Orchestrate the external input events processBuilder - .OnInputEvent(DocGenerationEvents.StartDocumentGeneration) + .OnInputEvent(ProcessInputEvents.StartDocumentGeneration) .SendEventTo(new(infoGatheringStep)); processBuilder @@ -87,39 +96,45 @@ public static ProcessBuilder CreateProcessBuilder(string processName = "Document .SendEventTo(new ProcessFunctionTargetBuilder(docsGenerationStep, functionName: GenerateDocumentationStep.ProcessFunctions.GenerateDocs)); docsGenerationStep - .OnEvent(GenerateDocumentationStep.OutputEvents.DocumentationGenerated) + .OnEvent(GenerateDocumentationStep.StepEvents.DocumentationGenerated) .SendEventTo(new ProcessFunctionTargetBuilder(docsProofreadStep)); docsProofreadStep - .OnEvent(ProofReadDocumentationStep.OutputEvents.DocumentationRejected) + .OnEvent(ProofReadDocumentationStep.StepEvents.DocumentationRejected) .SendEventTo(new ProcessFunctionTargetBuilder(docsGenerationStep, functionName: GenerateDocumentationStep.ProcessFunctions.ApplySuggestions)); // When the proofreader approves the documentation, send it to the 'docs' parameter of the docsPublishStep // Additionally, the generated document is emitted externally for user approval using the pre-configured proxyStep docsProofreadStep - .OnEvent(ProofReadDocumentationStep.OutputEvents.DocumentationApproved) - .EmitExternalEvent(proxyStep, DocGenerationTopics.RequestUserReview); + .OnEvent(ProofReadDocumentationStep.StepEvents.DocumentationApproved) + .EmitExternalEvent(proxyStep, DocGenerationTopics.RequestUserReview) + .EmitAsPublicEvent(); //.SendEventTo(new ProcessFunctionTargetBuilder(docsPublishStep, parameterName: "document")); processBuilder .ListenFor() .AllOf([ - new(messageType: ProofReadDocumentationStep.OutputEvents.DocumentationApproved, docsProofreadStep), - new(messageType: DocGenerationEvents.UserApprovedDocument, processBuilder), + new TypedMessageSourceBuilder(messageType: ProofReadDocumentationStep.StepEvents.DocumentationApproved, docsProofreadStep), + new TypedMessageSourceBuilder(messageType: ProcessInputEvents.UserApprovedDocument, processBuilder), ]) .SendEventTo(new ProcessStepTargetBuilder(docsPublishStep, inputMapping: (inputEvents) => { return new() { - { "document", inputEvents[docsProofreadStep.GetFullEventId(ProofReadDocumentationStep.OutputEvents.DocumentationApproved)] }, - { "userApproval", inputEvents[processBuilder.GetFullEventId(DocGenerationEvents.UserApprovedDocument)] }, + { "document", inputEvents[docsProofreadStep.GetFullEventId(ProofReadDocumentationStep.StepEvents.DocumentationApproved)] }, + { "userApproval", inputEvents[processBuilder.GetFullEventId(ProcessInputEvents.UserApprovedDocument)] }, }; })); // When event is approved by user, it gets published externally too docsPublishStep .OnFunctionResult() - .EmitExternalEvent(proxyStep, DocGenerationTopics.PublishDocumentation); + .EmitExternalEvent(proxyStep, DocGenerationTopics.PublishDocumentation) + .EmitAsPublicEvent(); return processBuilder; } } + +[JsonSerializable(typeof(ProductInfo))] +[JsonSerializable(typeof(DocumentInfo))] +public partial class DocumentJsonSerializerContext : JsonSerializerContext; diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/Steps/GenerateDocumentationStep.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/Steps/GenerateDocumentationStep.cs index 5b9f3b1591b4..58fab0b4b8e6 100644 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/Steps/GenerateDocumentationStep.cs +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/Steps/GenerateDocumentationStep.cs @@ -29,12 +29,12 @@ public static class ProcessFunctions /// /// Output events of the step, using this since 2 steps emit the same output event /// - public static class OutputEvents + public static new class StepEvents { /// /// Document Generated output event /// - public const string DocumentationGenerated = nameof(DocumentationGenerated); + public static readonly KernelProcessEventDescriptor DocumentationGenerated = new(nameof(DocumentationGenerated)); } internal GenerateDocumentationState _state = new(); @@ -84,7 +84,7 @@ public async Task GenerateDocumentationAsync(Kernel kernel, KernelProcessStepCon this._state!.LastGeneratedDocument = generatedContent; - await context.EmitEventAsync(OutputEvents.DocumentationGenerated, generatedContent); + await context.EmitEventAsync(StepEvents.DocumentationGenerated, generatedContent); } /// @@ -115,7 +115,7 @@ public async Task ApplySuggestionsAsync(Kernel kernel, KernelProcessStepContext this._state!.LastGeneratedDocument = updatedContent; - await context.EmitEventAsync(OutputEvents.DocumentationGenerated, updatedContent); + await context.EmitEventAsync(StepEvents.DocumentationGenerated, updatedContent); } } diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/Steps/ProofreadDocumentationStep.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/Steps/ProofreadDocumentationStep.cs index eddd0fda7c7e..6c757ddee6dd 100644 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/Steps/ProofreadDocumentationStep.cs +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/Steps/ProofreadDocumentationStep.cs @@ -17,16 +17,16 @@ public class ProofReadDocumentationStep : KernelProcessStep /// /// SK Process Events emitted by /// - public static class OutputEvents + public static new class StepEvents { /// /// Document has errors and needs to be revised event /// - public const string DocumentationRejected = nameof(DocumentationRejected); + public static readonly KernelProcessEventDescriptor DocumentationRejected = new(nameof(DocumentationRejected)); /// /// Document looks ok and can be processed by the next step /// - public const string DocumentationApproved = nameof(DocumentationApproved); + public static readonly KernelProcessEventDescriptor DocumentationApproved = new(nameof(DocumentationApproved)); } private readonly string _systemPrompt = """" @@ -70,16 +70,13 @@ public async Task ProofreadDocumentationAsync(Kernel kernel, KernelProcessStepCo { // Events that are getting piped to steps that will be resumed, like PublishDocumentationStep.OnPublishDocumentation // require events to be marked as public so they are persisted and restored correctly - await context.EmitEventAsync(OutputEvents.DocumentationApproved, data: document, visibility: KernelProcessEventVisibility.Public); + await context.EmitEventAsync(StepEvents.DocumentationApproved, document); } else { - await context.EmitEventAsync(new() - { - Id = OutputEvents.DocumentationRejected, + await context.EmitEventAsync(StepEvents.DocumentationRejected, // This event is getting piped to the GenerateDocumentationStep.ApplySuggestionsAsync step which expects a string with suggestions for the document - Data = $"Explanation = {formattedResponse.Explanation}, Suggestions = {string.Join(",", formattedResponse.Suggestions)} ", - }); + $"Explanation = {formattedResponse.Explanation}, Suggestions = {string.Join(",", formattedResponse.Suggestions)} "); } } diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Tests.LocalRuntime/ProcessWithCloudEvents.Tests.LocalRuntime.csproj b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Tests.LocalRuntime/ProcessWithCloudEvents.Tests.LocalRuntime.csproj new file mode 100644 index 000000000000..dc625d3d613a --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Tests.LocalRuntime/ProcessWithCloudEvents.Tests.LocalRuntime.csproj @@ -0,0 +1,53 @@ + + + + ProcessWithCloudEventsLocalRuntimeTests + net8.0 + enable + enable + + false + true + + + + $(NoWarn);CS8618,IDE0005,IDE0009,IDE1006,CA1051,CA1050,CA1707,CA1812,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0080,SKEXP0081,SKEXP0101,SKEXP0110,OPENAI001 + + Library + 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Tests.LocalRuntime/ProcessWithCloudEvents_DocumentGenerationProcess.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Tests.LocalRuntime/ProcessWithCloudEvents_DocumentGenerationProcess.cs new file mode 100644 index 000000000000..ac5c9df961fd --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Tests.LocalRuntime/ProcessWithCloudEvents_DocumentGenerationProcess.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel; +using ProcessWithCloudEvents.Processes; +using static ProcessWithCloudEvents.Processes.DocumentGenerationProcess; + +namespace ProcessWithCloudEvents.Tests.LocalRuntime; + +public class ProcessWithCloudEvents_DocumentGenerationProcess +{ + [Fact] + public void StartDocumentGenerationOnly() + { + var processBuilder = DocumentGenerationProcess.CreateProcessBuilder(); + + var t = ""; + } +} diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FishAndChipsProcess.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FishAndChipsProcess.cs index 5979fba627b4..1cc899a57ffb 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FishAndChipsProcess.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FishAndChipsProcess.cs @@ -46,9 +46,11 @@ public static ProcessBuilder CreateProcess(string processName = "FishAndChipsPro })); addCondimentsStep - .OnEvent(AddFishAndChipsCondimentsStep.OutputEvents.CondimentsAdded) + .OnEvent(AddFishAndChipsCondimentsStep.StepEvents.CondimentsAdded) .SendEventTo(new ProcessFunctionTargetBuilder(externalStep)); + externalStep.OnEvent(ExternalFishAndChipsStep.StepEvents.FishAndChipsReady).EmitAsPublicEvent(); + return processBuilder; } @@ -79,9 +81,11 @@ public static ProcessBuilder CreateProcessWithStatefulSteps(string processName = })); addCondimentsStep - .OnEvent(AddFishAndChipsCondimentsStep.OutputEvents.CondimentsAdded) + .OnEvent(AddFishAndChipsCondimentsStep.StepEvents.CondimentsAdded) .SendEventTo(new ProcessFunctionTargetBuilder(externalStep)); + externalStep.OnEvent(ExternalFishAndChipsStep.StepEvents.FishAndChipsReady).EmitAsPublicEvent(); + return processBuilder; } @@ -92,9 +96,9 @@ public static class ProcessFunctions public const string AddCondiments = nameof(AddCondiments); } - public static class OutputEvents + public static new class StepEvents { - public const string CondimentsAdded = nameof(CondimentsAdded); + public static readonly KernelProcessEventDescriptor> CondimentsAdded = new(nameof(CondimentsAdded)); } [KernelFunction(ProcessFunctions.AddCondiments)] @@ -103,12 +107,17 @@ public async Task AddCondimentsAsync(KernelProcessStepContext context, List> FishAndChipsReady = new(nameof(ProcessEvents.FishAndChipsReady)); + } + public ExternalFishAndChipsStep() : base(ProcessEvents.FishAndChipsReady) { } } } diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FishSandwichProcess.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FishSandwichProcess.cs index 9f42f1e72cbe..874047c47355 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FishSandwichProcess.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FishSandwichProcess.cs @@ -43,6 +43,8 @@ public static ProcessBuilder CreateProcess(string processName = "FishSandwichPro .OnEvent(AddSpecialSauceStep.OutputEvents.SpecialSauceAdded) .SendEventTo(new ProcessFunctionTargetBuilder(externalStep)); + externalStep.OnEvent(ProcessEvents.FishSandwichReady).EmitAsPublicEvent(); + return processBuilder; } @@ -99,6 +101,8 @@ public static ProcessBuilder CreateProcessWithStatefulStepsV2(string processName .OnEvent(AddSpecialSauceStep.OutputEvents.SpecialSauceAdded) .SendEventTo(new ProcessFunctionTargetBuilder(externalStep)); + externalStep.OnEvent(ExternalFriedFishStep.StepEvents.FishSandwichReady).EmitAsPublicEvent(); + return processBuilder; } @@ -146,6 +150,10 @@ public async Task SliceFoodAsync(KernelProcessStepContext context, List private sealed class ExternalFriedFishStep : ExternalStep { + public static new class StepEvents + { + public static readonly KernelProcessEventDescriptor> FishSandwichReady = new(nameof(ProcessEvents.FishSandwichReady)); + } public ExternalFriedFishStep() : base(ProcessEvents.FishSandwichReady) { } } } diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FriedFishProcess.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FriedFishProcess.cs index 40651be2548b..dcf96363dfd8 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FriedFishProcess.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FriedFishProcess.cs @@ -17,7 +17,7 @@ public static class ProcessEvents // When multiple processes use the same final step, the should event marked as public // so that the step event can be used as the output event of the process too. // In these samples both fried fish and potato fries end with FryStep success - public const string FriedFishReady = FryFoodStep.OutputEvents.FriedFoodReady; + public const string FriedFishReady = nameof(FryFoodStep.StepEvents.FriedFoodReady); } /// @@ -39,15 +39,15 @@ public static ProcessBuilder CreateProcess(string processName = "FriedFishProces .SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep)); gatherIngredientsStep - .OnEvent(GatherFriedFishIngredientsStep.OutputEvents.IngredientsGathered) + .OnEvent(GatherFriedFishIngredientsStep.StepEvents.IngredientsGathered) .SendEventTo(new ProcessFunctionTargetBuilder(chopStep, functionName: CutFoodStep.ProcessStepFunctions.ChopFood)); chopStep - .OnEvent(CutFoodStep.OutputEvents.ChoppingReady) + .OnEvent(CutFoodStep.StepEvents.ChoppingReady) .SendEventTo(new ProcessFunctionTargetBuilder(fryStep)); fryStep - .OnEvent(FryFoodStep.OutputEvents.FoodRuined) + .OnEvent(FryFoodStep.StepEvents.FoodRuined) .SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep)); return processBuilder; @@ -75,9 +75,11 @@ public static ProcessBuilder CreateProcessWithStatefulStepsV1(string processName .SendEventTo(new ProcessFunctionTargetBuilder(fryStep)); fryStep - .OnEvent(FryFoodStep.OutputEvents.FoodRuined) + .OnEvent(FryFoodStep.StepEvents.FoodRuined) .SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep)); + fryStep.OnEvent(FryFoodStep.StepEvents.FriedFoodReady).EmitAsPublicEvent(); + return processBuilder; } @@ -121,7 +123,7 @@ public static ProcessBuilder CreateProcessWithStatefulStepsV2(string processName .SendEventTo(new ProcessFunctionTargetBuilder(chopStep, functionName: CutFoodWithSharpeningStep.ProcessStepFunctions.ChopFood)); fryStep - .OnEvent(FryFoodStep.OutputEvents.FoodRuined) + .OnEvent(FryFoodStep.StepEvents.FoodRuined) .SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep)); return processBuilder; diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/PotatoFriesProcess.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/PotatoFriesProcess.cs index cd41c80167a9..608f6b080a75 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/PotatoFriesProcess.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/PotatoFriesProcess.cs @@ -17,7 +17,7 @@ public static class ProcessEvents // When multiple processes use the same final step, the should event marked as public // so that the step event can be used as the output event of the process too. // In these samples both fried fish and potato fries end with FryStep success - public const string PotatoFriesReady = nameof(FryFoodStep.OutputEvents.FriedFoodReady); + public const string PotatoFriesReady = nameof(FryFoodStep.StepEvents.FriedFoodReady); } /// @@ -39,17 +39,19 @@ public static ProcessBuilder CreateProcess(string processName = "PotatoFriesProc .SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep)); gatherIngredientsStep - .OnEvent(GatherPotatoFriesIngredientsStep.OutputEvents.IngredientsGathered) + .OnEvent(GatherPotatoFriesIngredientsStep.StepEvents.IngredientsGathered) .SendEventTo(new ProcessFunctionTargetBuilder(sliceStep, functionName: CutFoodStep.ProcessStepFunctions.SliceFood)); sliceStep - .OnEvent(CutFoodStep.OutputEvents.SlicingReady) + .OnEvent(CutFoodStep.StepEvents.SlicingReady) .SendEventTo(new ProcessFunctionTargetBuilder(fryStep)); fryStep - .OnEvent(FryFoodStep.OutputEvents.FoodRuined) + .OnEvent(FryFoodStep.StepEvents.FoodRuined) .SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep)); + fryStep.OnEvent(FryFoodStep.StepEvents.FriedFoodReady).EmitAsPublicEvent(); + return processBuilder; } @@ -92,7 +94,7 @@ public static ProcessBuilder CreateProcessWithStatefulSteps(string processName = .SendEventTo(new ProcessFunctionTargetBuilder(sliceStep, functionName: CutFoodWithSharpeningStep.ProcessStepFunctions.SliceFood)); fryStep - .OnEvent(FryFoodStep.OutputEvents.FoodRuined) + .OnEvent(FryFoodStep.StepEvents.FoodRuined) .SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep)); return processBuilder; diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/CutFoodStep.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/CutFoodStep.cs index 37f9aaff8ed6..5d715e4914b3 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/CutFoodStep.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/CutFoodStep.cs @@ -18,10 +18,10 @@ public static class ProcessStepFunctions public const string SliceFood = nameof(SliceFood); } - public static class OutputEvents + public static new class StepEvents { - public const string ChoppingReady = nameof(ChoppingReady); - public const string SlicingReady = nameof(SlicingReady); + public static readonly KernelProcessEventDescriptor> ChoppingReady = new(nameof(ChoppingReady)); + public static readonly KernelProcessEventDescriptor> SlicingReady = new(nameof(SlicingReady)); } [KernelFunction(ProcessStepFunctions.ChopFood)] @@ -30,7 +30,7 @@ public async Task ChopFoodAsync(KernelProcessStepContext context, List f var foodToBeCut = foodActions.First(); foodActions.Add(this.getActionString(foodToBeCut, "chopped")); Console.WriteLine($"CUTTING_STEP: Ingredient {foodToBeCut} has been chopped!"); - await context.EmitEventAsync(new() { Id = OutputEvents.ChoppingReady, Data = foodActions }); + await context.EmitEventAsync(StepEvents.ChoppingReady, foodActions); } [KernelFunction(ProcessStepFunctions.SliceFood)] @@ -39,7 +39,7 @@ public async Task SliceFoodAsync(KernelProcessStepContext context, List var foodToBeCut = foodActions.First(); foodActions.Add(this.getActionString(foodToBeCut, "sliced")); Console.WriteLine($"CUTTING_STEP: Ingredient {foodToBeCut} has been sliced!"); - await context.EmitEventAsync(new() { Id = OutputEvents.SlicingReady, Data = foodActions }); + await context.EmitEventAsync(StepEvents.SlicingReady, foodActions); } private string getActionString(string food, string action) diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/ExternalStep.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/ExternalStep.cs index b97c43138c7a..64d4f6bb705d 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/ExternalStep.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/ExternalStep.cs @@ -15,6 +15,6 @@ public class ExternalStep(string externalEventName) : KernelProcessStep [KernelFunction] public async Task EmitExternalEventAsync(KernelProcessStepContext context, object data) { - await context.EmitEventAsync(new() { Id = this._externalEventName, Data = data, Visibility = KernelProcessEventVisibility.Public }); + await context.EmitEventAsync(this._externalEventName, data); } } diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/FryFoodStep.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/FryFoodStep.cs index bfc2dd50f5ae..bef9636251ef 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/FryFoodStep.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/FryFoodStep.cs @@ -17,10 +17,10 @@ public static class ProcessStepFunctions public const string FryFood = nameof(FryFood); } - public static class OutputEvents + public static new class StepEvents { - public const string FoodRuined = nameof(FoodRuined); - public const string FriedFoodReady = nameof(FriedFoodReady); + public static readonly KernelProcessEventDescriptor> FoodRuined = new(nameof(FoodRuined)); + public static readonly KernelProcessEventDescriptor> FriedFoodReady = new(nameof(FriedFoodReady)); } private readonly Random _randomSeed = new(); @@ -38,12 +38,12 @@ public async Task FryFoodAsync(KernelProcessStepContext context, List fo // Oh no! Food got burnt :( foodActions.Add($"{foodToFry}_frying_failed"); Console.WriteLine($"FRYING_STEP: Ingredient {foodToFry} got burnt while frying :("); - await context.EmitEventAsync(new() { Id = OutputEvents.FoodRuined, Data = foodActions }); + await context.EmitEventAsync(StepEvents.FoodRuined, foodActions); return; } foodActions.Add($"{foodToFry}_frying_succeeded"); Console.WriteLine($"FRYING_STEP: Ingredient {foodToFry} is ready!"); - await context.EmitEventAsync(new() { Id = OutputEvents.FriedFoodReady, Data = foodActions, Visibility = KernelProcessEventVisibility.Public }); + await context.EmitEventAsync(StepEvents.FriedFoodReady, foodActions); } } diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/GatherIngredientsStep.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/GatherIngredientsStep.cs index 15d253f022b7..d18133ed69de 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/GatherIngredientsStep.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/GatherIngredientsStep.cs @@ -14,9 +14,9 @@ public static class ProcessStepFunctions public const string GatherIngredients = nameof(GatherIngredients); } - public static class OutputEvents + public static new class StepEvents { - public const string IngredientsGathered = nameof(IngredientsGathered); + public static readonly KernelProcessEventDescriptor> IngredientsGathered = new(nameof(IngredientsGathered)); } private readonly FoodIngredients _ingredient; @@ -45,7 +45,7 @@ public virtual async Task GatherIngredientsAsync(KernelProcessStepContext contex updatedFoodActions.Add($"{ingredient}_gathered"); Console.WriteLine($"GATHER_INGREDIENT: Gathered ingredient {ingredient}"); - await context.EmitEventAsync(new() { Id = OutputEvents.IngredientsGathered, Data = updatedFoodActions }); + await context.EmitEventAsync(StepEvents.IngredientsGathered, updatedFoodActions); } }