diff --git a/constants.py b/constants.py index b7ccc11b4..100a1a448 100644 --- a/constants.py +++ b/constants.py @@ -1,4 +1,4 @@ EXTENSION_TO_SKIP = [".png",".jpg",".jpeg",".gif",".bmp",".svg",".ico",".tif",".tiff"] DEFAULT_DIR = "generated" -DEFAULT_MODEL = "gpt-3.5-turbo" # we recommend 'gpt-4' if you have it # gpt3.5 is going to be worse at generating code so we strongly recommend gpt4. i know most people dont have access, we are working on a hosted version +DEFAULT_MODEL = "gpt-3.5-turbo" # we recommend 'gpt-4' or 'gpt-4-32k' if you have it # gpt3.5 is going to be worse at generating code so we strongly recommend gpt4. i know most people dont have access, we are working on a hosted version DEFAULT_MAX_TOKENS = 2000 # i wonder how to tweak this properly. we dont want it to be max length as it encourages verbosity of code. but too short and code also truncates suddenly. \ No newline at end of file diff --git a/examples/exampleChromeExtension/background.js b/examples/anthropicExtensionV0.1/background.js similarity index 100% rename from examples/exampleChromeExtension/background.js rename to examples/anthropicExtensionV0.1/background.js diff --git a/examples/exampleChromeExtension/content_script.js b/examples/anthropicExtensionV0.1/content_script.js similarity index 100% rename from examples/exampleChromeExtension/content_script.js rename to examples/anthropicExtensionV0.1/content_script.js diff --git a/examples/exampleChromeExtension/icon128.png b/examples/anthropicExtensionV0.1/icon128.png similarity index 100% rename from examples/exampleChromeExtension/icon128.png rename to examples/anthropicExtensionV0.1/icon128.png diff --git a/examples/exampleChromeExtension/icon16.png b/examples/anthropicExtensionV0.1/icon16.png similarity index 100% rename from examples/exampleChromeExtension/icon16.png rename to examples/anthropicExtensionV0.1/icon16.png diff --git a/examples/exampleChromeExtension/icon48.png b/examples/anthropicExtensionV0.1/icon48.png similarity index 100% rename from examples/exampleChromeExtension/icon48.png rename to examples/anthropicExtensionV0.1/icon48.png diff --git a/examples/exampleChromeExtension/manifest.json b/examples/anthropicExtensionV0.1/manifest.json similarity index 100% rename from examples/exampleChromeExtension/manifest.json rename to examples/anthropicExtensionV0.1/manifest.json diff --git a/examples/exampleChromeExtension/popup.html b/examples/anthropicExtensionV0.1/popup.html similarity index 100% rename from examples/exampleChromeExtension/popup.html rename to examples/anthropicExtensionV0.1/popup.html diff --git a/examples/exampleChromeExtension/popup.js b/examples/anthropicExtensionV0.1/popup.js similarity index 100% rename from examples/exampleChromeExtension/popup.js rename to examples/anthropicExtensionV0.1/popup.js diff --git a/examples/exampleChromeExtension/prompt used for this extension.md b/examples/anthropicExtensionV0.1/prompt used for this extension.md similarity index 96% rename from examples/exampleChromeExtension/prompt used for this extension.md rename to examples/anthropicExtensionV0.1/prompt used for this extension.md index 736a06fea..f378c894a 100644 --- a/examples/exampleChromeExtension/prompt used for this extension.md +++ b/examples/anthropicExtensionV0.1/prompt used for this extension.md @@ -9,7 +9,7 @@ a Chrome Manifest V3 extension that reads the current page, and offers a popup U - pops up a small window with a simple, modern, slick, minimalistic styled html popup - in the popup script - retrieves the page content data using a `getPageContent` action (and the background listens for the `getPageContent` action and retrieves that data) - - check extension storage for an `apiKey`, and if it isn't stored, asks for an API key to Anthropic Claude and stores it. + - check extension storage for an `apiKey`, and if it isn't stored, ask the user for an API key to Anthropic Claude and stores it. - calls the Anthropic model endpoint https://api.anthropic.com/v1/complete with the `claude-instant-v1-100k` model with: - append the page title - append the page content @@ -60,7 +60,7 @@ Important Details: - in the string prompt sent to Anthropic, first include the page title and page content, and finally append the prompt, clearly vertically separated by spacing. -- if the Anthropic api call is a 401, handle that by clearing the stored anthropic api key and asking for it again. +- if the Anthropic api call is a 401, handle that in popup.js by clearing the stored anthropic api key and asking for it again. - add styles to make sure the popup's styling follows the basic rules of web design, for example having margins around the body, and a system font stack. diff --git a/examples/exampleChromeExtension/shared_dependencies.md b/examples/anthropicExtensionV0.1/shared_dependencies.md similarity index 100% rename from examples/exampleChromeExtension/shared_dependencies.md rename to examples/anthropicExtensionV0.1/shared_dependencies.md diff --git a/examples/exampleChromeExtension/styles.css b/examples/anthropicExtensionV0.1/styles.css similarity index 100% rename from examples/exampleChromeExtension/styles.css rename to examples/anthropicExtensionV0.1/styles.css diff --git a/generated/.gitkeep b/generated/.gitkeep deleted file mode 100644 index 4eb9b6ffa..000000000 --- a/generated/.gitkeep +++ /dev/null @@ -1,3 +0,0 @@ -# generated folder - -by default, `main.py` will generate the app in this folder (you can customize with the `--directory=newFolderHere` flag). \ No newline at end of file diff --git a/karpathy.md b/karpathy.md new file mode 100644 index 000000000..a60d2b35f --- /dev/null +++ b/karpathy.md @@ -0,0 +1,77 @@ +# Anki Flash Card Generator + +https://twitter.com/karpathy/status/1663262981302681603 + +a Chrome extension that, when clicked, opens a small window with a page where you can enter a prompt for reading the currently open page and generating anki flash cards from the page content using the openai gpt-4 chatcompletion api + +## details of the chrome extension + +- follows Chrome Manifest v3 +- it is a chrome extension so only clientside js using chrome extension api's allowed +- has a default icon named `icon.png` in the root folder +- min height 600, width 400, center all content + +- when the user opens the popup: + - injects a content script that reads all content and sends it over to the popup + - ask me for my OpenAI api key, and when the user presses submit, use the user's API key to read the page content and create some anki cards in this format (from https://chat.openai.com/share/a54de047-8796-47b4-937d-5b7dc70bc16e): + +{QUESTION} +A: {CANDIDATE ANSWER 1} +B: {CANDIDATE ANSWER 2} +C: {CANDIDATE ANSWER 3} +D: {CANDIDATE ANSWER 4} +Answer: {LETTER} + +For example: + +What is the most populous state of the United States? +A: Florida +B: Texas +C: California +D: New York +Answer: C + +You'll notice that the Multiple Choice options are designed to be somewhat hard, with distractor answers that are plausible (e.g. Texas, Florida and New York are quite populous but not the most populous). + +## chatcompletion api example + + + +example request + +curl https://api.openai.com/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -d '{ + "model": "gpt-3.5-turbo", + "messages": [{"role": "user", "content": "Hello!"}] + }' + +example params + +{ + "model": "gpt-3.5-turbo", + "messages": [{"role": "user", "content": "Hello!"}] +} + + +example response + +{ + "id": "chatcmpl-123", + "object": "chat.completion", + "created": 1677652288, + "choices": [{ + "index": 0, + "message": { + "role": "assistant", + "content": "\n\nHello there, how may I assist you today?", + }, + "finish_reason": "stop" + }], + "usage": { + "prompt_tokens": 9, + "completion_tokens": 12, + "total_tokens": 21 + } +} diff --git a/main.py b/main.py index 9d7ecd5db..ddf520a57 100644 --- a/main.py +++ b/main.py @@ -1,33 +1,14 @@ +import sys import os -import modal import ast -from utils import clean_dir +from time import sleep +from utils import clean_dir, reportTokens, write_file from constants import DEFAULT_DIR, DEFAULT_MODEL, DEFAULT_MAX_TOKENS +from systemPrompts.developer import planPrompt1, planPrompt2, filePrompt + -stub = modal.Stub("smol-developer-v1") # yes we are recommending using Modal by default, as it helps with deployment. see readme for why. -openai_image = modal.Image.debian_slim().pip_install("openai", "tiktoken") - -@stub.function( - image=openai_image, - secret=modal.Secret.from_dotenv(), - retries=modal.Retries( - max_retries=5, - backoff_coefficient=2.0, - initial_delay=1.0, - ), - concurrency_limit=5, # many users report rate limit issues (https://github.com/smol-ai/developer/issues/10) so we try to do this but it is still inexact. would like ideas on how to improve - timeout=120, -) def generate_response(model, system_prompt, user_prompt, *args): - # IMPORTANT: Keep import statements here due to Modal container restrictions https://modal.com/docs/guide/custom-container#additional-python-packages import openai - import tiktoken - - def reportTokens(prompt): - encoding = tiktoken.encoding_for_model(model) - # print number of tokens in light gray, with first 50 characters of prompt in green. if truncated, show that it is truncated - print("\033[37m" + str(len(encoding.encode(prompt))) + " tokens\033[0m" + " in prompt: " + "\033[92m" + prompt[:50] + "\033[0m" + ("..." if len(prompt) > 50 else "")) - # Set up your OpenAI API credentials openai.api_key = os.environ["OPENAI_API_KEY"] @@ -37,7 +18,7 @@ def reportTokens(prompt): reportTokens(system_prompt) messages.append({"role": "user", "content": user_prompt}) reportTokens(user_prompt) - # Loop through each value in `args` and add it to messages alternating role between "assistant" and "user" + # loop thru each arg and add it to messages alternating role between "assistant" and "user" role = "assistant" for value in args: messages.append({"role": role, "content": value}) @@ -52,57 +33,41 @@ def reportTokens(prompt): } # Send the API request - response = openai.ChatCompletion.create(**params) + keep_trying = True + numTries = 0 + while keep_trying and numTries < 5: + try: + numTries += 1 + response = openai.ChatCompletion.create(**params) + keep_trying = False + except Exception as e: + # e.g. when the API is too busy, we don't want to fail everything + print("Failed to generate response. Error: ", e) + sleep(numTries) # linear backoff + print("Retrying...") # Get the reply from the API response reply = response.choices[0]["message"]["content"] return reply -@stub.function() -def generate_file(filename, model=DEFAULT_MODEL, filepaths_string=None, shared_dependencies=None, prompt=None): - # call openai api with this prompt - filecode = generate_response.call(model, - f"""You are an AI developer who is trying to write a program that will generate code for the user based on their intent. - - the app is: {prompt} - - the files we have decided to generate are: {filepaths_string} +def generate_file( + filename, + model=DEFAULT_MODEL, + filepaths_string=None, + shared_dependencies=None, + prompt=None, +): + systemPrompt, userPrompt = filePrompt(prompt, filepaths_string, shared_dependencies, filename) - the shared dependencies (like filenames and variable names) we have decided on are: {shared_dependencies} - - only write valid code for the given filepath and file type, and return only the code. - do not add any other explanation, only return valid code for that file type. - """, - f""" - We have broken up the program into per-file generation. - Now your job is to generate only the code for the file {filename}. - Make sure to have consistent filenames if you reference other files we are also generating. - - Remember that you must obey 3 things: - - you are generating code for the file {filename} - - do not stray from the names of the files and the shared dependencies we have decided on - - MOST IMPORTANT OF ALL - the purpose of our app is {prompt} - every line of code you generate must be valid code. Do not include code fences in your response, for example - - Bad response: - ```javascript - console.log("hello world") - ``` - - Good response: - console.log("hello world") - - Begin generating the code now. - - """, - ) + # call openai api with this prompt + filecode = generate_response(model, systemPrompt, userPrompt) return filename, filecode -@stub.local_entrypoint() def main(prompt, directory=DEFAULT_DIR, model=DEFAULT_MODEL, file=None): - # read file from prompt if it ends in a .md filetype + # read prompt from file if it ends in a .md filetype if prompt.endswith(".md"): with open(prompt, "r") as promptfile: prompt = promptfile.read() @@ -111,15 +76,14 @@ def main(prompt, directory=DEFAULT_DIR, model=DEFAULT_MODEL, file=None): # print the prompt in green color print("\033[92m" + prompt + "\033[0m") + # example prompt: + # a Chrome extension that, when clicked, opens a small window with a page where you can enter + # a prompt for reading the currently open page and generating some response from openai + # call openai api with this prompt - filepaths_string = generate_response.call(model, - """You are an AI developer who is trying to write a program that will generate code for the user based on their intent. - - When given their intent, create a complete, exhaustive list of filepaths that the user would write to make the program. - - only list the filepaths you would write, and return them as a python list of strings. - do not add any other explanation, only return a python list of strings. - """, + filepaths_string = generate_response( + model, + planPrompt1(), prompt, ) print(filepaths_string) @@ -137,61 +101,63 @@ def main(prompt, directory=DEFAULT_DIR, model=DEFAULT_MODEL, file=None): if file is not None: # check file print("file", file) - filename, filecode = generate_file(file, model=model, filepaths_string=filepaths_string, shared_dependencies=shared_dependencies, prompt=prompt) + filename, filecode = generate_file( + file, + model=model, + filepaths_string=filepaths_string, + shared_dependencies=shared_dependencies, + prompt=prompt, + ) write_file(filename, filecode, directory) else: clean_dir(directory) # understand shared dependencies - shared_dependencies = generate_response.call(model, - """You are an AI developer who is trying to write a program that will generate code for the user based on their intent. - - In response to the user's prompt: - - --- - the app is: {prompt} - --- - - the files we have decided to generate are: {filepaths_string} - - Now that we have a list of files, we need to understand what dependencies they share. - Please name and briefly describe what is shared between the files we are generating, including exported variables, data schemas, id names of every DOM elements that javascript functions will use, message names, and function names. - Exclusively focus on the names of the shared dependencies, and do not add any other explanation. - """, - prompt, + shared_dependencies = generate_response( + model, planPrompt2(prompt, filepaths_string), prompt ) print(shared_dependencies) # write shared dependencies as a md file inside the generated directory write_file("shared_dependencies.md", shared_dependencies, directory) - - # Iterate over generated files and write them to the specified directory - for filename, filecode in generate_file.map( - list_actual, order_outputs=False, kwargs=dict(model=model, filepaths_string=filepaths_string, shared_dependencies=shared_dependencies, prompt=prompt) - ): - write_file(filename, filecode, directory) + for name in list_actual: + filename, filecode = generate_file( + name, + model=model, + filepaths_string=filepaths_string, + shared_dependencies=shared_dependencies, + prompt=prompt, + ) + write_file(filename, filecode, directory) except ValueError: - print("Failed to parse result") + print("Failed to parse result: " + result) -def write_file(filename, filecode, directory): - # Output the filename in blue color - print("\033[94m" + filename + "\033[0m") - print(filecode) - - file_path = os.path.join(directory, filename) - dir = os.path.dirname(file_path) - - # Check if the filename is actually a directory - if os.path.isdir(file_path): - print(f"Error: {filename} is a directory, not a file.") - return - - os.makedirs(dir, exist_ok=True) - - # Open the file in write mode - with open(file_path, "w") as file: - # Write content to the file - file.write(filecode) +if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser() + parser.add_argument('-d', '--directory', default=DEFAULT_DIR, help='Directory to write generated files to.') + parser.add_argument('-f', '--file', help='If you only want to regenerate a single file, specify it here.') + parser.add_argument('-m', '--model', default=DEFAULT_MODEL, help='Specify your desired model here (we recommend using `gpt-4`)') + parser.add_argument('-p', '--prompt', help='Write your full prompt as a string, or give a path to a .md file with your prompt') + args = parser.parse_args() + + # Check for arguments + if len(sys.argv) < 2: + # Looks like we don't have a prompt. Check if prompt.md exists + if not os.path.exists("prompt.md"): + # Still no? Then we can't continue + print("Please provide a prompt") + sys.exit(1) + + # Still here? Assign the prompt file name to prompt + args.prompt = "prompt.md" + + else: + # Set prompt to the first argument + prompt = sys.argv[1] + + # Run the main function + main(args.prompt, directory = args.directory, model = args.model, file=args.file) diff --git a/main_no_modal.py b/main_no_modal.py deleted file mode 100644 index 133281372..000000000 --- a/main_no_modal.py +++ /dev/null @@ -1,230 +0,0 @@ -import sys -import os -import ast -from time import sleep -from utils import clean_dir -from constants import DEFAULT_DIR, DEFAULT_MODEL, DEFAULT_MAX_TOKENS - -def generate_response(system_prompt, user_prompt, *args): - import openai - import tiktoken - - def reportTokens(prompt): - encoding = tiktoken.encoding_for_model(DEFAULT_MODEL) - # print number of tokens in light gray, with first 10 characters of prompt in green - print( - "\033[37m" - + str(len(encoding.encode(prompt))) - + " tokens\033[0m" - + " in prompt: " - + "\033[92m" - + prompt[:50] - + "\033[0m" - ) - - # Set up your OpenAI API credentials - openai.api_key = os.environ["OPENAI_API_KEY"] - - messages = [] - messages.append({"role": "system", "content": system_prompt}) - reportTokens(system_prompt) - messages.append({"role": "user", "content": user_prompt}) - reportTokens(user_prompt) - # loop thru each arg and add it to messages alternating role between "assistant" and "user" - role = "assistant" - for value in args: - messages.append({"role": role, "content": value}) - reportTokens(value) - role = "user" if role == "assistant" else "assistant" - - params = { - "model": DEFAULT_MODEL, - "messages": messages, - "max_tokens": DEFAULT_MAX_TOKENS, - "temperature": 0, - } - - # Send the API request - keep_trying = True - while keep_trying: - try: - response = openai.ChatCompletion.create(**params) - keep_trying = False - except Exception as e: - # e.g. when the API is too busy, we don't want to fail everything - print("Failed to generate response. Error: ", e) - sleep(30) - print("Retrying...") - - # Get the reply from the API response - reply = response.choices[0]["message"]["content"] - return reply - - -def generate_file( - filename, filepaths_string=None, shared_dependencies=None, prompt=None -): - # call openai api with this prompt - filecode = generate_response( - f"""You are an AI developer who is trying to write a program that will generate code for the user based on their intent. - - the app is: {prompt} - - the files we have decided to generate are: {filepaths_string} - - the shared dependencies (like filenames and variable names) we have decided on are: {shared_dependencies} - - only write valid code for the given filepath and file type, and return only the code. - do not add any other explanation, only return valid code for that file type. - """, - f""" - We have broken up the program into per-file generation. - Now your job is to generate only the code for the file {filename}. - Make sure to have consistent filenames if you reference other files we are also generating. - - Remember that you must obey 3 things: - - you are generating code for the file {filename} - - do not stray from the names of the files and the shared dependencies we have decided on - - MOST IMPORTANT OF ALL - the purpose of our app is {prompt} - every line of code you generate must be valid code. Do not include code fences in your response, for example - - Bad response: - ```javascript - console.log("hello world") - ``` - - Good response: - console.log("hello world") - - Begin generating the code now. - - """, - ) - - return filename, filecode - - -def main(prompt, directory=DEFAULT_DIR, file=None): - # read file from prompt if it ends in a .md filetype - if prompt.endswith(".md"): - with open(prompt, "r") as promptfile: - prompt = promptfile.read() - - print("hi its me, šŸ£the smol developeršŸ£! you said you wanted:") - # print the prompt in green color - print("\033[92m" + prompt + "\033[0m") - - # example prompt: - # a Chrome extension that, when clicked, opens a small window with a page where you can enter - # a prompt for reading the currently open page and generating some response from openai - - # call openai api with this prompt - filepaths_string = generate_response( - """You are an AI developer who is trying to write a program that will generate code for the user based on their intent. - - When given their intent, create a complete, exhaustive list of filepaths that the user would write to make the program. - - only list the filepaths you would write, and return them as a python list of strings. - do not add any other explanation, only return a python list of strings. - """, - prompt, - ) - print(filepaths_string) - # parse the result into a python list - list_actual = [] - try: - list_actual = ast.literal_eval(filepaths_string) - - # if shared_dependencies.md is there, read it in, else set it to None - shared_dependencies = None - if os.path.exists("shared_dependencies.md"): - with open("shared_dependencies.md", "r") as shared_dependencies_file: - shared_dependencies = shared_dependencies_file.read() - - if file is not None: - # check file - print("file", file) - filename, filecode = generate_file( - file, - filepaths_string=filepaths_string, - shared_dependencies=shared_dependencies, - prompt=prompt, - ) - write_file(filename, filecode, directory) - else: - clean_dir(directory) - - # understand shared dependencies - shared_dependencies = generate_response( - """You are an AI developer who is trying to write a program that will generate code for the user based on their intent. - - In response to the user's prompt: - - --- - the app is: {prompt} - --- - - the files we have decided to generate are: {filepaths_string} - - Now that we have a list of files, we need to understand what dependencies they share. - Please name and briefly describe what is shared between the files we are generating, including exported variables, data schemas, id names of every DOM elements that javascript functions will use, message names, and function names. - Exclusively focus on the names of the shared dependencies, and do not add any other explanation. - """, - prompt, - ) - print(shared_dependencies) - # write shared dependencies as a md file inside the generated directory - write_file("shared_dependencies.md", shared_dependencies, directory) - - for name in list_actual: - filename, filecode = generate_file( - name, - filepaths_string=filepaths_string, - shared_dependencies=shared_dependencies, - prompt=prompt, - ) - write_file(filename, filecode, directory) - - except ValueError: - print("Failed to parse result: " + result) - - -def write_file(filename, filecode, directory): - # Output the filename in blue color - print("\033[94m" + filename + "\033[0m") - print(filecode) - - file_path = directory + "/" + filename - dir = os.path.dirname(file_path) - os.makedirs(dir, exist_ok=True) - - # Open the file in write mode - with open(file_path, "w") as file: - # Write content to the file - file.write(filecode) - - -if __name__ == "__main__": - - # Check for arguments - if len(sys.argv) < 2: - - # Looks like we don't have a prompt. Check if prompt.md exists - if not os.path.exists("prompt.md"): - - # Still no? Then we can't continue - print("Please provide a prompt") - sys.exit(1) - - # Still here? Assign the prompt file name to prompt - prompt = "prompt.md" - - else: - # Set prompt to the first argument - prompt = sys.argv[1] - - # Pull everything else as normal - directory = sys.argv[2] if len(sys.argv) > 2 else generatedDir - file = sys.argv[3] if len(sys.argv) > 3 else None - - # Run the main function - main(prompt, directory, file) diff --git a/modal.py b/modal.py new file mode 100644 index 000000000..2fe10a83b --- /dev/null +++ b/modal.py @@ -0,0 +1,104 @@ +import os +import modal +import ast +from utils import clean_dir, reportTokens, write_file +from constants import DEFAULT_DIR, DEFAULT_MODEL, DEFAULT_MAX_TOKENS +from main import generate_response, main + +stub = modal.Stub("smol-developer-v1") # yes we are recommending using Modal by default, as it helps with deployment. see readme for why. +openai_image = modal.Image.debian_slim().pip_install("openai", "tiktoken") + +@stub.function( + image=openai_image, + secret=modal.Secret.from_dotenv(), + retries=modal.Retries( + max_retries=5, + backoff_coefficient=2.0, + initial_delay=1.0, + ), + concurrency_limit=5, # many users report rate limit issues (https://github.com/smol-ai/developer/issues/10) so we try to do this but it is still inexact. would like ideas on how to improve + timeout=120, +) +def generate_response_modal(model, system_prompt, user_prompt, *args): + return generate_response(model, system_prompt, user_prompt, *args) + +@stub.function() +def generate_file(filename, model=DEFAULT_MODEL, filepaths_string=None, shared_dependencies=None, prompt=None): + # call openai api with this prompt + filecode = generate_response_modal.call(model, systemPrompt, userPrompt) + return filename, filecode + + +@stub.local_entrypoint() +def main_modal(prompt, directory=DEFAULT_DIR, model=DEFAULT_MODEL, file=None): + # read file from prompt if it ends in a .md filetype + if prompt.endswith(".md"): + with open(prompt, "r") as promptfile: + prompt = promptfile.read() + + print("hi its me, šŸ£the smol developeršŸ£! you said you wanted:") + # print the prompt in green color + print("\033[92m" + prompt + "\033[0m") + + # call openai api with this prompt + filepaths_string = generate_response.call(model, + """You are an AI developer who is trying to write a program that will generate code for the user based on their intent. + + When given their intent, create a complete, exhaustive list of filepaths that the user would write to make the program. + + only list the filepaths you would write, and return them as a python list of strings. + do not add any other explanation, only return a python list of strings. + """, + prompt, + ) + print(filepaths_string) + # parse the result into a python list + list_actual = [] + try: + list_actual = ast.literal_eval(filepaths_string) + + # if shared_dependencies.md is there, read it in, else set it to None + shared_dependencies = None + if os.path.exists("shared_dependencies.md"): + with open("shared_dependencies.md", "r") as shared_dependencies_file: + shared_dependencies = shared_dependencies_file.read() + + if file is not None: + # check file + print("file", file) + filename, filecode = generate_file(file, model=model, filepaths_string=filepaths_string, shared_dependencies=shared_dependencies, prompt=prompt) + write_file(filename, filecode, directory) + else: + clean_dir(directory) + + # understand shared dependencies + shared_dependencies = generate_response.call(model, + """You are an AI developer who is trying to write a program that will generate code for the user based on their intent. + + In response to the user's prompt: + + --- + the app is: {prompt} + --- + + the files we have decided to generate are: {filepaths_string} + + Now that we have a list of files, we need to understand what dependencies they share. + Please name and briefly describe what is shared between the files we are generating, including exported variables, data schemas, id names of every DOM elements that javascript functions will use, message names, and function names. + Exclusively focus on the names of the shared dependencies, and do not add any other explanation. + """, + prompt, + ) + print(shared_dependencies) + # write shared dependencies as a md file inside the generated directory + write_file("shared_dependencies.md", shared_dependencies, directory) + + # Iterate over generated files and write them to the specified directory + for filename, filecode in generate_file.map( + list_actual, order_outputs=False, kwargs=dict(model=model, filepaths_string=filepaths_string, shared_dependencies=shared_dependencies, prompt=prompt) + ): + write_file(filename, filecode, directory) + + + except ValueError: + print("Failed to parse result") diff --git a/prompt.md b/prompt.md index ec0f1e451..400d15dd4 100644 --- a/prompt.md +++ b/prompt.md @@ -72,6 +72,8 @@ Important Details: - It has to run in a browser environment, so no Nodejs APIs allowed. +- It follows Chrome Manifest v3 and has a default icon named `icon.png` in the root folder + - the return signature of the anthropic api is curl https://api.anthropic.com/v1/complete\ -H "x-api-key: $API_KEY"\ -H 'content-type: application/json'\ diff --git a/readme.md b/readme.md index 03ba942f8..6ec233482 100644 --- a/readme.md +++ b/readme.md @@ -110,22 +110,11 @@ The feedback loop is very slow right now (`time` says about 2-4 mins to generate ## install -it's basically: - -- `git clone https://github.com/smol-ai/developer`. -- copy over `.example.env` to `.env` filling in your API keys. - -There are no python dependencies to wrangle thanks to using Modal as a [self-provisioning runtime](https://www.google.com/search?q=self+provisioning+runtime). - -Unfortunately this project also uses 3 other things: - -- Modal.com - [sign up](https://modal.com/signup), then `pip install modal-client && modal token new` - - You can run this project w/o Modal following these instructions: - - `pip install -r requirements.txt` - - `export OPENAI_API_KEY=sk-xxxxxx` (your openai api key here) - - `python main_no_modal.py YOUR_PROMPT_HERE` -- GPT-4 api (private beta) - this project now defaults to using `gpt-3.5-turbo` but it obviously wont be as good. we are working on a hosted version so you can try this out on our keys. -- (for the demo project only) anthropic claude 100k context api (private beta) - not important unless you're exactly trying to repro my demo +```bash +git clone https://github.com/smol-ai/developer +pip install -r requirements.txt +export OPENAI_API_KEY=sk-xxxxxx # your openai api key here) +``` you'll have to adapt this code on a fork if you want to use it on other infra. please open issues/PRs and i'll happily highlight your fork here. @@ -147,15 +136,14 @@ this entire extension was generated by the prompt in `prompt.md` (except for the basic usage (by default it runs with `gpt-3.5-turbo`, but we strongly encourage running with `gpt-4` if you have access) ```bash -# inline prompt -modal run main.py --prompt "a Chrome extension that, when clicked, opens a small window with a page where you can enter a prompt for reading the currently open page and generating some response from openai" --model=gpt-4 +python main.py --prompt "a Chrome extension that, when clicked, opens a small window with a page where you can enter a prompt for reading the currently open page and generating some response from openai" --model=gpt-4 ``` after a while of adding to your prompt, you can extract your prompt to a file, as long as your "prompt" ends in a .md extension we'll go look for that file ```bash # prompt in markdown file -modal run main.py --prompt prompt.md --model=gpt-4 +python main.py --prompt prompt.md --model=gpt-4 ``` each time you run this, the generated directory is deleted (except for images) and all files are rewritten from scratch. @@ -170,17 +158,29 @@ if you make a tweak to the prompt and only want it to affect one file, and keep modal run main.py --prompt prompt.md --file popup.js ``` -### smol dev without modal.com +### deploying/running in the cloud with Modal + + +it's basically: + +- copy over `.example.env` to `.env` filling in your API key. + +We primarily test production deployment with Modal. With Modal we don't experience python dependency hell thanks to using Modal as a [self-provisioning runtime](https://www.google.com/search?q=self+provisioning+runtime). + + +Unfortunately this project also uses 3 other things: + +- Modal.com - [sign up](https://modal.com/signup), then `pip install modal-client && modal token new` +- GPT-4 api (private beta) - this project now defaults to using `gpt-3.5-turbo` but it obviously wont be as good. we are working on a hosted version so you can try this out on our keys. +- (for the demo project only) anthropic claude 100k context api (private beta) - not important unless you're exactly trying to repro my demo By default, `main.py` uses Modal, beacuse it provides a nice upgrade path to a hosted experience (coming soon, so you can try it out without needing GPT4 key access). However if you want to just run it on your own machine, you can run smol dev w/o Modal following these instructions: ```bash -pip install -r requirements.txt -export OPENAI_API_KEY=sk-xxxxxx # your openai api key here) - -python main_no_modal.py YOUR_PROMPT_HERE +# inline prompt +modal run main.py --prompt "a Chrome extension that, when clicked, opens a small window with a page where you can enter a prompt for reading the currently open page and generating some response from openai" --model=gpt-4 ``` If no command line argument is given, **and** the file `prompt.md` exists, the main function will automatically use the `prompt.md` file. All other command line arguments are left as default. *this is handy for those using the "run" function on a `venv` setup in PyCharm for Windows, where no opportunity is given to enter command line arguments. Thanks [@danmenzies](https://github.com/smol-ai/developer/pull/55)* diff --git a/static/readme.md b/static/readme.md index 75b80cebc..64af13701 100644 --- a/static/readme.md +++ b/static/readme.md @@ -1 +1 @@ -by default, files in here will be copied into the generated folder, useful for adding non-code files like images and large files like csv's. \ No newline at end of file +files to be copied into the generated folder, useful for adding non-code files like images and large files like csv's. \ No newline at end of file diff --git a/systemPrompts/developer.py b/systemPrompts/developer.py new file mode 100644 index 000000000..a64f9ac9f --- /dev/null +++ b/systemPrompts/developer.py @@ -0,0 +1,116 @@ +import re +import inspect + +def intercept(fstring): + frame = inspect.currentframe().f_back + code = frame.f_code + filename = code.co_filename + line_number = code.co_firstlineno + locals_dict = frame.f_locals + + # Extract expressions within the f-string + regex = r'\{([^{}]+)\}' + expressions = re.findall(regex, fstring) + + # Process the expressions + components = [] + for expr in expressions: + # Unescape braces + expr = expr.replace('{{', '{').replace('}}', '}') + + # Evaluate the expression + result = eval(expr, globals(), locals_dict) + components.append(result) + + # Log or perform any desired actions with the components + print("Intercepted components:", components) + print("File:", filename) + print("Line number:", line_number) + + # Pass through the f-string as-is + return fstring + + +def call(fstring): + print("Original f-string:", fstring) + + + + +def planPrompt1(): + return """You are an AI developer who is trying to write a program that will generate code for the user based on their intent. + + When given their intent, create a complete, exhaustive list of filepaths that the user would write to make the program. + + only list the filepaths you would write, and return them as a python list of strings. + do not add any other explanation, only return a python list of strings. + + Good response: + ["app.py","function.py","folder/file.py"] + + Bad response: + - app.py + - function.py + - folder/file.py + + Good response: + ["app.py","function.py","folder/file.py"] + + Bad response: + - `app.py` a description here + - `function.py` another description here + - `folder/file.py` more description here + """ + +def planPrompt2(sourcePrompt, filepaths_string): + + return """You are an AI developer who is trying to write a program that will generate code for the user based on their intent. + + In response to the user's prompt: + + --- + the app is: {prompt} + --- + + the files we have decided to generate are: {filepaths_string} + + Now that we have a list of files, we need to understand what dependencies they share. + Please name and briefly describe what is shared between the files we are generating, including exported variables, data schemas, id names of every DOM elements that javascript functions will use, message names, and function names. + Exclusively focus on the names of the shared dependencies, and do not add any other explanation. + """ + + +def filePrompt(sourcePrompt, filepaths_string, shared_dependencies, filename): + systemPrompt = f"""You are an AI developer who is trying to write a program that will generate code for the user based on their intent. + + the app is: {sourcePrompt} + + the files we have decided to generate are: {filepaths_string} + + the shared dependencies (like filenames and variable names) we have decided on are: {shared_dependencies} + + only write valid code for the given filepath and file type, and return only the code. + do not add any other explanation, only return valid code for that file type. + """ + userPrompt = f""" + We have broken up the program into per-file generation. + Now your job is to generate only the code for the file {filename}. + Make sure to have consistent filenames if you reference other files we are also generating. + + Remember that you must obey 3 things: + - you are generating code for the file {filename} + - do not stray from the names of the files and the shared dependencies we have decided on + - MOST IMPORTANT OF ALL - every line of code you generate must be valid code. Do not include code fences in your response, for example + + Bad response: + ```javascript + console.log("hello world") + ``` + + Good response: + console.log("hello world") + + Begin generating the code now. + + """ + return systemPrompt, userPrompt \ No newline at end of file diff --git a/utils.py b/utils.py index 376cbe063..ae5cb594d 100644 --- a/utils.py +++ b/utils.py @@ -1,5 +1,5 @@ import os -from constants import EXTENSION_TO_SKIP +from constants import EXTENSION_TO_SKIP, DEFAULT_MODEL def clean_dir(directory): # Check if the directory exists @@ -11,4 +11,45 @@ def clean_dir(directory): if extension not in EXTENSION_TO_SKIP: os.remove(os.path.join(dirpath, filename)) else: - os.makedirs(directory, exist_ok=True) \ No newline at end of file + os.makedirs(directory, exist_ok=True) + + +def reportTokens(prompt, model=DEFAULT_MODEL): + import tiktoken # keep import statements here to fit Modal container restrictions https://modal.com/docs/guide/custom-container#additional-python-packages + encoding = tiktoken.encoding_for_model(model) + # print number of tokens in light gray, with first 10 characters of prompt in green + + print('----------------') + print(prompt) + print('----------------') + print( + "\033[37m" + + str(len(encoding.encode(prompt))) + + " tokens\033[0m" + + " in prompt: " + + "\033[92m" + + prompt[:100] + + "\033[0m" + + ("..." if len(prompt) > 100 else "") + ) + + +def write_file(filename, filecode, directory): + # Output the filename in blue color + print("\033[94m" + filename + "\033[0m") + print(filecode) + + file_path = os.path.join(directory, filename) + dir = os.path.dirname(file_path) + + # Check if the filename is actually a directory + if os.path.isdir(file_path): + print(f"Error: {filename} is a directory, not a file.") + return + + os.makedirs(dir, exist_ok=True) + + # Open the file in write mode + with open(file_path, "w") as file: + # Write content to the file + file.write(filecode) \ No newline at end of file