Implement modifying large files in CodeMonkey

Try identifying and applying individual changes. If that fails, fallback
to getting the whole file at once.
This commit is contained in:
Senko Rasic
2023-12-26 09:51:15 +01:00
committed by Senko Rasic
parent 363faec636
commit cb95ec09bf
21 changed files with 1098 additions and 337 deletions

View File

@@ -200,29 +200,48 @@ IMPLEMENT_TASK = {
'properties': {
'type': {
'type': 'string',
'enum': ['command', 'code_change', 'human_intervention'],
'enum': ['command', 'save_file', 'modify_file', 'human_intervention'],
'description': 'Type of the development step that needs to be done to complete the entire task.',
},
'command': command_definition(),
'code_change': {
'save_file': {
'type': 'object',
'description': 'A code change that needs to be implemented. This should be used only if the task is of a type "code_change".',
'description': 'A file that needs to be created or file that needs to be completely replaced. This should be used for new files.',
'properties': {
'name': {
'type': 'string',
'description': 'Name of the file that needs to be implemented.',
'description': 'Name of the file that needs to be created or replaced.',
},
'path': {
'type': 'string',
'description': 'Full path of the file with the file name that needs to be implemented.',
'description': 'Full path of the file (with the file name) that needs to be created or replaced.',
},
'content': {
'type': 'string',
'description': 'Full content of the file that needs to be implemented. **IMPORTANT**When you want to add a comment that tells the user to add the previous implementation at that place, make sure that the comment starts with `[OLD CODE]` and add a description of what old code should be inserted here. For example, `[OLD CODE] Login route`.',
'description': 'Full content of the file that needs to be implemented. Remember, you MUST NOT omit any of the content that should go into this file.',
},
},
'required': ['name', 'path', 'content'],
},
'modify_file': {
'type': 'object',
'description': 'A file that should be modified. This should only be used for existing files.',
'properties': {
'name': {
'type': 'string',
'description': 'Name of the existing file that needs to be updated.',
},
'path': {
'type': 'string',
'description': 'Full path of the file with the file name that needs to be updated.',
},
'code_change_description': {
'type': 'string',
'description': 'Detailed description, with code snippets and any relevant context/explanation, of the changes that the developer should do.',
},
},
'required': ['name', 'path', 'code_change_description'],
},
'human_intervention_description': {
'type': 'string',
'description': 'Description of a step in debugging this issue when there is a human intervention needed. This should be used only if the task is of a type "human_intervention".',
@@ -431,74 +450,25 @@ EXECUTE_COMMANDS = {
}
}
GET_FILES = {
GET_FILE_TO_MODIFY = {
'definitions': [{
'name': 'get_files',
'description': 'Returns development files that are currently implemented so that they can be analized and so that changes can be appropriatelly made.',
'name': 'get_file_to_modify',
'description': 'File that needs to be modified.',
'parameters': {
'type': 'object',
'properties': {
'files': {
'type': 'array',
'description': 'List of files that need to be analized to implement the reqired changes. Any file name in this array MUST be from the directory tree listed in the previous message.',
'items': {
'type': 'string',
'description': 'A single file name that needs to be analized to implement the reqired changes. Remember, this is a file name with path relative to the project root. For example, if a file path is `{{project_root}}/models/model.py`, this value needs to be `models/model.py`. This file name MUST be listed in the directory from the previous message.',
}
'file': {
'type': 'string',
'description': 'Path to the file that needs to be modified, relative to the project root.',
}
},
'required': ['files'],
},
}
}
}],
'functions': {
'get_files': lambda files: files
'get_file_to_modify': lambda file: file
}
}
IMPLEMENT_CHANGES = {
'definitions': [{
'name': 'save_files',
'description': 'Iterates over the files passed to this function and saves them on the disk.',
'parameters': {
'type': 'object',
'properties': {
'files': {
'type': 'array',
'description': 'List of files that need to be saved.',
'items': {
'type': 'object',
'properties': {
'name': {
'type': 'string',
'description': 'Name of the file that needs to be saved on the disk.',
},
'path': {
'type': 'string',
'description': 'Full path of the file with the file name that needs to be saved.',
},
'content': {
'type': 'string',
'description': 'Full content of the file that needs to be saved on the disk. **IMPORTANT**When you want to add a comment that tells the user to add the previous implementation at that place, make sure that the comment starts with `[OLD CODE]` and add a description of what old code should be inserted here. For example, `[OLD CODE] Login route`.',
},
'description': {
'type': 'string',
'description': 'Description of the file that needs to be saved on the disk. This description doesn\'t need to explain what is being done currently in this task but rather what is the idea behind this file - what do we want to put in this file in the future. Write the description ONLY if this is the first time this file is being saved. If this file already exists on the disk, leave this field empty.',
},
},
'required': ['name', 'path', 'content'],
}
}
},
'required': ['files'],
},
}],
'functions': {
'save_files': lambda files: files
},
'to_message': lambda files: [
f'File `{file["name"]}` saved to the disk and currently looks like this:\n```\n{file["content"]}\n```' for file
in files]
}
GET_TEST_TYPE = {
'definitions': [{
@@ -626,27 +596,6 @@ GET_MISSING_SNIPPETS = {
}],
}
GET_FULLY_CODED_FILE = {
'definitions': [{
'name': 'get_fully_coded_file',
'description': 'Gets the fully coded file.',
'parameters': {
'type': 'object',
'properties': {
'file_content': {
'type': 'string',
'description': 'Fully coded file. This contains only the lines of code and no other text.',
}
},
'required': ['file_content'],
},
}],
'functions': {
'get_fully_coded_file': lambda file: file
},
}
GET_DOCUMENTATION_FILE = {
'definitions': [{
'name': 'get_documentation_file',
@@ -664,7 +613,7 @@ GET_DOCUMENTATION_FILE = {
},
'content': {
'type': 'string',
'description': 'Full content of the documentation file that needs to be saved on the disk. **IMPORTANT**When you want to add a comment that tells the user to add the previous implementation at that place, make sure that the comment starts with `[OLD CODE]` and add a description of what old code should be inserted here. For example, `[OLD CODE] Login route`.',
'description': 'Full content of the documentation file that needs to be saved on the disk.',
},
},
'required': ['name', 'path', 'content'],

View File

@@ -194,7 +194,7 @@ def get_app(app_id, error_if_not_found=True):
return app
except DoesNotExist:
if error_if_not_found:
raise ValueError(f"No app with id: {app_id}")
raise ValueError(f"No app with id: {app_id}; use python main.py --get-created-apps-with-steps to see created apps")
return None

View File

@@ -54,7 +54,7 @@ class AgentConvo:
# TODO: move this if block (and the other below) to Developer agent - https://github.com/Pythagora-io/gpt-pilot/issues/91#issuecomment-1751964079
# check if we already have the LLM response saved
if self.agent.__class__.__name__ == 'Developer':
if hasattr(self.agent, 'save_dev_steps') and self.agent.save_dev_steps:
self.agent.project.llm_req_num += 1
development_step = get_saved_development_step(self.agent.project)
if development_step is not None and self.agent.project.skip_steps:

View File

@@ -236,7 +236,7 @@ class Project:
_, full_path = self.get_full_file_path(file_path, file_path)
file_data = get_file_contents(full_path, self.root_path)
except ValueError:
file_data = {"path": file_path, "content": ''}
file_data = {"path": file_path, "name": os.path.basename(file_path), "content": ''}
files_with_content.append(file_data)
return files_with_content

View File

@@ -1,38 +1,403 @@
from const.function_calls import GET_FILES, IMPLEMENT_CHANGES
import os.path
import re
from typing import Optional
from helpers.AgentConvo import AgentConvo
from helpers.Agent import Agent
from helpers.files import get_file_contents
from const.function_calls import GET_FILE_TO_MODIFY
from utils.exit import trace_code_event
class CodeMonkey(Agent):
save_dev_steps = True
def __init__(self, project, developer):
super().__init__('code_monkey', project)
self.developer = developer
def implement_code_changes(self, convo, task_description, code_changes_description, step, step_index=0):
if convo is None:
def get_original_file(self, code_changes_description, step, files):
"""
Get the original file content and name.
:param code_changes_description: description of the code changes
:param step: information about the step being implemented
:param files: list of files to send to the LLM
:return: tuple of (file_name, file_content)
"""
# If we're called as a result of debugging, we don't have the name/path of the file
# to modify so we need to figure that out first.
if 'path' not in step or 'name' not in step:
file_to_change = self.identify_file_to_change(code_changes_description, files)
step['path'] = os.path.dirname(file_to_change)
step['name'] = os.path.basename(file_to_change)
rel_path, abs_path = self.project.get_full_file_path(step['path'], step['name'])
for f in files:
# Take into account that step path might start with "/"
if (f['path'] == step['path'] or (os.path.sep + f['path'] == step['path'])) and f['name'] == step['name'] and f['content']:
file_content = f['content']
break
else:
# If we didn't have the match (because of incorrect or double use of path separators or similar), fallback to directly loading the file
try:
file_content = get_file_contents(abs_path, self.project.root_path)['content']
if isinstance(file_content, bytes):
# We should never want to change a binary file, but if we do end up here, let's not crash
file_content = "... <binary file, content omitted> ..."
except ValueError:
# File doesn't exist, we probably need to create a new one
file_content = ""
file_name = os.path.join(rel_path, step['name'])
return file_name, file_content
def implement_code_changes(
self,
convo: Optional[AgentConvo],
code_changes_description: str,
step: dict[str, str],
) -> AgentConvo:
"""
Implement code changes described in `code_changes_description`.
:param convo: AgentConvo instance (optional)
:param task_description: description of the task
:param code_changes_description: description of the code changes
:param step: information about the step being implemented
:param step_index: index of the step to implement
"""
standalone = False
if not convo:
standalone = True
convo = AgentConvo(self)
# files_needed = convo.send_message('development/task/request_files_for_code_changes.prompt', {
# "step_description": code_changes_description,
# "directory_tree": self.project.get_directory_tree(True),
# "step_index": step_index,
# "finished_steps": ', '.join(f"#{j}" for j in range(step_index))
# }, GET_FILES)
files = self.project.get_all_coded_files()
file_name, file_content = self.get_original_file(code_changes_description, step, files)
content = file_content
llm_response = convo.send_message('development/implement_changes.prompt', {
"step_description": code_changes_description,
"step": step,
"task_description": task_description,
"step_index": step_index, # todo remove step_index because in debugging we reset steps and it looks confusing in convo
"directory_tree": self.project.get_directory_tree(True),
"files": self.project.get_all_coded_files() # self.project.get_files(files_needed),
}, IMPLEMENT_CHANGES)
convo.remove_last_x_messages(2)
if file_content:
# If we have the old file, try to replace individual code blocks
replace_complete_file, content = self.replace_code_blocks(
step,
convo,
standalone,
code_changes_description,
file_content,
file_name,
files,
)
else:
# We're creating a new file, so we need to get full output
replace_complete_file = True
changes = self.developer.replace_old_code_comments(llm_response['files'])
# If this is a new file or replacing individual code blocks failed,
# replace the complete file.
if replace_complete_file:
content = self.replace_complete_file(
convo,
standalone,
code_changes_description,
file_content,
file_name, files
)
if self.project.skip_until_dev_step != str(self.project.checkpoints['last_development_step'].id):
for file_data in changes:
self.project.save_file(file_data)
if content and content != file_content:
self.project.save_file({
'path': step['path'],
'name': step['name'],
'content': content,
})
return convo
def replace_code_blocks(
self,
step: dict[str, str],
convo: AgentConvo,
standalone: bool,
code_changes_description: str,
file_content: str,
file_name: str,
files: list[dict]
):
llm_response = convo.send_message('development/implement_changes.prompt', {
"full_output": False,
"standalone": standalone,
"code_changes_description": code_changes_description,
"file_content": file_content,
"file_name": file_name,
"files": files,
})
replace_complete_file = False
exchanged_messages = 2
content = None
# Allow for up to 2 retries
while exchanged_messages < 7:
# Modify a copy of the content in case we need to retry
content = file_content
if re.findall('(old|existing).+code', llm_response, re.IGNORECASE):
trace_code_event("codemonkey-file-update-error", {
"error": "old-code-comment",
"llm_response": llm_response,
})
llm_response = convo.send_message('utils/llm_response_error.prompt', {
"error": (
"You must not omit any code from NEW_CODE. "
"Please don't use coments like `// .. existing code goes here`."
)
})
exchanged_messages += 2
continue
# Split the response into pairs of old and new code blocks
block_pairs = self.get_code_blocks(llm_response)
if len(block_pairs) == 0:
if "```" in llm_response:
# We know some code blocks were outputted but we couldn't find them
print("Unable to parse code blocks from LLM response, asking to retry")
trace_code_event("codemonkey-file-update-error", {
"error": "error-parsing-blocks",
"llm_response": llm_response,
})
# If updating is more complicated than just replacing the complete file, don't bother.
if len(llm_response) > len(file_content):
replace_complete_file = True
break
llm_response = convo.send_message('utils/llm_response_error.prompt', {
"error": "I can't find CURRENT_CODE and NEW_CODE blocks in your response, please try again."
})
exchanged_messages += 2
continue
else:
print(f"No changes required for {step['name']}")
break
# Replace old code blocks with new code blocks
errors = []
for i, (old_code, new_code) in enumerate(block_pairs):
try:
old_code, new_code = self.dedent(old_code, new_code)
content = self.replace(content, old_code, new_code)
except ValueError as err:
errors.append((i + 1, str(err)))
if not errors:
break
trace_code_event("codemonkey-file-update-error", {
"error": "replace-errors",
"llm_response": llm_response,
"details": errors,
})
print(f"{len(errors)} error(s) while trying to update file, asking LLM to retry")
if len(llm_response) > len(file_content):
# If updating is more complicated than just replacing the complete file, don't bother.
replace_complete_file = True
break
# Otherwise, identify the problem block(s) and ask the LLM to retry
if content != file_content:
error_text = (
"Some changes were applied, but these failed:\n" +
"\n".join(f"Error in change {i}:\n{err}" for i, err in errors) +
"\nPlease fix the errors and try again (only output the blocks that failed to update, not all of them)."
)
else:
error_text = "\n".join(f"Error in change {i}:\n{err}" for i, err in errors)
llm_response = convo.send_message('utils/llm_response_error.prompt', {
"error": error_text,
})
exchanged_messages += 2
else:
# We failed after a few retries, so let's just replace the complete file
print("Unable to modify file, asking LLM to output the complete new file")
replace_complete_file = True
if replace_complete_file:
trace_code_event("codemonkey-file-update-error", {
"error": "fallback-complete-replace",
"llm_response": llm_response,
})
convo.remove_last_x_messages(exchanged_messages)
return replace_complete_file, content
def replace_complete_file(
self,
convo: AgentConvo,
standalone: bool,
code_changes_description: str,
file_content: str,
file_name: str,
files: list[dict]
) -> str:
"""
As a fallback, replace the complete file content.
This should only be used if we've failed to replace individual code blocks.
:param convo: AgentConvo instance
:param standalone: True if this is a standalone conversation
:param code_changes_description: description of the code changes
:param file_content: content of the file being updated
:param file_name: name of the file being updated
:param files: list of files to send to the LLM
:return: updated file content
Note: if even this fails for any reason, the original content is returned instead.
"""
llm_response = convo.send_message('development/implement_changes.prompt', {
"full_output": True,
"standalone": standalone,
"code_changes_description": code_changes_description,
"file_content": file_content,
"file_name": file_name,
"files": files,
})
start_pattern = re.compile(r"^\s*```([a-z0-9]+)?\n")
end_pattern = re.compile(r"\n```\s*$")
llm_response = start_pattern.sub("", llm_response)
llm_response = end_pattern.sub("", llm_response)
return llm_response
def identify_file_to_change(self, code_changes_description: str, files: list[dict]) -> str:
"""
Identify file to change based on the code changes description
:param code_changes_description: description of the code changes
:param files: list of files to send to the LLM
:return: file to change
"""
convo = AgentConvo(self)
llm_response = convo.send_message('development/identify_files_to_change.prompt', {
"code_changes_description": code_changes_description,
"files": files,
}, GET_FILE_TO_MODIFY)
return llm_response["file"]
@staticmethod
def get_code_blocks(llm_response: str) -> list[tuple[str, str]]:
"""
Split the response into code block(s).
Ignores any content outside of code blocks.
:param llm_response: response from the LLM
:return: list of pairs of current and new blocks
"""
pattern = re.compile(
r"CURRENT_CODE:\n```([a-z0-9]+)?\n(.*?)\n```\nNEW_CODE:\n```([a-z0-9]+)?\n(.*?)\n```\nEND\s*",
re.DOTALL
)
pairs = []
for block in pattern.findall(llm_response):
pairs.append((block[1], block[3]))
return pairs
@staticmethod
def dedent(old_code: str, new_code: str) -> tuple[str, str]:
"""
Remove common indentation from `old_code` and `new_code`.
This is useful because the LLM will sometimes indent the code blocks MORE
than in the original file, leading to no matches. Since we have indent
compensation, we can just remove any extra indent as long as we do it
consistently for both old and new code block.
:param old_code: old code block
:param new_code: new code block
:return: tuple of (old_code, new_code) with common indentation removed
"""
old_lines = old_code.splitlines()
new_lines = new_code.splitlines()
indent = 0
while all(ol.startswith(" ") for ol in old_lines) and all(ol.startswith(" ") for ol in new_lines):
indent -= 1
old_lines = [ol[1:] for ol in old_lines]
new_lines = [nl[1:] for nl in new_lines]
return "\n".join(old_lines), "\n".join(new_lines)
@staticmethod
def replace(haystack: str, needle: str, replacement: str) -> str:
"""
Replace `needle` text in `haystack`, allowing that `needle` is not
indented the same as the matching part of `haystack` and
compensating for it.
:param haystack: text to search in
:param needle: text to search for
:param replacement: text to replace `needle` with
:return: `haystack` with `needle` replaced with `replacement`
Example:
>>> haystack = "def foo():\n pass"
>>> needle = "pass"
>>> replacement = "return 42"
>>> replace(haystack, needle, replacement)
"def foo():\n return 42"
If `needle` is not found in `haystack` even with indent compensation,
or if it's found multiple times, raise a ValueError.
"""
def indent_text(text: str, indent: int) -> str:
return "\n".join((" " * indent + line) for line in text.splitlines())
def indent_sensitive_match(haystack: str, needle: str) -> int:
"""
Check if 'needle' is in 'haystack' but compare full lines.
"""
# This is required so we don't match text "foo" (no indentation) with line " foo"
# (2 spaces indentation). We want exact matches so we know exact indentation needed.
haystack_with_line_start_stop_markers = "\n".join(f"\x00{line}\x00" for line in haystack.splitlines())
needle_with_line_start_stop_markers = "\n".join(f"\x00{line}\x00" for line in needle.splitlines())
return haystack_with_line_start_stop_markers.count(needle_with_line_start_stop_markers)
# Try from the largest indents to the smallest so that we know the correct indentation of
# single-line old blocks that would otherwise match with 0 indent as well. If these single-line
# old blocks were then replaced with multi-line blocks and indentation wasn't not correctly re-applied,
# the new multiline block would only have the first line correctly indented. We want to avoid that.
matching_old_blocks = []
for indent in range(128, -1, -1):
text = indent_text(needle, indent)
if text not in haystack:
# If there are empty lines in the old code, `indent_text` will indent them as well. The original
# file might not have them indented as they're empty, so it is useful to try without indenting
# those empty lines.
text = "\n".join(
(line if line.strip() else "")
for line
in text.splitlines()
)
n_matches = indent_sensitive_match(haystack, text)
for i in range(n_matches):
matching_old_blocks.append((indent, text))
if len(matching_old_blocks) == 0:
raise ValueError(
f"Old code block not found in the original file:\n```\n{needle}\n```\n"
"Old block *MUST* contain the exact same text (including indentation, empty lines, etc.) as the original file "
"in order to match."
)
if len(matching_old_blocks) > 1:
raise ValueError(
f"Old code block found more than once ({len(matching_old_blocks)} matches) in the original file:\n```\n{needle}\n```\n\n"
"Please provide larger blocks (more context) to uniquely identify the code that needs to be changed."
)
indent, text = matching_old_blocks[0]
indented_replacement = indent_text(replacement, indent)
return haystack.replace(text, indented_replacement)

View File

@@ -24,8 +24,7 @@ from helpers.Agent import Agent
from helpers.AgentConvo import AgentConvo
from utils.utils import should_execute_step, array_of_objects_to_string, generate_app_data
from helpers.cli import run_command_until_success, execute_command_and_check_cli_response, running_processes
from const.function_calls import FILTER_OS_TECHNOLOGIES, EXECUTE_COMMANDS, GET_TEST_TYPE, IMPLEMENT_TASK, \
COMMAND_TO_RUN, GET_FULLY_CODED_FILE
from const.function_calls import FILTER_OS_TECHNOLOGIES, EXECUTE_COMMANDS, GET_TEST_TYPE, IMPLEMENT_TASK, COMMAND_TO_RUN
from database.database import save_progress, get_progress_steps, update_app_status
from utils.utils import get_os_info
@@ -88,7 +87,7 @@ class Developer(Agent):
self.project.dot_pilot_gpt.chat_log_folder(i + 1)
convo_dev_task = AgentConvo(self)
convo_dev_task.send_message('development/task/breakdown.prompt', {
instructions = convo_dev_task.send_message('development/task/breakdown.prompt', {
"name": self.project.args['name'],
"app_type": self.project.args['app_type'],
"app_summary": self.project.project_description,
@@ -104,9 +103,13 @@ class Developer(Agent):
"task_type": 'feature' if self.project.finished else 'app'
})
instructions_prefix = " ".join(instructions.split()[:5])
instructions_postfix = " ".join(instructions.split()[-5:])
response = convo_dev_task.send_message('development/parse_task.prompt', {
'running_processes': running_processes,
'os': platform.system(),
'instructions_prefix': instructions_prefix,
'instructions_postfix': instructions_postfix,
}, IMPLEMENT_TASK)
steps = response['tasks']
convo_dev_task.remove_last_x_messages(2)
@@ -143,54 +146,30 @@ class Developer(Agent):
logger.warning('Testing at end of task failed')
break
def replace_old_code_comments(self, files_with_changes):
files_with_comments = [{**file, 'comments': [line for line in file['content'].split('\n') if '[OLD CODE]' in line]} for file in files_with_changes]
for file in files_with_comments:
if len(file['comments']) > 0:
fully_coded_file_convo = AgentConvo(self)
fully_coded_file_response = fully_coded_file_convo.send_message(
'development/get_fully_coded_file.prompt', {
'file': self.project.get_files([file['path']])[0],
'new_file': file,
}, GET_FULLY_CODED_FILE)
file['content'] = fully_coded_file_response['file_content']
return files_with_comments
def step_code_change(self, convo, task_description, step, i, test_after_code_changes):
if 'code_change_description' in step:
# TODO this should be refactored so it always uses the same function call
print(f'Implementing code changes for `{step["code_change_description"]}`')
code_monkey = CodeMonkey(self.project, self)
updated_convo = code_monkey.implement_code_changes(convo, task_description, step['code_change_description'],
step, i)
updated_convo = code_monkey.implement_code_changes(convo, step['code_change_description'], step)
if test_after_code_changes:
return self.test_code_changes(code_monkey, updated_convo)
else:
return {"success": True}
# TODO fix this - the problem is in GPT response that sometimes doesn't return the correct JSON structure
if 'code_change' not in step:
data = step
else:
data = step['code_change']
data = self.replace_old_code_comments([data])[0]
data = step['save_file']
self.project.save_file(data)
# TODO end
return {"success": True}
def step_modify_file(self, convo, task_description, step, i, test_after_code_changes):
data = step['modify_file']
print(f'Updating existing file {data["name"]}: {data["code_change_description"].splitlines()[0]}')
code_monkey = CodeMonkey(self.project, self)
code_monkey.implement_code_changes(convo, data['code_change_description'], data)
return {"success": True}
def step_command_run(self, convo, step, i, success_with_cli_response=False):
logger.info('Running command: %s', step['command'])
# TODO fix this - the problem is in GPT response that sometimes doesn't return the correct JSON structure
if isinstance(step['command'], str):
data = step
else:
data = step['command']
# TODO END
data = step['command']
additional_message = '' # 'Let\'s start with the step #0:\n' if i == 0 else f'So far, steps { ", ".join(f"#{j}" for j in range(i+1)) } are finished so let\'s do step #{i + 1} now.\n'
command_id = data['command_id'] if 'command_id' in data else None
@@ -405,9 +384,12 @@ class Developer(Agent):
# if need_to_see_output and 'cli_response' in result:
# result['user_input'] = result['cli_response']
elif step['type'] == 'code_change':
elif step['type'] in ['save_file', 'code_change']:
result = self.step_code_change(convo, task_description, step, i, test_after_code_changes)
elif step['type'] == 'modify_file':
result = self.step_modify_file(convo, task_description, step, i, test_after_code_changes)
elif step['type'] == 'human_intervention':
result = self.step_human_intervention(convo, step)
@@ -510,9 +492,14 @@ class Developer(Agent):
"user_input": user_feedback,
})
instructions_prefix = " ".join(iteration_description.split()[:5])
instructions_postfix = " ".join(iteration_description.split()[-5:])
llm_response = iteration_convo.send_message('development/parse_task.prompt', {
'running_processes': running_processes,
'os': platform.system(),
'instructions_prefix': instructions_prefix,
'instructions_postfix': instructions_postfix,
}, IMPLEMENT_TASK)
iteration_convo.remove_last_x_messages(2)

View File

@@ -42,11 +42,7 @@ class TechnicalWriter(Agent):
"files": self.project.get_all_coded_files(),
}, GET_DOCUMENTATION_FILE)
changes = self.project.developer.replace_old_code_comments([llm_response])
for file_data in changes:
self.project.save_file(file_data)
self.project.save_file(llm_response)
return convo
def create_api_documentation(self):

View File

@@ -1,4 +1,3 @@
from .Architect import Architect, ARCHITECTURE_STEP
from .CodeMonkey import CodeMonkey, IMPLEMENT_CHANGES, GET_FILES
from .Developer import Developer, ENVIRONMENT_SETUP_STEP
from .TechLead import TechLead

View File

@@ -1,123 +1,420 @@
import re
import os
from unittest.mock import patch, MagicMock
from dotenv import load_dotenv
load_dotenv()
from .CodeMonkey import CodeMonkey
from .Developer import Developer
from database.models.files import File
from database.models.development_steps import DevelopmentSteps
from helpers.Project import Project, update_file, clear_directory
from helpers.AgentConvo import AgentConvo
from test.test_utils import mock_terminal_size
SEND_TO_LLM = False
WRITE_TO_FILE = False
class TestCodeMonkey:
def setup_method(self):
name = 'TestDeveloper'
self.project = Project({
'app_id': 'test-developer',
'name': name,
'app_type': ''
},
name=name,
architecture=[],
user_stories=[],
current_step='coding',
)
self.project.set_root_path(os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)),
'../../../workspace/TestDeveloper')))
self.project.technologies = []
last_step = DevelopmentSteps()
last_step.id = 1
self.project.checkpoints = {'last_development_step': last_step}
self.project.app = None
self.developer = Developer(self.project)
self.codeMonkey = CodeMonkey(self.project, developer=self.developer)
@patch('helpers.AgentConvo.get_saved_development_step', return_value=None)
@patch('helpers.AgentConvo.save_development_step')
@patch('os.get_terminal_size', mock_terminal_size)
@patch.object(File, 'insert')
def test_implement_code_changes(self, mock_get_dev, mock_save_dev, mock_file_insert):
# Given
task_description = "High level description of the task"
code_changes_description = "Write the word 'Washington' to a .txt file"
self.project.get_all_coded_files = lambda: []
if SEND_TO_LLM:
convo = AgentConvo(self.codeMonkey)
else:
convo = MagicMock()
mock_responses = [
# [],
{'files': [{
'content': 'Washington',
'description': "A new .txt file with the word 'Washington' in it.",
'name': 'washington.txt',
'path': 'washington.txt'
}]}
]
convo.send_message.side_effect = mock_responses
if WRITE_TO_FILE:
self.codeMonkey.implement_code_changes(convo, task_description, code_changes_description, {})
else:
# don't write the file, just
with patch.object(Project, 'save_file') as mock_save_file:
# When
self.codeMonkey.implement_code_changes(convo, task_description, code_changes_description, {})
# Then
mock_save_file.assert_called_once()
called_data = mock_save_file.call_args[0][0]
assert re.match(r'\w+\.txt$', called_data['name'])
assert (called_data['path'] == '/' or called_data['path'] == called_data['name'])
assert called_data['content'] == 'Washington'
@patch('helpers.AgentConvo.get_saved_development_step')
@patch('helpers.AgentConvo.save_development_step')
@patch('os.get_terminal_size', mock_terminal_size)
@patch.object(File, 'insert')
def test_implement_code_changes_with_read(self, mock_get_dev, mock_save_dev, mock_file_insert):
# Given
task_description = "High level description of the task"
code_changes_description = "Read the file called file_to_read.txt and write its content to a file called output.txt"
workspace = self.project.root_path
update_file(os.path.join(workspace, 'file_to_read.txt'), 'Hello World!\n')
self.project.get_all_coded_files = lambda: []
if SEND_TO_LLM:
convo = AgentConvo(self.codeMonkey)
else:
convo = MagicMock()
mock_responses = [
# ['file_to_read.txt', 'output.txt'],
{'files': [{
'content': 'Hello World!\n',
'description': 'This file is the output file. The content of file_to_read.txt is copied into this file.',
'name': 'output.txt',
'path': 'output.txt'
}]}
]
convo.send_message.side_effect = mock_responses
if WRITE_TO_FILE:
self.codeMonkey.implement_code_changes(convo, task_description, code_changes_description, {})
else:
with patch.object(Project, 'save_file') as mock_save_file:
# When
self.codeMonkey.implement_code_changes(convo, task_description, code_changes_description, {})
# Then
clear_directory(workspace)
mock_save_file.assert_called_once()
called_data = mock_save_file.call_args[0][0]
assert called_data['name'] == 'output.txt'
assert (called_data['path'] == '/' or called_data['path'] == called_data['name'])
assert called_data['content'] == 'Hello World!\n'
from unittest.mock import patch, MagicMock, call
from os.path import normpath, sep
import pytest
from helpers.agents.CodeMonkey import CodeMonkey
from const.function_calls import GET_FILE_TO_MODIFY
@pytest.mark.parametrize(
("content", "expected_blocks"),
[
("", []),
("no code blocks here", []),
("one\n```\ncode block\n```\nwithout CURRENT/NEW tags", []),
(
"Change\nCURRENT_CODE:\n```python\nold\n```\nNEW_CODE:\n```\nnew\n```\nEND\n",
[("old", "new")]
),
(
"\n".join([
"Code with markdown blocks in it",
"CURRENT_CODE:",
"```markdown",
"# Title",
"",
"```python",
"print('hello world')",
"```",
"Rest of markdown",
"```",
"NEW_CODE:",
"```markdown",
"# Title",
"",
"```python",
"print('goodbye world')",
"```",
"New markdown text here",
"```",
"END"
]),
[
(
"# Title\n\n```python\nprint('hello world')\n```\nRest of markdown",
"# Title\n\n```python\nprint('goodbye world')\n```\nNew markdown text here",
)
]
)
]
)
def test_get_code_blocks(content, expected_blocks):
code_monkey = CodeMonkey(None, None)
assert code_monkey.get_code_blocks(content) == expected_blocks
@pytest.mark.parametrize(
("haystack", "needle", "result", "error"),
[
### Oneliner old blocks ###
# Simple match
("first\nsecond\nthird", "second", "first\n@@NEW@@\nthird", None),
# No match
("first\nsecond\nthird", "fourth", None, "not found"),
# Too many matches on the same indentation level
("line\nline", "line", None, "found more than once"),
# Match, replacement should be indented
("first\n second\nthird", "second", "first\n @@NEW@@\nthird", None),
# Too many matches, on different indentation levels
("line\n line", "line", None, "found more than once"),
### Multiline old blocks ###
# Simple match
("first\nsecond\nthird", "second\nthird", "first\n@@NEW@@", None),
# No match
("first\nsecond\nthird", "second\n third", None, "not found"),
# Too many matches on the same indentation level
("a\nb\nc\nd\na\nb", "a\nb", None, "found more than once"),
# Too many matches on different indentation levels
("a\nb\nc\nd\n a\n b", "a\nb", None, "found more than once"),
# Match, replacement should be indented
("first\n second\n third", "second\nthird", "first\n @@NEW@@", None),
### Multiline with empty lines ###
# Simple match
("first\nsecond\n\nthird", "second\n\nthird", "first\n@@NEW@@", None),
# Indented match with empty lines also indentend
("first\n second\n \n third", "second\n\nthird", "first\n @@NEW@@", None),
# Indented match with empty lines not indentend
("first\n second\n\n third", "second\n\nthird", "first\n @@NEW@@", None),
]
)
def test_replace(haystack, needle, result, error):
code_monkey = CodeMonkey(None, None)
if error:
with pytest.raises(ValueError, match=error):
code_monkey.replace(haystack, needle, "@@NEW@@")
else:
assert code_monkey.replace(haystack, needle, "@@NEW@@") == result
@patch("helpers.agents.CodeMonkey.AgentConvo")
def test_identify_file_to_change(MockAgentConvo):
mock_convo = MockAgentConvo.return_value
mock_convo.send_message.return_value = {"file": "file.py"}
files = CodeMonkey(None, None).identify_file_to_change("some description", [])
assert files == "file.py"
mock_convo.send_message.assert_called_once_with(
"development/identify_files_to_change.prompt",
{
"code_changes_description": "some description",
"files": []
},
GET_FILE_TO_MODIFY
)
def test_dedent():
old_code = "\n".join([
" def foo():",
" print('bar')",
])
new_code = "\n".join([
" def bar():",
" print('foo')",
])
expected_old = "\n".join([
" def foo():",
" print('bar')",
])
expected_new = "\n".join([
"def bar():",
" print('foo')",
])
result_old, result_new = CodeMonkey.dedent(old_code, new_code)
assert result_old == expected_old
assert expected_new == result_new
def test_codemonkey_simple():
mock_project = MagicMock()
mock_project.get_all_coded_files.return_value = [
{
"path": "",
"name": "main.py",
"content": "one to the\nfoo\nto the three to the four"
},
]
mock_project.get_full_file_path.return_value = ("", normpath("/path/to/main.py"))
mock_convo = MagicMock()
mock_convo.send_message.return_value = "## Change\nCURRENT_CODE:\n```\nfoo\n```\nNEW_CODE:\n```\nbar\n```\nEND"
cm = CodeMonkey(mock_project, None)
cm.implement_code_changes(
mock_convo,
"Modify all references from `foo` to `bar`",
{
"path": sep,
"name": "main.py",
}
)
mock_project.get_all_coded_files.assert_called_once()
mock_project.get_full_file_path.assert_called_once_with(sep, "main.py")
mock_convo.send_message.assert_called_once_with(
"development/implement_changes.prompt", {
"full_output": False,
"standalone": False,
"code_changes_description": "Modify all references from `foo` to `bar`",
"file_content": "one to the\nfoo\nto the three to the four",
"file_name": "main.py",
"files": mock_project.get_all_coded_files.return_value,
})
mock_project.save_file.assert_called_once_with({
"path": sep,
"name": "main.py",
"content": "one to the\nbar\nto the three to the four"
})
@patch("helpers.agents.CodeMonkey.trace_code_event")
def test_codemonkey_retry(trace_code_event):
file_content = (
"one to the\nfoo\nto the three to the four\n"
"the rest of this file is filler so it's big enought not to "
"trigger the full replace fallback immediately upon the first failure"
)
mock_project = MagicMock()
mock_project.get_all_coded_files.return_value = [
{
"path": "",
"name": "main.py",
"content": file_content,
},
]
mock_project.get_full_file_path.return_value = ("", normpath("/path/to/main.py"))
mock_convo = MagicMock()
mock_convo.send_message.side_effect = [
# Incorrect match
"## Change\nCURRENT_CODE:\n```\ntwo\n```\nNEW_CODE:\n```\nbar\n```\nEND\n",
# Corrected match on retry
"Apologies, here is the corrected version. ## Change\nCURRENT_CODE:\n```\n foo\n```\nNEW_CODE:\n```\n bar\n```\nEND\n",
]
cm = CodeMonkey(mock_project, None)
cm.implement_code_changes(
mock_convo,
"Modify all references from `foo` to `bar`",
{
"path": sep,
"name": "main.py",
}
)
mock_project.get_all_coded_files.assert_called_once()
mock_project.get_full_file_path.assert_called_once_with(sep, "main.py")
mock_convo.send_message.assert_has_calls([
call(
"development/implement_changes.prompt", {
"full_output": False,
"standalone": False,
"code_changes_description": "Modify all references from `foo` to `bar`",
"file_content": file_content,
"file_name": "main.py",
"files": mock_project.get_all_coded_files.return_value,
}
),
call(
"utils/llm_response_error.prompt", {
"error": (
"Error in change 1:\n"
"Old code block not found in the original file:\n```\ntwo\n```\n"
"Old block *MUST* contain the exact same text (including indentation, empty lines, etc.) "
"as the original file in order to match."
),
}
)
])
mock_project.save_file.assert_called_once_with({
"path": sep,
"name": "main.py",
"content": file_content.replace("foo", "bar"),
})
trace_code_event.assert_called_once_with(
"codemonkey-file-update-error",
{
"error": "replace-errors",
"llm_response": "## Change\nCURRENT_CODE:\n```\ntwo\n```\nNEW_CODE:\n```\nbar\n```\nEND\n",
"details": [(1, (
'Old code block not found in the original file:\n```\ntwo\n```\n'
'Old block *MUST* contain the exact same text (including indentation, empty lines, etc.) '
'as the original file in order to match.'
))]
}
)
@patch("helpers.agents.CodeMonkey.trace_code_event")
def test_codemonkey_fallback(trace_code_event):
mock_project = MagicMock()
mock_project.get_all_coded_files.return_value = [
{
"path": "",
"name": "main.py",
"content": "one to the\nfoo\nto the three to the four"
},
]
mock_project.get_full_file_path.return_value = ("", normpath("/path/to/main.py"))
mock_convo = MagicMock()
mock_convo.send_message.side_effect = [
# Incorrect match (END within block), will cause immediate fallback because of short file
"1 ## Change\nCURRENT_CODE:\n```\nfoo\n```\nNEW_CODE:\n```\nbar\nEND\n```\n",
# Fallback returns entire new file
"```\none to the\nbar\nto the three to the four\n```\n",
]
cm = CodeMonkey(mock_project, None)
cm.implement_code_changes(
mock_convo,
"Modify all references from `foo` to `bar`",
{
"path": sep,
"name": "main.py",
}
)
mock_project.get_all_coded_files.assert_called_once()
mock_project.get_full_file_path.assert_called_once_with(sep, "main.py")
mock_convo.send_message.assert_has_calls([
call(
"development/implement_changes.prompt", {
"full_output": False,
"standalone": False,
"code_changes_description": "Modify all references from `foo` to `bar`",
"file_content": "one to the\nfoo\nto the three to the four",
"file_name": "main.py",
"files": mock_project.get_all_coded_files.return_value,
}
),
call(
'development/implement_changes.prompt', {
"full_output": True,
"standalone": False,
"code_changes_description": "Modify all references from `foo` to `bar`",
"file_content": "one to the\nfoo\nto the three to the four",
"file_name": "main.py",
"files": mock_project.get_all_coded_files.return_value,
}
)
])
mock_project.save_file.assert_called_once_with({
"path": sep,
"name": "main.py",
"content": "one to the\nbar\nto the three to the four"
})
trace_code_event.assert_has_calls([
call(
'codemonkey-file-update-error',
{
'error': 'error-parsing-blocks',
'llm_response': '1 ## Change\nCURRENT_CODE:\n```\nfoo\n```\nNEW_CODE:\n```\nbar\nEND\n```\n'
}
),
call(
'codemonkey-file-update-error',
{
'error': 'fallback-complete-replace',
'llm_response': '1 ## Change\nCURRENT_CODE:\n```\nfoo\n```\nNEW_CODE:\n```\nbar\nEND\n```\n'
}
)
])
@patch("helpers.agents.CodeMonkey.trace_code_event")
@patch("helpers.agents.CodeMonkey.get_file_contents")
@patch("helpers.agents.CodeMonkey.AgentConvo")
def test_codemonkey_implement_changes_after_debugging(MockAgentConvo, mock_get_file_contents, trace_code_event):
"""
Test that the flow to figure out files that need to be changed
(which happens after debugging where we only have a description of the
changes needed, not file name).
Also test standalone conversation (though that's not happening after debugging).
"""
mock_project = MagicMock()
mock_project.get_all_coded_files.return_value = []
mock_project.get_full_file_path.return_value = ("", "/path/to/main.py")
mock_convo = MockAgentConvo.return_value
mock_convo.send_message.return_value = "## Change\nCURRENT_CODE:\n```\nfoo\n```\nNEW_CODE:\n```\nbar\n```\nEND"
mock_get_file_contents.return_value = {
"name": "main.py",
"path": "",
"content": "one to the\nfoo\nto the three to the four",
"full_path": "/path/to/main.py",
}
cm = CodeMonkey(mock_project, None)
with patch.object(cm, "identify_file_to_change") as mock_identify_file_to_change:
mock_identify_file_to_change.return_value = "/main.py"
cm.implement_code_changes(
None,
"Modify all references from `foo` to `bar`",
{},
)
MockAgentConvo.assert_called_once_with(cm)
mock_project.get_all_coded_files.assert_called_once()
mock_project.get_full_file_path.assert_called_once_with("/", "main.py")
mock_convo.send_message.assert_called_once_with(
"development/implement_changes.prompt", {
"full_output": False,
"standalone": True,
"code_changes_description": "Modify all references from `foo` to `bar`",
"file_content": "one to the\nfoo\nto the three to the four",
"file_name": "main.py",
"files": mock_project.get_all_coded_files.return_value,
})
mock_project.save_file.assert_called_once_with({
"path": "/",
"name": "main.py",
"content": "one to the\nbar\nto the three to the four"
})
trace_code_event.assert_not_called()
@patch("helpers.agents.CodeMonkey.trace_code_event")
@patch("helpers.agents.CodeMonkey.get_file_contents")
def test_codemonkey_original_file_not_found(mock_get_file_contents, _trace_code_event):
mock_project = MagicMock()
mock_project.get_all_coded_files.return_value = []
mock_project.get_full_file_path.return_value = ("", normpath("/path/to/main.py"))
mock_convo = MagicMock()
mock_convo.send_message.return_value = "```\none to the\nbar\nto the three to the four\n```\n"
mock_get_file_contents.side_effect = ValueError("File not found: /path/to/main.py")
cm = CodeMonkey(mock_project, None)
cm.implement_code_changes(
mock_convo,
"Modify all references from `foo` to `bar`",
{
"path": sep,
"name": "main.py",
}
)
mock_project.get_all_coded_files.assert_called_once()
mock_project.get_full_file_path.assert_called_once_with(sep, "main.py")
mock_convo.send_message.assert_called_once_with(
'development/implement_changes.prompt', {
"full_output": True,
"standalone": False,
"code_changes_description": "Modify all references from `foo` to `bar`",
"file_content": "",
"file_name": "main.py",
"files": mock_project.get_all_coded_files.return_value,
}
)
mock_project.save_file.assert_called_once_with({
"path": sep,
"name": "main.py",
"content": "one to the\nbar\nto the three to the four"
})

