diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..287d82f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,22 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "**/Thumbs.db": true, + ".vscode": true, + "luna/__pycache__": true, + "luna/training/clock/clock/__pycache__": true, + "luna/training/clock/__pycache__": true, + "luna/training/getip/__pycache__": true + }, + "hide-files.files": [ + ".vscode", + "luna/__pycache__", + "luna/training/clock/clock/__pycache__", + "luna/training/clock/__pycache__", + "luna/training/getip/__pycache__" + ] +} \ No newline at end of file diff --git a/luna/__main__.py b/luna/__main__.py new file mode 100644 index 0000000..2fc898b --- /dev/null +++ b/luna/__main__.py @@ -0,0 +1,2 @@ +import main +main.main() \ No newline at end of file diff --git a/luna/ai.py b/luna/ai.py new file mode 100644 index 0000000..b6ad670 --- /dev/null +++ b/luna/ai.py @@ -0,0 +1,23 @@ +"""Main module for Luna.""" + +import os +import openai + +from dotenv import load_dotenv + +load_dotenv() + +if os.getenv('OPENAI_API_BASE'): + openai.api_base = os.getenv('OPENAI_API_BASE') +openai.api_key = os.getenv('OPENAI_API_KEY') + +MODEL = os.getenv('OPENAI_CHAT_MODEL') or 'gpt-3.5-turbo' + +def generate(messages: str): + """Generates a new message based on the given messages.""" + + return openai.ChatCompletion.create( + model=MODEL, + messages=messages, + temperature=0.2 + ).choices[0].message.content diff --git a/luna/main.py b/luna/main.py new file mode 100644 index 0000000..47b3bf2 --- /dev/null +++ b/luna/main.py @@ -0,0 +1,64 @@ +import ai +import os +import prompts +import testing + +from rich import print +from rich.progress import track +from rich.console import Console + +OUTPUT_PATH = r'C:\Users\Lynx\Desktop\luna_outp' + +console = Console(record=True) + +print('[light_steel_blue]Welcome to [bold]L u n a[/] 💜') + +def main(): + """Asks for a project description and generate the project.""" + + try: + prompt = console.input("[orchid1 bold]What kind of project should I create for you? 💬[/] ") + except KeyboardInterrupt: + console.print('\n[orange1 bold]Bye! 👋[/]') + return + + console.print('[yellow2]This might take a while, so go ahead and enjoy a cup of tea. ☕[/]') + + messages = prompts.generate_messages() + messages.append({ + 'role': 'user', + 'content': f'[FILE-STRUCTURE]: {prompt}' + }) + + file_structure = ai.generate(messages).strip() + file_list = ''.join(['\t' + path for path in file_structure]) + console.print(f'[turquoise2 bold]File structure:[/]\n{file_list}') + + messages.append({ + 'role': 'assistant', + 'content': file_structure + }) + + for file_path in track(file_structure.splitlines(), description='[light_steel_blue]Generating files...'): + messages.append({ + 'role': 'user', + 'content': f'[FILE-CONTENT]: {file_path}' + }) + + file_content = ai.generate(messages) + + messages.append({ + 'role': 'assistant', + 'content': file_content + }) + + path = os.path.join(OUTPUT_PATH, file_path).replace('\\', '/') + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'w', encoding='utf8') as file_: + file_.write(file_content) + + console.print('[sea_green1 bold]Done! ✅[/]') + +if __name__ == '__main__': + main() diff --git a/luna/prompts.json b/luna/prompts.json new file mode 100644 index 0000000..146c494 --- /dev/null +++ b/luna/prompts.json @@ -0,0 +1,110 @@ +[ + { + "role": "system", + "content": "You program Python projects by providing every file's\ncontents, a documentation in the form of a Markdown file as well as a list of all files.\nIt is really important that also tests are provided, which check the functionality of the\nproject! The tests should check the functionality of the project, and should be created at the end.\nThe first file content to provide is the README.md file, which should contain a description of the\nproject, as well as a simple documentation on how to run the project, as well as how it works.\n\nFor example, for a web app, you should not only provide the commands to run the project, but also\nthe URL paths and what they do.\n\nKeep in mind to also use a nice CSS design if the project is a web app!\nCompletely avoid using APIs which require an API key, as this would make it impossible to run the project!\nWhenever possible, use APIs which are free to use and don't require an API key instead!\n" + }, + { + "role": "user", + "content": "[FILE-STRUCTURE]: A simple website called SimpleClock. It should display the current time on /." + }, + { + "role": "assistant", + "content": "main.py\nREADME.md\ntests.py\nsimpleclock/__init__.py\nsimpleclock/static/index.js\nsimpleclock/static/style.css\nsimpleclock/templates/base.html\nsimpleclock/templates/index.html\n" + }, + { + "role": "user", + "content": "[FILE-CONTENT]: main.py" + }, + { + "role": "assistant", + "content": "\"\"\"Web server start module.\"\"\"\n\nimport sys\n\nfrom clock import create_app\n\ndef run(port=sys.argv[1] if len(sys.argv) > 1 else 5000):\n \"\"\"Starts the web server.\"\"\"\n create_app().run(port=port)\n\nif __name__ == '__main__':\n run()\n" + }, + { + "role": "user", + "content": "[FILE-CONTENT]: README.md" + }, + { + "role": "assistant", + "content": "# SimpleClock\nA simple web app which displays the current time.\n\n## Get started\nTo get started, follow these steps:\n\n1. Run the app using\n```bash\npython main.py\n```\n2. Open the app in your browser at http://localhost:5000\n3. Enjoy!" + }, + { + "role": "user", + "content": "[FILE-CONTENT]: tests.py" + }, + { + "role": "assistant", + "content": "\"\"\"LUNA Tests.\"\"\"\n\nimport time\nimport requests\nimport multiprocessing\n\nclass Tester:\n \"\"\"Tests if the program is working as expected.\"\"\"\n\n def __init__(self):\n \"\"\"Runs the tests.\"\"\"\n # pylint: disable=import-outside-toplevel\n import main\n\n self.server = multiprocessing.Process(target=main.run, args=(5000,))\n self.server.start()\n\n time.sleep(1)\n print('Server started!')\n\n resp = requests.get('http://localhost:5000/', timeout=5)\n assert 'The current time is:' in resp.text\n\n def stop(self):\n \"\"\"Runs after the tests are done, whether they have been successful or not.\"\"\"\n self.server.terminate()\n" + }, + { + "role": "user", + "content": "[FILE-CONTENT]: simpleclock/__init__.py" + }, + { + "role": "assistant", + "content": "import flask\n\ndef create_app():\n \"\"\"Returns the Flask app.\"\"\"\n\n app = flask.Flask(__name__)\n\n @app.route('/')\n def index():\n \"\"\"Display the current time.\"\"\"\n return flask.render_template('index.html')\n\n return app\n" + }, + { + "role": "user", + "content": "[FILE-CONTENT]: simpleclock/static/index.js" + }, + { + "role": "assistant", + "content": "// use momentjs to display the time, nicely formatted.\n// updates every second\n\nfunction update() {\n time.innerHTML = moment().format('h:mm:ss a');\n }\n\nupdate();\nsetInterval(update, 1000);\n" + }, + { + "role": "user", + "content": "[FILE-CONTENT]: simpleclock/static/style.css" + }, + { + "role": "assistant", + "content": "body {\n font-size: 1.2rem;\n scroll-behavior: smooth;\n}\n\n* {\n box-sizing: border-box;\n font-family: sans-serif;\n}\n\nmain {\n max-width: 800px;\n margin: 0 auto;\n padding: 1rem;\n}\n\nh1 {\n font-size: 2rem;\n}\n\np {\n font-size: 1.2rem;\n}\n\n#time {\n font-weight: bold;\n}\n" + }, + { + "role": "user", + "content": "[FILE-CONTENT]: simpleclock/templates/base.html" + }, + { + "role": "assistant", + "content": "\n\n
\nWelcome to this website!
\nThe current time is: .
\n{% endblock %}" + }, + { + "role": "user", + "content": "[FILE-STRUCTURE]: A very simple CLI which returns the user's IP address." + }, + { + "role": "assistant", + "content": "cli.py\nREADME.md\ntests.py\n" + }, + { + "role": "user", + "content": "[FILE-CONTENT]: cli.py" + }, + { + "role": "assistant", + "content": "\"\"\"Get the IP address of the client.\"\"\"\n\nimport typer\nimport requests\n\napp = typer.Typer()\n\n@app.command()\ndef main():\n \"\"\"Shows the IP address of the client.\"\"\"\n print(requests.get('https://checkip.amazonaws.com', timeout=5).text.strip())\n\nif __name__ == '__main__':\n typer.run(main)\n" + }, + { + "role": "user", + "content": "[FILE-CONTENT]: README.md" + }, + { + "role": "assistant", + "content": "# `GetIP`\nA very simple CLI app that displays the user's IP address.\n\n## Get started\nIt's very simple to run this app. Just run the following command in the terminal:\n\n```bash\npython cli.py\n```\n\nExample output:\n```\n123.123.34.34\n```\n" + }, + { + "role": "user", + "content": "[FILE-CONTENT]: tests.py" + }, + { + "role": "assistant", + "content": "\"\"\"LUNA Tests.\"\"\"\n\nimport os\nimport requests\nimport subprocess\n\nclass Tester:\n \"\"\"Tests if the program is working as expected.\"\"\"\n\n def __init__(self):\n \"\"\"Runs the tests.\"\"\"\n\n # check the print output of cli.app.main()\n os.chdir(os.path.dirname(__file__))\n output = subprocess.check_output(['python', 'cli.py']).decode('utf8')\n\n ip_addr = requests.get('https://checkip.amazonaws.com', timeout=5).text.strip()\n assert ip_addr in output\n\n def stop(self):\n \"\"\"Runs after the tests are done, whether they have been successful or not.\"\"\"\n # no need to stop anything\n" + } +] \ No newline at end of file diff --git a/luna/prompts.py b/luna/prompts.py new file mode 100644 index 0000000..36eb851 --- /dev/null +++ b/luna/prompts.py @@ -0,0 +1,76 @@ +"""The prompt module contains the demo "conversation" messages used to provide an example for the +AI of how to respond to the user.""" + +import os +import json + +messages = [ + {'role': 'system', 'content': """You program Python projects by providing every file's +contents, a documentation in the form of a Markdown file as well as a list of all files. + +It is really important that also tests are provided, which check the functionality of the +project! The tests should check the functionality of the project, and should be created at the end. +The first file content to provide is the README.md file, which should contain a description of the +project, as well as a simple documentation on how to run the project, as well as how it works. + +For example, for a web app, you should not only provide the commands to run the project, but also +the URL paths and what they do. + +Keep in mind to also use a nice CSS design if the project is a web app! +Completely avoid using APIs which require an API key, as this would make it impossible to run the project! +Whenever possible, use APIs which are free to use and don't require an API key instead! +"""} +] + +def generate_messages(): + """Generates the messages for the AI "conversation". + """ + + for example in os.listdir('luna/training'): + example_path = f'luna/training/{example}' + + with open(f'{example_path}/_prompt.luna.txt', encoding='utf8') as prompt_file: + prompt = prompt_file.read() + + messages.append({ + 'role': 'user', + 'content': f'[FILE-STRUCTURE]: {prompt}' + }) + + contents = {} + + for root, _, files in os.walk(example_path): + for file_ in files: + file_path = os.path.join(root, file_)\ +.replace('\\', '/').replace(example_path + '/', '') + + if '__pycache__' in file_path or '.luna.' in file_path: + continue + + contents[file_path] = open(os.path.join(root, file_), encoding='utf8').read() + + structure = '' + + for file_path in contents: + structure += f'{file_path}\n' + + messages.append({ + 'role': 'assistant', + 'content': structure + }) + + for file_path, content in contents.items(): + messages.append({ + 'role': 'user', + 'content': f'[FILE-CONTENT]: {file_path}' + }) + + messages.append({ + 'role': 'assistant', + 'content': content + }) + + return messages + +if __name__ == '__main__': + json.dump(generate_messages(), open('luna/prompts.json', 'w', encoding='utf8'), indent=4) diff --git a/luna/testing.py b/luna/testing.py new file mode 100644 index 0000000..144f37d --- /dev/null +++ b/luna/testing.py @@ -0,0 +1,32 @@ +"""Provides a function to run tests in a module.""" + +import sys +import traceback + +def run_tests(tests_module): + """Runs the tests in the given module.""" + + success = False + + tester = None + + try: + tester = tests_module.Tester() + + # pylint: disable=broad-except + except Exception: + print('-----BEGIN LUNA ERROR-----') + traceback.print_exc() + print('-----END LUNA ERROR-----') + + else: + success = True + print('-----LUNA SUCCESS-----') + + finally: + try: + tester.stop() + except AttributeError: + pass + + sys.exit(int(not success)) diff --git a/luna/tokens.py b/luna/tokens.py new file mode 100644 index 0000000..94d20d4 --- /dev/null +++ b/luna/tokens.py @@ -0,0 +1,31 @@ +import tiktoken + +def num_tokens_from_messages(messages, model="gpt-3.5-turbo-0301"): + """Returns the number of tokens used by a list of messages.""" + try: + encoding = tiktoken.encoding_for_model(model) + except KeyError: + encoding = tiktoken.get_encoding("cl100k_base") + if model == "gpt-3.5-turbo": + return num_tokens_from_messages(messages, model="gpt-3.5-turbo-0301") + elif model == "gpt-4": + return num_tokens_from_messages(messages, model="gpt-4-0314") + elif model == "gpt-3.5-turbo-0301": + tokens_per_message = 4 # every message follows <|start|>{role/name}\n{content}<|end|>\n + tokens_per_name = -1 # if there's a name, the role is omitted + elif model == "gpt-4-0314": + tokens_per_message = 3 + tokens_per_name = 1 + else: + raise NotImplementedError(f"""num_tokens_from_messages() is not implemented for model {model}. See https://github.com/openai/openai-python/blob/main/chatml.md for information on how messages are converted to tokens.""") + num_tokens = 0 + for message in messages: + num_tokens += tokens_per_message + for key, value in message.items(): + num_tokens += len(encoding.encode(value)) + if key == "name": + num_tokens += tokens_per_name + num_tokens += 3 # every reply is primed with <|start|>assistant<|message|> + return num_tokens + +print(num_tokens_from_messages(messages)) diff --git a/luna/training/clock/README.md b/luna/training/clock/README.md new file mode 100644 index 0000000..f0ee2b3 --- /dev/null +++ b/luna/training/clock/README.md @@ -0,0 +1,12 @@ +# SimpleClock +A simple web app which displays the current time. + +## Get started +To get started, follow these steps: + +1. Run the app using +```bash +python main.py +``` +2. Open the app in your browser at http://localhost:5000 +3. Enjoy! \ No newline at end of file diff --git a/luna/training/clock/_prompt.luna.txt b/luna/training/clock/_prompt.luna.txt new file mode 100644 index 0000000..c28ce3f --- /dev/null +++ b/luna/training/clock/_prompt.luna.txt @@ -0,0 +1 @@ +A simple website called SimpleClock. It should display the current time on /. \ No newline at end of file diff --git a/luna/training/clock/main.py b/luna/training/clock/main.py new file mode 100644 index 0000000..f74eab1 --- /dev/null +++ b/luna/training/clock/main.py @@ -0,0 +1,12 @@ +"""Web server start module.""" + +import sys + +from clock import create_app + +def run(port=sys.argv[1] if len(sys.argv) > 1 else 5000): + """Starts the web server.""" + create_app().run(port=port) + +if __name__ == '__main__': + run() diff --git a/luna/training/clock/simpleclock/__init__.py b/luna/training/clock/simpleclock/__init__.py new file mode 100644 index 0000000..a95c280 --- /dev/null +++ b/luna/training/clock/simpleclock/__init__.py @@ -0,0 +1,13 @@ +import flask + +def create_app(): + """Returns the Flask app.""" + + app = flask.Flask(__name__) + + @app.route('/') + def index(): + """Display the current time.""" + return flask.render_template('index.html') + + return app diff --git a/luna/training/clock/simpleclock/static/index.js b/luna/training/clock/simpleclock/static/index.js new file mode 100644 index 0000000..43edc5c --- /dev/null +++ b/luna/training/clock/simpleclock/static/index.js @@ -0,0 +1,9 @@ +// use momentjs to display the time, nicely formatted. +// updates every second + +function update() { + time.innerHTML = moment().format('h:mm:ss a'); + } + +update(); +setInterval(update, 1000); diff --git a/luna/training/clock/simpleclock/static/style.css b/luna/training/clock/simpleclock/static/style.css new file mode 100644 index 0000000..0407081 --- /dev/null +++ b/luna/training/clock/simpleclock/static/style.css @@ -0,0 +1,27 @@ +body { + font-size: 1.2rem; + scroll-behavior: smooth; +} + +* { + box-sizing: border-box; + font-family: sans-serif; +} + +main { + max-width: 800px; + margin: 0 auto; + padding: 1rem; +} + +h1 { + font-size: 2rem; +} + +p { + font-size: 1.2rem; +} + +#time { + font-weight: bold; +} diff --git a/luna/training/clock/simpleclock/templates/base.html b/luna/training/clock/simpleclock/templates/base.html new file mode 100644 index 0000000..7a55c95 --- /dev/null +++ b/luna/training/clock/simpleclock/templates/base.html @@ -0,0 +1,15 @@ + + + +Welcome to this website!
+The current time is: .
+{% endblock %} \ No newline at end of file diff --git a/luna/training/clock/tests.py b/luna/training/clock/tests.py new file mode 100644 index 0000000..b68f78e --- /dev/null +++ b/luna/training/clock/tests.py @@ -0,0 +1,26 @@ +"""LUNA Tests.""" + +import time +import multiprocessing +import requests + +class Tester: + """Tests if the program is working as expected.""" + + def __init__(self): + """Runs the tests.""" + # pylint: disable=import-outside-toplevel + import main + + self.server = multiprocessing.Process(target=main.run, args=(5000,)) + self.server.start() + + time.sleep(1) + print('Server started!') + + resp = requests.get('http://localhost:5000/', timeout=5) + assert 'The current time is:' in resp.text + + def stop(self): + """Runs after the tests are done, whether they have been successful or not.""" + self.server.terminate() diff --git a/luna/training/getip/README.md b/luna/training/getip/README.md new file mode 100644 index 0000000..db89df2 --- /dev/null +++ b/luna/training/getip/README.md @@ -0,0 +1,14 @@ +# `GetIP` +A very simple CLI app that displays the user's IP address. + +## Get started +It's very simple to run this app. Just run the following command in the terminal: + +```bash +python cli.py +``` + +Example output: +``` +123.123.34.34 +``` diff --git a/luna/training/getip/_prompt.luna.txt b/luna/training/getip/_prompt.luna.txt new file mode 100644 index 0000000..0f39659 --- /dev/null +++ b/luna/training/getip/_prompt.luna.txt @@ -0,0 +1 @@ +A very simple CLI which returns the user's IP address. \ No newline at end of file diff --git a/luna/training/getip/cli.py b/luna/training/getip/cli.py new file mode 100644 index 0000000..135cc36 --- /dev/null +++ b/luna/training/getip/cli.py @@ -0,0 +1,14 @@ +"""Get the IP address of the client.""" + +import typer +import requests + +app = typer.Typer() + +@app.command() +def main(): + """Shows the IP address of the client.""" + print(requests.get('https://checkip.amazonaws.com', timeout=5).text.strip()) + +if __name__ == '__main__': + typer.run(main) diff --git a/luna/training/getip/tests.py b/luna/training/getip/tests.py new file mode 100644 index 0000000..fa7682e --- /dev/null +++ b/luna/training/getip/tests.py @@ -0,0 +1,22 @@ +"""LUNA Tests.""" + +import os +import requests +import subprocess + +class Tester: + """Tests if the program is working as expected.""" + + def __init__(self): + """Runs the tests.""" + + # check the print output of cli.app.main() + os.chdir(os.path.dirname(__file__)) + output = subprocess.check_output(['python', 'cli.py']).decode('utf8') + + ip_addr = requests.get('https://checkip.amazonaws.com', timeout=5).text.strip() + assert ip_addr in output + + def stop(self): + """Runs after the tests are done, whether they have been successful or not.""" + # no need to stop anything