Source code for tools.render_mermaid

"""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)