Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rebase of @leomariga's Existing Project #578

Merged
merged 46 commits into from
Aug 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
4c09545
Clarify missing file error
leomariga Jun 24, 2023
df2a78d
Merge branch 'main' of https://github.com/leomariga/gpt-engineer into…
leomariga Jul 1, 2023
43d67c8
Add strategy to fix existing code in computer
leomariga Jul 1, 2023
80791c5
add comments and remove debug prints
leomariga Jul 1, 2023
11adb98
Format using black
leomariga Jul 1, 2023
a8ea0d0
lint with ruff
leomariga Jul 1, 2023
493d63b
add new line at end of file
leomariga Jul 1, 2023
5c3a034
Fix method naming convention and other reviewing details
leomariga Jul 2, 2023
5505ec4
Fix identation and typos
leomariga Jul 3, 2023
0308d4e
Improve code with .gpteng folder, add file selection window with tkinter
leomariga Jul 9, 2023
c733d72
add file selector using terminal
leomariga Jul 11, 2023
387badb
fix typos and pep8 case rules, change workspace to cwd
leomariga Jul 16, 2023
14e1ff7
Added better functionality to file selector input, fixed some bugs, m…
lectair Jul 11, 2023
5615abc
Delete requirements.txt
lectair Jul 19, 2023
ef689b6
Delete file_list.txt
lectair Jul 19, 2023
912bd74
Merge branch 'main' into improve-existing-codebase
lectair Jul 19, 2023
301911d
Merge pull request #1 from lectair/improve-existing-codebase
leomariga Jul 21, 2023
2425610
fix typing for max and min value in range
leomariga Jul 21, 2023
64b4327
Add strategy to fix existing code in computer
leomariga Jul 1, 2023
ca5b806
add comments and remove debug prints
leomariga Jul 1, 2023
c878b0f
Format using black
leomariga Jul 1, 2023
6aea25d
lint with ruff
leomariga Jul 1, 2023
f80ff8e
add new line at end of file
leomariga Jul 1, 2023
4e4a9e2
Fix method naming convention and other reviewing details
leomariga Jul 2, 2023
78fcde1
Fix identation and typos
leomariga Jul 3, 2023
8d390e3
Improve code with .gpteng folder, add file selection window with tkinter
leomariga Jul 9, 2023
214fb77
add file selector using terminal
leomariga Jul 11, 2023
39599b6
Added better functionality to file selector input, fixed some bugs, m…
lectair Jul 11, 2023
8ad46a3
Delete requirements.txt
lectair Jul 19, 2023
0f9bbea
Delete file_list.txt
lectair Jul 19, 2023
d46c3f6
fix typos and pep8 case rules, change workspace to cwd
leomariga Jul 16, 2023
eead934
fix typing for max and min value in range
leomariga Jul 21, 2023
2d869b3
Rebased from a recent version.
pbharrin Aug 9, 2023
d1e33b6
Merge branch 'main' of github.com:pbharrin/gpt-engineer
pbharrin Aug 9, 2023
3128673
During existing file mode, the file section options start with 1 not …
pbharrin Aug 9, 2023
8a9eb33
fixed rebase bugs overwritten by automerge
pbharrin Aug 9, 2023
2a529be
In Existing Code mode the default selection for files is file_list.tx…
pbharrin Aug 9, 2023
f689094
added additional formatting to improve_existing_code() so that the ou…
pbharrin Aug 10, 2023
76c95b5
Update gpt_engineer/main.py
pbharrin Aug 13, 2023
5203a39
Update gpt_engineer/main.py
pbharrin Aug 13, 2023
63052f8
Update gpt_engineer/main.py
pbharrin Aug 13, 2023
97931ba
Update gpt_engineer/main.py
pbharrin Aug 13, 2023
fbb8d28
Update gpt_engineer/main.py
pbharrin Aug 13, 2023
5e1280c
Update projects/example/prompt
pbharrin Aug 13, 2023
962d2be
Update gpt_engineer/chat_to_files.py
pbharrin Aug 14, 2023
3ec3bdf
Merge branch 'main' into main
pbharrin Aug 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions gpt_engineer/chat_to_files.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import re


Expand Down Expand Up @@ -40,3 +41,56 @@ def to_files(chat, workspace):
files = parse_chat(chat)
for file_name, file_content in files:
workspace[file_name] = file_content


def overwrite_files(chat, dbs, replace_files):
"""
Replace the AI files to the older local files.
"""
dbs.workspace["all_output.txt"] = chat