View File

@@ -1,11 +1,15 @@
# main.py
from __future__ import print_function, unicode_literals
import builtins
import os
import sys
import traceback
from dotenv import load_dotenv
try:
from dotenv import load_dotenv
except ImportError:
raise RuntimeError('Python environment for GPT Pilot is not completely set up: required package "python-dotenv" is missing.') from None
load_dotenv()
from utils.style import color_red
@@ -40,15 +44,19 @@ if __name__ == "__main__":
ask_feedback = True
project = None
run_exit_fn = True
args = init()
try:
# sys.argv.append('--ux-test=' + 'continue_development')
args = init()
builtins.print, ipc_client_instance = get_custom_print(args)
if '--api-key' in args:
os.environ["OPENAI_API_KEY"] = args['--api-key']
if '--api-endpoint' in args:
os.environ["OPENAI_ENDPOINT"] = args['--api-endpoint']
if '--get-created-apps-with-steps' in args:
run_exit_fn = False
@@ -110,4 +118,3 @@ if __name__ == "__main__":
project.finish_loading()
if run_exit_fn:
exit_gpt_pilot(project, ask_feedback)
sys.exit(0)

View File

@@ -1,18 +0,0 @@
Here is how the file `{{ file.path }}/{{ file.name }}` looks like right now:
```
{{ file.content }}
```
And here is the new implementation for the same file:
```
{{ new_file.content }}
```
Now, implement the new changes into the previously implemented file and return the entirely coded file. Do not add any lines of code that are not in the previously coded file or the new implementation but only combine those two.
**IMPORTANT**
All lines of code in the new implementation should be present. From the old implementation, only take the ones that are replaced with the comment `[OLD CODE]` in the new implementation
**IMPORTANT**
Make sure that you respond with all lines of code that are replaced by these comments, including all control structures, error handling, and any other relevant logic that was in the original code.
Under no circumstances should you ever leave any part of the code snippet unwritten. Every single line of code that exists in the place where the comment lives right now should be replaced. Do not include any code that is above or below the comment but only the code that should be in the position of the comment.

