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 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d5686d0
initial process markdown generation
joslat Nov 13, 2024
828cace
mermaid flowchart code generation, image generation on flowchart and …
joslat Nov 14, 2024
d7b13fa
Merge branch 'main' into joslat-process-framework-markdown-renderer
joslat Nov 14, 2024
5f4f404
improvement: nested sub processes
joslat Nov 14, 2024
bc44cf4
Merge branch 'joslat-process-framework-markdown-renderer' of https://…
joslat Nov 14, 2024
57bcdb6
Merge branch 'main' into joslat-process-framework-markdown-renderer
joslat Nov 15, 2024
0c8ec9c
Merge branch 'main' into joslat-process-framework-markdown-renderer
crickman Nov 19, 2024
0d402bc
Merge branch 'microsoft:main' into joslat-process-framework-markdown-…
joslat Nov 20, 2024
12d4140
usings ordering fix
joslat Nov 20, 2024
9cb8249
Merge branch 'main' into joslat-process-framework-markdown-renderer
joslat Nov 21, 2024
c7cbdb9
Merge branch 'main' into joslat-process-framework-markdown-renderer
joslat Nov 25, 2024
95cbfec
Merge branch 'main' into joslat-process-framework-markdown-renderer
crickman Dec 2, 2024
9ea0dc3
Merge branch 'main' into joslat-process-framework-markdown-renderer
crickman Dec 8, 2024
794a5de
Merge branch 'main' into joslat-process-framework-markdown-renderer
joslat Dec 10, 2024
d8a8c1e
Merge branch 'microsoft:main' into joslat-process-framework-markdown-…
joslat Dec 12, 2024
9eaed21
some improvements
joslat Dec 12, 2024
26ef5e5
more improvements, filepath of rendered workflow returned.
joslat Dec 12, 2024
90bdb09
Merge branch 'main' into joslat-process-framework-markdown-renderer
joslat Dec 12, 2024
d1e1541
PR minor adjustments
joslat Dec 17, 2024
55937a0
some defensive coding
joslat Dec 17, 2024
8dbb90d
Merge branch 'main' into joslat-process-framework-markdown-renderer
joslat Dec 17, 2024
2190449
Merge branch 'main' into joslat-process-framework-markdown-renderer
joslat Dec 18, 2024
ef1e63a
Merge branch 'main' into joslat-process-framework-markdown-renderer
joslat Dec 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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");
joslat marked this conversation as resolved.
Show resolved Hide resolved

// 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)
joslat marked this conversation as resolved.
Show resolved Hide resolved
{
// Locate the current assembly's directory
string? assemblyPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
joslat marked this conversation as resolved.
Show resolved Hide resolved
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);
joslat marked this conversation as resolved.
Show resolved Hide resolved
Console.WriteLine($"Diagram generated at: {outputFilePath}");
}
}
126 changes: 126 additions & 0 deletions dotnet/src/Experimental/Process.Core/ProcessVisualizationExtensions.cs
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;
joslat marked this conversation as resolved.
Show resolved Hide resolved

/// <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