Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

.Net: Process mermaid flowchart code generation, image generation on flowchart and sample usage. #9705

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
1 change: 1 addition & 0 deletions dotnet/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
<PackageVersion Include="FastBertTokenizer" Version="1.0.28" />
<PackageVersion Include="PdfPig" Version="0.1.9" />
<PackageVersion Include="Pinecone.NET" Version="2.1.1" />
<PackageVersion Include="PuppeteerSharp" Version="20.0.5" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="8.0.1" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
<PackageVersion Include="System.Memory.Data" Version="8.0.1" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
<PackageReference Include="coverlet.collector" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="PuppeteerSharp" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.abstractions" />
<PackageReference Include="xunit.runner.visualstudio" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using SharedSteps;
using Microsoft.SemanticKernel.Process;
using Utilities;

namespace Step01;

Expand Down Expand Up @@ -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 });
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Reflection;
using PuppeteerSharp;

namespace Utilities;

/// <summary>
/// Renders Mermaid diagrams to images using Puppeteer-Sharp.
/// </summary>
public static class MermaidRenderer
{
/// <summary>
/// Generates a Mermaid diagram image from the provided Mermaid code.
/// </summary>
/// <param name="mermaidCode"></param>
/// <param name="filename"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
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 = $@"
<html>
<head>
<style>
body {{
display: flex;
align-items: center;
justify-content: center;
margin: 0;
height: 100vh;
}}
</style>
<script type=""module"">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
mermaid.initialize({{ startOnLoad: true }});
</script>
</head>
<body>
<div class=""mermaid"">
{mermaidCode}
</div>
</body>
</html>";

// 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}");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Linq;
using System.Text;

namespace Microsoft.SemanticKernel.Process;

/// <summary>
/// Provides extension methods to visualize a process as a Mermaid diagram.
/// </summary>
public static class ProcessVisualizationExtensions
{
/// <summary>
/// Generates a Mermaid diagram from a process builder.
/// </summary>
/// <param name="processBuilder"></param>
/// <returns></returns>
public static string ToMermaid(this ProcessBuilder processBuilder)
{
var process = processBuilder.Build();
return process.ToMermaid();
}

/// <summary>
/// Generates a Mermaid diagram from a kernel process.
/// </summary>
/// <param name="process"></param>
/// <returns></returns>
public static string ToMermaid(this KernelProcess process)
{
StringBuilder sb = new();
sb.AppendLine("flowchart LR");
//sb.AppendLine("graph LR");
joslat marked this conversation as resolved.
Show resolved Hide resolved

// 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();
}

/// <summary>
/// Generates the Mermaid graph for a given process.
/// </summary>
/// <param name="process"></param>
/// <returns></returns>
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)
.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)
{
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}]");
}
}
}

// 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)
joslat marked this conversation as resolved.
Show resolved Hide resolved
{
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($"{indentation}{source} --> {target}");
}
}
}
}

return sb.ToString();
}
}
Loading