自動開発ツール、GPT-Engineer の入門記4。React , FastAPI 構成の todo アプリを生成する。

# 目次
1. はじめに
2. 実装に用いた GPT-Engineer のバージョンに関して
3. 実装のフロー
4. プロジェクトの作成
5. バックエンド生成と動作確認
6. フロントエンド生成と動作確認
7. 最後に

# はじめに
[前回記事](https://qiita.com/nassy20/items/c5ce9e1e2178b1f1bbb7)でパッケージ管理をカスタマイズできるようになりました。今回は React , FastAPI 構成の Todo アプリを生成に挑戦してみます。

また、こちらの記事に関しては続きものです。以下も適宜ご参照ください。

https://qiita.com/nassy20/items/963cd41e968f5840d7d9

https://qiita.com/nassy20/items/257c831aed10bdbeba09

https://qiita.com/nassy20/items/c5ce9e1e2178b1f1bbb7

# 実装に用いた GPT-Engineer のバージョンに関して
- gpt-engineer 0.2.5
- model: GPT-4

# 実装のフロー
GPT-Engineer で SPA を生成する際にバックエンド・フロントエンドを一度に生成しようとすると、上手くいかないことが多いです。バックエンド生成が上手くいくことを確認して、フロントエンド生成といった具合に、着実に追加していくが良いです。

```mermaid
graph LR
    A[バックエンド生成] --> B[バックエンド動作確認]
    B --> C[フロントエンド生成]
    C --> D[フロントエンドの動作確認]
    D --> E[要件追加]
```

# プロジェクトの作成

プロジェクトディレクトリを todo とします。
(プロジェクト名称は各自置き換えてください。)

## .env の用意
プロジェクトルート(GPT-Engineerの実行ディレクトリ)に .env ファイルを作成します。

```touch .env```

.env ファイルには OpenAI API key を設定します。

`OPENAI_API_KEY=<OPENAI_API_KEY>`

## prompt ファイルの作成

プロジェクトのルートにおいて、以下のコマンドで prompt ファイルを作成します。

`touch prompt`

## Preprompts のカスタマイズ

プロジェクトのルートにおいて、以下のコマンドで philosophy ファイルを作成します。

`mkdir -p preprompts && touch preprompts/philosophy`

[前回の記事](https://qiita.com/nassy20/items/c5ce9e1e2178b1f1bbb7)を参考にして、`philosophy` ファイルを以下のようにカスタマイズします。

差分
```diff
Almost always put different classes in different files.
Always use the programming language the user asks for.
- For Python, you always create an appropriate requirements.txt file.
+ Prepare a `pyproject.toml` file assuming the use of Poetry for a Python project, and do not prepare a requirements.txt file.
+ The package name in `pyproject.toml` should be the same as the project name.
+ Set the version in `pyproject.toml` to 0.1.0.
+ Set the compatible Python versions in `pyproject.toml` to `^3.11`.
+ List the necessary libraries in `[tool.poetry.dependencies]` of `pyproject.toml`.
For NodeJS, you always create an appropriate package.json file.
Always add a comment briefly describing the purpose of the function definition.
Add comments explaining very complex bits of logic.
Always follow the best practices for the requested languages for folder/file structure and how to package the project.
```

フルテキスト
```
Almost always put different classes in different files.
Always use the programming language the user asks for.
Prepare a `pyproject.toml` file assuming the use of Poetry for a Python project and Do not prepare requirements.txt file.
The Package name in `pyproject.toml` should be the same as the project name.
Set the Version in `pyproject.toml` to 0.1.0.
Set the Compatible Python versions in `pyproject.toml` to `^3.11`.
List the necessary libraries in `[tool.poetry.dependencies]` of `pyproject.toml`.
For NodeJS, you always create an appropriate package.json file.
Always add a comment briefly describing the purpose of the function definition.
Add comments explaining very complex bits of logic.
Always follow the best practices for the requested languages for folder/file structure and how to package the project.


Python toolbelt preferences:
- pytest
- dataclasses

```

この時点でプロジェクは以下のようになっています。

```

. (todo)
├── preprompts
│   └── philosophy
└── prompt
```

# バックエンド生成 と 動作確認

### プロンプト 記載

prompt ファイルに以下の仕様を記載します。

``` prompt
Todo を SPA の構成で作成してください。

# アプリケーションの仕様
## Todo 画面
- Todo の一覧を表示する。
- Todo の一覧は、タイトル、内容、期限、完了フラグを表示する。
- Todoの作成もできる。

# バックエンド構成
- バックエンドは FastAPI を利用する。
- api/app フォルダー下にアプリの実装をする。
- api フォルダ下に pyproject.toml を用意してください。
- pyproject.toml の [tool.poetry.dependencies] の各パッケージのバージョンは"*"としてください。
- api の起動は api フォルダ下で `uvicorn app.main:app --reload` で起動するようにしてください。
- api/app/main.py に リクエストのハンドラー(@router.getなど) を定義してください。
- api/app/schemas.py に api のレスポンスモデルを定義してください。
- api/app/database.py に sqlite の接続情報を定義してください。
- api/app/database.py に get_db 関数を定義してください。

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

- __init__.py には "# This is __init__.py" とコメントを入れてください。
- API 起動時に、sqliteのテーブルを作成する。
- sqlite は標準ライブラリなので、install は不要です。
- cors で全てを許可してください

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # すべてのオリジンを許可
    allow_credentials=True,
    allow_methods=["*"],  # すべての HTTP メソッドを許可
    allow_headers=["*"],  # すべてのヘッダーを許可
)
```

## 生成コマンドを実行する

プロジェクトのルートにおいて以下のコマンドを実行します。

`gpte . gpt-4-1106-preview --use-custom-preprompts`

`.` は現在ディレクトリを対象にする意味があります。
`gpt-4-1106-preview` は OpenAIのモデル名です。(省略した場合、gpt-4が利用可能です。)
`--use-custom-preprompts` は Preprompts をカスタマイズを利用する場合に必要なフラグです。

生成結果は以下のようになりました。

```
$ tree .
.
├── api
│   ├── app
│   │   ├── __init__.py
│   │   ├── database.py
│   │   ├── main.py
│   │   └── schemas.py
│   └── pyproject.toml
├── preprompts
│   ├── clarify
│   ├── entrypoint
│   ├── file_format
│   ├── file_format_fix
│   ├── generate
│   ├── improve
│   ├── philosophy
│   └── roadmap
├── prompt
└── run.sh
```

``` todo/api/pyproject.toml
[tool.poetry]
name = "todo_api"
version = "0.1.0"
description = "A simple Todo API"
authors = ["Your Name <you@example.com>"]
license = "MIT"

[tool.poetry.dependencies]
python = "^3.11"
fastapi = "*"
uvicorn = "*"
sqlalchemy = "*"
pydantic = "*"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
```

```todo/api/app/__init__.py
# This is __init__.py
```

```todo/api/app/database.py
from sqlalchemy import create_engine, Column, Integer, String, Boolean, DateTime
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, scoped_session

DATABASE_URL = "sqlite:///./test.db"

engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine))

Base = declarative_base()

class TodoModel(Base):
    __tablename__ = "todos"

    id = Column(Integer, primary_key=True, index=True)
    title = Column(String, index=True)
    content = Column(String, index=True)
    deadline = Column(DateTime)
    completed = Column(Boolean, default=False)

Base.metadata.create_all(bind=engine)

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()
```

```todo/api/app/main.py
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from . import schemas, database
from .database import TodoModel, get_db
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

@app.post("/todos/", response_model=schemas.Todo)
def create_todo(todo: schemas.TodoCreate, db: Session = Depends(get_db)):
    db_todo = TodoModel(title=todo.title, content=todo.content, deadline=todo.deadline)
    db.add(db_todo)
    db.commit()
    db.refresh(db_todo)
    return db_todo

@app.get("/todos/", response_model=list[schemas.Todo])
def read_todos(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
    todos = db.query(TodoModel).offset(skip).limit(limit).all()
    return todos

@app.get("/todos/{todo_id}", response_model=schemas.Todo)
def read_todo(todo_id: int, db: Session = Depends(get_db)):
    db_todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first()
    if db_todo is None:
        raise HTTPException(status_code=404, detail="Todo not found")
    return db_todo
```

```todo/api/app/schemas.py
from pydantic import BaseModel
from datetime import datetime
from typing import Optional

class TodoCreate(BaseModel):
    title: str
    content: str
    deadline: datetime

class Todo(TodoCreate):
    id: int
    completed: bool

    class Config:
        orm_mode = True
```

```todo/run.sh
# a) Install dependencies
cd api
poetry install

# b) Run all necessary parts of the codebase
poetry run uvicorn app.main:app --reload
```


## 動作確認をする

生成された run.sh を参考に、 FastAPI の server を立ち上げます。
http://localhost:8000/docs にアクセスをして、 SwaggerUI を立ち上げます。
todo エンドポイントの create や read を実行できる API を実行してみて、期待通りに動くか確かめます。

<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110482/a532befb-9d86-7fe5-ea66-b378ff6e050b.png">

また、sqlite データベースの中身を確認して、期待したデータの作成ができているかを確かめます。

# フロントエンド生成 と 動作確認
### create-react-app を実行する。

react アプリの全てを OpenAI で生成することは難しいようなので、先に create-react-app で土台を作成します。プロジェクトのルートで以下のコマンドを実行します。

`npx create-react-app gui --template typescript`

react アプリ server を立ち上げて動作確認をしましょう。

この時点でプロジェクトは以下のようになっている認識です。

```
$ tree . -L 2
.
├── api
│   ├── app
│   ├── poetry.lock
│   ├── pyproject.toml
│   └── test.db
├── gui
│   ├── README.md
│   ├── node_modules
│   ├── package-lock.json
│   ├── package.json
│   ├── public
│   ├── src
│   └── tsconfig.json
├── preprompts
│   ├── clarify
│   ├── entrypoint
│   ├── file_format
│   ├── file_format_fix
│   ├── generate
│   ├── improve
│   ├── philosophy
│   └── roadmap
├── prompt
└── run.sh

```

### プロンプト 記載

prompt ファイルを以下の内容に書き換えます。
<ここにopenapi.jsonの中身を書く> には、 SwaggerUI から出力した openapi.json の内容を貼り付けます。

```
Todo を SPA の構成で作成してください。

# アプリケーションの仕様
## Todo 画面
- Todo の一覧を表示する。
- Todo の一覧は、タイトル、内容、期限、完了フラグを表示する。
- Todoの作成もできる。

## バックエンドの実装
バックエンドの実装には手を加えないでください。

## フロントエンドの実装
- フロントエンドはTypescriptを利用してください。
- 追加実装は gui/src/App.tsx のみに実装を行ってください。
- api は localhost:8000 で起動しており、以下の仕様になっています。

<ここにopenapi.jsonの中身を書く>
```

## 生成コマンドを実行する

プロジェクトのルートにおいて以下のコマンドを実行します。

`gpte . gpt-4-1106-preview -i`

`-i` は既存ファイルに修正を行うモードです。

コマンドを実行すると、修正するファイルの選択を求められるので、gui/src/App.tsx を選択します。


生成結果は以下のようになりました。

```
gui/src/App.tsx
<<<<<<< HEAD
import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;
=======
import React, { useState, useEffect } from 'react';
import './App.css';

type Todo = {
  id: number;
  title: string;
  content: string;
  deadline: string;
  completed: boolean;
};

function App() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [newTodo, setNewTodo] = useState({ title: '', content: '', deadline: '' });

  useEffect(() => {
    fetch('http://localhost:8000/todos/')
      .then(response => response.json())
      .then(data => setTodos(data))
      .catch(error => console.error('Error fetching todos:', error));
  }, []);

  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setNewTodo({ ...newTodo, [event.target.name]: event.target.value });
  };

  const handleSubmit = (event: React.FormEvent) => {
    event.preventDefault();
    fetch('http://localhost:8000/todos/', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(newTodo),
    })
      .then(response => response.json())
      .then(data => {
        setTodos([...todos, data]);
        setNewTodo({ title: '', content: '', deadline: '' }); // Reset form
      })
      .catch(error => console.error('Error posting new todo:', error));
  };

  return (
    <div className="App">
      <header className="App-header">
        <h1>Todo List</h1>
        <form onSubmit={handleSubmit}>
          <input
            type="text"
            name="title"
            value={newTodo.title}
            onChange={handleInputChange}
            placeholder="Title"
            required
          />
          <input
            type="text"
            name="content"
            value={newTodo.content}
            onChange={handleInputChange}
            placeholder="Content"
            required
          />
          <input
            type="datetime-local"
            name="deadline"
            value={newTodo.deadline}
            onChange={handleInputChange}
            required
          />
          <button type="submit">Add Todo</button>
        </form>
        <ul>
          {todos.map(todo => (
            <li key={todo.id}>
              <h2>{todo.title}</h2>
              <p>{todo.content}</p>
              <p>Deadline: {todo.deadline}</p>
              <p>Completed: {todo.completed ? 'Yes' : 'No'}</p>
            </li>
          ))}
        </ul>
      </header>
    </div>
  );
}

export default App;
>>>>>>> updated
```

react server を立ち上げると、動作する todo アプリが立ち上がりました。

<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110482/4d2eb792-9a15-5c5e-b72c-3441f8dfdddb.png">

こちらで、一旦 todo アプリは完成とします🎉🎉
お疲れ様でした。以降、機能を追加するには、 prompt を書き換え ->  `gpte . gpt-4-1106-preview -i` を繰り返すことになります。

# 最後に
今後も GPT-Engineer や LLM 関連の記事を書いていくので、良かったらフォローといいね 👍 をお願い致します。