Skip to content

Commit c474f6f

Browse files
eloycotoelai-shalev
authored andcommitted
feat: add mcp-server to preview workflows
This adds a new tool that allows the LLM to render a workflow for the user. When the LLM determines that everything is ready, it will provide the user with a workflow display to help them understand the process. This implementation is somewhat complex because SWFEditor always depends on a browser, and Java classes are not available in Maven Central. I've taken an approach that renders using static HTML and extracts it using a headless browser. While not optimal for performance, this solution works until we can make changes to the Stunner editor. Related repositories: - https://github.com/apache/incubator-kie-tools/tree/main/packages/ - Editor: https://github.com/apache/incubator-kie-tools/tree/ab3030b602d74d2f30227cba5c2c88aaccf2d2a8/packages/serverless-workflow-standalone-editor Signed-off-by: Eloy Coto <eloy.coto@acalustra.com>
1 parent e866e34 commit c474f6f

File tree

6 files changed

+321
-1
lines changed

6 files changed

+321
-1
lines changed

assets/workflow-renderer/editor.js

Lines changed: 2 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Workflow Renderer</title>
7+
<script src="editor.js"></script>
8+
</head>
9+
<body>
10+
<div id="editorStatus">Loading editor...</div>
11+
<div id="editorWorkflow"></div>
12+
<div id="renderWorkflow"></div>
13+
14+
<script>
15+
16+
sample_data = {
17+
"id": "hello_world",
18+
"version": "1.0",
19+
"specVersion": "0.8",
20+
"name": "Hello World Workflow",
21+
"description": "JSON based hello world workflow",
22+
"start": "Inject Hello World",
23+
"states": [
24+
{
25+
"name": "Inject Hello World",
26+
"type": "inject",
27+
"data": {
28+
"greeting": "Hello World"
29+
},
30+
"transition": "Inject Mantra"
31+
},
32+
{
33+
"name": "Inject Mantra",
34+
"type": "inject",
35+
"data": {
36+
"mantra": "Serverless Workflow is awesome!"
37+
},
38+
"end": true
39+
}
40+
]
41+
};
42+
43+
const editor = SwfEditor.open({
44+
container: document.getElementById("editorWorkflow"),
45+
initialContent: Promise.resolve(JSON.stringify({})),
46+
readOnly: true,
47+
languageType: "json",
48+
swfPreviewOptions: { editorMode: "diagram", defaultWidth: "100%" },
49+
});
50+
51+
let EditorIsReady = false;
52+
53+
const render_workflow = function(container, data) {
54+
55+
console.log("render_workflow called with data:", data);
56+
console.log("Container element:", container);
57+
// @TODO data should be an string
58+
editor.setContent('workflow.sw.json', data)
59+
.then(async function() {
60+
console.log("setContent completed successfully");
61+
try {
62+
console.log("Waiting for preview...");
63+
preview = await waitForPreview()
64+
console.log("Preview received:", preview);
65+
console.log("Preview length:", preview ? preview.length : 0);
66+
container.innerHTML = preview;
67+
console.log("Container innerHTML set, current content:", container.innerHTML);
68+
} catch(error){
69+
console.log("Cannot render the preview", error);
70+
}
71+
})
72+
.catch(function(error) {
73+
console.log("Error in setContent:", error);
74+
});
75+
}
76+
77+
const ready = function() {
78+
try {
79+
editor.getContent().then(function() {
80+
EditorIsReady = true;
81+
})
82+
}catch(error) {
83+
EditorIsReady = false;
84+
}
85+
return false;
86+
}
87+
88+
const waitForPreview = function(maxAttempts = 20, interval = 500) {
89+
return new Promise((resolve, reject) => {
90+
let attempts = 0;
91+
const checkPreview = async () => {
92+
try {
93+
const preview = await editor.getPreview();
94+
if (preview) {
95+
resolve(preview);
96+
} else if (attempts >= maxAttempts) {
97+
reject(new Error("Preview not available after maximum attempts"));
98+
} else {
99+
attempts++;
100+
setTimeout(checkPreview, interval);
101+
}
102+
} catch (error) {
103+
attempts++;
104+
setTimeout(checkPreview, interval);
105+
}
106+
};
107+
checkPreview();
108+
});
109+
}
110+
111+
</script>
112+
</body>
113+
</html>

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ dependencies = [
1212
"requests>=2.31.0",
1313
"typing-extensions>=4.8.0",
1414
"fastmcp>=0.1.0",
15+
"playwright>=1.54.0",
16+
"ipdb >= 0.13.13",
1517
]
1618

