Merge pull request #5 from jina-ai/feat-gpt-turbo

feat: support gpt turbo
This commit is contained in:
Florian Hönicke
2023-04-15 02:43:22 +02:00
committed by GitHub
6 changed files with 166 additions and 104 deletions

View File

@@ -7,20 +7,23 @@ from openai.error import RateLimitError, Timeout
from src.constants import PRICING_GPT4_PROMPT, PRICING_GPT4_GENERATION, PRICING_GPT3_5_TURBO_PROMPT, \
PRICING_GPT3_5_TURBO_GENERATION
from src.options.generate.prompt_system import system_base_definition
from src.options.generate.prompt_system import system_base_definition, executor_example, docarray_example, client_example
from src.utils.io import timeout_generator_wrapper, GenerationTimeoutError
from src.utils.string_tools import print_colored
class GPTSession:
def __init__(self):
def __init__(self, model: str = 'gpt-4'):
self.configure_openai_api_key()
if self.is_gpt4_available():
if model == 'gpt-4' and self.is_gpt4_available():
self.supported_model = 'gpt-4'
self.pricing_prompt = PRICING_GPT4_PROMPT
self.pricing_generation = PRICING_GPT4_GENERATION
else:
raise Exception('The OPENAI_API_KEY does not have access to GPT-4. We are working on 3.5-turbo - support')
self.supported_model = 'gpt-3.5-turbo'
if model == 'gpt-4':
print_colored('GPT-4 is not available. Using GPT-3.5-turbo instead.', 'yellow')
model = 'gpt-3.5-turbo'
self.supported_model = model
self.pricing_prompt = PRICING_GPT3_5_TURBO_PROMPT
self.pricing_generation = PRICING_GPT3_5_TURBO_GENERATION
self.chars_prompt_so_far = 0
@@ -58,26 +61,25 @@ If you have updated it already, please restart your terminal.
self.chars_prompt_so_far += chars_prompt
self.chars_generation_so_far += chars_generation
print('\n')
money_prompt = round(self.chars_prompt_so_far / 3.4 * self.pricing_prompt / 1000, 2)
money_generation = round(self.chars_generation_so_far / 3.4 * self.pricing_generation / 1000, 2)
money_prompt = round(self.chars_prompt_so_far / 3.4 * self.pricing_prompt / 1000, 3)
money_generation = round(self.chars_generation_so_far / 3.4 * self.pricing_generation / 1000, 3)
print('Estimated costs on openai.com:')
# print('money prompt:', f'${money_prompt}')
# print('money generation:', f'${money_generation}')
print('total money spent so far:', f'${money_prompt + money_generation}')
print('\n')
def get_conversation(self):
return _GPTConversation(self.supported_model, self.cost_callback)
def get_conversation(self, system_definition_examples: List[str] = ['executor', 'docarray', 'client']):
return _GPTConversation(self.supported_model, self.cost_callback, system_definition_examples)
class _GPTConversation:
def __init__(self, model: str, cost_callback, prompt_list: List[Tuple[str, str]] = None):
def __init__(self, model: str, cost_callback, system_definition_examples: List[str] = ['executor', 'docarray', 'client']):
self.model = model
if prompt_list is None:
prompt_list = [('system', system_base_definition)]
self.prompt_list = prompt_list
self.cost_callback = cost_callback
print_colored('system', system_base_definition, 'magenta')
self.prompt_list = [None]
self.set_system_definition(system_definition_examples)
print_colored('system', self.prompt_list[0][1], 'magenta')
def query(self, prompt: str):
print_colored('user', prompt, 'blue')
@@ -86,6 +88,16 @@ class _GPTConversation:
self.prompt_list.append(('assistant', response))
return response
def set_system_definition(self, system_definition_examples: List[str] = []):
system_message = system_base_definition
if 'executor' in system_definition_examples:
system_message += f'\n{executor_example}'
if 'docarray' in system_definition_examples:
system_message += f'\n{docarray_example}'
if 'client' in system_definition_examples:
system_message += f'\n{client_example}'
self.prompt_list[0] = ('system', system_message)
def get_response_from_stream(self, response_generator):
response_generator_with_timeout = timeout_generator_wrapper(response_generator, 10)
complete_string = ''
@@ -102,7 +114,7 @@ class _GPTConversation:
try:
response_generator = openai.ChatCompletion.create(
temperature=0,
max_tokens=2_000,
max_tokens=2_000 if self.model == 'gpt-4' else None,
model=self.model,
stream=True,
messages=[

View File

@@ -250,7 +250,7 @@ def process_error_message(error_message):
if last_matching_line_index is not None:
relevant_lines = lines[last_matching_line_index:]
return '\n'.join(relevant_lines[-25:])
return '\n'.join(relevant_lines[-25:]).strip()
def build_docker(path):

View File

@@ -45,10 +45,12 @@ def main(ctx):
@click.option('--description', required=True, help='Description of the microservice.')
@click.option('--test', required=True, help='Test scenario for the microservice.')
@path_param
@click.option('--model', default='gpt-4', help='GPT model to use (default: gpt-4).')
def generate(
description,
test,
path,
model='gpt-4'
):
from src.options.generate.generator import Generator
path = os.path.expanduser(path)
@@ -57,7 +59,7 @@ def generate(
if os.listdir(path):
click.echo(f"Error: The path {path} you provided via --path is not empty. Please choose a directory that does not exist or is empty.")
return
generator = Generator()
generator = Generator(model=model)
generator.generate(description, test, path)
@main.command()

View File

@@ -6,22 +6,28 @@ from src.apis import gpt
from src.constants import FILE_AND_TAG_PAIRS, NUM_IMPLEMENTATION_STRATEGIES, MAX_DEBUGGING_ITERATIONS
from src.apis.jina_cloud import process_error_message, push_executor
from src.options.generate.prompt_tasks import general_guidelines, chain_of_thought_creation, executor_file_task, \
not_allowed, chain_of_thought_optimization, test_executor_file_task, requirements_file_task, docker_file_task
not_allowed_executor, chain_of_thought_optimization, test_executor_file_task, requirements_file_task, docker_file_task
from src.utils.io import persist_file, get_all_microservice_files_with_content, get_microservice_path
from src.utils.string_tools import print_colored
class Generator:
def __init__(self):
self.gpt_session = gpt.GPTSession()
def __init__(self, model='gpt-4'):
self.gpt_session = gpt.GPTSession(model=model)
def extract_content_from_result(self, plain_text, file_name):
def extract_content_from_result(self, plain_text, file_name, match_single_block=False):
pattern = fr"^\*\*{file_name}\*\*\n```(?:\w+\n)?([\s\S]*?)```"
match = re.search(pattern, plain_text, re.MULTILINE)
if match:
return match.group(1).strip()
else:
return ''
# Check for a single code block
single_code_block_pattern = r"^```(?:\w+\n)?([\s\S]*?)```"
single_code_block_match = re.findall(single_code_block_pattern, plain_text, re.MULTILINE)
if match_single_block and len(single_code_block_match) == 1:
return single_code_block_match[0].strip()
else:
return ''
def write_config_yml(self, microservice_name, dest_folder):
config_content = f'''
@@ -41,7 +47,7 @@ class Generator:
all_microservice_files_string += f'**{file_name}**\n'
all_microservice_files_string += f'```{tag}\n'
all_microservice_files_string += file_name_to_content[file_name]
all_microservice_files_string += '\n```\n\n'
all_microservice_files_string += '\n```'
return all_microservice_files_string
def wrap_content_in_code_block(self, microservice_content, file_name, tag):
@@ -64,15 +70,19 @@ class Generator:
user_query = (
general_guidelines()
+ executor_file_task(microservice_name, description, test, package)
+ chain_of_thought_creation()
+ '\n\n' + chain_of_thought_creation()
)
conversation = self.gpt_session.get_conversation()
microservice_content_raw = conversation.query(user_query)
if is_chain_of_thought:
microservice_content_raw = conversation.query(
f"General rules: " + not_allowed() + chain_of_thought_optimization('python', 'microservice.py'))
microservice_content = self.extract_content_from_result(microservice_content_raw, 'microservice.py')
f"General rules: " + not_allowed_executor() + chain_of_thought_optimization('python', 'microservice.py'))
microservice_content = self.extract_content_from_result(microservice_content_raw, 'microservice.py', match_single_block=True)
if microservice_content == '':
microservice_content_raw = conversation.query('You must add the executor code.')
microservice_content = self.extract_content_from_result(
microservice_content_raw, 'microservice.py', match_single_block=True
)
persist_file(microservice_content, os.path.join(MICROSERVICE_FOLDER_v1, 'microservice.py'))
print_colored('', '############# Test Microservice #############', 'red')
@@ -85,12 +95,14 @@ class Generator:
test_microservice_content_raw = conversation.query(user_query)
if is_chain_of_thought:
test_microservice_content_raw = conversation.query(
f"General rules: " + not_allowed() +
f"General rules: " + not_allowed_executor() +
chain_of_thought_optimization('python', 'test_microservice.py')
+ "Don't add any additional tests. "
)
test_microservice_content = self.extract_content_from_result(test_microservice_content_raw, 'test_microservice.py')
persist_file(test_microservice_content, os.path.join(MICROSERVICE_FOLDER_v1, 'test_microservice.py'))
microservice_content = self.extract_content_from_result(
microservice_content_raw, 'microservice.py', match_single_block=True
)
persist_file(microservice_content, os.path.join(MICROSERVICE_FOLDER_v1, 'test_microservice.py'))
print_colored('', '############# Requirements #############', 'red')
requirements_path = os.path.join(MICROSERVICE_FOLDER_v1, 'requirements.txt')
@@ -106,7 +118,7 @@ class Generator:
requirements_content_raw = conversation.query(
chain_of_thought_optimization('', requirements_path) + "Keep the same version of jina ")
requirements_content = self.extract_content_from_result(requirements_content_raw, 'requirements.txt')
requirements_content = self.extract_content_from_result(requirements_content_raw, 'requirements.txt', match_single_block=True)
persist_file(requirements_content, requirements_path)
print_colored('', '############# Dockerfile #############', 'red')
@@ -121,8 +133,8 @@ class Generator:
dockerfile_content_raw = conversation.query(user_query)
if is_chain_of_thought:
dockerfile_content_raw = conversation.query(
f"General rules: " + not_allowed() + chain_of_thought_optimization('dockerfile', 'Dockerfile'))
dockerfile_content = self.extract_content_from_result(dockerfile_content_raw, 'Dockerfile')
f"General rules: " + not_allowed_executor() + chain_of_thought_optimization('dockerfile', 'Dockerfile'))
dockerfile_content = self.extract_content_from_result(dockerfile_content_raw, 'Dockerfile', match_single_block=True)
persist_file(dockerfile_content, os.path.join(MICROSERVICE_FOLDER_v1, 'Dockerfile'))
self.write_config_yml(microservice_name, MICROSERVICE_FOLDER_v1)
@@ -141,24 +153,24 @@ class Generator:
Create a playground for the executor {microservice_name} using streamlit.
The playground must look like it was made by a professional designer.
All the ui elements are well thought out to make them visually appealing and easy to use.
This is an example how you can connect to the executor assuming the document (d) is already defined:
```
from jina import Client, Document, DocumentArray
client = Client(host=host)
response = client.post('/', inputs=DocumentArray([d])) # always use '/'
print(response[0].text)
print(response[0].text) # can also be blob in case of image/audio..., this should be visualized in the streamlit app
```
Note that the response will always be in response[0].text
You must provide the complete file with the exact same syntax to wrap the code.
The playground (app.py) must read the host from sys.argv because it will be started with a custom host: streamlit run app.py -- --host grpc://...
The playground (app.py) must not let the user configure the host on the ui.
'''
)
conversation = self.gpt_session.get_conversation()
conversation = self.gpt_session.get_conversation([])
conversation.query(user_query)
playground_content_raw = conversation.query(
f"General rules: " + not_allowed() + chain_of_thought_optimization('python', 'app.py'))
playground_content = self.extract_content_from_result(playground_content_raw, 'app.py')
persist_file(playground_content, os.path.join(microservice_path, 'app.py'))
playground_content_raw = conversation.query(chain_of_thought_optimization('python', 'app.py', 'the playground'))
playground_content = self.extract_content_from_result(playground_content_raw, 'app.py', match_single_block=True)
persist_file(playground_content, os.path.join(miicroservice_path, 'app.py'))
def debug_microservice(self, path, microservice_name, num_approach, packages, description, test):
@@ -172,36 +184,48 @@ The playground (app.py) must not let the user configure the host on the ui.
error = process_error_message(log_hubble)
if error:
os.makedirs(next_microservice_path)
file_name_to_content = get_all_microservice_files_with_content(previous_microservice_path)
all_files_string = self.files_to_string(file_name_to_content)
user_query = (
f"General rules: " + not_allowed()
+ 'Here is the description of the task the executor must solve:\n'
+ description
+ '\n\nHere is the test scenario the executor must pass:\n'
+ test
+ 'Here are all the files I use:\n'
+ all_files_string
+ (('This is an error that is already fixed before:\n'
+ error_before) if error_before else '')
+ '\n\nNow, I get the following error:\n'
+ error + '\n'
+ 'Think quickly about possible reasons the error might caused by. '
'Decide which files need to be changed. '
'Then output the files that need change. '
"Don't output files that don't need change. "
"If you output a file, then write the complete file. "
"Use the exact same syntax to wrap the code:\n"
f"**...**\n"
f"```...\n"
f"...code...\n"
f"```\n\n"
)
file_name_to_content = self.get_all_microservice_files_with_content(previous_executor_path)
is_dependency_issue = self.is_dependency_issue(error, file_name_to_content['Dockerfile'])
if is_dependency_issue:
all_files_string = self.files_to_string({
key: val for key, val in file_name_to_content.items() if key in ['requirements.txt', 'Dockerfile']
})
user_query = (
f"Your task is to provide guidance on how to solve an error that occurred during the Docker "
f"build process. The error message is:\n{error}\nTo solve this error, you should first "
f"identify the type of error by examining the stack trace. Once you have identified the "
f"error, you should suggest how to solve it. Your response should include the files that "
f"need to be changed, but not files that don't need to be changed. For files that need to "
f"be changed, you must provide the complete file with the exact same syntax to wrap the code.\n\n"
f"You are given the following files:\n\n{all_files_string}"
)
else:
all_files_string = self.files_to_string(file_name_to_content)
user_query = (
f"General rules: " + not_allowed_executor()
+ f'Here is the description of the task the executor must solve:\n{description}'
+ f'\n\nHere is the test scenario the executor must pass:\n{test}'
+ f'Here are all the files I use:\n{all_files_string}'
+ ((f'This is an error that I already fixed before:\n{error_before}\n\n') if error_before else '')
+ f'\n\nThis is the error I encounter currently during the docker build process:\n{error}\n\n'
+ 'Look at the stack trace of the current error. First, think about what kind of error is this? '
'Then think about possible reasons which might have caused it. Then suggest how to '
'solve it. Output the files that need change. '
"Don't output files that don't need change. If you output a file, then write the "
"complete file. Use the exact same syntax to wrap the code:\n"
f"**...**\n"
f"```...\n"
f"...code...\n"
f"```"
)
conversation = self.gpt_session.get_conversation()
returned_files_raw = conversation.query(user_query)
for file_name, tag in FILE_AND_TAG_PAIRS:
updated_file = self.extract_content_from_result(returned_files_raw, file_name)
if updated_file:
if updated_file and (not is_dependency_issue or file_name in ['requirements.txt', 'Dockerfile']):
file_name_to_content[file_name] = updated_file
for file_name, content in file_name_to_content.items():
@@ -218,6 +242,20 @@ The playground (app.py) must not let the user configure the host on the ui.
class MaxDebugTimeReachedException(BaseException):
pass
def is_dependency_issue(self, error, docker_file: str):
# a few heuristics to quickly jump ahead
if any([error_message in error for error_message in ['AttributeError', 'NameError', 'AssertionError']]):
return False
conversation = self.gpt_session.get_conversation([])
answer = conversation.query(
f'Your task is to assist in identifying the root cause of a Docker build error for a python application. '
f'The error message is as follows::\n\n{error}\n\n'
f'The docker file is as follows:\n\n{docker_file}\n\n'
f'Is this a dependency installation failure? Answer with "yes" or "no".'
)
return 'yes' in answer.lower()
def generate_microservice_name(self, description):
conversation = self.gpt_session.get_conversation()
user_query = f'''
@@ -250,7 +288,7 @@ For each subtask:
For each package:
Write down some non-obvious thoughts about the challenges you might face for the task and give multiple approaches on how you handle them.
For example, there might be some packages you must not use because they do not obay the rules:
{not_allowed()}
{not_allowed_executor()}
Discuss the pros and cons for all of these packages.
Create a list of package subsets that you could use to solve the task.
The list is sorted in a way that the most promising subset of packages is at the top.

View File

@@ -1,7 +1,6 @@
from src.constants import FLOW_URL_PLACEHOLDER
executor_example = '''
Using the Jina framework, users can define executors.
executor_example = '''Using the Jina framework, users can define executors.
Here is an example of how an executor can be defined. It always starts with a comment:
**microservice.py**
@@ -24,18 +23,19 @@ class MyInfoExecutor(Executor):
An Executor gets a DocumentArray as input and returns a DocumentArray as output.
'''
docarray_example = f'''
A DocumentArray is a python class that can be seen as a list of Documents.
docarray_example = f'''A DocumentArray is a python class that can be seen as a list of Documents.
A Document is a python class that represents a single document.
Here is the protobuf definition of a Document:
```
message DocumentProto {{
// used to store json data the executor gets and returns
string text = 1;
}}
```
Here are examples of how a DocumentArray can be defined:
```
from jina import DocumentArray, Document
import json
@@ -53,11 +53,11 @@ array_list = array.tolist()
d3 = Document(text=json.dumps(array_list))
d4 = Document()
d4.text = '{{"uri": "https://.../logo.png"}}'
```
'''
client_example = f'''
After the executor is deployed, it can be called via Jina Client.
client_example = f'''After the executor is deployed, it can be called via Jina Client.
Here is an example of a client file:
**client.py**
@@ -68,13 +68,7 @@ d = Document(uri='...')
d.load_uri_to_blob()
response = client.post('/', inputs=DocumentArray([d])) # the client must be called on '/'
print(response[0].text)
```
'''
```'''
system_base_definition = f'''
You are a principal engineer working at Jina - an open source company."
{executor_example}
{docarray_example}
{client_example}
'''
system_base_definition = f'''You are a principal engineer working at Jina - an open source company. You accurately satisfy all of the user's requirements.'''

View File

@@ -17,24 +17,29 @@ def general_guidelines():
)
def _task(task, tag_name, file_name):
def _task(task, tag_name, file_name, purpose=None):
into_string = file_name
if purpose:
into_string += f"/{purpose}"
return (
task + f"The code will go into {file_name}. Wrap the code into:\n"
task + f"The code will go into {into_string}. Make sure to wrap the code into ``` marks even if you only "
f"output code:\n"
f"**{file_name}**\n"
f"```{tag_name}\n"
f"...code...\n"
f"```\n\n"
f"```\nYou must provide the complete file with the exact same syntax to wrap the code."
)
def executor_file_task(executor_name, executor_description, test_scenario, package):
return _task(f'''
Write the executor called '{executor_name}'.
Write the executor called '{executor_name}'. The name is very important to keep.
It matches the following description: '{executor_description}'.
It will be tested with the following scenario: '{test_scenario}'.
For the implementation use the following package: '{package}'.
Have in mind that d.uri is never a path to a local file. It is always a url.
''' + not_allowed(),
''' + not_allowed_executor(),
EXECUTOR_FILE_TAG,
EXECUTOR_FILE_NAME
)
@@ -50,8 +55,8 @@ def test_executor_file_task(executor_name, test_scenario):
if test_scenario else ""
)
+ "Use the following import to import the executor: "
f"from microservice import {executor_name} "
+ not_allowed()
f"```\nfrom microservice import {executor_name}\n```"
+ not_allowed_executor()
+ "The test must not open local files. "
+ "The test must not mock a function of the executor. "
+ "The test must not use other data than the one provided in the test scenario. ",
@@ -73,15 +78,15 @@ def requirements_file_task():
def docker_file_task():
return _task(
"Write the Dockerfile that defines the environment with all necessary dependencies that the executor uses. "
"The Dockerfile runs the test during the build process. "
"It is important to make sure that all libs are installed that are required by the python packages. "
"Usually libraries are installed with apt-get. "
"Be aware that the machine the docker container is running on does not have a GPU - only CPU. "
"Add the config.yml file to the Dockerfile. "
"Add the config.yml file to the Dockerfile. Note that the Dockerfile only has access to the files: "
"executor.py, requirements.txt, config.yml, test_executor.py. "
"The base image of the Dockerfile is FROM jinaai/jina:3.14.1-py39-standard. "
'The entrypoint is ENTRYPOINT ["jina", "executor", "--uses", "config.yml"]. '
'Make sure the all files are in the /workdir. '
"The Dockerfile runs the test during the build process. " + not_allowed(),
"The Dockerfile runs the test during the build process. " + not_allowed_docker(),
DOCKER_FILE_TAG,
DOCKER_FILE_NAME
)
@@ -104,28 +109,33 @@ def streamlit_file_task():
def chain_of_thought_creation():
return (
"First, write down some non-obvious thoughts about the challenges of the task and give multiple approaches on how you handle them. "
"For example, the given package you could used in different ways and not all of them obay the rules: "
+ "Discuss the pros and cons for all of these approaches and then decide for one of the approaches. "
"Then write as I told you. "
return (f'''
First, write down some non-obvious thoughts about the challenges of the task and give multiple approaches on how you handle them.
For example, the given package you could used in different ways and not all of them obey the instructions.
Discuss the pros and cons for all of these approaches and then decide for one of the approaches.
Then write the code.
'''
)
def chain_of_thought_optimization(tag_name, file_name):
def chain_of_thought_optimization(tag_name, file_name, file_name_function=None):
file_name_or_function = file_name
if file_name_function:
file_name_or_function += f"/{file_name_function}"
return _task(
f'First, write down an extensive list of obvious and non-obvious observations about {file_name} that could need an adjustment. Explain why. '
f'First, write down an extensive list of obvious and non-obvious observations about {file_name_or_function} that could need an adjustment. Explain why. '
f"Think if all the changes are required and finally decide for the changes you want to make, "
f"but you are not allowed disregard the instructions in the previous message. "
f"Be very hesitant to change the code. Only make a change if you are sure that it is necessary. "
f"Output only {file_name} "
f"Write the whole content of {file_name} - even if you decided to change only a small thing or even nothing. ",
f"Output only {file_name_or_function} "
f"Write the whole content of {file_name_or_function} - even if you decided to change only a small thing or even nothing. ",
tag_name,
file_name
file_name,
file_name_function
)
def not_allowed():
def not_allowed_executor():
return '''
The executor must not use the GPU.
The executor must not access a database.
@@ -135,4 +145,10 @@ The executor must not load data from the local file system unless it was created
The executor must not use a pre-trained model unless it is explicitly mentioned in the description.
The executor must not train a model.
The executor must not use any attribute of Document accept Document.text.
'''
'''
def not_allowed_docker():
return '''
Note that the Dockerfile only has access to the files: executor.py, requirements.txt, config.yml, test_executor.py.
Note that the Dockerfile runs the test_microservice.py during the build process.
'''