Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

initial refactor of prompts #63

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion constants.py
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down
3 changes: 0 additions & 3 deletions generated/.gitkeep

This file was deleted.

77 changes: 77 additions & 0 deletions karpathy.md
Original file line number Diff line number Diff line change
@@ -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

<!-- https://platform.openai.com/docs/api-reference/chat -->

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
}
}
196 changes: 81 additions & 115 deletions main.py
Original file line number Diff line number Diff line change
@@ -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"]
Expand All @@ -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})
Expand All @@ -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()
Expand All @@ -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)
Expand All @@ -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)
Loading