Skip to content

Commit

Permalink
Merge pull request #175 from Pythagora-io/feature/121-dot_gpt-pilot-f…
Browse files Browse the repository at this point in the history
…older

create .gpt-pilot directory, save project info & chat log
  • Loading branch information
LeonOstrez authored Oct 12, 2023
2 parents f1ff82e + 2a165d3 commit 217893b
Show file tree
Hide file tree
Showing 13 changed files with 191 additions and 29 deletions.
1 change: 1 addition & 0 deletions pilot/const/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

IGNORE_FOLDERS = [
'.git',
'.gpt-pilot',
'.idea',
'.vscode',
'__pycache__',
Expand Down
9 changes: 8 additions & 1 deletion pilot/helpers/Project.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@
from database.models.file_snapshot import FileSnapshot
from database.models.files import File
from logger.logger import logger
from utils.dot_gpt_pilot import DotGptPilot


class Project:
def __init__(self, args, name=None, description=None, user_stories=None, user_tasks=None, architecture=None,
development_plan=None, current_step=None, ipc_client_instance=None):
development_plan=None, current_step=None, ipc_client_instance=None, enable_dot_pilot_gpt=True):
"""
Initialize a project.
Expand Down Expand Up @@ -69,6 +70,11 @@ def __init__(self, args, name=None, description=None, user_stories=None, user_ta
self.architecture = architecture
# if development_plan is not None:
# self.development_plan = development_plan
self.dot_pilot_gpt = DotGptPilot(log_chat_completions=enable_dot_pilot_gpt)

def set_root_path(self, root_path: str):
self.root_path = root_path
self.dot_pilot_gpt.with_root_path(root_path)

def start(self):
"""
Expand Down Expand Up @@ -128,6 +134,7 @@ def start(self):
break
# TODO END

self.dot_pilot_gpt.write_project(self)
print(json.dumps({
"project_stage": "coding"
}), type='info')
Expand Down
3 changes: 2 additions & 1 deletion pilot/helpers/agents/Developer.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,13 @@ def start_coding(self):
self.implement_task(i, dev_task)

# DEVELOPMENT END

self.project.dot_pilot_gpt.chat_log_folder(None)
logger.info('The app is DONE!!! Yay...you can use it now.')
print(green_bold("The app is DONE!!! Yay...you can use it now.\n"))

def implement_task(self, i, development_task=None):
print(green_bold(f'Implementing task #{i + 1}: ') + green(f' {development_task["description"]}\n'))
self.project.dot_pilot_gpt.chat_log_folder(i + 1)

convo_dev_task = AgentConvo(self)
convo_dev_task.send_message('development/task/breakdown.prompt', {
Expand Down
4 changes: 2 additions & 2 deletions pilot/helpers/agents/ProductOwner.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def get_project_description(self):
step = get_progress_steps(self.project.args['app_id'], PROJECT_DESCRIPTION_STEP)
if step and not should_execute_step(self.project.args['step'], PROJECT_DESCRIPTION_STEP):
step_already_finished(self.project.args, step)
self.project.root_path = setup_workspace(self.project.args)
self.project.set_root_path(setup_workspace(self.project.args))
self.project.project_description = step['summary']
self.project.project_description_messages = step['messages']
return
Expand All @@ -39,7 +39,7 @@ def get_project_description(self):
if 'name' not in self.project.args:
self.project.args['name'] = clean_filename(ask_user(self.project, 'What is the project name?'))

self.project.root_path = setup_workspace(self.project.args)
self.project.set_root_path(setup_workspace(self.project.args))

self.project.app = save_app(self.project)

Expand Down
4 changes: 2 additions & 2 deletions pilot/helpers/agents/test_CodeMonkey.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ def setup_method(self):
current_step='coding',
)

self.project.root_path = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)),
'../../../workspace/TestDeveloper'))
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
Expand Down
4 changes: 2 additions & 2 deletions pilot/helpers/agents/test_Developer.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ def setup_method(self):
user_stories=[]
)

