Merge branch 'main' of https://github.com/jina-ai/gptdeploy into feat-gpt-turbo

# Conflicts:
#	src/executor_factory.py
#	src/gpt.py
#	src/prompt_system.py
This commit is contained in:
Joschka Braun
2023-04-14 11:38:51 +02:00
17 changed files with 220 additions and 190 deletions

View File

@@ -1,2 +1,2 @@
__version__ = '0.18.11'
__version__ = '0.18.15'
from src.cli import main

View File

@@ -7,7 +7,7 @@ from src.constants import FILE_AND_TAG_PAIRS
from src.jina_cloud import push_executor, process_error_message
from src.prompt_tasks import general_guidelines, executor_file_task, chain_of_thought_creation, test_executor_file_task, \
chain_of_thought_optimization, requirements_file_task, docker_file_task, not_allowed
from src.utils.io import recreate_folder, persist_file
from src.utils.io import persist_file
from src.utils.string_tools import print_colored
@@ -72,10 +72,11 @@ class ExecutorFactory:
output_path,
executor_name,
package,
num_approach,
is_chain_of_thought=False,
):
EXECUTOR_FOLDER_v1 = self.get_executor_path(output_path, package, 1)
recreate_folder(EXECUTOR_FOLDER_v1)
EXECUTOR_FOLDER_v1 = self.get_executor_path(output_path, executor_name, package, num_approach, 1)
os.makedirs(EXECUTOR_FOLDER_v1)
print_colored('', '############# Executor #############', 'red')
user_query = (
@@ -175,42 +176,32 @@ Please provide the complete file with the exact same syntax to wrap the code.
playground_content = self.extract_content_from_result(playground_content_raw, 'app.py', match_single_block=True)
persist_file(playground_content, os.path.join(executor_path, 'app.py'))
def get_executor_path(self, output_path, package, version):
def get_executor_path(self, output_path, executor_name, package, num_approach, version):
package_path = '_'.join(package)
return os.path.join(output_path, package_path, f'v{version}')
return os.path.join(output_path, executor_name, f'{num_approach}_{package_path}', f'v{version}')
def debug_executor(self, output_path, package, description, test):
def debug_executor(self, output_path, executor_name, num_approach, packages, description, test):
MAX_DEBUGGING_ITERATIONS = 10
error_before = ''
# conversation = self.gpt_session.get_conversation()
for i in range(1, MAX_DEBUGGING_ITERATIONS):
print('Debugging iteration', i)
previous_executor_path = self.get_executor_path(output_path, package, i)
next_executor_path = self.get_executor_path(output_path, package, i + 1)
print('Trying to build the microservice. Might take a while...')
previous_executor_path = self.get_executor_path(output_path, executor_name, packages, num_approach, i)
next_executor_path = self.get_executor_path(output_path, executor_name, packages, num_approach, i + 1)
log_hubble = push_executor(previous_executor_path)
error = process_error_message(log_hubble)
if error:
recreate_folder(next_executor_path)
os.makedirs(next_executor_path)
file_name_to_content = self.get_all_executor_files_with_content(previous_executor_path)
is_dependency_issue = self.is_dependency_issue(error, file_name_to_content['Dockerfile'])
print(f'Current error is a dependency issue: {is_dependency_issue}')
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'I have the following files:\n{all_files_string}\n\n'
# + f'This error happens during the docker build process:\n{error}\n\n'
# + 'First, think about what kind of error is this? Look at exactly at the stack trace and 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"```"
# )
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 "
@@ -221,7 +212,6 @@ Please provide the complete file with the exact same syntax to wrap the code.
f"You are given the following files:\n\n{all_files_string}"
)
else:
# if i == 1:
all_files_string = self.files_to_string(file_name_to_content)
user_query = (
f"General rules: " + not_allowed()
@@ -229,8 +219,9 @@ Please provide the complete file with the exact same syntax to wrap the code.
+ 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'\n\nThis error happens during the docker build process:\n{error}\n\n'
+ 'First, think about what kind of error is this? Look at exactly at the stack trace and then '
"suggest how to solve it. Output the files that need change. "
+ 'Look at exactly at the stack trace. 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"
@@ -238,9 +229,6 @@ Please provide the complete file with the exact same syntax to wrap the code.
f"...code...\n"
f"```"
)
# else:
# conversation.set_system_definition()
# user_query = f'Now this error happens during the docker build process:\n{error}'
conversation = self.gpt_session.get_conversation()
returned_files_raw = conversation.query(user_query)
@@ -251,13 +239,13 @@ Please provide the complete file with the exact same syntax to wrap the code.
for file_name, content in file_name_to_content.items():
persist_file(content, os.path.join(next_executor_path, file_name))
error_before = error_before + '\n' + error
error_before = error_before
else:
break
if i == MAX_DEBUGGING_ITERATIONS - 1:
raise self.MaxDebugTimeReachedException('Could not debug the executor.')
return self.get_executor_path(output_path, package, i)
return self.get_executor_path(output_path, executor_name, packages, num_approach, i)
class MaxDebugTimeReachedException(BaseException):
pass
@@ -329,22 +317,22 @@ package2,package3,...
generated_name = self.generate_executor_name(description)
executor_name = f'{generated_name}{random.randint(0, 1000_000)}'
packages_list = self.get_possible_packages(description, num_approaches)
recreate_folder(output_path)
for packages in packages_list:
for num_approach, packages in enumerate(packages_list):
try:
self.create_executor(description, test, output_path, executor_name, packages)
executor_path = self.debug_executor(output_path, packages, description, test)
self.create_executor(description, test, output_path, executor_name, packages, num_approach)
executor_path = self.debug_executor(output_path, executor_name, num_approach, packages, description, test)
host = jina_cloud.deploy_flow(executor_name, executor_path)
self.create_playground(executor_name, executor_path, host)
except self.MaxDebugTimeReachedException:
print('Could not debug the executor.')
print('Could not debug the Executor.')
continue
print(f'''
Executor name: {executor_name}
Executor path: {executor_path}
Host: {host}
Playground: streamlit run {os.path.join(executor_path, "app.py")}
Run the following command to start the playground:
streamlit run {os.path.join(executor_path, "app.py")}
'''
)
break

