Using LLMs to generate Mermaid flowcharts and don’t pay fees to sites!
Introduction
Following a previous post on how to generate Mermaid charts using Ollama and local LLMs (because I get really bored with sites which ask me to subscribe and pay a fee), I decided to enhance and update my application. There are two essential reasons for that;
- My first app was containing hard-coded information regarding the LLM I want to use.
- The application was not iterative.
🛠️ Essential Enhancements
The core of your enhancement should involve abstracting the LLM choice and creating a clean, repeatable workflow.
Dynamic LLM Selection (Addressing Hardcoded Info)
Instead of having a single hardcoded model, your application should dynamically discover and utilize any model available via your local Ollama instance.
- Implement Model Discovery: The application must send a request to the Ollama API’s /api/tags endpoint (
http://localhost:11434/api/tags). This endpoint returns a JSON list of all locally installed models. - Create a Selection Interface-CLI: Present the discovered list of models with numbered indices and prompt the user to choose one by number.
- Create a Selection Interface-CLI with a GUI (Streamlit/other): Use a dropdown or radio button group populated by the retrieved model names.
- Pass the Model Name: The chosen model name (e.g.,
llama3:8b-instruct-q4_0) must then be used as a variable in the payload for all subsequent API calls to the/api/chatendpoint.
Iterative Workflow and Error Handling (Addressing Iterativeness)
A non-iterative application forces a restart for every chart, which is frustrating. Iterativeness isn’t just about looping; it’s about handling success/failure gracefully within the same session.
- Main Execution Loop: Wrap your primary logic (
prompt user > call LLM > generate image) in awhile Trueloop that only breaks when the user explicitly chooses to quit. - Session State (GUI): If you use a GUI framework like Streamlit (as we discussed previously), we must use Session State (st.session_state) to preserve the generated Mermaid code and the image path across button clicks and re-renders. This maintains the context of the user's current diagram.
- Error Prevention/Debugging-Input Validation: Check if the user’s prompt is empty.
- Error Prevention/Debugging-Connection Check: Check if the Ollama server is running before trying to fetch models or generate code.
- File Handling Safety: Since it is creating temporary files for mmdc, ensure the cleanup logic is debuggable (e.g., only delete temp files if
DEBUG_MODEis disabled).
And my ideas for V3… (to be continued)
| Enhancement | Description | Value Proposition |
| --------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| **Code Review/Repair Mode** | If `mmdc` fails to render (due to a syntax error), automatically send the Mermaid code *and* the `mmdc` error log back to the LLM (with a specific system prompt) to ask it to fix the syntax. | Reduces user frustration and automatically fixes common LLM-induced syntax errors. |
| **Diagram History** | Store the generated text prompt, the output code, and the corresponding image file path in a simple local database (like SQLite) or a structured file (like JSON/YAML). | Allows users to easily revisit and reuse past diagrams without regenerating them. |
| **Output Format Options** | Add options to output the diagram in formats other than PNG, such as **SVG** (better for scaling) or **PDF**. | Increases versatility for users needing high-quality vector graphics. |
| **Persistent Settings** | Save the last used LLM model to a configuration file (e.g., `config.json`). | Saves the user time by automatically selecting their preferred model upon startup. |
Code(s) and Implementation(s)
1 — The console version
- Let’s jump into the first console mode application. As usual build a virtual Python environment.
pip install --upgrade pip
pip install requests
npm install -g @mermaid-js/mermaid-cli
- And the code 🧑💻
# app_V3.py
import subprocess
import os
import requests
import json
import re
import glob
import sys
import time
from pathlib import Path
DEBUG_MODE = True
OLLAMA_BASE_URL = "http://localhost:11434"
OLLAMA_CHAT_URL = f"{OLLAMA_BASE_URL}/api/chat"
OLLAMA_TAGS_URL = f"{OLLAMA_BASE_URL}/api/tags"
INPUT_DIR = Path("./input")
OUTPUT_DIR = Path("./output")
def check_mmdc_installed():
"""Checks if 'mmdc' is installed."""
try:
subprocess.run(['mmdc', '--version'], check=True, capture_output=True, timeout=5)
return True
except (FileNotFoundError, subprocess.TimeoutExpired, subprocess.CalledProcessError):
print("Error: Mermaid CLI (mmdc) not found or misconfigured.")
print("Try: npm install -g @mermaid-js/mermaid-cli")
return False
# MODEL SELECTION
def get_installed_models():
"""Fetches locally installed Ollama models."""
try:
response = requests.get(OLLAMA_TAGS_URL, timeout=5)
response.raise_for_status()
return sorted([m['name'] for m in response.json().get('models', [])])
except:
return []
def select_model_interactive():
"""Interactive menu to choose a model."""
print("\n--- Ollama Model Selection ---")
models = get_installed_models()
if not models:
return input("No models found. Enter model name manually (e.g., llama3): ").strip() or "llama3"
for idx, model in enumerate(models, 1):
print(f"{idx}. {model}")
while True:
choice = input(f"\nSelect a model (1-{len(models)}) or type custom name: ").strip()
if choice.isdigit() and 1 <= int(choice) <= len(models):
return models[int(choice) - 1]
elif choice:
return choice
def clean_mermaid_code(code_string):
"""Clean common LLM formatting errors from Mermaid code."""
cleaned = code_string.replace(u'\xa0', ' ').replace(u'\u200b', '')
cleaned = cleaned.replace("```
mermaid", "").replace("
```", "")
cleaned = re.sub(r'[ \t\r\f\v]+', ' ', cleaned)
lines = cleaned.splitlines()
rebuilt = []
for line in lines:
s_line = line.strip()
if s_line:
rebuilt.append(s_line)
final = '\n'.join(rebuilt)
final = re.sub(r'(\])([A-Za-z0-9])', r'\1\n\2', final)
return final.strip()
def generate_mermaid_code(user_prompt, model_name):
"""Calls Ollama to generate the code."""
system_msg = (
"You are a Mermaid Diagram Generator. Output ONLY valid Mermaid code. "
"Do not include explanations. Start with 'graph TD' or 'flowchart LR'. "
"Use simple ASCII characters for node IDs."
)
payload = {
"model": model_name,
"messages": [{"role": "system", "content": system_msg}, {"role": "user", "content": user_prompt}],
"stream": False,
"options": {"temperature": 0.1}
}
try:
print(f"Thinking ({model_name})...")
response = requests.post(OLLAMA_CHAT_URL, json=payload, timeout=60)
response.raise_for_status()
content = response.json().get("message", {}).get("content", "").strip()
match = re.search(r"```
mermaid\n(.*?)\n
```", content, re.DOTALL)
code = match.group(1) if match else content
return clean_mermaid_code(code)
except Exception as e:
print(f"Error communicating with Ollama: {e}")
return None
def translate_mermaid_to_image(mermaid_definition, output_path_base, output_format='png'):
"""Generates image from Mermaid code."""
if not check_mmdc_installed(): return False
output_path_base.parent.mkdir(parents=True, exist_ok=True)
output_file = output_path_base.with_suffix(f'.{output_format}')
temp_file = f"debug_{int(time.time())}.mmd"
try:
with open(temp_file, "w", encoding='utf-8') as f:
f.write(mermaid_definition)
command = ['mmdc', '-i', temp_file, '-o', str(output_file)]
process = subprocess.run(command, capture_output=True, text=True)
if process.returncode != 0:
print(f"\n❌ ERROR: mmdc failed for {output_file.name}")
print(f"--- STDERR ---\n{process.stderr}\n--------------")
if DEBUG_MODE:
print(f"⚠️ DEBUG MODE: Kept temporary file at: {os.path.abspath(temp_file)}")
print(" Open this file in a text editor to check for syntax errors.")
return False
print(f"✅ Saved: {output_file}")
return True
except Exception as e:
print(f"Unexpected error: {e}")
return False
finally:
if not DEBUG_MODE and os.path.exists(temp_file):
os.remove(temp_file)
def main():
print("Welcome to the Ollama Mermaid Generator (Debug Mode)")
if not check_mmdc_installed(): return
selected_model = select_model_interactive()
OUTPUT_DIR.mkdir(exist_ok=True)
while True:
choice = input("\n(1) Describe flowchart (2) Process files (q) Quit\n> ").strip().lower()
mermaid_codes = []
if choice == '1':
desc = input("Describe flowchart: ")
if not desc: continue
code = generate_mermaid_code(desc, selected_model)
if code:
name = f"chart_{int(time.time())}"
mermaid_codes.append((code, OUTPUT_DIR / name))
elif choice == '2':
if not INPUT_DIR.exists(): INPUT_DIR.mkdir(); print(f"Created {INPUT_DIR}"); continue
files = list(INPUT_DIR.glob('**/*.mmd'))
if not files: print("No files found."); continue
for f in files:
try:
code = clean_mermaid_code(f.read_text(encoding='utf-8'))
mermaid_codes.append((code, OUTPUT_DIR / f.relative_to(INPUT_DIR).with_suffix('')))
except Exception as e: print(f"Error reading {f}: {e}")
elif choice == 'q': break
for code, path in mermaid_codes:
print(f"\nProcessing {path.name}...")
if DEBUG_MODE:
print(f"--- Code Preview ---\n{code[:100]}...\n--------------------")
success = translate_mermaid_to_image(code, path)
if not success:
input("\n⚠️ Generation failed. Read errors above and press Enter to continue...")
if __name__ == "__main__":
main()
- The output 📤
python app_V3.py
Welcome to the Ollama Mermaid Generator (Debug Mode)
--- Ollama Model Selection ---
1. all-minilm:latest
2. deepseek-r1:latest
3. embeddinggemma:latest
4. granite-embedding:278m
5. granite-embedding:latest
6. granite3.2-vision:2b
7. granite3.3:latest
8. granite4:latest
9. granite4:micro-h
10. ibm/granite4:latest
11. ibm/granite4:micro
12. ibm/granite4:tiny-h
13. llama3.2-vision:latest
14. llama3:8b-instruct-q4_0
15. llama3:latest
16. ministral-3:latest
17. mistral:7b
18. mxbai-embed-large:latest
19. nomic-embed-text:latest
20. qwen3-vl:235b-cloud
Select a model (1-20) or type custom name: 8
(1) Describe flowchart (2) Process files (q) Quit
>
- The generated sample result if you want to generate a flowchart mermaid (.mmd) file ☕
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
- Or if you want to give a definition and generate a visual flow;
2 — The GUI version using Streamlit
- This version uses Streamlit to provide an enhanced interactive interface. Inside the virtual environment, we need to install the additional package.
# already done above
pip install --upgrade pip
pip install requests
npm install -g @mermaid-js/mermaid-cli
# new installation
pip install streamlit
- And the code 🆓
# appST.py
import streamlit as st
import subprocess
import requests
import json
import re
import os
import time
from pathlib import Path
# --- Configuration ---
OLLAMA_BASE_URL = "http://localhost:11434"
OLLAMA_CHAT_URL = f"{OLLAMA_BASE_URL}/api/chat"
OLLAMA_TAGS_URL = f"{OLLAMA_BASE_URL}/api/tags"
OUTPUT_DIR = Path("./output")
OUTPUT_DIR.mkdir(exist_ok=True)
st.set_page_config(
page_title="Ollama Mermaid Architect",
page_icon="🎨",
layout="wide"
)
if 'mermaid_code' not in st.session_state:
st.session_state['mermaid_code'] = ""
if 'generated_image_path' not in st.session_state:
st.session_state['generated_image_path'] = None
def check_mmdc_installed():
"""Checks if mmdc is available."""
try:
subprocess.run(['mmdc', '--version'], check=True, capture_output=True, timeout=5)
return True
except:
return False
def get_installed_models():
"""Fetches models from Ollama."""
try:
response = requests.get(OLLAMA_TAGS_URL, timeout=2)
if response.status_code == 200:
return [m['name'] for m in response.json().get('models', [])]
return []
except:
return []
def clean_mermaid_code(code_string):
"""Cleans up LLM output."""
clean = re.sub(r'```
mermaid', '', code_string, flags=re.IGNORECASE)
clean = re.sub(r'
```', '', clean)
clean = clean.replace(u'\xa0', ' ').replace(u'\u200b', '')
lines = [line.strip() for line in clean.splitlines() if line.strip()]
return '\n'.join(lines)
def generate_code(prompt, model):
"""Calls Ollama API."""
system_msg = (
"You are a Mermaid Diagram Generator. "
"Output ONLY valid Mermaid code. Start with 'graph TD' or 'flowchart LR'. "
"Do NOT include explanations or markdown ticks."
)
payload = {
"model": model,
"messages": [
{"role": "system", "content": system_msg},
{"role": "user", "content": prompt}
],
"stream": False,
"options": {"temperature": 0.2}
}
try:
response = requests.post(OLLAMA_CHAT_URL, json=payload, timeout=60)
if response.status_code == 200:
content = response.json()['message']['content']
return clean_mermaid_code(content)
else:
st.error(f"Ollama Error: {response.text}")
return None
except Exception as e:
st.error(f"Connection Error: {e}")
return None
def render_diagram(mermaid_code):
"""Runs mmdc to generate an SVG for display."""
timestamp = int(time.time())
temp_mmd = OUTPUT_DIR / f"temp_{timestamp}.mmd"
output_svg = OUTPUT_DIR / f"diagram_{timestamp}.svg"
try:
with open(temp_mmd, "w", encoding='utf-8') as f:
f.write(mermaid_code)
cmd = ['mmdc', '-i', str(temp_mmd), '-o', str(output_svg), '-b', 'transparent']
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
return str(output_svg)
else:
st.error("Mermaid Compilation Failed:")
st.code(result.stderr)
return None
except Exception as e:
st.error(f"System Error: {e}")
return None
finally:
if temp_mmd.exists():
temp_mmd.unlink()
# --- GUI Layout ---
# Sidebar
with st.sidebar:
st.header("⚙️ Configuration")
if check_mmdc_installed():
st.success("Mermaid CLI: Detected ✅")
else:
st.error("Mermaid CLI: Not Found ❌")
st.info("Run: `npm install -g @mermaid-js/mermaid-cli`")
st.stop()
models = get_installed_models()
if models:
selected_model = st.selectbox("Select Ollama Model", models, index=0)
else:
st.warning("Ollama not detected or no models found.")
selected_model = st.text_input("Manually enter model name", "llama3")
st.markdown("---")
st.markdown("### Tips")
st.markdown("- Be specific about direction (Top-Down vs Left-Right).")
st.markdown("- Mention specific node shapes if needed.")
st.title("🎨 Ollama Mermaid Architect")
st.markdown("Generate, Visualize, and Edit flowcharts using local LLMs.")
user_prompt = st.text_area("Describe your flowchart:", height=100, placeholder="e.g. Create a flowchart for a login process including password reset...")
col1, col2 = st.columns([1, 5])
with col1:
generate_btn = st.button("🚀 Generate", type="primary")
if generate_btn and user_prompt:
with st.spinner(f"Asking {selected_model} to design your chart..."):
generated_code = generate_code(user_prompt, selected_model)
if generated_code:
st.session_state['mermaid_code'] = generated_code
svg_path = render_diagram(generated_code)
st.session_state['generated_image_path'] = svg_path
if st.session_state['mermaid_code']:
st.markdown("---")
tab_visual, tab_code = st.tabs(["🖼️ Diagram Visualization", "📝 Edit Code"])
with tab_visual:
if st.session_state['generated_image_path'] and os.path.exists(st.session_state['generated_image_path']):
st.image(st.session_state['generated_image_path'], use_container_width=True)
with open(st.session_state['generated_image_path'], "rb") as file:
btn = st.download_button(
label="📥 Download SVG",
data=file,
file_name="flowchart.svg",
mime="image/svg+xml"
)
else:
st.warning("No image generated yet or generation failed.")
with tab_code:
st.info("You can edit the code below and press 'Update' to re-render.")
edited_code = st.text_area("Mermaid Code", st.session_state['mermaid_code'], height=300)
if st.button("🔄 Update Diagram"):
st.session_state['mermaid_code'] = edited_code
new_svg_path = render_diagram(edited_code)
if new_svg_path:
st.session_state['generated_image_path'] = new_svg_path
st.rerun() # Refresh to show new image
- Run the code 🏃➡️
streamlit run appST.py
You can now view your Streamlit app in your browser.
Local URL: http://localhost:8501
Network URL: http://192.168.1.148:8501
That’s a wrap, thanks for reading 😅
🎯 Conclusion: Empowering Local Diagram Generation
In summary, we’ve successfully developed a powerful, fully local and iterative application for generating Mermaid flowcharts using Ollama and local Large Language Models (LLMs). This process demonstrates that tasks such as diagram generation — can be achieved effectively and flexibly using open-source tools and local computation, entirely eliminating the need for subscription-based services.
Links
- GitHub Repository of the code: https://github.com/aairom/ollama-mermaid-architect





Top comments (0)