Zum Inhalt springen

Fun project of the week, Mermaid flowcharts generator!

Using a LLM to generate Mermaid flowcharts!

Image description

Introduction

It is not so long I discovered mermaid MMD flowchart definition and generation (I use draw.io app a lot and I’m quite satisfied) but sometimes Mermaid flowcharts are more handy to explain the things. So, what I discovered was that on the world wide net, there are lots of Mermaid flowchart tools but they are proposing limited number of charts to generate unless the user upgrades to a paid version. So I decided to build my own 🤓 and… it works fine!

I decided to build a program which offers flexible ways to generate Mermaid diagrams. It can either take my natural language descriptions in a chat-like interaction, leveraging a local Large Language Model (LLM) like Ollama with Granite 3.3 to interpret my ideas and generate the Mermaid definition. Alternatively, for those times when I already have a precise diagram in mind, it can directly accept a Mermaid description from a .mmd file located in an „input“ folder. In both scenarios, the application then processes the Mermaid code to produce high-quality PNG and SVG image files, making it a versatile tool for visualizing concepts.

The code 🆓

It would not come with a surprise if I mention that I used Granite 3.3 alongside with Ollama locally. In fact, it’s been quite the adventure, turning my humble machine into a personal diagram-generating wizard! Who needs cloud services when you have the power of a large language model right there on your desktop, ready to conjure up flowcharts with a flick of a prompt? It’s like having a tiny, very smart, and endlessly patient assistant dedicated solely to my diagramming whims… 😉

There we go with the implementation.

  • Prepare the environment;
#!/bin/sh 
python3 -m venv venv
source venv/bin/activate

pip install --upgrade pip

npm install -g @mermaid-js/mermaid-cli

Disclaimer: not being a 100% die-hard, I installed the mermaid-cli package.

  • The code!!!
import subprocess
import os
import requests
import json
import re

# --- Configuration ---
OLLAMA_API_URL = "http://localhost:11434/api/chat"
OLLAMA_MODEL = "granite3.3" # Ensure you have pulled this model with 'ollama pull granite3.3'

def check_mmdc_installed():
    """
    Checks if the 'mmdc' command-line tool is installed and accessible.
    Provides instructions if not found.
    """
    try:
        subprocess.run(['mmdc', '--version'], check=True, capture_output=True)
        print("Mermaid CLI (mmdc) is installed and ready.")
        return True
    except FileNotFoundError:
        print("Error: Mermaid CLI (mmdc) not found.")
        print("Please install it by running: npm install -g @mermaid-js/mermaid-cli")
        print("You'll need Node.js and npm installed first.")
        return False
    except subprocess.CalledProcessError as e:
        print(f"Error checking mmdc version: {e}")
        print(f"Stdout: {e.stdout.decode()}")
        print(f"Stderr: {e.stderr.decode()}")
        return False

