diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index 8ea8825027bb..b960a6214166 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -47,6 +47,7 @@
+
diff --git a/dotnet/samples/GettingStartedWithProcesses/GettingStartedWithProcesses.csproj b/dotnet/samples/GettingStartedWithProcesses/GettingStartedWithProcesses.csproj
index 595ef9d24f6a..911139b69872 100644
--- a/dotnet/samples/GettingStartedWithProcesses/GettingStartedWithProcesses.csproj
+++ b/dotnet/samples/GettingStartedWithProcesses/GettingStartedWithProcesses.csproj
@@ -31,6 +31,7 @@
+
diff --git a/dotnet/samples/GettingStartedWithProcesses/Step01/Step01_Processes.cs b/dotnet/samples/GettingStartedWithProcesses/Step01/Step01_Processes.cs
index 635ab27f33fc..eda87b18cd7e 100644
--- a/dotnet/samples/GettingStartedWithProcesses/Step01/Step01_Processes.cs
+++ b/dotnet/samples/GettingStartedWithProcesses/Step01/Step01_Processes.cs
@@ -4,7 +4,9 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
+using Microsoft.SemanticKernel.Process;
using SharedSteps;
+using Utilities;
namespace Step01;
@@ -64,6 +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/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/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
new file mode 100644
index 000000000000..dd93011dc181
--- /dev/null
+++ b/dotnet/src/Experimental/Process.Core/ProcessVisualizationExtensions.cs
@@ -0,0 +1,159 @@
+// 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, int maxLevel = 2)
+ {
+ var process = processBuilder.Build();
+ return process.ToMermaid(maxLevel);
+ }
+
+ ///
+ /// Generates a Mermaid diagram from a kernel process.
+ ///
+ ///
+ ///
+ ///
+ public static string ToMermaid(this KernelProcess process, int maxLevel = 2)
+ {
+ StringBuilder sb = new();
+ sb.AppendLine("flowchart LR");
+
+ // Generate the Mermaid flowchart content with indentation
+ string flowchartContent = RenderProcess(process, 1, isSubProcess: false, maxLevel);
+
+ // Append the formatted content to the main StringBuilder
+ sb.Append(flowchartContent);
+
+ return sb.ToString();
+ }
+
+ ///
+ /// Renders a process and its nested processes recursively as a Mermaid flowchart.
+ ///
+ /// 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 * level);
+
+ // 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!
+ );
+
+ // Add Start and End nodes only if this is not a sub-process
+ if (!isSubProcess)
+ {
+ sb.AppendLine($"{indentation}Start[\"Start\"]");
+ sb.AppendLine($"{indentation}End[\"End\"]");
+ }
+
+ // Process each step
+ foreach (var step in process.Steps)
+ {
+ 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)
+ {
+ foreach (var kvp in step.Edges)
+ {
+ 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 target;
+
+ // Check if the target step is the end node by function name
+ if (edge.OutputTarget.FunctionName.Equals("end", StringComparison.OrdinalIgnoreCase) && !isSubProcess)
+ {
+ target = "End[\"End\"]";
+ }
+ else if (stepNames.TryGetValue(edge.OutputTarget.StepId, out string? targetStepName))
+ {
+ target = $"{targetStepName}[\"{targetStepName}\"]";
+ }
+ else
+ {
+ // 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
+ 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();
+ }
+}