View File

@@ -0,0 +1,22 @@
You're a senior software developer implementing changes in one file in the project.
Based on the provided instructions and full file list, identify the file
that needs to be modified.
All files in the project:
{% for file in files %}
### {{ file['path'] }}/{{ file['name'] }}
```
{{ file['content'] }}
```
{% endfor %}
Instructions:
{{ code_changes_description }}
Output *ONLY* the file path, relative to project root, in a single Markdown code block,
without any comments or explanation, like this:
```
path/to/file
```

View File

@@ -1,26 +1,115 @@
{{ files_list }}
Here is a description of changes for a task you're currently working on:
I need to modify file `{{ file_name }}` that currently looks like this:
```
{{ task_description }}
{{ file_content }}
```
This task is broken down into steps. Please tell me the changes needed to implement the step #{{ step_index }} that is described like this:
This file needs to be modified by these instructions:
---------------------instructions------------------------------
{{ code_changes_description }}
----------------------end_of_instructions-----------------------------
{% if full_output %}
I want you to implement the instructions and show me the COMPLETE NEW VERSION of this file.
**IMPORTANT**: Your reply should not omit any code in the new implementation or substitute anything with comments like `// .. rest of the code goes here ..`, because I will overwrite the existing file with the content you provide. Output ONLY the content for this file, without additional explanation.
{% else %}
I want you to implement the instructions and show me the exact changes (`diff`) in the file `{{ file_name }}`. Reply only with the modifications (`diff`) in the following format:
-----------------------format----------------------------
CURRENT_CODE:
```
{{ step_description }}
(All lines of code from specific code block in the current file that will be replaced by the code under NEW_CODE.)
```
Within the file modifications, anything needs to be written by the user, add the comment in the same line as the code that starts with `// INPUT_REQUIRED {input_description}` where `input_description` is a description of what needs to be added here by the user. Just make sure that you put comments only inside files that support comments (e.g. not in JSON files)..
NEW_CODE:
```
(All lines of code that will replace the code under CURRENT_CODE. That includes new lines of code and old lines of code that are not being changed but are part of that code block.)
```
END
------------------------end_of_format---------------------------
**IMPORTANT**
When you think about in which file should the new code go to, always try to make files as small as possible and put code in more smaller files rather than in one big file. Whenever a file becomes too large (more than 50 lines of code) split it into smaller files.
Here are rules how to give good response. You have to strictly follow all rules at all times:
**IMPORTANT**
When you want to add a comment that tells the user to add the previous implementation at that place, make sure that the comment starts with `[OLD CODE]` and add a description of what old code should be inserted here. For example, `[OLD CODE] Login route`.
Rule #1:
This is most important rule and there must never be reason to break this rule!
When the instructions contain hints such as `# .. insert existing code here ...`, it is imperative to interpret and insert the relevant code from the original. Never omit any code that belongs in the new block, and never replace any code with comments such as `// the rest of the code goes here`, '# existing code from another file', or similar, even if the instructions explicitly request it!
If the instruction examples reference existing code to be pasted in place, always use the specified code from the previous messages in this conversation instead of copying the comment, as illustrated in the following example:
------------------------example_1---------------------------
Instructions: "Rename function increase() { // ... existing code } to function inc() { // ... existing code } and increase counter by 10 instead of 1."
------------------------BAD response for example_1:---------------------------
CURRENT_CODE:
```
function increase() {
// ... existing code
}
```
NEW_CODE:
```
function inc() {
// ... existing code
return value + 10;
}
```
------------------------GOOD response for example_1:---------------------------
{% if 'code_change' in step %}
{% for file in step['code_change'] %}
**IMPORTANT**
Implement changes only for the file `{{ step['code_change']['path'] }}` and no other files.
{% endfor %}
CURRENT_CODE:
```
function increase(value) {
if (typeof value !== 'number') {
throw new Error('Argument must be number');
}
return value + 1;
}
```
NEW_CODE:
```
function inc(value) {
if (typeof value !== 'number') {
throw new Error('Argument must be number');
}
return value + 10;
}
END
```
------------------------end_of_example_1---------------------------
Rule #2:
For each change that needs to be done, you must show exactly one CURRENT_CODE code block and one NEW_CODE code block. You can think of this as difference (`diff`) between the current implementation and the new implementation.
If there are no lines of code that need to be replaced by the NEW_CODE (if the NEW_CODE needs to be added into the CURRENT_CODE), show a couple of lines of code in the CURRENT_CODE before the place where NEW_CODE needs to be added.
Here is an example of how to add one line `i--;` in the for loop:
------------------------example_2---------------------------
CURRENT_CODE:
```
let i = 0;
i++;
for (let j = 0; j < 100; j++) {
```
NEW_CODE:
```
let i = 0;
i++;
for (let j = 0; j < 100; j++) {
i--;
```
END
------------------------end_of_example_2---------------------------
Here's an example how to add code to the beginning of the file:
------------------------example_3---------------------------
CURRENT_CODE:
```
const app = express();
const bodyParser = require('body-parser');
```
NEW_CODE:
```
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
```
END
------------------------end_of_example_3---------------------------
Rule #3:
Do not show the entire file under CURRENT_CODE and NEW_CODE but only the lines that need to be replaced. If any lines should be left as they are in CURRENT_CODE, do not write them.
Rule #4:
You must output the CURRENT_CODE exactly as it is in the original file, including the indentation from the original code, as it will be used for search-replace, and it should only match the original file in ONE place.
In the NEW_CODE, remember to follow the same coding style that is used in the rest of the file. Pay special attention to the indentation of the new code and make sure to include all the required old and new code, without omitting anything.
Pay very close attention to parenthesis and make sure that when CURRENT_CODE is replaced with NEW_CODE there are no extra parenthesis or any parenthesis missing.
{% endif %}

