ChatGPTを使ってPlantUML形式のクラス図からGolangのコードを自動生成してみた

AIを使った自動生成を徹底的に行うためクラス図からコード生成をやってみた。

PlantUML形式のクラス図

クラス図これ。

@startuml

entity Prompt {
  contents string 

  get_next() void
}

interface AIClient {
  ask(promt Prompt) string
}

class ChatGPTClient {
  ask(promt Prompt) string
}

interface CodeRepository {
  load(key string) void
  save(key string, contents string) void
}

class FileIORepository {
  load(key string) void
  save(key string, contents string) void
}

class CodeOperator {
  generate(order string, key string) string
  request(promt Prompt) string
}


FileIORepository --|> CodeRepository

ChatGPTClient --|> AIClient
Prompt --- AIClient : ask

CodeOperator ..> AIClient
CodeOperator ..> CodeRepository
CodeOperator --- Prompt


@enduml

Generator.py

ChatGPTにプロンプトを投げるpythonスクリプトがこれ

import os
from openai import OpenAI

client = OpenAI(
    # This is the default and can be omitted
    api_key=os.environ.get("OPENAI_API_KEY"),
)

def call(content: str) -> str: 
    print ("Call GPT-4 with: ```" + content + "```")

    completion = client.chat.completions.create(
        messages=[
            {
                "role": "user",
                "content": content,
            }
        ],
        model="gpt-4",
    )

    return completion.choices[0].message.content



def load_file(file_name: str):
    with open(file_name, "r", encoding="utf-8") as f:
        return f.read()

def save_file(file_name: str, content: str):
    with open(file_name, "w", encoding="utf-8") as f:
        f.write(content)


class_diagram = load_file("my-class-diagrams.puml")


msg = '''\
    Generate code in Golang from class diagrams using PlantUML.

    - Separate the code for each class, entity, and interface.
    - Output the generated code in the following format.
    ```
    filename:{class name lowercase}
    {code}
    ```

    The class diagrams is below. 
'''

result = call(msg + class_diagram)
save_file("generated.go", result)

print ("Finished!")

出来たファイル

出来たファイルがこれ

filename:prompt.go
```golang
package main

type Prompt struct {
    Contents string
}

func (p *Prompt) get_next() {
}
```

filename:aiclient.go
```golang
package main

type AIClient interface {
    Ask(promt Prompt) string
}
```

filename:chatgptclient.go
```golang
package main

type ChatGPTClient struct {
}

func (c *ChatGPTClient) Ask(promt Prompt) string {
    return ""
}
```

filename:coderepository.go
```golang
package main

type CodeRepository interface {
    Load(key string)
    Save(key string, contents string)
}
```

filename:fileiorepository.go
```golang
package main

type FileIORepository struct {
}

func (f *FileIORepository) Load(key string) {
}

func (f *FileIORepository) Save(key string, contents string) {
}
```

filename:codeoperator.go
```golang
package main

type CodeOperator struct {
}

func (co *CodeOperator) Generate(order string, key string) string {
    return ""
}

func (co *CodeOperator) Request(promt Prompt) string {
    return ""
}
```

separator.py

手動で分割するのが嫌だったので分割するスクリプトを書いた。これ。

import re
import os
import asyncio

with open("generated.go", 'r') as file:
    lines = file.readlines()

code_block = False
for line in lines:
    # ファイル名の検出
    if re.match(r'filename:(.+\.go)', line):
        file_name_match = re.match(r'filename:(.+\.go)', line)
        new_file_name = file_name_match.group(1).strip()
        code_block = True
        code = ''
        continue

    if line.strip() == '```golang':
        continue

    # コードブロックの終了を検出
    if line.strip() == '```' and code_block:
        code_block = False
        with open(new_file_name, 'w') as new_file:
            new_file.write(code)
        continue

    # コードブロックの中のテキストを追加
    if code_block:
        code += line

書いたというか大半ChatGPTとCopilotに書かせて自分は軽く手直ししただけなのだが。

結果

こんな感じのファイルが生成された。

package main

type CodeOperator struct {
}

func (co *CodeOperator) Generate(order string, key string) string {
    return ""
}

func (co *CodeOperator) Request(promt Prompt) string {
    return ""
}

所感

悪くないんだがこれだけだと所詮足場にすぎないし、あまりに不徹底。この程度で終わりでは話にならない。結局クラス図は処理の情報がないから処理部分が書けない。

しかし知見は得られた。

  1. 手間のかかるプロンプト作成を予めスクリプトに仕込んでおくと再利用性を期待できる。開発に必要なスクリプトはある程度決まっているものも多かろうし、ポリシー、プロシージャ、そしてプロンプトを最初から用意しておくと相当に効果的であろう。
  2. ファイルのRead/WriteとChatGPT APIを組み合わせることで、コピペではできない速度の開発ができる。msレベルの速度は必要ないし(っていうかどうせAPIの呼び出しで時間がかかるので非同期とパラレルができれば大差ない)pythonが楽。
  3. クラス図からでは生成できるものがちゃちすぎる。詳細設計レベルまで落とし込む必要がある。