View File

@@ -19,7 +19,7 @@ class GPTSession:
self.supported_model = 'gpt-4'
self.pricing_prompt = PRICING_GPT4_PROMPT
self.pricing_generation = PRICING_GPT4_GENERATION
elif (model == 'gpt-4' and not self.is_gpt4_available()) or model == 'gpt-3.5-turbo':
else:
if model == 'gpt-4':
print_colored('GPT-4 is not available. Using GPT-3.5-turbo instead.', 'yellow')
model = 'gpt-3.5-turbo'
@@ -31,7 +31,11 @@ class GPTSession:
def get_openai_api_key(self):
if 'OPENAI_API_KEY' not in os.environ:
raise Exception('You need to set OPENAI_API_KEY in your environment')
raise Exception('''
You need to set OPENAI_API_KEY in your environment.
If you have updated it already, please restart your terminal.
'''
)
openai.api_key = os.environ['OPENAI_API_KEY']
def is_gpt4_available(self):
@@ -42,9 +46,10 @@ class GPTSession:
model="gpt-4",
messages=[{
"role": 'system',
"content": 'test'
"content": 'you respond nothing'
}]
)
break
except RateLimitError:
sleep(1)
continue
@@ -61,7 +66,7 @@ class GPTSession:
print('Estimated costs on openai.com:')
# print('money prompt:', f'${money_prompt}')
# print('money generation:', f'${money_generation}')
print('total money so far:', f'${money_prompt + money_generation}')
print('total money spent so far:', f'${money_prompt + money_generation}')
print('\n')
def get_conversation(self, system_definition_examples: List[str] = ['executor', 'docarray', 'client']):
@@ -100,7 +105,7 @@ class _GPTConversation:
delta = chunk['choices'][0]['delta']
if 'content' in delta:
content = delta['content']
print_colored('' if complete_string else 'assistent', content, 'green', end='')
print_colored('' if complete_string else 'assistant', content, 'green', end='')
complete_string += content
return complete_string

