From 4bda49618132efbdba7386fd6a5bc81c00655789 Mon Sep 17 00:00:00 2001 From: Estefania Tenorio <8483207+esttenorio@users.noreply.github.com> Date: Thu, 14 Nov 2024 15:32:25 -0800 Subject: [PATCH 01/13] Cloud events initial hook up changes + working sample --- .../Step03/Processes/FriedFishProcess.cs | 69 ++++++++++++--- .../Step03/Step03a_FoodPreparation.cs | 2 + .../Process.Abstractions/KernelProcess.cs | 8 +- .../Process.Abstractions/KernelProcessEdge.cs | 12 ++- .../KernelProcessEventsSubscriber.cs | 38 +++++++++ .../KernelProcessEventsSubscriberInfo.cs | 84 +++++++++++++++++++ .../Process.Core/ProcessBuilder.cs | 50 ++++++++++- .../Process.Core/ProcessEdgeBuilder.cs | 2 +- .../Process.Core/ProcessStepBuilder.cs | 7 +- .../Process.Core/ProcessStepEdgeBuilder.cs | 28 ++++++- .../Process.LocalRuntime/LocalProcess.cs | 22 ++++- .../ProcessMessageSerializationTests.cs | 2 +- .../Core/ProcessStepBuilderTests.cs | 2 +- .../Core/ProcessStepEdgeBuilderTests.cs | 14 ++-- .../Process.Utilities.UnitTests/CloneTests.cs | 8 +- .../process/Runtime/ProcessMessage.cs | 4 + .../process/Runtime/ProcessMessageFactory.cs | 2 +- 17 files changed, 314 insertions(+), 40 deletions(-) create mode 100644 dotnet/src/Experimental/Process.Abstractions/KernelProcessEventsSubscriber.cs create mode 100644 dotnet/src/Experimental/Process.Abstractions/KernelProcessEventsSubscriberInfo.cs diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FriedFishProcess.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FriedFishProcess.cs index 076c4f932641..9b4b5389ae0e 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FriedFishProcess.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FriedFishProcess.cs @@ -6,6 +6,40 @@ using Step03.Steps; namespace Step03.Processes; +public enum FishProcessEvents +{ + PrepareFriedFish, + MiddleStep, + FriedFishFailed, + FriedFishReady, +} + +public class FriedFishEventSubscribers : KernelProcessEventsSubscriber +{ + [ProcessEventSubscriber(FishProcessEvents.MiddleStep)] + public void OnMiddleStep(List data) + { + // do something with data + Console.WriteLine($"=============> ON MIDDLE STEP: {data.FirstOrDefault() ?? ""}"); + } + + [ProcessEventSubscriber(FishProcessEvents.FriedFishReady)] + public void OnPrepareFish(object data) + { + // do something with data + // TODO: if event is linked to last event it doesnt get hit + // even when it may be linked to StopProcess() -> need additional special step? + Console.WriteLine("=============> ON FISH READY"); + } + + [ProcessEventSubscriber(FishProcessEvents.FriedFishFailed)] + public void OnFriedFisFailed(object data) + { + // do something with data + Console.WriteLine("=============> ON FISH FAILED"); + } +} + /// /// Sample process that showcases how to create a process with sequential steps and reuse of existing steps.
///
@@ -26,20 +60,21 @@ public static class ProcessEvents /// /// name of the process /// - public static ProcessBuilder CreateProcess(string processName = "FriedFishProcess") + public static ProcessBuilder CreateProcess(string processName = "FriedFishProcess") { - var processBuilder = new ProcessBuilder(processName); + var processBuilder = new ProcessBuilder(processName); var gatherIngredientsStep = processBuilder.AddStepFromType(); var chopStep = processBuilder.AddStepFromType(); var fryStep = processBuilder.AddStepFromType(); processBuilder - .OnInputEvent(ProcessEvents.PrepareFriedFish) + .OnInputEvent(FishProcessEvents.PrepareFriedFish) .SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep)); gatherIngredientsStep .OnEvent(GatherFriedFishIngredientsStep.OutputEvents.IngredientsGathered) + .EmitAsProcessEvent(processBuilder.GetProcessEvent(FishProcessEvents.MiddleStep)) .SendEventTo(new ProcessFunctionTargetBuilder(chopStep, functionName: CutFoodStep.Functions.ChopFood)); chopStep @@ -48,8 +83,14 @@ public static ProcessBuilder CreateProcess(string processName = "FriedFishProces fryStep .OnEvent(FryFoodStep.OutputEvents.FoodRuined) + .EmitAsProcessEvent(processBuilder.GetProcessEvent(FishProcessEvents.FriedFishFailed)) .SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep)); + fryStep + .OnEvent(FryFoodStep.OutputEvents.FriedFoodReady) + .EmitAsProcessEvent(processBuilder.GetProcessEvent(FishProcessEvents.FriedFishReady)) + .StopProcess(); + return processBuilder; } @@ -81,23 +122,23 @@ public static ProcessBuilder CreateProcessWithStatefulStepsV1(string processName return processBuilder; } - /// - /// For a visual reference of the FriedFishProcess with stateful steps check this - /// diagram - /// - /// name of the process - /// - public static ProcessBuilder CreateProcessWithStatefulStepsV2(string processName = "FriedFishWithStatefulStepsProcess") + /// + /// For a visual reference of the FriedFishProcess with stateful steps check this + /// diagram + /// + /// name of the process + /// + public static ProcessBuilder CreateProcessWithStatefulStepsV2(string processName = "FriedFishWithStatefulStepsProcess") { // It is recommended to specify process version in case this process is used as a step by another process - var processBuilder = new ProcessBuilder(processName) { Version = "FriedFishProcess.v2" }; + var processBuilder = new ProcessBuilder(processName) { Version = "FriedFishProcess.v2" }; var gatherIngredientsStep = processBuilder.AddStepFromType(name: "gatherFishIngredientStep", aliases: ["GatherFriedFishIngredientsWithStockStep"]); var chopStep = processBuilder.AddStepFromType(name: "chopFishStep", aliases: ["CutFoodStep"]); var fryStep = processBuilder.AddStepFromType(name: "fryFishStep", aliases: ["FryFoodStep"]); processBuilder - .OnInputEvent(ProcessEvents.PrepareFriedFish) + .GetProcessEvent(FishProcessEvents.PrepareFriedFish) .SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep)); gatherIngredientsStep @@ -106,6 +147,7 @@ public static ProcessBuilder CreateProcessWithStatefulStepsV2(string processName gatherIngredientsStep .OnEvent(GatherFriedFishIngredientsWithStockStep.OutputEvents.IngredientsOutOfStock) + .EmitAsProcessEvent(processBuilder.GetProcessEvent(FishProcessEvents.FriedFishFailed)) .StopProcess(); chopStep @@ -124,6 +166,9 @@ public static ProcessBuilder CreateProcessWithStatefulStepsV2(string processName .OnEvent(FryFoodStep.OutputEvents.FoodRuined) .SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep)); + fryStep.OnEvent(FryFoodStep.OutputEvents.FriedFoodReady) + .EmitAsProcessEvent(processBuilder.GetProcessEvent(FishProcessEvents.FriedFishReady)); + return processBuilder; } diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Step03a_FoodPreparation.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Step03a_FoodPreparation.cs index c299960c07a9..477e7d3a70ad 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step03/Step03a_FoodPreparation.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Step03a_FoodPreparation.cs @@ -22,6 +22,8 @@ public class Step03a_FoodPreparation(ITestOutputHelper output) : BaseTest(output public async Task UsePrepareFriedFishProcessAsync() { var process = FriedFishProcess.CreateProcess(); + process.LinkEventSubscribersFromType(); + await UsePrepareSpecificProductAsync(process, FriedFishProcess.ProcessEvents.PrepareFriedFish); } diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcess.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcess.cs index 3b72a9aff192..743851e7b14d 100644 --- a/dotnet/src/Experimental/Process.Abstractions/KernelProcess.cs +++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcess.cs @@ -9,13 +9,15 @@ namespace Microsoft.SemanticKernel; /// /// A serializable representation of a Process. /// -public sealed record KernelProcess : KernelProcessStepInfo +public sealed record KernelProcess : KernelProcessStepInfo // TODO: Should be renamed to KernelProcessInfo to keep consistent names { /// /// The collection of Steps in the Process. /// public IList Steps { get; } + public KernelProcessEventsSubscriberInfo? EventsSubscriber { get; set; } = null; + /// /// Captures Kernel Process State into after process has run /// @@ -31,12 +33,14 @@ public KernelProcessStateMetadata ToProcessStateMetadata() /// The process state. /// The steps of the process. /// The edges of the process. - public KernelProcess(KernelProcessState state, IList steps, Dictionary>? edges = null) + /// TODO: may need to reorder params + public KernelProcess(KernelProcessState state, IList steps, Dictionary>? edges = null, KernelProcessEventsSubscriberInfo? eventsSubscriber = null) : base(typeof(KernelProcess), state, edges ?? []) { Verify.NotNull(steps); Verify.NotNullOrWhiteSpace(state.Name); this.Steps = [.. steps]; + this.EventsSubscriber = eventsSubscriber; } } diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessEdge.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessEdge.cs index 224d5b67bb56..71b86dd2b472 100644 --- a/dotnet/src/Experimental/Process.Abstractions/KernelProcessEdge.cs +++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessEdge.cs @@ -17,6 +17,12 @@ public sealed class KernelProcessEdge [DataMember] public string SourceStepId { get; init; } + [DataMember] + public string SourceEventName { get; init; } + + [DataMember] + public string SourceEventId { get; init; } + /// /// The collection of s that are the output of the source Step. /// @@ -26,12 +32,16 @@ public sealed class KernelProcessEdge /// /// Creates a new instance of the class. /// - public KernelProcessEdge(string sourceStepId, KernelProcessFunctionTarget outputTarget) + public KernelProcessEdge(string sourceStepId, KernelProcessFunctionTarget outputTarget, string sourceEventName, string sourceEventId) { Verify.NotNullOrWhiteSpace(sourceStepId); + Verify.NotNullOrWhiteSpace(sourceEventId); + Verify.NotNullOrWhiteSpace(sourceEventName); Verify.NotNull(outputTarget); this.SourceStepId = sourceStepId; + this.SourceEventId = sourceEventId; + this.SourceEventName = sourceEventName; this.OutputTarget = outputTarget; } } diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventsSubscriber.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventsSubscriber.cs new file mode 100644 index 000000000000..56357b167349 --- /dev/null +++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventsSubscriber.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace Microsoft.SemanticKernel.Process; +/// +/// Attribute to set Process related steps to link Process Events to specific functions to execute when the event is emitted outside the Process +/// +/// Enum that contains all process events that could be subscribed to +public abstract class KernelProcessEventsSubscriber where TEvents : Enum +{ + /// + /// Attribute to set Process related steps to link Process Events to specific functions to execute when the event is emitted outside the Process + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class ProcessEventSubscriberAttribute : Attribute + { + /// + /// Gets the enum of the event that the function is linked to + /// + public TEvents EventEnum { get; } + + /// + /// Gets the string of the event name that the function is linked to + /// + public string EventName { get; } + + /// + /// Initializes the attribute. + /// + /// Specific Process Event enum + public ProcessEventSubscriberAttribute(TEvents eventEnum) + { + this.EventEnum = eventEnum; + this.EventName = Enum.GetName(typeof(TEvents), eventEnum) ?? ""; + } + } +} diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventsSubscriberInfo.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventsSubscriberInfo.cs new file mode 100644 index 000000000000..43bbdf6b8fd0 --- /dev/null +++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventsSubscriberInfo.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System; +using Microsoft.SemanticKernel.Process; + +namespace Microsoft.SemanticKernel; + +public class KernelProcessEventsSubscriberInfo +{ + private readonly Dictionary> _eventHandlers = []; + private readonly Dictionary _stepEventProcessEventMap = []; + private Type? _processEventSubscriberType = null; + + protected void Subscribe(string eventName, MethodInfo method) + { + if (this._eventHandlers.TryGetValue(eventName, out List? eventHandlers) && eventHandlers != null) + { + eventHandlers.Add(method); + } + } + + public void LinkStepEventToProcessEvent(string stepEventId, string processEventId) + { + this._stepEventProcessEventMap.Add(stepEventId, processEventId); + if (!this._eventHandlers.ContainsKey(processEventId)) + { + this._eventHandlers.Add(processEventId, []); + } + } + + public void TryInvokeProcessEventFromStepMessage(string stepEventId, object? data) + { + if (this._stepEventProcessEventMap.TryGetValue(stepEventId, out var processEvent) && processEvent != null) + { + this.InvokeProcessEvent(processEvent, data); + } + } + + public void InvokeProcessEvent(string eventName, object? data) + { + if (this._processEventSubscriberType != null && this._eventHandlers.TryGetValue(eventName, out List? linkedMethods) && linkedMethods != null) + { + foreach (var method in linkedMethods) + { + // TODO-estenori: Avoid creating a new instance every time a function is invoked - create instance once only? + var instance = Activator.CreateInstance(this._processEventSubscriberType, []); + method.Invoke(instance, [data]); + } + } + } + + /// + /// Extracts the event properties and function details of the functions with the annotator + /// + /// + /// + /// + public void SubscribeToEventsFromClass() where TEventListeners : KernelProcessEventsSubscriber where TEvents : Enum + { + var methods = typeof(TEventListeners).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.DeclaredOnly); + + foreach (var method in methods) + { + if (method.GetCustomAttributes(typeof(KernelProcessEventsSubscriber<>.ProcessEventSubscriberAttribute), false).FirstOrDefault() is KernelProcessEventsSubscriber.ProcessEventSubscriberAttribute attribute) + { + if (attribute.EventEnum.GetType() != typeof(TEvents)) + { + throw new InvalidOperationException($"The event type {attribute.EventEnum.GetType().Name} does not match the expected type {typeof(TEvents).Name}"); + } + + this.Subscribe(attribute.EventName, method); + } + } + + this._processEventSubscriberType = typeof(TEventListeners); + } + + public KernelProcessEventsSubscriberInfo() + { + } +} diff --git a/dotnet/src/Experimental/Process.Core/ProcessBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessBuilder.cs index e8ed21744da1..1cf889e04ab4 100644 --- a/dotnet/src/Experimental/Process.Core/ProcessBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/ProcessBuilder.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.SemanticKernel.Process; using Microsoft.SemanticKernel.Process.Internal; using Microsoft.SemanticKernel.Process.Models; @@ -11,7 +12,7 @@ namespace Microsoft.SemanticKernel; /// /// Provides functionality for incrementally defining a process. /// -public sealed class ProcessBuilder : ProcessStepBuilder +public class ProcessBuilder : ProcessStepBuilder { /// The collection of steps within this process. private readonly List _steps = []; @@ -22,6 +23,8 @@ public sealed class ProcessBuilder : ProcessStepBuilder /// Maps external input event Ids to the target entry step for the event. private readonly Dictionary _externalEventTargetMap = []; + internal KernelProcessEventsSubscriberInfo _eventsSubscriber; + /// /// A boolean indicating if the current process is a step within another process. /// @@ -108,7 +111,6 @@ internal override KernelProcessStepInfo BuildStep(KernelProcessStepStateMetadata } #region Public Interface - /// /// A read-only collection of steps in the process. /// @@ -258,7 +260,7 @@ public KernelProcess Build(KernelProcessStateMetadata? stateMetadata = null) // Create the process var state = new KernelProcessState(this.Name, version: this.Version, id: this.HasParentProcess ? this.Id : null); - var process = new KernelProcess(state, builtSteps, builtEdges); + var process = new KernelProcess(state, builtSteps, builtEdges, this._eventsSubscriber); return process; } @@ -269,7 +271,49 @@ public KernelProcess Build(KernelProcessStateMetadata? stateMetadata = null) public ProcessBuilder(string name) : base(name) { + this._eventsSubscriber = new(); + } + + #endregion +} + +public sealed class ProcessBuilder : ProcessBuilder where TEvents : Enum, new() +{ + private readonly Dictionary _eventNames = []; + + private void PopulateEventNames() + { + foreach (TEvents processEvent in Enum.GetValues(typeof(TEvents))) + { + this._eventNames.Add(processEvent, Enum.GetName(typeof(TEvents), processEvent)!); + } + } + + #region Public Interface + + public void LinkEventSubscribersFromType() where TEventListeners : KernelProcessEventsSubscriber + { + this._eventsSubscriber.SubscribeToEventsFromClass(); + } + + public ProcessEdgeBuilder OnInputEvent(TEvents eventId) + { + return this.OnInputEvent(this.GetEventName(eventId)); + } + + public string GetEventName(TEvents processEvent) + { + return this._eventNames[processEvent]; } + public ProcessEdgeBuilder GetProcessEvent(TEvents processEvent) + { + return this.OnInputEvent(this.GetEventName(processEvent)); + } + + public ProcessBuilder(string name) : base(name) + { + this.PopulateEventNames(); + } #endregion } diff --git a/dotnet/src/Experimental/Process.Core/ProcessEdgeBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessEdgeBuilder.cs index 076912f318ec..bc6712dcd75a 100644 --- a/dotnet/src/Experimental/Process.Core/ProcessEdgeBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/ProcessEdgeBuilder.cs @@ -36,7 +36,7 @@ internal ProcessEdgeBuilder(ProcessBuilder source, string eventId) public ProcessEdgeBuilder SendEventTo(ProcessFunctionTargetBuilder target) { this.Target = target; - ProcessStepEdgeBuilder edgeBuilder = new(this.Source, this.EventId) { Target = this.Target }; + ProcessStepEdgeBuilder edgeBuilder = new(this.Source, this.EventId, this.EventId) { Target = this.Target }; this.Source.LinkTo(this.EventId, edgeBuilder); return new ProcessEdgeBuilder(this.Source, this.EventId); diff --git a/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs index 9cc9aabcbac0..3b7058ee0718 100644 --- a/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs @@ -37,11 +37,11 @@ public abstract class ProcessStepBuilder /// /// The Id of the event of interest. /// An instance of . - public ProcessStepEdgeBuilder OnEvent(string eventId) + public ProcessStepEdgeBuilder OnEvent(string eventName) { // scope the event to this instance of this step - var scopedEventId = this.GetScopedEventId(eventId); - return new ProcessStepEdgeBuilder(this, scopedEventId); + var scopedEventId = this.GetScopedEventId(eventName); + return new ProcessStepEdgeBuilder(this, scopedEventId, eventName); } /// @@ -51,6 +51,7 @@ public ProcessStepEdgeBuilder OnEvent(string eventId) /// An instance of . public ProcessStepEdgeBuilder OnFunctionResult(string functionName) { + // TODO: ADD CHECK SO FUNCTION_NAME IS NOT EMPTY OR ADD FUNCTION RESOLVER IN CASE STEP HAS ONLY ONE FUNCTION return this.OnEvent($"{functionName}.OnResult"); } diff --git a/dotnet/src/Experimental/Process.Core/ProcessStepEdgeBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessStepEdgeBuilder.cs index 2e4afbfa51e9..5aafadc096c0 100644 --- a/dotnet/src/Experimental/Process.Core/ProcessStepEdgeBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/ProcessStepEdgeBuilder.cs @@ -14,9 +14,15 @@ public sealed class ProcessStepEdgeBuilder /// /// The event Id that the edge fires on. + /// Unique event Id linked to the source id. /// internal string EventId { get; } + /// + /// The event name that the edge fires on. + /// + internal string EventName { get; } + /// /// The source step of the edge. /// @@ -27,13 +33,16 @@ public sealed class ProcessStepEdgeBuilder /// /// The source step. /// The Id of the event. - internal ProcessStepEdgeBuilder(ProcessStepBuilder source, string eventId) + /// The name of the event + internal ProcessStepEdgeBuilder(ProcessStepBuilder source, string eventId, string eventName) { Verify.NotNull(source); Verify.NotNullOrWhiteSpace(eventId); + Verify.NotNullOrWhiteSpace(eventName); this.Source = source; this.EventId = eventId; + this.EventName = eventName; } /// @@ -44,7 +53,7 @@ internal KernelProcessEdge Build() Verify.NotNull(this.Source?.Id); Verify.NotNull(this.Target); - return new KernelProcessEdge(this.Source.Id, this.Target.Build()); + return new KernelProcessEdge(this.Source.Id, this.Target.Build(), this.EventName, this.EventId); } /// @@ -62,7 +71,20 @@ public ProcessStepEdgeBuilder SendEventTo(ProcessFunctionTargetBuilder target) this.Target = target; this.Source.LinkTo(this.EventId, this); - return new ProcessStepEdgeBuilder(this.Source, this.EventId); + return new ProcessStepEdgeBuilder(this.Source, this.EventId, this.EventName); + } + + /// + /// Forward specific step events to process events so specific functions linked get executed + /// when receiving the specific event + /// + /// + /// + public ProcessStepEdgeBuilder EmitAsProcessEvent(ProcessEdgeBuilder processEdge) + { + processEdge.Source._eventsSubscriber?.LinkStepEventToProcessEvent(this.EventId, processEventId: processEdge.EventId); + + return new ProcessStepEdgeBuilder(this.Source, this.EventId, this.EventName); } /// diff --git a/dotnet/src/Experimental/Process.LocalRuntime/LocalProcess.cs b/dotnet/src/Experimental/Process.LocalRuntime/LocalProcess.cs index 7b4f239f8965..4234a727710a 100644 --- a/dotnet/src/Experimental/Process.LocalRuntime/LocalProcess.cs +++ b/dotnet/src/Experimental/Process.LocalRuntime/LocalProcess.cs @@ -256,6 +256,9 @@ private async Task Internal_ExecuteAsync(Kernel? kernel = null, int maxSuperstep List messageTasks = []; foreach (var message in messagesToProcess) { + // Check if message has external event handler linked to it + this.TryEmitMessageToExternalSubscribers(message); + // Check for end condition if (message.DestinationId.Equals(ProcessConstants.EndStepName, StringComparison.OrdinalIgnoreCase)) { @@ -291,6 +294,16 @@ private async Task Internal_ExecuteAsync(Kernel? kernel = null, int maxSuperstep return; } + private void TryEmitMessageToExternalSubscribers(string processEventId, object? processEventData) + { + this._process.EventsSubscriber?.TryInvokeProcessEventFromStepMessage(processEventId, processEventData); + } + + private void TryEmitMessageToExternalSubscribers(ProcessMessage message) + { + this.TryEmitMessageToExternalSubscribers(message.EventId, message.TargetEventData); + } + /// /// Processes external events that have been sent to the process, translates them to s, and enqueues /// them to the provided message channel so that they can be processed in the next superstep. @@ -338,9 +351,9 @@ private void EnqueueStepMessages(LocalStep step, Queue messageCh } // Error event was raised with no edge to handle it, send it to an edge defined as the global error target. - if (!foundEdge && stepEvent.IsError) + if (!foundEdge) { - if (this._outputEdges.TryGetValue(ProcessConstants.GlobalErrorEventId, out List? edges)) + if (stepEvent.IsError && this._outputEdges.TryGetValue(ProcessConstants.GlobalErrorEventId, out List? edges)) { foreach (KernelProcessEdge edge in edges) { @@ -348,6 +361,11 @@ private void EnqueueStepMessages(LocalStep step, Queue messageCh messageChannel.Enqueue(message); } } + else + { + // Checking in case the step with no edges linked to it has event that should be emitted externally + this.TryEmitMessageToExternalSubscribers(stepEvent.QualifiedId, stepEvent.Data); + } } } } diff --git a/dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/ProcessMessageSerializationTests.cs b/dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/ProcessMessageSerializationTests.cs index f3de5b7cfa32..494a468efd44 100644 --- a/dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/ProcessMessageSerializationTests.cs +++ b/dotnet/src/Experimental/Process.Runtime.Dapr.UnitTests/ProcessMessageSerializationTests.cs @@ -127,7 +127,7 @@ private static void VerifyContainerSerialization(ProcessMessage[] processMessage private static ProcessMessage CreateMessage(Dictionary values) { - return new ProcessMessage("test-source", "test-destination", "test-function", values) + return new ProcessMessage("test-event", "test-eventid", "test-source", "test-destination", "test-function", values) { TargetEventData = "testdata", TargetEventId = "targetevent", diff --git a/dotnet/src/Experimental/Process.UnitTests/Core/ProcessStepBuilderTests.cs b/dotnet/src/Experimental/Process.UnitTests/Core/ProcessStepBuilderTests.cs index 07c4802c8731..0d5e085f2ac7 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Core/ProcessStepBuilderTests.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Core/ProcessStepBuilderTests.cs @@ -91,7 +91,7 @@ public void LinkToShouldAddEdge() { // Arrange var stepBuilder = new TestProcessStepBuilder("TestStep"); - var edgeBuilder = new ProcessStepEdgeBuilder(stepBuilder, "TestEvent"); + var edgeBuilder = new ProcessStepEdgeBuilder(stepBuilder, "TestEvent", "TestEvent"); // Act stepBuilder.LinkTo("TestEvent", edgeBuilder); diff --git a/dotnet/src/Experimental/Process.UnitTests/Core/ProcessStepEdgeBuilderTests.cs b/dotnet/src/Experimental/Process.UnitTests/Core/ProcessStepEdgeBuilderTests.cs index 3e3f128e1753..35d81ef1ca97 100644 --- a/dotnet/src/Experimental/Process.UnitTests/Core/ProcessStepEdgeBuilderTests.cs +++ b/dotnet/src/Experimental/Process.UnitTests/Core/ProcessStepEdgeBuilderTests.cs @@ -21,7 +21,7 @@ public void ConstructorShouldInitializeProperties() var eventType = "Event1"; // Act - var builder = new ProcessStepEdgeBuilder(source, eventType); + var builder = new ProcessStepEdgeBuilder(source, eventType, eventType); // Assert Assert.Equal(source, builder.Source); @@ -36,7 +36,7 @@ public void SendEventToShouldSetOutputTarget() { // Arrange var source = new ProcessStepBuilder(TestStep.Name); - var builder = new ProcessStepEdgeBuilder(source, "Event1"); + var builder = new ProcessStepEdgeBuilder(source, "Event1", "Event1"); var outputTarget = new ProcessFunctionTargetBuilder(new ProcessStepBuilder("OutputStep")); // Act @@ -54,7 +54,7 @@ public void SendEventToShouldSetMultipleOutputTargets() { // Arrange var source = new ProcessStepBuilder(TestStep.Name); - var builder = new ProcessStepEdgeBuilder(source, "Event1"); + var builder = new ProcessStepEdgeBuilder(source, "Event1", "Event1"); var outputTargetA = new ProcessFunctionTargetBuilder(new ProcessStepBuilder("StepA")); var outputTargetB = new ProcessFunctionTargetBuilder(new ProcessStepBuilder("StepB")); @@ -75,7 +75,7 @@ public void SendEventToShouldThrowIfOutputTargetAlreadySet() { // Arrange var source = new ProcessStepBuilder(TestStep.Name); - var builder = new ProcessStepEdgeBuilder(source, "Event1"); + var builder = new ProcessStepEdgeBuilder(source, "Event1", "Event1"); var outputTarget1 = new ProcessFunctionTargetBuilder(source); var outputTarget2 = new ProcessFunctionTargetBuilder(source); @@ -94,7 +94,7 @@ public void StopProcessShouldSetOutputTargetToEndStep() { // Arrange var source = new ProcessStepBuilder(TestStep.Name); - var builder = new ProcessStepEdgeBuilder(source, "Event1"); + var builder = new ProcessStepEdgeBuilder(source, "Event1", "Event1"); // Act builder.StopProcess(); @@ -111,7 +111,7 @@ public void StopProcessShouldThrowIfOutputTargetAlreadySet() { // Arrange var source = new ProcessStepBuilder(TestStep.Name); - var builder = new ProcessStepEdgeBuilder(source, "Event1"); + var builder = new ProcessStepEdgeBuilder(source, "Event1", "Event1"); var outputTarget = new ProcessFunctionTargetBuilder(source); // Act @@ -129,7 +129,7 @@ public void BuildShouldReturnKernelProcessEdge() { // Arrange var source = new ProcessStepBuilder(TestStep.Name); - var builder = new ProcessStepEdgeBuilder(source, "Event1"); + var builder = new ProcessStepEdgeBuilder(source, "Event1", "Event1"); var outputTarget = new ProcessFunctionTargetBuilder(source); builder.SendEventTo(outputTarget); diff --git a/dotnet/src/Experimental/Process.Utilities.UnitTests/CloneTests.cs b/dotnet/src/Experimental/Process.Utilities.UnitTests/CloneTests.cs index e1f8957038cd..f615a2d2c4e8 100644 --- a/dotnet/src/Experimental/Process.Utilities.UnitTests/CloneTests.cs +++ b/dotnet/src/Experimental/Process.Utilities.UnitTests/CloneTests.cs @@ -139,16 +139,18 @@ private static void VerifyProcess(KernelProcess expected, KernelProcess actual) } } - private static Dictionary> CreateTestEdges() => - new() + private static Dictionary> CreateTestEdges() + { + return new() { { "sourceId", [ - new KernelProcessEdge("sourceId", new KernelProcessFunctionTarget("sourceId", "targetFunction", "targetParameter", "targetEventId")), + new KernelProcessEdge("sourceId", new KernelProcessFunctionTarget("sourceId", "targetFunction", "targetParameter", "targetEventId"), "eventName", "eventId"), ] } }; + } private sealed record TestState { diff --git a/dotnet/src/InternalUtilities/process/Runtime/ProcessMessage.cs b/dotnet/src/InternalUtilities/process/Runtime/ProcessMessage.cs index 6b7a73c57a15..92b15a5495f3 100644 --- a/dotnet/src/InternalUtilities/process/Runtime/ProcessMessage.cs +++ b/dotnet/src/InternalUtilities/process/Runtime/ProcessMessage.cs @@ -10,12 +10,16 @@ namespace Microsoft.SemanticKernel.Process.Runtime; /// /// Initializes a new instance of the class. /// +/// Original name of the name of the event triggered +/// Original name of the name of the event triggered /// The source identifier of the message. /// The destination identifier of the message. /// The name of the function associated with the message. /// The dictionary of values associated with the message. [KnownType(typeof(KernelProcessError))] public record ProcessMessage( + string EventName, + string EventId, string SourceId, string DestinationId, string FunctionName, diff --git a/dotnet/src/InternalUtilities/process/Runtime/ProcessMessageFactory.cs b/dotnet/src/InternalUtilities/process/Runtime/ProcessMessageFactory.cs index f1bcea825c22..2b706e8b39bf 100644 --- a/dotnet/src/InternalUtilities/process/Runtime/ProcessMessageFactory.cs +++ b/dotnet/src/InternalUtilities/process/Runtime/ProcessMessageFactory.cs @@ -24,7 +24,7 @@ internal static ProcessMessage CreateFromEdge(KernelProcessEdge edge, object? da parameterValue.Add(target.ParameterName!, data); } - ProcessMessage newMessage = new(edge.SourceStepId, target.StepId, target.FunctionName, parameterValue) + ProcessMessage newMessage = new(edge.SourceEventName, edge.SourceEventId, edge.SourceStepId, target.StepId, target.FunctionName, parameterValue) { TargetEventId = target.TargetEventId, TargetEventData = data From 2f600027091bd75dcba7b6aadabd8b9e0f9e0bfa Mon Sep 17 00:00:00 2001 From: Estefania Tenorio <8483207+esttenorio@users.noreply.github.com> Date: Thu, 14 Nov 2024 16:00:06 -0800 Subject: [PATCH 02/13] hooking up IServiceProvider to Subscribers class --- .../Step03/Processes/FriedFishProcess.cs | 3 +++ .../KernelProcessEventsSubscriber.cs | 13 +++++++++++- .../KernelProcessEventsSubscriberInfo.cs | 20 +++++++++++++------ .../Process.Core/ProcessBuilder.cs | 4 ++-- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FriedFishProcess.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FriedFishProcess.cs index 9b4b5389ae0e..4f0a7201ae51 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FriedFishProcess.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FriedFishProcess.cs @@ -16,6 +16,9 @@ public enum FishProcessEvents public class FriedFishEventSubscribers : KernelProcessEventsSubscriber { + // TODO-estenori: figure out how to disallow and not need constructor on when using KernelProcessEventsSubscriber as base class + public FriedFishEventSubscribers(IServiceProvider? serviceProvider = null) : base(serviceProvider) { } + [ProcessEventSubscriber(FishProcessEvents.MiddleStep)] public void OnMiddleStep(List data) { diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventsSubscriber.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventsSubscriber.cs index 56357b167349..99cc687f8733 100644 --- a/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventsSubscriber.cs +++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventsSubscriber.cs @@ -7,8 +7,19 @@ namespace Microsoft.SemanticKernel.Process; /// Attribute to set Process related steps to link Process Events to specific functions to execute when the event is emitted outside the Process /// /// Enum that contains all process events that could be subscribed to -public abstract class KernelProcessEventsSubscriber where TEvents : Enum +public class KernelProcessEventsSubscriber where TEvents : Enum { + protected readonly IServiceProvider? ServiceProvider; + + /// + /// Initializes a new instance of the class. + /// + /// Optional service provider for resolving dependencies + public KernelProcessEventsSubscriber(IServiceProvider? serviceProvider = null) + { + this.ServiceProvider = serviceProvider; + } + /// /// Attribute to set Process related steps to link Process Events to specific functions to execute when the event is emitted outside the Process /// diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventsSubscriberInfo.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventsSubscriberInfo.cs index 43bbdf6b8fd0..cbdd4883e878 100644 --- a/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventsSubscriberInfo.cs +++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventsSubscriberInfo.cs @@ -14,6 +14,8 @@ public class KernelProcessEventsSubscriberInfo private readonly Dictionary _stepEventProcessEventMap = []; private Type? _processEventSubscriberType = null; + private IServiceProvider? _subscriberServiceProvider = null; + protected void Subscribe(string eventName, MethodInfo method) { if (this._eventHandlers.TryGetValue(eventName, out List? eventHandlers) && eventHandlers != null) @@ -46,22 +48,27 @@ public void InvokeProcessEvent(string eventName, object? data) foreach (var method in linkedMethods) { // TODO-estenori: Avoid creating a new instance every time a function is invoked - create instance once only? - var instance = Activator.CreateInstance(this._processEventSubscriberType, []); + var instance = Activator.CreateInstance(this._processEventSubscriberType, [this._subscriberServiceProvider]); method.Invoke(instance, [data]); } } } /// - /// Extracts the event properties and function details of the functions with the annotator + /// Extracts the event properties and function details of the functions with the annotator + /// /// - /// - /// + /// Type of the class that make uses of the annotators and contains the functionality to be executed + /// Enum that contains the process subscribable events /// - public void SubscribeToEventsFromClass() where TEventListeners : KernelProcessEventsSubscriber where TEvents : Enum + public void SubscribeToEventsFromClass(IServiceProvider? serviceProvider = null) where TEventListeners : KernelProcessEventsSubscriber where TEvents : Enum { - var methods = typeof(TEventListeners).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.DeclaredOnly); + if (this._subscriberServiceProvider != null) + { + throw new KernelException("Already linked process to a specific service provider class"); + } + var methods = typeof(TEventListeners).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.DeclaredOnly); foreach (var method in methods) { if (method.GetCustomAttributes(typeof(KernelProcessEventsSubscriber<>.ProcessEventSubscriberAttribute), false).FirstOrDefault() is KernelProcessEventsSubscriber.ProcessEventSubscriberAttribute attribute) @@ -75,6 +82,7 @@ public void SubscribeToEventsFromClass() where TEventL } } + this._subscriberServiceProvider = serviceProvider; this._processEventSubscriberType = typeof(TEventListeners); } diff --git a/dotnet/src/Experimental/Process.Core/ProcessBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessBuilder.cs index 1cf889e04ab4..6489cc806985 100644 --- a/dotnet/src/Experimental/Process.Core/ProcessBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/ProcessBuilder.cs @@ -291,9 +291,9 @@ private void PopulateEventNames() #region Public Interface - public void LinkEventSubscribersFromType() where TEventListeners : KernelProcessEventsSubscriber + public void LinkEventSubscribersFromType(IServiceProvider? serviceProvider = null) where TEventListeners : KernelProcessEventsSubscriber { - this._eventsSubscriber.SubscribeToEventsFromClass(); + this._eventsSubscriber.SubscribeToEventsFromClass(serviceProvider); } public ProcessEdgeBuilder OnInputEvent(TEvents eventId) From 67f8114966038dca502d06f6eec5741765af3abe Mon Sep 17 00:00:00 2001 From: Estefania Tenorio <8483207+esttenorio@users.noreply.github.com> Date: Fri, 15 Nov 2024 14:29:00 -0800 Subject: [PATCH 03/13] demo samples showcasing cloud events with eventSubscribers --- dotnet/Directory.Packages.props | 1 + dotnet/SK-dotnet.sln | 9 + .../Demos/ProcessWithCloudEvents/AppConfig.cs | 58 ++++ .../Controllers/CounterBaseController.cs | 108 ++++++++ .../CounterWithCloudStepsController.cs | 51 ++++ .../CounterWithCloudSubscribersController.cs | 54 ++++ .../GraphServiceProvider.cs | 51 ++++ .../MicrosoftGraph/GraphRequestFactory.cs | 29 ++ .../ProcessWithCloudEvents.csproj | 25 ++ .../ProcessWithCloudEvents.http | 24 ++ .../Processes/RequestCounterProcess.cs | 173 ++++++++++++ .../Processes/Steps/CounterInterceptorStep.cs | 26 ++ .../Processes/Steps/CounterStep.cs | 71 +++++ .../Processes/Steps/SendEmailStep.cs | 83 ++++++ .../Demos/ProcessWithCloudEvents/Program.cs | 34 +++ .../Demos/ProcessWithCloudEvents/README.md | 255 ++++++++++++++++++ .../ProcessWithCloudEvents/appsettings.json | 23 ++ .../Process.Core/ProcessStepBuilder.cs | 1 + 18 files changed, 1076 insertions(+) create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/AppConfig.cs create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterBaseController.cs create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterWithCloudStepsController.cs create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterWithCloudSubscribersController.cs create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/GraphServiceProvider.cs create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/MicrosoftGraph/GraphRequestFactory.cs create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.csproj create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.http create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/Processes/RequestCounterProcess.cs create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/CounterInterceptorStep.cs create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/CounterStep.cs create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/SendEmailStep.cs create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/Program.cs create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/README.md create mode 100644 dotnet/samples/Demos/ProcessWithCloudEvents/appsettings.json diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 05a06f7c9901..b03ac4b706b9 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -43,6 +43,7 @@ + diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index 9a125d10798a..309602f4de4d 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -426,6 +426,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OllamaFunctionCalling", "sa EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenAIRealtime", "samples\Demos\OpenAIRealtime\OpenAIRealtime.csproj", "{6154129E-7A35-44A5-998E-B7001B5EDE14}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProcessWithCloudEvents", "samples\Demos\ProcessWithCloudEvents\ProcessWithCloudEvents.csproj", "{36E94769-8A05-4009-808C-E23A0FD2A0F0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1141,6 +1143,12 @@ Global {6154129E-7A35-44A5-998E-B7001B5EDE14}.Publish|Any CPU.Build.0 = Debug|Any CPU {6154129E-7A35-44A5-998E-B7001B5EDE14}.Release|Any CPU.ActiveCfg = Release|Any CPU {6154129E-7A35-44A5-998E-B7001B5EDE14}.Release|Any CPU.Build.0 = Release|Any CPU + {36E94769-8A05-4009-808C-E23A0FD2A0F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {36E94769-8A05-4009-808C-E23A0FD2A0F0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {36E94769-8A05-4009-808C-E23A0FD2A0F0}.Publish|Any CPU.ActiveCfg = Debug|Any CPU + {36E94769-8A05-4009-808C-E23A0FD2A0F0}.Publish|Any CPU.Build.0 = Debug|Any CPU + {36E94769-8A05-4009-808C-E23A0FD2A0F0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {36E94769-8A05-4009-808C-E23A0FD2A0F0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1297,6 +1305,7 @@ Global {B35B1DEB-04DF-4141-9163-01031B22C5D1} = {0D8C6358-5DAA-4EA6-A924-C268A9A21BC9} {481A680F-476A-4627-83DE-2F56C484525E} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} {6154129E-7A35-44A5-998E-B7001B5EDE14} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} + {36E94769-8A05-4009-808C-E23A0FD2A0F0} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83} diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/AppConfig.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/AppConfig.cs new file mode 100644 index 000000000000..d9d980ce5075 --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/AppConfig.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft. All rights reserved. + +internal sealed class AppConfig +{ + /// + /// The configuration for the Azure EntraId authentication. + /// + public AzureEntraIdConfig? AzureEntraId { get; set; } + + /// + /// Ensures that the configuration is valid. + /// + internal void Validate() + { + ArgumentNullException.ThrowIfNull(this.AzureEntraId?.ClientId, nameof(this.AzureEntraId.ClientId)); + ArgumentNullException.ThrowIfNull(this.AzureEntraId?.TenantId, nameof(this.AzureEntraId.TenantId)); + + if (this.AzureEntraId.InteractiveBrowserAuthentication) + { + ArgumentNullException.ThrowIfNull(this.AzureEntraId.InteractiveBrowserRedirectUri, nameof(this.AzureEntraId.InteractiveBrowserRedirectUri)); + } + else + { + ArgumentNullException.ThrowIfNull(this.AzureEntraId?.ClientSecret, nameof(this.AzureEntraId.ClientSecret)); + } + } + + internal sealed class AzureEntraIdConfig + { + /// + /// App Registration Client Id + /// + public string? ClientId { get; set; } + + /// + /// App Registration Tenant Id + /// + public string? TenantId { get; set; } + + /// + /// The client secret to use for the Azure EntraId authentication. + /// + /// + /// This is required if InteractiveBrowserAuthentication is false. (App Authentication) + /// + public string? ClientSecret { get; set; } + + /// + /// Specifies whether to use interactive browser authentication (Delegated User Authentication) or App authentication. + /// + public bool InteractiveBrowserAuthentication { get; set; } + + /// + /// When using interactive browser authentication, the redirect URI to use. + /// + public string? InteractiveBrowserRedirectUri { get; set; } = "http://localhost"; + } +} diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterBaseController.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterBaseController.cs new file mode 100644 index 000000000000..c31bf8eecad3 --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterBaseController.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Graph; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Process.Models; +using ProcessWithCloudEvents.Processes; +using ProcessWithCloudEvents.Processes.Steps; + +namespace ProcessWithCloudEvents.Controllers; +public abstract class CounterBaseController : ControllerBase +{ + internal Kernel Kernel { get; init; } + internal KernelProcess Process { get; init; } + + private static readonly JsonSerializerOptions s_jsonOptions = new() + { + WriteIndented = true, + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull + }; + + internal Kernel BuildKernel(GraphServiceClient? graphClient = null) + { + var builder = Kernel.CreateBuilder(); + if (graphClient != null) + { + builder.Services.AddSingleton(graphClient); + } + return builder.Build(); + } + + internal KernelProcess InitializeProcess(ProcessBuilder process) + { + this.InitializeStateFile(process.Name); + var processState = this.LoadProcessState(process.Name); + return process.Build(processState); + } + + private string GetTemporaryProcessFilePath(string processName) + { + return Path.Combine(Path.GetTempPath(), $"{processName}.json"); + } + + internal void InitializeStateFile(string processName) + { + // Initialize the path for the temporary file + var tempProcessFile = this.GetTemporaryProcessFilePath(processName); + + // If the file does not exist, create it and initialize with zero + if (!System.IO.File.Exists(tempProcessFile)) + { + System.IO.File.WriteAllText(tempProcessFile, ""); + } + } + + internal void SaveProcessState(string processName, KernelProcessStateMetadata processStateInfo) + { + var content = JsonSerializer.Serialize(processStateInfo, s_jsonOptions); + System.IO.File.WriteAllText(this.GetTemporaryProcessFilePath(processName), content); + } + + internal KernelProcessStateMetadata? LoadProcessState(string processName) + { + try + { + using StreamReader reader = new(this.GetTemporaryProcessFilePath(processName)); + var content = reader.ReadToEnd(); + return JsonSerializer.Deserialize(content, s_jsonOptions); + } + catch (Exception) + { + return null; + } + } + + internal void StoreProcessState(KernelProcess process) + { + var stateMetadata = process.ToProcessStateMetadata(); + this.SaveProcessState(process.State.Name, stateMetadata); + } + + internal KernelProcessStepState? GetCounterState(KernelProcess process) + { + // TODO: Replace when there is a better way of extracting snapshot of local state + return process.Steps + .First(step => step.State.Name == RequestCounterProcess.StepNames.Counter).State as KernelProcessStepState; + } + + internal async Task StartProcessWithEventAsync(string eventName, object? eventData = null) + { + var runningProcess = await this.Process.StartAsync(this.Kernel, new() { Id = eventName, Data = eventData }); + var processState = await runningProcess.GetStateAsync(); + this.StoreProcessState(processState); + + return processState; + } + + public virtual async Task IncreaseCounterAsync() + { + return await Task.FromResult(0); + } + + public virtual async Task DecreaseCounterAsync() + { + return await Task.FromResult(0); + } +} diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterWithCloudStepsController.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterWithCloudStepsController.cs new file mode 100644 index 000000000000..1bed5fff5fb6 --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterWithCloudStepsController.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.AspNetCore.Mvc; +using Microsoft.Graph; +using ProcessWithCloudEvents.Processes; + +namespace ProcessWithCloudEvents.Controllers; +[ApiController] +[Route("[controller]")] +public class CounterWithCloudStepsController : CounterBaseController +{ + private readonly ILogger _logger; + + public CounterWithCloudStepsController(ILogger logger, GraphServiceClient graphClient) + { + this._logger = logger; + + this.Kernel = this.BuildKernel(graphClient); + this.Process = this.InitializeProcess(RequestCounterProcess.CreateProcessWithCloudSteps()); + } + + [HttpGet("increase", Name = "IncreaseWithCloudSteps")] + public override async Task IncreaseCounterAsync() + { + var eventName = RequestCounterProcess.GetEventName(RequestCounterProcess.CounterProcessEvents.IncreaseCounterRequest); + var runningProcess = await this.StartProcessWithEventAsync(eventName); + var counterState = this.GetCounterState(runningProcess); + + return counterState?.State?.Counter ?? -1; + } + + [HttpGet("decrease", Name = "DecreaseWithCloudSteps")] + public override async Task DecreaseCounterAsync() + { + var eventName = RequestCounterProcess.GetEventName(RequestCounterProcess.CounterProcessEvents.DecreaseCounterRequest); + var runningProcess = await this.StartProcessWithEventAsync(eventName); + var counterState = this.GetCounterState(runningProcess); + + return counterState?.State?.Counter ?? -1; + } + + [HttpGet("reset", Name = "ResetCounterWithCloudSteps")] + public async Task ResetCounterAsync() + { + var eventName = RequestCounterProcess.GetEventName(RequestCounterProcess.CounterProcessEvents.ResetCounterRequest); + var runningProcess = await this.StartProcessWithEventAsync(eventName); + var counterState = this.GetCounterState(runningProcess); + + return counterState?.State?.Counter ?? -1; + } +} diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterWithCloudSubscribersController.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterWithCloudSubscribersController.cs new file mode 100644 index 000000000000..105721eabb78 --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterWithCloudSubscribersController.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.AspNetCore.Mvc; +using Microsoft.Graph; +using ProcessWithCloudEvents.Processes; + +namespace ProcessWithCloudEvents.Controllers; +[ApiController] +[Route("[controller]")] +public class CounterWithCloudSubscribersController : CounterBaseController +{ + private readonly ILogger _logger; + + public CounterWithCloudSubscribersController(ILogger logger, GraphServiceClient graphClient) + { + this._logger = logger; + this.Kernel = this.BuildKernel(); + + var serviceProvider = new ServiceCollection() + .AddSingleton(graphClient) + .BuildServiceProvider(); + this.Process = this.InitializeProcess(RequestCounterProcess.CreateProcessWithProcessSubscriber(serviceProvider)); + } + + [HttpGet("increase", Name = "IncreaseCounterWithCloudSubscribers")] + public override async Task IncreaseCounterAsync() + { + var eventName = RequestCounterProcess.GetEventName(RequestCounterProcess.CounterProcessEvents.IncreaseCounterRequest); + var runningProcess = await this.StartProcessWithEventAsync(eventName); + var counterState = this.GetCounterState(runningProcess); + + return counterState?.State?.Counter ?? -1; + } + + [HttpGet("decrease", Name = "DecreaseCounterWithCloudSubscribers")] + public override async Task DecreaseCounterAsync() + { + var eventName = RequestCounterProcess.GetEventName(RequestCounterProcess.CounterProcessEvents.DecreaseCounterRequest); + var runningProcess = await this.StartProcessWithEventAsync(eventName); + var counterState = this.GetCounterState(runningProcess); + + return counterState?.State?.Counter ?? -1; + } + + [HttpGet("reset", Name = "ResetCounterWithCloudSubscribers")] + public async Task ResetCounterAsync() + { + var eventName = RequestCounterProcess.GetEventName(RequestCounterProcess.CounterProcessEvents.ResetCounterRequest); + var runningProcess = await this.StartProcessWithEventAsync(eventName); + var counterState = this.GetCounterState(runningProcess); + + return counterState?.State?.Counter ?? -1; + } +} diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/GraphServiceProvider.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/GraphServiceProvider.cs new file mode 100644 index 000000000000..470352b928b6 --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/GraphServiceProvider.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.Core; +using Azure.Identity; +using Microsoft.Graph; + +public static class GraphServiceProvider +{ + public static GraphServiceClient CreateGraphService() + { + string[] scopes; + + var config = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) // Set the base path for appsettings.json + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) // Load appsettings.json + .AddUserSecrets() + .AddEnvironmentVariables() + .Build() + .Get() ?? + throw new InvalidOperationException("Configuration is not setup correctly."); + + config.Validate(); + + TokenCredential credential = null!; + if (config.AzureEntraId!.InteractiveBrowserAuthentication) // Authentication As User + { + /// Use this if using user delegated permissions + scopes = ["User.Read", "Mail.Send"]; + + credential = new InteractiveBrowserCredential( + new InteractiveBrowserCredentialOptions + { + TenantId = config.AzureEntraId.TenantId, + ClientId = config.AzureEntraId.ClientId, + AuthorityHost = AzureAuthorityHosts.AzurePublicCloud, + RedirectUri = new Uri(config.AzureEntraId.InteractiveBrowserRedirectUri!) + }); + } + else // Authentication As Application + { + scopes = ["https://graph.microsoft.com/.default"]; + + credential = new ClientSecretCredential( + config.AzureEntraId.TenantId, + config.AzureEntraId.ClientId, + config.AzureEntraId.ClientSecret); + } + + return new GraphServiceClient(credential, scopes); + } +} diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/MicrosoftGraph/GraphRequestFactory.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/MicrosoftGraph/GraphRequestFactory.cs new file mode 100644 index 000000000000..915aa261342b --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/MicrosoftGraph/GraphRequestFactory.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Graph.Me.SendMail; +using Microsoft.Graph.Models; + +namespace ProcessWithCloudEvents.MicrosoftGraph; + +public static class GraphRequestFactory +{ + public static SendMailPostRequestBody CreateEmailBody(string subject, string content, List recipients) + { + var message = new SendMailPostRequestBody() + { + Message = new Microsoft.Graph.Models.Message() + { + Subject = subject, + Body = new() + { + ContentType = Microsoft.Graph.Models.BodyType.Text, + Content = content, + }, + ToRecipients = recipients.Select(address => new Recipient { EmailAddress = new() { Address = address } }).ToList(), + }, + SaveToSentItems = true, + }; + + return message; + } +} diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.csproj b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.csproj new file mode 100644 index 000000000000..b6ea023f8336 --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + + $(NoWarn);CA2007,CA1861,VSTHRD111,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0080,SKEXP0110 + + 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0 + + + + + + + + + + + + + + + diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.http b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.http new file mode 100644 index 000000000000..7111d4bcb145 --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.http @@ -0,0 +1,24 @@ +@ProcessWithCloudEvents_HostAddress = http://localhost:5077 + +GET {{ProcessWithCloudEvents_HostAddress}}/CounterWithCloudSteps/increase +Accept: application/json + +### +GET {{ProcessWithCloudEvents_HostAddress}}/CounterWithCloudSteps/decrease +Accept: application/json + +### +GET {{ProcessWithCloudEvents_HostAddress}}/CounterWithCloudSteps/reset +Accept: application/json + +### +GET {{ProcessWithCloudEvents_HostAddress}}/CounterWithCloudSubscribers/increase +Accept: application/json + +### +GET {{ProcessWithCloudEvents_HostAddress}}/CounterWithCloudSubscribers/decrease +Accept: application/json + +### +GET {{ProcessWithCloudEvents_HostAddress}}/CounterWithCloudSubscribers/reset +Accept: application/json \ No newline at end of file diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/RequestCounterProcess.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/RequestCounterProcess.cs new file mode 100644 index 000000000000..0e6c4a05c76a --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/RequestCounterProcess.cs @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Graph; +using Microsoft.Graph.Me.SendMail; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Process; +using ProcessWithCloudEvents.MicrosoftGraph; +using ProcessWithCloudEvents.Processes.Steps; + +namespace ProcessWithCloudEvents.Processes; + +public static class RequestCounterProcess +{ + public static class StepNames + { + public const string Counter = nameof(Counter); + public const string CounterInterceptor = nameof(CounterInterceptor); + public const string SendEmail = nameof(SendEmail); + } + + public enum CounterProcessEvents + { + IncreaseCounterRequest, + DecreaseCounterRequest, + ResetCounterRequest, + OnCounterReset, + OnCounterResult + } + + public static string GetEventName(CounterProcessEvents processEvent) + { + return Enum.GetName(processEvent) ?? ""; + } + + public static ProcessBuilder CreateProcessWithCloudSteps() + { + var processBuilder = new ProcessBuilder("RequestCounterProcess"); + + var counterStep = processBuilder.AddStepFromType(StepNames.Counter); + var counterInterceptorStep = processBuilder.AddStepFromType(StepNames.CounterInterceptor); + var emailSenderStep = processBuilder.AddStepFromType(StepNames.SendEmail); + + processBuilder + .OnInputEvent(processBuilder.GetEventName(CounterProcessEvents.IncreaseCounterRequest)) + .SendEventTo(new ProcessFunctionTargetBuilder(counterStep, functionName: CounterStep.Functions.IncreaseCounter)); + + processBuilder + .OnInputEvent(processBuilder.GetEventName(CounterProcessEvents.DecreaseCounterRequest)) + .SendEventTo(new ProcessFunctionTargetBuilder(counterStep, functionName: CounterStep.Functions.DecreaseCounter)); + + processBuilder + .OnInputEvent(processBuilder.GetEventName(CounterProcessEvents.ResetCounterRequest)) + .SendEventTo(new ProcessFunctionTargetBuilder(counterStep, functionName: CounterStep.Functions.ResetCounter)); + + counterStep + .OnFunctionResult(CounterStep.Functions.IncreaseCounter) + .SendEventTo(new ProcessFunctionTargetBuilder(counterInterceptorStep)); + + counterStep + .OnFunctionResult(CounterStep.Functions.DecreaseCounter) + .SendEventTo(new ProcessFunctionTargetBuilder(counterInterceptorStep)); + + counterStep + .OnFunctionResult(CounterStep.Functions.ResetCounter) + .SendEventTo(new ProcessFunctionTargetBuilder(emailSenderStep, SendEmailStep.Functions.SendCounterResetEmail)); + + counterInterceptorStep + .OnFunctionResult(CounterInterceptorStep.Functions.InterceptCounter) + .SendEventTo(new ProcessFunctionTargetBuilder(emailSenderStep, SendEmailStep.Functions.SendCounterChangeEmail)); + + return processBuilder; + } + + public static ProcessBuilder CreateProcessWithProcessSubscriber(IServiceProvider serviceProvider) + { + var processBuilder = new ProcessBuilder("CounterWithProcessSubscriber"); + + var counterStep = processBuilder.AddStepFromType(StepNames.Counter); + var counterInterceptorStep = processBuilder.AddStepFromType(StepNames.CounterInterceptor); + + processBuilder + .OnInputEvent(processBuilder.GetEventName(CounterProcessEvents.IncreaseCounterRequest)) + .SendEventTo(new ProcessFunctionTargetBuilder(counterStep, functionName: CounterStep.Functions.IncreaseCounter)); + + processBuilder + .OnInputEvent(processBuilder.GetEventName(CounterProcessEvents.DecreaseCounterRequest)) + .SendEventTo(new ProcessFunctionTargetBuilder(counterStep, functionName: CounterStep.Functions.DecreaseCounter)); + + processBuilder + .OnInputEvent(processBuilder.GetEventName(CounterProcessEvents.ResetCounterRequest)) + .SendEventTo(new ProcessFunctionTargetBuilder(counterStep, functionName: CounterStep.Functions.ResetCounter)); + + counterStep + .OnFunctionResult(CounterStep.Functions.IncreaseCounter) + .SendEventTo(new ProcessFunctionTargetBuilder(counterInterceptorStep)); + + counterStep + .OnFunctionResult(CounterStep.Functions.DecreaseCounter) + .SendEventTo(new ProcessFunctionTargetBuilder(counterInterceptorStep)); + + counterStep + .OnFunctionResult(CounterStep.Functions.ResetCounter) + .EmitAsProcessEvent(processBuilder.GetProcessEvent(CounterProcessEvents.OnCounterReset)) + .SendEventTo(new ProcessFunctionTargetBuilder(counterInterceptorStep)); + + counterInterceptorStep + .OnFunctionResult(CounterInterceptorStep.Functions.InterceptCounter) + .EmitAsProcessEvent(processBuilder.GetProcessEvent(CounterProcessEvents.OnCounterResult)); + + processBuilder.LinkEventSubscribersFromType(serviceProvider); + + return processBuilder; + } + + public class CounterProcessSubscriber : KernelProcessEventsSubscriber + { + public CounterProcessSubscriber(IServiceProvider serviceProvider) : base(serviceProvider) + { + } + + private SendMailPostRequestBody GenerateEmailRequest(int counter, string emailAddress, string subject) + { + var message = GraphRequestFactory.CreateEmailBody( + subject: $"{subject} - using SK event subscribers", + content: $"The counter is {counter}", + recipients: [emailAddress]); + + return message; + } + + [ProcessEventSubscriber(CounterProcessEvents.OnCounterResult)] + public async Task OnCounterResultReceivedAsync(int? counterResult) + { + if (!counterResult.HasValue) + { + return; + } + + try + { + var graphClient = this.ServiceProvider?.GetRequiredService(); + var user = await graphClient.Me.GetAsync(); + var graphEmailMessage = this.GenerateEmailRequest(counterResult.Value, user!.Mail!, subject: "The counter has changed"); + await graphClient?.Me.SendMail.PostAsync(graphEmailMessage); + } + catch (Exception e) + { + throw new KernelException($"Something went wrong and couldn't send email - {e}"); + } + } + + [ProcessEventSubscriber(CounterProcessEvents.OnCounterReset)] + public async Task OnCounterResetReceivedAsync(int? counterResult) + { + if (!counterResult.HasValue) + { + return; + } + + try + { + var graphClient = this.ServiceProvider?.GetRequiredService(); + var user = await graphClient.Me.GetAsync(); + var graphEmailMessage = this.GenerateEmailRequest(counterResult.Value, user!.Mail!, subject: "The counter has been reset"); + await graphClient?.Me.SendMail.PostAsync(graphEmailMessage); + } + catch (Exception e) + { + throw new KernelException($"Something went wrong and couldn't send email - {e}"); + } + } + } +} diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/CounterInterceptorStep.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/CounterInterceptorStep.cs new file mode 100644 index 000000000000..28fd970fed3b --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/CounterInterceptorStep.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel; + +namespace ProcessWithCloudEvents.Processes.Steps; + +public class CounterInterceptorStep : KernelProcessStep +{ + public static class Functions + { + public const string InterceptCounter = nameof(InterceptCounter); + } + + [KernelFunction(Functions.InterceptCounter)] + public int? InterceptCounter(int counterStatus) + { + var multipleOf = 3; + if (counterStatus != 0 && counterStatus % multipleOf == 0) + { + // Only return counter if counter is a multiple of "multipleOf" + return counterStatus; + } + + return null; + } +} diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/CounterStep.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/CounterStep.cs new file mode 100644 index 000000000000..34da8d17d2d8 --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/CounterStep.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel; + +namespace ProcessWithCloudEvents.Processes.Steps; + +public class CounterStep : KernelProcessStep +{ + public static class Functions + { + public const string IncreaseCounter = nameof(IncreaseCounter); + public const string DecreaseCounter = nameof(DecreaseCounter); + public const string ResetCounter = nameof(ResetCounter); + } + + public static class OutputEvents + { + public const string CounterResult = nameof(CounterResult); + } + + internal CounterStepState? _state; + + public override ValueTask ActivateAsync(KernelProcessStepState state) + { + this._state = state.State; + return ValueTask.CompletedTask; + } + + [KernelFunction(Functions.IncreaseCounter)] + public async Task IncreaseCounterAsync(KernelProcessStepContext context) + { + this._state!.Counter += this._state.CounterIncrements; + + if (this._state!.Counter > 5) + { + await context.EmitEventAsync(OutputEvents.CounterResult, this._state.Counter); + } + this._state.LastCounterUpdate = DateTime.UtcNow; + + return this._state.Counter; + } + + [KernelFunction(Functions.DecreaseCounter)] + public async Task DecreaseCounterAsync(KernelProcessStepContext context) + { + this._state!.Counter -= this._state.CounterIncrements; + + if (this._state!.Counter > 5) + { + await context.EmitEventAsync(OutputEvents.CounterResult, this._state.Counter); + } + this._state.LastCounterUpdate = DateTime.UtcNow; + + return this._state.Counter; + } + + [KernelFunction(Functions.ResetCounter)] + public async Task ResetCounterAsync(KernelProcessStepContext context) + { + this._state!.Counter = 0; + return this._state.Counter; + } +} + +public class CounterStepState +{ + public int Counter { get; set; } = 0; + public int CounterIncrements { get; set; } = 1; + + public DateTime? LastCounterUpdate { get; set; } = null; +} diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/SendEmailStep.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/SendEmailStep.cs new file mode 100644 index 000000000000..e44c6d1da9d5 --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/SendEmailStep.cs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Graph; +using Microsoft.Graph.Me.SendMail; +using Microsoft.SemanticKernel; +using ProcessWithCloudEvents.MicrosoftGraph; + +namespace ProcessWithCloudEvents.Processes.Steps; + +public class SendEmailStep : KernelProcessStep +{ + public static class OutputEvents + { + public const string SendEmailSuccess = nameof(SendEmailSuccess); + public const string SendEmailFailure = nameof(SendEmailFailure); + } + + public static class Functions + { + public const string SendCounterChangeEmail = nameof(SendCounterChangeEmail); + public const string SendCounterResetEmail = nameof(SendCounterResetEmail); + } + + public SendEmailStep() { } + + protected SendMailPostRequestBody PopulateMicrosoftGraphMailMessage(object inputData, string emailAddress, string subject) + { + var message = GraphRequestFactory.CreateEmailBody( + subject: $"{subject} - using SK cloud step", + content: $"The counter is {(int)inputData}", + recipients: [emailAddress]); + + return message; + } + + [KernelFunction(Functions.SendCounterChangeEmail)] + public async Task PublishCounterChangedEmailMessageAsync(KernelProcessStepContext context, Kernel kernel, object inputData) + { + if (inputData == null) + { + return; + } + + try + { + var graphClient = kernel.GetRequiredService(); + var user = await graphClient.Me.GetAsync(); + var graphEmailMessage = this.PopulateMicrosoftGraphMailMessage(inputData, user!.Mail!, subject: "The counter has changed"); + await graphClient.Me.SendMail.PostAsync(graphEmailMessage).ConfigureAwait(false); + + await context.EmitEventAsync(OutputEvents.SendEmailSuccess); + } + catch (Exception e) + { + await context.EmitEventAsync(OutputEvents.SendEmailFailure, e, visibility: KernelProcessEventVisibility.Public); + throw new KernelException($"Something went wrong and couldn't send email - {e}"); + } + } + + [KernelFunction(Functions.SendCounterResetEmail)] + public async Task PublishCounterResetEmailMessageAsync(KernelProcessStepContext context, Kernel kernel, object inputData) + { + if (inputData == null) + { + return; + } + + try + { + var graphClient = kernel.GetRequiredService(); + var user = await graphClient.Me.GetAsync(); + var graphEmailMessage = this.PopulateMicrosoftGraphMailMessage(inputData, user!.Mail!, subject: "The counter has been reset"); + await graphClient.Me.SendMail.PostAsync(graphEmailMessage).ConfigureAwait(false); + + await context.EmitEventAsync(OutputEvents.SendEmailSuccess); + } + catch (Exception e) + { + await context.EmitEventAsync(OutputEvents.SendEmailFailure, e, visibility: KernelProcessEventVisibility.Public); + throw new KernelException($"Something went wrong and couldn't send email - {e}"); + } + } +} diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/Program.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/Program.cs new file mode 100644 index 000000000000..dae96b88b210 --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/Program.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Graph; +using ProcessWithCloudEvents.Controllers; + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddSingleton(GraphServiceProvider.CreateGraphService()); + +// For demo purposes making the Counter a singleton so it is not instantiated on every new request +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +// Add services to the container. +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/README.md b/dotnet/samples/Demos/ProcessWithCloudEvents/README.md new file mode 100644 index 000000000000..9ee47f5e8506 --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/README.md @@ -0,0 +1,255 @@ +# Process With Cloud Events Demo + +This demo contains an ASP.NET core API that showcases the use of cloud events using SK Processes Steps and SK Process with Event Subscribers. + + +For more information about Semantic Kernel Processes, see the following documentation: + +## Semantic Kernel Processes + +- [Overview of the Process Framework (docs)](https://learn.microsoft.com/semantic-kernel/frameworks/process/process-framework) +- [Getting Started with Processes (samples)](../../GettingStartedWithProcesses/) + +## Demo + +### Process: Counter with Cloud Events + +#### Steps + +##### Counter Step + +A simple counter has 3 main functionalities: + +- Increase count +- Decrease count +- Reset count (set counter to 0) + +To achive this behavior the SK Stateful Step `Processes/Steps/CounterStep.cs` was created. +On every request it stores that state that can be used to restore the state on the next request. + +##### Counter Interceptor Step + +This step works as a filter that only passes the counter value if it is a multiple of `multipleOf` else passes a null value. + +##### Send Email Step + +This step sends an email if receiving a not nullable int value to the same email used on log in. + +#### Processes + +##### Process With Cloud Steps + +```mermaid +flowchart LR + subgraph API + ApiIncrease["/increse"] + ApiDecrease["/decrease"] + ApiReset["/reset"] + end + + subgraph process[SK Process] + direction LR + subgraph counter[Counter Step] + increaseCounter[IncreaseCounterAsync
Function] + decreaseCounter[DecreaseCounterAsync
Function] + resetCounter[ResetCounterAsync
Function] + end + + counterInterceptor[Counter
Interceptor
Step] + + subgraph sendEmail[Send Email Step] + sendCounterChangedEmail[PublishCounterChangedEmailMessageAsync
Function] + sendResetEmail[PublishCounterResetEmailMessageAsync
Function] + end + + increaseCounter--> counterInterceptor + decreaseCounter--> counterInterceptor + + counterInterceptor-->sendCounterChangedEmail + resetCounter-->sendResetEmail + end + + ApiIncrease<-->|IncreaseCounterRequest|increaseCounter + ApiDecrease<-->|DecreaseCounterRequest|decreaseCounter + ApiReset<-->|ResetCounterRequest|resetCounter +``` + +Cloud events related logic is encapsulated in a step. + +**Breakdown** + +- When building the process Kernel used in the SK Process, the cloud event client has to be passed to the Kernel. + +- When using `Microsoft Graph`, after completing the [Microsoft Graph Setup](./#microsoft-graph-setup), To achieve the proper setup the following is needed: + + 1. The specific service (`GraphServiceClient` in this case) needs to be added to the Services that are used by the kernel of the process: + + ```C# + internal Kernel BuildKernel(GraphServiceClient? graphClient = null) + { + var builder = Kernel.CreateBuilder(); + if (graphClient != null) + { + builder.Services.AddSingleton(graphClient); + } + return builder.Build(); + } + ``` + 2. Since now all steps have access to the configured kernel, inside a step, it now can make use of the service by doing: + ```C# + var graphClient = kernel.GetRequiredService(); + ``` + +##### Process With Cloud Process Subscribers + +Cloud events related logic is encapsulated in SK Event Subscribers. + +```mermaid +flowchart LR + subgraph API + ApiIncrease["/increse"] + ApiDecrease["/decrease"] + ApiReset["/reset"] + end + + subgraph process[SK Process - CreateProcessWithProcessSubscriber] + direction TB + subgraph counter[Counter Step] + increaseCounter[IncreaseCounterAsync
Function] + decreaseCounter[DecreaseCounterAsync
Function] + resetCounter[ResetCounterAsync
Function] + end + counterInterceptor[Counter
Interceptor
Step] + + increaseCounter--> counterInterceptor + decreaseCounter--> counterInterceptor + end + + subgraph processInterceptor[SK Process Subscribers - CounterProcessSubscriber] + OnCounterResultReceivedAsync + OnCounterResetReceivedAsync + end + + counterInterceptor-->|OnCounterResult|OnCounterResultReceivedAsync + resetCounter-->|OnCounterReset|OnCounterResetReceivedAsync + + ApiIncrease<-->|IncreaseCounterRequest|increaseCounter + ApiDecrease<-->|DecreaseCounterRequest|decreaseCounter + ApiReset<-->|ResetCounterRequest|resetCounter +``` +**Breakdown** + +- When building the process Kernel used in the SK Process, the cloud event client has to be passed to the Event Subscribers. + +- When using `Microsoft Graph`, after completing the [Microsoft Graph Setup](./#microsoft-graph-setup), the Event Subscribers can be linked by doing: + 1. Creating an enum that contains the process events of interest. + ```C# + public enum CounterProcessEvents + { + IncreaseCounterRequest, + DecreaseCounterRequest, + ResetCounterRequest, + OnCounterReset, + OnCounterResult + } + ``` + 2. On the existing process, adding which events can be accessed externally using `EmitAsProcessEvent()`: + ```C# + var processBuilder = new ProcessBuilder("CounterWithProcessSubscriber"); + + ... + + processBuilder + .OnInputEvent(processBuilder.GetEventName(CounterProcessEvents.IncreaseCounterRequest)) + .SendEventTo(new ProcessFunctionTargetBuilder(counterStep, functionName: CounterStep.Functions.IncreaseCounter)); + + processBuilder + .OnInputEvent(processBuilder.GetEventName(CounterProcessEvents.DecreaseCounterRequest)) + .SendEventTo(new ProcessFunctionTargetBuilder(counterStep, functionName: CounterStep.Functions.DecreaseCounter)); + + processBuilder + .OnInputEvent(processBuilder.GetEventName(CounterProcessEvents.ResetCounterRequest)) + .SendEventTo(new ProcessFunctionTargetBuilder(counterStep, functionName: CounterStep.Functions.ResetCounter)); + + ... + + counterStep + .OnFunctionResult(CounterStep.Functions.ResetCounter) + .EmitAsProcessEvent(processBuilder.GetProcessEvent(CounterProcessEvents.OnCounterReset)) + .SendEventTo(new ProcessFunctionTargetBuilder(counterInterceptorStep)); + + counterInterceptorStep + .OnFunctionResult(CounterInterceptorStep.Functions.InterceptCounter) + .EmitAsProcessEvent(processBuilder.GetProcessEvent(CounterProcessEvents.OnCounterResult)); + ``` + 3. Create a `KernelProcessEventsSubscriber` based class that with the `ProcessEventSubscriber` attributes to link specific process events to specific methods to execute. + ```C# + public class CounterProcessSubscriber : KernelProcessEventsSubscriber + { + public CounterProcessSubscriber(IServiceProvider serviceProvider) : base(serviceProvider) + { + } + + [ProcessEventSubscriber(CounterProcessEvents.OnCounterResult)] + public async Task OnCounterResultReceivedAsync(int? counterResult) + { + if (!counterResult.HasValue) + { + return; + } + + try + { + var graphClient = this.ServiceProvider?.GetRequiredService(); + var user = await graphClient.Me.GetAsync(); + var graphEmailMessage = this.GenerateEmailRequest(counterResult.Value, user!.Mail!, subject: "The counter has changed"); + await graphClient?.Me.SendMail.PostAsync(graphEmailMessage); + } + catch (Exception e) + { + throw new KernelException($"Something went wrong and couldn't send email - {e}"); + } + } + } + ``` + 4. Link the `KernelProcessEventsSubscriber` based class (example: `CounterProcessSubscriber`) to the process builder. + ```C# + processBuilder.LinkEventSubscribersFromType(serviceProvider); + ``` + +### Setup + +#### Microsoft Graph Setup + +##### Create an App Registration in Azure Active Directory + +1. Go to the [Azure Portal](https://portal.azure.com/). +2. Select the Azure Active Directory service. +3. Select App registrations and click on New registration. +4. Fill in the required fields and click on Register. +5. Copy the Application **(client) Id** for later use. +6. Save Directory **(tenant) Id** for later use.. +7. Click on Certificates & secrets and create a new client secret. (Any name and expiration date will work) +8. Copy the **client secret** value for later use. +9. Click on API permissions and add the following permissions: + - Microsoft Graph + - Delegated permissions + - OpenId permissions + - email + - profile + - openid + - User.Read + - Mail.Send (Necessary for sending emails from your account) + +##### Set Secrets using .NET [Secret Manager](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets) + +```powershell +dotnet user-secrets set "AzureEntraId:TenantId" " ... your tenant id ... " +dotnet user-secrets set "AzureEntraId:ClientId" " ... your client id ... " + +# App Registration Authentication +dotnet user-secrets set "AzureEntraId:ClientSecret" " ... your client secret ... " +# OR User Authentication (Interactive) +dotnet user-secrets set "AzureEntraId:InteractiveBrowserAuthentication" "true" +dotnet user-secrets set "AzureEntraId:RedirectUri" " ... your redirect uri ... " +``` \ No newline at end of file diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/appsettings.json b/dotnet/samples/Demos/ProcessWithCloudEvents/appsettings.json new file mode 100644 index 000000000000..b8a90ee3e4dc --- /dev/null +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/appsettings.json @@ -0,0 +1,23 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "MicrosoftGraph.TenantId": "", + "MicrosoftGraph.ClientId": "" + }, + "AzureEntraId": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "", + "ClientId": "", + "ClientSecret": "", + "InteractiveBrowserAuthentication": true + }, + "MicrosoftGraph": { + "BaseUrl": "https://graph.microsoft.com/v1.0" + } +} diff --git a/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs index 3b7058ee0718..43acaec7842f 100644 --- a/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs @@ -52,6 +52,7 @@ public ProcessStepEdgeBuilder OnEvent(string eventName) public ProcessStepEdgeBuilder OnFunctionResult(string functionName) { // TODO: ADD CHECK SO FUNCTION_NAME IS NOT EMPTY OR ADD FUNCTION RESOLVER IN CASE STEP HAS ONLY ONE FUNCTION + // TODO: Add check functionName is valid return this.OnEvent($"{functionName}.OnResult"); } From df219780acc7b4bb3cda1c849ca4ea59fabe442c Mon Sep 17 00:00:00 2001 From: Estefania Tenorio <8483207+esttenorio@users.noreply.github.com> Date: Mon, 18 Nov 2024 09:59:22 -0800 Subject: [PATCH 04/13] removing unnecessary code from existing samples --- .../Step03/Processes/FriedFishProcess.cs | 54 ++----------------- .../Step03/Step03a_FoodPreparation.cs | 1 - 2 files changed, 5 insertions(+), 50 deletions(-) diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FriedFishProcess.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FriedFishProcess.cs index 4f0a7201ae51..4c22317086c7 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FriedFishProcess.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FriedFishProcess.cs @@ -6,43 +6,6 @@ using Step03.Steps; namespace Step03.Processes; -public enum FishProcessEvents -{ - PrepareFriedFish, - MiddleStep, - FriedFishFailed, - FriedFishReady, -} - -public class FriedFishEventSubscribers : KernelProcessEventsSubscriber -{ - // TODO-estenori: figure out how to disallow and not need constructor on when using KernelProcessEventsSubscriber as base class - public FriedFishEventSubscribers(IServiceProvider? serviceProvider = null) : base(serviceProvider) { } - - [ProcessEventSubscriber(FishProcessEvents.MiddleStep)] - public void OnMiddleStep(List data) - { - // do something with data - Console.WriteLine($"=============> ON MIDDLE STEP: {data.FirstOrDefault() ?? ""}"); - } - - [ProcessEventSubscriber(FishProcessEvents.FriedFishReady)] - public void OnPrepareFish(object data) - { - // do something with data - // TODO: if event is linked to last event it doesnt get hit - // even when it may be linked to StopProcess() -> need additional special step? - Console.WriteLine("=============> ON FISH READY"); - } - - [ProcessEventSubscriber(FishProcessEvents.FriedFishFailed)] - public void OnFriedFisFailed(object data) - { - // do something with data - Console.WriteLine("=============> ON FISH FAILED"); - } -} - /// /// Sample process that showcases how to create a process with sequential steps and reuse of existing steps.
///
@@ -63,21 +26,20 @@ public static class ProcessEvents ///
/// name of the process /// - public static ProcessBuilder CreateProcess(string processName = "FriedFishProcess") + public static ProcessBuilder CreateProcess(string processName = "FriedFishProcess") { - var processBuilder = new ProcessBuilder(processName); + var processBuilder = new ProcessBuilder(processName); var gatherIngredientsStep = processBuilder.AddStepFromType(); var chopStep = processBuilder.AddStepFromType(); var fryStep = processBuilder.AddStepFromType(); processBuilder - .OnInputEvent(FishProcessEvents.PrepareFriedFish) + .OnInputEvent(ProcessEvents.PrepareFriedFish) .SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep)); gatherIngredientsStep .OnEvent(GatherFriedFishIngredientsStep.OutputEvents.IngredientsGathered) - .EmitAsProcessEvent(processBuilder.GetProcessEvent(FishProcessEvents.MiddleStep)) .SendEventTo(new ProcessFunctionTargetBuilder(chopStep, functionName: CutFoodStep.Functions.ChopFood)); chopStep @@ -86,12 +48,10 @@ public static ProcessBuilder CreateProcess(string processName fryStep .OnEvent(FryFoodStep.OutputEvents.FoodRuined) - .EmitAsProcessEvent(processBuilder.GetProcessEvent(FishProcessEvents.FriedFishFailed)) .SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep)); fryStep .OnEvent(FryFoodStep.OutputEvents.FriedFoodReady) - .EmitAsProcessEvent(processBuilder.GetProcessEvent(FishProcessEvents.FriedFishReady)) .StopProcess(); return processBuilder; @@ -134,14 +94,14 @@ public static ProcessBuilder CreateProcessWithStatefulStepsV1(string processName public static ProcessBuilder CreateProcessWithStatefulStepsV2(string processName = "FriedFishWithStatefulStepsProcess") { // It is recommended to specify process version in case this process is used as a step by another process - var processBuilder = new ProcessBuilder(processName) { Version = "FriedFishProcess.v2" }; + var processBuilder = new ProcessBuilder(processName) { Version = "FriedFishProcess.v2" }; var gatherIngredientsStep = processBuilder.AddStepFromType(name: "gatherFishIngredientStep", aliases: ["GatherFriedFishIngredientsWithStockStep"]); var chopStep = processBuilder.AddStepFromType(name: "chopFishStep", aliases: ["CutFoodStep"]); var fryStep = processBuilder.AddStepFromType(name: "fryFishStep", aliases: ["FryFoodStep"]); processBuilder - .GetProcessEvent(FishProcessEvents.PrepareFriedFish) + .OnInputEvent(ProcessEvents.PrepareFriedFish) .SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep)); gatherIngredientsStep @@ -150,7 +110,6 @@ public static ProcessBuilder CreateProcessWithStatefulStepsV2(string processName gatherIngredientsStep .OnEvent(GatherFriedFishIngredientsWithStockStep.OutputEvents.IngredientsOutOfStock) - .EmitAsProcessEvent(processBuilder.GetProcessEvent(FishProcessEvents.FriedFishFailed)) .StopProcess(); chopStep @@ -169,9 +128,6 @@ public static ProcessBuilder CreateProcessWithStatefulStepsV2(string processName .OnEvent(FryFoodStep.OutputEvents.FoodRuined) .SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep)); - fryStep.OnEvent(FryFoodStep.OutputEvents.FriedFoodReady) - .EmitAsProcessEvent(processBuilder.GetProcessEvent(FishProcessEvents.FriedFishReady)); - return processBuilder; } diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Step03a_FoodPreparation.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Step03a_FoodPreparation.cs index 477e7d3a70ad..37a9c6460723 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step03/Step03a_FoodPreparation.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Step03a_FoodPreparation.cs @@ -22,7 +22,6 @@ public class Step03a_FoodPreparation(ITestOutputHelper output) : BaseTest(output public async Task UsePrepareFriedFishProcessAsync() { var process = FriedFishProcess.CreateProcess(); - process.LinkEventSubscribersFromType(); await UsePrepareSpecificProductAsync(process, FriedFishProcess.ProcessEvents.PrepareFriedFish); } From 38f64a11aea56c956148e7b413ae41b7a610de9e Mon Sep 17 00:00:00 2001 From: Estefania Tenorio <8483207+esttenorio@users.noreply.github.com> Date: Mon, 18 Nov 2024 10:00:56 -0800 Subject: [PATCH 05/13] missing unnecessary code --- .../Step03/Processes/FriedFishProcess.cs | 4 ---- .../Step03/Step03a_FoodPreparation.cs | 1 - 2 files changed, 5 deletions(-) diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FriedFishProcess.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FriedFishProcess.cs index 4c22317086c7..7a04f1ad5093 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FriedFishProcess.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FriedFishProcess.cs @@ -50,10 +50,6 @@ public static ProcessBuilder CreateProcess(string processName = "FriedFishProces .OnEvent(FryFoodStep.OutputEvents.FoodRuined) .SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep)); - fryStep - .OnEvent(FryFoodStep.OutputEvents.FriedFoodReady) - .StopProcess(); - return processBuilder; } diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Step03a_FoodPreparation.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Step03a_FoodPreparation.cs index 37a9c6460723..c299960c07a9 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step03/Step03a_FoodPreparation.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Step03a_FoodPreparation.cs @@ -22,7 +22,6 @@ public class Step03a_FoodPreparation(ITestOutputHelper output) : BaseTest(output public async Task UsePrepareFriedFishProcessAsync() { var process = FriedFishProcess.CreateProcess(); - await UsePrepareSpecificProductAsync(process, FriedFishProcess.ProcessEvents.PrepareFriedFish); } From 1e275a2120a95f104765ce3159b758369f769c06 Mon Sep 17 00:00:00 2001 From: Estefania Tenorio <8483207+esttenorio@users.noreply.github.com> Date: Mon, 18 Nov 2024 12:11:57 -0800 Subject: [PATCH 06/13] changing to support only one subscriber instance of creating an instance every time --- .../Processes/RequestCounterProcess.cs | 6 +--- .../KernelProcessEventsSubscriber.cs | 20 +++++++------ .../KernelProcessEventsSubscriberInfo.cs | 28 +++++++++++++------ 3 files changed, 32 insertions(+), 22 deletions(-) diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/RequestCounterProcess.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/RequestCounterProcess.cs index 0e6c4a05c76a..b8b52bc3ef98 100644 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/RequestCounterProcess.cs +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/RequestCounterProcess.cs @@ -114,10 +114,6 @@ public static ProcessBuilder CreateProcessWithProcessSubsc public class CounterProcessSubscriber : KernelProcessEventsSubscriber { - public CounterProcessSubscriber(IServiceProvider serviceProvider) : base(serviceProvider) - { - } - private SendMailPostRequestBody GenerateEmailRequest(int counter, string emailAddress, string subject) { var message = GraphRequestFactory.CreateEmailBody( @@ -139,7 +135,7 @@ public async Task OnCounterResultReceivedAsync(int? counterResult) try { var graphClient = this.ServiceProvider?.GetRequiredService(); - var user = await graphClient.Me.GetAsync(); + var user = await graphClient?.Me.GetAsync(); var graphEmailMessage = this.GenerateEmailRequest(counterResult.Value, user!.Mail!, subject: "The counter has changed"); await graphClient?.Me.SendMail.PostAsync(graphEmailMessage); } diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventsSubscriber.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventsSubscriber.cs index 99cc687f8733..acc60431cf23 100644 --- a/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventsSubscriber.cs +++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventsSubscriber.cs @@ -3,28 +3,30 @@ using System; namespace Microsoft.SemanticKernel.Process; + +public class KernelProcessEventsSubscriber +{ + public IServiceProvider? ServiceProvider { get; init; } + + protected KernelProcessEventsSubscriber() { } +} + /// /// Attribute to set Process related steps to link Process Events to specific functions to execute when the event is emitted outside the Process /// /// Enum that contains all process events that could be subscribed to -public class KernelProcessEventsSubscriber where TEvents : Enum +public class KernelProcessEventsSubscriber : KernelProcessEventsSubscriber where TEvents : Enum { - protected readonly IServiceProvider? ServiceProvider; - /// /// Initializes a new instance of the class. /// - /// Optional service provider for resolving dependencies - public KernelProcessEventsSubscriber(IServiceProvider? serviceProvider = null) - { - this.ServiceProvider = serviceProvider; - } + public KernelProcessEventsSubscriber() { } /// /// Attribute to set Process related steps to link Process Events to specific functions to execute when the event is emitted outside the Process /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - public class ProcessEventSubscriberAttribute : Attribute + public sealed class ProcessEventSubscriberAttribute : Attribute { /// /// Gets the enum of the event that the function is linked to diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventsSubscriberInfo.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventsSubscriberInfo.cs index cbdd4883e878..bc60385721d2 100644 --- a/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventsSubscriberInfo.cs +++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventsSubscriberInfo.cs @@ -1,9 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; -using System; using Microsoft.SemanticKernel.Process; namespace Microsoft.SemanticKernel; @@ -12,9 +12,12 @@ public class KernelProcessEventsSubscriberInfo { private readonly Dictionary> _eventHandlers = []; private readonly Dictionary _stepEventProcessEventMap = []; - private Type? _processEventSubscriberType = null; + // potentially _processEventSubscriberType, _subscriberServiceProvider, _processEventSubscriber can be converted to a dictionary to support + // many unique subscriber classes that could be linked to different ServiceProviders + private Type? _processEventSubscriberType = null; private IServiceProvider? _subscriberServiceProvider = null; + private KernelProcessEventsSubscriber? _processEventSubscriber = null; protected void Subscribe(string eventName, MethodInfo method) { @@ -45,11 +48,22 @@ public void InvokeProcessEvent(string eventName, object? data) { if (this._processEventSubscriberType != null && this._eventHandlers.TryGetValue(eventName, out List? linkedMethods) && linkedMethods != null) { + if (this._processEventSubscriber == null) + { + try + { + this._processEventSubscriber = (KernelProcessEventsSubscriber?)Activator.CreateInstance(this._processEventSubscriberType, []); + this._processEventSubscriberType.GetProperty(nameof(KernelProcessEventsSubscriber.ServiceProvider))?.SetValue(this._processEventSubscriber, this._subscriberServiceProvider); + } + catch (Exception) + { + throw new KernelException($"Could not create an instance of {this._processEventSubscriberType.Name} to be used in KernelProcessSubscriberInfo"); + } + } + foreach (var method in linkedMethods) { - // TODO-estenori: Avoid creating a new instance every time a function is invoked - create instance once only? - var instance = Activator.CreateInstance(this._processEventSubscriberType, [this._subscriberServiceProvider]); - method.Invoke(instance, [data]); + method.Invoke(this._processEventSubscriber, [data]); } } } @@ -86,7 +100,5 @@ public void SubscribeToEventsFromClass(IServiceProvide this._processEventSubscriberType = typeof(TEventListeners); } - public KernelProcessEventsSubscriberInfo() - { - } + public KernelProcessEventsSubscriberInfo() { } } From 8cbd24a2b6fe3e1672cd240af3c9c1858d27d114 Mon Sep 17 00:00:00 2001 From: Estefania Tenorio <8483207+esttenorio@users.noreply.github.com> Date: Mon, 18 Nov 2024 12:23:21 -0800 Subject: [PATCH 07/13] fixing spelling errors --- dotnet/samples/Demos/ProcessWithCloudEvents/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/README.md b/dotnet/samples/Demos/ProcessWithCloudEvents/README.md index 9ee47f5e8506..474c2606240f 100644 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/README.md +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/README.md @@ -24,7 +24,7 @@ A simple counter has 3 main functionalities: - Decrease count - Reset count (set counter to 0) -To achive this behavior the SK Stateful Step `Processes/Steps/CounterStep.cs` was created. +To achieve this behavior the SK Stateful Step `Processes/Steps/CounterStep.cs` was created. On every request it stores that state that can be used to restore the state on the next request. ##### Counter Interceptor Step @@ -42,7 +42,7 @@ This step sends an email if receiving a not nullable int value to the same email ```mermaid flowchart LR subgraph API - ApiIncrease["/increse"] + ApiIncrease["/increase"] ApiDecrease["/decrease"] ApiReset["/reset"] end @@ -107,7 +107,7 @@ Cloud events related logic is encapsulated in SK Event Subscribers. ```mermaid flowchart LR subgraph API - ApiIncrease["/increse"] + ApiIncrease["/increase"] ApiDecrease["/decrease"] ApiReset["/reset"] end From 6685021f954cf587e341fcf072dfd058f32a2371 Mon Sep 17 00:00:00 2001 From: Estefania Tenorio <8483207+esttenorio@users.noreply.github.com> Date: Mon, 18 Nov 2024 13:53:11 -0800 Subject: [PATCH 08/13] addressing pipeline failures --- .../Controllers/CounterBaseController.cs | 27 ++++++++++++++++ .../CounterWithCloudStepsController.cs | 5 ++- .../CounterWithCloudSubscribersController.cs | 5 ++- .../MicrosoftGraph/GraphRequestFactory.cs | 10 ++++++ .../Processes/RequestCounterProcess.cs | 32 +++++++++---------- .../Processes/Steps/CounterInterceptorStep.cs | 4 +-- .../Processes/Steps/CounterStep.cs | 8 ++--- .../Processes/Steps/SendEmailStep.cs | 6 ++-- .../Demos/ProcessWithCloudEvents/README.md | 4 --- 9 files changed, 70 insertions(+), 31 deletions(-) diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterBaseController.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterBaseController.cs index c31bf8eecad3..98ef47862db0 100644 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterBaseController.cs +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterBaseController.cs @@ -9,9 +9,19 @@ using ProcessWithCloudEvents.Processes.Steps; namespace ProcessWithCloudEvents.Controllers; +/// +/// Base class that contains common methods to be used when using SK Processes and Counter common api entrypoints +/// public abstract class CounterBaseController : ControllerBase { + /// + /// Kernel to be used to run the SK Process + /// internal Kernel Kernel { get; init; } + + /// + /// SK Process to be used to hold the counter logic + /// internal KernelProcess Process { get; init; } private static readonly JsonSerializerOptions s_jsonOptions = new() @@ -96,13 +106,30 @@ internal async Task StartProcessWithEventAsync(string eventName, return processState; } + /// + /// API entry point to increase the counter + /// + /// current counter value public virtual async Task IncreaseCounterAsync() { return await Task.FromResult(0); } + /// + /// API entry point to decrease the counter + /// + /// current counter value public virtual async Task DecreaseCounterAsync() { return await Task.FromResult(0); } + + /// + /// API entry point to reset counter value to 0 + /// + /// current counter value + public virtual async Task ResetCounterAsync() + { + return await Task.FromResult(0); + } } diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterWithCloudStepsController.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterWithCloudStepsController.cs index 1bed5fff5fb6..9b069851180e 100644 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterWithCloudStepsController.cs +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterWithCloudStepsController.cs @@ -19,6 +19,7 @@ public CounterWithCloudStepsController(ILogger this.Process = this.InitializeProcess(RequestCounterProcess.CreateProcessWithCloudSteps()); } + /// [HttpGet("increase", Name = "IncreaseWithCloudSteps")] public override async Task IncreaseCounterAsync() { @@ -29,6 +30,7 @@ public override async Task IncreaseCounterAsync() return counterState?.State?.Counter ?? -1; } + /// [HttpGet("decrease", Name = "DecreaseWithCloudSteps")] public override async Task DecreaseCounterAsync() { @@ -39,8 +41,9 @@ public override async Task DecreaseCounterAsync() return counterState?.State?.Counter ?? -1; } + /// [HttpGet("reset", Name = "ResetCounterWithCloudSteps")] - public async Task ResetCounterAsync() + public override async Task ResetCounterAsync() { var eventName = RequestCounterProcess.GetEventName(RequestCounterProcess.CounterProcessEvents.ResetCounterRequest); var runningProcess = await this.StartProcessWithEventAsync(eventName); diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterWithCloudSubscribersController.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterWithCloudSubscribersController.cs index 105721eabb78..bc57705e0a34 100644 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterWithCloudSubscribersController.cs +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/Controllers/CounterWithCloudSubscribersController.cs @@ -22,6 +22,7 @@ public CounterWithCloudSubscribersController(ILogger [HttpGet("increase", Name = "IncreaseCounterWithCloudSubscribers")] public override async Task IncreaseCounterAsync() { @@ -32,6 +33,7 @@ public override async Task IncreaseCounterAsync() return counterState?.State?.Counter ?? -1; } + /// [HttpGet("decrease", Name = "DecreaseCounterWithCloudSubscribers")] public override async Task DecreaseCounterAsync() { @@ -42,8 +44,9 @@ public override async Task DecreaseCounterAsync() return counterState?.State?.Counter ?? -1; } + /// [HttpGet("reset", Name = "ResetCounterWithCloudSubscribers")] - public async Task ResetCounterAsync() + public override async Task ResetCounterAsync() { var eventName = RequestCounterProcess.GetEventName(RequestCounterProcess.CounterProcessEvents.ResetCounterRequest); var runningProcess = await this.StartProcessWithEventAsync(eventName); diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/MicrosoftGraph/GraphRequestFactory.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/MicrosoftGraph/GraphRequestFactory.cs index 915aa261342b..f7253d3e2833 100644 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/MicrosoftGraph/GraphRequestFactory.cs +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/MicrosoftGraph/GraphRequestFactory.cs @@ -5,8 +5,18 @@ namespace ProcessWithCloudEvents.MicrosoftGraph; +/// +/// Factory that creates Microsoft Graph related objects +/// public static class GraphRequestFactory { + /// + /// Method that creates MailPost Body with defined subject, content and recipients + /// + /// subject of the email + /// content of the email + /// recipients of the email + /// public static SendMailPostRequestBody CreateEmailBody(string subject, string content, List recipients) { var message = new SendMailPostRequestBody() diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/RequestCounterProcess.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/RequestCounterProcess.cs index b8b52bc3ef98..edef808000bb 100644 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/RequestCounterProcess.cs +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/RequestCounterProcess.cs @@ -42,31 +42,31 @@ public static ProcessBuilder CreateProcessWithCloudSteps() processBuilder .OnInputEvent(processBuilder.GetEventName(CounterProcessEvents.IncreaseCounterRequest)) - .SendEventTo(new ProcessFunctionTargetBuilder(counterStep, functionName: CounterStep.Functions.IncreaseCounter)); + .SendEventTo(new ProcessFunctionTargetBuilder(counterStep, functionName: CounterStep.StepFunctions.IncreaseCounter)); processBuilder .OnInputEvent(processBuilder.GetEventName(CounterProcessEvents.DecreaseCounterRequest)) - .SendEventTo(new ProcessFunctionTargetBuilder(counterStep, functionName: CounterStep.Functions.DecreaseCounter)); + .SendEventTo(new ProcessFunctionTargetBuilder(counterStep, functionName: CounterStep.StepFunctions.DecreaseCounter)); processBuilder .OnInputEvent(processBuilder.GetEventName(CounterProcessEvents.ResetCounterRequest)) - .SendEventTo(new ProcessFunctionTargetBuilder(counterStep, functionName: CounterStep.Functions.ResetCounter)); + .SendEventTo(new ProcessFunctionTargetBuilder(counterStep, functionName: CounterStep.StepFunctions.ResetCounter)); counterStep - .OnFunctionResult(CounterStep.Functions.IncreaseCounter) + .OnFunctionResult(CounterStep.StepFunctions.IncreaseCounter) .SendEventTo(new ProcessFunctionTargetBuilder(counterInterceptorStep)); counterStep - .OnFunctionResult(CounterStep.Functions.DecreaseCounter) + .OnFunctionResult(CounterStep.StepFunctions.DecreaseCounter) .SendEventTo(new ProcessFunctionTargetBuilder(counterInterceptorStep)); counterStep - .OnFunctionResult(CounterStep.Functions.ResetCounter) - .SendEventTo(new ProcessFunctionTargetBuilder(emailSenderStep, SendEmailStep.Functions.SendCounterResetEmail)); + .OnFunctionResult(CounterStep.StepFunctions.ResetCounter) + .SendEventTo(new ProcessFunctionTargetBuilder(emailSenderStep, SendEmailStep.StepFunctions.SendCounterResetEmail)); counterInterceptorStep - .OnFunctionResult(CounterInterceptorStep.Functions.InterceptCounter) - .SendEventTo(new ProcessFunctionTargetBuilder(emailSenderStep, SendEmailStep.Functions.SendCounterChangeEmail)); + .OnFunctionResult(CounterInterceptorStep.StepFunctions.InterceptCounter) + .SendEventTo(new ProcessFunctionTargetBuilder(emailSenderStep, SendEmailStep.StepFunctions.SendCounterChangeEmail)); return processBuilder; } @@ -80,31 +80,31 @@ public static ProcessBuilder CreateProcessWithProcessSubsc processBuilder .OnInputEvent(processBuilder.GetEventName(CounterProcessEvents.IncreaseCounterRequest)) - .SendEventTo(new ProcessFunctionTargetBuilder(counterStep, functionName: CounterStep.Functions.IncreaseCounter)); + .SendEventTo(new ProcessFunctionTargetBuilder(counterStep, functionName: CounterStep.StepFunctions.IncreaseCounter)); processBuilder .OnInputEvent(processBuilder.GetEventName(CounterProcessEvents.DecreaseCounterRequest)) - .SendEventTo(new ProcessFunctionTargetBuilder(counterStep, functionName: CounterStep.Functions.DecreaseCounter)); + .SendEventTo(new ProcessFunctionTargetBuilder(counterStep, functionName: CounterStep.StepFunctions.DecreaseCounter)); processBuilder .OnInputEvent(processBuilder.GetEventName(CounterProcessEvents.ResetCounterRequest)) - .SendEventTo(new ProcessFunctionTargetBuilder(counterStep, functionName: CounterStep.Functions.ResetCounter)); + .SendEventTo(new ProcessFunctionTargetBuilder(counterStep, functionName: CounterStep.StepFunctions.ResetCounter)); counterStep - .OnFunctionResult(CounterStep.Functions.IncreaseCounter) + .OnFunctionResult(CounterStep.StepFunctions.IncreaseCounter) .SendEventTo(new ProcessFunctionTargetBuilder(counterInterceptorStep)); counterStep - .OnFunctionResult(CounterStep.Functions.DecreaseCounter) + .OnFunctionResult(CounterStep.StepFunctions.DecreaseCounter) .SendEventTo(new ProcessFunctionTargetBuilder(counterInterceptorStep)); counterStep - .OnFunctionResult(CounterStep.Functions.ResetCounter) + .OnFunctionResult(CounterStep.StepFunctions.ResetCounter) .EmitAsProcessEvent(processBuilder.GetProcessEvent(CounterProcessEvents.OnCounterReset)) .SendEventTo(new ProcessFunctionTargetBuilder(counterInterceptorStep)); counterInterceptorStep - .OnFunctionResult(CounterInterceptorStep.Functions.InterceptCounter) + .OnFunctionResult(CounterInterceptorStep.StepFunctions.InterceptCounter) .EmitAsProcessEvent(processBuilder.GetProcessEvent(CounterProcessEvents.OnCounterResult)); processBuilder.LinkEventSubscribersFromType(serviceProvider); diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/CounterInterceptorStep.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/CounterInterceptorStep.cs index 28fd970fed3b..827b346f9232 100644 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/CounterInterceptorStep.cs +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/CounterInterceptorStep.cs @@ -6,12 +6,12 @@ namespace ProcessWithCloudEvents.Processes.Steps; public class CounterInterceptorStep : KernelProcessStep { - public static class Functions + public static class StepFunctions { public const string InterceptCounter = nameof(InterceptCounter); } - [KernelFunction(Functions.InterceptCounter)] + [KernelFunction(StepFunctions.InterceptCounter)] public int? InterceptCounter(int counterStatus) { var multipleOf = 3; diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/CounterStep.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/CounterStep.cs index 34da8d17d2d8..48738c0bfed4 100644 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/CounterStep.cs +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/CounterStep.cs @@ -6,7 +6,7 @@ namespace ProcessWithCloudEvents.Processes.Steps; public class CounterStep : KernelProcessStep { - public static class Functions + public static class StepFunctions { public const string IncreaseCounter = nameof(IncreaseCounter); public const string DecreaseCounter = nameof(DecreaseCounter); @@ -26,7 +26,7 @@ public override ValueTask ActivateAsync(KernelProcessStepState return ValueTask.CompletedTask; } - [KernelFunction(Functions.IncreaseCounter)] + [KernelFunction(StepFunctions.IncreaseCounter)] public async Task IncreaseCounterAsync(KernelProcessStepContext context) { this._state!.Counter += this._state.CounterIncrements; @@ -40,7 +40,7 @@ public async Task IncreaseCounterAsync(KernelProcessStepContext context) return this._state.Counter; } - [KernelFunction(Functions.DecreaseCounter)] + [KernelFunction(StepFunctions.DecreaseCounter)] public async Task DecreaseCounterAsync(KernelProcessStepContext context) { this._state!.Counter -= this._state.CounterIncrements; @@ -54,7 +54,7 @@ public async Task DecreaseCounterAsync(KernelProcessStepContext context) return this._state.Counter; } - [KernelFunction(Functions.ResetCounter)] + [KernelFunction(StepFunctions.ResetCounter)] public async Task ResetCounterAsync(KernelProcessStepContext context) { this._state!.Counter = 0; diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/SendEmailStep.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/SendEmailStep.cs index e44c6d1da9d5..92fc6244c925 100644 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/SendEmailStep.cs +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/Processes/Steps/SendEmailStep.cs @@ -15,7 +15,7 @@ public static class OutputEvents public const string SendEmailFailure = nameof(SendEmailFailure); } - public static class Functions + public static class StepFunctions { public const string SendCounterChangeEmail = nameof(SendCounterChangeEmail); public const string SendCounterResetEmail = nameof(SendCounterResetEmail); @@ -33,7 +33,7 @@ protected SendMailPostRequestBody PopulateMicrosoftGraphMailMessage(object input return message; } - [KernelFunction(Functions.SendCounterChangeEmail)] + [KernelFunction(StepFunctions.SendCounterChangeEmail)] public async Task PublishCounterChangedEmailMessageAsync(KernelProcessStepContext context, Kernel kernel, object inputData) { if (inputData == null) @@ -57,7 +57,7 @@ public async Task PublishCounterChangedEmailMessageAsync(KernelProcessStepContex } } - [KernelFunction(Functions.SendCounterResetEmail)] + [KernelFunction(StepFunctions.SendCounterResetEmail)] public async Task PublishCounterResetEmailMessageAsync(KernelProcessStepContext context, Kernel kernel, object inputData) { if (inputData == null) diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/README.md b/dotnet/samples/Demos/ProcessWithCloudEvents/README.md index 474c2606240f..3d5f2983cdfe 100644 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/README.md +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/README.md @@ -186,10 +186,6 @@ flowchart LR ```C# public class CounterProcessSubscriber : KernelProcessEventsSubscriber { - public CounterProcessSubscriber(IServiceProvider serviceProvider) : base(serviceProvider) - { - } - [ProcessEventSubscriber(CounterProcessEvents.OnCounterResult)] public async Task OnCounterResultReceivedAsync(int? counterResult) { From 10416626adc09c4231e536344ac5a0f680f115aa Mon Sep 17 00:00:00 2001 From: Estefania Tenorio <8483207+esttenorio@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:20:36 -0800 Subject: [PATCH 09/13] adding warning exception --- .../Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.csproj b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.csproj index b6ea023f8336..0d8d4711ef5b 100644 --- a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.csproj +++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.csproj @@ -5,7 +5,7 @@ enable enable - $(NoWarn);CA2007,CA1861,VSTHRD111,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0080,SKEXP0110 + $(NoWarn);CA2007,CA1861,CA1050,VSTHRD111,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0080,SKEXP0110 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0 From 6a77254135324fe93a9b7e4bb3bcd628d90ba16e Mon Sep 17 00:00:00 2001 From: Estefania Tenorio <8483207+esttenorio@users.noreply.github.com> Date: Tue, 19 Nov 2024 09:52:00 -0800 Subject: [PATCH 10/13] fixing formatting --- .../Step03/Processes/FriedFishProcess.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FriedFishProcess.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FriedFishProcess.cs index 7a04f1ad5093..076c4f932641 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FriedFishProcess.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FriedFishProcess.cs @@ -81,13 +81,13 @@ public static ProcessBuilder CreateProcessWithStatefulStepsV1(string processName return processBuilder; } - /// - /// For a visual reference of the FriedFishProcess with stateful steps check this - /// diagram - /// - /// name of the process - /// - public static ProcessBuilder CreateProcessWithStatefulStepsV2(string processName = "FriedFishWithStatefulStepsProcess") + /// + /// For a visual reference of the FriedFishProcess with stateful steps check this + /// diagram + /// + /// name of the process + /// + public static ProcessBuilder CreateProcessWithStatefulStepsV2(string processName = "FriedFishWithStatefulStepsProcess") { // It is recommended to specify process version in case this process is used as a step by another process var processBuilder = new ProcessBuilder(processName) { Version = "FriedFishProcess.v2" }; From 1c09f764b4d0caefcca0d2064457224d50368010 Mon Sep 17 00:00:00 2001 From: Estefania Tenorio <8483207+esttenorio@users.noreply.github.com> Date: Tue, 19 Nov 2024 15:59:02 -0800 Subject: [PATCH 11/13] Updating account opening sample to introduce SK Event Subscribers + updating readme --- .../GettingStartedWithProcesses/README.md | 136 ++++++++++++++++++ .../Step02/Models/AccountDetails.cs | 2 +- .../Step02/Models/AccountOpeningEvents.cs | 2 +- .../Models/AccountUserInteractionDetails.cs | 2 +- .../Step02/Models/MarketingNewEntryDetails.cs | 2 +- .../Step02/Models/NewCustomerForm.cs | 2 +- ...ntOpening.cs => Step02a_AccountOpening.cs} | 2 +- .../Process.Core/ProcessStepBuilder.cs | 1 - 8 files changed, 142 insertions(+), 7 deletions(-) rename dotnet/samples/GettingStartedWithProcesses/Step02/{Step02_AccountOpening.cs => Step02a_AccountOpening.cs} (99%) diff --git a/dotnet/samples/GettingStartedWithProcesses/README.md b/dotnet/samples/GettingStartedWithProcesses/README.md index ff28c1a91a80..624899dbacfc 100644 --- a/dotnet/samples/GettingStartedWithProcesses/README.md +++ b/dotnet/samples/GettingStartedWithProcesses/README.md @@ -49,6 +49,27 @@ flowchart LR ### Step02_AccountOpening +The account opening sample has 3 different implementations covering the same scenario, it just uses different SK components to achieve the same goal. + +In addition, the sample introduces the concept of using smaller process as steps to maintain the main process readable and manageble for future improvements and unit testing. +Also introduces the use of SK Event Subscribers. + +A process for opening an account for this sample has the following steps: +- Fill New User Account Application Form +- Verify Applicant Credit Score +- Apply Fraud Detection Analysis to the Application Form +- Create New Entry in Core System Records +- Add new account to Marketing Records +- CRM Record Creation +- Mail user a user a notification about: + - Failure to open a new account due to Credit Score Check + - Failure to open a new account due to Fraud Detection Alert + - Welcome package including new account details + +A SK process that only connects the steps listed above as is (no use of subprocesses as steps) for opening an account look like this: + +#### Step02a_AccountOpening + ```mermaid flowchart LR User(User) -->|Provides user details| FillForm(Fill New
Customer
Form) @@ -79,6 +100,121 @@ flowchart LR Mailer -->|End of Interaction| User ``` +#### Step02b_AccountOpening + +After grouping steps that have a common theme/dependencies, and creating smaller subprocesses and using them as steps, +the root process looks like this: + +```mermaid +flowchart LR + User(User) + FillForm(Chat With User
to Fill New
Customer Form) + NewAccountVerification[[New Account Verification
Process]] + NewAccountCreation[[New Account Creation
Process]] + Mailer(Mail
Service) + + User<-->|Provides user details|FillForm + FillForm-->|New User Form|NewAccountVerification + NewAccountVerification-->|Account Verification Failed|Mailer + NewAccountVerification-->|Account Verification Succeded|NewAccountCreation + NewAccountCreation-->|Account Creation Succeded|Mailer +``` + +Where processes used as steps, which are reusing the same steps used [`Step02a_AccountOpening`](#step02a_accountopening), are: + +```mermaid +graph LR + NewUserForm([New User Form]) + NewUserFormConv([Form Filling Interaction]) + + subgraph AccountCreation[Account Creation Process] + direction LR + AccountValidation([Account Verification Passed]) + NewUser1([New User Form]) + NewUserFormConv1([Form Filling Interaction]) + + CoreSystem(Core System
Record
Creation) + Marketing(New Marketing
Record Creation) + CRM(CRM Record
Creation) + Welcome(Welcome
Packet) + NewAccountCreation([New Account Success]) + + NewUser1-->CoreSystem + NewUserFormConv1-->CoreSystem + + AccountValidation-->CoreSystem + CoreSystem-->CRM-->|Success|Welcome + CoreSystem-->Marketing-->|Success|Welcome + CoreSystem-->|Account Details|Welcome + + Welcome-->NewAccountCreation + end + + subgraph AccountVerification[Account Verification Process] + direction LR + NewUser2([New User Form]) + CreditScoreCheck[Credit Check
Step] + FraudCheck[Fraud Detection
Step] + AccountVerificationPass([Account Verification Passed]) + AccountCreditCheckFail([Credit Check Failed]) + AccoutFraudCheckFail([Fraud Check Failed]) + + + NewUser2-->CreditScoreCheck-->|Credit Score
Check Passed|FraudCheck + FraudCheck-->AccountVerificationPass + + CreditScoreCheck-->AccountCreditCheckFail + FraudCheck-->AccoutFraudCheckFail + end + + AccountVerificationPass-->AccountValidation + NewUserForm-->NewUser1 + NewUserForm-->NewUser2 + NewUserFormConv-->NewUserFormConv1 + +``` + +#### Step02c_AccountOpeningWithCloudEvents + +An additional optimization that could be made to the Account Creation sample, is to make use of SK Event subscriber to isolate logic that has to do with cloud events. +In this sample, the cloud event logic is mocked by the Mail Service functionality, which mocks sending an email to the user in different circumstances: + +- When new user credit score check fails +- When new user fraud detection fails +- When a new account was created successfully after passing all checks and creation steps + +When using SK Event subscribers, specific process events when trigged will emit the event data externally to +any subscribers linked to specific events. + +```mermaid +graph LR + subgraph EventSubscribers[SK Event Subscribers] + OnSendMailDueCreditCheckFailure + OnSendMailDueFraudCheckFailure + OnSendMailWithNewAccountInfo + end + + subgraph Process[SK Process] + direction LR + User(User) + FillForm(Chat With User
to Fill New
Customer Form) + NewAccountVerification[[New Account Verification
Process]] + NewAccountCreation[[New Account Creation
Process]] + + User<-->|Provides user details|FillForm + FillForm-->|New User Form|NewAccountVerification-->|Account Verification
Succeded|NewAccountCreation + end + + NewAccountVerification-->|Account Credit Check Failed|OnSendMailDueCreditCheckFailure + NewAccountVerification-->|Account Fraud Detection Failed|OnSendMailDueFraudCheckFailure + NewAccountCreation-->|Account Creation Succeded|OnSendMailWithNewAccountInfo + +``` +Creating a separation with SK Process when using cloud events (even though in this sample it's a mock of a Mailer), it is useful since +it can help to isolate additional logic related to authentication, use of additional frameworks, etc. + +For a more realistic sample of SK Process emitting real cloud events check out the [`ProcessWithCloudEvents` Demo](../Demos/ProcessWithCloudEvents/README.md). + ### Step03a_FoodPreparation This tutorial contains a set of food recipes associated with the Food Preparation Processes of a restaurant. diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountDetails.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountDetails.cs index 6f732669d5dc..0e8274fe6900 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountDetails.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountDetails.cs @@ -4,7 +4,7 @@ namespace Step02.Models; /// /// Represents the data structure for a form capturing details of a new customer, including personal information, contact details, account id and account type.
-/// Class used in samples +/// Class used in samples ///
public class AccountDetails : NewCustomerForm { diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountOpeningEvents.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountOpeningEvents.cs index de1110854e27..32bcd0cca4d9 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountOpeningEvents.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountOpeningEvents.cs @@ -3,7 +3,7 @@ namespace Step02.Models; /// /// Processes Events related to Account Opening scenarios.
-/// Class used in samples +/// Class used in samples ///
public static class AccountOpeningEvents { diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountUserInteractionDetails.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountUserInteractionDetails.cs index 123f0b2e417d..0db9a7987fa1 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountUserInteractionDetails.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Models/AccountUserInteractionDetails.cs @@ -7,7 +7,7 @@ namespace Step02.Models; /// /// Represents the details of interactions between a user and service, including a unique identifier for the account, /// a transcript of conversation with the user, and the type of user interaction.
-/// Class used in samples +/// Class used in samples ///
public record AccountUserInteractionDetails { diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Models/MarketingNewEntryDetails.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Models/MarketingNewEntryDetails.cs index 057e97c81597..fd10646a8b74 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step02/Models/MarketingNewEntryDetails.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Models/MarketingNewEntryDetails.cs @@ -4,7 +4,7 @@ namespace Step02.Models; /// /// Holds details for a new entry in a marketing database, including the account identifier, contact name, phone number, and email address.
-/// Class used in samples +/// Class used in samples ///
public record MarketingNewEntryDetails { diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Models/NewCustomerForm.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Models/NewCustomerForm.cs index c000b8491d24..1d469b19d994 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step02/Models/NewCustomerForm.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Models/NewCustomerForm.cs @@ -7,7 +7,7 @@ namespace Step02.Models; /// /// Represents the data structure for a form capturing details of a new customer, including personal information and contact details.
-/// Class used in samples +/// Class used in samples ///
public class NewCustomerForm { diff --git a/dotnet/samples/GettingStartedWithProcesses/Step02/Step02_AccountOpening.cs b/dotnet/samples/GettingStartedWithProcesses/Step02/Step02a_AccountOpening.cs similarity index 99% rename from dotnet/samples/GettingStartedWithProcesses/Step02/Step02_AccountOpening.cs rename to dotnet/samples/GettingStartedWithProcesses/Step02/Step02a_AccountOpening.cs index a523dc4119a3..2c033dfad8e0 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step02/Step02_AccountOpening.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step02/Step02a_AccountOpening.cs @@ -14,7 +14,7 @@ namespace Step02; /// For each test there is a different set of user messages that will cause different steps to be triggered using the same pipeline.
/// For visual reference of the process check the diagram . ///
-public class Step02_AccountOpening(ITestOutputHelper output) : BaseTest(output, redirectSystemConsoleOutput: true) +public class Step02a_AccountOpening(ITestOutputHelper output) : BaseTest(output, redirectSystemConsoleOutput: true) { // Target Open AI Services protected override bool ForceOpenAI => true; diff --git a/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs index 2d4975f0be24..be2f697f704d 100644 --- a/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs +++ b/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs @@ -35,7 +35,6 @@ 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. /// An instance of . public ProcessStepEdgeBuilder OnEvent(string eventName) { From 0db3b43adc12152c0da3320241f4cbc8224cc562 Mon Sep 17 00:00:00 2001 From: Estefania Tenorio <8483207+esttenorio@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:06:10 -0800 Subject: [PATCH 12/13] updating readme mermaid graphs to not cut off --- dotnet/samples/GettingStartedWithProcesses/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dotnet/samples/GettingStartedWithProcesses/README.md b/dotnet/samples/GettingStartedWithProcesses/README.md index 624899dbacfc..7f2198eaa32a 100644 --- a/dotnet/samples/GettingStartedWithProcesses/README.md +++ b/dotnet/samples/GettingStartedWithProcesses/README.md @@ -115,9 +115,9 @@ flowchart LR User<-->|Provides user details|FillForm FillForm-->|New User Form|NewAccountVerification - NewAccountVerification-->|Account Verification Failed|Mailer - NewAccountVerification-->|Account Verification Succeded|NewAccountCreation - NewAccountCreation-->|Account Creation Succeded|Mailer + NewAccountVerification-->|Account Verification
Failed|Mailer + NewAccountVerification-->|Account Verification
Succeded|NewAccountCreation + NewAccountCreation-->|Account Creation
Succeded|Mailer ``` Where processes used as steps, which are reusing the same steps used [`Step02a_AccountOpening`](#step02a_accountopening), are: @@ -205,9 +205,9 @@ graph LR FillForm-->|New User Form|NewAccountVerification-->|Account Verification
Succeded|NewAccountCreation end - NewAccountVerification-->|Account Credit Check Failed|OnSendMailDueCreditCheckFailure - NewAccountVerification-->|Account Fraud Detection Failed|OnSendMailDueFraudCheckFailure - NewAccountCreation-->|Account Creation Succeded|OnSendMailWithNewAccountInfo + NewAccountVerification-->|Account Credit Check
Failed|OnSendMailDueCreditCheckFailure + NewAccountVerification-->|Account Fraud Detection
Failed|OnSendMailDueFraudCheckFailure + NewAccountCreation-->|Account Creation
Succeded|OnSendMailWithNewAccountInfo ``` Creating a separation with SK Process when using cloud events (even though in this sample it's a mock of a Mailer), it is useful since From 1719ca4361dd54280458fa305fe85bb7bbfadf64 Mon Sep 17 00:00:00 2001 From: Estefania Tenorio <8483207+esttenorio@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:12:34 -0800 Subject: [PATCH 13/13] fixing readability of Step02c_AccountOpeningWithCloudEvents mermaid graph --- dotnet/samples/GettingStartedWithProcesses/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dotnet/samples/GettingStartedWithProcesses/README.md b/dotnet/samples/GettingStartedWithProcesses/README.md index 7f2198eaa32a..b207fae3c2e8 100644 --- a/dotnet/samples/GettingStartedWithProcesses/README.md +++ b/dotnet/samples/GettingStartedWithProcesses/README.md @@ -189,9 +189,9 @@ any subscribers linked to specific events. ```mermaid graph LR subgraph EventSubscribers[SK Event Subscribers] - OnSendMailDueCreditCheckFailure - OnSendMailDueFraudCheckFailure - OnSendMailWithNewAccountInfo + OnSendMailDueCreditCheckFailure[OnSendMailDueCredit
CheckFailure] + OnSendMailDueFraudCheckFailure[OnSendMailDueFraud
CheckFailure] + OnSendMailWithNewAccountInfo[OnSendMailWith
NewAccountInfo] end subgraph Process[SK Process]