self.project.root_path = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)),
'../../../workspace/TestDeveloper'))
self.project.set_root_path(os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)),
'../../../workspace/TestDeveloper')))
self.project.technologies = []
self.project.current_step = ENVIRONMENT_SETUP_STEP
self.developer = Developer(self.project)
Expand Down
4 changes: 2 additions & 2 deletions pilot/helpers/agents/test_TechLead.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ def setup_method(self):
user_stories=[]
)

self.project.root_path = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)),
'../../../workspace/TestTechLead'))
self.project.set_root_path(os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)),
'../../../workspace/TestTechLead')))
self.project.technologies = []
self.project.project_description = '''
The project entails creating a web-based chat application, tentatively named "chat_app."
Expand Down
88 changes: 73 additions & 15 deletions pilot/helpers/test_Project.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import os
import json
import pytest
from unittest.mock import patch
from unittest.mock import patch, MagicMock
from helpers.Project import Project
from database.models.files import File

test_root = os.path.join(os.path.dirname(__file__), '../../workspace/gpt-pilot-test').replace('\\', '/')


def create_project():
project = Project({
Expand All @@ -14,17 +18,17 @@ def create_project():
architecture=[],
user_stories=[]
)
project.root_path = "/temp/gpt-pilot-test"
project.set_root_path(test_root)
project.app = 'test'
return project


@pytest.mark.parametrize('test_data', [
{'name': 'package.json', 'path': 'package.json', 'saved_to': '/temp/gpt-pilot-test/package.json'},
{'name': 'package.json', 'path': '', 'saved_to': '/temp/gpt-pilot-test/package.json'},
# {'name': 'Dockerfile', 'path': None, 'saved_to': '/temp/gpt-pilot-test/Dockerfile'},
{'name': None, 'path': 'public/index.html', 'saved_to': '/temp/gpt-pilot-test/public/index.html'},
{'name': '', 'path': 'public/index.html', 'saved_to': '/temp/gpt-pilot-test/public/index.html'},
{'name': 'package.json', 'path': 'package.json', 'saved_to': f'{test_root}/package.json'},
{'name': 'package.json', 'path': '', 'saved_to': f'{test_root}/package.json'},
# {'name': 'Dockerfile', 'path': None, 'saved_to': f'{test_root}/Dockerfile'},
{'name': None, 'path': 'public/index.html', 'saved_to': f'{test_root}/public/index.html'},
{'name': '', 'path': 'public/index.html', 'saved_to': f'{test_root}/public/index.html'},
# TODO: Treatment of paths outside of the project workspace - https://github.com/Pythagora-io/gpt-pilot/issues/129
# {'name': '/etc/hosts', 'path': None, 'saved_to': '/etc/hosts'},
Expand Down Expand Up @@ -65,12 +69,12 @@ def test_save_file(mock_file_insert, mock_update_file, test_data):


@pytest.mark.parametrize('file_path, file_name, expected', [
('file.txt', 'file.txt', '/temp/gpt-pilot-test/file.txt'),
('', 'file.txt', '/temp/gpt-pilot-test/file.txt'),
('path/', 'file.txt', '/temp/gpt-pilot-test/path/file.txt'),
('path/to/', 'file.txt', '/temp/gpt-pilot-test/path/to/file.txt'),
('path/to/file.txt', 'file.txt', '/temp/gpt-pilot-test/path/to/file.txt'),
('./path/to/file.txt', 'file.txt', '/temp/gpt-pilot-test/./path/to/file.txt'), # ideally result would not have `./`
('file.txt', 'file.txt', f'{test_root}/file.txt'),
('', 'file.txt', f'{test_root}/file.txt'),
('path/', 'file.txt', f'{test_root}/path/file.txt'),
('path/to/', 'file.txt', f'{test_root}/path/to/file.txt'),
('path/to/file.txt', 'file.txt', f'{test_root}/path/to/file.txt'),
('./path/to/file.txt', 'file.txt', f'{test_root}/./path/to/file.txt'), # ideally result would not have `./`
])
def test_get_full_path(file_path, file_name, expected):
# Given
Expand Down Expand Up @@ -100,7 +104,6 @@ def test_get_full_path_absolute(file_path, file_name, expected):
# Then
assert absolute_path == expected


# This is known to fail and should be avoided
# def test_get_full_file_path_error():
# # Given
Expand All @@ -111,4 +114,59 @@ def test_get_full_path_absolute(file_path, file_name, expected):
# full_path = project.get_full_file_path(file_path, file_name)
#
# # Then
# assert full_path == '/temp/gpt-pilot-test/path/to/file/'
# assert full_path == f'{test_root}/path/to/file/'


class TestProjectFileLists:
def setup_method(self):
# Given a project
project = create_project()
self.project = project
project.set_root_path(os.path.join(os.path.dirname(__file__), '../../workspace/directory_tree'))
project.project_description = 'Test Project'
project.development_plan = [{
'description': 'Test User Story',
'programmatic_goal': 'Test Programmatic Goal',
'user_review_goal': 'Test User Review Goal',
}]

# with directories including common.IGNORE_FOLDERS
src = os.path.join(project.root_path, 'src')
os.makedirs(src, exist_ok=True)
for dir in ['.git', '.idea', '.vscode', '__pycache__', 'node_modules', 'venv', 'dist', 'build']:
os.makedirs(os.path.join(project.root_path, dir), exist_ok=True)

# ...and files
with open(os.path.join(project.root_path, 'package.json'), 'w') as file:
json.dump({'name': 'test app'}, file, indent=2)
with open(os.path.join(src, 'main.js'), 'w') as file:
file.write('console.log("Hello World!");')

# and a non-empty .gpt-pilot directory
project.dot_pilot_gpt.write_project(project)

def test_get_directory_tree(self):
# When
tree = self.project.get_directory_tree()

# Then we should not be including the .gpt-pilot directory or other ignored directories
assert tree == '''
|-- /
| |-- package.json
| |-- src/
| | |-- main.js
'''.lstrip()

@patch('helpers.Project.DevelopmentSteps.get_or_create', return_value=('test', True))
@patch('helpers.Project.File.get_or_create', return_value=('test', True))
@patch('helpers.Project.FileSnapshot.get_or_create', return_value=(MagicMock(), True))
def test_save_files_snapshot(self, mock_snap, mock_file, mock_step):
# Given a snapshot of the files in the project

# When we save the file snapshot
self.project.save_files_snapshot('test')

# Then the files should be saved to the project, but nothing from `.gpt-pilot/`
assert mock_file.call_count == 2
assert mock_file.call_args_list[0][1]['name'] == 'package.json'
assert mock_file.call_args_list[1][1]['name'] == 'main.js'
4 changes: 2 additions & 2 deletions pilot/test/ux_tests/run_command_until_success.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ def run_command_until_success():
user_stories=[]
)

project.root_path = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)),
'../../../workspace/TestDeveloper'))
project.set_root_path(os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)),
'../../../workspace/TestDeveloper')))
project.technologies = []
project.current_step = ENVIRONMENT_SETUP_STEP
project.app = save_app(project)
Expand Down
91 changes: 91 additions & 0 deletions pilot/utils/dot_gpt_pilot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import json
import os
import yaml
from datetime import datetime
from dotenv import load_dotenv

load_dotenv()

USE_GPTPILOT_FOLDER = os.getenv('USE_GPTPILOT_FOLDER') == 'true'


# TODO: Parse files from the `.gpt-pilot` directory to resume a project - `user_stories` may have changed - include checksums for sections which may need to be reprocessed.
# TODO: Save a summary at the end of each task/sprint.
class DotGptPilot:
"""
Manages the `.gpt-pilot` directory.
"""
def __init__(self, log_chat_completions: bool = True):
if not USE_GPTPILOT_FOLDER:
return
self.log_chat_completions = log_chat_completions
self.dot_gpt_pilot_path = self.with_root_path('~', create=False)
self.chat_log_path = self.chat_log_folder(None)

def with_root_path(self, root_path: str, create=True):
if not USE_GPTPILOT_FOLDER:
return
dot_gpt_pilot_path = os.path.join(root_path, '.gpt-pilot')
self.dot_gpt_pilot_path = dot_gpt_pilot_path

# Create the `.gpt-pilot` directory if required.
if create and self.log_chat_completions: # (... or ...):
self.chat_log_folder(None)

return dot_gpt_pilot_path

def chat_log_folder(self, task):
if not USE_GPTPILOT_FOLDER:
return
chat_log_path = os.path.join(self.dot_gpt_pilot_path, 'chat_log')
if task is not None:
chat_log_path = os.path.join(chat_log_path, 'task_' + str(task))

os.makedirs(chat_log_path, exist_ok=True)
self.chat_log_path = chat_log_path
return chat_log_path

def log_chat_completion(self, endpoint: str, model: str, req_type: str, messages: list[dict], response: str):
if not USE_GPTPILOT_FOLDER:
return
if self.log_chat_completions:
time = datetime.now().strftime('%Y-%m-%d_%H_%M_%S')
with open(os.path.join(self.chat_log_path, f'{time}-{req_type}.yaml'), 'w') as file:
data = {
'endpoint': endpoint,
'model': model,
'messages': messages,
'response': response,
}

yaml.safe_dump(data, file, width=120, indent=2, default_flow_style=False, sort_keys=False)

def log_chat_completion_json(self, endpoint: str, model: str, req_type: str, functions: dict, json_response: str):
if not USE_GPTPILOT_FOLDER:
return
if self.log_chat_completions:
time = datetime.now().strftime('%Y-%m-%d_%H_%M_%S')

with open(os.path.join(self.chat_log_path, f'{time}-{req_type}.json'), 'w') as file:
data = {
'endpoint': endpoint,
'model': model,
'functions': functions,
'response': json.loads(json_response),
}

json.dump(data, file, indent=2)

def write_project(self, project):
if not USE_GPTPILOT_FOLDER:
return
data = {
'name': project.args['name'],
'description': project.project_description,
'user_stories': project.user_stories,
'architecture': project.architecture,
'development_plan': project.development_plan,
}

with open(os.path.join(self.dot_gpt_pilot_path, 'project.yaml'), 'w') as file:
yaml.safe_dump(data, file, width=120, indent=2, default_flow_style=False, sort_keys=False)
5 changes: 4 additions & 1 deletion pilot/utils/llm_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
from utils.function_calling import add_function_calls_to_request, FunctionCallSet, FunctionType
from utils.questionary import styled_text


def get_tokens_in_messages(messages: List[str]) -> int:
tokenizer = tiktoken.get_encoding("cl100k_base") # GPT-4 tokenizer
tokenized_messages = [tokenizer.encode(message['content']) for message in messages]
Expand Down Expand Up @@ -335,6 +334,7 @@ def return_result(result_data, lines_printed):
)

if response.status_code != 200:
project.dot_pilot_gpt.log_chat_completion(endpoint, model, req_type, data['messages'], response.text)
logger.info(f'problem with request (status {response.status_code}): {response.text}')
raise Exception(f"API responded with status code: {response.status_code}. Response text: {response.text}")

Expand Down Expand Up @@ -408,10 +408,13 @@ def return_result(result_data, lines_printed):
# function_calls['arguments'] = load_data_to_json(function_calls['arguments'])
# return return_result({'function_calls': function_calls}, lines_printed)
logger.info('<<<<<<<<<< LLM Response <<<<<<<<<<\n%s\n<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<', gpt_response)
project.dot_pilot_gpt.log_chat_completion(endpoint, model, req_type, data['messages'], gpt_response)

if expecting_json:
gpt_response = clean_json_response(gpt_response)
assert_json_schema(gpt_response, expecting_json)
# Note, we log JSON separately from the YAML log above incase the JSON is invalid and an error is raised
project.dot_pilot_gpt.log_chat_completion_json(endpoint, model, req_type, expecting_json, gpt_response)

new_code = postprocessing(gpt_response, req_type) # TODO add type dynamically
return return_result({'text': new_code}, lines_printed)
Expand Down
2 changes: 1 addition & 1 deletion pilot/utils/test_llm_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

load_dotenv()

project = Project({'app_id': 'test-app'}, current_step='test')
project = Project({'app_id': 'test-app'}, current_step='test', enable_dot_pilot_gpt=False)


def test_clean_json_response_True_False():
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ psycopg2-binary==2.9.6
python-dotenv==1.0.0
python-editor==1.0.4
pytest==7.4.2
pyyaml==6.0.1
questionary==1.10.0
readchar==4.0.5
regex==2023.6.3
Expand Down

0 comments on commit 217893b

Please sign in to comment.