diff --git a/.gitignore b/.gitignore index da34ea7..6086c3c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -venv +openai_key.txt +wolverine.log .venv .env env/ diff --git a/wolverine.py b/wolverine.py index 194a0fc..72c15f9 100644 --- a/wolverine.py +++ b/wolverine.py @@ -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") @@ -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: @@ -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() @@ -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 = [ { @@ -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)}") + +# 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("+"): @@ -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") @@ -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( @@ -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) +