View File

@@ -1,10 +1,49 @@
Ok, now, take your previous message and convert it to actionable items. An item might be a code change or a command run. When you need to change code, make sure that you put the entire content of the file in the value of `content` key even though you will likely copy and paste the most of the previous message. The commands must be able to run on a {{ os }} machine.
**IMPORTANT**
Within the file modifications, anything needs to be written by the user, add the comment in the same line as the code that starts with `// INPUT_REQUIRED {input_description}` where `input_description` is a description of what needs to be added here by the user. Just make sure that you put comments only inside files that support comments (e.g. not in JSON files).
**IMPORTANT**
When you want to add a comment that tells the user to add the previous implementation at that place, make sure that the comment starts with `[OLD CODE]` and add a description of what old code should be inserted here. For example, `[OLD CODE] Login route`.
**IMPORTANT**
When you think about in which file should the new code go to, always try to make files as small as possible and put code in more smaller files rather than in one big file. Whenever a file becomes too large (more than 50 lines of code) split it into smaller files.
Ok, now, take your previous message that starts with `{{ instructions_prefix }}` and ends with `{{ instructions_postfix }}` and convert it to a list of actionable steps that will be executed by a machine. Analyze the entire message, think step by step and make sure that you don't omit any information when converting this message to steps.
Each step can be either:
* `command` - command to run (must be able to run on a {{ os }} machine, assume current working directory is project root folder)
* `save_file` - create new or update ONE existing file; use this if the existing file is smaller than 20 lines or if many lines need to be changed
* `modify_file` - update ONE existing file; use this if the existing file is larger than 20 lines and only a few lines need to be updated
If the step is of type `save_file` or `modify_file`, it needs to contain instructions on how to change only ONE file.
**IMPORTANT**: In `code_change_description` field of `modify_file` step, you must provide full information (including code samples, if any) from the previous message, so that the developer can correctly implement the change. For `save_file`, you MUST include FULL file contents, without omitting anything or adding comments like `// rest of the code goes here`.
Examples:
------------------------example_1---------------------------
```
{
"tasks": [
{
"type": "modify_file",
"modify_file": {
"name": "server.js",
"path": "/server.ejs",
"code_change_description": "Update code to use port from environment instead of hardcoding it.\nReplace this line:\nconst port = 3001;\nwith\nconst port = process.env.PORT || 3001;\n",
},
},
{
"type": "modify_file",
"modify_file": {
"name": "server.js",
"path": "/server.ejs",
"code_change_description": "Within findByEmail() method of User model, replace `return await User.find({email});` with a try/catch block:\ntry\n{ return await User.find({email});\n} catch (err)\n{ return null; }\n",
},
},
{
"type": "save_file",
"save_file": {
"name": "README.md",
"path": "/README.md",
"content": "# Example Readme\n\nThis is an example readme for the example project.\n\nThanks to everyone who contributes to this repository!\n"
}
}
]
}
```
------------------------end_of_example_1---------------------------
Within the file modifications, mark any settings that the user must configure manually with `// INPUT_REQUIRED {input_description}` comment, where `input_description` is a description of what needs to be added here by the user. Use appropriate syntax for comments in the file you're saving. If the file type doesn't support comments (eg JSON), don't add any.
Remember: you must provide all the information (context) for file modification steps that you thought of in the previous message, so that the developer can correctly implement your changes. This is very important!