files = parse_chat(chat)
for file_name, file_content in files:
# Verify if the file created by the AI agent was in the input list
if file_name in replace_files:
# If the AI created a file from our input list, we replace it.
with open(replace_files[file_name], "w") as text_file:
text_file.write(file_content)
else:
# If the AI create a new file I don't know where to put it yet
# maybe we can think in a smarter solution for this in the future
# like asking the AI where to put it.
#
# by now, just add this to the workspace inside .gpteng folder
print(
f"Could not find file path for '{file_name}', creating file in workspace"
)
dbs.workspace[file_name] = file_content


def get_code_strings(input) -> dict[str, str]:
"""
Read file_list.txt and return file names and its content.
"""
files_paths = input["file_list.txt"].strip().split("\n")
files_dict = {}
for file_path in files_paths:
with open(file_path, "r") as file:
file_data = file.read()
if file_data:
file_name = os.path.basename(file_path).split("/")[-1]
pbharrin marked this conversation as resolved.
Show resolved Hide resolved
files_dict[file_name] = file_data
return files_dict


def format_file_to_input(file_name: str, file_content: str) -> str:
"""
Format a file string to use as input to AI agent
"""
file_str = f"""
{file_name}
```
{file_content}
```
"""
return file_str
321 changes: 321 additions & 0 deletions gpt_engineer/file_selector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
import os
import re
import sys
import tkinter as tk
import tkinter.filedialog as fd

from pathlib import Path
from typing import List, Union

IGNORE_FOLDERS = {"site-packages", "node_modules"}

class DisplayablePath(object):
"""
A class representing a displayable path in a file explorer.
"""

display_filename_prefix_middle = "├── "
display_filename_prefix_last = "└── "
display_parent_prefix_middle = " "
display_parent_prefix_last = "│ "

def __init__(
self, path: Union[str, Path], parent_path: "DisplayablePath", is_last: bool
):
"""
Initialize a DisplayablePath object.

Args:
path (Union[str, Path]): The path of the file or directory.
parent_path (DisplayablePath): The parent path of the file or directory.
is_last (bool): Whether the file or directory is the last child of its parent.
"""
self.depth: int = 0
self.path = Path(str(path))
self.parent = parent_path
self.is_last = is_last
if self.parent:
self.depth = self.parent.depth + 1

@property
def display_name(self) -> str:
"""
Get the display name of the file or directory.

Returns:
str: The display name.
"""
if self.path.is_dir():
return self.path.name + "/"
return self.path.name

@classmethod
def make_tree(cls, root: Union[str, Path], parent=None, is_last=False, criteria=None):
"""
Generate a tree of DisplayablePath objects.

Args:
root: The root path of the tree.
parent: The parent path of the root path. Defaults to None.
is_last: Whether the root path is the last child of its parent.
criteria: The criteria function to filter the paths. Defaults to None.

Yields:
DisplayablePath: The DisplayablePath objects in the tree.
"""
root = Path(str(root))
criteria = criteria or cls._default_criteria

displayable_root = cls(root, parent, is_last)
yield displayable_root

children = sorted(
list(path for path in root.iterdir() if criteria(path)),
key=lambda s: str(s).lower(),
)
count = 1
for path in children:
is_last = count == len(children)
if path.is_dir() and path.name not in IGNORE_FOLDERS:
yield from cls.make_tree(
path, parent=displayable_root, is_last=is_last, criteria=criteria
)
else:
yield cls(path, displayable_root, is_last)
count += 1

@classmethod
def _default_criteria(cls, path: Path) -> bool:
"""
The default criteria function to filter the paths.

Args:
path: The path to check.

Returns:
bool: True if the path should be included, False otherwise.
"""
return True

def displayable(self) -> str:
"""
Get the displayable string representation of the file or directory.

Returns:
str: The displayable string representation.
"""
if self.parent is None:
return self.display_name

_filename_prefix = (
self.display_filename_prefix_last
if self.is_last
else self.display_filename_prefix_middle
)

parts = ["{!s} {!s}".format(_filename_prefix, self.display_name)]

parent = self.parent
while parent and parent.parent is not None:
parts.append(
self.display_parent_prefix_middle
if parent.is_last
else self.display_parent_prefix_last
)
parent = parent.parent

return "".join(reversed(parts))


class TerminalFileSelector:
def __init__(self, root_folder_path: Path) -> None:
self.number_of_selectable_items = 0
self.selectable_file_paths: dict[int, str] = {}
self.file_path_list: list = []
self.db_paths = DisplayablePath.make_tree(
root_folder_path, parent=None, criteria=is_in_ignoring_extensions
)

def display(self):
"""
Select files from a directory and display the selected files.
"""
count = 0
file_path_enumeration = {}
file_path_list = []
for path in self.db_paths:
n_digits = len(str(count))
n_spaces = 3 - n_digits
if n_spaces < 0:
# We can only print 1000 aligned files. I think it is decent enough
n_spaces = 0
spaces_str = " " * n_spaces
if not path.path.is_dir():
print(f"{count}. {spaces_str}{path.displayable()}")
file_path_enumeration[count] = path.path
file_path_list.append(path.path)
count += 1
else:
# By now we do not accept selecting entire dirs.
# But could add that in the future. Just need to add more functions
# and remove this else block...
number_space = " " * n_digits
print(f"{number_space} {spaces_str}{path.displayable()}")

