diff --git a/.env.sample b/.env.sample index 113aa6d..2184c45 100644 --- a/.env.sample +++ b/.env.sample @@ -1 +1,2 @@ -OPENAI_API_KEY = +OPENAI_API_KEY=your_api_key +#DEFAULT_MODEL=gpt-3.5-turbo diff --git a/README.md b/README.md index e6f4efb..2aaa07a 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ For a quick demonstration see my [demo video on twitter](https://twitter.com/bio Add your openAI api key to `.env` +_warning!_ By default wolverine uses GPT-4 and may make many repeated calls to the api. + ## Example Usage To run with gpt-4 (the default, tested option): @@ -39,6 +41,14 @@ You can also run with other models, but be warned they may not adhere to the edi python wolverine.py --model=gpt-3.5-turbo -f buggy_script.py -y "subtract" 20 3 +If you want to use GPT-3.5 by default instead of GPT-4 uncomment the default model line in `.env`: + + DEFAULT_MODEL=gpt-3.5-turbo + +You can also use flag `--confirm=True` which will ask you `yes or no` before making changes to the file. If flag is not used then it will apply the changes to the file + + python wolverine.py buggy_script.py "subtract" 20 3 --confirm=True + ## Future Plans This is just a quick prototype I threw together in a few hours. There are many possible extensions and contributions are welcome: @@ -49,3 +59,7 @@ This is just a quick prototype I threw together in a few hours. There are many p - multiple files / codebases: send GPT everything that appears in the stacktrace - graceful handling of large files - should we just send GPT relevant classes / functions? - extension to languages other than python + +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=biobootloader/wolverine&type=Date)](https://star-history.com/#biobootloader/wolverine) diff --git a/buggy_script.js b/buggy_script.js new file mode 100644 index 0000000..ad05a00 --- /dev/null +++ b/buggy_script.js @@ -0,0 +1,31 @@ +const subtractNumbers = (a, b) => { + return a - b; +}; + +const multiplyNumbers = (a, b) => { + return a * b; +}; + +const divideNumbers = (a, b) => { + return a / b; +}; + +function calculate(operation, num1, num2) { + let result = ''; + if (operation == 'add') { + result = addNumbers(num1, num2); + } else if (operation == 'subtract') { + result = subtractNumbers(num1, num2); + } else if (operation == 'multiply') { + result = multiplyNumbers(num1, num2); + } else if (operation == 'divide') { + result = divideNumbers(num1, num2); + } else { + console.log('Invalid operation'); + } + + return res; +} + +const [, , operation, num1, num2] = process.argv; +calculate(operation, num1, num2); diff --git a/prompt.txt b/prompt.txt index 4376ab2..ec0582a 100644 --- a/prompt.txt +++ b/prompt.txt @@ -4,10 +4,13 @@ Because you are part of an automated system, the format you respond in is very s In addition to the changes, please also provide short explanations of the what went wrong. A single explanation is required, but if you think it's helpful, feel free to provide more explanations for groups of more complicated changes. Be careful to use proper indentation and spacing in your changes. An example response could be: +Be ABSOLUTELY SURE to include the CORRECT INDENTATION when making replacements. + +example response: [ {"explanation": "this is just an example, this would usually be a brief explanation of what went wrong"}, {"operation": "InsertAfter", "line": 10, "content": "x = 1\ny = 2\nz = x * y"}, {"operation": "Delete", "line": 15, "content": ""}, - {"operation": "Replace", "line": 18, "content": "x += 1"}, + {"operation": "Replace", "line": 18, "content": " x += 1"}, {"operation": "Delete", "line": 20, "content": ""} ] diff --git a/wolverine.py b/wolverine.py index 05228e2..8777225 100644 --- a/wolverine.py +++ b/wolverine.py @@ -5,29 +5,87 @@ import os import shutil import subprocess import sys -from dotenv import load_dotenv -from args import parser import openai from termcolor import cprint +from dotenv import load_dotenv + +from args import parser + # Set up the OpenAI API load_dotenv() openai.api_key = os.getenv("OPENAI_API_KEY") +DEFAULT_MODEL = os.environ.get("DEFAULT_MODEL", "gpt-4") + + +with open("prompt.txt") as f: + SYSTEM_PROMPT = f.read() + def run_script(script_name, script_args): script_args = [str(arg) for arg in script_args] + """ + If script_name.endswith(".py") then run with python + else run with node + """ + subprocess_args = ( + [sys.executable, script_name, *script_args] + if script_name.endswith(".py") + else ["node", script_name, *script_args] + ) + try: - result = subprocess.check_output( - [sys.executable, script_name, *script_args], stderr=subprocess.STDOUT - ) + result = subprocess.check_output(subprocess_args, stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: return e.output.decode("utf-8"), e.returncode return result.decode("utf-8"), 0 -def send_error_to_gpt(file_path, args, error_message, model): +def json_validated_response(model, messages): + """ + This function is needed because the API can return a non-json response. + This will run recursively until a valid json response is returned. + todo: might want to stop after a certain number of retries + """ + response = openai.ChatCompletion.create( + model=model, + messages=messages, + temperature=0.5, + ) + messages.append(response.choices[0].message) + content = response.choices[0].message.content + # see if json can be parsed + try: + json_start_index = content.index( + "[" + ) # find the starting position of the JSON data + json_data = content[ + json_start_index: + ] # extract the JSON data from the response string + json_response = json.loads(json_data) + except (json.decoder.JSONDecodeError, ValueError) as e: + cprint(f"{e}. Re-running the query.", "red") + # debug + cprint(f"\nGPT RESPONSE:\n\n{content}\n\n", "yellow") + # append a user message that says the json is invalid + messages.append( + { + "role": "user", + "content": "Your response could not be parsed by json.loads. Please restate your last message as pure JSON.", + } + ) + # rerun the api call + return json_validated_response(model, messages) + except Exception as e: + cprint(f"Unknown error: {e}", "red") + cprint(f"\nGPT RESPONSE:\n\n{content}\n\n", "yellow") + raise e + return json_response + + +def send_error_to_gpt(file_path, args, error_message, model=DEFAULT_MODEL): with open(file_path, "r") as f: file_lines = f.readlines() @@ -36,12 +94,7 @@ def send_error_to_gpt(file_path, args, error_message, model): file_with_lines.append(str(i + 1) + ": " + line) file_with_lines = "".join(file_with_lines) - with open("prompt.txt") as f: - initial_prompt_text = f.read() - prompt = ( - initial_prompt_text + - "\n\n" "Here is the script that needs fixing:\n\n" f"{file_with_lines}\n\n" "Here are the arguments it was provided:\n\n" @@ -53,27 +106,27 @@ def send_error_to_gpt(file_path, args, error_message, model): ) # print(prompt) + messages = [ + { + "role": "system", + "content": SYSTEM_PROMPT, + }, + { + "role": "user", + "content": prompt, + }, + ] - response = openai.ChatCompletion.create( - model=model, - messages=[ - { - "role": "user", - "content": prompt, - } - ], - temperature=1.0, - ) - - return response.choices[0].message.content.strip() + return json_validated_response(model, messages) -def apply_changes(file_path, changes_json): +def apply_changes(file_path, changes: list, confirm=False): + """ + Pass changes as loaded json (list of dicts) + """ with open(file_path, "r") as f: original_file_lines = f.readlines() - changes = json.loads(changes_json) - # Filter out explanation elements operation_changes = [change for change in changes if "operation" in change] explanations = [ @@ -96,6 +149,25 @@ def apply_changes(file_path, changes_json): elif operation == "InsertAfter": file_lines.insert(line, content + "\n") + # Ask for user confirmation before writing changes + print("\nChanges to be made:") + + diff = difflib.unified_diff(original_file_lines, file_lines, lineterm="") + for line in diff: + if line.startswith("+"): + cprint(line, "green", end="") + elif line.startswith("-"): + cprint(line, "red", end="") + else: + print(line, end="") + + # Checking if user used confirm flag + if confirm: + confirmation = input("Do you want to apply these changes? (y/n): ") + if confirmation.lower() != "y": + print("Changes not applied") + sys.exit(0) + with open(file_path, "w") as f: f.writelines(file_lines) @@ -106,7 +178,8 @@ def apply_changes(file_path, changes_json): # Show the diff print("\nChanges:") - diff = difflib.unified_diff(original_file_lines, file_lines, lineterm="") + diff = difflib.unified_diff( + original_file_lines, file_lines, lineterm="") for line in diff: if line.startswith("+"): cprint(line, "green", end="") @@ -115,13 +188,14 @@ def apply_changes(file_path, changes_json): else: print(line, end="") + print("Changes applied.") def main(): args = parser.parse_args() script_name = args.file script_args = args.args revert = args.revert - model = args.model + model = args.model if args.model else DEFAULT_MODEL run_until_success = args.yes if revert: backup_file = script_name + ".bak" @@ -157,12 +231,13 @@ def main(): print("Output:", output) json_response = send_error_to_gpt( - file_path=script_name, - args=script_args, - error_message=output, - model=model, + file_path=script_name, + args=script_args, + error_message=output, + model=model, ) - apply_changes(script_name, json_response) + + apply_changes(script_name, json_response, confirm=confirm) cprint("Changes applied. Rerunning...", "blue")