diff --git a/dotnet/SK-dotnet.slnx b/dotnet/SK-dotnet.slnx
index 495a570d6941..e3826a91ff8d 100644
--- a/dotnet/SK-dotnet.slnx
+++ b/dotnet/SK-dotnet.slnx
@@ -79,8 +79,10 @@
+
+
diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/DocumentGenerationProcess.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/DocumentGenerationProcess.cs
index fdfc8c7497fe..c1030ffa3ba3 100644
--- a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/DocumentGenerationProcess.cs
+++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/DocumentGenerationProcess.cs
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.
+using System.Text.Json.Serialization;
using Microsoft.SemanticKernel;
+using ProcessWithCloudEvents.Processes.Models;
using ProcessWithCloudEvents.Processes.Steps;
namespace ProcessWithCloudEvents.Processes;
@@ -34,6 +36,13 @@ public static class DocGenerationEvents
public const string UserApprovedDocument = nameof(UserApprovedDocument);
}
+ public static class ProcessInputEvents
+ {
+ public static readonly KernelProcessEventDescriptor StartDocumentGeneration = new(nameof(StartDocumentGeneration));
+ public static readonly KernelProcessEventDescriptor UserApprovedDocument = new(nameof(UserApprovedDocument));
+ public static readonly KernelProcessEventDescriptor UserRejectedDocument = new(nameof(UserRejectedDocument));
+ }
+
///
/// SK Process topics emitted by
/// Topics are used to emit events to external systems
@@ -57,8 +66,8 @@ public static class DocGenerationTopics
/// instance of
public static ProcessBuilder CreateProcessBuilder(string processName = "DocumentationGeneration")
{
- // Create the process builder
- ProcessBuilder processBuilder = new(processName);
+ // Create the process builder
+ ProcessBuilder processBuilder = new(processName, processOptions: new() { JsonSerializerAdditionalContexts = [DocumentJsonSerializerContext.Default] });
// Add the steps
var infoGatheringStep = processBuilder.AddStepFromType();
@@ -70,7 +79,7 @@ public static ProcessBuilder CreateProcessBuilder(string processName = "Document
// Orchestrate the external input events
processBuilder
- .OnInputEvent(DocGenerationEvents.StartDocumentGeneration)
+ .OnInputEvent(ProcessInputEvents.StartDocumentGeneration)
.SendEventTo(new(infoGatheringStep));
processBuilder
@@ -87,39 +96,45 @@ public static ProcessBuilder CreateProcessBuilder(string processName = "Document
.SendEventTo(new ProcessFunctionTargetBuilder(docsGenerationStep, functionName: GenerateDocumentationStep.ProcessFunctions.GenerateDocs));
docsGenerationStep
- .OnEvent(GenerateDocumentationStep.OutputEvents.DocumentationGenerated)
+ .OnEvent(GenerateDocumentationStep.StepEvents.DocumentationGenerated)
.SendEventTo(new ProcessFunctionTargetBuilder(docsProofreadStep));
docsProofreadStep
- .OnEvent(ProofReadDocumentationStep.OutputEvents.DocumentationRejected)
+ .OnEvent(ProofReadDocumentationStep.StepEvents.DocumentationRejected)
.SendEventTo(new ProcessFunctionTargetBuilder(docsGenerationStep, functionName: GenerateDocumentationStep.ProcessFunctions.ApplySuggestions));
// When the proofreader approves the documentation, send it to the 'docs' parameter of the docsPublishStep
// Additionally, the generated document is emitted externally for user approval using the pre-configured proxyStep
docsProofreadStep
- .OnEvent(ProofReadDocumentationStep.OutputEvents.DocumentationApproved)
- .EmitExternalEvent(proxyStep, DocGenerationTopics.RequestUserReview);
+ .OnEvent(ProofReadDocumentationStep.StepEvents.DocumentationApproved)
+ .EmitExternalEvent(proxyStep, DocGenerationTopics.RequestUserReview)
+ .EmitAsPublicEvent();
//.SendEventTo(new ProcessFunctionTargetBuilder(docsPublishStep, parameterName: "document"));
processBuilder
.ListenFor()
.AllOf([
- new(messageType: ProofReadDocumentationStep.OutputEvents.DocumentationApproved, docsProofreadStep),
- new(messageType: DocGenerationEvents.UserApprovedDocument, processBuilder),
+ new TypedMessageSourceBuilder(messageType: ProofReadDocumentationStep.StepEvents.DocumentationApproved, docsProofreadStep),
+ new TypedMessageSourceBuilder(messageType: ProcessInputEvents.UserApprovedDocument, processBuilder),
])
.SendEventTo(new ProcessStepTargetBuilder(docsPublishStep, inputMapping: (inputEvents) =>
{
return new() {
- { "document", inputEvents[docsProofreadStep.GetFullEventId(ProofReadDocumentationStep.OutputEvents.DocumentationApproved)] },
- { "userApproval", inputEvents[processBuilder.GetFullEventId(DocGenerationEvents.UserApprovedDocument)] },
+ { "document", inputEvents[docsProofreadStep.GetFullEventId(ProofReadDocumentationStep.StepEvents.DocumentationApproved)] },
+ { "userApproval", inputEvents[processBuilder.GetFullEventId(ProcessInputEvents.UserApprovedDocument)] },
};
}));
// When event is approved by user, it gets published externally too
docsPublishStep
.OnFunctionResult()
- .EmitExternalEvent(proxyStep, DocGenerationTopics.PublishDocumentation);
+ .EmitExternalEvent(proxyStep, DocGenerationTopics.PublishDocumentation)
+ .EmitAsPublicEvent();
return processBuilder;
}
}
+
+[JsonSerializable(typeof(ProductInfo))]
+[JsonSerializable(typeof(DocumentInfo))]
+public partial class DocumentJsonSerializerContext : JsonSerializerContext;
diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/Steps/GenerateDocumentationStep.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/Steps/GenerateDocumentationStep.cs
index 5b9f3b1591b4..58fab0b4b8e6 100644
--- a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/Steps/GenerateDocumentationStep.cs
+++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/Steps/GenerateDocumentationStep.cs
@@ -29,12 +29,12 @@ public static class ProcessFunctions
///
/// Output events of the step, using this since 2 steps emit the same output event
///
- public static class OutputEvents
+ public static new class StepEvents
{
///
/// Document Generated output event
///
- public const string DocumentationGenerated = nameof(DocumentationGenerated);
+ public static readonly KernelProcessEventDescriptor DocumentationGenerated = new(nameof(DocumentationGenerated));
}
internal GenerateDocumentationState _state = new();
@@ -84,7 +84,7 @@ public async Task GenerateDocumentationAsync(Kernel kernel, KernelProcessStepCon
this._state!.LastGeneratedDocument = generatedContent;
- await context.EmitEventAsync(OutputEvents.DocumentationGenerated, generatedContent);
+ await context.EmitEventAsync(StepEvents.DocumentationGenerated, generatedContent);
}
///
@@ -115,7 +115,7 @@ public async Task ApplySuggestionsAsync(Kernel kernel, KernelProcessStepContext
this._state!.LastGeneratedDocument = updatedContent;
- await context.EmitEventAsync(OutputEvents.DocumentationGenerated, updatedContent);
+ await context.EmitEventAsync(StepEvents.DocumentationGenerated, updatedContent);
}
}
diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/Steps/ProofreadDocumentationStep.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/Steps/ProofreadDocumentationStep.cs
index eddd0fda7c7e..6c757ddee6dd 100644
--- a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/Steps/ProofreadDocumentationStep.cs
+++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/Steps/ProofreadDocumentationStep.cs
@@ -17,16 +17,16 @@ public class ProofReadDocumentationStep : KernelProcessStep
///
/// SK Process Events emitted by
///
- public static class OutputEvents
+ public static new class StepEvents
{
///
/// Document has errors and needs to be revised event
///
- public const string DocumentationRejected = nameof(DocumentationRejected);
+ public static readonly KernelProcessEventDescriptor DocumentationRejected = new(nameof(DocumentationRejected));
///
/// Document looks ok and can be processed by the next step
///
- public const string DocumentationApproved = nameof(DocumentationApproved);
+ public static readonly KernelProcessEventDescriptor DocumentationApproved = new(nameof(DocumentationApproved));
}
private readonly string _systemPrompt = """"
@@ -70,16 +70,13 @@ public async Task ProofreadDocumentationAsync(Kernel kernel, KernelProcessStepCo
{
// Events that are getting piped to steps that will be resumed, like PublishDocumentationStep.OnPublishDocumentation
// require events to be marked as public so they are persisted and restored correctly
- await context.EmitEventAsync(OutputEvents.DocumentationApproved, data: document, visibility: KernelProcessEventVisibility.Public);
+ await context.EmitEventAsync(StepEvents.DocumentationApproved, document);
}
else
{
- await context.EmitEventAsync(new()
- {
- Id = OutputEvents.DocumentationRejected,
+ await context.EmitEventAsync(StepEvents.DocumentationRejected,
// This event is getting piped to the GenerateDocumentationStep.ApplySuggestionsAsync step which expects a string with suggestions for the document
- Data = $"Explanation = {formattedResponse.Explanation}, Suggestions = {string.Join(",", formattedResponse.Suggestions)} ",
- });
+ $"Explanation = {formattedResponse.Explanation}, Suggestions = {string.Join(",", formattedResponse.Suggestions)} ");
}
}
diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Tests.LocalRuntime/ProcessWithCloudEvents.Tests.LocalRuntime.csproj b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Tests.LocalRuntime/ProcessWithCloudEvents.Tests.LocalRuntime.csproj
new file mode 100644
index 000000000000..dc625d3d613a
--- /dev/null
+++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Tests.LocalRuntime/ProcessWithCloudEvents.Tests.LocalRuntime.csproj
@@ -0,0 +1,53 @@
+
+
+
+ ProcessWithCloudEventsLocalRuntimeTests
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+ $(NoWarn);CS8618,IDE0005,IDE0009,IDE1006,CA1051,CA1050,CA1707,CA1812,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0080,SKEXP0081,SKEXP0101,SKEXP0110,OPENAI001
+
+ Library
+ 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Tests.LocalRuntime/ProcessWithCloudEvents_DocumentGenerationProcess.cs b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Tests.LocalRuntime/ProcessWithCloudEvents_DocumentGenerationProcess.cs
new file mode 100644
index 000000000000..ac5c9df961fd
--- /dev/null
+++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Tests.LocalRuntime/ProcessWithCloudEvents_DocumentGenerationProcess.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.SemanticKernel;
+using ProcessWithCloudEvents.Processes;
+using static ProcessWithCloudEvents.Processes.DocumentGenerationProcess;
+
+namespace ProcessWithCloudEvents.Tests.LocalRuntime;
+
+public class ProcessWithCloudEvents_DocumentGenerationProcess
+{
+ [Fact]
+ public void StartDocumentGenerationOnly()
+ {
+ var processBuilder = DocumentGenerationProcess.CreateProcessBuilder();
+
+ var t = "";
+ }
+}
diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FishAndChipsProcess.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FishAndChipsProcess.cs
index 5979fba627b4..1cc899a57ffb 100644
--- a/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FishAndChipsProcess.cs
+++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FishAndChipsProcess.cs
@@ -46,9 +46,11 @@ public static ProcessBuilder CreateProcess(string processName = "FishAndChipsPro
}));
addCondimentsStep
- .OnEvent(AddFishAndChipsCondimentsStep.OutputEvents.CondimentsAdded)
+ .OnEvent(AddFishAndChipsCondimentsStep.StepEvents.CondimentsAdded)
.SendEventTo(new ProcessFunctionTargetBuilder(externalStep));
+ externalStep.OnEvent(ExternalFishAndChipsStep.StepEvents.FishAndChipsReady).EmitAsPublicEvent();
+
return processBuilder;
}
@@ -79,9 +81,11 @@ public static ProcessBuilder CreateProcessWithStatefulSteps(string processName =
}));
addCondimentsStep
- .OnEvent(AddFishAndChipsCondimentsStep.OutputEvents.CondimentsAdded)
+ .OnEvent(AddFishAndChipsCondimentsStep.StepEvents.CondimentsAdded)
.SendEventTo(new ProcessFunctionTargetBuilder(externalStep));
+ externalStep.OnEvent(ExternalFishAndChipsStep.StepEvents.FishAndChipsReady).EmitAsPublicEvent();
+
return processBuilder;
}
@@ -92,9 +96,9 @@ public static class ProcessFunctions
public const string AddCondiments = nameof(AddCondiments);
}
- public static class OutputEvents
+ public static new class StepEvents
{
- public const string CondimentsAdded = nameof(CondimentsAdded);
+ public static readonly KernelProcessEventDescriptor> CondimentsAdded = new(nameof(CondimentsAdded));
}
[KernelFunction(ProcessFunctions.AddCondiments)]
@@ -103,12 +107,17 @@ public async Task AddCondimentsAsync(KernelProcessStepContext context, List> FishAndChipsReady = new(nameof(ProcessEvents.FishAndChipsReady));
+ }
+
public ExternalFishAndChipsStep() : base(ProcessEvents.FishAndChipsReady) { }
}
}
diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FishSandwichProcess.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FishSandwichProcess.cs
index 9f42f1e72cbe..874047c47355 100644
--- a/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FishSandwichProcess.cs
+++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FishSandwichProcess.cs
@@ -43,6 +43,8 @@ public static ProcessBuilder CreateProcess(string processName = "FishSandwichPro
.OnEvent(AddSpecialSauceStep.OutputEvents.SpecialSauceAdded)
.SendEventTo(new ProcessFunctionTargetBuilder(externalStep));
+ externalStep.OnEvent(ProcessEvents.FishSandwichReady).EmitAsPublicEvent();
+
return processBuilder;
}
@@ -99,6 +101,8 @@ public static ProcessBuilder CreateProcessWithStatefulStepsV2(string processName
.OnEvent(AddSpecialSauceStep.OutputEvents.SpecialSauceAdded)
.SendEventTo(new ProcessFunctionTargetBuilder(externalStep));
+ externalStep.OnEvent(ExternalFriedFishStep.StepEvents.FishSandwichReady).EmitAsPublicEvent();
+
return processBuilder;
}
@@ -146,6 +150,10 @@ public async Task SliceFoodAsync(KernelProcessStepContext context, List
private sealed class ExternalFriedFishStep : ExternalStep
{
+ public static new class StepEvents
+ {
+ public static readonly KernelProcessEventDescriptor> FishSandwichReady = new(nameof(ProcessEvents.FishSandwichReady));
+ }
public ExternalFriedFishStep() : base(ProcessEvents.FishSandwichReady) { }
}
}
diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FriedFishProcess.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FriedFishProcess.cs
index 40651be2548b..dcf96363dfd8 100644
--- a/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FriedFishProcess.cs
+++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FriedFishProcess.cs
@@ -17,7 +17,7 @@ public static class ProcessEvents
// When multiple processes use the same final step, the should event marked as public
// so that the step event can be used as the output event of the process too.
// In these samples both fried fish and potato fries end with FryStep success
- public const string FriedFishReady = FryFoodStep.OutputEvents.FriedFoodReady;
+ public const string FriedFishReady = nameof(FryFoodStep.StepEvents.FriedFoodReady);
}
///
@@ -39,15 +39,15 @@ public static ProcessBuilder CreateProcess(string processName = "FriedFishProces
.SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep));
gatherIngredientsStep
- .OnEvent(GatherFriedFishIngredientsStep.OutputEvents.IngredientsGathered)
+ .OnEvent(GatherFriedFishIngredientsStep.StepEvents.IngredientsGathered)
.SendEventTo(new ProcessFunctionTargetBuilder(chopStep, functionName: CutFoodStep.ProcessStepFunctions.ChopFood));
chopStep
- .OnEvent(CutFoodStep.OutputEvents.ChoppingReady)
+ .OnEvent(CutFoodStep.StepEvents.ChoppingReady)
.SendEventTo(new ProcessFunctionTargetBuilder(fryStep));
fryStep
- .OnEvent(FryFoodStep.OutputEvents.FoodRuined)
+ .OnEvent(FryFoodStep.StepEvents.FoodRuined)
.SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep));
return processBuilder;
@@ -75,9 +75,11 @@ public static ProcessBuilder CreateProcessWithStatefulStepsV1(string processName
.SendEventTo(new ProcessFunctionTargetBuilder(fryStep));
fryStep
- .OnEvent(FryFoodStep.OutputEvents.FoodRuined)
+ .OnEvent(FryFoodStep.StepEvents.FoodRuined)
.SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep));
+ fryStep.OnEvent(FryFoodStep.StepEvents.FriedFoodReady).EmitAsPublicEvent();
+
return processBuilder;
}
@@ -121,7 +123,7 @@ public static ProcessBuilder CreateProcessWithStatefulStepsV2(string processName
.SendEventTo(new ProcessFunctionTargetBuilder(chopStep, functionName: CutFoodWithSharpeningStep.ProcessStepFunctions.ChopFood));
fryStep
- .OnEvent(FryFoodStep.OutputEvents.FoodRuined)
+ .OnEvent(FryFoodStep.StepEvents.FoodRuined)
.SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep));
return processBuilder;
diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/PotatoFriesProcess.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/PotatoFriesProcess.cs
index cd41c80167a9..608f6b080a75 100644
--- a/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/PotatoFriesProcess.cs
+++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/PotatoFriesProcess.cs
@@ -17,7 +17,7 @@ public static class ProcessEvents
// When multiple processes use the same final step, the should event marked as public
// so that the step event can be used as the output event of the process too.
// In these samples both fried fish and potato fries end with FryStep success
- public const string PotatoFriesReady = nameof(FryFoodStep.OutputEvents.FriedFoodReady);
+ public const string PotatoFriesReady = nameof(FryFoodStep.StepEvents.FriedFoodReady);
}
///
@@ -39,17 +39,19 @@ public static ProcessBuilder CreateProcess(string processName = "PotatoFriesProc
.SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep));
gatherIngredientsStep
- .OnEvent(GatherPotatoFriesIngredientsStep.OutputEvents.IngredientsGathered)
+ .OnEvent(GatherPotatoFriesIngredientsStep.StepEvents.IngredientsGathered)
.SendEventTo(new ProcessFunctionTargetBuilder(sliceStep, functionName: CutFoodStep.ProcessStepFunctions.SliceFood));
sliceStep
- .OnEvent(CutFoodStep.OutputEvents.SlicingReady)
+ .OnEvent(CutFoodStep.StepEvents.SlicingReady)
.SendEventTo(new ProcessFunctionTargetBuilder(fryStep));
fryStep
- .OnEvent(FryFoodStep.OutputEvents.FoodRuined)
+ .OnEvent(FryFoodStep.StepEvents.FoodRuined)
.SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep));
+ fryStep.OnEvent(FryFoodStep.StepEvents.FriedFoodReady).EmitAsPublicEvent();
+
return processBuilder;
}
@@ -92,7 +94,7 @@ public static ProcessBuilder CreateProcessWithStatefulSteps(string processName =
.SendEventTo(new ProcessFunctionTargetBuilder(sliceStep, functionName: CutFoodWithSharpeningStep.ProcessStepFunctions.SliceFood));
fryStep
- .OnEvent(FryFoodStep.OutputEvents.FoodRuined)
+ .OnEvent(FryFoodStep.StepEvents.FoodRuined)
.SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep));
return processBuilder;
diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/CutFoodStep.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/CutFoodStep.cs
index 37f9aaff8ed6..5d715e4914b3 100644
--- a/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/CutFoodStep.cs
+++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/CutFoodStep.cs
@@ -18,10 +18,10 @@ public static class ProcessStepFunctions
public const string SliceFood = nameof(SliceFood);
}
- public static class OutputEvents
+ public static new class StepEvents
{
- public const string ChoppingReady = nameof(ChoppingReady);
- public const string SlicingReady = nameof(SlicingReady);
+ public static readonly KernelProcessEventDescriptor> ChoppingReady = new(nameof(ChoppingReady));
+ public static readonly KernelProcessEventDescriptor> SlicingReady = new(nameof(SlicingReady));
}
[KernelFunction(ProcessStepFunctions.ChopFood)]
@@ -30,7 +30,7 @@ public async Task ChopFoodAsync(KernelProcessStepContext context, List f
var foodToBeCut = foodActions.First();
foodActions.Add(this.getActionString(foodToBeCut, "chopped"));
Console.WriteLine($"CUTTING_STEP: Ingredient {foodToBeCut} has been chopped!");
- await context.EmitEventAsync(new() { Id = OutputEvents.ChoppingReady, Data = foodActions });
+ await context.EmitEventAsync(StepEvents.ChoppingReady, foodActions);
}
[KernelFunction(ProcessStepFunctions.SliceFood)]
@@ -39,7 +39,7 @@ public async Task SliceFoodAsync(KernelProcessStepContext context, List
var foodToBeCut = foodActions.First();
foodActions.Add(this.getActionString(foodToBeCut, "sliced"));
Console.WriteLine($"CUTTING_STEP: Ingredient {foodToBeCut} has been sliced!");
- await context.EmitEventAsync(new() { Id = OutputEvents.SlicingReady, Data = foodActions });
+ await context.EmitEventAsync(StepEvents.SlicingReady, foodActions);
}
private string getActionString(string food, string action)
diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/ExternalStep.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/ExternalStep.cs
index b97c43138c7a..64d4f6bb705d 100644
--- a/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/ExternalStep.cs
+++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/ExternalStep.cs
@@ -15,6 +15,6 @@ public class ExternalStep(string externalEventName) : KernelProcessStep
[KernelFunction]
public async Task EmitExternalEventAsync(KernelProcessStepContext context, object data)
{
- await context.EmitEventAsync(new() { Id = this._externalEventName, Data = data, Visibility = KernelProcessEventVisibility.Public });
+ await context.EmitEventAsync(this._externalEventName, data);
}
}
diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/FryFoodStep.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/FryFoodStep.cs
index bfc2dd50f5ae..bef9636251ef 100644
--- a/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/FryFoodStep.cs
+++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/FryFoodStep.cs
@@ -17,10 +17,10 @@ public static class ProcessStepFunctions
public const string FryFood = nameof(FryFood);
}
- public static class OutputEvents
+ public static new class StepEvents
{
- public const string FoodRuined = nameof(FoodRuined);
- public const string FriedFoodReady = nameof(FriedFoodReady);
+ public static readonly KernelProcessEventDescriptor> FoodRuined = new(nameof(FoodRuined));
+ public static readonly KernelProcessEventDescriptor> FriedFoodReady = new(nameof(FriedFoodReady));
}
private readonly Random _randomSeed = new();
@@ -38,12 +38,12 @@ public async Task FryFoodAsync(KernelProcessStepContext context, List fo
// Oh no! Food got burnt :(
foodActions.Add($"{foodToFry}_frying_failed");
Console.WriteLine($"FRYING_STEP: Ingredient {foodToFry} got burnt while frying :(");
- await context.EmitEventAsync(new() { Id = OutputEvents.FoodRuined, Data = foodActions });
+ await context.EmitEventAsync(StepEvents.FoodRuined, foodActions);
return;
}
foodActions.Add($"{foodToFry}_frying_succeeded");
Console.WriteLine($"FRYING_STEP: Ingredient {foodToFry} is ready!");
- await context.EmitEventAsync(new() { Id = OutputEvents.FriedFoodReady, Data = foodActions, Visibility = KernelProcessEventVisibility.Public });
+ await context.EmitEventAsync(StepEvents.FriedFoodReady, foodActions);
}
}
diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/GatherIngredientsStep.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/GatherIngredientsStep.cs
index 15d253f022b7..d18133ed69de 100644
--- a/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/GatherIngredientsStep.cs
+++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/GatherIngredientsStep.cs
@@ -14,9 +14,9 @@ public static class ProcessStepFunctions
public const string GatherIngredients = nameof(GatherIngredients);
}
- public static class OutputEvents
+ public static new class StepEvents
{
- public const string IngredientsGathered = nameof(IngredientsGathered);
+ public static readonly KernelProcessEventDescriptor> IngredientsGathered = new(nameof(IngredientsGathered));
}
private readonly FoodIngredients _ingredient;
@@ -45,7 +45,7 @@ public virtual async Task GatherIngredientsAsync(KernelProcessStepContext contex
updatedFoodActions.Add($"{ingredient}_gathered");
Console.WriteLine($"GATHER_INGREDIENT: Gathered ingredient {ingredient}");
- await context.EmitEventAsync(new() { Id = OutputEvents.IngredientsGathered, Data = updatedFoodActions });
+ await context.EmitEventAsync(StepEvents.IngredientsGathered, updatedFoodActions);
}
}
diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventDescriptor.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventDescriptor.cs
new file mode 100644
index 000000000000..cba0a190fbf7
--- /dev/null
+++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessEventDescriptor.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+
+namespace Microsoft.SemanticKernel;
+
+///
+/// Represents a descriptor for a kernel process event, including its name and associated type.
+/// It allows ensuring that events are uniquely defined before runtime and also determines the type of data that the event carries.
+///
+/// This class provides metadata about a kernel process event, including its name and the type of data it
+/// carries. It is commonly used to define and identify events in a kernel process event system.
+/// The type of the event data associated with the kernel process event data.
+public class KernelProcessEventDescriptor
+{
+ ///
+ /// Name of the event emitted by the Process Step.
+ ///
+ public string EventName { get; }
+ ///
+ /// Type of the event data associated with the event.
+ ///
+ public Type EventType { get; }
+
+ ///
+ /// Constructor for the KernelProcessEventDescriptor class.
+ ///
+ /// The name of the event emitted by the Process Step.
+ /// Thrown when the event name is null or empty.
+ public KernelProcessEventDescriptor(string eventName)
+ {
+ if (string.IsNullOrWhiteSpace(eventName))
+ {
+ throw new ArgumentException("Event name cannot be null or empty.", nameof(eventName));
+ }
+
+ this.EventName = eventName;
+ this.EventType = typeof(T);
+ }
+}
diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessOptions.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessOptions.cs
new file mode 100644
index 000000000000..6818179f5c49
--- /dev/null
+++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessOptions.cs
@@ -0,0 +1,13 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.SemanticKernel;
+
+///
+/// Options for configuring the Kernel process
+///
+public record KernelProcessOptions
+{
+ public JsonSerializerContext[] JsonSerializerAdditionalContexts { get; init; } = [];
+}
diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessStep.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessStep.cs
index 81d434c92c84..83f45e685391 100644
--- a/dotnet/src/Experimental/Process.Abstractions/KernelProcessStep.cs
+++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessStep.cs
@@ -9,6 +9,21 @@ namespace Microsoft.SemanticKernel;
///
public class KernelProcessStep
{
+ ///
+ /// Class containing events related to the step. This class is used to define events that can be raised during the execution of a step in a process.
+ /// This class should contain only public static readonly components.
+ /// When using in a custom step, it should be overwritten with:
+ ///
+ /// public static new class StepEvents
+ /// {
+ /// ...custom step events described with KernelProcessEventDescriptor...
+ /// }
+ ///
+ ///
+ public static class StepEvents
+ {
+ }
+
///
/// Name of the step given by the StepBuilder id.
///
diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepContext.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepContext.cs
index e652e0adb367..1de1365951ce 100644
--- a/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepContext.cs
+++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepContext.cs
@@ -1,5 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.Threading.Tasks;
namespace Microsoft.SemanticKernel;
@@ -10,14 +12,17 @@ namespace Microsoft.SemanticKernel;
public sealed class KernelProcessStepContext
{
private readonly IKernelProcessMessageChannel _stepMessageChannel;
+ private readonly IReadOnlyDictionary _outputEventsData;
///
/// Initializes a new instance of the class.
///
/// An instance of .
- public KernelProcessStepContext(IKernelProcessMessageChannel channel)
+ /// event data of output events emitted by step
+ public KernelProcessStepContext(IKernelProcessMessageChannel channel, IReadOnlyDictionary? outputEventsData)
{
this._stepMessageChannel = channel;
+ this._outputEventsData = outputEventsData ?? new ReadOnlyDictionary(new Dictionary());
}
///
@@ -44,12 +49,43 @@ public ValueTask EmitEventAsync(
{
Verify.NotNullOrWhiteSpace(eventId, nameof(eventId));
+ var eventVisibility = KernelProcessEventVisibility.Internal;
+ if (this._outputEventsData.TryGetValue(eventId, out var eventData))
+ {
+ if (eventData.IsPublic)
+ {
+ eventVisibility = KernelProcessEventVisibility.Public;
+ }
+ }
+ else
+ {
+ // TODO: Log a warning that the event is not registered in the step metadata -> event mismatch between step implementation and step builder event usage
+ var t = "Event with ID '" + eventId + "' is not registered in the step metadata.";
+ }
+
return this._stepMessageChannel.EmitEventAsync(
new KernelProcessEvent
{
Id = eventId,
Data = data,
- Visibility = visibility
+ Visibility = eventVisibility,
});
}
+
+ public ValueTask EmitEventAsync(KernelProcessEventDescriptor eventIdData, object? data = null)
+ {
+ Verify.NotNull(eventIdData, nameof(eventIdData));
+ var eventVisibility = KernelProcessEventVisibility.Internal;
+ if (this._outputEventsData.TryGetValue(eventIdData.EventName, out var eventData))
+ {
+ if (eventData.IsPublic)
+ {
+ eventVisibility = KernelProcessEventVisibility.Public;
+ }
+ }
+
+ // TODO: Runtime check/warning can be added were to validate that the data type matches the expected type of the event.
+
+ return this._stepMessageChannel.EmitEventAsync(new KernelProcessEvent { Id = eventIdData.EventName, Data = data, Visibility = eventVisibility });
+ }
}
diff --git a/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepInfo.cs b/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepInfo.cs
index a1f5d0cef358..443edcba02e5 100644
--- a/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepInfo.cs
+++ b/dotnet/src/Experimental/Process.Abstractions/KernelProcessStepInfo.cs
@@ -46,6 +46,11 @@ public KernelProcessStepState State
///
public IReadOnlyDictionary? IncomingEdgeGroups { get; }
+ ///
+ /// A dictionary of output events type data for the step.
+ ///
+ public IReadOnlyDictionary? OutputEventsData { get; init; }
+
///
/// Initializes a new instance of the class.
///
diff --git a/dotnet/src/Experimental/Process.Abstractions/Models/KernelEventTypeData.cs b/dotnet/src/Experimental/Process.Abstractions/Models/KernelEventTypeData.cs
new file mode 100644
index 000000000000..2b4d2479b170
--- /dev/null
+++ b/dotnet/src/Experimental/Process.Abstractions/Models/KernelEventTypeData.cs
@@ -0,0 +1,88 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Text.Json;
+
+namespace Microsoft.SemanticKernel.Process.Models;
+
+///
+/// Properties to describe a serializable object as input or output event of a SK Process
+///
+public record KernelEventTypeData
+{
+ ///
+ /// Dotnet type of the object
+ ///
+ public Type? DataType { get; init; } = null;
+
+ ///
+ /// Gets or sets the short description of the data type.
+ ///
+ public string? Description { get; set; }
+
+ ///
+ /// Gets or sets whether the event data is considered required (rather than optional).
+ ///
+ ///
+ /// The default is false.
+ ///
+ public bool Required { get; set; } = false;
+
+ ///
+ /// Gets or sets JSON Schema describing this data type.
+ ///
+ public string? JsonSchema { get; set; }
+}
+
+///
+/// Methods to create from Semantic Kernel function specific objects
+///
+public static class KernelEventTypeDataExtensions
+{
+ ///
+ /// Converts the specified instance to a
+ /// object.
+ ///
+ /// The metadata describing the kernel parameter to be converted. Cannot be .
+ /// A object containing the schema, data type, description, and required status
+ /// derived from the specified .
+ public static KernelEventTypeData ToKernelEventTypeData(this KernelParameterMetadata kernelParameterMetadata)
+ {
+ Verify.NotNull(kernelParameterMetadata);
+ return new KernelEventTypeData
+ {
+ JsonSchema = kernelParameterMetadata.Schema?.ToString(),
+ DataType = kernelParameterMetadata.ParameterType,
+ Description = kernelParameterMetadata.Description,
+ Required = kernelParameterMetadata.IsRequired,
+ };
+ }
+
+ ///
+ /// Converts the specified instance to a object.
+ ///
+ /// The instance containing metadata about the kernel parameter.
+ /// A object populated with the corresponding data from the provided .
+ public static KernelEventTypeData ToKernelEventTypeData(this KernelReturnParameterMetadata kernelParameterMetadata)
+ {
+ Verify.NotNull(kernelParameterMetadata);
+ return new KernelEventTypeData
+ {
+ JsonSchema = kernelParameterMetadata.Schema?.ToString(),
+ DataType = kernelParameterMetadata.ParameterType,
+ Description = kernelParameterMetadata.Description,
+ Required = false,
+ };
+ }
+
+ public static KernelEventTypeData FromObjectType(Type objectType, JsonSerializerOptions jsonSerializerOptions)
+ {
+ Verify.NotNull(objectType, nameof(objectType));
+ // TODO: this is the centralized SK way to generate json schemas
+ var returnType = KernelReturnParameterMetadataFactory.CreateFromType(objectType, jsonSerializerOptions);
+
+ return returnType.ToKernelEventTypeData();
+ }
+}
diff --git a/dotnet/src/Experimental/Process.Abstractions/ProcessStepEventData.cs b/dotnet/src/Experimental/Process.Abstractions/ProcessStepEventData.cs
new file mode 100644
index 000000000000..520536467dec
--- /dev/null
+++ b/dotnet/src/Experimental/Process.Abstractions/ProcessStepEventData.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.SemanticKernel.Process.Models;
+
+namespace Microsoft.SemanticKernel;
+
+///
+/// Represents data associated with a process step event.
+///
+public record ProcessStepEventData
+{
+ ///
+ /// Gets the unique identifier for the event. Recommended to be human readable and unique within the process.
+ ///
+ public string EventId { get; init; } = string.Empty;
+
+ ///
+ /// Determines whether the event is public outside the step parent process or not.
+ ///
+ public bool IsPublic { get; set; } = false;
+
+ ///
+ /// Gets the event type data associated with the process event.
+ ///
+ public KernelEventTypeData? EventTypeData { get; init; } = null;
+
+ ///
+ /// Initializes a new instance of the class with the specified event ID and
+ /// optional event type data.
+ ///
+ /// The unique identifier for the event. This value cannot be null or empty.
+ /// Optional data describing the type of the event. If not provided, the event type data will be null.
+ public ProcessStepEventData(string eventId, KernelEventTypeData? eventTypeData = null)
+ {
+ this.EventId = eventId;
+ this.EventTypeData = eventTypeData;
+ }
+}
diff --git a/dotnet/src/Experimental/Process.Abstractions/Serialization/KernelProcessAbstractionsJsonContext.cs b/dotnet/src/Experimental/Process.Abstractions/Serialization/KernelProcessAbstractionsJsonContext.cs
new file mode 100644
index 000000000000..5e9c637c25b2
--- /dev/null
+++ b/dotnet/src/Experimental/Process.Abstractions/Serialization/KernelProcessAbstractionsJsonContext.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.SemanticKernel.Process.Serialization;
+
+// Internal Process Framework objects used in Process Framework should be added to the serialization context
+[JsonSerializable(typeof(KernelProcessStepContext))]
+[JsonSerializable(typeof(KernelProcessStepExternalContext))]
+[JsonSerializable(typeof(KernelProcessProxyMessage))]
+internal partial class KernelProcessAbstractionsJsonContext : JsonSerializerContext
+{
+}
+
+///
+/// Extensions for Semantic Kernel process abstractions JSON serialization.
+///
+public static class KernelProcessJsonSerializationExtensions
+{
+ ///
+ /// Method that allows adding additional serialization contexts to the default options used within Semantic Kernel for process abstractions.
+ ///
+ /// List of additional serializations contexts to be added
+ ///
+ public static JsonSerializerOptions GetKernelProcessCustomJsonSerializationOptions(JsonSerializerContext[] additionalContexts)
+ {
+ List predefinedContexts = new() { KernelProcessAbstractionsJsonContext.Default };
+ if (additionalContexts != null)
+ {
+ predefinedContexts.AddRange(additionalContexts);
+ }
+
+ var jsonOptions = KernelJsonSerializationOptions.GetKernelCustomJsonSerializationOptions(predefinedContexts.ToArray());
+
+ return jsonOptions;
+ }
+}
diff --git a/dotnet/src/Experimental/Process.Core/ListenForBuilder.cs b/dotnet/src/Experimental/Process.Core/ListenForBuilder.cs
index f8cd2aa8c319..8bee056898aa 100644
--- a/dotnet/src/Experimental/Process.Core/ListenForBuilder.cs
+++ b/dotnet/src/Experimental/Process.Core/ListenForBuilder.cs
@@ -5,6 +5,7 @@
using System.Linq;
using System.Security.Cryptography;
using System.Text;
+using Microsoft.SemanticKernel.Process.Models;
namespace Microsoft.SemanticKernel;
@@ -76,6 +77,29 @@ public ListenForTargetBuilder AllOf(List messageSources)
{
Verify.NotNullOrEmpty(messageSources, nameof(messageSources));
+ // verify mapped message sources output events do exist
+ foreach (var source in messageSources)
+ {
+ var messageSourceType = source.GetType();
+ Type? eventTypeData = null;
+ if (messageSourceType.IsGenericType && messageSourceType.GetGenericTypeDefinition() == typeof(TypedMessageSourceBuilder<>))
+ {
+ eventTypeData = messageSourceType.GenericTypeArguments.FirstOrDefault();
+ }
+
+ if (source.Source is ProcessBuilder sourceProcessBuilder)
+ {
+ sourceProcessBuilder.AddInputEventToProcess(source.MessageType, eventTypeData);
+ }
+ else
+ {
+ if (!source.Source.OutputStepEvents.ContainsKey(source.MessageType))
+ {
+ throw new InvalidOperationException($"Output Event {source.MessageType} is not emitted by {source.Source.StepId}");
+ }
+ }
+ }
+
var edgeGroup = new KernelProcessEdgeGroupBuilder(this.GetGroupId(messageSources), messageSources);
this._targetBuilder = new ListenForTargetBuilder(messageSources, this._processBuilder, edgeGroup: edgeGroup);
return this._targetBuilder;
diff --git a/dotnet/src/Experimental/Process.Core/MessageSourceBuilder.cs b/dotnet/src/Experimental/Process.Core/MessageSourceBuilder.cs
index 91723bc542bf..5215c6f632a8 100644
--- a/dotnet/src/Experimental/Process.Core/MessageSourceBuilder.cs
+++ b/dotnet/src/Experimental/Process.Core/MessageSourceBuilder.cs
@@ -7,7 +7,7 @@ namespace Microsoft.SemanticKernel;
///
/// Represents a builder for defining the source of a message in a process.
///
-public sealed class MessageSourceBuilder
+public class MessageSourceBuilder
{
///
/// Initializes a new instance of the class.
@@ -37,3 +37,19 @@ public MessageSourceBuilder(string messageType, ProcessStepBuilder source, Kerne
///
public KernelProcessEdgeCondition Condition { get; }
}
+
+public sealed class TypedMessageSourceBuilder : MessageSourceBuilder
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The source step builder
+ /// Condition that must be met for the message to be processed
+ public TypedMessageSourceBuilder(KernelProcessEventDescriptor messageType, ProcessStepBuilder source, KernelProcessEdgeCondition? condition = null)
+ : base(messageType.EventName, source, condition)
+ {
+ this.MessageDescriptor = messageType;
+ }
+
+ public KernelProcessEventDescriptor MessageDescriptor { get; }
+}
diff --git a/dotnet/src/Experimental/Process.Core/ProcessBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessBuilder.cs
index 2b72d156ac4c..4588f6b428db 100644
--- a/dotnet/src/Experimental/Process.Core/ProcessBuilder.cs
+++ b/dotnet/src/Experimental/Process.Core/ProcessBuilder.cs
@@ -2,12 +2,15 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.AzureAI;
using Microsoft.SemanticKernel.Process;
using Microsoft.SemanticKernel.Process.Internal;
+using Microsoft.SemanticKernel.Process.Models;
+using Microsoft.SemanticKernel.Process.Serialization;
using Microsoft.SemanticKernel.Process.Tools;
namespace Microsoft.SemanticKernel;
@@ -36,6 +39,8 @@ public sealed partial class ProcessBuilder : ProcessStepBuilder
///
internal bool HasParentProcess { get; set; }
+ internal Dictionary InputProcessEvents { get; init; } = [];
+
///
/// Version of the process, used when saving the state of the process
///
@@ -51,6 +56,8 @@ public sealed partial class ProcessBuilder : ProcessStepBuilder
///
public string Description { get; init; } = string.Empty;
+ public KernelProcessOptions Options { get; init; } = new KernelProcessOptions();
+
///
/// Initializes a new instance of the class.
///
@@ -58,12 +65,19 @@ public sealed partial class ProcessBuilder : ProcessStepBuilder
/// The semantic description of the Process being built.
/// ProcessBuilder to copy from
/// The type of the state. This is optional.
- public ProcessBuilder(string id, string? description = null, ProcessBuilder? processBuilder = null, Type? stateType = null)
+ ///
+ public ProcessBuilder(string id, string? description = null, ProcessBuilder? processBuilder = null, Type? stateType = null, KernelProcessOptions? processOptions = null)
: base(id, processBuilder)
{
Verify.NotNullOrWhiteSpace(id, nameof(id));
this.StateType = stateType;
this.Description = description ?? string.Empty;
+ this.Options = processOptions ?? new KernelProcessOptions();
+
+ if (this.JsonSerializerOptions == null)
+ {
+ this.JsonSerializerOptions = KernelProcessJsonSerializationExtensions.GetKernelProcessCustomJsonSerializationOptions(processOptions?.JsonSerializerAdditionalContexts ?? []);
+ }
}
///
@@ -125,6 +139,29 @@ internal override Dictionary GetFunctionMetadata
.ToDictionary(pair => pair.Key, pair => pair.Value);
}
+ internal void AddInputEventToProcess(string eventName, Type? inputType)
+ {
+ var processStepEventData = inputType != null ? KernelEventTypeDataExtensions.FromObjectType(inputType, this.JsonSerializerOptions!) : new();
+ this.AddInputEventToProcess(eventName, processStepEventData);
+ }
+
+ internal void AddInputEventToProcess(string eventName, KernelEventTypeData inputTypeData)
+ {
+ // If the event is already registered, we can skip adding it.
+ if (this.InputProcessEvents.ContainsKey(eventName))
+ {
+ // TODO: verify that the event type data is the same as the one already registered.
+ return;
+ }
+ // Add the input event to the process.
+ this.InputProcessEvents.Add(eventName, inputTypeData);
+ }
+
+ internal void AddOutputEventToProcess(ProcessStepEventData outputEvent)
+ {
+ this.OutputStepEvents.Add(outputEvent.EventId, outputEvent);
+ }
+
///
/// Builds the step.
///
@@ -191,7 +228,7 @@ private TBuilder AddStep(TBuilder builder, IReadOnlyList? alia
/// An instance of
public ProcessStepBuilder AddStepFromType(string? id = null, IReadOnlyList? aliases = null) where TStep : KernelProcessStep
{
- ProcessStepBuilder stepBuilder = new(id: id ?? typeof(TStep).Name, this.ProcessBuilder);
+ ProcessStepBuilder stepBuilder = new(id: id ?? typeof(TStep).Name, this, jsonSerializerOptions: this.JsonSerializerOptions);
return this.AddStep(stepBuilder, aliases);
}
@@ -205,7 +242,7 @@ public ProcessStepBuilder AddStepFromType(string? id = null, IReadOnlyLis
/// An instance of
public ProcessStepBuilder AddStepFromType(Type stepType, string? id = null, IReadOnlyList? aliases = null)
{
- ProcessStepBuilderTyped stepBuilder = new(stepType: stepType, id: id ?? stepType.Name, this.ProcessBuilder);
+ ProcessStepBuilderTyped stepBuilder = new(stepType: stepType, id: id ?? stepType.Name, this, jsonSerializerOptions: this.JsonSerializerOptions);
return this.AddStep(stepBuilder, aliases);
}
@@ -221,7 +258,7 @@ public ProcessStepBuilder AddStepFromType(Type stepType, string? id = null, IRea
/// An instance of
public ProcessStepBuilder AddStepFromType(TState initialState, string? id = null, IReadOnlyList? aliases = null) where TStep : KernelProcessStep where TState : class, new()
{
- ProcessStepBuilder stepBuilder = new(id ?? typeof(TStep).Name, this.ProcessBuilder, initialState: initialState);
+ ProcessStepBuilder stepBuilder = new(id ?? typeof(TStep).Name, this, initialState: initialState) { JsonSerializerOptions = this.JsonSerializerOptions };
return this.AddStep(stepBuilder, aliases);
}
@@ -250,7 +287,7 @@ public ProcessStepBuilder AddStepFromType(Type stepType, string? id = null, IRea
threadName = agentDefinition.Name;
}
- var stepBuilder = new ProcessAgentBuilder(agentDefinition, threadName: threadName, [], this.ProcessBuilder, id) { HumanInLoopMode = humanInLoopMode }; // TODO: Add inputs to the agent
+ var stepBuilder = new ProcessAgentBuilder(agentDefinition, threadName: threadName, [], this, id) { HumanInLoopMode = humanInLoopMode, JsonSerializerOptions = this.JsonSerializerOptions }; // TODO: Add inputs to the agent
return this.AddStep(stepBuilder, aliases);
}
@@ -278,7 +315,7 @@ public ProcessAgentBuilder AddStepFromAgent(AgentDefinition agentDefinition, str
threadName = agentDefinition.Name;
}
- var stepBuilder = new ProcessAgentBuilder(agentDefinition, threadName: threadName, [], this.ProcessBuilder, id) { HumanInLoopMode = humanInLoopMode };
+ var stepBuilder = new ProcessAgentBuilder(agentDefinition, threadName: threadName, [], this, id) { HumanInLoopMode = humanInLoopMode, JsonSerializerOptions = this.JsonSerializerOptions };
return this.AddStep(stepBuilder, aliases);
}
@@ -320,7 +357,7 @@ public ProcessAgentBuilder AddStepFromAgent(AgentDefinition agentDefinition, str
return Task.FromResult(result);
});
- var stepBuilder = new ProcessAgentBuilder(agentDefinition, threadName: threadName, [], this.ProcessBuilder, stepId) { AgentIdResolver = agentIdResolver, HumanInLoopMode = humanInLoopMode }; // TODO: Add inputs to the agent
+ var stepBuilder = new ProcessAgentBuilder(agentDefinition, threadName: threadName, [], this, stepId) { AgentIdResolver = agentIdResolver, HumanInLoopMode = humanInLoopMode, JsonSerializerOptions = this.JsonSerializerOptions }; // TODO: Add inputs to the agent
return this.AddStep(stepBuilder, aliases);
}
@@ -342,7 +379,9 @@ public ProcessStepBuilder AddEndStep()
/// An instance of
public ProcessBuilder AddStepFromProcess(ProcessBuilder kernelProcess, IReadOnlyList? aliases = null)
{
+ kernelProcess.ProcessBuilder = this;
kernelProcess.HasParentProcess = true;
+ kernelProcess.JsonSerializerOptions = this.JsonSerializerOptions;
return this.AddStep(kernelProcess, aliases);
}
@@ -356,9 +395,9 @@ public ProcessBuilder AddStepFromProcess(ProcessBuilder kernelProcess, IReadOnly
/// An instance of
public ProcessMapBuilder AddMapStepFromType(string? id = null, IReadOnlyList? aliases = null) where TStep : KernelProcessStep
{
- ProcessStepBuilder stepBuilder = new(id ?? typeof(TStep).Name, this.ProcessBuilder);
+ ProcessStepBuilder stepBuilder = new(id ?? typeof(TStep).Name, this.ProcessBuilder) { JsonSerializerOptions = this.JsonSerializerOptions };
- ProcessMapBuilder mapBuilder = new(stepBuilder);
+ ProcessMapBuilder mapBuilder = new(stepBuilder) { JsonSerializerOptions = this.JsonSerializerOptions };
return this.AddStep(mapBuilder, aliases);
}
@@ -374,9 +413,9 @@ public ProcessMapBuilder AddMapStepFromType(string? id = null, IReadOnlyL
/// An instance of
public ProcessMapBuilder AddMapStepFromType(TState initialState, string id, IReadOnlyList? aliases = null) where TStep : KernelProcessStep where TState : class, new()
{
- ProcessStepBuilder stepBuilder = new(id, this.ProcessBuilder, initialState: initialState);
+ ProcessStepBuilder stepBuilder = new(id, this.ProcessBuilder, initialState: initialState) { JsonSerializerOptions = this.JsonSerializerOptions };
- ProcessMapBuilder mapBuilder = new(stepBuilder);
+ ProcessMapBuilder mapBuilder = new(stepBuilder) { JsonSerializerOptions = this.JsonSerializerOptions };
return this.AddStep(mapBuilder, aliases);
}
@@ -393,7 +432,7 @@ public ProcessMapBuilder AddMapStepFromProcess(ProcessBuilder process, IReadOnly
{
process.HasParentProcess = true;
- ProcessMapBuilder mapBuilder = new(process);
+ ProcessMapBuilder mapBuilder = new(process) { JsonSerializerOptions = this.JsonSerializerOptions };
return this.AddStep(mapBuilder, aliases);
}
@@ -461,6 +500,25 @@ public ProcessEdgeBuilder OnInputEvent(string eventId)
return new ProcessEdgeBuilder(this, eventId);
}
+
+ public ProcessEdgeBuilder OnInputEvent(KernelProcessEventDescriptor eventDescriptor)
+ {
+ this.AddInputEventToProcess(eventDescriptor.EventName, eventDescriptor.EventType);
+
+ return this.OnInputEvent(eventDescriptor.EventName);
+ }
+
+ ///
+ public override ProcessStepEdgeBuilder OnEvent(string eventId, bool isPublic = false)
+ {
+ if (!this.OutputStepEvents.ContainsKey(eventId))
+ {
+ throw new InvalidOperationException($"Output Event {eventId} is not emitted publicly by {this.StepId}");
+ }
+
+ return base.OnEvent(eventId, isPublic);
+ }
+
///
/// Provides an instance of for defining an edge to a
/// step that responds to an unhandled process error.
diff --git a/dotnet/src/Experimental/Process.Core/ProcessEdgeBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessEdgeBuilder.cs
index f1f61469332d..8d725c608037 100644
--- a/dotnet/src/Experimental/Process.Core/ProcessEdgeBuilder.cs
+++ b/dotnet/src/Experimental/Process.Core/ProcessEdgeBuilder.cs
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
+using System.Linq;
+using Microsoft.SemanticKernel.Process.Models;
namespace Microsoft.SemanticKernel;
@@ -29,9 +31,51 @@ internal ProcessEdgeBuilder(ProcessBuilder source, string eventId) : base(source
///
public ProcessEdgeBuilder SendEventTo(ProcessFunctionTargetBuilder target)
{
+ if (!target.Step.InputParametersTypeData.TryGetValue(target.FunctionName, out var targetFunctionParameters))
+ {
+ throw new InvalidOperationException($"Target function {target.FunctionName} not found");
+ }
+
+ KernelEventTypeData targetParameterData = new();
+ if (target.ParameterName != null)
+ {
+ if (!targetFunctionParameters.TryGetValue(target.ParameterName, out var parameterData))
+ {
+ throw new InvalidOperationException($"Target function {target.FunctionName} has no parameter named {target.ParameterName}");
+ }
+
+ targetParameterData = parameterData;
+ }
+ this.Source.AddInputEventToProcess(this.EventData.EventId, targetParameterData);
+
return this.SendEventTo_Int(target as ProcessTargetBuilder);
}
+ public ProcessStepEdgeBuilder SendEventTo(ProcessFunctionTargetBuilder target) where TParameterDataType : class, new()
+ {
+ if (!target.Step.InputParametersTypeData.TryGetValue(target.FunctionName, out var targetFunctionInput))
+ {
+ throw new InvalidOperationException($"Step {target.Step.StepId} does not have input parameter schemas for function {target.FunctionName}");
+ }
+
+ var matchingParameterTypeValues = targetFunctionInput.Where(p => p.Value.DataType == typeof(TParameterDataType)).ToList();
+ if (matchingParameterTypeValues.Count == 0)
+ {
+ throw new InvalidOperationException($"No matching parameters of type {typeof(TParameterDataType).Name} found for function {target.FunctionName} in step {target.Step.StepId}.");
+ }
+
+ KernelEventTypeData targetParameterData = matchingParameterTypeValues.FirstOrDefault().Value;
+
+ if (targetParameterData == null)
+ {
+ throw new InvalidOperationException("No matching parameters found.");
+ }
+
+ this.Source.AddInputEventToProcess(this.EventData.EventId, targetParameterData);
+
+ return this.SendEventTo(target);
+ }
+
///
/// Sends the output of the source step to the specified target when the associated event fires.
///
diff --git a/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs b/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs
index 85096dbe4464..d2388ab8f9b9 100644
--- a/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs
+++ b/dotnet/src/Experimental/Process.Core/ProcessStepBuilder.cs
@@ -3,8 +3,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Reflection;
+using System.Text.Json;
+using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Process;
using Microsoft.SemanticKernel.Process.Internal;
+using Microsoft.SemanticKernel.Process.Models;
namespace Microsoft.SemanticKernel;
@@ -35,20 +39,30 @@ public abstract class ProcessStepBuilder
/// Define the behavior of the step when the event with the specified Id is fired.
///
/// The Id of the event of interest.
+ /// Determins if the event is accessible outside of the parent process
/// An instance of .
- public ProcessStepEdgeBuilder OnEvent(string eventId)
+ public virtual ProcessStepEdgeBuilder OnEvent(string eventId, bool isPublic = false)
{
+ if (isPublic)
+ {
+ this.MarkPublicEvent(eventId);
+ }
// scope the event to this instance of this step
var scopedEventId = this.GetScopedEventId(eventId);
return new ProcessStepEdgeBuilder(this, scopedEventId, eventId);
}
- ///
- /// Returns the event Id that is used to identify the result of a function.
- ///
- /// Optional: name of the step function the result is expected from
- ///
- public string GetFunctionResultEventId(string? functionName = null)
+ public virtual ProcessStepEdgeBuilder OnEvent(KernelProcessEventDescriptor eventDescriptor, bool isPublic = false)
+ {
+ return this.OnEvent(eventDescriptor.EventName, isPublic);
+ }
+
+ ///
+ /// Returns the event Id that is used to identify the result of a function.
+ ///
+ /// Optional: name of the step function the result is expected from
+ ///
+ public string GetFunctionResultEventId(string? functionName = null)
{
// TODO: Add a check to see if the function name is valid if provided
if (string.IsNullOrWhiteSpace(functionName))
@@ -75,6 +89,11 @@ public string GetFullEventId(string? eventName = null, string? functionName = nu
return $"{this.StepId}.{eventName}";
}
+ public string GetFullEventId(KernelProcessEventDescriptor eventDescriptor, string? functionName = null)
+ {
+ return this.GetFullEventId(eventDescriptor.EventName, functionName);
+ }
+
///
/// Define the behavior of the step when the specified function has been successfully invoked.
///
@@ -107,11 +126,16 @@ public ProcessStepEdgeBuilder OnFunctionError(string? functionName = null)
/// The namespace for events that are scoped to this step.
private readonly string _eventNamespace;
+ internal JsonSerializerOptions? JsonSerializerOptions { get; set; } = null;
+
///
/// A mapping of function names to the functions themselves.
///
internal Dictionary FunctionsDict { get; set; }
+ internal Dictionary> InputParametersTypeData { get; init; } = [];
+ internal Dictionary OutputStepEvents { get; init; } = [];
+
///
/// A mapping of event Ids to the edges that are triggered by those events.
///
@@ -120,7 +144,7 @@ public ProcessStepEdgeBuilder OnFunctionError(string? functionName = null)
///
/// The process builder that this step is a part of. This may be null if the step is itself a process.
///
- internal ProcessBuilder? ProcessBuilder { get; }
+ internal ProcessBuilder? ProcessBuilder { get; set; }
///
/// Builds the step with step state
@@ -163,6 +187,21 @@ private string ResolveFunctionName()
return this.FunctionsDict.Keys.First();
}
+ internal void MarkPublicEvent(string eventId)
+ {
+ if (this.OutputStepEvents.TryGetValue(eventId, out var stepEventData) && stepEventData != null)
+ {
+ stepEventData.IsPublic = true;
+
+ // Update parent process as well since this step event is an output edge of the process
+ this.ProcessBuilder?.AddOutputEventToProcess(stepEventData with { IsPublic = false });
+
+ return;
+ }
+
+ throw new KernelException($"The event {eventId} does not exist on step {this.StepId}.");
+ }
+
///
/// Links the output of the current step to the an input of another step via the specified event type.
///
@@ -181,8 +220,11 @@ internal virtual void LinkTo(string eventId, ProcessStepEdgeBuilder edgeBuilder)
internal static bool FilterSupportedParameterTypes(Type? parameterType, bool hasDefaultValue = false)
{
- if (parameterType != typeof(KernelProcessStepContext) &&
- parameterType != typeof(KernelProcessStepExternalContext))
+ // Should match parameters piped in in StepExtensions.FindInputChannels
+ if (parameterType != typeof(Kernel) &&
+ parameterType != typeof(KernelProcessStepContext) &&
+ parameterType != typeof(KernelProcessStepExternalContext) &&
+ parameterType != typeof(AgentDefinition))
{
return !hasDefaultValue;
}
@@ -308,14 +350,173 @@ public class ProcessStepBuilderTyped : ProcessStepBuilder
/// The unique id of the step.
/// The process builder that this step is a part of.
/// Initial state of the step to be used on the step building stage
- internal ProcessStepBuilderTyped(Type stepType, string id, ProcessBuilder? processBuilder, object? initialState = default)
+ internal ProcessStepBuilderTyped(Type stepType, string id, ProcessBuilder? processBuilder, object? initialState = default, JsonSerializerOptions? jsonSerializerOptions = null)
: base(id, processBuilder)
{
Verify.NotNull(stepType);
+ this.JsonSerializerOptions ??= jsonSerializerOptions;
+
this._stepType = stepType;
this.FunctionsDict = this.GetFunctionMetadataMap();
this._initialState = initialState;
+
+ this.InputParametersTypeData = this.GetInputParameterFunctionDataMap(this.FunctionsDict);
+ this.OutputStepEvents = this.ExtractStepOutputEvents(stepType, this.FunctionsDict);
+ }
+
+ internal Dictionary> GetInputParameterFunctionDataMap(IDictionary functionDict)
+ {
+ var inputDataDict = new Dictionary>();
+
+ foreach (var kvp in functionDict)
+ {
+ var parameters = kvp.Value.Parameters;
+ if (!inputDataDict.TryGetValue(kvp.Key, out Dictionary? value))
+ {
+ value = new Dictionary();
+ inputDataDict[kvp.Key] = value;
+ }
+
+ foreach (var parameter in parameters)
+ {
+ if (FilterSupportedParameterTypes(parameter.ParameterType, hasDefaultValue: parameter.DefaultValue != null))
+ {
+ value[parameter.Name] = parameter.ToKernelEventTypeData();
+ }
+ }
+ }
+
+ return inputDataDict;
+ }
+
+ ///
+ /// Inspects default function return data if any, since this could be used with the method
+ ///
+ ///
+ ///
+ internal Dictionary ExtractOnFunctionResultEventsData(IDictionary functionDict)
+ {
+ var eventDataDict = new Dictionary();
+
+ foreach (var kvp in functionDict)
+ {
+ var eventId = this.GetFunctionResultEventId(kvp.Key);
+ if (kvp.Value.ReturnParameter.Schema != null)
+ {
+ var outputEventData = kvp.Value.ReturnParameter.ToKernelEventTypeData();
+ eventDataDict[eventId] = new(eventId, outputEventData);
+ }
+ }
+
+ return eventDataDict;
+ }
+
+ ///
+ /// Inspects Custom step implementation to extract custom output events declared in the class if any.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ internal Dictionary ExtractCustomOutputEventsData(Type stepType)
+ {
+ if (stepType == null)
+ {
+ throw new ArgumentNullException(nameof(stepType));
+ }
+
+ if (!typeof(KernelProcessStep).IsAssignableFrom(stepType))
+ {
+ throw new ArgumentException($"The type {stepType.Name} must derive from {nameof(KernelProcessStep)}.", nameof(stepType));
+ }
+
+ var stepEventsType = stepType.GetNestedType(nameof(KernelProcessStep.StepEvents), BindingFlags.Public | BindingFlags.Static);
+ if (stepEventsType == null)
+ {
+ // since there is no StepEvents found, attepting to using base class instead in case there is one
+ stepEventsType = stepType.BaseType?.GetNestedType(nameof(KernelProcessStep.StepEvents), BindingFlags.Public | BindingFlags.Static);
+ if (stepEventsType == null)
+ {
+ // no StepEvents found in base class either, log warning, maybe user is only relying on FunctionResult events only
+ //throw new ArgumentException($"No static {nameof(KernelProcessStep.StepEvents)} class found in {stepType.Name}");
+ return [];
+ }
+ }
+
+ var eventClassFields = stepEventsType.GetFields(BindingFlags.Public | BindingFlags.Static);
+ var processEventDescriptors = eventClassFields.Where(field => field.FieldType.IsGenericType && field.FieldType.GetGenericTypeDefinition() == typeof(KernelProcessEventDescriptor<>)).ToList();
+ if (processEventDescriptors.Count == 0)
+ {
+ // no StepEvents found in base class either, log warning, maybe user is only relying on FunctionResult events only
+ //throw new ArgumentException($"No public static fields of type {nameof(KernelProcessEventDescriptor