kopia lustrzana https://github.com/biobootloader/wolverine
change to inform model unavailable, fix conflicts
commit
12d78109cf
|
@ -0,0 +1,2 @@
|
||||||
|
OPENAI_API_KEY=your_api_key
|
||||||
|
#DEFAULT_MODEL=gpt-3.5-turbo
|
|
@ -1,2 +1,5 @@
|
||||||
venv
|
venv
|
||||||
openai_key.txt
|
.venv
|
||||||
|
.env
|
||||||
|
env/
|
||||||
|
.vscode/
|
||||||
|
|
21
README.md
21
README.md
|
@ -13,26 +13,41 @@ For a quick demonstration see my [demo video on twitter](https://twitter.com/bio
|
||||||
python3 -m venv venv
|
python3 -m venv venv
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
cp .env.sample .env
|
||||||
|
|
||||||
Add your openAI api key to `openai_key.txt` - _warning!_ by default this uses GPT-4 and may make many repeated calls to the api.
|
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
|
## Example Usage
|
||||||
|
|
||||||
To run with gpt-4 (the default, tested option):
|
To run with gpt-4 (the default, tested option):
|
||||||
|
|
||||||
python wolverine.py buggy_script.py "subtract" 20 3
|
python wolverine.py examples/buggy_script.py "subtract" 20 3
|
||||||
|
|
||||||
You can also run with other models, but be warned they may not adhere to the edit format as well:
|
You can also run with other models, but be warned they may not adhere to the edit format as well:
|
||||||
|
|
||||||
python wolverine.py --model=gpt-3.5-turbo buggy_script.py "subtract" 20 3
|
python wolverine.py --model=gpt-3.5-turbo buggy_script.py "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
|
## Future Plans
|
||||||
|
|
||||||
This is just a quick prototype I threw together in a few hours. There are many possible extensions and contributions are welcome:
|
This is just a quick prototype I threw together in a few hours. There are many possible extensions and contributions are welcome:
|
||||||
|
|
||||||
- add flags to customize usage, such as asking for user confirmation before running changed code
|
- add flags to customize usage, such as asking for user confirmation before running changed code
|
||||||
- further iterations on the edit format that GPT responds in. Currently it struggles a bit with indentation, but I'm sure that can be improved
|
- further iterations on the edit format that GPT responds in. Currently it struggles a bit with indentation, but I'm sure that can be improved
|
||||||
- a suite of example buggy files that we can test prompts on to ensure reliablity and measure improvement
|
- a suite of example buggy files that we can test prompts on to ensure reliability and measure improvement
|
||||||
- multiple files / codebases: send GPT everything that appears in the stacktrace
|
- multiple files / codebases: send GPT everything that appears in the stacktrace
|
||||||
- graceful handling of large files - should we just send GPT relevant classes / functions?
|
- graceful handling of large files - should we just send GPT relevant classes / functions?
|
||||||
- extension to languages other than python
|
- extension to languages other than python
|
||||||
|
|
||||||
|
## Star History
|
||||||
|
|
||||||
|
[](https://star-history.com/#biobootloader/wolverine)
|
||||||
|
|
|
@ -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);
|
|
@ -1,5 +1,9 @@
|
||||||
import sys
|
import sys
|
||||||
import fire
|
import fire
|
||||||
|
"""
|
||||||
|
Run With: `wolverine examples/buggy_script.py "subtract" 20 3`
|
||||||
|
Purpose: Show self-regenerating fixing of subtraction operator
|
||||||
|
"""
|
||||||
|
|
||||||
def add_numbers(a, b):
|
def add_numbers(a, b):
|
||||||
return a + b
|
return a + b
|
|
@ -0,0 +1,27 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import fire
|
||||||
|
|
||||||
|
"""
|
||||||
|
Run With: with `python wolverine.py examples/buggy_script_2.py`
|
||||||
|
Purpose: Fix singleton code bug in Python
|
||||||
|
"""
|
||||||
|
|
||||||
|
class SingletonClass(object):
|
||||||
|
def __new__(cls):
|
||||||
|
cls.instance = super(SingletonClass, cls).__new__(cls)
|
||||||
|
return cls.instance
|
||||||
|
|
||||||
|
def check_singleton_works():
|
||||||
|
"""
|
||||||
|
check that singleton pattern is working
|
||||||
|
"""
|
||||||
|
singleton = SingletonClass()
|
||||||
|
new_singleton = SingletonClass()
|
||||||
|
singleton.a = 1
|
||||||
|
new_singleton.a = 2
|
||||||
|
should_be_4 = (singleton.a + new_singleton.a)
|
||||||
|
assert should_be_4 == 4
|
||||||
|
|
||||||
|
if __name__=="__main__":
|
||||||
|
fire.Fire(check_singleton_works)
|
||||||
|
|
|
@ -4,6 +4,9 @@ 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:
|
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"},
|
{"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": "InsertAfter", "line": 10, "content": "x = 1\ny = 2\nz = x * y"},
|
||||||
|
|
|
@ -13,6 +13,7 @@ multidict==6.0.4
|
||||||
openai==0.27.2
|
openai==0.27.2
|
||||||
pycodestyle==2.10.0
|
pycodestyle==2.10.0
|
||||||
pyflakes==3.0.1
|
pyflakes==3.0.1
|
||||||
|
python-dotenv==1.0.0
|
||||||
requests==2.28.2
|
requests==2.28.2
|
||||||
six==1.16.0
|
six==1.16.0
|
||||||
termcolor==2.2.0
|
termcolor==2.2.0
|
||||||
|
|
144
wolverine.py
144
wolverine.py
|
@ -5,26 +5,86 @@ import os
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import openai
|
import openai
|
||||||
from termcolor import cprint
|
from termcolor import cprint
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
|
||||||
# Set up the OpenAI API
|
# Set up the OpenAI API
|
||||||
with open("openai_key.txt") as f:
|
load_dotenv()
|
||||||
openai.api_key = f.read().strip()
|
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):
|
def run_script(script_name, script_args):
|
||||||
script_args = [str(arg) for arg in script_args]
|
script_args = [str(arg) for arg in script_args]
|
||||||
try:
|
"""
|
||||||
result = subprocess.check_output(
|
If script_name.endswith(".py") then run with python
|
||||||
[sys.executable, script_name, *script_args], stderr=subprocess.STDOUT
|
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(subprocess_args, stderr=subprocess.STDOUT)
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
return e.output.decode("utf-8"), e.returncode
|
return e.output.decode("utf-8"), e.returncode
|
||||||
return result.decode("utf-8"), 0
|
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:
|
with open(file_path, "r") as f:
|
||||||
file_lines = f.readlines()
|
file_lines = f.readlines()
|
||||||
|
|
||||||
|
@ -33,12 +93,7 @@ def send_error_to_gpt(file_path, args, error_message, model):
|
||||||
file_with_lines.append(str(i + 1) + ": " + line)
|
file_with_lines.append(str(i + 1) + ": " + line)
|
||||||
file_with_lines = "".join(file_with_lines)
|
file_with_lines = "".join(file_with_lines)
|
||||||
|
|
||||||
with open("prompt.txt") as f:
|
|
||||||
initial_prompt_text = f.read()
|
|
||||||
|
|
||||||
prompt = (
|
prompt = (
|
||||||
initial_prompt_text +
|
|
||||||
"\n\n"
|
|
||||||
"Here is the script that needs fixing:\n\n"
|
"Here is the script that needs fixing:\n\n"
|
||||||
f"{file_with_lines}\n\n"
|
f"{file_with_lines}\n\n"
|
||||||
"Here are the arguments it was provided:\n\n"
|
"Here are the arguments it was provided:\n\n"
|
||||||
|
@ -50,27 +105,27 @@ def send_error_to_gpt(file_path, args, error_message, model):
|
||||||
)
|
)
|
||||||
|
|
||||||
# print(prompt)
|
# print(prompt)
|
||||||
|
|
||||||
response = openai.ChatCompletion.create(
|
|
||||||
model=model,
|
|
||||||
messages = [
|
messages = [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": SYSTEM_PROMPT,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"role": "user",
|
"role": "user",
|
||||||
"content": prompt,
|
"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:
|
with open(file_path, "r") as f:
|
||||||
original_file_lines = f.readlines()
|
original_file_lines = f.readlines()
|
||||||
|
|
||||||
changes = json.loads(changes_json)
|
|
||||||
|
|
||||||
# Filter out explanation elements
|
# Filter out explanation elements
|
||||||
operation_changes = [change for change in changes if "operation" in change]
|
operation_changes = [change for change in changes if "operation" in change]
|
||||||
explanations = [
|
explanations = [
|
||||||
|
@ -93,16 +148,13 @@ def apply_changes(file_path, changes_json):
|
||||||
elif operation == "InsertAfter":
|
elif operation == "InsertAfter":
|
||||||
file_lines.insert(line, content + "\n")
|
file_lines.insert(line, content + "\n")
|
||||||
|
|
||||||
with open(file_path, "w") as f:
|
|
||||||
f.writelines(file_lines)
|
|
||||||
|
|
||||||
# Print explanations
|
# Print explanations
|
||||||
cprint("Explanations:", "blue")
|
cprint("Explanations:", "blue")
|
||||||
for explanation in explanations:
|
for explanation in explanations:
|
||||||
cprint(f"- {explanation}", "blue")
|
cprint(f"- {explanation}", "blue")
|
||||||
|
|
||||||
# Show the diff
|
# Display changes diff
|
||||||
print("\nChanges:")
|
print("\nChanges to be made:")
|
||||||
diff = difflib.unified_diff(original_file_lines, file_lines, lineterm="")
|
diff = difflib.unified_diff(original_file_lines, file_lines, lineterm="")
|
||||||
for line in diff:
|
for line in diff:
|
||||||
if line.startswith("+"):
|
if line.startswith("+"):
|
||||||
|
@ -112,14 +164,30 @@ def apply_changes(file_path, changes_json):
|
||||||
else:
|
else:
|
||||||
print(line, end="")
|
print(line, end="")
|
||||||
|
|
||||||
def avaibility(openaiObj):
|
if confirm:
|
||||||
if "gpt-4" not in [x['id'] for x in openai.Model.list()['data']]:
|
# check if user wants to apply changes or exit
|
||||||
model = "gpt-3.5-turbo"
|
confirmation = input("Do you want to apply these changes? (y/n): ")
|
||||||
else:
|
if confirmation.lower() != "y":
|
||||||
model = "gpt-4"
|
print("Changes not applied")
|
||||||
return model
|
sys.exit(0)
|
||||||
|
|
||||||
def main(script_name, *script_args, revert=False, model=avaibility(openai)):
|
with open(file_path, "w") as f:
|
||||||
|
f.writelines(file_lines)
|
||||||
|
print("Changes applied.")
|
||||||
|
|
||||||
|
|
||||||
|
def check_model_availability(model):
|
||||||
|
available_models = [x['id'] for x in openai.Model.list()["data"]]
|
||||||
|
if model not in available_models:
|
||||||
|
print(
|
||||||
|
f"Model {model} is not available. Perhaps try running with "
|
||||||
|
"`--model=gpt-3.5-turbo` instead? You can also configure a "
|
||||||
|
"default model in the .env"
|
||||||
|
)
|
||||||
|
exit()
|
||||||
|
|
||||||
|
|
||||||
|
def main(script_name, *script_args, revert=False, model=DEFAULT_MODEL, confirm=False):
|
||||||
if revert:
|
if revert:
|
||||||
backup_file = script_name + ".bak"
|
backup_file = script_name + ".bak"
|
||||||
if os.path.exists(backup_file):
|
if os.path.exists(backup_file):
|
||||||
|
@ -130,6 +198,9 @@ def main(script_name, *script_args, revert=False, model=avaibility(openai)):
|
||||||
print(f"No backup file found for {script_name}")
|
print(f"No backup file found for {script_name}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# check if model is available
|
||||||
|
check_model_availability(model)
|
||||||
|
|
||||||
# Make a backup of the original script
|
# Make a backup of the original script
|
||||||
shutil.copy(script_name, script_name + ".bak")
|
shutil.copy(script_name, script_name + ".bak")
|
||||||
|
|
||||||
|
@ -150,7 +221,8 @@ def main(script_name, *script_args, revert=False, model=avaibility(openai)):
|
||||||
error_message=output,
|
error_message=output,
|
||||||
model=model,
|
model=model,
|
||||||
)
|
)
|
||||||
apply_changes(script_name, json_response)
|
|
||||||
|
apply_changes(script_name, json_response, confirm=confirm)
|
||||||
cprint("Changes applied. Rerunning...", "blue")
|
cprint("Changes applied. Rerunning...", "blue")
|
||||||
|
|
||||||
|
|
||||||
|
|
Ładowanie…
Reference in New Issue