def call_ollama_granite(user_prompt):
    """
    Calls the local Ollama instance with the Granite 3.3 model
    to generate a Mermaid flowchart definition, incorporating a 'thinking' control message.

    Args:
        user_prompt (str): The natural language description of the diagram.

    Returns:
        str: The generated Mermaid definition string, or None if an error occurs.
    """
    # System message to guide the LLM's behavior
    system_message_content = (
        "You are an expert in Mermaid diagram syntax. "
        "Your task is to convert natural language descriptions into valid Mermaid flowchart definitions. "
        "The output MUST be ONLY the Mermaid code block, enclosed in triple backticks and 'mermaid' language tag. "
        "For example:n"
        "```

mermaidn"
        "graph TD;n"
        "    A[Start] --> B[End];n"
        "

```n"
        "Ensure the generated diagram is a flowchart (graph TD or LR). "
        "IMPORTANT: For node text, use square brackets `[]` for standard process steps. "
        "If using curly braces `{}` for decision nodes, keep the text inside simple and concise, "
        "avoiding special characters like parentheses `()`, commas `,`, or other punctuation that could confuse the parser. "
        "If you need to list multiple items, consider putting them in separate nodes or simplifying the main node text significantly."
    )

    # Construct the messages array including the 'control' role for thinking
    messages = [
        {"role": "control", "content": "thinking"}, # Added thinking capability
        {"role": "system", "content": system_message_content},
        {"role": "user", "content": user_prompt}
    ]

    payload = {
        "model": OLLAMA_MODEL,
        "messages": messages, # Using messages array instead of prompt
        "stream": False # We want the full response at once
    }

    headers = {'Content-Type': 'application/json'}

    try:
        print(f"Calling Ollama with model: {OLLAMA_MODEL} and thinking control...")
        response = requests.post(OLLAMA_API_URL, headers=headers, data=json.dumps(payload))
        response.raise_for_status() #

        result = response.json()
        generated_content = result.get("message", {}).get("content", "").strip()

        # Extract the Mermaid code block using regex
        match = re.search(r"```

mermaidn(.*?)n

```", generated_content, re.DOTALL)
        if match:
            mermaid_code = match.group(1).strip()
            print("Successfully extracted Mermaid code from LLM response.")
            return mermaid_code
        else:
            print("Warning: Could not find a valid Mermaid code block in the LLM's response.")
            print("LLM Raw Response:n", generated_content)
            return None

    except requests.exceptions.ConnectionError:
        print(f"Error: Could not connect to Ollama server at {OLLAMA_API_URL}.")
        print(f"Please ensure Ollama is running and the model ('{OLLAMA_MODEL}') is pulled.")
        return None
    except requests.exceptions.RequestException as e:
        print(f"Error calling Ollama API: {e}")
        print(f"Response content: {response.text if 'response' in locals() else 'N/A'}")
        return None
    except json.JSONDecodeError:
        print("Error: Could not decode JSON response from Ollama.")
        return None
    except Exception as e:
        print(f"An unexpected error occurred during Ollama call: {e}")
        return None

def translate_mermaid_to_image(mermaid_definition, output_filename, output_format='png'):
    """
    Translates a Mermaid definition string into an image file (PNG or SVG).

    Args:
        mermaid_definition (str): The Mermaid diagram definition string.
        output_filename (str): The desired name for the output image file (without extension).
        output_format (str): The desired output format ('png' or 'svg').
                             Note: mmdc does not directly support GIF for static diagrams.
    """
    if not check_mmdc_installed():
        return False

    supported_formats = ['png', 'svg'] # Limiting to common image formats for direct output
    if output_format.lower() not in supported_formats:
        print(f"Error: Unsupported output format '{output_format}'. Please choose one of: {', '.join(supported_formats)}.")
        return False

    # Create a temporary file for the Mermaid definition
    temp_mermaid_file = "temp_mermaid_diagram.mmd"
    output_file_full_path = f"{output_filename}.{output_format}"
    try:
        with open(temp_mermaid_file, "w") as f:
            f.write(mermaid_definition)

        command = [
            'mmdc',
            '-i', temp_mermaid_file,
            '-o', output_file_full_path
        ]

        print(f"Executing command: {' '.join(command)}")
        process = subprocess.run(command, capture_output=True, text=True, check=True)

        print(f"Successfully translated Mermaid to {output_file_full_path}")
        if process.stdout:
            print("mmdc stdout:n", process.stdout)
        if process.stderr:
            print("mmdc stderr:n", process.stderr)
        return True

    except subprocess.CalledProcessError as e:
        print(f"Error translating Mermaid: {e}")
        print(f"mmdc stdout:n{e.stdout}")
        print(f"mmdc stderr:n{e.stderr}")
        return False
    except Exception as e:
        print(f"An unexpected error occurred during image generation: {e}")
        return False
    finally:
        # Clean up the temporary Mermaid file
        if os.path.exists(temp_mermaid_file):
            os.remove(temp_mermaid_file)

def main():
    """
    Main function to run the interactive Mermaid diagram generator.
    """
    print("Welcome to my Ollama-powered Mermaid Diagram Generator!")
    print("You can either describe a flowchart for me to generate,")
    print("or provide a Mermaid definition file from your 'input' folder.")
    print("nMake sure Ollama is running and you have 'granite3.3' model pulled.")
    print("Also ensure 'mmdc' (Mermaid CLI) is installed.")

    while True:
        choice = input("nDo you want to (1) Describe a flowchart or (2) Provide a Mermaid file? (1/2): ").strip()

        mermaid_code = None
        output_base_name = "generated_flowchart" # Default output base name

        if choice == '1':
            user_description = input("Provide the definition of the flowchart you want to build:n> ")
            if not user_description.strip():
                print("Description cannot be empty. Please try again.")
                continue
            print("nGenerating Mermaid definition using Ollama...")
            mermaid_code = call_ollama_granite(user_description)
            output_base_name = "generated_flowchart" # Ensure default for descriptions
        elif choice == '2':
            input_dir = "input"
            # Create 'input' directory if it doesn't exist
            if not os.path.exists(input_dir):
                os.makedirs(input_dir)
                print(f"Created '{input_dir}' directory. Please place your Mermaid files here and try again.")
                continue # Loop back to prompt for choice after creating dir

            file_name = input(f"Enter the Mermaid file name (e.g., my_diagram.mmd) from the '{input_dir}' folder:n> ")
            file_path = os.path.join(input_dir, file_name)

            if os.path.exists(file_path):
                try:
                    with open(file_path, 'r') as f:
                        mermaid_code = f.read()
                    print(f"Successfully read Mermaid definition from '{file_path}'.")
                    # Set output base name from the input file name
                    output_base_name = os.path.splitext(file_name)[0]
                except Exception as e:
                    print(f"Error reading file '{file_path}': {e}")
                    continue
            else:
                print(f"Error: File '{file_path}' not found. Please ensure it exists in the '{input_dir}' folder.")
                continue
        else:
            print("Invalid choice. Please enter '1' or '2'.")
            continue

        if mermaid_code:
            print("n--- Mermaid Definition ---")
            print(mermaid_code)
            print("--------------------------")

            print(f"nAttempting to generate images: {output_base_name}.png and {output_base_name}.svg")
            print("Note: GIF output is not directly supported by mmdc for static diagrams.")

            # Generate PNG
            png_success = translate_mermaid_to_image(mermaid_code, output_base_name, "png")

            # Generate SVG
            svg_success = translate_mermaid_to_image(mermaid_code, output_base_name, "svg")

            if png_success and svg_success:
                print(f"nSuccessfully created '{output_base_name}.png' and '{output_base_name}.svg'.")
            else:
                print("nImage generation failed for one or both formats. Please check the errors above.")
        else:
            print("nFailed to get Mermaid code. Please check your input, Ollama connection, or LLM response.")

        another = input("nDo you want to generate another diagram? (yes/no): ").lower()
        if another != 'yes':
            print("Exiting. Goodbye!")
            break

if __name__ == "__main__":
    main()
  • Running the code gives entire satisfactory results!
  • Example 1;
build a chart describing document chucking and embedding into a RAG vector database from various document sources such as PDF, DOC, PPTX and HTML 

Image description

  • Example 2: in this one I give a MMD file as input;
flowchart TD
    subgraph Client_Application [Your_main2]
        A[Input_Document_e_g_PDF] --> B[Docling_DocumentConverter_convert]
        B --> C{Docling_Internal_Pipelines_and_Parser_Backends}
        C -- PDF_Image_Processing --> D[OCR_Engine_e_g_EasyOCR]
        C -- Layout_Analysis --> E[Layout_Model]
        C -- Table_Recognition --> F[TableFormer_Model]
        D & E & F --> G[Docling_Document_Object_Structured_Data]
        G --> H[Iterate_Document_Elements]
        H --> I{Text_Table_Item}
        I -- Yes --> J[Call_translate_function]
        J --> K[Ollama_Client_Python_ollama_library]
        K -- REST_API_Call --> L[Ollama_Server]
        L -- Loads --> M[Granite3_Dense_LLM]
        M -- Translated_Text --> K
        K --> J
        J --> N[Update_Docling_Document_Object]
        N --> H
        H -- No_More_Elements --> O[Docling_Document_save_as_markdown]
        O --> P[Translated_Markdown_Output]
    end

    subgraph Ollama_Service
        L --- M
    end

    style A fill:#e0f7fa,stroke:#333,stroke-width:2px
    style P fill:#e0f7fa,stroke:#333,stroke-width:2px
    style B fill:#b3e5fc,stroke:#333,stroke-width:2px
    style C fill:#c7eafc,stroke:#333,stroke-width:2px
    style D fill:#dbeefc,stroke:#333,stroke-width:2px
    style E fill:#dbeefc,stroke:#333,stroke-width:2px
    style F fill:#dbeefc,stroke:#333,stroke-width:2px
    style G fill:#e0f7fa,stroke:#333,stroke-width:2px
    style H fill:#a7d9f7,stroke:#333,stroke-width:2px
    style I fill:#b7e2f7,stroke:#333,stroke-width:2px
    style J fill:#c7eafc,stroke:#333,stroke-width:2px
    style K fill:#d7f1fc,stroke:#333,stroke-width:2px
    style L fill:#e7f7fd,stroke:#333,stroke-width:2px
    style M fill:#f7fcfd,stroke:#333,stroke-width:2px
    style N fill:#a7d9f7,stroke:#333,stroke-width:2px
    style O fill:#b3e5fc,stroke:#333,stroke-width:2px

Image description

Tobe honest… the magic is in the CLI not my code, nonetheless from now on I can generate my Mermaid flowcharts locally using my app with my LLM of choice 😉😀

Image description

Thanks for reading!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert