From d5686d0e8f5fd9df2445db4b98f2f7522daeeb17 Mon Sep 17 00:00:00 2001 From: Jose Luis Latorre Millas Date: Wed, 13 Nov 2024 20:44:18 +0100 Subject: [PATCH 1/4] initial process markdown generation --- .../Step01/Step01_Processes.cs | 6 + .../ProcessVisualizationExtensions.cs | 111 ++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 dotnet/src/Experimental/Process.Core/ProcessVisualizationExtensions.cs diff --git a/dotnet/samples/GettingStartedWithProcesses/Step01/Step01_Processes.cs b/dotnet/samples/GettingStartedWithProcesses/Step01/Step01_Processes.cs index 8d26116c194a..3b8d4e63faa7 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step01/Step01_Processes.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step01/Step01_Processes.cs @@ -5,6 +5,7 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using SharedSteps; +using Microsoft.SemanticKernel.Process; namespace Step01; @@ -64,6 +65,11 @@ public async Task UseSimpleProcessAsync() // Build the process to get a handle that can be started KernelProcess kernelProcess = process.Build(); + string mermaidGraph = kernelProcess.ToMermaid(); + Console.WriteLine($"=== Start - Mermaid Diagram for '{process.Name}' ==="); + Console.WriteLine(mermaidGraph); + Console.WriteLine($"=== End - Mermaid Diagram for '{process.Name}' ==="); + // Start the process with an initial external event using var runningProcess = await kernelProcess.StartAsync(kernel, new KernelProcessEvent() { Id = ChatBotEvents.StartProcess, Data = null }); } diff --git a/dotnet/src/Experimental/Process.Core/ProcessVisualizationExtensions.cs b/dotnet/src/Experimental/Process.Core/ProcessVisualizationExtensions.cs new file mode 100644 index 000000000000..5b95e3b46ad2 --- /dev/null +++ b/dotnet/src/Experimental/Process.Core/ProcessVisualizationExtensions.cs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Linq; +using System.Text; + +namespace Microsoft.SemanticKernel.Process; + +/// +/// Provides extension methods to visualize a process as a Mermaid diagram. +/// +public static class ProcessVisualizationExtensions +{ + /// + /// Generates a Mermaid diagram from a process builder. + /// + /// + /// + public static string ToMermaid(this ProcessBuilder processBuilder) + { + var process = processBuilder.Build(); + return process.ToMermaid(); + } + + /// + /// Generates a Mermaid diagram from a kernel process. + /// + /// + /// + public static string ToMermaid(this KernelProcess process) + { + StringBuilder sb = new(); + sb.AppendLine("flowchart LR"); + //sb.AppendLine("graph LR"); + + // Dictionary to map step IDs to step names + var stepNames = process.Steps + .Where(step => step.State.Id != null && step.State.Name != null) + .ToDictionary( + step => step.State.Id!, + step => step.State.Name! + ); + + //var stepNames = process.Steps.ToDictionary( + // step => step.State.Id, + // step => step.State.Name); + + // Add Start and End nodes with proper Mermaid styling + sb.AppendLine("Start[Start]"); + sb.AppendLine("End[End]"); + + // Handle all edges without a predefined "Start" + foreach (var kvp in process.Edges) + { + var stepId = kvp.Key; + var edges = kvp.Value; + + foreach (var edge in edges) + { + string targetStepName = stepNames[edge.OutputTarget.StepId]; + + // Link edges without a specific preceding step to the Start node + if (!process.Steps.Any(s => s.Edges.ContainsKey(stepId))) + { + sb.AppendLine($"Start[Start] --> {targetStepName}[{targetStepName}]"); + } + } + } + + // Process each step + foreach (var step in process.Steps) + { + var stepId = step.State.Id; + var stepName = step.State.Name; + + // Handle edges from this step + if (step.Edges != null) + { + foreach (var kvp in step.Edges) + { + var eventId = kvp.Key; + var stepEdges = kvp.Value; + + foreach (var edge in stepEdges) + { + string source = $"{stepName}[{stepName}]"; + string target; + + // Check if the target step is the end node by function name + if (edge.OutputTarget.FunctionName.Equals( + "end", + StringComparison.OrdinalIgnoreCase)) + { + target = "End[End]"; + } + else + { + string targetStepName = stepNames[edge.OutputTarget.StepId]; + target = $"{targetStepName}[{targetStepName}]"; + } + + // Append the connection without showing IDs + sb.AppendLine($"{source} --> {target}"); + } + } + } + } + + return sb.ToString(); + } +} From 828cace930de742607504ae1419804ad50c51515 Mon Sep 17 00:00:00 2001 From: Jose Luis Latorre Millas Date: Thu, 14 Nov 2024 20:23:35 +0100 Subject: [PATCH 2/4] mermaid flowchart code generation, image generation on flowchart and sample usage. --- dotnet/Directory.Packages.props | 1 + .../GettingStartedWithProcesses.csproj | 1 + .../Step01/Step01_Processes.cs | 5 ++ .../Utilities/MermaidRenderer.cs | 84 +++++++++++++++++++ .../ProcessVisualizationExtensions.cs | 31 +++++-- 5 files changed, 114 insertions(+), 8 deletions(-) create mode 100644 dotnet/samples/GettingStartedWithProcesses/Utilities/MermaidRenderer.cs diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 05a06f7c9901..28123e26a272 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -43,6 +43,7 @@ + diff --git a/dotnet/samples/GettingStartedWithProcesses/GettingStartedWithProcesses.csproj b/dotnet/samples/GettingStartedWithProcesses/GettingStartedWithProcesses.csproj index 30ffffc43c8f..9b1131f4baa5 100644 --- a/dotnet/samples/GettingStartedWithProcesses/GettingStartedWithProcesses.csproj +++ b/dotnet/samples/GettingStartedWithProcesses/GettingStartedWithProcesses.csproj @@ -29,6 +29,7 @@ + diff --git a/dotnet/samples/GettingStartedWithProcesses/Step01/Step01_Processes.cs b/dotnet/samples/GettingStartedWithProcesses/Step01/Step01_Processes.cs index 3b8d4e63faa7..4bf5865f3832 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step01/Step01_Processes.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step01/Step01_Processes.cs @@ -6,6 +6,7 @@ using Microsoft.SemanticKernel.ChatCompletion; using SharedSteps; using Microsoft.SemanticKernel.Process; +using Utilities; namespace Step01; @@ -65,11 +66,15 @@ public async Task UseSimpleProcessAsync() // Build the process to get a handle that can be started KernelProcess kernelProcess = process.Build(); + // Generate a Mermaid diagram for the process and print it to the console string mermaidGraph = kernelProcess.ToMermaid(); Console.WriteLine($"=== Start - Mermaid Diagram for '{process.Name}' ==="); Console.WriteLine(mermaidGraph); Console.WriteLine($"=== End - Mermaid Diagram for '{process.Name}' ==="); + // Generate an image from the Mermaid diagram + await MermaidRenderer.GenerateMermaidImageAsync(mermaidGraph, "ChatBotProcess.png"); + // Start the process with an initial external event using var runningProcess = await kernelProcess.StartAsync(kernel, new KernelProcessEvent() { Id = ChatBotEvents.StartProcess, Data = null }); } diff --git a/dotnet/samples/GettingStartedWithProcesses/Utilities/MermaidRenderer.cs b/dotnet/samples/GettingStartedWithProcesses/Utilities/MermaidRenderer.cs new file mode 100644 index 000000000000..9b09d8d3873c --- /dev/null +++ b/dotnet/samples/GettingStartedWithProcesses/Utilities/MermaidRenderer.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Reflection; +using PuppeteerSharp; + +namespace Utilities; + +/// +/// Renders Mermaid diagrams to images using Puppeteer-Sharp. +/// +public static class MermaidRenderer +{ + /// + /// Generates a Mermaid diagram image from the provided Mermaid code. + /// + /// + /// + /// + /// + public static async Task GenerateMermaidImageAsync(string mermaidCode, string filename) + { + // Locate the current assembly's directory + string? assemblyPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + if (assemblyPath == null) + { + throw new InvalidOperationException("Could not determine the assembly path."); + } + + // Define the output folder path and create it if it doesn't exist + string outputPath = Path.Combine(assemblyPath, "output"); + Directory.CreateDirectory(outputPath); + + // Full path for the output file + string outputFilePath = Path.Combine(outputPath, filename); + + // Download Chromium if it hasn't been installed yet + BrowserFetcher browserFetcher = new(); + browserFetcher.Browser = SupportedBrowser.Chrome; + await browserFetcher.DownloadAsync(); + //await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultChromiumRevision); + + // Define the HTML template with Mermaid.js CDN + string htmlContent = $@" + + + + + + +
+ {mermaidCode} +
+ + "; + + // Create a temporary HTML file with the Mermaid code + string tempHtmlFile = Path.Combine(Path.GetTempPath(), "mermaid_temp.html"); + await File.WriteAllTextAsync(tempHtmlFile, htmlContent); + + // Launch Puppeteer-Sharp with a headless browser to render the Mermaid diagram + using (var browser = await Puppeteer.LaunchAsync(new LaunchOptions { Headless = true })) + using (var page = await browser.NewPageAsync()) + { + await page.GoToAsync($"file://{tempHtmlFile}"); + await page.WaitForSelectorAsync(".mermaid"); // Wait for Mermaid to render + await page.ScreenshotAsync(outputFilePath, new ScreenshotOptions { FullPage = true }); + } + + // Clean up the temporary HTML file + File.Delete(tempHtmlFile); + Console.WriteLine($"Diagram generated at: {outputFilePath}"); + } +} diff --git a/dotnet/src/Experimental/Process.Core/ProcessVisualizationExtensions.cs b/dotnet/src/Experimental/Process.Core/ProcessVisualizationExtensions.cs index 5b95e3b46ad2..e560622127d4 100644 --- a/dotnet/src/Experimental/Process.Core/ProcessVisualizationExtensions.cs +++ b/dotnet/src/Experimental/Process.Core/ProcessVisualizationExtensions.cs @@ -33,6 +33,25 @@ public static string ToMermaid(this KernelProcess process) sb.AppendLine("flowchart LR"); //sb.AppendLine("graph LR"); + // Generate the Mermaid flowchart content with indentation + string flowchartContent = GenerateMermaidFlowchart(process); + + // Append the formatted content to the main StringBuilder + sb.Append(flowchartContent); + + return sb.ToString(); + } + + /// + /// Generates the Mermaid graph for a given process. + /// + /// + /// + private static string GenerateMermaidFlowchart(KernelProcess process) + { + StringBuilder sb = new(); + string indentation = new(' ', 4); + // Dictionary to map step IDs to step names var stepNames = process.Steps .Where(step => step.State.Id != null && step.State.Name != null) @@ -41,13 +60,9 @@ public static string ToMermaid(this KernelProcess process) step => step.State.Name! ); - //var stepNames = process.Steps.ToDictionary( - // step => step.State.Id, - // step => step.State.Name); - // Add Start and End nodes with proper Mermaid styling - sb.AppendLine("Start[Start]"); - sb.AppendLine("End[End]"); + sb.AppendLine($"{indentation}Start[Start]"); + sb.AppendLine($"{indentation}End[End]"); // Handle all edges without a predefined "Start" foreach (var kvp in process.Edges) @@ -62,7 +77,7 @@ public static string ToMermaid(this KernelProcess process) // Link edges without a specific preceding step to the Start node if (!process.Steps.Any(s => s.Edges.ContainsKey(stepId))) { - sb.AppendLine($"Start[Start] --> {targetStepName}[{targetStepName}]"); + sb.AppendLine($"{indentation}Start[Start] --> {targetStepName}[{targetStepName}]"); } } } @@ -100,7 +115,7 @@ public static string ToMermaid(this KernelProcess process) } // Append the connection without showing IDs - sb.AppendLine($"{source} --> {target}"); + sb.AppendLine($"{indentation}{source} --> {target}"); } } } From 5f4f404c96aed0251c06e460da73972bb079d6a4 Mon Sep 17 00:00:00 2001 From: Jose Luis Latorre Millas Date: Fri, 15 Nov 2024 00:44:56 +0100 Subject: [PATCH 3/4] improvement: nested sub processes --- .../Step03/Step03a_FoodPreparation.cs | 7 ++ .../ProcessVisualizationExtensions.cs | 109 ++++++++++++------ 2 files changed, 78 insertions(+), 38 deletions(-) diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Step03a_FoodPreparation.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Step03a_FoodPreparation.cs index c299960c07a9..3f5eb04cd7e0 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step03/Step03a_FoodPreparation.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Step03a_FoodPreparation.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Process; using Microsoft.SemanticKernel.Process.Models; using Step03.Processes; using Utilities; @@ -36,6 +37,12 @@ public async Task UsePreparePotatoFriesProcessAsync() public async Task UsePrepareFishSandwichProcessAsync() { var process = FishSandwichProcess.CreateProcess(); + + string mermaidGraph = process.ToMermaid(2); + Console.WriteLine($"=== Start - Mermaid Diagram for '{process.Name}' ==="); + Console.WriteLine(mermaidGraph); + Console.WriteLine($"=== End - Mermaid Diagram for '{process.Name}' ==="); + await UsePrepareSpecificProductAsync(process, FishSandwichProcess.ProcessEvents.PrepareFishSandwich); } diff --git a/dotnet/src/Experimental/Process.Core/ProcessVisualizationExtensions.cs b/dotnet/src/Experimental/Process.Core/ProcessVisualizationExtensions.cs index e560622127d4..dd93011dc181 100644 --- a/dotnet/src/Experimental/Process.Core/ProcessVisualizationExtensions.cs +++ b/dotnet/src/Experimental/Process.Core/ProcessVisualizationExtensions.cs @@ -15,26 +15,27 @@ public static class ProcessVisualizationExtensions /// Generates a Mermaid diagram from a process builder. /// /// + /// /// - public static string ToMermaid(this ProcessBuilder processBuilder) + public static string ToMermaid(this ProcessBuilder processBuilder, int maxLevel = 2) { var process = processBuilder.Build(); - return process.ToMermaid(); + return process.ToMermaid(maxLevel); } /// /// Generates a Mermaid diagram from a kernel process. /// /// + /// /// - public static string ToMermaid(this KernelProcess process) + public static string ToMermaid(this KernelProcess process, int maxLevel = 2) { StringBuilder sb = new(); sb.AppendLine("flowchart LR"); - //sb.AppendLine("graph LR"); // Generate the Mermaid flowchart content with indentation - string flowchartContent = GenerateMermaidFlowchart(process); + string flowchartContent = RenderProcess(process, 1, isSubProcess: false, maxLevel); // Append the formatted content to the main StringBuilder sb.Append(flowchartContent); @@ -43,14 +44,17 @@ public static string ToMermaid(this KernelProcess process) } /// - /// Generates the Mermaid graph for a given process. + /// Renders a process and its nested processes recursively as a Mermaid flowchart. /// - /// - /// - private static string GenerateMermaidFlowchart(KernelProcess process) + /// The process to render. + /// The indentation level for nested processes. + /// Indicates if the current process is a sub-process. + /// + /// A string representation of the process in Mermaid syntax. + private static string RenderProcess(KernelProcess process, int level, bool isSubProcess, int maxLevel = 2) { StringBuilder sb = new(); - string indentation = new(' ', 4); + string indentation = new(' ', 4 * level); // Dictionary to map step IDs to step names var stepNames = process.Steps @@ -58,28 +62,13 @@ private static string GenerateMermaidFlowchart(KernelProcess process) .ToDictionary( step => step.State.Id!, step => step.State.Name! - ); + ); - // Add Start and End nodes with proper Mermaid styling - sb.AppendLine($"{indentation}Start[Start]"); - sb.AppendLine($"{indentation}End[End]"); - - // Handle all edges without a predefined "Start" - foreach (var kvp in process.Edges) + // Add Start and End nodes only if this is not a sub-process + if (!isSubProcess) { - var stepId = kvp.Key; - var edges = kvp.Value; - - foreach (var edge in edges) - { - string targetStepName = stepNames[edge.OutputTarget.StepId]; - - // Link edges without a specific preceding step to the Start node - if (!process.Steps.Any(s => s.Edges.ContainsKey(stepId))) - { - sb.AppendLine($"{indentation}Start[Start] --> {targetStepName}[{targetStepName}]"); - } - } + sb.AppendLine($"{indentation}Start[\"Start\"]"); + sb.AppendLine($"{indentation}End[\"End\"]"); } // Process each step @@ -88,6 +77,29 @@ private static string GenerateMermaidFlowchart(KernelProcess process) var stepId = step.State.Id; var stepName = step.State.Name; + // Check if the step is a nested process (sub-process) + if (step is KernelProcess nestedProcess && level < maxLevel) + { + sb.AppendLine($"{indentation}subgraph {stepName.Replace(" ", "")}[\"{stepName}\"]"); + sb.AppendLine($"{indentation} direction LR"); + + // Render the nested process content without its own Start/End nodes + string nestedFlowchart = RenderProcess(nestedProcess, level + 1, isSubProcess: true, maxLevel); + + sb.Append(nestedFlowchart); + sb.AppendLine($"{indentation}end"); + } + else if (step is KernelProcess nestedProcess2 && level >= maxLevel) + { + // Render a subprocess step + sb.AppendLine($"{indentation}{stepName}[[\"{stepName}\"]]"); + } + else + { + // Render the regular step + sb.AppendLine($"{indentation}{stepName}[\"{stepName}\"]"); + } + // Handle edges from this step if (step.Edges != null) { @@ -96,31 +108,52 @@ private static string GenerateMermaidFlowchart(KernelProcess process) var eventId = kvp.Key; var stepEdges = kvp.Value; + // Skip drawing edges that point to a nested process as an entry point + if (stepNames.ContainsKey(eventId) && process.Steps.Any(s => s.State.Name == eventId && s is KernelProcess)) + { + continue; + } + foreach (var edge in stepEdges) { - string source = $"{stepName}[{stepName}]"; + string source = $"{stepName}[\"{stepName}\"]"; string target; // Check if the target step is the end node by function name - if (edge.OutputTarget.FunctionName.Equals( - "end", - StringComparison.OrdinalIgnoreCase)) + if (edge.OutputTarget.FunctionName.Equals("end", StringComparison.OrdinalIgnoreCase) && !isSubProcess) { - target = "End[End]"; + target = "End[\"End\"]"; + } + else if (stepNames.TryGetValue(edge.OutputTarget.StepId, out string? targetStepName)) + { + target = $"{targetStepName}[\"{targetStepName}\"]"; } else { - string targetStepName = stepNames[edge.OutputTarget.StepId]; - target = $"{targetStepName}[{targetStepName}]"; + // Handle cases where the target step is not in the current dictionary, possibly a nested step or placeholder + // As we have events from the step that, when it is a subprocess, that go to a step in the subprocess + // Those are triggered by events and do not have an origin step, also they are not connected to the Start node + // So we need to handle them separately - we ignore them for now + continue; } - // Append the connection without showing IDs + // Append the connection sb.AppendLine($"{indentation}{source} --> {target}"); } } } } + // Connect Start to the first step and the last step to End (only for the main process) + if (!isSubProcess && process.Steps.Count > 0) + { + var firstStepName = process.Steps.First().State.Name; + var lastStepName = process.Steps.Last().State.Name; + + sb.AppendLine($"{indentation}Start --> {firstStepName}[\"{firstStepName}\"]"); + sb.AppendLine($"{indentation}{lastStepName}[\"{lastStepName}\"] --> End"); + } + return sb.ToString(); } } From 12d4140a958de0f6e55fc245ab670eed4c36e5ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Luis=20Latorre=20Millas?= Date: Thu, 21 Nov 2024 00:57:28 +0100 Subject: [PATCH 4/4] usings ordering fix --- .../GettingStartedWithProcesses/Step01/Step01_Processes.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/samples/GettingStartedWithProcesses/Step01/Step01_Processes.cs b/dotnet/samples/GettingStartedWithProcesses/Step01/Step01_Processes.cs index 9425ddef0a48..eda87b18cd7e 100644 --- a/dotnet/samples/GettingStartedWithProcesses/Step01/Step01_Processes.cs +++ b/dotnet/samples/GettingStartedWithProcesses/Step01/Step01_Processes.cs @@ -4,8 +4,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; -using SharedSteps; using Microsoft.SemanticKernel.Process; +using SharedSteps; using Utilities; namespace Step01;