View File

@@ -28,9 +28,6 @@ After all the code is finished, a human developer will check if the app works th
Now, tell me all the code that needs to be written to implement ONLY this task and have it fully working and all commands that need to be run to implement this task.
**IMPORTANT**
When you think about in which file should the new code go to, always try to make files as small as possible and put code in more smaller files rather than in one big file. Whenever a file becomes too large (more than 50 lines of code) split it into smaller files.
**IMPORTANT**
{%- if task_type == 'app' %}
Remember, I created an empty folder where I will start writing files that you tell me and that are needed for this app.

View File

@@ -0,0 +1 @@
You are a full stack software developer who works in a software development agency. You write very modular code. Your job is to implement tasks that your tech lead assigns you.

View File

@@ -0,0 +1,5 @@
There was an error processing your response:
{{ error }}
Please think carefully and try again.

View File

@@ -58,4 +58,3 @@ def test_end_to_end(endpoint, model, monkeypatch):
# When
with patch('utils.questionary.questionary', mock_questionary):
project.start()

View File

@@ -5,7 +5,7 @@ import sys
import uuid
from getpass import getuser
from database.database import get_app, get_app_by_user_workspace
from utils.style import color_green_bold, style_config
from utils.style import color_green_bold, color_red, style_config
from utils.utils import should_execute_step
from const.common import STEPS
@@ -47,7 +47,11 @@ def get_arguments():
if 'app_id' in arguments:
if app is None:
app = get_app(arguments['app_id'])
try:
app = get_app(arguments['app_id'])
except ValueError as err:
print(color_red(f"Error: {err}"))
sys.exit(-1)
arguments['app_type'] = app.app_type
arguments['name'] = app.name

View File

@@ -22,8 +22,8 @@ def send_telemetry(path_id=None, event='pilot-exit'):
try:
response = requests.post("https://api.pythagora.io/telemetry", json=telemetry_data)
response.raise_for_status()
except requests.RequestException as err:
print(f"Failed to send telemetry data: {err}")
except: # noqa
pass
def send_feedback(feedback, path_id):
@@ -42,6 +42,29 @@ def send_feedback(feedback, path_id):
print(f"Failed to send feedback data: {err}")
def trace_code_event(name: str, data: dict):
"""
Record a code event to trace potential logic bugs.
:param name: name of the event
:param data: data to send with the event
"""
path_id = get_path_id()
# Prepare the telemetry data
telemetry_data = {
"pathId": path_id,
"event": f"trace-{name}",
"data": data,
}
try:
response = requests.post("https://api.pythagora.io/telemetry", json=telemetry_data)
response.raise_for_status()
except: # noqa
pass
def get_path_id():
return telemetry.telemetry_id