diff --git a/.gitignore b/.gitignore index 68bc17f..66381f8 100644 --- a/.gitignore +++ b/.gitignore @@ -157,4 +157,8 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +.idea/ + + +# Logger +/euclid/logger/debug.log \ No newline at end of file diff --git a/euclid/bin/Activate.ps1 b/euclid/bin/Activate.ps1 deleted file mode 100644 index b49d77b..0000000 --- a/euclid/bin/Activate.ps1 +++ /dev/null @@ -1,247 +0,0 @@ -<# -.Synopsis -Activate a Python virtual environment for the current PowerShell session. - -.Description -Pushes the python executable for a virtual environment to the front of the -$Env:PATH environment variable and sets the prompt to signify that you are -in a Python virtual environment. Makes use of the command line switches as -well as the `pyvenv.cfg` file values present in the virtual environment. - -.Parameter VenvDir -Path to the directory that contains the virtual environment to activate. The -default value for this is the parent of the directory that the Activate.ps1 -script is located within. - -.Parameter Prompt -The prompt prefix to display when this virtual environment is activated. By -default, this prompt is the name of the virtual environment folder (VenvDir) -surrounded by parentheses and followed by a single space (ie. '(.venv) '). - -.Example -Activate.ps1 -Activates the Python virtual environment that contains the Activate.ps1 script. - -.Example -Activate.ps1 -Verbose -Activates the Python virtual environment that contains the Activate.ps1 script, -and shows extra information about the activation as it executes. - -.Example -Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv -Activates the Python virtual environment located in the specified location. - -.Example -Activate.ps1 -Prompt "MyPython" -Activates the Python virtual environment that contains the Activate.ps1 script, -and prefixes the current prompt with the specified string (surrounded in -parentheses) while the virtual environment is active. - -.Notes -On Windows, it may be required to enable this Activate.ps1 script by setting the -execution policy for the user. You can do this by issuing the following PowerShell -command: - -PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser - -For more information on Execution Policies: -https://go.microsoft.com/fwlink/?LinkID=135170 - -#> -Param( - [Parameter(Mandatory = $false)] - [String] - $VenvDir, - [Parameter(Mandatory = $false)] - [String] - $Prompt -) - -<# Function declarations --------------------------------------------------- #> - -<# -.Synopsis -Remove all shell session elements added by the Activate script, including the -addition of the virtual environment's Python executable from the beginning of -the PATH variable. - -.Parameter NonDestructive -If present, do not remove this function from the global namespace for the -session. - -#> -function global:deactivate ([switch]$NonDestructive) { - # Revert to original values - - # The prior prompt: - if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) { - Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt - Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT - } - - # The prior PYTHONHOME: - if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) { - Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME - Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME - } - - # The prior PATH: - if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) { - Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH - Remove-Item -Path Env:_OLD_VIRTUAL_PATH - } - - # Just remove the VIRTUAL_ENV altogether: - if (Test-Path -Path Env:VIRTUAL_ENV) { - Remove-Item -Path env:VIRTUAL_ENV - } - - # Just remove VIRTUAL_ENV_PROMPT altogether. - if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) { - Remove-Item -Path env:VIRTUAL_ENV_PROMPT - } - - # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: - if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { - Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force - } - - # Leave deactivate function in the global namespace if requested: - if (-not $NonDestructive) { - Remove-Item -Path function:deactivate - } -} - -<# -.Description -Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the -given folder, and returns them in a map. - -For each line in the pyvenv.cfg file, if that line can be parsed into exactly -two strings separated by `=` (with any amount of whitespace surrounding the =) -then it is considered a `key = value` line. The left hand string is the key, -the right hand is the value. - -If the value starts with a `'` or a `"` then the first and last character is -stripped from the value before being captured. - -.Parameter ConfigDir -Path to the directory that contains the `pyvenv.cfg` file. -#> -function Get-PyVenvConfig( - [String] - $ConfigDir -) { - Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" - - # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). - $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue - - # An empty map will be returned if no config file is found. - $pyvenvConfig = @{ } - - if ($pyvenvConfigPath) { - - Write-Verbose "File exists, parse `key = value` lines" - $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath - - $pyvenvConfigContent | ForEach-Object { - $keyval = $PSItem -split "\s*=\s*", 2 - if ($keyval[0] -and $keyval[1]) { - $val = $keyval[1] - - # Remove extraneous quotations around a string value. - if ("'""".Contains($val.Substring(0, 1))) { - $val = $val.Substring(1, $val.Length - 2) - } - - $pyvenvConfig[$keyval[0]] = $val - Write-Verbose "Adding Key: '$($keyval[0])'='$val'" - } - } - } - return $pyvenvConfig -} - - -<# Begin Activate script --------------------------------------------------- #> - -# Determine the containing directory of this script -$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition -$VenvExecDir = Get-Item -Path $VenvExecPath - -Write-Verbose "Activation script is located in path: '$VenvExecPath'" -Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" -Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)" - -# Set values required in priority: CmdLine, ConfigFile, Default -# First, get the location of the virtual environment, it might not be -# VenvExecDir if specified on the command line. -if ($VenvDir) { - Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" -} -else { - Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." - $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") - Write-Verbose "VenvDir=$VenvDir" -} - -# Next, read the `pyvenv.cfg` file to determine any required value such -# as `prompt`. -$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir - -# Next, set the prompt from the command line, or the config file, or -# just use the name of the virtual environment folder. -if ($Prompt) { - Write-Verbose "Prompt specified as argument, using '$Prompt'" -} -else { - Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" - if ($pyvenvCfg -and $pyvenvCfg['prompt']) { - Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" - $Prompt = $pyvenvCfg['prompt']; - } - else { - Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)" - Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'" - $Prompt = Split-Path -Path $venvDir -Leaf - } -} - -Write-Verbose "Prompt = '$Prompt'" -Write-Verbose "VenvDir='$VenvDir'" - -# Deactivate any currently active virtual environment, but leave the -# deactivate function in place. -deactivate -nondestructive - -# Now set the environment variable VIRTUAL_ENV, used by many tools to determine -# that there is an activated venv. -$env:VIRTUAL_ENV = $VenvDir - -if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { - - Write-Verbose "Setting prompt to '$Prompt'" - - # Set the prompt to include the env name - # Make sure _OLD_VIRTUAL_PROMPT is global - function global:_OLD_VIRTUAL_PROMPT { "" } - Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT - New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt - - function global:prompt { - Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) " - _OLD_VIRTUAL_PROMPT - } - $env:VIRTUAL_ENV_PROMPT = $Prompt -} - -# Clear PYTHONHOME -if (Test-Path -Path Env:PYTHONHOME) { - Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME - Remove-Item -Path Env:PYTHONHOME -} - -# Add the venv to the PATH -Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH -$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH" diff --git a/euclid/bin/activate b/euclid/bin/activate deleted file mode 100644 index e7d3a5b..0000000 --- a/euclid/bin/activate +++ /dev/null @@ -1,69 +0,0 @@ -# This file must be used with "source bin/activate" *from bash* -# you cannot run it directly - -deactivate () { - # reset old environment variables - if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then - PATH="${_OLD_VIRTUAL_PATH:-}" - export PATH - unset _OLD_VIRTUAL_PATH - fi - if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then - PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" - export PYTHONHOME - unset _OLD_VIRTUAL_PYTHONHOME - fi - - # This should detect bash and zsh, which have a hash command that must - # be called to get it to forget past commands. Without forgetting - # past commands the $PATH changes we made may not be respected - if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then - hash -r 2> /dev/null - fi - - if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then - PS1="${_OLD_VIRTUAL_PS1:-}" - export PS1 - unset _OLD_VIRTUAL_PS1 - fi - - unset VIRTUAL_ENV - unset VIRTUAL_ENV_PROMPT - if [ ! "${1:-}" = "nondestructive" ] ; then - # Self destruct! - unset -f deactivate - fi -} - -# unset irrelevant variables -deactivate nondestructive - -VIRTUAL_ENV="/Users/zvonimirsabljic/Development/euclid/euclid" -export VIRTUAL_ENV - -_OLD_VIRTUAL_PATH="$PATH" -PATH="$VIRTUAL_ENV/bin:$PATH" -export PATH - -# unset PYTHONHOME if set -# this will fail if PYTHONHOME is set to the empty string (which is bad anyway) -# could use `if (set -u; : $PYTHONHOME) ;` in bash -if [ -n "${PYTHONHOME:-}" ] ; then - _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" - unset PYTHONHOME -fi - -if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then - _OLD_VIRTUAL_PS1="${PS1:-}" - PS1="(euclid) ${PS1:-}" - export PS1 - VIRTUAL_ENV_PROMPT="(euclid) " - export VIRTUAL_ENV_PROMPT -fi - -# This should detect bash and zsh, which have a hash command that must -# be called to get it to forget past commands. Without forgetting -# past commands the $PATH changes we made may not be respected -if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then - hash -r 2> /dev/null -fi diff --git a/euclid/bin/activate.csh b/euclid/bin/activate.csh deleted file mode 100644 index 73b9ad8..0000000 --- a/euclid/bin/activate.csh +++ /dev/null @@ -1,26 +0,0 @@ -# This file must be used with "source bin/activate.csh" *from csh*. -# You cannot run it directly. -# Created by Davide Di Blasi . -# Ported to Python 3.3 venv by Andrew Svetlov - -alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate' - -# Unset irrelevant variables. -deactivate nondestructive - -setenv VIRTUAL_ENV "/Users/zvonimirsabljic/Development/euclid/euclid" - -set _OLD_VIRTUAL_PATH="$PATH" -setenv PATH "$VIRTUAL_ENV/bin:$PATH" - - -set _OLD_VIRTUAL_PROMPT="$prompt" - -if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then - set prompt = "(euclid) $prompt" - setenv VIRTUAL_ENV_PROMPT "(euclid) " -endif - -alias pydoc python -m pydoc - -rehash diff --git a/euclid/bin/activate.fish b/euclid/bin/activate.fish deleted file mode 100644 index 0fd1ab7..0000000 --- a/euclid/bin/activate.fish +++ /dev/null @@ -1,69 +0,0 @@ -# This file must be used with "source /bin/activate.fish" *from fish* -# (https://fishshell.com/); you cannot run it directly. - -function deactivate -d "Exit virtual environment and return to normal shell environment" - # reset old environment variables - if test -n "$_OLD_VIRTUAL_PATH" - set -gx PATH $_OLD_VIRTUAL_PATH - set -e _OLD_VIRTUAL_PATH - end - if test -n "$_OLD_VIRTUAL_PYTHONHOME" - set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME - set -e _OLD_VIRTUAL_PYTHONHOME - end - - if test -n "$_OLD_FISH_PROMPT_OVERRIDE" - set -e _OLD_FISH_PROMPT_OVERRIDE - # prevents error when using nested fish instances (Issue #93858) - if functions -q _old_fish_prompt - functions -e fish_prompt - functions -c _old_fish_prompt fish_prompt - functions -e _old_fish_prompt - end - end - - set -e VIRTUAL_ENV - set -e VIRTUAL_ENV_PROMPT - if test "$argv[1]" != "nondestructive" - # Self-destruct! - functions -e deactivate - end -end - -# Unset irrelevant variables. -deactivate nondestructive - -set -gx VIRTUAL_ENV "/Users/zvonimirsabljic/Development/euclid/euclid" - -set -gx _OLD_VIRTUAL_PATH $PATH -set -gx PATH "$VIRTUAL_ENV/bin" $PATH - -# Unset PYTHONHOME if set. -if set -q PYTHONHOME - set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME - set -e PYTHONHOME -end - -if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" - # fish uses a function instead of an env var to generate the prompt. - - # Save the current fish_prompt function as the function _old_fish_prompt. - functions -c fish_prompt _old_fish_prompt - - # With the original prompt function renamed, we can override with our own. - function fish_prompt - # Save the return status of the last command. - set -l old_status $status - - # Output the venv prompt; color taken from the blue of the Python logo. - printf "%s%s%s" (set_color 4B8BBE) "(euclid) " (set_color normal) - - # Restore the return status of the previous command. - echo "exit $old_status" | . - # Output the original/"old" prompt. - _old_fish_prompt - end - - set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" - set -gx VIRTUAL_ENV_PROMPT "(euclid) " -end diff --git a/euclid/bin/dotenv b/euclid/bin/dotenv deleted file mode 100755 index ccd565a..0000000 --- a/euclid/bin/dotenv +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/zvonimirsabljic/Development/euclid/euclid/bin/python3.11 -# -*- coding: utf-8 -*- -import re -import sys -from dotenv.__main__ import cli -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(cli()) diff --git a/euclid/bin/normalizer b/euclid/bin/normalizer deleted file mode 100755 index b4af46b..0000000 --- a/euclid/bin/normalizer +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/zvonimirsabljic/Development/euclid/euclid/bin/python3.11 -# -*- coding: utf-8 -*- -import re -import sys -from charset_normalizer.cli.normalizer import cli_detect -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(cli_detect()) diff --git a/euclid/bin/pip b/euclid/bin/pip deleted file mode 100755 index 33f1b9a..0000000 --- a/euclid/bin/pip +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/zvonimirsabljic/Development/euclid/euclid/bin/python3.11 -# -*- coding: utf-8 -*- -import re -import sys -from pip._internal.cli.main import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/euclid/bin/pip3 b/euclid/bin/pip3 deleted file mode 100755 index 33f1b9a..0000000 --- a/euclid/bin/pip3 +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/zvonimirsabljic/Development/euclid/euclid/bin/python3.11 -# -*- coding: utf-8 -*- -import re -import sys -from pip._internal.cli.main import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/euclid/bin/pip3.11 b/euclid/bin/pip3.11 deleted file mode 100755 index 33f1b9a..0000000 --- a/euclid/bin/pip3.11 +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/zvonimirsabljic/Development/euclid/euclid/bin/python3.11 -# -*- coding: utf-8 -*- -import re -import sys -from pip._internal.cli.main import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/euclid/bin/pygmentize b/euclid/bin/pygmentize deleted file mode 100755 index 7b454e1..0000000 --- a/euclid/bin/pygmentize +++ /dev/null @@ -1,8 +0,0 @@ -#!/Users/zvonimirsabljic/Development/euclid/euclid/bin/python3.11 -# -*- coding: utf-8 -*- -import re -import sys -from pygments.cmdline import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/euclid/bin/python b/euclid/bin/python deleted file mode 120000 index 6e7f3c7..0000000 --- a/euclid/bin/python +++ /dev/null @@ -1 +0,0 @@ -python3.11 \ No newline at end of file diff --git a/euclid/bin/python3 b/euclid/bin/python3 deleted file mode 120000 index 6e7f3c7..0000000 --- a/euclid/bin/python3 +++ /dev/null @@ -1 +0,0 @@ -python3.11 \ No newline at end of file diff --git a/euclid/bin/python3.11 b/euclid/bin/python3.11 deleted file mode 120000 index 3cf1fbd..0000000 --- a/euclid/bin/python3.11 +++ /dev/null @@ -1 +0,0 @@ -/opt/homebrew/opt/python@3.11/bin/python3.11 \ No newline at end of file diff --git a/euclid/const/prompts.py b/euclid/const/prompts.py index 061d6d3..261e3a7 100644 --- a/euclid/const/prompts.py +++ b/euclid/const/prompts.py @@ -1,4 +1,8 @@ SYS_MESSAGE = { - 'tdd_engineer': {'role': 'system', 'content': 'You are a QA engineer and your main goal is to find ways to break the application you\'re testing. You are proficient in writing automated integration tests for Node.js API servers.\n' + - 'When you respond, you don\'t say anything except the code - no formatting, no explanation - only code.\n' }, -} \ No newline at end of file + "tdd_engineer": { + "role": "system", + "content": "You are an experienced software engineer who is proficient in node.js and who practices TDD (Test " + "Driven Development). Usually, you look at the code that already exists and a written test - then " + "you think step by step and modify the function that's being tested to make the test pass." + }, +} diff --git a/euclid/database/__init__.py b/euclid/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/euclid/database.py b/euclid/database/database.py similarity index 86% rename from euclid/database.py rename to euclid/database/database.py index 9607c20..738f328 100644 --- a/euclid/database.py +++ b/euclid/database/database.py @@ -1,8 +1,11 @@ # database.py import psycopg2 +import json from psycopg2 import sql -from euclid.const import db +from const import db +from logger.logger import logger + def create_connection(): conn = psycopg2.connect( @@ -13,6 +16,7 @@ def create_connection(): password=db.DB_PASSWORD) return conn + def create_tables(): commands = ( """ @@ -48,6 +52,7 @@ def create_tables(): id SERIAL PRIMARY KEY, app_id INTEGER NOT NULL, step VARCHAR(255) NOT NULL, + data TEXT, completed BOOLEAN NOT NULL, completed_at TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -57,6 +62,7 @@ def create_tables(): ON UPDATE CASCADE ON DELETE CASCADE ) """) + conn = None try: conn = create_connection() @@ -79,21 +85,31 @@ def save_app(user_id, app_type): cursor.execute("SELECT * FROM users WHERE id = %s", (str(user_id),)) if cursor.fetchone() is None: # If user doesn't exist, create a new user - cursor.execute("INSERT INTO users (id, username, email, password) VALUES (%s, 'username', 'email', 'password')", (str(user_id),)) + cursor.execute("INSERT INTO users (id, username, email, password) VALUES (%s, 'username', 'email', 'password')", + (str(user_id),)) # Now save the app - cursor.execute("INSERT INTO apps (user_id, app_type, status) VALUES (%s, %s, 'started') RETURNING id", (str(user_id), app_type)) + cursor.execute("INSERT INTO apps (user_id, app_type, status) VALUES (%s, %s, 'started') RETURNING id", + (str(user_id), app_type)) app_id = cursor.fetchone()[0] conn.commit() cursor.close() conn.close() + + logger.info('User saved') + return app_id + def save_progress(app_id, step, data): conn = create_connection() cursor = conn.cursor() + # Check if the data is a dictionary. If it is, convert it to a JSON string. + if isinstance(data, dict): + data = json.dumps(data) + insert = sql.SQL( "INSERT INTO progress_steps (app_id, step, data, completed) VALUES (%s, %s, %s, false)" ) @@ -103,5 +119,6 @@ def save_progress(app_id, step, data): cursor.close() conn.close() + if __name__ == "__main__": create_tables() diff --git a/euclid/logger/logger.py b/euclid/logger/logger.py new file mode 100644 index 0000000..448b092 --- /dev/null +++ b/euclid/logger/logger.py @@ -0,0 +1,25 @@ +# logger.py +import logging + + +def setup_logger(): + # Create a custom format for your logs + log_format = "%(asctime)s [%(filename)s:%(lineno)s - %(funcName)20s() ] %(levelname)s: %(message)s" + + # Create a log handler for file output + file_handler = logging.FileHandler(filename='logger/debug.log', mode='w') + file_handler.setLevel(logging.DEBUG) + + # Apply the custom format to the handler + formatter = logging.Formatter(log_format) + file_handler.setFormatter(formatter) + + # Create a logger and add the handler + logger = logging.getLogger() + logger.setLevel(logging.DEBUG) + logger.addHandler(file_handler) + + return logger + + +logger = setup_logger() diff --git a/euclid/main.py b/euclid/main.py index d7859c8..65fdc03 100644 --- a/euclid/main.py +++ b/euclid/main.py @@ -1,104 +1,36 @@ -# main.py - +# main_old.py from __future__ import print_function, unicode_literals -from euclid.const import common -from euclid.utils import llm_connection -import inquirer + import uuid -from inquirer.themes import GreenPassion -from euclid.database import save_progress, save_app +from dotenv import load_dotenv -def break_down_user_flows(description): - user_flows = parse_description_into_user_flows(description) - for flow_index, user_flow in enumerate(user_flows): - is_correct = False - while not is_correct: - print(f"User Flow {flow_index+1}: {user_flow}") - is_correct = ask_for_user_flow_confirmation(flow_index) - save_progress(app_id, f'user_flow_{flow_index+1}', user_flow) - -def ask_for_user_flow_confirmation(flow_index): - questions = [ - inquirer.List('confirmation', - message=f"Does user flow {flow_index+1} meet your requirements? (Yes/No)", - choices=['Yes', 'No'], - ) - ] - - answers = inquirer.prompt(questions, theme=GreenPassion()) - - if answers is None: - print("No input provided!") - return - - if answers['confirmation'] == 'Yes': - return True - else: - return modify_user_flow(flow_index) - -def modify_user_flow(flow_index): - questions = [ - inquirer.Text('correction', message=f"Please provide corrections for user flow {flow_index+1}.") - ] - - answers = inquirer.prompt(questions, theme=GreenPassion()) - user_flows[flow_index] = answers['correction'] - return False - -def ask_for_app_type(): - questions = [ - inquirer.List('type', - message="What type of app do you want to build?", - choices=common.APP_TYPES, - ) - ] - - answers = inquirer.prompt(questions, theme=GreenPassion()) - - while answers is None or 'unavailable' in answers['type']: - if answers is None: - print("You need to make a selection.") - else: - print("Sorry, that option is not available.") - - answers = inquirer.prompt(questions, theme=GreenPassion()) - - print("You chose: " + answers['type']) - return answers['type'] - -def ask_for_main_app_definition(): - questions = [ - inquirer.Text('description', message="Describe your app in as many details as possible.") - ] - - answers = inquirer.prompt(questions, theme=GreenPassion()) - if answers is None: - print("No input provided!") - return - - description = answers['description'] - - while True: - questions = [ - inquirer.Text('confirmation', message="Do you want to add anything else? If not, just press ENTER.") - ] - - answers = inquirer.prompt(questions, theme=GreenPassion()) - if answers is None or answers['confirmation'] == '': - break - elif description[-1] not in ['.', '!', '?', ';']: - description += '.' - - description += ' ' + answers['confirmation'] - - return description +from database.database import save_progress, save_app +from logger.logger import logger +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 if __name__ == "__main__": - app_type = ask_for_app_type(); - user_id = str(uuid.uuid4()); - app_id = save_app(user_id, app_type) - description = ask_for_main_app_definition(); - save_progress(app_id, 'main_description', description); - user_flows = break_down_user_flows(description); + logger.info('Starting') + load_dotenv() + app_type = ask_for_app_type() + + user_id = str(uuid.uuid4()) + app_id = save_app(user_id, app_type) + + description = ask_for_main_app_definition() + + messages = get_additional_info_from_openai(generate_messages_from_description(description, app_type)) + + summary = execute_chat_prompt('summary.pt', + {'conversation': '\n'.join([f"{msg['role']}: {msg['content']}" for msg in messages])}, + 'summarize', + 'Project summary') + + save_progress(app_id, 'main_description', {"messages": messages, "summary": summary}) + + stories = execute_chat_prompt('user_stories.pt', + {'summary': summary, 'app_type': app_type}, + 'user_stories', + 'User stories') diff --git a/euclid/prompts/clarification.pt b/euclid/prompts/clarification.pt new file mode 100644 index 0000000..a10ea2e --- /dev/null +++ b/euclid/prompts/clarification.pt @@ -0,0 +1,12 @@ +{% set description = description.strip() %} +You are an AI engineer trying to understand a project description in order to build it. I want you to create {{app_type}}, however, the current description most likely needs more details for a comprehensive understanding: + +```{{ description }}``` + +If needed, please ask specific and targeted questions to get more information about the project. The questions should be oriented towards understanding the project's requirements, functionalities, target audience, user interactions, desired look and feel, and any other aspect that may be unclear from the initial description. + +In your response don't write any explanations, comments, your thoughts or anything like that. I want your response to be only one question at a time. I will ask you again when I am ready for next question. + +Ask maximum of {{maximum_questions}} questions and after that I want you to respond with "Thank you!". + +If everything is clear before asking those {{maximum_questions}} questions, just respond with "Thank you!". diff --git a/euclid/prompts/prompts.py b/euclid/prompts/prompts.py new file mode 100644 index 0000000..a1caeea --- /dev/null +++ b/euclid/prompts/prompts.py @@ -0,0 +1,137 @@ +# prompts/prompts.py +import inquirer +from inquirer.themes import GreenPassion +from termcolor import colored + +from const import common +from const.prompts import SYS_MESSAGE +from utils.llm_connection import create_gpt_chat_completion, get_prompt +from logger.logger import logger + + +def ask_for_app_type(): + questions = [ + inquirer.List('type', + message="What type of app do you want to build?", + choices=common.APP_TYPES, + ) + ] + + answers = inquirer.prompt(questions, theme=GreenPassion()) + if answers is None: + print("Exiting application.") + exit(0) + + while 'unavailable' in answers['type']: + print("Sorry, that option is not available.") + answers = inquirer.prompt(questions, theme=GreenPassion()) + if answers is None: + print("Exiting application.") + exit(0) + + print("You chose: " + answers['type']) + logger.info(f"You chose: {answers['type']}") + return answers['type'] + + +def ask_for_main_app_definition(): + questions = [ + inquirer.Text('description', message="Describe your app in as many details as possible.") + ] + + answers = inquirer.prompt(questions, theme=GreenPassion()) + if answers is None: + print("No input provided!") + return + + description = answers['description'] + + while True: + questions = [ + inquirer.Text('confirmation', message="Do you want to add anything else? If not, just press ENTER.") + ] + + answers = inquirer.prompt(questions, theme=GreenPassion()) + if answers is None or answers['confirmation'] == '': + break + elif description[-1] not in ['.', '!', '?', ';']: + description += '.' + + description += ' ' + answers['confirmation'] + + logger.info('Initial App description done') + + return description + + +def ask_user(question): + while True: + questions = [ + inquirer.Text('answer', message=question) + ] + + answers = inquirer.prompt(questions, theme=GreenPassion()) + + if answers is None: + print("Exiting application.") + exit(0) + + if answers['answer'].strip() == '': + print("No input provided! Please try again.") + continue + else: + return answers['answer'] + + +def get_additional_info_from_openai(messages): + is_complete = False + while not is_complete: + # Obtain clarifications using the OpenAI API + response = create_gpt_chat_completion(messages, 'additional_info') + + if response is not None: + # Check if the response is "Thank you!" + if response.strip() == "Thank you!": + print(response) + return messages + + # Ask the question to the user + answer = ask_user(response) + + # Add the answer to the messages + messages.append({'role': 'assistant', 'content': response}) + messages.append({'role': 'user', 'content': answer}) + else: + is_complete = True + + logger.info('Getting additional info done') + + return messages + + +def generate_messages_from_description(description, app_type): + prompt = get_prompt('clarification.pt', {'description': description, 'app_type': app_type, 'maximum_questions': 3}) + + return [ + SYS_MESSAGE['tdd_engineer'], + {"role": "user", "content": prompt}, + ] + + +def execute_chat_prompt(prompt_file, prompt_data, chat_completion_type, print_msg): + # Generate a prompt for the completion type. + prompt = get_prompt(prompt_file, prompt_data) + + # Pass the prompt to the API. + messages = [ + SYS_MESSAGE['tdd_engineer'], + {"role": "user", "content": prompt}, + ] + + response = create_gpt_chat_completion(messages, chat_completion_type) + + print(colored(f"{print_msg}:\n", "green")) + print(f"{response}") + logger.info(f"{print_msg}: {response}") + + return response diff --git a/euclid/prompts/summary.pt b/euclid/prompts/summary.pt new file mode 100644 index 0000000..c313ef0 --- /dev/null +++ b/euclid/prompts/summary.pt @@ -0,0 +1,2 @@ +Based on the following conversation, write a summary of the project: +{{conversation}} \ No newline at end of file diff --git a/euclid/prompts/system.pt b/euclid/prompts/system.pt index e6d3a56..aba3cd9 100644 --- a/euclid/prompts/system.pt +++ b/euclid/prompts/system.pt @@ -1 +1 @@ -You are an experienced software engineer who is proficient in node.js and who practices TDD (Test Driven Development). Usually, you look at the code that already exists and a written test - then you think step by step and modify the function that's being tested to make the test pass. \ No newline at end of file +You are an experienced software engineer who practices TDD (Test Driven Development). You always try to think of all details needed for project to be developed using all best practices, to have great UX and great UI. \ No newline at end of file diff --git a/euclid/prompts/user_stories.pt b/euclid/prompts/user_stories.pt new file mode 100644 index 0000000..09a8288 --- /dev/null +++ b/euclid/prompts/user_stories.pt @@ -0,0 +1,3 @@ +Here is the summary of the project I want to create: +{{summary}} +I want you to help me create it. For start I want you to break down each user story that user can do within {{app_type}} that you can think of from description above. \ No newline at end of file diff --git a/euclid/pyvenv.cfg b/euclid/pyvenv.cfg deleted file mode 100644 index b37c220..0000000 --- a/euclid/pyvenv.cfg +++ /dev/null @@ -1,5 +0,0 @@ -home = /opt/homebrew/opt/python@3.11/bin -include-system-site-packages = false -version = 3.11.3 -executable = /opt/homebrew/Cellar/python@3.11/3.11.3/Frameworks/Python.framework/Versions/3.11/bin/python3.11 -command = /opt/homebrew/opt/python@3.11/bin/python3.11 -m venv /Users/zvonimirsabljic/Development/euclid/euclid diff --git a/euclid/utils/llm_connection.py b/euclid/utils/llm_connection.py index f0785ff..bc65900 100644 --- a/euclid/utils/llm_connection.py +++ b/euclid/utils/llm_connection.py @@ -1,37 +1,25 @@ -# llm_connection.py - -import re +# llm_connection_old.py import requests -from dotenv import load_dotenv import os -from tiktoken import Tokenizer +import json +# from tiktoken import Tokenizer from typing import List -from http.server import BaseHTTPRequestHandler -from socketserver import ThreadingMixIn -from http.server import HTTPServer -from euclid.const.llm import MIN_TOKENS_FOR_GPT_RESPONSE, MAX_GPT_MODEL_TOKENS -from euclid.const.prompts import SYS_MESSAGE from jinja2 import Environment, FileSystemLoader +from const.llm import MIN_TOKENS_FOR_GPT_RESPONSE, MAX_GPT_MODEL_TOKENS +from const.prompts import SYS_MESSAGE +from logger.logger import logger +from termcolor import colored + + def connect_to_llm(): pass -def get_user_flows(description): - prompt = get_prompt('breakdown_1_user_flows.prompt', {'description': description}) - - messages = [ - SYS_MESSAGE['tdd_engineer'], - # app type - # - {"role": "user", "content": prompt}, - ] - - create_gpt_chat_completion(messages, min_tokens=MIN_TOKENS_FOR_GPT_RESPONSE) - def get_prompt(prompt_name, data): + logger.debug(f"Getting prompt for {prompt_name} with data {data}") # logging here # Create a file system loader with the directory of the templates - file_loader = FileSystemLoader('../prompts') + file_loader = FileSystemLoader('prompts') # Create the Jinja2 environment env = Environment(loader=file_loader) @@ -44,14 +32,30 @@ def get_prompt(prompt_name, data): return output + +def get_user_flows(description): + prompt = get_prompt('breakdown_1_user_flows.prompt', {'description': description}) + + messages = [ + SYS_MESSAGE['tdd_engineer'], + # app type + # + {"role": "user", "content": prompt}, + ] + + create_gpt_chat_completion(messages, 'user_flows') + + def get_tokens_in_messages(messages: List[str]) -> int: tokenizer = Tokenizer() tokenized_messages = [tokenizer.encode(message) for message in messages] return sum(len(tokens) for tokens in tokenized_messages) -def create_gpt_chat_completion(messages: List[dict], min_tokens=MIN_TOKENS_FOR_GPT_RESPONSE): + +def create_gpt_chat_completion(messages: List[dict], req_type, min_tokens=MIN_TOKENS_FOR_GPT_RESPONSE): api_key = os.getenv("OPENAI_API_KEY") - tokens_in_messages = get_tokens_in_messages(messages) + # tokens_in_messages = get_tokens_in_messages(messages) + tokens_in_messages = 100 if tokens_in_messages + min_tokens > MAX_GPT_MODEL_TOKENS: raise ValueError(f'Too many tokens in messages: {tokens_in_messages}. Please try a different test.') @@ -64,12 +68,20 @@ def create_gpt_chat_completion(messages: List[dict], min_tokens=MIN_TOKENS_FOR_G } try: - return stream_gpt_completion(gpt_data, api_key) + return stream_gpt_completion(gpt_data, req_type) except Exception as e: - print('The request to OpenAI API failed. Might be due to GPT being down or due to the too large message. It\'s best if you try another export.') + print( + 'The request to OpenAI API failed. Might be due to GPT being down or due to the too large message. It\'s ' + 'best if you try again.') print(e) -def stream_gpt_completion(data, api_key): + +def stream_gpt_completion(data, req_type): + print(colored("Waiting for OpenAI API response...", 'yellow')) + api_key = os.getenv("OPENAI_API_KEY") + + logger.info(f'Request data: {data}') + response = requests.post( 'https://api.openai.com/v1/chat/completions', headers={'Content-Type': 'application/json', 'Authorization': 'Bearer ' + api_key}, @@ -77,23 +89,58 @@ def stream_gpt_completion(data, api_key): stream=True ) + # Log the response status code and message + logger.info(f'Response status code: {response.status_code}') + if response.status_code != 200: print(f'problem with request: {response.text}') + logger.debug(f'problem with request: {response.text}') return gpt_response = '' for line in response.iter_lines(): - if line: # filter out keep-alive new lines - json_line = json.loads(line) - if 'error' in json_line or 'message' in json_line: - print(json_line, end="") - return - content = json_line.get('choices')[0]['message']['content'] - gpt_response += content - print(content, end="") + # Ignore keep-alive new lines + if line: + line = line.decode("utf-8") # decode the bytes to string - new_code = postprocessing(gpt_response, 'user_flows') # TODO add type dynamically + if line.startswith('data: '): + line = line[6:] # remove the 'data: ' prefix + + # Check if the line is "[DONE]" before trying to parse it as JSON + if line == "[DONE]": + continue + + try: + json_line = json.loads(line) + except json.JSONDecodeError: + logger.error(f'Unable to decode line: {line}') + continue # skip to the next line + + if 'choices' in json_line: + content = json_line['choices'][0]['delta'].get('content') + if content: + gpt_response += content + + logger.info(f'Response message: {gpt_response}') + new_code = postprocessing(gpt_response, req_type) # TODO add type dynamically return new_code -def postprocessing(gpt_response, type): - pass \ No newline at end of file + +def get_clarifications(description): + prompt = get_prompt('clarification.pt', {'description': description}) + + messages = [ + SYS_MESSAGE['tdd_engineer'], + {"role": "user", "content": prompt}, + ] + + response = create_gpt_chat_completion(messages, 'get_clarifications') + + if response is not None: + messages.append({'role': 'assistant', 'content': response}) + + return messages, response + + +def postprocessing(gpt_response, req_type): + return gpt_response diff --git a/euclid/utils/utils.py b/euclid/utils/utils.py new file mode 100644 index 0000000..25b8721 --- /dev/null +++ b/euclid/utils/utils.py @@ -0,0 +1,48 @@ +# utils/utils.py +import inquirer +from inquirer.themes import GreenPassion + + +def break_down_user_flows(description): + return 'false' + user_flows = parse_description_into_user_flows(description) + for flow_index, user_flow in enumerate(user_flows): + is_correct = False + while not is_correct: + print(f"User Flow {flow_index + 1}: {user_flow}") + is_correct = ask_for_user_flow_confirmation(flow_index) + save_progress(app_id, f'user_flow_{flow_index + 1}', user_flow) + + +def ask_for_user_flow_confirmation(flow_index): + questions = [ + inquirer.List('confirmation', + message=f"Does user flow {flow_index + 1} meet your requirements? (Yes/No)", + choices=['Yes', 'No'], + ) + ] + + answers = inquirer.prompt(questions, theme=GreenPassion()) + + if answers is None: + print("No input provided!") + return + + if answers['confirmation'] == 'Yes': + return True + else: + return modify_user_flow(flow_index) + + +def modify_user_flow(flow_index): + questions = [ + inquirer.Text('correction', message=f"Please provide corrections for user flow {flow_index + 1}.") + ] + + answers = inquirer.prompt(questions, theme=GreenPassion()) + if answers is None: + print("No input provided!") + return False + + user_flows[flow_index] = answers['correction'] + return False