こんにちは。気づいたら新卒 2 年目の statiolake です。人間がやらなくていいことは極力プログラムにやらせようと思って日々を過ごしています。
さて、最近関わっている業務の中で、draw.io を利用してかかれたフロー図をコードの修正に応じてメンテナンスするというタスクが発生しました。draw.io はこういう図を書くのに本当に便利ではあるのですが、だとしても後からステップや条件分岐を足したりするのは面倒です。正直コードを書く労力の 10 倍くらい大変な作業に感じました。この手間をなんとかなくしたいと考え、draw.io は難しくてもテキストベースの Mermaid であれば Python から自動生成することも可能なのではないか? と思い至り、色々試してみたというエントリです。
忙しい方向け
Python のコードを Mermaid に変換するツールを作りました。コードは以下のリポジトリに置いています。
また、このツールを Web から使うことができるようにもしました。以下のリンクから覗いてみてください。
Mermaid とは
Mermaid はテキストベースで色々なグラフがかけるツールです。その中にはフローチャートもあります。例えば以下のようなテキストを書いたとしましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
flowchart TD; A("Begin: sum_numbers"); B("End: sum_numbers"); C["total = 0"]; G("return: total"); subgraph "Begin: for num in numbers" D[/"Begin: for num in numbers"\]; E[\"End: for num in numbers"/]; F["total += num"]; end A --> C; E --> G; G --> B; C --> D; D --> F; F --> E; |
ここから以下のようなフローチャートを表示できます。
Mermaid は Notion や GitHub との相性もいいです。Markdown のコードブロックに上の Mermaid のコードを書くだけで、自動的にフローチャートが表示されるようになります。
本題: Python から Mermaid に変換したい
広く使われている言語であるところの Python からこんなに便利な Mermaid に変換する機能、需要もありそうですし、探せばどこかにはあるだろうと思っていました。しかし意外にも Google で検索した限りでは見つかりませんでした。となると何らかの方法で自分で用意する必要があります。
生成 AI の力を頼ってみる
現代なので、とりあえずこういう汎用的なタスクには生成 AI が使えるのではないかと考えるのが常です。実際、Python から Rust に書き換えるなど、プログラミング言語間の変換は割と正しくやってくれるイメージです。というわけで、同じように Python から Mermaid のコードに変換するようお願いしてみました。
今回使用した Python のサンプルコードは以下のコードです。いい感じにループや条件分岐があってよさそうな感じなので採用させていただきました。生成 AI によって生成された (ものを一部手直しした) 、ブラックジャックのスコア計算っぽい関数です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
def calculate_score(self, hand): score = 0 aces = 0 for card in hand: if card == 'Ace': aces += 1 score += 11 elif card in ['Jack', 'Queen', 'King']: score += 10 else: score += int(card) while aces > 0 and score > 21: score -= 10 aces -= 1 return score |
で、まずこれを Claude 3 Haiku に投げてみました。プロンプトにはあまり凝っていません。それで生成されたのがこちら。
1 2 3 4 5 6 7 8 9 10 |
flowchart start[開始] --> init[score = 0, aces = 0] init --> loop{"card in hand"} loop -->|"card == 'Ace'"| a["aces += 1, score += 11"] loop -->|"card in ['Jack', 'Queen', 'King']"| b["score += 10"] loop -->|else| c["score += int(card)"] loop --> d{"aces > 0 and score > 21"} d -->|true| e["score -= 10, aces -= 1"] d -->|false| return["return score"] e --> d |
うーん、雰囲気は感じますが、フローチャートではないですね。突然矢印が途切れていますし、これを見せられたらより混乱してしまうような気がします。まだコードを直接読んだほうが簡単ではないでしょうか。ちなみに出てきたものをそのまま与えると syntax error になってしまい、手動での手直しも必要でした。
もう一つ、GPT-4o にも聞いてみました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
flowchart TD A[Start] --> B{カードが残っているか?} B -- Yes --> C[カードを取得] C --> D{カードは'Ace'か?} D -- Yes --> E[acesに1を加算<br>scoreに11を加算] D -- No --> F{カードは'Jack', 'Queen', 'King'か?} F -- Yes --> G[scoreに10を加算] F -- No --> H[scoreにカードの数値を加算] E --> B G --> B H --> B B -- No --> I{aces > 0 and score > 21か?} I -- Yes --> J[scoreから10を減算<br>acesを1減らす] J --> I I -- No --> K[scoreを返す] K --> L[End] |
さすがというべきか、正しい処理フローは抑えています。ただこれもフローチャートとしては見づらいです。これも結局元のコードのほうが読みやすい気がします。
まあ、フローが間違っていない以上、GPT-4o 側というよりはどちらかというと Mermaid 側のレイアウト計算にとって難しいという問題なのかもしれません。だとしてもループなどの制御構造についてはもう少し見やすいほうがありがたく、まるで goto 文を見ているかのようです。
諦めて自力で実装してみた
結局生成 AI にそのまま聞くだけでは満足行く成果は得られませんでした。冷静に考えると元々が機械可読な Python のコードなわけですから、機械的に変換してしまったほうがよいかもしれません。こんな感じのステップでいけるはずです。
Rust に Python のパーサーがあったことが決め手となり、実装に踏み切りました。そしてその完成したツールがこちらです。
これを使って生成したフローチャートが以下です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
flowchart TD; A("Begin: calculate_score"); B("End: calculate_score"); C["score = 0"]; D["aces = 0"]; Q("return: score"); subgraph "Begin: for card in hand" E[/"Begin: for card in hand"\]; F[\"End: for card in hand"/]; G{"if: card == 'Ace' ?"}; H["aces += 1"]; I["score += 11"]; J{"if: card in ['Jack', 'Queen', 'King'] ?"}; K["score += 10"]; L["score += int(card)"]; end subgraph "Begin: while aces > 0 and score > 21" M[/"Begin: while aces > 0 and score > 21"\]; N[\"End: while aces > 0 and score > 21"/]; O["score -= 10"]; P["aces -= 1"]; end A --> C; C --> D; N --> Q; Q --> B; D --> E; E --> G; G -->|"T"| H; H --> I; G -->|"F"| J; J -->|"T"| K; J -->|"F"| L; I --> F; K --> F; L --> F; F --> M; M --> O; O --> P; P --> N; |
どうでしょう。個人的には一番きれいに生成できているのではないかと思います。また、生成 AI と違ってこのプログラムは何回実行しても同じフローチャートを返します。スタンドアロンなプログラムなので CI 環境下でも使えます。つまり… GitHub Actions に仕込めば、変更を自動的にフローチャートに反映させることも可能です。夢が広がりますね。
また、ここまででも結構いい感じなのですがもう一歩、この段階まで用意してあげてから生成 AI にかけると、構造を維持しながらラベルをもう少しわかりやすくすることもできます。あるいはもっとうまくプロンプトを指定してあげれば、冗長な部分を削ったり適当な粒度でステップを結合してくれたりもするかもしれません。そこまでできたらもう手動でフロー図を書く必要はないと言っても良さそうです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
flowchart TD; A("計算開始: 得点計算"); B("計算終了: 得点計算"); C["得点 = 0"]; D["エース = 0"]; Q("戻り値: 得点"); subgraph "開始: 手札の各カードについて" E[/"開始: 手札の各カードについて"\]; F[\"終了: 手札の各カードについて"/]; G{"もしカードがエースであれば ?"}; H["エース数を 1 増やす"]; I["得点を 11 加算"]; J{"もしカードがジャック、クイーン、キングであれば ?"}; K["得点を 10 加算"]; L["得点にカードの数値を加算"]; end subgraph "開始: エース数が 0より大きく、得点が 21より大きい間" M[/"開始: エース数が 0より大きく、得点が 21より大きい間"\]; N[\"終了: エース数が 0より大きく、得点が 21より大きい間"/]; O["得点を 10 減算"]; P["エース数を 1 減算"]; end A --> C; C --> D; N --> Q; Q --> B; D --> E; E --> G; G -->|"はい"| H; H --> I; G -->|"いいえ"| J; J -->|"はい"| K; J -->|"いいえ"| L; I --> F; K --> F; L --> F; F --> M; M --> O; O --> P; P --> N; |
なお、今後の余裕があればラベルをソースコード側でカスタマイズできるようにする機能追加も考えています。
おまけ: 手軽に試してみたいあなたに
せっかくなので、このツールを気軽に試せるよう、GitHub Pages を使って Web 化しました。
お手元の Python ソースコードをつっこんで遊んでやってください。
…ところでコアロジックは Rust で書かれています。GitHub Pages は静的サイトホスティングサービスなので、裏で Rust の API サーバーが動いているわけではありません。それではこの Web ページはどうやって変換処理を実行しているのでしょうか?
実はこの Web ページでは WebAssembly にコンパイルされた Rust のコードがあなたのパソコンの中で動いています!
今回のフロントエンドは GitHub Actions + SvelteKit (adapter-static) + Rust WebAssembly で構成されています。びっくりするほど簡単に Rust のプログラムをブラウザで動かすことができたので、もしかしたら次の記事で紹介するかもしれません。
終わりに
今回は Python のコードを Mermaid のフローチャートに変換するツールを実装しました。こういったルールベースの厳密な変換処理はただ適当に生成 AI に投げるだけでは厳しく、まだまだ手書きしたほうが強い部分もありそうです。また、確率的に文言を生成する AI の仕組み上、一定の品質で決定的な結果を得ることも難しいです。
ただし、ラベルの調整部分は AI の得意分野で、非常にわかりやすいフロー図にブラッシュアップしてくれました。些細な例ですが、このように他の手法と AI による手法を組み合わせることで質が大きく向上する例はいくらでもありそうです。AI をただ適当にそのまま使うのではなく、うまく組み合わせて更に高い価値を生み出すことができるように精進していきたいと感じました。