"""Render Mermaid diagrams to images via mermaid-cli and send to the current channel."""
from __future__ import annotations
import hashlib
import json
import logging
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from tool_context import ToolContext
logger = logging.getLogger(__name__)
MAX_DIAGRAM_SIZE = 50 * 1024 # 50KB
TOOL_NAME = "render_mermaid"
TOOL_DESCRIPTION = (
"Render a Mermaid diagram from its textual definition and send the resulting "
"image to the current channel. Supports flowcharts, sequence diagrams, "
"class diagrams, state diagrams, and other Mermaid chart types."
)
TOOL_PARAMETERS = {
"type": "object",
"properties": {
"mermaid_diagram": {
"type": "string",
"description": (
"The Mermaid diagram definition as text. "
"Example: 'graph TD\\n A[Start] --> B[End]'"
),
},
"format": {
"type": "string",
"description": "Output format: png or svg. Default: png",
"enum": ["png", "svg"],
},
},
"required": ["mermaid_diagram"],
}
[docs]
async def run(
mermaid_diagram: str,
format: str = "png",
ctx: ToolContext | None = None,
) -> str:
"""Render a Mermaid diagram and send the image to the channel.
Args:
mermaid_diagram: The Mermaid diagram definition as text.
format: Output format (png or svg). Default: png.
ctx: Tool execution context providing access to bot internals.
Returns:
JSON string with success/error result.
"""
if ctx is None or ctx.adapter is None:
return json.dumps({"error": "No platform adapter available."})
diagram = (mermaid_diagram or "").strip()
if not diagram:
return json.dumps({"error": "Mermaid diagram cannot be empty."})
if len(diagram.encode("utf-8")) > MAX_DIAGRAM_SIZE:
return json.dumps({
"error": f"Diagram exceeds maximum size ({MAX_DIAGRAM_SIZE // 1024}KB).",
})
if format == "svg":
mimetype = "image/svg+xml"
ext = "svg"
else:
mimetype = "image/png"
ext = "png"
try:
from mermaid_cli import render_mermaid
_title, _desc, img_bytes = await render_mermaid(
diagram,
output_format=format,
viewport={"width": 1920, "height": 1080, "deviceScaleFactor": 4},
background_color="#1e1e1e",
mermaid_config={"theme": "dark"},
playwright_config={
"headless": True,
"args": ["--no-sandbox", "--disable-gpu"],
},
)
except ImportError as exc:
logger.error("mermaid-cli not installed: %s", exc)
return json.dumps({
"error": "mermaid-cli is not installed. Run: pip install mermaid-cli && playwright install chromium",
})
except Exception as exc:
logger.error("mermaid-cli render failed: %s", exc, exc_info=True)
return json.dumps({"error": f"Failed to render diagram: {exc}"})
if not img_bytes:
return json.dumps({"error": "Received empty image from renderer."})
h = hashlib.sha256(img_bytes).hexdigest()[:16]
fname = f"mermaid_{h}.{ext}"
try:
file_url = await ctx.adapter.send_file(
ctx.channel_id, img_bytes, fname, mimetype,
)
ctx.sent_files.append({
"data": img_bytes,
"filename": fname,
"mimetype": mimetype,
"file_url": file_url or "",
})
except Exception as exc:
logger.error("Failed to send file to channel: %s", exc, exc_info=True)
return json.dumps({"error": f"Failed to send image to channel: {exc}"})
result = {
"success": True,
"filename": fname,
"result": "Diagram rendered and sent to the channel.",
}
if file_url:
result["file_url"] = file_url
return json.dumps(result)