From 6bbc0f109c0c4805f21a89f7bd2f8164c6ceaacd Mon Sep 17 00:00:00 2001 From: LeonOstrez Date: Fri, 28 Jul 2023 10:24:13 +0200 Subject: [PATCH] add user_tasks, use questionary instead of inquirer, refactor main.py --- euclid/const/common.py | 12 ++- euclid/database/database.py | 11 ++- euclid/main.py | 96 ++----------------- euclid/prompts/prompts.py | 42 ++++---- .../project_description.py | 48 ++++++++++ euclid/steps/user_stories/user_stories.py | 46 +++++++++ euclid/steps/user_tasks/user_tasks.py | 46 +++++++++ euclid/utils/utils.py | 6 +- requirements.txt | 2 + 9 files changed, 199 insertions(+), 110 deletions(-) create mode 100644 euclid/steps/project_description/project_description.py create mode 100644 euclid/steps/user_stories/user_stories.py create mode 100644 euclid/steps/user_tasks/user_tasks.py diff --git a/euclid/const/common.py b/euclid/const/common.py index d24e05d..166421e 100644 --- a/euclid/const/common.py +++ b/euclid/const/common.py @@ -1,8 +1,16 @@ APP_TYPES = ['Web App', 'Script', 'Mobile App (unavailable)', 'Chrome Extension (unavailable)'] ROLES = { - 'product_owner': ['project_summary', 'user_stories', 'user_tasks'], + 'product_owner': ['project_description', 'user_stories', 'user_tasks'], 'architect': ['architecture'], 'tech_lead': ['development_planing'], 'full_stack_developer': ['create_scripts', 'coding'] } -STEPS = ['project_summary', 'user_stories', 'user_tasks', 'development_planing', 'create_scripts', 'coding'] +STEPS = [ + 'project_description', + 'user_stories', + 'user_tasks', + 'architecture', + 'development_planing', + 'create_scripts', + 'coding' +] diff --git a/euclid/database/database.py b/euclid/database/database.py index cfbd850..3191aa8 100644 --- a/euclid/database/database.py +++ b/euclid/database/database.py @@ -63,7 +63,8 @@ def create_tables(): updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (app_id) REFERENCES apps (app_id) - ON UPDATE CASCADE ON DELETE CASCADE + ON UPDATE CASCADE ON DELETE CASCADE, + UNIQUE (app_id, step) ) """) @@ -113,9 +114,14 @@ def save_progress(app_id, step, data): if isinstance(data, dict): data = json.dumps(data) + # INSERT the data, but on conflict (if the app_id and step combination already exists) UPDATE the data insert = sql.SQL( - "INSERT INTO progress_steps (app_id, step, data, completed) VALUES (%s, %s, %s, false)" + """INSERT INTO progress_steps (app_id, step, data, completed) + VALUES (%s, %s, %s, false) + ON CONFLICT (app_id, step) DO UPDATE + SET data = excluded.data, completed = excluded.completed""" ) + cursor.execute(insert, (app_id, step, data)) conn.commit() @@ -123,6 +129,7 @@ def save_progress(app_id, step, data): conn.close() + def get_apps_by_id(app_id): conn = create_connection() cursor = conn.cursor() diff --git a/euclid/main.py b/euclid/main.py index 20dc7e6..166c689 100644 --- a/euclid/main.py +++ b/euclid/main.py @@ -1,17 +1,14 @@ -# main_old.py +# main.py from __future__ import print_function, unicode_literals -import uuid -import json from dotenv import load_dotenv -from termcolor import colored -from utils.utils import get_arguments, execute_step, split_into_bullets, find_role_from_step -from database.database import save_progress, save_app, get_progress_steps +from utils.utils import get_arguments from logger.logger import logger -from prompts.prompts import ask_for_app_type, ask_for_main_app_definition, get_additional_info_from_openai, \ - get_additional_info_from_user, generate_messages_from_description, execute_chat_prompt -from utils.llm_connection import get_prompt + +from steps.project_description.project_description import get_project_description +from steps.user_stories.user_stories import get_user_stories +from steps.user_tasks.user_tasks import get_user_tasks def init(): @@ -24,85 +21,6 @@ def init(): return arguments -def get_project_description(args): - current_step = 'project_summary' - # If this app_id already did this step, just get all data from DB and don't ask user again - steps = get_progress_steps(args['app_id'], current_step) - if steps and not execute_step(args['step'], current_step): - first_step = steps[0] - data = json.loads(first_step['data']) - - summary = data.get('summary') - app_data = data.get('app_data') - args.update(app_data) - - message = f"Project summary already done for this app_id: {args['app_id']}. Moving to next step..." - print(colored(message, "green")) - logger.info(message) - - return summary - - # PROJECT DESCRIPTION - app_type = ask_for_app_type() - - save_app(args['user_id'], args['app_id'], app_type) - - description = ask_for_main_app_definition() - - high_level_messages = get_additional_info_from_openai(generate_messages_from_description(description, app_type)) - - high_level_summary = execute_chat_prompt('utils/summary.prompt', - {'conversation': '\n'.join( - [f"{msg['role']}: {msg['content']}" for msg in high_level_messages])}, - current_step) - - app_data = {'app_id': args['app_id'], 'app_type': app_type} - args['app_type'] = app_type - - save_progress(args['app_id'], current_step, - {"messages": high_level_messages, "summary": high_level_summary, "app_data": app_data}) - - return high_level_summary - # PROJECT DESCRIPTION END - - -def get_user_stories(summary, args): - current_step = 'user_stories' - role = find_role_from_step(current_step) - # If this app_id already did this step, just get all data from DB and don't ask user again - steps = get_progress_steps(args['app_id'], current_step) - if steps and not execute_step(args['step'], current_step): - first_step = steps[0] - data = json.loads(first_step['data']) - - summary = data.get('summary') - app_data = data.get('app_data') - args.update(app_data) - - message = f"User stories already done for this app_id: {args['app_id']}. Moving to next step..." - print(colored(message, "green")) - logger.info(message) - return summary, args - - # USER STORIES - print(colored(f"Generating user stories...\n", "green")) - logger.info(f"Generating user stories...") - - user_stories = execute_chat_prompt('user_stories/specs.prompt', - {'summary': summary, 'app_type': args['app_type']}, - current_step) - - logger.info(split_into_bullets(user_stories)) - user_stories = get_additional_info_from_user(split_into_bullets(user_stories), role) - - logger.info(f"Final user stories: {user_stories}") - - save_progress(args['app_id'], current_step, {"user_stories": user_stories}) - - return user_stories - # USER STORIES END - - if __name__ == "__main__": args = init() @@ -110,6 +28,8 @@ if __name__ == "__main__": user_stories = get_user_stories(high_level_summary, args) + user_tasks = get_user_tasks(user_stories, args) + # get architecture plan # development diff --git a/euclid/prompts/prompts.py b/euclid/prompts/prompts.py index 058d480..31c4070 100644 --- a/euclid/prompts/prompts.py +++ b/euclid/prompts/prompts.py @@ -2,6 +2,7 @@ import inquirer from inquirer.themes import GreenPassion from termcolor import colored +import questionary from const import common from const.llm import MAX_QUESTIONS, END_RESPONSE @@ -67,21 +68,17 @@ def ask_for_main_app_definition(): def ask_user(question): while True: - questions = [ - inquirer.Text('answer', message=question) - ] + answer = questionary.text(question).ask() - answers = inquirer.prompt(questions, theme=GreenPassion()) - - if answers is None: + if answer is None: print("Exiting application.") exit(0) - if answers['answer'].strip() == '': + if answer.strip() == '': print("No input provided! Please try again.") continue else: - return answers['answer'] + return answer def get_additional_info_from_openai(messages): @@ -115,12 +112,15 @@ def get_additional_info_from_user(messages, role): for message in messages: while True: - print(colored(f"Please check this message and say what needs to be changed. If everything is ok just type 'DONE'.", "yellow")) + print(colored( + f"Please check this message and say what needs to be changed. If everything is ok just type 'DONE'.", + "yellow")) answer = ask_user(message) if answer.lower() == 'done': break response = create_gpt_chat_completion( - generate_messages_from_custom_conversation(role, [get_prompt('utils/update.prompt'), message, answer], 'user'), + generate_messages_from_custom_conversation(role, [get_prompt('utils/update.prompt'), message, answer], + 'user'), 'additional_info') message = response @@ -146,6 +146,7 @@ def generate_messages_from_description(description, app_type): def generate_messages_from_custom_conversation(role, messages, start_role='user'): + # messages is list of strings result = [get_sys_message(role)] for i, message in enumerate(messages): @@ -157,21 +158,28 @@ def generate_messages_from_custom_conversation(role, messages, start_role='user' return result -def execute_chat_prompt(prompt_file, prompt_data, chat_type): +def execute_chat_prompt(prompt_file, prompt_data, chat_type, previous_messages=None): # Generate a prompt for the completion type. prompt = get_prompt(prompt_file, prompt_data) + new_message = {"role": "user", "content": prompt} - # Pass the prompt to the API. - messages = [ - get_sys_message(find_role_from_step(chat_type)), - {"role": "user", "content": prompt}, - ] + if previous_messages: + # Use the provided previous_messages instead of the default system message. + messages = previous_messages + [new_message] + else: + # Use the default system message. + messages = [ + get_sys_message(find_role_from_step(chat_type)), + new_message, + ] response = create_gpt_chat_completion(messages, chat_type) + messages.append({"role": "assistant", "content": response}) + print_msg = capitalize_first_word_with_underscores(chat_type) print(colored(f"{print_msg}:\n", "green")) print(f"{response}") logger.info(f"{print_msg}: {response}\n") - return response \ No newline at end of file + return response, messages diff --git a/euclid/steps/project_description/project_description.py b/euclid/steps/project_description/project_description.py new file mode 100644 index 0000000..87ec11d --- /dev/null +++ b/euclid/steps/project_description/project_description.py @@ -0,0 +1,48 @@ +# project_description.py +import json +from termcolor import colored + +from logger.logger import logger +from database.database import save_progress, save_app, get_progress_steps +from utils.utils import execute_step, generate_app_data +from prompts.prompts import ask_for_app_type, ask_for_main_app_definition, get_additional_info_from_openai, \ + generate_messages_from_description, execute_chat_prompt + +def get_project_description(args): + current_step = 'project_description' + # If this app_id already did this step, just get all data from DB and don't ask user again + steps = get_progress_steps(args['app_id'], current_step) + if steps and not execute_step(args['step'], current_step): + first_step = steps[0] + data = json.loads(first_step['data']) + + summary = data.get('summary') + app_data = data.get('app_data') + args.update(app_data) + + message = f"Project summary already done for this app_id: {args['app_id']}. Moving to next step..." + print(colored(message, "green")) + logger.info(message) + + return summary + + # PROJECT DESCRIPTION + args['app_type'] = ask_for_app_type() + + save_app(args['user_id'], args['app_id'], args['app_type']) + + description = ask_for_main_app_definition() + + high_level_messages = get_additional_info_from_openai( + generate_messages_from_description(description, args['app_type'])) + + high_level_summary, high_level_messages = execute_chat_prompt('utils/summary.prompt', + {'conversation': '\n'.join( + [f"{msg['role']}: {msg['content']}" for msg in high_level_messages])}, + current_step) + + save_progress(args['app_id'], current_step, + {"messages": high_level_messages, "summary": high_level_summary, "app_data": generate_app_data(args)}) + + return high_level_summary + # PROJECT DESCRIPTION END diff --git a/euclid/steps/user_stories/user_stories.py b/euclid/steps/user_stories/user_stories.py new file mode 100644 index 0000000..4a92421 --- /dev/null +++ b/euclid/steps/user_stories/user_stories.py @@ -0,0 +1,46 @@ +# user_stories.py +import json +from termcolor import colored + +from utils.utils import execute_step, split_into_bullets, find_role_from_step, generate_app_data +from database.database import save_progress, get_progress_steps +from logger.logger import logger +from prompts.prompts import get_additional_info_from_user, execute_chat_prompt + + +def get_user_stories(summary, args): + current_step = 'user_stories' + role = find_role_from_step(current_step) + # If this app_id already did this step, just get all data from DB and don't ask user again + steps = get_progress_steps(args['app_id'], current_step) + if steps and not execute_step(args['step'], current_step): + first_step = steps[0] + data = json.loads(first_step['data']) + + user_stories = data.get('user_stories') + app_data = data.get('app_data') + if app_data is not None: + args.update(app_data) + + message = f"User stories already done for this app_id: {args['app_id']}. Moving to next step..." + print(colored(message, "green")) + logger.info(message) + return user_stories + + # USER STORIES + print(colored(f"Generating user stories...\n", "green")) + logger.info(f"Generating user stories...") + + user_stories, user_stories_messages = execute_chat_prompt('user_stories/specs.prompt', + {'prompt': summary, 'app_type': args['app_type']}, + current_step) + + logger.info(split_into_bullets(user_stories)) + user_stories = get_additional_info_from_user(split_into_bullets(user_stories), role) + + logger.info(f"Final user stories: {user_stories}") + + save_progress(args['app_id'], current_step, {"user_stories": user_stories, "app_data": generate_app_data(args)}) + + return user_stories + # USER STORIES END \ No newline at end of file diff --git a/euclid/steps/user_tasks/user_tasks.py b/euclid/steps/user_tasks/user_tasks.py new file mode 100644 index 0000000..76f213f --- /dev/null +++ b/euclid/steps/user_tasks/user_tasks.py @@ -0,0 +1,46 @@ +# user_tasks.py +import json +from termcolor import colored + +from utils.utils import execute_step, split_into_bullets, find_role_from_step, generate_app_data +from database.database import save_progress, get_progress_steps +from logger.logger import logger +from prompts.prompts import get_additional_info_from_user, execute_chat_prompt + + +def get_user_tasks(summary, args): + current_step = 'user_tasks' + role = find_role_from_step(current_step) + # If this app_id already did this step, just get all data from DB and don't ask user again + steps = get_progress_steps(args['app_id'], current_step) + if steps and not execute_step(args['step'], current_step): + first_step = steps[0] + data = json.loads(first_step['data']) + + summary = data.get('summary') + app_data = data.get('app_data') + if app_data is not None: + args.update(app_data) + + message = f"User tasks already done for this app_id: {args['app_id']}. Moving to next step..." + print(colored(message, "green")) + logger.info(message) + return summary + + # USER TASKS + print(colored(f"Generating user tasks...\n", "green")) + logger.info(f"Generating user tasks...") + + user_tasks, user_tasks_messages = execute_chat_prompt('user_tasks/specs.prompt', + {'prompt': summary, 'app_type': args['app_type']}, + current_step) + + logger.info(split_into_bullets(user_tasks)) + user_tasks = get_additional_info_from_user(split_into_bullets(user_tasks), role) + + logger.info(f"Final user tasks: {user_tasks}") + + save_progress(args['app_id'], current_step, {"user_tasks": user_tasks, "app_data": generate_app_data(args)}) + + return user_tasks + # USER TASKS END \ No newline at end of file diff --git a/euclid/utils/utils.py b/euclid/utils/utils.py index 2808b4c..5d2f05d 100644 --- a/euclid/utils/utils.py +++ b/euclid/utils/utils.py @@ -38,7 +38,7 @@ def get_arguments(): if 'step' not in arguments: arguments['step'] = None - print(f"If you wish to continue with this project in future run 'python main.py app_id={arguments['app_id']}") + print(f"If you wish to continue with this project in future run 'python main.py app_id={arguments['app_id']}'") return arguments @@ -149,3 +149,7 @@ def split_into_bullets(text): split_text = re.split(pattern, text) split_text = [bullet for bullet in split_text if bullet] # Remove any empty strings from the list return split_text + + +def generate_app_data(args): + return {'app_id': args['app_id'], 'app_type': args['app_type']} diff --git a/requirements.txt b/requirements.txt index 22978e1..e3c2bc4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,9 +6,11 @@ idna==3.4 inquirer==3.1.3 Jinja2==3.1.2 MarkupSafe==2.1.3 +prompt-toolkit==3.0.39 psycopg2==2.9.6 python-dotenv==1.0.0 python-editor==1.0.4 +questionary==1.10.0 readchar==4.0.5 regex==2023.6.3 requests==2.31.0