View File

@@ -10,6 +10,9 @@ import hubble
from hubble.executor.helper import upload_file, archive_package, get_request_header
from jcloud.flow import CloudFlow
from src.utils.io import suppress_stdout
from src.utils.string_tools import print_colored
def redirect_callback(href):
print(
@@ -23,6 +26,12 @@ def jina_auth_login():
try:
hubble.Client(jsonify=True).get_user_info(log_error=False)
except hubble.AuthenticationRequiredError:
print('You need login to Jina first to use GPTDeploy')
print_colored('', '''
If you just created an account, it can happen that the login callback is not working.
In this case, please cancel this run, rerun your gptdeploy command and login into your account again.
''', 'green'
)
hubble.login(prompt='login', redirect_callback=redirect_callback)
@@ -41,7 +50,9 @@ def push_executor(dir_path):
'verbose': 'True',
'md5sum': md5_digest,
}
req_header = get_request_header()
with suppress_stdout():
req_header = get_request_header()
resp = upload_file(
'https://api.hubble.jina.ai/v2/rpc/executor.push',
'filename',

View File

@@ -80,8 +80,12 @@ def set_env_variable(shell, key):
with open(config_file, "a") as file:
file.write(f"\n{export_line}\n")
click.echo(
f"✅ Success, OPENAI_API_KEY has been set in {config_file}\nPlease restart your shell to apply the changes.")
click.echo(f'''
✅ Success, OPENAI_API_KEY has been set in {config_file}.
Please restart your shell to apply the changes or run:
source {config_file}
'''
)
except FileNotFoundError:
click.echo(f"Error: {config_file} not found. Please set the environment variable manually.")
@@ -93,7 +97,12 @@ def set_api_key(key):
if system_platform == "windows":
set_env_variable_command = f'setx OPENAI_API_KEY "{key}"'
subprocess.call(set_env_variable_command, shell=True)
click.echo("✅ Success, OPENAI_API_KEY has been set.\nPlease restart your Command Prompt to apply the changes.")
click.echo('''
✅ Success, OPENAI_API_KEY has been set.
Please restart your Command Prompt to apply the changes.
'''
)
elif system_platform in ["linux", "darwin"]:
if "OPENAI_API_KEY" in os.environ or is_key_set_in_config_file(key):
if not click.confirm("OPENAI_API_KEY is already set. Do you want to overwrite it?"):

View File

@@ -7,83 +7,52 @@ Here is an example of how an executor can be defined. It always starts with a co
```python
# this executor binary files as input and returns the length of each binary file as output
from jina import Executor, requests, DocumentArray, Document
import json
class MyInfoExecutor(Executor):
def __init__(self, **kwargs):
super().__init__()
@requests() # each executor must have exactly this decorator without parameters
@requests() # each Executor must have exactly this decorator without parameters
def foo(self, docs: DocumentArray, **kwargs) => DocumentArray:
for d in docs:
d.load_uri_to_blob()
d.blob = None
content = json.loads(d.text)
...
d.text = json.dumps(modified_content)
return docs
```
An executor gets a DocumentArray as input and returns a DocumentArray as output.'''
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.
A Document is a python class that represents a single document.
Here is the protobuf definition of a Document:
message DocumentProto {{
// A hexdigest that represents a unique document ID
string id = 1;
oneof content {{
// the raw binary content of this document, which often represents the original document when comes into jina
bytes blob = 2;
// the ndarray of the image/audio/video document
NdArrayProto tensor = 3;
// a text document
string text = 4;
}}
// a uri of the document is a remote url starts with http or https or data URI scheme
string uri = 5;
// list of the sub-documents of this document (recursive structure)
repeated DocumentProto chunks = 6;
// the matched documents on the same level (recursive structure)
repeated DocumentProto matches = 7;
// the embedding of this document
NdArrayProto embedding = 8;
// used to store json data the executor gets and returns
string text = 1;
}}
Here is an example of how a DocumentArray can be defined:
Here are examples of how a DocumentArray can be defined:
from jina import DocumentArray, Document
import json
d1 = Document(text='hello')
d1 = Document(text=json.dumps({{'he_says': 'hello'}}))
# you can load binary data into a document
url = 'https://...'
response = requests.get(url)
obj_data = response.content
d2 = Document(blob=obj_data) # blob is bytes like b'\\x89PNG\\r\\n\\x1a\\n\
base64_data = base64.b64encode(png_data).decode('utf-8')
d2 = Document(text=json.dumps({{'image': base64_data}}))
d3 = Document(tensor=numpy.array([1, 2, 3]), chunks=[Document(uri=/local/path/to/file)]
d4 = Document(
uri='https://docs.docarray.org/img/logo.png',
)
d5 = Document()
d5.tensor = np.ones((2,4))
d5.uri = 'https://audio.com/audio.mp3'
d6 = Document()
d6.blob # like b'RIFF\\x00\\x00\\x00\\x00WAVEfmt \\x10\\x00...'
docs = DocumentArray([
d1, d2, d3, d4
])
d7 = Document()
d7.text = 'test string'
d8 = Document()
d8.text = json.dumps([{{"id": "1", "text": ["hello", 'test']}}, {{"id": "2", "text": "world"}}])
# the document has a helper function load_uri_to_blob:
# For instance, d4.load_uri_to_blob() downloads the file from d4.uri and stores it in d4.blob.
# If d4.uri was something like 'https://website.web/img.jpg', then d4.blob would be something like b'\\xff\\xd8\\xff\\xe0\\x00\\x10JFIF\\x00\\x01\\x01... '''
array = numpy.array([1, 2, 3])
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.

View File

@@ -9,7 +9,7 @@ def general_guidelines():
"Every file starts with comments describing what the code is doing before the first import. "
"Comments can only be written within code blocks. "
"Then all imports are listed. "
"It is important to import all modules that could be needed in the executor code. "
"It is important to import all modules that could be needed in the Executor code. "
"Always import: "
"from jina import Executor, DocumentArray, Document, requests "
"Start from top-level and then fully implement all methods. "
@@ -143,5 +143,5 @@ The executor must not access external apis except unless it is explicitly mentio
The executor must not load data from the local file system unless it was created by the executor itself.
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 Document.tags.
The executor must not use any attribute of Document accept Document.text.
'''

View File

@@ -3,14 +3,12 @@ import shutil
import concurrent.futures
import concurrent.futures
from typing import Generator
import sys
from contextlib import contextmanager
def recreate_folder(folder_path):
if os.path.exists(folder_path) and os.path.isdir(folder_path):
shutil.rmtree(folder_path)
os.makedirs(folder_path)
def persist_file(file_content, file_name):
with open(f'{file_name}', 'w') as f:
def persist_file(file_content, file_path):
with open(file_path, 'w') as f:
f.write(file_content)
@@ -34,4 +32,14 @@ def timeout_generator_wrapper(generator, timeout):
except concurrent.futures.TimeoutError:
raise GenerationTimeoutError(f"Generation took longer than {timeout} seconds")
return wrapper()
return wrapper()
@contextmanager
def suppress_stdout():
original_stdout = sys.stdout
sys.stdout = open(os.devnull, 'w')
try:
yield
finally:
sys.stdout.close()
sys.stdout = original_stdout