self.number_of_selectable_items = count
self.file_path_list = file_path_list
self.selectable_file_paths = file_path_enumeration

def ask_for_selection(self) -> List[str]:
"""
Ask user to select files from the terminal after displaying it

Returns:
List[str]: list of selected paths
"""
user_input = input(
"\nSelect files by entering the numbers separated by commas/spaces or "
+ "specify range with a dash. "
+ "Example: 1,2,3-5,7,9,13-15,18,20 (enter 'all' to select everything)"
+ "\n\nSelect files:"
)
selected_paths = []
regex = r"\d+(-\d+)?([, ]\d+(-\d+)?)*"

if user_input.lower() == "all":
selected_paths = self.file_path_list
elif re.match(regex, user_input):
try:
user_input = (
user_input.replace("", ",") if " " in user_input else user_input
)
selected_files = user_input.split(",")
for file_number_str in selected_files:
if "-" in file_number_str:
start_str, end_str = file_number_str.split("-")
start = int(start_str)
end = int(end_str)
for num in range(start, end + 1):
selected_paths.append(str(self.selectable_file_paths[num]))
else:
num = int(file_number_str)
selected_paths.append(str(self.selectable_file_paths[num]))

except ValueError:
pass
else:
print("Please use a valid number/series of numbers.\n")
sys.exit(1)

return selected_paths

def is_in_ignoring_extensions(path: Path) -> bool:
"""
Check if a path is not hidden or in the __pycache__ directory.

Args:
path: The path to check.

Returns:
bool: True if the path is not in ignored rules. False otherwise.
"""
is_hidden = not path.name.startswith(".")
is_pycache = "__pycache__" not in path.name
return is_hidden and is_pycache


def ask_for_files(db_input) -> dict[str, str]:
"""
Ask user to select files to improve.
It can be done by terminal, gui, or using the old selection.

Returns:
dict[str, str]: Dictionary where key = file name and value = file path
"""
use_last_string = ""
is_valid_selection = False
can_use_last = False
if "file_list.txt" in db_input:
can_use_last = True
use_last_string = (
"3. Use previous file list (available at "
+ f"{os.path.join(db_input.path, 'file_list.txt')})\n"
)
selection_number = 3
else:
selection_number = 1
selection_str = f"""How do you want to select the files?

1. Use Command-Line.
2. Use File explorer.
{use_last_string if len(use_last_string) > 1 else ""}
Select option and press Enter (default={selection_number}): """
file_path_list = []
selected_number_str = input(selection_str)
if selected_number_str:
try:
selection_number = int(selected_number_str)
except ValueError:
print("Invalid number. Select a number from the list above.\n")
sys.exit(1)
if selection_number == 1:
# Open terminal selection
file_path_list = terminal_file_selector()
is_valid_selection = True
elif selection_number == 2:
# Open GUI selection
file_path_list = gui_file_selector()
is_valid_selection = True
else:
if can_use_last and selection_number == 3:
# Use previous file list
is_valid_selection = True
if not is_valid_selection:
print("Invalid number. Select a number from the list above.\n")
sys.exit(1)

file_list_string = ""
file_path_info = {}
if not selection_number == 3:
# New files
for file_path in file_path_list:
file_list_string += str(file_path) + "\n"
# Return a dict with key=file_name and value=file_path
file_path_info[os.path.basename(file_path).split("/")[-1]] = file_path
# Write in file_list so the user can edit and remember what was done
db_input["file_list.txt"] = file_list_string
else:
# If using the the previous file list, we don't need to write file_list.txt
file_list_string = db_input["file_list.txt"]
for file_path in file_path_list:
# Return a dict with key=file_name and value=file_path
file_path_info[os.path.basename(file_path).split("/")[-1]] = file_path

return file_path_info


def gui_file_selector() -> List[str]:
"""
Display a tkinter file selection window to select context files.
"""
root = tk.Tk()
root.withdraw()
root.call("wm", "attributes", ".", "-topmost", True)
file_list = list(
fd.askopenfilenames(
parent=root,
initialdir=os.getcwd(),
title="Select files to improve (or give context):",
)
)
return file_list


def terminal_file_selector() -> List[str]:
"""
Display a terminal file selection to select context files.
"""
file_selector = TerminalFileSelector(Path(os.getcwd()))
file_selector.display()
selected_list = file_selector.ask_for_selection()
return selected_list
Loading