jensinjames commited on
Commit
66214f3
1 Parent(s): ed21180

Upload 9 files

Browse files
Files changed (9) hide show
  1. __init__.py +0 -0
  2. ai.py +63 -0
  3. chat_to_files.py +42 -0
  4. ci.yaml +32 -0
  5. db.py +43 -0
  6. main.py +65 -0
  7. pre-commit.yaml +14 -0
  8. release.yaml +52 -0
  9. steps.py +278 -0
__init__.py ADDED
File without changes
ai.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+
5
+ import openai
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ class AI:
11
+ def __init__(self, model="gpt-4", temperature=0.1):
12
+ self.temperature = temperature
13
+
14
+ try:
15
+ openai.Model.retrieve(model)
16
+ self.model = model
17
+ except openai.InvalidRequestError:
18
+ print(
19
+ f"Model {model} not available for provided API key. Reverting "
20
+ "to gpt-3.5-turbo. Sign up for the GPT-4 wait list here: "
21
+ "https://openai.com/waitlist/gpt-4-api"
22
+ )
23
+ self.model = "gpt-3.5-turbo"
24
+
25
+ def start(self, system, user):
26
+ messages = [
27
+ {"role": "system", "content": system},
28
+ {"role": "user", "content": user},
29
+ ]
30
+
31
+ return self.next(messages)
32
+
33
+ def fsystem(self, msg):
34
+ return {"role": "system", "content": msg}
35
+
36
+ def fuser(self, msg):
37
+ return {"role": "user", "content": msg}
38
+
39
+ def fassistant(self, msg):
40
+ return {"role": "assistant", "content": msg}
41
+
42
+ def next(self, messages: list[dict[str, str]], prompt=None):
43
+ if prompt:
44
+ messages += [{"role": "user", "content": prompt}]
45
+
46
+ logger.debug(f"Creating a new chat completion: {messages}")
47
+ response = openai.ChatCompletion.create(
48
+ messages=messages,
49
+ stream=True,
50
+ model=self.model,
51
+ temperature=self.temperature,
52
+ )
53
+
54
+ chat = []
55
+ for chunk in response:
56
+ delta = chunk["choices"][0]["delta"]
57
+ msg = delta.get("content", "")
58
+ print(msg, end="")
59
+ chat.append(msg)
60
+ print()
61
+ messages += [{"role": "assistant", "content": "".join(chat)}]
62
+ logger.debug(f"Chat completion finished: {messages}")
63
+ return messages
chat_to_files.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+
3
+
4
+ def parse_chat(chat): # -> List[Tuple[str, str]]:
5
+ # Get all ``` blocks and preceding filenames
6
+ regex = r"(\S+)\n\s*```[^\n]*\n(.+?)```"
7
+ matches = re.finditer(regex, chat, re.DOTALL)
8
+
9
+ files = []
10
+ for match in matches:
11
+ # Strip the filename of any non-allowed characters and convert / to \
12
+ path = re.sub(r'[<>"|?*]', "", match.group(1))
13
+
14
+ # Remove leading and trailing brackets
15
+ path = re.sub(r"^\[(.*)\]$", r"\1", path)
16
+
17
+ # Remove leading and trailing backticks
18
+ path = re.sub(r"^`(.*)`$", r"\1", path)
19
+
20
+ # Remove trailing ]
21
+ path = re.sub(r"\]$", "", path)
22
+
23
+ # Get the code
24
+ code = match.group(2)
25
+
26
+ # Add the file to the list
27
+ files.append((path, code))
28
+
29
+ # Get all the text before the first ``` block
30
+ readme = chat.split("```")[0]
31
+ files.append(("README.md", readme))
32
+
33
+ # Return the files
34
+ return files
35
+
36
+
37
+ def to_files(chat, workspace):
38
+ workspace["all_output.txt"] = chat
39
+
40
+ files = parse_chat(chat)
41
+ for file_name, file_content in files:
42
+ workspace[file_name] = file_content
ci.yaml ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Pytest Execution
2
+ on:
3
+ pull_request:
4
+ branches:
5
+ - main
6
+ push:
7
+ branches:
8
+ - main
9
+
10
+ jobs:
11
+ test:
12
+ runs-on: ubuntu-latest
13
+ strategy:
14
+ matrix:
15
+ python-version:
16
+ - "3.10"
17
+ steps:
18
+ - uses: actions/checkout@v3
19
+
20
+ - uses: actions/setup-python@v4
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+ cache: pip
24
+
25
+ - name: Install package
26
+ run: pip install -e .
27
+
28
+ - name: Install test runner
29
+ run: pip install pytest pytest-cov
30
+
31
+ - name: Run unit tests
32
+ run: pytest --cov=gpt_engineer
db.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from pathlib import Path
3
+
4
+
5
+ # This class represents a simple database that stores its data as files in a directory.
6
+ class DB:
7
+ """A simple key-value store, where keys are filenames and values are file contents."""
8
+
9
+ def __init__(self, path):
10
+ self.path = Path(path).absolute()
11
+
12
+ self.path.mkdir(parents=True, exist_ok=True)
13
+
14
+ def __contains__(self, key):
15
+ return (self.path / key).is_file()
16
+
17
+ def __getitem__(self, key):
18
+ full_path = self.path / key
19
+
20
+ if not full_path.is_file():
21
+ raise KeyError(key)
22
+ with full_path.open("r", encoding="utf-8") as f:
23
+ return f.read()
24
+
25
+ def __setitem__(self, key, val):
26
+ full_path = self.path / key
27
+ full_path.parent.mkdir(parents=True, exist_ok=True)
28
+
29
+ if isinstance(val, str):
30
+ full_path.write_text(val, encoding="utf-8")
31
+ else:
32
+ # If val is neither a string nor bytes, raise an error.
33
+ raise TypeError("val must be either a str or bytes")
34
+
35
+
36
+ # dataclass for all dbs:
37
+ @dataclass
38
+ class DBs:
39
+ memory: DB
40
+ logs: DB
41
+ preprompts: DB
42
+ input: DB
43
+ workspace: DB
main.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import logging
3
+ import shutil
4
+
5
+ from pathlib import Path
6
+
7
+ import typer
8
+
9
+ from gpt_engineer import steps
10
+ from gpt_engineer.ai import AI
11
+ from gpt_engineer.db import DB, DBs
12
+ from gpt_engineer.steps import STEPS
13
+
14
+ app = typer.Typer()
15
+
16
+
17
+ @app.command()
18
+ def main(
19
+ project_path: str = typer.Argument("example", help="path"),
20
+ delete_existing: bool = typer.Argument(False, help="delete existing files"),
21
+ model: str = "gpt-4",
22
+ temperature: float = 0.1,
23
+ steps_config: steps.Config = typer.Option(
24
+ steps.Config.DEFAULT, "--steps", "-s", help="decide which steps to run"
25
+ ),
26
+ verbose: bool = typer.Option(False, "--verbose", "-v"),
27
+ run_prefix: str = typer.Option(
28
+ "",
29
+ help=(
30
+ "run prefix, if you want to run multiple variants of the same project and "
31
+ "later compare them"
32
+ ),
33
+ ),
34
+ ):
35
+ logging.basicConfig(level=logging.DEBUG if verbose else logging.INFO)
36
+
37
+ input_path = Path(project_path).absolute()
38
+ memory_path = input_path / f"{run_prefix}memory"
39
+ workspace_path = input_path / f"{run_prefix}workspace"
40
+
41
+ if delete_existing:
42
+ # Delete files and subdirectories in paths
43
+ shutil.rmtree(memory_path, ignore_errors=True)
44
+ shutil.rmtree(workspace_path, ignore_errors=True)
45
+
46
+ ai = AI(
47
+ model=model,
48
+ temperature=temperature,
49
+ )
50
+
51
+ dbs = DBs(
52
+ memory=DB(memory_path),
53
+ logs=DB(memory_path / "logs"),
54
+ input=DB(input_path),
55
+ workspace=DB(workspace_path),
56
+ preprompts=DB(Path(__file__).parent / "preprompts"),
57
+ )
58
+
59
+ for step in STEPS[steps_config]:
60
+ messages = step(ai, dbs)
61
+ dbs.logs[step.__name__] = json.dumps(messages)
62
+
63
+
64
+ if __name__ == "__main__":
65
+ app()
pre-commit.yaml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: pre-commit
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [main]
7
+
8
+ jobs:
9
+ pre-commit:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v3
13
+ - uses: actions/setup-python@v4
14
+ - uses: pre-commit/[email protected]
release.yaml ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Build and publish Python packages to PyPI
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ release:
6
+ types:
7
+ - published
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version:
15
+ - "3.10"
16
+ steps:
17
+ - uses: actions/checkout@v3
18
+
19
+ - uses: actions/setup-python@v4
20
+ with:
21
+ python-version: ${{ matrix.python-version }}
22
+ cache: pip
23
+
24
+ - name: Install build tool
25
+ run: pip install build
26
+
27
+ - name: Build package
28
+ run: python -m build
29
+
30
+ - name: Upload package as build artifact
31
+ uses: actions/upload-artifact@v3
32
+ with:
33
+ name: package
34
+ path: dist/
35
+
36
+ publish:
37
+ runs-on: ubuntu-latest
38
+ needs: build
39
+ environment:
40
+ name: pypi
41
+ url: https://pypi.org/p/gpt-engineer
42
+ permissions:
43
+ id-token: write
44
+ steps:
45
+ - name: Collect packages to release
46
+ uses: actions/download-artifact@v3
47
+ with:
48
+ name: package
49
+ path: dist/
50
+
51
+ - name: Publish packages to PyPI
52
+ uses: pypa/gh-action-pypi-publish@release/v1
steps.py ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import re
3
+ import subprocess
4
+
5
+ from enum import Enum
6
+ from typing import Callable, TypeVar
7
+
8
+ from gpt_engineer.ai import AI
9
+ from gpt_engineer.chat_to_files import to_files
10
+ from gpt_engineer.db import DBs
11
+
12
+
13
+ def setup_sys_prompt(dbs):
14
+ return (
15
+ dbs.preprompts["generate"] + "\nUseful to know:\n" + dbs.preprompts["philosophy"]
16
+ )
17
+
18
+
19
+ Step = TypeVar("Step", bound=Callable[[AI, DBs], list[dict]])
20
+
21
+
22
+ def simple_gen(ai: AI, dbs: DBs):
23
+ """Run the AI on the main prompt and save the results"""
24
+ messages = ai.start(
25
+ setup_sys_prompt(dbs),
26
+ dbs.input["main_prompt"],
27
+ )
28
+ to_files(messages[-1]["content"], dbs.workspace)
29
+ return messages
30
+
31
+
32
+ def clarify(ai: AI, dbs: DBs):
33
+ """
34
+ Ask the user if they want to clarify anything and save the results to the workspace
35
+ """
36
+ messages = [ai.fsystem(dbs.preprompts["qa"])]
37
+ user = dbs.input["main_prompt"]
38
+ while True:
39
+ messages = ai.next(messages, user)
40
+
41
+ if messages[-1]["content"].strip().lower().startswith("no"):
42
+ break
43
+
44
+ print()
45
+ user = input('(answer in text, or "c" to move on)\n')
46
+ print()
47
+
48
+ if not user or user == "c":
49
+ break
50
+
51
+ user += (
52
+ "\n\n"
53
+ "Is anything else unclear? If yes, only answer in the form:\n"
54
+ "{remaining unclear areas} remaining questions.\n"
55
+ "{Next question}\n"
56
+ 'If everything is sufficiently clear, only answer "no".'
57
+ )
58
+
59
+ print()
60
+ return messages
61
+
62
+
63
+ def gen_spec(ai: AI, dbs: DBs):
64
+ """
65
+ Generate a spec from the main prompt + clarifications and save the results to
66
+ the workspace
67
+ """
68
+ messages = [
69
+ ai.fsystem(setup_sys_prompt(dbs)),
70
+ ai.fsystem(f"Instructions: {dbs.input['main_prompt']}"),
71
+ ]
72
+
73
+ messages = ai.next(messages, dbs.preprompts["spec"])
74
+
75
+ dbs.memory["specification"] = messages[-1]["content"]
76
+
77
+ return messages
78
+
79
+
80
+ def respec(ai: AI, dbs: DBs):
81
+ messages = json.loads(dbs.logs[gen_spec.__name__])
82
+ messages += [ai.fsystem(dbs.preprompts["respec"])]
83
+
84
+ messages = ai.next(messages)
85
+ messages = ai.next(
86
+ messages,
87
+ (
88
+ "Based on the conversation so far, please reiterate the specification for "
89
+ "the program. "
90
+ "If there are things that can be improved, please incorporate the "
91
+ "improvements. "
92
+ "If you are satisfied with the specification, just write out the "
93
+ "specification word by word again."
94
+ ),
95
+ )
96
+
97
+ dbs.memory["specification"] = messages[-1]["content"]
98
+ return messages
99
+
100
+
101
+ def gen_unit_tests(ai: AI, dbs: DBs):
102
+ """
103
+ Generate unit tests based on the specification, that should work.
104
+ """
105
+ messages = [
106
+ ai.fsystem(setup_sys_prompt(dbs)),
107
+ ai.fuser(f"Instructions: {dbs.input['main_prompt']}"),
108
+ ai.fuser(f"Specification:\n\n{dbs.memory['specification']}"),
109
+ ]
110
+
111
+ messages = ai.next(messages, dbs.preprompts["unit_tests"])
112
+
113
+ dbs.memory["unit_tests"] = messages[-1]["content"]
114
+ to_files(dbs.memory["unit_tests"], dbs.workspace)
115
+
116
+ return messages
117
+
118
+
119
+ def gen_clarified_code(ai: AI, dbs: DBs):
120
+ # get the messages from previous step
121
+
122
+ messages = json.loads(dbs.logs[clarify.__name__])
123
+
124
+ messages = [
125
+ ai.fsystem(setup_sys_prompt(dbs)),
126
+ ] + messages[1:]
127
+ messages = ai.next(messages, dbs.preprompts["use_qa"])
128
+
129
+ to_files(messages[-1]["content"], dbs.workspace)
130
+ return messages
131
+
132
+
133
+ def gen_code(ai: AI, dbs: DBs):
134
+ # get the messages from previous step
135
+
136
+ messages = [
137
+ ai.fsystem(setup_sys_prompt(dbs)),
138
+ ai.fuser(f"Instructions: {dbs.input['main_prompt']}"),
139
+ ai.fuser(f"Specification:\n\n{dbs.memory['specification']}"),
140
+ ai.fuser(f"Unit tests:\n\n{dbs.memory['unit_tests']}"),
141
+ ]
142
+ messages = ai.next(messages, dbs.preprompts["use_qa"])
143
+ to_files(messages[-1]["content"], dbs.workspace)
144
+ return messages
145
+
146
+
147
+ def execute_entrypoint(ai, dbs):
148
+ command = dbs.workspace["run.sh"]
149
+
150
+ print("Do you want to execute this code?")
151
+ print()
152
+ print(command)
153
+ print()
154
+ print('If yes, press enter. Otherwise, type "no"')
155
+ print()
156
+ if input() not in ["", "y", "yes"]:
157
+ print("Ok, not executing the code.")
158
+ return []
159
+ print("Executing the code...")
160
+ print(
161
+ "\033[92m" # green color
162
+ + "Note: If it does not work as expected, please consider running the code'"
163
+ + " in another way than above."
164
+ + "\033[0m"
165
+ )
166
+ print()
167
+ subprocess.run("bash run.sh", shell=True, cwd=dbs.workspace.path)
168
+ return []
169
+
170
+
171
+ def gen_entrypoint(ai, dbs):
172
+ messages = ai.start(
173
+ system=(
174
+ "You will get information about a codebase that is currently on disk in "
175
+ "the current folder.\n"
176
+ "From this you will answer with code blocks that includes all the necessary "
177
+ "unix terminal commands to "
178
+ "a) install dependencies "
179
+ "b) run all necessary parts of the codebase (in parallell if necessary).\n"
180
+ "Do not install globally. Do not use sudo.\n"
181
+ "Do not explain the code, just give the commands.\n"
182
+ "Do not use placeholders, use example values (like . for a folder argument) "
183
+ "if necessary.\n"
184
+ ),
185
+ user="Information about the codebase:\n\n" + dbs.workspace["all_output.txt"],
186
+ )
187
+ print()
188
+
189
+ regex = r"```\S*\n(.+?)```"
190
+ matches = re.finditer(regex, messages[-1]["content"], re.DOTALL)
191
+ dbs.workspace["run.sh"] = "\n".join(match.group(1) for match in matches)
192
+ return messages
193
+
194
+
195
+ def use_feedback(ai: AI, dbs: DBs):
196
+ messages = [
197
+ ai.fsystem(setup_sys_prompt(dbs)),
198
+ ai.fuser(f"Instructions: {dbs.input['main_prompt']}"),
199
+ ai.fassistant(dbs.workspace["all_output.txt"]),
200
+ ai.fsystem(dbs.preprompts["use_feedback"]),
201
+ ]
202
+ messages = ai.next(messages, dbs.input["feedback"])
203
+ to_files(messages[-1]["content"], dbs.workspace)
204
+ return messages
205
+
206
+
207
+ def fix_code(ai: AI, dbs: DBs):
208
+ code_output = json.loads(dbs.logs[gen_code.__name__])[-1]["content"]
209
+ messages = [
210
+ ai.fsystem(setup_sys_prompt(dbs)),
211
+ ai.fuser(f"Instructions: {dbs.input['main_prompt']}"),
212
+ ai.fuser(code_output),
213
+ ai.fsystem(dbs.preprompts["fix_code"]),
214
+ ]
215
+ messages = ai.next(messages, "Please fix any errors in the code above.")
216
+ to_files(messages[-1]["content"], dbs.workspace)
217
+ return messages
218
+
219
+
220
+ class Config(str, Enum):
221
+ DEFAULT = "default"
222
+ BENCHMARK = "benchmark"
223
+ SIMPLE = "simple"
224
+ TDD = "tdd"
225
+ TDD_PLUS = "tdd+"
226
+ CLARIFY = "clarify"
227
+ RESPEC = "respec"
228
+ EXECUTE_ONLY = "execute_only"
229
+ USE_FEEDBACK = "use_feedback"
230
+
231
+
232
+ # Different configs of what steps to run
233
+ STEPS = {
234
+ Config.DEFAULT: [
235
+ clarify,
236
+ gen_clarified_code,
237
+ gen_entrypoint,
238
+ execute_entrypoint,
239
+ ],
240
+ Config.BENCHMARK: [simple_gen, gen_entrypoint],
241
+ Config.SIMPLE: [simple_gen, gen_entrypoint, execute_entrypoint],
242
+ Config.TDD: [
243
+ gen_spec,
244
+ gen_unit_tests,
245
+ gen_code,
246
+ gen_entrypoint,
247
+ execute_entrypoint,
248
+ ],
249
+ Config.TDD_PLUS: [
250
+ gen_spec,
251
+ gen_unit_tests,
252
+ gen_code,
253
+ fix_code,
254
+ gen_entrypoint,
255
+ execute_entrypoint,
256
+ ],
257
+ Config.CLARIFY: [
258
+ clarify,
259
+ gen_clarified_code,
260
+ gen_entrypoint,
261
+ execute_entrypoint,
262
+ ],
263
+ Config.RESPEC: [
264
+ gen_spec,
265
+ respec,
266
+ gen_unit_tests,
267
+ gen_code,
268
+ fix_code,
269
+ gen_entrypoint,
270
+ execute_entrypoint,
271
+ ],
272
+ Config.USE_FEEDBACK: [use_feedback, gen_entrypoint, execute_entrypoint],
273
+ Config.EXECUTE_ONLY: [gen_entrypoint, execute_entrypoint],
274
+ }
275
+
276
+ # Future steps that can be added:
277
+ # run_tests_and_fix_files
278
+ # execute_entrypoint_and_fix_files_if_needed