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

Better Error Handling & Interactive Mode Feature #23

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
venv
openai_key.txt
wolverine.log
.venv
.env
env/
Expand Down
166 changes: 122 additions & 44 deletions wolverine.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
import subprocess
import sys
import openai
import logging
from termcolor import cprint
from dotenv import load_dotenv



# Set up the OpenAI API
load_dotenv()
openai.api_key = os.getenv("OPENAI_API_KEY")
Expand All @@ -19,8 +21,17 @@

with open("prompt.txt") as f:
SYSTEM_PROMPT = f.read()

# Set up logging
def configure_logging():
logging.basicConfig(
filename="wolverine.log",
format="%(asctime)s - %(levelname)s - %(message)s",
level=logging.INFO,
)


# Run the provided script and return the output and return code
def run_script(script_name, script_args):
script_args = [str(arg) for arg in script_args]
try:
Expand Down Expand Up @@ -73,8 +84,8 @@ def json_validated_response(model, messages):
raise e
return json_response


def send_error_to_gpt(file_path, args, error_message, model=DEFAULT_MODEL):
# Send the error to GPT and receive suggestions
def send_error_to_gpt(file_path, args, error_message, model=DEFAULT_MODEL, prompt_length_limit=4096):
with open(file_path, "r") as f:
file_lines = f.readlines()

Expand All @@ -94,6 +105,10 @@ def send_error_to_gpt(file_path, args, error_message, model=DEFAULT_MODEL):
"exact format as described above."
)

# Truncate the prompt if it exceeds the limit
if len(prompt) > prompt_length_limit:
prompt = prompt[:prompt_length_limit]

# print(prompt)
messages = [
{
Expand All @@ -107,47 +122,85 @@ def send_error_to_gpt(file_path, args, error_message, model=DEFAULT_MODEL):
]

return json_validated_response(model, messages)


# Apply the changes suggested by GPT
def apply_changes(file_path, changes: list):
"""
Pass changes as loaded json (list of dicts)
"""
try:
with open(file_path, "r") as f:
original_file_lines = f.readlines()

operation_changes = [change for change in changes if "operation" in change]
explanations = [
change["explanation"] for change in changes if "explanation" in change
]

operation_changes.sort(key=lambda x: x["line"], reverse=True)

file_lines = original_file_lines.copy()
for change in operation_changes:
operation = change["operation"]
line = change["line"]
content = change["content"]

if operation == "Replace":
file_lines[line - 1] = content + "\n"
elif operation == "Delete":
del file_lines[line - 1]
elif operation == "InsertAfter":
file_lines.insert(line, content + "\n")

with open(file_path, "w") as f:
f.writelines(file_lines)

# Print explanations
cprint("Explanations:", "blue")
for explanation in explanations:
cprint(f"- {explanation}", "blue")

# Show the diff
print("\nChanges:")
print_diff(original_file_lines, file_lines)
except Exception as e:
raise Exception(f"Failed to apply changes: {str(e)}")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what exceptions were you encountering that caused you to add this? Maybe the response wasn't proper JSON (now fixed)?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was getting non-real JSON in the output responses, so it wasn't parsing if I recall correctly.


# Apply a single change suggested by GPT interactively
def apply_change_interactive(file_path, change):
with open(file_path, "r") as f:
original_file_lines = f.readlines()

# Filter out explanation elements
operation_changes = [change for change in changes if "operation" in change]
explanations = [
change["explanation"] for change in changes if "explanation" in change
]

# Sort the changes in reverse line order
operation_changes.sort(key=lambda x: x["line"], reverse=True)
operation = change["operation"]
line = change["line"]
content = change["content"]

file_lines = original_file_lines.copy()
for change in operation_changes:
operation = change["operation"]
line = change["line"]
content = change["content"]

if operation == "Replace":
file_lines[line - 1] = content + "\n"
elif operation == "Delete":
del file_lines[line - 1]
elif operation == "InsertAfter":
file_lines.insert(line, content + "\n")

with open(file_path, "w") as f:
f.writelines(file_lines)

# Print explanations
cprint("Explanations:", "blue")
for explanation in explanations:
cprint(f"- {explanation}", "blue")

# Show the diff
print("\nChanges:")
if operation == "Replace":
file_lines[line - 1] = content + "\n"
elif operation == "Delete":
del file_lines[line - 1]
elif operation == "InsertAfter":
file_lines.insert(line, content + "\n")

print("\nSuggested change:")
print_diff(original_file_lines, file_lines)

while True:
decision = input("Do you want to apply this change? (y/n): ").lower()
if decision == "y":
with open(file_path, "w") as f:
f.writelines(file_lines)
logging.info(f"Applied change: {change}")
return True
elif decision == "n":
logging.info(f"Rejected change: {change}")
return False
else:
print("Invalid input. Please enter 'y' or 'n'.")

# Print the differences between two file contents
def print_diff(original_file_lines, file_lines):
diff = difflib.unified_diff(original_file_lines, file_lines, lineterm="")
for line in diff:
if line.startswith("+"):
Expand All @@ -157,17 +210,15 @@ def apply_changes(file_path, changes: list):
else:
print(line, end="")


def main(script_name, *script_args, revert=False, model=DEFAULT_MODEL):
def main(script_name, *script_args, revert=False, model=DEFAULT_MODEL, interactive=False):
if revert:
backup_file = script_name + ".bak"
if os.path.exists(backup_file):
shutil.copy(backup_file, script_name)
print(f"Reverted changes to {script_name}")
sys.exit(0)
return
else:
print(f"No backup file found for {script_name}")
sys.exit(1)
raise Exception(f"No backup file found for {script_name}")

# Make a backup of the original script
shutil.copy(script_name, script_name + ".bak")
Expand All @@ -178,9 +229,11 @@ def main(script_name, *script_args, revert=False, model=DEFAULT_MODEL):
if returncode == 0:
cprint("Script ran successfully.", "blue")
print("Output:", output)
logging.info("Script ran successfully.")
break
else:
cprint("Script crashed. Trying to fix...", "blue")
logging.error(f"Script crashed with return code {returncode}.")
print("Output:", output)

json_response = send_error_to_gpt(
Expand All @@ -189,10 +242,35 @@ def main(script_name, *script_args, revert=False, model=DEFAULT_MODEL):
error_message=output,
model=model,
)

apply_changes(script_name, json_response)
cprint("Changes applied. Rerunning...", "blue")
if interactive:
changes = json_response
operation_changes = [change for change in changes if "operation" in change]
explanations = [
change["explanation"] for change in changes if "explanation" in change
]

for change in operation_changes:
if apply_change_interactive(script_name, change):
cprint("Change applied.", "green")
else:
cprint("Change rejected.", "red")
cprint("Finished applying changes. Rerunning...", "blue")
logging.info("Finished applying changes in interactive mode.")
else:
try:
apply_changes(script_name, json_response)
cprint("Changes applied. Rerunning...", "blue")
logging.info("Changes applied.")
except Exception as e:
raise Exception(f"Failed to fix the script: {str(e)}")


if __name__ == "__main__":
fire.Fire(main)
configure_logging()
try:
fire.Fire(main)
except Exception as e:
print(str(e))
logging.error(f"Error: {str(e)}")
sys.exit(1)