diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4e7e0f3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: Test & QA + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.7, 3.8, 3.9, 3.10, 3.11] + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Lint + run: | + pip install flake8 + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # stop the build if there are Python syntax errors or undefined names + #ruff --format=github --select=E9,F63,F7,F82 --target-version=py37 . + # default set of ruff rules with GitHub Annotations + #ruff --format=github --target-version=py37 . + + - name: Run tests + run: | + pip install pytest + pytest diff --git a/README.md b/README.md index 27b34ba..2c43b00 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ Other than the research, GPT Pilot needs to be debugged to work in different sce # 🔗 Connect with us 🌟 As an open source tool, it would mean the world to us if you starred the GPT-pilot repo 🌟 -💬 Join [the Discord server](https://discord.gg/FWnRZdCb) to get in touch. +💬 Join [the Discord server](https://discord.gg/HaqXugmxr9) to get in touch.



diff --git a/pilot/const/common.py b/pilot/const/common.py index 6e39416..9ac0120 100644 --- a/pilot/const/common.py +++ b/pilot/const/common.py @@ -1,4 +1,4 @@ -APP_TYPES = ['Web App', 'Script', 'Mobile App (unavailable)', 'Chrome Extension (unavailable)'] +APP_TYPES = ['Web App', 'Script', 'Mobile App', 'Chrome Extension'] ROLES = { 'product_owner': ['project_description', 'user_stories', 'user_tasks'], 'architect': ['architecture'], diff --git a/pilot/database/database.py b/pilot/database/database.py index 5980227..770c9e8 100644 --- a/pilot/database/database.py +++ b/pilot/database/database.py @@ -23,6 +23,7 @@ from database.models.environment_setup import EnvironmentSetup from database.models.development import Development from database.models.file_snapshot import FileSnapshot from database.models.command_runs import CommandRuns +from database.models.user_apps import UserApps from database.models.user_inputs import UserInputs from database.models.files import File @@ -84,6 +85,16 @@ def save_app(args): return app +def save_user_app(user_id, app_id, workspace): + try: + user_app = UserApps.get((UserApps.user == user_id) & (UserApps.app == app_id)) + user_app.workspace = workspace + user_app.save() + except DoesNotExist: + user_app = UserApps.create(user=user_id, app=app_id, workspace=workspace) + + return user_app + def save_progress(app_id, step, data): progress_table_map = { 'project_description': ProjectDescription, @@ -124,6 +135,14 @@ def get_app(app_id): raise ValueError(f"No app with id: {app_id}") +def get_app_by_user_workspace(user_id, workspace): + try: + user_app = UserApps.get((UserApps.user == user_id) & (UserApps.workspace == workspace)) + return user_app.app + except DoesNotExist: + return None + + def get_progress_steps(app_id, step=None): progress_table_map = { 'project_description': ProjectDescription, @@ -309,7 +328,7 @@ def get_all_connected_steps(step, previous_step_field_name): def delete_all_app_development_data(app): - models = [DevelopmentSteps, CommandRuns, UserInputs, File, FileSnapshot] + models = [DevelopmentSteps, CommandRuns, UserInputs, UserApps, File, FileSnapshot] for model in models: model.delete().where(model.app == app).execute() @@ -354,6 +373,7 @@ def create_tables(): Development, FileSnapshot, CommandRuns, + UserApps, UserInputs, File, ]) @@ -374,10 +394,11 @@ def drop_tables(): Development, FileSnapshot, CommandRuns, + UserApps, UserInputs, File, ]: - if DATABASE_TYPE == "postgresql": + if DATABASE_TYPE == "postgres": sql = f'DROP TABLE IF EXISTS "{table._meta.table_name}" CASCADE' elif DATABASE_TYPE == "sqlite": sql = f'DROP TABLE IF EXISTS "{table._meta.table_name}"' @@ -423,7 +444,7 @@ def create_database(): def tables_exist(): tables = [User, App, ProjectDescription, UserStories, UserTasks, Architecture, DevelopmentPlanning, - DevelopmentSteps, EnvironmentSetup, Development, FileSnapshot, CommandRuns, UserInputs, File] + DevelopmentSteps, EnvironmentSetup, Development, FileSnapshot, CommandRuns, UserApps, UserInputs, File] if DATABASE_TYPE == "postgres": for table in tables: diff --git a/pilot/database/models/user_apps.py b/pilot/database/models/user_apps.py new file mode 100644 index 0000000..d70672f --- /dev/null +++ b/pilot/database/models/user_apps.py @@ -0,0 +1,18 @@ +from peewee import * + +from database.models.components.base_models import BaseModel +from database.models.app import App +from database.models.user import User + + +class UserApps(BaseModel): + id = AutoField() + app = ForeignKeyField(App, on_delete='CASCADE') + user = ForeignKeyField(User, on_delete='CASCADE') + workspace = CharField(null=True) + + class Meta: + db_table = 'user_apps' + indexes = ( + (('app', 'user'), True), + ) diff --git a/pilot/helpers/Project.py b/pilot/helpers/Project.py index 90d3099..44fbf57 100644 --- a/pilot/helpers/Project.py +++ b/pilot/helpers/Project.py @@ -25,7 +25,7 @@ class Project: Initialize a project. Args: - args (dict): Project arguments. + args (dict): Project arguments - app_id, (app_type, name), user_id, email, password, step name (str, optional): Project name. Default is None. description (str, optional): Project description. Default is None. user_stories (list, optional): List of user stories. Default is None. @@ -97,7 +97,7 @@ class Project: # TODO END self.developer = Developer(self) - self.developer.set_up_environment(); + self.developer.set_up_environment() self.developer.start_coding() diff --git a/pilot/helpers/agents/Developer.py b/pilot/helpers/agents/Developer.py index a0074bc..47e4175 100644 --- a/pilot/helpers/agents/Developer.py +++ b/pilot/helpers/agents/Developer.py @@ -40,6 +40,7 @@ class Developer(Agent): convo_dev_task = AgentConvo(self) task_description = 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, "clarification": [], "user_stories": self.project.user_stories, @@ -134,6 +135,7 @@ class Developer(Agent): iteration_convo = AgentConvo(self) iteration_convo.send_message('development/iteration.prompt', { "name": self.project.args['name'], + "app_type": self.project.args['app_type'], "app_summary": self.project.project_description, "clarification": [], "user_stories": self.project.user_stories, @@ -175,7 +177,12 @@ class Developer(Agent): os_info = get_os_info() os_specific_techologies = self.convo_os_specific_tech.send_message('development/env_setup/specs.prompt', - { "name": self.project.args['name'], "os_info": os_info, "technologies": self.project.architecture }, FILTER_OS_TECHNOLOGIES) + { + "name": self.project.args['name'], + "app_type": self.project.args['app_type'], + "os_info": os_info, + "technologies": self.project.architecture + }, FILTER_OS_TECHNOLOGIES) for technology in os_specific_techologies: # TODO move the functions definisions to function_calls.py @@ -248,7 +255,7 @@ class Developer(Agent): 'step_type': type, 'directory_tree': directory_tree, 'step_index': step_index - }, EXECUTE_COMMANDS); + }, EXECUTE_COMMANDS) if type == 'COMMAND': for cmd in step_details: run_command_until_success(cmd['command'], cmd['timeout'], convo) diff --git a/pilot/helpers/agents/ProductOwner.py b/pilot/helpers/agents/ProductOwner.py index d592fb3..4f4b932 100644 --- a/pilot/helpers/agents/ProductOwner.py +++ b/pilot/helpers/agents/ProductOwner.py @@ -24,16 +24,17 @@ class ProductOwner(Agent): step = get_progress_steps(self.project.args['app_id'], self.project.current_step) if step and not execute_step(self.project.args['step'], self.project.current_step): step_already_finished(self.project.args, step) - self.project.root_path = setup_workspace(self.project.args['name']) + self.project.root_path = setup_workspace(self.project.args) self.project.project_description = step['summary'] self.project.project_description_messages = step['messages'] return # PROJECT DESCRIPTION self.project.args['app_type'] = ask_for_app_type() - self.project.args['name'] = clean_filename(ask_user(self.project, 'What is the project name?')) + 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['name']) + self.project.root_path = setup_workspace(self.project.args) self.project.app = save_app(self.project.args) @@ -45,7 +46,9 @@ class ProductOwner(Agent): print(colored('Project Summary:\n', 'green', attrs=['bold'])) high_level_summary = convo_project_description.send_message('utils/summary.prompt', - {'conversation': '\n'.join([f"{msg['role']}: {msg['content']}" for msg in high_level_messages])}) + {'conversation': '\n'.join( + [f"{msg['role']}: {msg['content']}" for msg in + high_level_messages])}) save_progress(self.project.args['app_id'], self.project.current_step, { "prompt": main_prompt, @@ -59,7 +62,6 @@ class ProductOwner(Agent): return # PROJECT DESCRIPTION END - def get_user_stories(self): self.project.current_step = 'user_stories' self.convo_user_stories = AgentConvo(self) @@ -111,7 +113,7 @@ class ProductOwner(Agent): logger.info(msg) self.project.user_tasks = self.convo_user_stories.continuous_conversation('user_stories/user_tasks.prompt', - { 'END_RESPONSE': END_RESPONSE }) + {'END_RESPONSE': END_RESPONSE}) logger.info(f"Final user tasks: {self.project.user_tasks}") diff --git a/pilot/helpers/agents/TechLead.py b/pilot/helpers/agents/TechLead.py index 9ec5e89..08f55fb 100644 --- a/pilot/helpers/agents/TechLead.py +++ b/pilot/helpers/agents/TechLead.py @@ -36,6 +36,7 @@ class TechLead(Agent): self.development_plan = self.convo_development_plan.send_message('development/plan.prompt', { "name": self.project.args['name'], + "app_type": self.project.args['app_type'], "app_summary": self.project.project_description, "clarification": [], "user_stories": self.project.user_stories, diff --git a/pilot/main.py b/pilot/main.py index a13abfc..751ea9a 100644 --- a/pilot/main.py +++ b/pilot/main.py @@ -4,6 +4,7 @@ from __future__ import print_function, unicode_literals import sys from dotenv import load_dotenv +from termcolor import colored load_dotenv() from helpers.Project import Project @@ -38,6 +39,9 @@ if __name__ == "__main__": except KeyboardInterrupt: exit_gpt_pilot() except Exception as e: + print(colored('---------- GPT PILOT EXITING WITH ERROR ----------', 'red')) + print(colored(e, 'red')) + print(colored('--------------------------------------------------', 'red')) exit_gpt_pilot() finally: sys.exit(0) diff --git a/pilot/prompts/development/env_setup/specs.prompt b/pilot/prompts/development/env_setup/specs.prompt index 1ea1b33..a6c978d 100644 --- a/pilot/prompts/development/env_setup/specs.prompt +++ b/pilot/prompts/development/env_setup/specs.prompt @@ -1,4 +1,4 @@ -You are working in a software development agency and a project manager and software architect approach you telling you that you're assigned to work on a new project. You are working on a web app called "{{ name }}" and your first job is to set up the environment on a computer. +You are working in a software development agency and a project manager and software architect approach you telling you that you're assigned to work on a new project. You are working on a {{ app_type }} called "{{ name }}" and your first job is to set up the environment on a computer. Here are the technologies that you need to use for this project: ``` diff --git a/pilot/prompts/development/iteration.prompt b/pilot/prompts/development/iteration.prompt index 4b984b1..29a973e 100644 --- a/pilot/prompts/development/iteration.prompt +++ b/pilot/prompts/development/iteration.prompt @@ -1,4 +1,4 @@ -You are working on a web app called "{{ name }}" and you need to write code for the entire application. +You are working on a {{ app_type }} called "{{ name }}" and you need to write code for the entire application. Here is a high level description of "{{ name }}": ``` diff --git a/pilot/prompts/development/plan.prompt b/pilot/prompts/development/plan.prompt index 40cfbd2..dd3a43f 100644 --- a/pilot/prompts/development/plan.prompt +++ b/pilot/prompts/development/plan.prompt @@ -1,4 +1,4 @@ -You are working in a software development agency and a project manager and software architect approach you telling you that you're assigned to work on a new project. You are working on a web app called "{{ name }}" and you need to create a detailed development plan so that developers can start developing the app. +You are working in a software development agency and a project manager and software architect approach you telling you that you're assigned to work on a new project. You are working on a {{ app_type }} called "{{ name }}" and you need to create a detailed development plan so that developers can start developing the app. Here is a high level description of "{{ name }}": ``` diff --git a/pilot/prompts/development/task/breakdown.prompt b/pilot/prompts/development/task/breakdown.prompt index 3be9d57..ee2eb0b 100644 --- a/pilot/prompts/development/task/breakdown.prompt +++ b/pilot/prompts/development/task/breakdown.prompt @@ -1,4 +1,4 @@ -You are working on a web app called "{{ name }}" and you need to write code for the entire application based on the tasks that the tech lead gives you. So that you understand better what you're working on, you're given other specs for "{{ name }}" as well. +You are working on a {{ app_type }} called "{{ name }}" and you need to write code for the entire application based on the tasks that the tech lead gives you. So that you understand better what you're working on, you're given other specs for "{{ name }}" as well. Here is a high level description of "{{ name }}": ``` diff --git a/pilot/prompts/prompts.py b/pilot/prompts/prompts.py index c667268..6780f09 100644 --- a/pilot/prompts/prompts.py +++ b/pilot/prompts/prompts.py @@ -12,7 +12,7 @@ from logger.logger import logger def ask_for_app_type(): - return 'app' + return 'Web App' answer = styled_select( "What type of app do you want to build?", choices=common.APP_TYPES diff --git a/pilot/prompts/system_messages/architect.prompt b/pilot/prompts/system_messages/architect.prompt index b32cdb9..3bf1d46 100644 --- a/pilot/prompts/system_messages/architect.prompt +++ b/pilot/prompts/system_messages/architect.prompt @@ -1,4 +1,4 @@ -You are an experienced software architect. Your expertise is in creating an architecture for an MVP (minimum viable products) for web apps that can be developed as fast as possible by using as many ready-made technologies as possible. The technologies that you prefer using when other technologies are not explicitly specified are: +You are an experienced software architect. Your expertise is in creating an architecture for an MVP (minimum viable products) for {{ app_type }}s that can be developed as fast as possible by using as many ready-made technologies as possible. The technologies that you prefer using when other technologies are not explicitly specified are: **Scripts**: you prefer using Node.js for writing scripts that are meant to be ran just with the CLI. **Backend**: you prefer using Node.js with Mongo database if not explicitely specified otherwise. When you're using Mongo, you always use Mongoose and when you're using Postgresql, you always use PeeWee as an ORM. diff --git a/pilot/utils/arguments.py b/pilot/utils/arguments.py index de334e4..cc7104c 100644 --- a/pilot/utils/arguments.py +++ b/pilot/utils/arguments.py @@ -1,12 +1,11 @@ -import getpass +import hashlib import os import re import sys import uuid - +from getpass import getuser from termcolor import colored - -from database.database import get_app +from database.database import get_app, get_app_by_user_workspace def get_arguments(): @@ -25,21 +24,38 @@ def get_arguments(): else: arguments[arg] = True + if 'user_id' not in arguments: + arguments['user_id'] = username_to_uuid(getuser()) + + app = None + if 'workspace' in arguments: + app = get_app_by_user_workspace(arguments['user_id'], arguments['workspace']) + if app is not None: + arguments['app_id'] = app.id + else: + arguments['workspace'] = None + if 'app_id' in arguments: try: - app = get_app(arguments['app_id']) - arguments['user_id'] = str(app.user.id) + if app is None: + app = get_app(arguments['app_id']) + arguments['app_type'] = app.app_type arguments['name'] = app.name # Add any other fields from the App model you wish to include + + print(colored('\n------------------ LOADING PROJECT ----------------------', 'green', attrs=['bold'])) + print(colored(f'{app.name} (app_id={arguments["app_id"]})', 'green', attrs=['bold'])) + print(colored('--------------------------------------------------------------\n', 'green', attrs=['bold'])) except ValueError as e: print(e) # Handle the error as needed, possibly exiting the script else: arguments['app_id'] = str(uuid.uuid4()) - - if 'user_id' not in arguments: - arguments['user_id'] = getpass.getuser() + print(colored('\n------------------ STARTING NEW PROJECT ----------------------', 'green', attrs=['bold'])) + print(f"If you wish to continue with this project in future run:") + print(colored(f'python {sys.argv[0]} app_id={arguments["app_id"]}', 'green', attrs=['bold'])) + print(colored('--------------------------------------------------------------\n', 'green', attrs=['bold'])) if 'email' not in arguments: arguments['email'] = get_email() @@ -50,10 +66,6 @@ def get_arguments(): if 'step' not in arguments: arguments['step'] = None - print(colored('\n------------------ STARTING NEW PROJECT ----------------------', 'green', attrs=['bold'])) - print(f"If you wish to continue with this project in future run:") - print(colored(f'python main.py app_id={arguments["app_id"]}', 'green', attrs=['bold'])) - print(colored('--------------------------------------------------------------\n', 'green', attrs=['bold'])) return arguments @@ -75,3 +87,10 @@ def get_email(): # todo change email so its not uuid4 but make sure to fix storing of development steps where # 1 user can have multiple apps. In that case each app should have its own development steps return str(uuid.uuid4()) + + +# TODO can we make BaseModel.id a CharField with default=uuid4? +def username_to_uuid(username): + sha1 = hashlib.sha1(username.encode()).hexdigest() + uuid_str = "{}-{}-{}-{}-{}".format(sha1[:8], sha1[8:12], sha1[12:16], sha1[16:20], sha1[20:32]) + return str(uuid.UUID(uuid_str)) diff --git a/pilot/utils/files.py b/pilot/utils/files.py index bce78eb..b77b5ae 100644 --- a/pilot/utils/files.py +++ b/pilot/utils/files.py @@ -1,6 +1,6 @@ import os from pathlib import Path - +from database.database import save_user_app def get_parent_folder(folder_name): current_path = Path(os.path.abspath(__file__)) # get the path of the current script @@ -11,10 +11,18 @@ def get_parent_folder(folder_name): return current_path.parent -def setup_workspace(project_name): +def setup_workspace(args): + if args['workspace'] is not None: + try: + save_user_app(args['user_id'], args['app_id'], args['workspace']) + except Exception as e: + print(str(e)) + + return args['workspace'] + root = get_parent_folder('pilot') create_directory(root, 'workspace') - project_path = create_directory(os.path.join(root, 'workspace'), project_name) + project_path = create_directory(os.path.join(root, 'workspace'), args['name']) create_directory(project_path, 'tests') return project_path diff --git a/pilot/utils/questionary.py b/pilot/utils/questionary.py index b78c803..7cde642 100644 --- a/pilot/utils/questionary.py +++ b/pilot/utils/questionary.py @@ -43,4 +43,4 @@ def get_user_feedback(): config = { 'style': custom_style, } - return questionary.text("Thank you for trying GPT-Pilot. Please give us your feedback or just press ENTER to exit: ", **config).unsafe_ask() + return questionary.text("How did GPT Pilot do? Were you able to create any app that works? Please write any feedback you have or just press ENTER to exit: ", **config).unsafe_ask() diff --git a/pilot/utils/test_files.py b/pilot/utils/test_files.py new file mode 100644 index 0000000..74aa277 --- /dev/null +++ b/pilot/utils/test_files.py @@ -0,0 +1,26 @@ +import pytest +from .files import setup_workspace + + +def test_setup_workspace_with_existing_workspace(): + args = {'workspace': 'some_directory', 'name': 'sample'} + result = setup_workspace(args) + assert result == 'some_directory' + + +def mocked_create_directory(path, exist_ok=True): + return + + +def mocked_abspath(file): + return "/root_path/pilot/helpers" + + +def test_setup_workspace_without_existing_workspace(monkeypatch): + args = {'workspace': None, 'name': 'project_name'} + + monkeypatch.setattr('os.path.abspath', mocked_abspath) + monkeypatch.setattr('os.makedirs', mocked_create_directory) + + result = setup_workspace(args) + assert result.replace('\\', '/') == "/root_path/workspace/project_name"