1719
[project.optional-dependencies]

tools/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from .orchestrator_creation_workflow_rules import creation_workflow_rules
2+
from .orchestrator_workflow_renderer import orchestrator_preview_workflow
23

3-
__all__ = [creation_workflow_rules]
4+
__all__ = [creation_workflow_rules, orchestrator_preview_workflow]
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import json
2+
import logging
3+
from pathlib import Path
4+
5+
from playwright.async_api import async_playwright
6+
7+
from .orchestrator_service import orchestrator_mcp
8+
9+
logger = logging.getLogger(__name__)
10+
11+
12+
class WorkflowRenderer:
13+
def __init__(self):
14+
self.html_path = (
15+
Path(__file__).parent.parent / "assets" / "workflow-renderer" / "index.html"
16+
)
17+
18+
async def render_workflow_to_svg(self, workflow_data: str) -> str:
19+
"""
20+
Render workflow data to SVG using headless browser
21+
22+
Args:
23+
workflow_data (str): JSON string of workflow data
24+
25+
Returns:
26+
str: SVG content
27+
"""
28+
logger.info("Starting workflow rendering process...")
29+
logger.info(f"HTML path: {self.html_path.absolute()}")
30+
31+
async with async_playwright() as p:
32+
logger.info("Launching headless browser...")
33+
browser = await p.chromium.launch()
34+
page = await browser.new_page()
35+
36+
page.on(
37+
"console",
38+
lambda msg: logger.info(f"Browser console [{msg.type}]: {msg.text}"),
39+
)
40+
page.on("pageerror", lambda err: logger.error(f"Browser error: {err}"))
41+
42+
# Load the HTML file
43+
logger.info(f"Loading HTML file: file://{self.html_path.absolute()}")
44+
await page.goto(f"file://{self.html_path.absolute()}")
45+
46+
# Wait for editor to initialize
47+
logger.info("Waiting for editor to initialize...")
48+
await page.wait_for_function("typeof render_workflow === 'function'")
49+
await page.wait_for_function("ready(); EditorIsReady === true")
50+
51+
# Execute the workflow rendering on page load
52+
try:
53+
# Example rendering
54+
# await page.evaluate("""
55+
# render_workflow(
56+
# document.getElementById("renderWorkflow"),
57+
# JSON.stringify(sample_data)
58+
# );
59+
# """)
60+
61+
await page.evaluate(f"""
62+
render_workflow(
63+
document.getElementById("renderWorkflow"),
64+
{workflow_data}
65+
);
66+
""")
67+
68+
# Get the SVG content
69+
await page.wait_for_function(
70+
"document.getElementById('renderWorkflow')."
71+
"querySelector('svg') !== null"
72+
)
73+
svg_content = await page.evaluate(
74+
"document.getElementById('renderWorkflow').innerHTML"
75+
)
76+
except Exception as e:
77+
logger.error(f"Error calling render_workflow: {e}")
78+
# Try to get any error messages from the page
79+
errors = await page.evaluate(
80+
"document.querySelector('#renderWorkflow').innerHTML"
81+
)
82+
logger.info(f"Container content: {errors}")
83+
raise
84+
85+
logger.info(
86+
f"SVG generated, length: "
87+
f"{len(svg_content) if svg_content else 0} characters"
88+
)
89+
await browser.close()
90+
logger.info("Browser closed")
91+
return svg_content
92+
93+
94+
@orchestrator_mcp.tool()
95+
async def orchestrator_preview_workflow(session_id: str, workflow: str) -> str:
96+
"""
97+
Generate SVG preview of a orchestrator workflow.
98+
99+
Args:
100+
session_id: Session identifier for tracking
101+
workflow: JSON string representing the serverless workflow
102+
103+
Returns:
104+
str: SVG content of the rendered workflow
105+
"""
106+
try:
107+
logger.info(f"Generating workflow preview for session {session_id}")
108+
109+
# Validate that workflow is valid JSON
110+
try:
111+
json.loads(workflow)
112+
except json.JSONDecodeError as e:
113+
logger.error(f"Invalid JSON workflow: {e}")
114+
raise ValueError(f"Invalid JSON workflow: {e}")
115+
116+
# Create renderer and generate SVG
117+
renderer = WorkflowRenderer()
118+
svg_content = await renderer.render_workflow_to_svg(workflow)
119+
120+
logger.info(f"Successfully generated SVG for session {session_id}")
121+
return svg_content
122+
123+
except Exception as e:
124+
logger.error(f"Error generating workflow preview for session {session_id}: {e}")
125+
raise

0 commit comments

Comments
 (0)