From eea80d1d9c8588ca635428c8c01f9aa347993d28 Mon Sep 17 00:00:00 2001 From: Zvonimir Sabljic Date: Fri, 11 Aug 2023 10:53:46 +0200 Subject: [PATCH 1/3] A couple of fixes --- euclid/const/llm.py | 2 +- euclid/database/database.py | 40 +++++++++++++------ euclid/helpers/Project.py | 14 ++++++- euclid/helpers/agents/Developer.py | 8 ++-- euclid/helpers/cli.py | 11 ++--- euclid/prompts/dev_ops/ran_command.prompt | 2 +- .../development/implement_changes.prompt | 2 +- euclid/utils/llm_connection.py | 2 +- 8 files changed, 53 insertions(+), 28 deletions(-) diff --git a/euclid/const/llm.py b/euclid/const/llm.py index 9c10992..6fc84c8 100644 --- a/euclid/const/llm.py +++ b/euclid/const/llm.py @@ -1,4 +1,4 @@ -MIN_TOKENS_FOR_GPT_RESPONSE = 60 +MIN_TOKENS_FOR_GPT_RESPONSE = 600 MAX_GPT_MODEL_TOKENS = 8192 MAX_QUESTIONS = 3 END_RESPONSE = "EVERYTHING_CLEAR" diff --git a/euclid/database/database.py b/euclid/database/database.py index 6d00d0b..3af5f90 100644 --- a/euclid/database/database.py +++ b/euclid/database/database.py @@ -179,11 +179,11 @@ def hash_and_save_step(Model, app_id, hash_data_args, data_fields, message): try: inserted_id = (Model - .insert(**data_to_insert) - .on_conflict(conflict_target=[Model.app, Model.hash_id], - preserve=fields_to_preserve, - update={}) - .execute()) + .insert(**data_to_insert) + .on_conflict(conflict_target=[Model.app, Model.hash_id], + preserve=fields_to_preserve, + update=data_fields) + .execute()) record = Model.get_by_id(inserted_id) print(colored(f"{message} with id {record.id}", "yellow")) @@ -266,18 +266,19 @@ def get_user_input_from_hash_id(project, query): return user_input def delete_all_subsequent_steps(project): - delete_subsequent_steps(DevelopmentSteps, project.checkpoints['last_development_step'], 'previous_step') - delete_subsequent_steps(CommandRuns, project.checkpoints['last_command_run'], 'previous_step') - delete_subsequent_steps(UserInputs, project.checkpoints['last_user_input'], 'previous_step') + delete_subsequent_steps(DevelopmentSteps, project.checkpoints['last_development_step']) + delete_subsequent_steps(CommandRuns, project.checkpoints['last_command_run']) + delete_subsequent_steps(UserInputs, project.checkpoints['last_user_input']) -def delete_subsequent_steps(model, step, step_field_name): +def delete_subsequent_steps(model, step): if step is None: return print(colored(f"Deleting subsequent {model.__name__} steps after {step.id}", "red")) - subsequent_step = model.get_or_none(**{step_field_name: step.id}) - if subsequent_step: - delete_subsequent_steps(model, subsequent_step, step_field_name) - subsequent_step.delete_instance() + subsequent_steps = model.select().where(model.previous_step == step.id) + for subsequent_step in subsequent_steps: + if subsequent_step: + delete_subsequent_steps(model, subsequent_step) + subsequent_step.delete_instance() def get_all_connected_steps(step, previous_step_field_name): """Recursively get all steps connected to the given step.""" @@ -288,6 +289,11 @@ def get_all_connected_steps(step, previous_step_field_name): prev_step = getattr(prev_step, previous_step_field_name) return connected_steps +def delete_all_steps_from_app(app): + models = [DevelopmentSteps, CommandRuns, UserInputs] + for model in models: + model.delete().where(model.app == app).execute() + def delete_unconnected_steps_from(step, previous_step_field_name): if step is None: return @@ -303,6 +309,14 @@ def delete_unconnected_steps_from(step, previous_step_field_name): print(colored(f"Deleting unconnected {step.__class__.__name__} step {unconnected_step.id}", "red")) unconnected_step.delete_instance() +def save_file_description(project, path, name, description): + (File.insert(app=project.app, path=path, name=name, description=description) + .on_conflict( + conflict_target=[File.app, File.name, File.path], + preserve=[], + update={'description': description}) + .execute()) + def create_tables(): with database: database.create_tables([ diff --git a/euclid/helpers/Project.py b/euclid/helpers/Project.py index bf98a40..eba0717 100644 --- a/euclid/helpers/Project.py +++ b/euclid/helpers/Project.py @@ -3,7 +3,7 @@ import os from termcolor import colored from const.common import IGNORE_FOLDERS from database.models.app import App -from database.database import get_app, delete_unconnected_steps_from +from database.database import get_app, delete_unconnected_steps_from, delete_all_steps_from_app from utils.questionary import styled_text from helpers.files import get_files_content, clear_directory from helpers.cli import build_directory_tree @@ -30,8 +30,16 @@ class Project: 'last_command_run': None, 'last_development_step': None, } + if 'skip_until_dev_step' in args: + self.skip_until_dev_step = args['skip_until_dev_step'] + if args['skip_until_dev_step'] == '0': + delete_all_steps_from_app(args['app_id']) + self.skip_steps = False + else: + self.skip_until_dev_step = None + self.skip_steps = True self.skip_steps = False if ('skip_until_dev_step' in args and args['skip_until_dev_step'] == '0') else True - self.skip_until_dev_step = args['skip_until_dev_step'] if 'skip_until_dev_step' in args else None + # TODO make flexible # self.root_path = get_parent_folder('euclid') self.root_path = '' @@ -99,6 +107,8 @@ class Project: file_path = file_path.replace('./', '', 1).rstrip(file_name) if not file_path.endswith('/'): file_path = file_path + '/' + if not file_path.startswith('/'): + file_path = '/' + file_path return self.root_path + file_path + file_name def save_files_snapshot(self, development_step_id): diff --git a/euclid/helpers/agents/Developer.py b/euclid/helpers/agents/Developer.py index 07d9a85..bde8919 100644 --- a/euclid/helpers/agents/Developer.py +++ b/euclid/helpers/agents/Developer.py @@ -154,19 +154,19 @@ class Developer(Agent): 'development/task/step_check.prompt', {}, GET_TEST_TYPE) - + if test_type == 'command_test': run_command_until_success(command['command'], command['timeout'], convo) elif test_type == 'automated_test': code_monkey.implement_code_changes(convo, automated_test_description, 0) elif test_type == 'manual_test': # TODO make the message better - response = self.project.ask_for_human_intervention( + user_feedback = self.project.ask_for_human_intervention( 'Message from Euclid: I need your help. Can you please test if this was successful?', manual_test_description ) - if response is not None and response != 'DONE': - self.test_code_changes(code_monkey, convo) + if user_feedback is not None: + debug(convo, user_input=user_feedback, issue_description=manual_test_description) def implement_step(self, convo, step_index, type, description): # TODO remove hardcoded folder path diff --git a/euclid/helpers/cli.py b/euclid/helpers/cli.py index 2a72fee..207b3e7 100644 --- a/euclid/helpers/cli.py +++ b/euclid/helpers/cli.py @@ -45,6 +45,12 @@ def execute_command(project, command, timeout=5000): timeout = min(max(timeout, MIN_COMMAND_RUN_TIME), MAX_COMMAND_RUN_TIME) print(colored(f'Can i execute the command: `{command}` with {timeout}ms timeout?', 'white', attrs=['bold'])) + + answer = styled_text( + project, + 'If yes, just press ENTER and if not, please paste the output of running this command here and press ENTER' + ) + project.command_runs_count += 1 command_run = get_command_run_from_hash_id(project, command) if command_run is not None and project.skip_steps: @@ -53,11 +59,6 @@ def execute_command(project, command, timeout=5000): print(colored(f'Restoring command run response id {command_run.id}:\n```\n{command_run.cli_response}```', 'yellow')) return command_run.cli_response - answer = styled_text( - project, - 'If yes, just press ENTER and if not, please paste the output of running this command here and press ENTER' - ) - return_value = None if answer != '': diff --git a/euclid/prompts/dev_ops/ran_command.prompt b/euclid/prompts/dev_ops/ran_command.prompt index 40f473e..c0cb0cf 100644 --- a/euclid/prompts/dev_ops/ran_command.prompt +++ b/euclid/prompts/dev_ops/ran_command.prompt @@ -1,4 +1,4 @@ -I ran the command `{{ command }}` and for this response from CLI: +{{ additional_info }}I ran the command `{{ command }}` and for this response from CLI: ``` {{ cli_response }} ``` diff --git a/euclid/prompts/development/implement_changes.prompt b/euclid/prompts/development/implement_changes.prompt index c415418..d5d77f1 100644 --- a/euclid/prompts/development/implement_changes.prompt +++ b/euclid/prompts/development/implement_changes.prompt @@ -9,6 +9,6 @@ Here is how files look now: {% endfor %} {% endif %} -Now, think step by step and apply the needed changes into appropriate files and return the changed files. +Now, think step by step and apply the needed changes for step #{{ step_index }} - {{ step_description }}. 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. Finally, you can save the modified files on the disk by calling `save_files` function. \ No newline at end of file diff --git a/euclid/utils/llm_connection.py b/euclid/utils/llm_connection.py index b106e56..7b6d063 100644 --- a/euclid/utils/llm_connection.py +++ b/euclid/utils/llm_connection.py @@ -182,7 +182,7 @@ def stream_gpt_completion(data, req_type): if function_calls['arguments'] != '': logger.info(f'Response via function call: {function_calls["arguments"]}') function_calls['arguments'] = load_data_to_json(function_calls['arguments']) - return return_result({'function_calls': function_calls}); + return return_result({'function_calls': function_calls}) logger.info(f'Response message: {gpt_response}') new_code = postprocessing(gpt_response, req_type) # TODO add type dynamically return return_result({'text': new_code}) From f180267a66971158115468c438132e8fa6bacfe3 Mon Sep 17 00:00:00 2001 From: Zvonimir Sabljic Date: Mon, 14 Aug 2023 12:21:57 +0200 Subject: [PATCH 2/3] Refactored saving files and file snapshot so they are connected --- euclid/database/database.py | 4 +- euclid/database/models/file_snapshot.py | 7 ++- euclid/database/models/files.py | 4 +- euclid/helpers/Project.py | 74 ++++++++++++++++++------- euclid/helpers/agents/CodeMonkey.py | 12 +--- euclid/helpers/agents/Developer.py | 3 +- euclid/helpers/files.py | 7 ++- 7 files changed, 73 insertions(+), 38 deletions(-) diff --git a/euclid/database/database.py b/euclid/database/database.py index 3af5f90..ecd5b4f 100644 --- a/euclid/database/database.py +++ b/euclid/database/database.py @@ -289,8 +289,8 @@ def get_all_connected_steps(step, previous_step_field_name): prev_step = getattr(prev_step, previous_step_field_name) return connected_steps -def delete_all_steps_from_app(app): - models = [DevelopmentSteps, CommandRuns, UserInputs] +def delete_all_app_development_data(app): + models = [DevelopmentSteps, CommandRuns, UserInputs, File, FileSnapshot] for model in models: model.delete().where(model.app == app).execute() diff --git a/euclid/database/models/file_snapshot.py b/euclid/database/models/file_snapshot.py index 483f2ee..9138a5f 100644 --- a/euclid/database/models/file_snapshot.py +++ b/euclid/database/models/file_snapshot.py @@ -2,14 +2,17 @@ from peewee import * from database.models.components.base_models import BaseModel from database.models.development_steps import DevelopmentSteps +from database.models.app import App +from database.models.files import File class FileSnapshot(BaseModel): + app = ForeignKeyField(App, on_delete='CASCADE') development_step = ForeignKeyField(DevelopmentSteps, backref='files', on_delete='CASCADE') - name = CharField() + file = ForeignKeyField(File, on_delete='CASCADE', null=True) content = TextField() class Meta: db_table = 'file_snapshot' indexes = ( - (('development_step', 'name'), True), + (('development_step', 'file'), True), ) \ No newline at end of file diff --git a/euclid/database/models/files.py b/euclid/database/models/files.py index 9719223..e674498 100644 --- a/euclid/database/models/files.py +++ b/euclid/database/models/files.py @@ -5,10 +5,12 @@ from database.models.development_steps import DevelopmentSteps from database.models.app import App class File(BaseModel): + id = AutoField() app = ForeignKeyField(App, on_delete='CASCADE') name = CharField() path = CharField() - description = TextField() + full_path = CharField() + description = TextField(null=True) class Meta: indexes = ( diff --git a/euclid/helpers/Project.py b/euclid/helpers/Project.py index eba0717..fee995e 100644 --- a/euclid/helpers/Project.py +++ b/euclid/helpers/Project.py @@ -3,9 +3,9 @@ import os from termcolor import colored from const.common import IGNORE_FOLDERS from database.models.app import App -from database.database import get_app, delete_unconnected_steps_from, delete_all_steps_from_app +from database.database import get_app, delete_unconnected_steps_from, delete_all_app_development_data from utils.questionary import styled_text -from helpers.files import get_files_content, clear_directory +from helpers.files import get_files_content, clear_directory, update_file from helpers.cli import build_directory_tree from helpers.agents.TechLead import TechLead from helpers.agents.Developer import Developer @@ -30,16 +30,6 @@ class Project: 'last_command_run': None, 'last_development_step': None, } - if 'skip_until_dev_step' in args: - self.skip_until_dev_step = args['skip_until_dev_step'] - if args['skip_until_dev_step'] == '0': - delete_all_steps_from_app(args['app_id']) - self.skip_steps = False - else: - self.skip_until_dev_step = None - self.skip_steps = True - self.skip_steps = False if ('skip_until_dev_step' in args and args['skip_until_dev_step'] == '0') else True - # TODO make flexible # self.root_path = get_parent_folder('euclid') self.root_path = '' @@ -72,6 +62,20 @@ class Project: self.tech_lead = TechLead(self) self.development_plan = self.tech_lead.create_development_plan() + # TODO move to constructor eventually + if 'skip_until_dev_step' in self.args: + self.skip_until_dev_step = self.args['skip_until_dev_step'] + if self.args['skip_until_dev_step'] == '0': + clear_directory(self.root_path, IGNORE_FOLDERS) + delete_all_app_development_data(self.args['app_id']) + self.skip_steps = False + else: + self.skip_until_dev_step = None + self.skip_steps = True + + self.skip_steps = False if ('skip_until_dev_step' in self.args and self.args['skip_until_dev_step'] == '0') else True + # TODO END + self.developer = Developer(self) self.developer.set_up_environment(); @@ -79,21 +83,27 @@ class Project: def get_directory_tree(self, with_descriptions=False): files = {} - if with_descriptions: + if with_descriptions and False: files = File.select().where(File.app_id == self.args['app_id']) files = {snapshot.name: snapshot for snapshot in files} - return build_directory_tree(self.root_path + '/', ignore=IGNORE_FOLDERS, files=files, add_descriptions=True) + return build_directory_tree(self.root_path + '/', ignore=IGNORE_FOLDERS, files=files, add_descriptions=False) def get_test_directory_tree(self): # TODO remove hardcoded path return build_directory_tree(self.root_path + '/tests', ignore=IGNORE_FOLDERS) + def get_all_coded_files(self): + files = File.select().where(File.app_id == self.args['app_id']) + files = self.get_files([file.path + file.name for file in files]) + return files + def get_files(self, files): files_with_content = [] for file in files: # TODO this is a hack, fix it try: - file_content = open(self.get_full_file_path('', file), 'r').read() + relative_path, full_path = self.get_full_file_path('', file) + file_content = open(full_path, 'r').read() except: file_content = '' @@ -103,22 +113,48 @@ class Project: }) return files_with_content + def save_file(self, data): + data['path'], data['full_path'] = self.get_full_file_path(data['path'], data['name']) + update_file(data['full_path'], data['content']) + + file_in_db, created = File.get_or_create( + app=self.app, + name=data['name'], + path=data['path'], + full_path=data['full_path'], + ) + def get_full_file_path(self, file_path, file_name): file_path = file_path.replace('./', '', 1).rstrip(file_name) + if not file_path.endswith('/'): file_path = file_path + '/' + + if file_name.startswith('/'): + file_name = file_name[1:] + if not file_path.startswith('/'): file_path = '/' + file_path - return self.root_path + file_path + file_name + + return (file_path, self.root_path + file_path + file_name) def save_files_snapshot(self, development_step_id): files = get_files_content(self.root_path, ignore=IGNORE_FOLDERS) development_step, created = DevelopmentSteps.get_or_create(id=development_step_id) for file in files: - file_snapshot, created = FileSnapshot.get_or_create( - development_step=development_step, + # TODO this can be optimized so we don't go to the db each time + file_in_db, created = File.get_or_create( + app=self.app, name=file['name'], + path=file['path'], + full_path=file['full_path'], + ) + + file_snapshot, created = FileSnapshot.get_or_create( + app=self.app, + development_step=development_step, + file=file_in_db, defaults={'content': file.get('content', '')} ) file_snapshot.content = content = file['content'] @@ -130,7 +166,7 @@ class Project: clear_directory(self.root_path, IGNORE_FOLDERS) for file_snapshot in file_snapshots: - full_path = self.root_path + '/' + file_snapshot.name + full_path = self.root_path + file_snapshot.file.path + '/' + file_snapshot.file.name # Ensure directory exists os.makedirs(os.path.dirname(full_path), exist_ok=True) diff --git a/euclid/helpers/agents/CodeMonkey.py b/euclid/helpers/agents/CodeMonkey.py index f2dcc0f..0cdfca8 100644 --- a/euclid/helpers/agents/CodeMonkey.py +++ b/euclid/helpers/agents/CodeMonkey.py @@ -28,16 +28,6 @@ class CodeMonkey(Agent): }, IMPLEMENT_CHANGES) for file_data in changes: - file_data['full_path'] = self.project.get_full_file_path(file_data['path'], file_data['name']) - - if file_data['description'] != '': - (File.insert(app=self.project.app, path=file_data['path'], name=file_data['name'], description=file_data['description']) - .on_conflict( - conflict_target=[File.app, File.name, File.path], - preserve=[], - update={'description': file_data['description']}) - .execute()) - - update_file(file_data['full_path'], file_data['content']) + self.project.save_file(file_data) return convo diff --git a/euclid/helpers/agents/Developer.py b/euclid/helpers/agents/Developer.py index 6a39f54..b2c96f4 100644 --- a/euclid/helpers/agents/Developer.py +++ b/euclid/helpers/agents/Developer.py @@ -1,6 +1,7 @@ import json import uuid from termcolor import colored +from helpers.files import update_file from utils.utils import step_already_finished from helpers.agents.CodeMonkey import CodeMonkey from logger.logger import logger @@ -98,7 +99,7 @@ class Developer(Agent): if step and not execute_step(self.project.args['step'], self.project.current_step): step_already_finished(self.project.args, step) return - + # ENVIRONMENT SETUP print(colored(f"Setting up the environment...\n", "green")) logger.info(f"Setting up the environment...") diff --git a/euclid/helpers/files.py b/euclid/helpers/files.py index c668e69..b97676f 100644 --- a/euclid/helpers/files.py +++ b/euclid/helpers/files.py @@ -28,9 +28,12 @@ def get_files_content(directory, ignore=[]): with open(path, 'r', encoding='utf-8', errors='ignore') as f: file_content = f.read() + file_name = path.replace(directory + '/', '') return_array.append({ - 'name': path.replace(directory + '/', ''), - 'content': file_content + 'name': file_name, + 'path': '/' + file.replace(file_name, ''), + 'content': file_content, + 'full_path': path, }) return return_array From 0a573ea9b7b9433cd76ba26799b17e1b450ffb17 Mon Sep 17 00:00:00 2001 From: Zvonimir Sabljic Date: Mon, 14 Aug 2023 12:24:04 +0200 Subject: [PATCH 3/3] Hardcoded fix --- euclid/helpers/AgentConvo.py | 7 ++++++- euclid/utils/llm_connection.py | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/euclid/helpers/AgentConvo.py b/euclid/helpers/AgentConvo.py index 6ee00a1..ee2609f 100644 --- a/euclid/helpers/AgentConvo.py +++ b/euclid/helpers/AgentConvo.py @@ -22,12 +22,14 @@ class AgentConvo: self.messages.append(get_sys_message(self.agent.role)) def send_message(self, prompt_path=None, prompt_data=None, function_calls=None): - # craft message if prompt_path is not None and prompt_data is not None: prompt = get_prompt(prompt_path, prompt_data) self.messages.append({"role": "user", "content": prompt}) + if function_calls is not None and 'function_calls' in function_calls: + self.messages[-1]['content'] += '\nMAKE SURE THAT YOU RESPOND WITH A CORRECT JSON FORMAT!!!' + # check if we already have the LLM response saved self.agent.project.llm_req_num += 1 development_step = get_development_step_from_hash_id(self.agent.project, prompt_path, prompt_data, self.agent.project.llm_req_num) @@ -130,3 +132,6 @@ class AgentConvo: content = file.read() process = subprocess.Popen('pbcopy', stdin=subprocess.PIPE) process.communicate(content.replace('{{messages}}', str(self.messages)).encode('utf-8')) + + def remove_last_x_messages(self, x): + self.messages = self.messages[:-x] \ No newline at end of file diff --git a/euclid/utils/llm_connection.py b/euclid/utils/llm_connection.py index 7b6d063..e331f5f 100644 --- a/euclid/utils/llm_connection.py +++ b/euclid/utils/llm_connection.py @@ -169,9 +169,11 @@ def stream_gpt_completion(data, req_type): if 'name' in json_line['function_call']: function_calls['name'] = json_line['function_call']['name'] print(f'Function call: {function_calls["name"]}') + if 'arguments' in json_line['function_call']: function_calls['arguments'] += json_line['function_call']['arguments'] print(json_line['function_call']['arguments'], end='', flush=True) + if 'content' in json_line: content = json_line.get('content') if content: