From 811acc44366cb483125d625fcb4529067b6d1ffc Mon Sep 17 00:00:00 2001 From: Joschka Braun Date: Tue, 2 May 2023 15:17:11 +0200 Subject: [PATCH 01/67] :sparkles: feat: avoid loops --- src/options/generate/generator.py | 28 +++++++++++---- .../static_files/microservice/Dockerfile | 2 +- src/options/generate/templates_user.py | 35 +++++++++++++++---- 3 files changed, 51 insertions(+), 14 deletions(-) diff --git a/src/options/generate/generator.py b/src/options/generate/generator.py index ad6b45b..d8c901f 100644 --- a/src/options/generate/generator.py +++ b/src/options/generate/generator.py @@ -21,12 +21,12 @@ from src.constants import FILE_AND_TAG_PAIRS, NUM_IMPLEMENTATION_STRATEGIES, MAX from src.options.generate.templates_system import system_task_iteration, system_task_introduction, system_test_iteration from src.options.generate.templates_user import template_generate_microservice_name, \ template_generate_possible_packages, \ - template_solve_code_issue, \ + template_implement_solution_code_issue, \ template_solve_pip_dependency_issue, template_is_dependency_issue, template_generate_playground, \ template_generate_function, template_generate_test, template_generate_requirements, \ template_chain_of_thought, template_summarize_error, \ template_generate_apt_get_install, template_solve_apt_get_dependency_issue, template_pm_task_iteration, \ - template_pm_test_iteration + template_pm_test_iteration, template_suggest_solutions_code_issue from src.options.generate.ui import get_random_employee from src.utils.io import persist_file, get_all_microservice_files_with_content, get_microservice_path @@ -201,7 +201,6 @@ metas: # }) # ) - with open(os.path.join(os.path.dirname(__file__), 'static_files', 'microservice', 'Dockerfile'), 'r', encoding='utf-8') as f: docker_file_template_lines = f.readlines() docker_file_template_lines = [line for line in docker_file_template_lines if not line.startswith('RUN apt-get update')] @@ -348,15 +347,32 @@ pytest all_files_string=dock_req_string, ) else: + all_files_string = self.files_to_string( + {key: val for key, val in file_name_to_content.items() if key != EXECUTOR_FILE_NAME} + ) + suggested_solutions = json.loads( + self.generate_and_persist_file( + section_title='Suggest solution for code issue', + template=template_suggest_solutions_code_issue, + destination_folder=next_microservice_path, + file_name_s=['solutions.json'], + summarized_error=summarized_error, + task_description=self.microservice_specification.task, + test_description=self.microservice_specification.test, + all_files_string=all_files_string, + )['solutions.json'] + ) + self.generate_and_persist_file( - section_title='Debugging code issue', - template=template_solve_code_issue, + section_title='Implementing suggestion solution for code issue', + template=template_implement_solution_code_issue, destination_folder=next_microservice_path, file_name_s=[IMPLEMENTATION_FILE_NAME, TEST_EXECUTOR_FILE_NAME, REQUIREMENTS_FILE_NAME], summarized_error=summarized_error, task_description=self.microservice_specification.task, test_description=self.microservice_specification.test, - all_files_string=self.files_to_string({key: val for key, val in file_name_to_content.items() if key != EXECUTOR_FILE_NAME}), + all_files_string=all_files_string, + suggested_solution=suggested_solutions["1"], ) class MaxDebugTimeReachedException(BaseException): diff --git a/src/options/generate/static_files/microservice/Dockerfile b/src/options/generate/static_files/microservice/Dockerfile index d0de26d..cbe5f8e 100644 --- a/src/options/generate/static_files/microservice/Dockerfile +++ b/src/options/generate/static_files/microservice/Dockerfile @@ -1,4 +1,4 @@ -FROM jinaai/jina:3.14.1-py39-standard +FROM jinaai/jina:3.15.1-dev14-py39-standard # update pip RUN pip install --upgrade pip diff --git a/src/options/generate/templates_user.py b/src/options/generate/templates_user.py index a505435..2d53c0f 100644 --- a/src/options/generate/templates_user.py +++ b/src/options/generate/templates_user.py @@ -272,7 +272,7 @@ Output them as a white space separated list:''' ) -template_solve_code_issue = PromptTemplate.from_template( +template_suggest_solutions_code_issue = PromptTemplate.from_template( '''General rules: ''' + not_allowed_function_string + ''' @@ -288,14 +288,35 @@ Here are all the files I use: Here is the summary of the error that occurred: {summarized_error} -To solve this error, you should: -1. Suggest 3 to 5 possible solutions on how to solve it. You have no access to the documentation of the package. -2. Decide for the best solution and explain it in detail. -3. Write down the files that need to be changed, but not files that don't need to be changed. -Note that any changes needed to make the test pass must be written under the constraint that ''' + IMPLEMENTATION_FILE_NAME + ''' will be used in a different file as well. +You should suggest 3 to 5 possible solutions on how to solve it. Obey the following rules: +You have no access to the documentation of the package. +Note that any changes needed to make the test pass must be written under the constraint that ''' + IMPLEMENTATION_FILE_NAME + ''' will be used in a different file as well. ''' + f'{not_allowed_function_string}\n{not_allowed_docker_string}\n{gpt_35_turbo_usage_string}' + ''' + +After thinking about the possible solutions, output them as JSON ranked from best to worst. Like this: +**solutions.json** +```json +{{ + "1": "", + "2": "<2nd best solution>" +}} +```''' +) + + +template_implement_solution_code_issue = PromptTemplate.from_template( + '''Here is the description of the task the function must solve: +{task_description} + +Here is the test scenario the function must pass: +{test_description} +Here are all the files I use: +{all_files_string} + +Implemented the suggested solution: {suggested_solution} + Output all the files that need change. You must not change the Dockerfile. Don't output files that don't need change. If you output a file, then write the complete file. Use the exact following syntax to wrap the code: @@ -307,7 +328,7 @@ Use the exact following syntax to wrap the code: Example: -**microservice.py** +**implementation.py** ```python print('hello world') ```''' From e21f952665c4dc209e0bb69b7aacc383af3ab571 Mon Sep 17 00:00:00 2001 From: Joschka Braun Date: Wed, 3 May 2023 10:09:19 +0200 Subject: [PATCH 02/67] :bug: fix: import --- src/options/generate/generator.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/options/generate/generator.py b/src/options/generate/generator.py index 4add6fd..16ac304 100644 --- a/src/options/generate/generator.py +++ b/src/options/generate/generator.py @@ -26,9 +26,8 @@ from src.options.generate.templates_user import template_generate_microservice_n template_generate_function, template_generate_test, template_generate_requirements, \ template_chain_of_thought, template_summarize_error, \ template_solve_apt_get_dependency_issue, template_pm_task_iteration, \ - template_pm_test_iteration - template_generate_apt_get_install, template_solve_apt_get_dependency_issue, template_pm_task_iteration, \ - template_pm_test_iteration, template_suggest_solutions_code_issue + template_pm_test_iteration, template_generate_apt_get_install, template_solve_apt_get_dependency_issue,\ + template_pm_task_iteration, template_pm_test_iteration, template_suggest_solutions_code_issue from src.options.generate.ui import get_random_employee from src.utils.io import persist_file, get_all_microservice_files_with_content, get_microservice_path From 879579926b41f9775c0dbe17ab22dd070b0aa25e Mon Sep 17 00:00:00 2001 From: Joschka Braun Date: Wed, 3 May 2023 16:17:05 +0200 Subject: [PATCH 03/67] :bug: fix: don't change docker --- src/options/generate/templates_user.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/options/generate/templates_user.py b/src/options/generate/templates_user.py index 03069e2..1871a48 100644 --- a/src/options/generate/templates_user.py +++ b/src/options/generate/templates_user.py @@ -323,6 +323,7 @@ Here is the summary of the error that occurred: You should suggest 3 to 5 possible solutions on how to solve it. Obey the following rules: You have no access to the documentation of the package. +You must not change the Dockerfile. Note that any changes needed to make the test pass must be written under the constraint that ''' + IMPLEMENTATION_FILE_NAME + ''' will be used in a different file as well. ''' + f'{not_allowed_function_string}\n{not_allowed_docker_string}\n{gpt_35_turbo_usage_string}' + ''' From 0763b9a12ec3fc8ac5021e231e07404a616c851b Mon Sep 17 00:00:00 2001 From: Joschka Braun Date: Wed, 3 May 2023 16:17:16 +0200 Subject: [PATCH 04/67] :bug: fix: summarize error --- src/options/generate/templates_user.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/options/generate/templates_user.py b/src/options/generate/templates_user.py index 1871a48..9fab116 100644 --- a/src/options/generate/templates_user.py +++ b/src/options/generate/templates_user.py @@ -209,10 +209,9 @@ The output would be: template_summarize_error = PromptTemplate.from_template( - '''Here is an error message I encountered during the docker build process: + '''Your task is to condense an error encountered during the docker build process. The error message is as follows: "{error}" -Your task is to summarize the error message as compact and informative as possible while maintaining all information necessary to debug the core issue. -Warnings are not worth mentioning.''' +Your response should be concise and informative, highlighting the core issue while omitting any warnings. It should also provide some additional context regarding the specific file and line number where the error occurred. The actual core error message should also be included.''' ) From 19d561a0f265e866479378a1064b1da79c799677 Mon Sep 17 00:00:00 2001 From: Joschka Braun Date: Wed, 3 May 2023 16:39:46 +0200 Subject: [PATCH 05/67] :bug: fix: pattern to extract --- src/options/generate/generator.py | 2 +- src/options/generate/templates_user.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/options/generate/generator.py b/src/options/generate/generator.py index 00f4a80..22fe0be 100644 --- a/src/options/generate/generator.py +++ b/src/options/generate/generator.py @@ -48,7 +48,7 @@ class Generator: def extract_content_from_result(self, plain_text, file_name, match_single_block=False, can_contain_code_block=True): optional_line_break = '\n' if can_contain_code_block else '' # the \n at the end makes sure that ``` within the generated code is not matched because it is not right before a line break - pattern = fr"\*?\*?{file_name}\*?\*?\n```(?:\w+\n)?([\s\S]*?){optional_line_break}```" + pattern = fr"^\*?\*?{file_name}\*?\*?\n```(?:\w+\n)?([\s\S]*?){optional_line_break}```" matches = re.findall(pattern, plain_text, re.MULTILINE) if matches: return matches[-1].strip() diff --git a/src/options/generate/templates_user.py b/src/options/generate/templates_user.py index c7412cc..9bb1f65 100644 --- a/src/options/generate/templates_user.py +++ b/src/options/generate/templates_user.py @@ -203,9 +203,9 @@ template_summarize_error = PromptTemplate.from_template( "{error}" Your task is to summarize the error message as compact and informative as possible \ while maintaining all information necessary to debug the core issue (100 words). +It should also provide some additional context regarding the specific file and line number where the error occurred. \ Note that you must not suggest a solution to the error. -Warnings are not worth mentioning. -Your response should be concise and informative, highlighting the core issue while omitting any warnings. It should also provide some additional context regarding the specific file and line number where the error occurred. The actual core error message should also be included.''' +Warnings are not worth mentioning.''' ) From 6d14ebef2f22ad1950e5dcd4166ff8d2585bc84d Mon Sep 17 00:00:00 2001 From: Joschka Braun Date: Thu, 4 May 2023 16:40:45 +0200 Subject: [PATCH 06/67] :recycle: refactor: use generator attributes --- src/options/generate/generator.py | 148 +++++++++++++++++------------- 1 file changed, 82 insertions(+), 66 deletions(-) diff --git a/src/options/generate/generator.py b/src/options/generate/generator.py index 22fe0be..89cdb9d 100644 --- a/src/options/generate/generator.py +++ b/src/options/generate/generator.py @@ -25,9 +25,9 @@ from src.options.generate.templates_user import template_generate_microservice_n template_solve_pip_dependency_issue, template_is_dependency_issue, template_generate_playground, \ template_generate_function, template_generate_test, template_generate_requirements, \ template_chain_of_thought, template_summarize_error, \ - template_solve_apt_get_dependency_issue, template_pm_task_iteration, \ - template_pm_test_iteration, template_generate_apt_get_install, template_solve_apt_get_dependency_issue,\ - template_pm_task_iteration, template_pm_test_iteration, template_suggest_solutions_code_issue + template_solve_apt_get_dependency_issue, \ + template_pm_task_iteration, template_pm_test_iteration, template_suggest_solutions_code_issue, \ + template_was_error_seen_before, template_was_solution_tried_before from src.options.generate.ui import get_random_employee from src.utils.io import persist_file, get_all_microservice_files_with_content, get_microservice_path @@ -45,10 +45,13 @@ class Generator: self.gpt_session = gpt.GPTSession(task_description, model=model) self.microservice_specification = TaskSpecification(task=task_description, test=None) self.microservice_root_path = path + self.microservice_name = None + self.previous_microservice_path = None + self.cur_microservice_path = None def extract_content_from_result(self, plain_text, file_name, match_single_block=False, can_contain_code_block=True): optional_line_break = '\n' if can_contain_code_block else '' # the \n at the end makes sure that ``` within the generated code is not matched because it is not right before a line break - pattern = fr"^\*?\*?{file_name}\*?\*?\n```(?:\w+\n)?([\s\S]*?){optional_line_break}```" + pattern = fr"(?:\*|\*\*| ){file_name}\*?\*?\n```(?:\w+\n)?([\s\S]*?){optional_line_break}```" matches = re.findall(pattern, plain_text, re.MULTILINE) if matches: return matches[-1].strip() @@ -92,7 +95,7 @@ metas: self, section_title: str, template: PromptTemplate, - destination_folder: str, + destination_folder: str = None, file_name_s: List[str] = None, parse_result_fn: Callable = None, system_definition_examples: List[str] = [], @@ -104,7 +107,8 @@ metas: Args: section_title (str): The title of the section to be printed in the console. template (PromptTemplate): The template to be used for generating the file(s). - destination_folder (str): The destination folder where the generated file(s) should be persisted. + destination_folder (str): The destination folder where the generated file(s) should be persisted. If None, + the current microservice path is used. Defaults to None. file_name_s (List[str], optional): The name of the file(s) to be generated. Defaults to None. parse_result_fn (Callable, optional): A function that parses the generated content and returns a dictionary mapping file_name to its content. If no content could be extract, it returns an empty dictionary. @@ -112,6 +116,9 @@ metas: system_definition_examples (List[str], optional): The system definition examples to be used for the conversation. Defaults to []. **template_kwargs: The keyword arguments to be passed to the template. """ + if destination_folder is None: + destination_folder = self.cur_microservice_path + if parse_result_fn is None: parse_result_fn = self.get_default_parse_result_fn(file_name_s) @@ -139,27 +146,26 @@ metas: def generate_microservice( self, - microservice_name, packages, num_approach, ): - MICROSERVICE_FOLDER_v1 = get_microservice_path(self.microservice_root_path, microservice_name, packages, - num_approach, 1) - os.makedirs(MICROSERVICE_FOLDER_v1) + self.cur_microservice_path = get_microservice_path( + self.microservice_root_path, self.microservice_name, packages, num_approach, 1 + ) + os.makedirs(self.cur_microservice_path) with open(os.path.join(os.path.dirname(__file__), 'static_files', 'microservice', 'jina_wrapper.py'), 'r', encoding='utf-8') as f: microservice_executor_boilerplate = f.read() microservice_executor_code = microservice_executor_boilerplate.replace('class GPTDeployExecutor(Executor):', - f'class {microservice_name}(Executor):') - persist_file(microservice_executor_code, os.path.join(MICROSERVICE_FOLDER_v1, EXECUTOR_FILE_NAME)) + f'class {self.microservice_name}(Executor):') + persist_file(microservice_executor_code, os.path.join(self.cur_microservice_path, EXECUTOR_FILE_NAME)) with open(os.path.join(os.path.dirname(__file__), 'static_files', 'microservice', 'apis.py'), 'r', encoding='utf-8') as f: - persist_file(f.read(), os.path.join(MICROSERVICE_FOLDER_v1, 'apis.py')) + persist_file(f.read(), os.path.join(self.cur_microservice_path, 'apis.py')) microservice_content = self.generate_and_persist_file( section_title='Microservice', template=template_generate_function, - destination_folder=MICROSERVICE_FOLDER_v1, microservice_description=self.microservice_specification.task, test_description=self.microservice_specification.test, packages=packages, @@ -171,9 +177,8 @@ metas: test_microservice_content = self.generate_and_persist_file( 'Test Microservice', template_generate_test, - MICROSERVICE_FOLDER_v1, code_files_wrapped=self.files_to_string({EXECUTOR_FILE_NAME: microservice_content}), - microservice_name=microservice_name, + microservice_name=self.microservice_name, microservice_description=self.microservice_specification.task, test_description=self.microservice_specification.test, file_name_purpose=TEST_EXECUTOR_FILE_NAME, @@ -181,10 +186,9 @@ metas: file_name_s=[TEST_EXECUTOR_FILE_NAME], )[TEST_EXECUTOR_FILE_NAME] - requirements_content = self.generate_and_persist_file( + self.generate_and_persist_file( 'Requirements', template_generate_requirements, - MICROSERVICE_FOLDER_v1, code_files_wrapped=self.files_to_string({ IMPLEMENTATION_FILE_NAME: microservice_content, TEST_EXECUTOR_FILE_NAME: test_microservice_content, @@ -193,21 +197,7 @@ metas: file_name_s=[REQUIREMENTS_FILE_NAME], parse_result_fn=self.parse_result_fn_requirements, tag_name=REQUIREMENTS_FILE_TAG, - )[REQUIREMENTS_FILE_NAME] - - # I deactivated this because 3.5-turbo was halucinating packages that were not needed - # now, in the first iteration the default dockerfile is used - # self.generate_and_persist_file( - # section_title='Generate Dockerfile', - # template=template_generate_apt_get_install, - # destination_folder=MICROSERVICE_FOLDER_v1, - # file_name_s=None, - # parse_result_fn=self.parse_result_fn_dockerfile, - # docker_file_wrapped=self.read_docker_template(), - # requirements_file_wrapped=self.files_to_string({ - # REQUIREMENTS_FILE_NAME: requirements_content, - # }) - # ) + ) with open(os.path.join(os.path.dirname(__file__), 'static_files', 'microservice', 'Dockerfile'), 'r', encoding='utf-8') as f: @@ -217,9 +207,9 @@ metas: for line in docker_file_template_lines ] docker_file_content = '\n'.join(docker_file_template_lines) - persist_file(docker_file_content, os.path.join(MICROSERVICE_FOLDER_v1, 'Dockerfile')) + persist_file(docker_file_content, os.path.join(self.cur_microservice_path, 'Dockerfile')) - self.write_config_yml(microservice_name, MICROSERVICE_FOLDER_v1) + self.write_config_yml(self.microservice_name, self.cur_microservice_path) print('\nFirst version of the microservice generated. Start iterating on it to make the tests pass...') @@ -249,15 +239,15 @@ pytest {os.linesep.join(lines)}''' return {REQUIREMENTS_FILE_NAME: content_modified} - def generate_playground(self, microservice_name, microservice_path): + def generate_playground(self): print_colored('', '\n\n############# Playground #############', 'blue') - file_name_to_content = get_all_microservice_files_with_content(microservice_path) + file_name_to_content = get_all_microservice_files_with_content(self.cur_microservice_path) conversation = self.gpt_session.get_conversation() conversation.chat( template_generate_playground.format( code_files_wrapped=self.files_to_string(file_name_to_content, ['test_microservice.py']), - microservice_name=microservice_name, + microservice_name=self.microservice_name, ) ) playground_content_raw = conversation.chat( @@ -274,12 +264,12 @@ pytest content_raw, 'app.py', match_single_block=True ) - gateway_path = os.path.join(microservice_path, 'gateway') + gateway_path = os.path.join(self.cur_microservice_path, 'gateway') shutil.copytree(os.path.join(os.path.dirname(__file__), 'static_files', 'gateway'), gateway_path) persist_file(playground_content, os.path.join(gateway_path, 'app.py')) # fill-in name of microservice - gateway_name = f'Gateway{microservice_name}' + gateway_name = f'Gateway{self.microservice_name}' custom_gateway_path = os.path.join(gateway_path, 'custom_gateway.py') with open(custom_gateway_path, 'r', encoding='utf-8') as f: custom_gateway_content = f.read() @@ -297,40 +287,38 @@ pytest print('Final step...') hubble_log = push_executor(gateway_path) if not is_executor_in_hub(gateway_name): - raise Exception(f'{microservice_name} not in hub. Hubble logs: {hubble_log}') + raise Exception(f'{self.microservice_name} not in hub. Hubble logs: {hubble_log}') - def debug_microservice(self, microservice_name, num_approach, packages): + def debug_microservice(self, num_approach, packages): for i in range(1, MAX_DEBUGGING_ITERATIONS): print('Debugging iteration', i) print('Trying to debug the microservice. Might take a while...') - previous_microservice_path = get_microservice_path(self.microservice_root_path, microservice_name, packages, - num_approach, i) - next_microservice_path = get_microservice_path(self.microservice_root_path, microservice_name, packages, - num_approach, i + 1) - clean_requirements_txt(previous_microservice_path) - log_hubble = push_executor(previous_microservice_path) + clean_requirements_txt(self.cur_microservice_path) + log_hubble = push_executor(self.cur_microservice_path) error = process_error_message(log_hubble) if error: print('An error occurred during the build process. Feeding the error back to the assistant...') - self.do_debug_iteration(error, next_microservice_path, previous_microservice_path) + self.previous_microservice_path = self.cur_microservice_path + self.cur_microservice_path = get_microservice_path( + self.microservice_root_path, self.microservice_name, packages, num_approach, i + 1 + ) + os.makedirs(self.cur_microservice_path) + self.do_debug_iteration(error) if i == MAX_DEBUGGING_ITERATIONS - 1: raise self.MaxDebugTimeReachedException('Could not debug the microservice.') else: # at the moment, there can be cases where no error log is extracted but the executor is still not published # it leads to problems later on when someone tries a run or deployment - if is_executor_in_hub(microservice_name): + if is_executor_in_hub(self.microservice_name): print('Successfully build microservice.') break else: - raise Exception(f'{microservice_name} not in hub. Hubble logs: {log_hubble}') + raise Exception(f'{self.microservice_name} not in hub. Hubble logs: {log_hubble}') - return get_microservice_path(self.microservice_root_path, microservice_name, packages, num_approach, i) - - def do_debug_iteration(self, error, next_microservice_path, previous_microservice_path): - os.makedirs(next_microservice_path) - file_name_to_content = get_all_microservice_files_with_content(previous_microservice_path) + def do_debug_iteration(self, error): + file_name_to_content = get_all_microservice_files_with_content(self.previous_microservice_path) for file_name, content in file_name_to_content.items(): - persist_file(content, os.path.join(next_microservice_path, file_name)) + persist_file(content, os.path.join(self.cur_microservice_path, file_name)) summarized_error = self.summarize_error(error) dock_req_string = self.files_to_string({ @@ -343,7 +331,6 @@ pytest self.generate_and_persist_file( section_title='Debugging apt-get dependency issue', template=template_solve_apt_get_dependency_issue, - destination_folder=next_microservice_path, file_name_s=['apt-get-packages.json'], parse_result_fn=self.parse_result_fn_dockerfile, system_definition_examples=[], @@ -357,7 +344,6 @@ pytest self.generate_and_persist_file( section_title='Debugging pip dependency issue', template=template_solve_pip_dependency_issue, - destination_folder=next_microservice_path, file_name_s=[REQUIREMENTS_FILE_NAME], summarized_error=summarized_error, all_files_string=dock_req_string, @@ -370,7 +356,6 @@ pytest self.generate_and_persist_file( section_title='Suggest solution for code issue', template=template_suggest_solutions_code_issue, - destination_folder=next_microservice_path, file_name_s=['solutions.json'], summarized_error=summarized_error, task_description=self.microservice_specification.task, @@ -379,16 +364,47 @@ pytest )['solutions.json'] ) + was_error_seen_before = json.loads( + self.generate_and_persist_file( + section_title='Check if error was seen before', + template=template_was_error_seen_before, + file_name_s=['response.json'], + summarized_error=summarized_error, + previous_errors=None, # todo: fill-in mapping from previous errors to suggested solutions + system_definition_examples=None, + )['response.json'] + )['was_error_seen_before'].lower() == 'yes' + + suggested_solution = None + if was_error_seen_before: + for _num_solution in range(len(suggested_solutions)): + _suggested_solution = suggested_solutions[str(_num_solution)] + was_solution_tried_before = json.loads( + self.generate_and_persist_file( + section_title='Check if solution was tried before', + template=template_was_solution_tried_before, + file_name_s=['response.json'], + tried_solutions=None, # todo: fill-in mapping from tried solutions to suggested solutions + suggested_solution=_suggested_solution, + system_definition_examples=None, + )['response.json'] + )['will_lead_to_different_actions'].lower() == 'no' + if not was_solution_tried_before: + suggested_solution = _suggested_solution + break + + if suggested_solution is None: + suggested_solution = f"solve error: {summarized_error}" + self.generate_and_persist_file( section_title='Implementing suggestion solution for code issue', template=template_implement_solution_code_issue, - destination_folder=next_microservice_path, file_name_s=[IMPLEMENTATION_FILE_NAME, TEST_EXECUTOR_FILE_NAME, REQUIREMENTS_FILE_NAME], summarized_error=summarized_error, task_description=self.microservice_specification.task, test_description=self.microservice_specification.test, all_files_string=all_files_string, - suggested_solution=suggested_solutions["1"], + suggested_solution=suggested_solution, ) class MaxDebugTimeReachedException(BaseException): @@ -449,13 +465,13 @@ pytest self.refine_specification() os.makedirs(self.microservice_root_path) generated_name = self.generate_microservice_name(self.microservice_specification.task) - microservice_name = f'{generated_name}{random.randint(0, 10_000_000)}' + self.microservice_name = f'{generated_name}{random.randint(0, 10_000_000)}' packages_list = self.get_possible_packages() for num_approach, packages in enumerate(packages_list): try: - self.generate_microservice(microservice_name, packages, num_approach) - final_version_path = self.debug_microservice(microservice_name, num_approach, packages) - self.generate_playground(microservice_name, final_version_path) + self.generate_microservice(packages, num_approach) + self.debug_microservice(num_approach, packages) + self.generate_playground() except self.MaxDebugTimeReachedException: print('Could not debug the Microservice with the approach:', packages) if num_approach == len(packages_list) - 1: From dc4b17d2efdf20fff65fe8b669f728421a9c87b0 Mon Sep 17 00:00:00 2001 From: Joschka Braun Date: Thu, 4 May 2023 16:56:46 +0200 Subject: [PATCH 07/67] :rocket: feat: generate novel solution suggestions --- src/options/generate/generator.py | 99 +++++++++++++++----------- src/options/generate/templates_user.py | 47 ++++++++++-- 2 files changed, 97 insertions(+), 49 deletions(-) diff --git a/src/options/generate/generator.py b/src/options/generate/generator.py index 89cdb9d..b035ba9 100644 --- a/src/options/generate/generator.py +++ b/src/options/generate/generator.py @@ -13,7 +13,7 @@ from pydantic.dataclasses import dataclass from src.apis import gpt from src.apis.gpt import _GPTConversation from src.apis.jina_cloud import process_error_message, push_executor, is_executor_in_hub -from src.apis.pypi import is_package_on_pypi, get_latest_package_version, clean_requirements_txt +from src.apis.pypi import is_package_on_pypi, clean_requirements_txt from src.constants import FILE_AND_TAG_PAIRS, NUM_IMPLEMENTATION_STRATEGIES, MAX_DEBUGGING_ITERATIONS, \ BLACKLISTED_PACKAGES, EXECUTOR_FILE_NAME, TEST_EXECUTOR_FILE_NAME, TEST_EXECUTOR_FILE_TAG, \ REQUIREMENTS_FILE_NAME, REQUIREMENTS_FILE_TAG, DOCKER_FILE_NAME, IMPLEMENTATION_FILE_NAME, \ @@ -48,6 +48,8 @@ class Generator: self.microservice_name = None self.previous_microservice_path = None self.cur_microservice_path = None + self.previous_errors = [] + self.previous_solutions = [] def extract_content_from_result(self, plain_text, file_name, match_single_block=False, can_contain_code_block=True): optional_line_break = '\n' if can_contain_code_block else '' # the \n at the end makes sure that ``` within the generated code is not matched because it is not right before a line break @@ -352,49 +354,8 @@ pytest all_files_string = self.files_to_string( {key: val for key, val in file_name_to_content.items() if key != EXECUTOR_FILE_NAME} ) - suggested_solutions = json.loads( - self.generate_and_persist_file( - section_title='Suggest solution for code issue', - template=template_suggest_solutions_code_issue, - file_name_s=['solutions.json'], - summarized_error=summarized_error, - task_description=self.microservice_specification.task, - test_description=self.microservice_specification.test, - all_files_string=all_files_string, - )['solutions.json'] - ) - was_error_seen_before = json.loads( - self.generate_and_persist_file( - section_title='Check if error was seen before', - template=template_was_error_seen_before, - file_name_s=['response.json'], - summarized_error=summarized_error, - previous_errors=None, # todo: fill-in mapping from previous errors to suggested solutions - system_definition_examples=None, - )['response.json'] - )['was_error_seen_before'].lower() == 'yes' - - suggested_solution = None - if was_error_seen_before: - for _num_solution in range(len(suggested_solutions)): - _suggested_solution = suggested_solutions[str(_num_solution)] - was_solution_tried_before = json.loads( - self.generate_and_persist_file( - section_title='Check if solution was tried before', - template=template_was_solution_tried_before, - file_name_s=['response.json'], - tried_solutions=None, # todo: fill-in mapping from tried solutions to suggested solutions - suggested_solution=_suggested_solution, - system_definition_examples=None, - )['response.json'] - )['will_lead_to_different_actions'].lower() == 'no' - if not was_solution_tried_before: - suggested_solution = _suggested_solution - break - - if suggested_solution is None: - suggested_solution = f"solve error: {summarized_error}" + suggested_solution = self.generate_solution_suggestion(summarized_error, all_files_string) self.generate_and_persist_file( section_title='Implementing suggestion solution for code issue', @@ -407,6 +368,58 @@ pytest suggested_solution=suggested_solution, ) + def generate_solution_suggestion(self, summarized_error, all_files_string): + suggested_solutions = json.loads( + self.generate_and_persist_file( + section_title='Suggest solution for code issue', + template=template_suggest_solutions_code_issue, + file_name_s=['solutions.json'], + summarized_error=summarized_error, + task_description=self.microservice_specification.task, + test_description=self.microservice_specification.test, + all_files_string=all_files_string, + )['solutions.json'] + ) + + if len(self.previous_errors) > 0: + was_error_seen_before = json.loads( + self.generate_and_persist_file( + section_title='Check if error was seen before', + template=template_was_error_seen_before, + file_name_s=['response.json'], + summarized_error=summarized_error, + previous_errors=f'- "{os.linesep}"'.join(self.previous_errors), + system_definition_examples=None, + )['response.json'] + )['was_error_seen_before'].lower() == 'yes' + + suggested_solution = None + if was_error_seen_before: + for _num_solution in range(1, len(suggested_solutions) + 1): + _suggested_solution = suggested_solutions[str(_num_solution)] + was_solution_tried_before = json.loads( + self.generate_and_persist_file( + section_title='Check if solution was tried before', + template=template_was_solution_tried_before, + file_name_s=['response.json'], + tried_solutions=f'- "{os.linesep}"'.join(self.previous_solutions), + suggested_solution=_suggested_solution, + system_definition_examples=None, + )['response.json'] + )['will_lead_to_different_actions'].lower() == 'no' + if not was_solution_tried_before: + suggested_solution = _suggested_solution + break + else: + suggested_solution = suggested_solutions['1'] + + if suggested_solution is None: + suggested_solution = f"solve error: {summarized_error}" + else: + suggested_solution = suggested_solutions['1'] + + return suggested_solution + class MaxDebugTimeReachedException(BaseException): pass diff --git a/src/options/generate/templates_user.py b/src/options/generate/templates_user.py index 9bb1f65..4ac0af2 100644 --- a/src/options/generate/templates_user.py +++ b/src/options/generate/templates_user.py @@ -62,7 +62,7 @@ e) the implementation of the core problem using the package would obey the follo When answering, just write "yes" or "no". 4. For each approach, list the required python package combinations as discibed in the following. -You must output the package combinations as json wrapped into tripple backticks ``` and name it **strategies.json**. \ +You must output the package combinations as json wrapped into triple backticks ``` and name it **strategies.json**. \ Note that you can also leave a list empty to indicate that one of the strategies does not require any package and can be done in plain python. Write the output using double asterisks and triple backticks like this: **strategies.json** @@ -78,7 +78,7 @@ Write the output using double asterisks and triple backticks like this: template_code_wrapping_string = '''The code will go into {file_name_purpose}. -Note that you must obey the double asterisk and tripple backtick syntax from like this: +Note that you must obey the double asterisk and triple backtick syntax from like this: **{file_name}** ```{tag_name} ...code... @@ -228,7 +228,7 @@ Is this error happening because a PACKAGE_MANAGER package is missing or failed t ```json {{"dependency_installation_failure": ""}} ``` -Note that you must obey the double asterisk and tripple backtick syntax from above. +Note that you must obey the double asterisk and triple backtick syntax from above. ''' ) @@ -313,8 +313,9 @@ Here are all the files I use: Here is the summary of the error that occurred: {summarized_error} -You should suggest 3 to 5 possible solutions on how to solve it. +You should suggest 3 to 5 possible solution approaches on how to solve it. Obey the following rules: +Do not implement the solution. You have no access to the documentation of the package. You must not change the Dockerfile. Note that any changes needed to make the test pass must be written under the constraint that ''' + IMPLEMENTATION_FILE_NAME + ''' will be used in a different file as well. @@ -332,6 +333,40 @@ After thinking about the possible solutions, output them as JSON ranked from bes ) +template_was_error_seen_before = PromptTemplate.from_template( + '''Previously encountered error messages: +{previous_errors} + +Now encountered error message: "{summarized_error}" +Was this error message encountered before? + +Write down your final answer as json in the following format: +**response.json** +```json +{{"was_error_seen_before": ""}} +``` +Note that you must obey the double asterisk and triple backtick syntax from above. +''' +) + + +template_was_solution_tried_before = PromptTemplate.from_template( + '''Previously tried solutions: +{tried_solutions} + +Suggested solution: "{suggested_solution}" + +Will the suggested solution lead to different actions than the previously tried solutions? + +Write down your final answer as json in the following format: +**response.json** +```json +{{"will_lead_to_different_actions": ""}} +``` +Note that you must obey the double asterisk and triple backtick syntax from above.''' +) + + template_implement_solution_code_issue = PromptTemplate.from_template( '''Here is the description of the task the function must solve: {task_description} @@ -442,7 +477,7 @@ Or write the detailed microservice description all mentioned code samples, docum }} ``` Note that your response must be either prompt.json or final.json. You must not write both. -Note that you must obey the double asterisk and tripple backtick syntax from above. +Note that you must obey the double asterisk and triple backtick syntax from above. Note that the last sequence of characters in your response must be ``` (triple backtick). Note that prompt.json must not only contain one question. Note that if urls, secrets, database names, etc. are mentioned, they must be part of the summary. @@ -486,7 +521,7 @@ Example for the case where the example is already mentioned in the refined descr }} ``` Note that your response must be either prompt.json or final.json. You must not write both. -Note that you must obey the double asterisk and tripple backtick syntax from above. +Note that you must obey the double asterisk and triple backtick syntax from above. Note that the last sequence of characters in your response must be ``` (triple backtick). Note that your response must start with the character sequence ** (double asterisk). Note that prompt.json must only contain one question. From 3aa670ccd6d408b72013bf869b2cd3e88f68c400 Mon Sep 17 00:00:00 2001 From: Joschka Braun Date: Thu, 4 May 2023 17:33:49 +0200 Subject: [PATCH 08/67] :rocket: feat: generate novel solution suggestions --- src/options/generate/generator.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/options/generate/generator.py b/src/options/generate/generator.py index 1c96c48..48fe280 100644 --- a/src/options/generate/generator.py +++ b/src/options/generate/generator.py @@ -369,6 +369,9 @@ pytest suggested_solution=suggested_solution, ) + self.previous_errors.append(summarized_error) + self.previous_solutions.append(suggested_solution) + def generate_solution_suggestion(self, summarized_error, all_files_string): suggested_solutions = json.loads( self.generate_and_persist_file( From 649c64fe3f023967e9aad3e7e294a194e229ff41 Mon Sep 17 00:00:00 2001 From: Joschka Braun Date: Thu, 4 May 2023 17:43:33 +0200 Subject: [PATCH 09/67] :rocket: fix: fix typo --- src/options/generate/generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/options/generate/generator.py b/src/options/generate/generator.py index 48fe280..6004002 100644 --- a/src/options/generate/generator.py +++ b/src/options/generate/generator.py @@ -129,7 +129,7 @@ metas: system_introduction_message = _GPTConversation._create_system_message(self.microservice_specification.task, self.microservice_specification.test, system_definition_examples) - conversation = self.gpt_session.get_conversation(messages=[system_introduction_message]) + conversation = self.gpt_session.get_conversation(messages=[system_introduction_message] if system_introduction_message else []) template_kwargs = {k: v for k, v in template_kwargs.items() if k in template.input_variables} if 'file_name' in template.input_variables and len(file_name_s) == 1: template_kwargs['file_name'] = file_name_s[0] From d4741d466c52d35b066ee998fc5a708575610435 Mon Sep 17 00:00:00 2001 From: Joschka Braun Date: Fri, 5 May 2023 11:49:26 +0200 Subject: [PATCH 10/67] :rocket: fix: merge main --- dev_gpt/options/generate/generator.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/dev_gpt/options/generate/generator.py b/dev_gpt/options/generate/generator.py index f03f566..28aeb4f 100644 --- a/dev_gpt/options/generate/generator.py +++ b/dev_gpt/options/generate/generator.py @@ -26,7 +26,8 @@ from dev_gpt.options.generate.templates_user import template_generate_microservi template_generate_function, template_generate_test, template_generate_requirements, \ template_chain_of_thought, template_summarize_error, \ template_solve_apt_get_dependency_issue, template_pm_task_iteration, \ - template_pm_test_iteration + template_pm_test_iteration, template_suggest_solutions_code_issue, template_was_error_seen_before, \ + template_was_solution_tried_before from dev_gpt.options.generate.ui import get_random_employee from dev_gpt.utils.io import persist_file, get_all_microservice_files_with_content, get_microservice_path from dev_gpt.utils.string_tools import print_colored @@ -99,7 +100,7 @@ metas: destination_folder: str = None, file_name_s: List[str] = None, parse_result_fn: Callable = None, - system_definition_examples: List[str] = [], # todo: rename to use_system_definition_exampels: bool = True + use_system_message: bool = True, **template_kwargs ): """This function generates file(s) using the given template and persists it/them in the given destination folder. @@ -114,7 +115,7 @@ metas: parse_result_fn (Callable, optional): A function that parses the generated content and returns a dictionary mapping file_name to its content. If no content could be extract, it returns an empty dictionary. Defaults to None. If None, default parsing is used which uses the file_name to extract from the generated content. - system_definition_examples (List[str], optional): The system definition examples to be used for the conversation. Defaults to []. + use_system_message (bool, optional): whether to use custom system message or not. Defaults to True. **template_kwargs: The keyword arguments to be passed to the template. """ if destination_folder is None: @@ -128,7 +129,9 @@ metas: self.microservice_specification.task, self.microservice_specification.test ) - conversation = self.gpt_session.get_conversation(messages=[system_introduction_message] if use_system_message else []) + conversation = self.gpt_session.get_conversation( + messages=[system_introduction_message] if use_system_message else [] + ) template_kwargs = {k: v for k, v in template_kwargs.items() if k in template.input_variables} if 'file_name' in template.input_variables and len(file_name_s) == 1: template_kwargs['file_name'] = file_name_s[0] @@ -158,8 +161,8 @@ metas: with open(os.path.join(os.path.dirname(__file__), 'static_files', 'microservice', 'jina_wrapper.py'), 'r', encoding='utf-8') as f: microservice_executor_boilerplate = f.read() - microservice_executor_code = microservice_executor_boilerplate.replace('class DevGPTExecutor(Executor):', - f'class {microservice_name}(Executor):') + microservice_executor_code = microservice_executor_boilerplate \ + .replace('class DevGPTExecutor(Executor):', f'class {self.microservice_name}(Executor):') persist_file(microservice_executor_code, os.path.join(self.cur_microservice_path, EXECUTOR_FILE_NAME)) with open(os.path.join(os.path.dirname(__file__), 'static_files', 'microservice', 'apis.py'), 'r', encoding='utf-8') as f: @@ -335,7 +338,6 @@ pytest template=template_solve_apt_get_dependency_issue, file_name_s=['apt-get-packages.json'], parse_result_fn=self.parse_result_fn_dockerfile, - system_definition_examples=[], summarized_error=summarized_error, all_files_string=dock_req_string, ) @@ -392,7 +394,7 @@ pytest file_name_s=['response.json'], summarized_error=summarized_error, previous_errors=f'- "{os.linesep}"'.join(self.previous_errors), - system_definition_examples=None, + use_system_message=False, )['response.json'] )['was_error_seen_before'].lower() == 'yes' @@ -407,7 +409,7 @@ pytest file_name_s=['response.json'], tried_solutions=f'- "{os.linesep}"'.join(self.previous_solutions), suggested_solution=_suggested_solution, - system_definition_examples=None, + use_system_message=False, )['response.json'] )['will_lead_to_different_actions'].lower() == 'no' if not was_solution_tried_before: @@ -466,7 +468,6 @@ pytest template=template_generate_possible_packages, destination_folder=self.microservice_root_path, file_name_s=['strategies.json'], - system_definition_examples=[], description=self.microservice_specification.task )['strategies.json'] packages_list = [[pkg.strip().lower() for pkg in packages] for packages in json.loads(packages_json_string)] From 7063a7e25dc408c7e2709345d69fd28a5e60257c Mon Sep 17 00:00:00 2001 From: Joschka Braun Date: Fri, 5 May 2023 11:52:32 +0200 Subject: [PATCH 11/67] :rocket: test: add level 5 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index abfc233..b3b71b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: strategy: fail-fast: false matrix: - group: [0, 1, 2, 3, 4] + group: [0, 1, 2, 3, 4, 5] steps: - uses: actions/checkout@v2 - name: Set up Python 3.8 From 55c0e52c5d69c81aec77adb46d22e6720d95f9df Mon Sep 17 00:00:00 2001 From: Joschka Braun Date: Fri, 5 May 2023 12:19:14 +0200 Subject: [PATCH 12/67] :rocket: fix: fix asking for content --- dev_gpt/apis/gpt.py | 1 - dev_gpt/options/generate/generator.py | 10 +++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/dev_gpt/apis/gpt.py b/dev_gpt/apis/gpt.py index f9416e3..3ebf3d9 100644 --- a/dev_gpt/apis/gpt.py +++ b/dev_gpt/apis/gpt.py @@ -109,7 +109,6 @@ class _GPTConversation: self.print_stream = print_stream self.print_costs = print_costs - def print_messages(self, messages): for i, message in enumerate(messages): if os.environ['VERBOSE'].lower() == 'true': diff --git a/dev_gpt/options/generate/generator.py b/dev_gpt/options/generate/generator.py index 28aeb4f..ea516e4 100644 --- a/dev_gpt/options/generate/generator.py +++ b/dev_gpt/options/generate/generator.py @@ -142,8 +142,16 @@ metas: ) content = parse_result_fn(content_raw) if content == {}: + conversation = self.gpt_session.get_conversation(messages=[AIMessage(content=content_raw)]) content_raw = conversation.chat( - 'You must add the content' + (f' for {file_name_s[0]}' if len(file_name_s) == 1 else '')) + 'You must add the content' + (f' for {file_name_s[0]}' if len(file_name_s) == 1 else '') + + ''' in triple backticks. A general example is this: + +**file_name.file_ending** +``` +```''' + ) content = parse_result_fn(content_raw) for _file_name, _file_content in content.items(): persist_file(_file_content, os.path.join(destination_folder, _file_name)) From 23dedf3ccc0686acd56ab6af96e29e7aa820618a Mon Sep 17 00:00:00 2001 From: Joschka Braun Date: Fri, 5 May 2023 15:02:41 +0200 Subject: [PATCH 13/67] :rocket: fix: fix asking for content --- dev_gpt/options/generate/generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev_gpt/options/generate/generator.py b/dev_gpt/options/generate/generator.py index ea516e4..be50f58 100644 --- a/dev_gpt/options/generate/generator.py +++ b/dev_gpt/options/generate/generator.py @@ -145,7 +145,7 @@ metas: conversation = self.gpt_session.get_conversation(messages=[AIMessage(content=content_raw)]) content_raw = conversation.chat( 'You must add the content' + (f' for {file_name_s[0]}' if len(file_name_s) == 1 else '') + - ''' in triple backticks. A general example is this: + '''. Make sure to wrap any code in triple backticks in the beginning and end of any code. A general example is this: **file_name.file_ending** ``` Date: Fri, 5 May 2023 15:10:47 +0200 Subject: [PATCH 14/67] :rocket: fix: fix asking for content --- dev_gpt/options/generate/generator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev_gpt/options/generate/generator.py b/dev_gpt/options/generate/generator.py index be50f58..315a493 100644 --- a/dev_gpt/options/generate/generator.py +++ b/dev_gpt/options/generate/generator.py @@ -144,8 +144,8 @@ metas: if content == {}: conversation = self.gpt_session.get_conversation(messages=[AIMessage(content=content_raw)]) content_raw = conversation.chat( - 'You must add the content' + (f' for {file_name_s[0]}' if len(file_name_s) == 1 else '') + - '''. Make sure to wrap any code in triple backticks in the beginning and end of any code. A general example is this: + 'You must add the content' + (f' for `{file_name_s[0]}`' if len(file_name_s) == 1 else '') + + '''. You must wrap any code in triple backticks at the beginning and end of any file.. A general example is this: **file_name.file_ending** ``` Date: Fri, 5 May 2023 16:43:13 +0200 Subject: [PATCH 15/67] :rocket: fix: use system message for playground --- dev_gpt/options/generate/generator.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/dev_gpt/options/generate/generator.py b/dev_gpt/options/generate/generator.py index 315a493..151201b 100644 --- a/dev_gpt/options/generate/generator.py +++ b/dev_gpt/options/generate/generator.py @@ -100,7 +100,7 @@ metas: destination_folder: str = None, file_name_s: List[str] = None, parse_result_fn: Callable = None, - use_system_message: bool = True, + use_custom_system_message: bool = True, **template_kwargs ): """This function generates file(s) using the given template and persists it/them in the given destination folder. @@ -115,7 +115,7 @@ metas: parse_result_fn (Callable, optional): A function that parses the generated content and returns a dictionary mapping file_name to its content. If no content could be extract, it returns an empty dictionary. Defaults to None. If None, default parsing is used which uses the file_name to extract from the generated content. - use_system_message (bool, optional): whether to use custom system message or not. Defaults to True. + use_custom_system_message (bool, optional): whether to use custom system message or not. Defaults to True. **template_kwargs: The keyword arguments to be passed to the template. """ if destination_folder is None: @@ -125,12 +125,15 @@ metas: parse_result_fn = self.get_default_parse_result_fn(file_name_s) print_colored('', f'\n\n############# {section_title} #############', 'blue') - system_introduction_message = _GPTConversation._create_system_message( - self.microservice_specification.task, - self.microservice_specification.test - ) + if use_custom_system_message: + system_introduction_message = _GPTConversation._create_system_message( + self.microservice_specification.task, + self.microservice_specification.test + ) + else: + system_introduction_message = SystemMessage(content='You are a helpful assistant.') conversation = self.gpt_session.get_conversation( - messages=[system_introduction_message] if use_system_message else [] + messages=[system_introduction_message] if use_custom_system_message else [] ) template_kwargs = {k: v for k, v in template_kwargs.items() if k in template.input_variables} if 'file_name' in template.input_variables and len(file_name_s) == 1: @@ -142,10 +145,12 @@ metas: ) content = parse_result_fn(content_raw) if content == {}: - conversation = self.gpt_session.get_conversation(messages=[AIMessage(content=content_raw)]) + conversation = self.gpt_session.get_conversation( + messages=[SystemMessage(content='You are a helpful assistant.'), AIMessage(content=content_raw)] + ) content_raw = conversation.chat( 'You must add the content' + (f' for `{file_name_s[0]}`' if len(file_name_s) == 1 else '') + - '''. You must wrap any code in triple backticks at the beginning and end of any file.. A general example is this: + '''. You must wrap any code in triple backticks at the beginning and end of it. A general example is this: **file_name.file_ending** ``` Date: Fri, 5 May 2023 17:26:26 +0200 Subject: [PATCH 16/67] :rocket: fix: example of for file wrapping --- dev_gpt/options/generate/generator.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/dev_gpt/options/generate/generator.py b/dev_gpt/options/generate/generator.py index 151201b..a88a023 100644 --- a/dev_gpt/options/generate/generator.py +++ b/dev_gpt/options/generate/generator.py @@ -148,14 +148,27 @@ metas: conversation = self.gpt_session.get_conversation( messages=[SystemMessage(content='You are a helpful assistant.'), AIMessage(content=content_raw)] ) - content_raw = conversation.chat( - 'You must add the content' + (f' for `{file_name_s[0]}`' if len(file_name_s) == 1 else '') + - '''. You must wrap any code in triple backticks at the beginning and end of it. A general example is this: - -**file_name.file_ending** + if len(file_name_s) == 1: + file_ending = file_name_s[0].split('.')[-1] + if file_ending == 'py': + tag = 'python' + elif file_ending == 'json': + tag = 'json' + else: + tag = '' + file_wrapping_example = f'''**{file_name_s[0]}** +```{tag} + +```''' + else: + file_wrapping_example = '''**file_name.file_ending** ``` ```''' + content_raw = conversation.chat( + 'You must add the content' + (f' for `{file_name_s[0]}`' if len(file_name_s) == 1 else '') + + '. You must wrap any file in triple backticks at the beginning and end of it. Like this:\n' + + file_wrapping_example ) content = parse_result_fn(content_raw) for _file_name, _file_content in content.items(): @@ -406,7 +419,7 @@ pytest template=template_was_error_seen_before, file_name_s=['response.json'], summarized_error=summarized_error, - previous_errors=f'- "{os.linesep}"'.join(self.previous_errors), + previous_errors=f'- "{os.linesep}"\n'.join(self.previous_errors), use_custom_system_message=False, )['response.json'] )['was_error_seen_before'].lower() == 'yes' @@ -420,7 +433,7 @@ pytest section_title='Check if solution was tried before', template=template_was_solution_tried_before, file_name_s=['response.json'], - tried_solutions=f'- "{os.linesep}"'.join(self.previous_solutions), + tried_solutions=f'- "{os.linesep}"\n'.join(self.previous_solutions), suggested_solution=_suggested_solution, use_custom_system_message=False, )['response.json'] From b8a3d12be559917014d2001ca9be7fb70773419a Mon Sep 17 00:00:00 2001 From: Joschka Braun Date: Mon, 8 May 2023 10:11:01 +0200 Subject: [PATCH 17/67] :rocket: fix: fix name --- dev_gpt/options/generate/generator.py | 4 ++-- dev_gpt/options/generate/templates_user.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dev_gpt/options/generate/generator.py b/dev_gpt/options/generate/generator.py index a88a023..f4dc278 100644 --- a/dev_gpt/options/generate/generator.py +++ b/dev_gpt/options/generate/generator.py @@ -417,7 +417,7 @@ pytest self.generate_and_persist_file( section_title='Check if error was seen before', template=template_was_error_seen_before, - file_name_s=['response.json'], + file_name_s=['was_error_seen_before.json'], summarized_error=summarized_error, previous_errors=f'- "{os.linesep}"\n'.join(self.previous_errors), use_custom_system_message=False, @@ -432,7 +432,7 @@ pytest self.generate_and_persist_file( section_title='Check if solution was tried before', template=template_was_solution_tried_before, - file_name_s=['response.json'], + file_name_s=['will_lead_to_different_actions.json'], tried_solutions=f'- "{os.linesep}"\n'.join(self.previous_solutions), suggested_solution=_suggested_solution, use_custom_system_message=False, diff --git a/dev_gpt/options/generate/templates_user.py b/dev_gpt/options/generate/templates_user.py index 34913a5..d51bb18 100644 --- a/dev_gpt/options/generate/templates_user.py +++ b/dev_gpt/options/generate/templates_user.py @@ -338,7 +338,7 @@ Now encountered error message: "{summarized_error}" Was this error message encountered before? Write down your final answer as json in the following format: -**response.json** +**was_error_seen_before.json** ```json {{"was_error_seen_before": ""}} ``` @@ -356,7 +356,7 @@ Suggested solution: "{suggested_solution}" Will the suggested solution lead to different actions than the previously tried solutions? Write down your final answer as json in the following format: -**response.json** +**will_lead_to_different_actions.json** ```json {{"will_lead_to_different_actions": ""}} ``` From 9f24ee9c75948f88168f3ce94a652eec43cab383 Mon Sep 17 00:00:00 2001 From: Joschka Braun Date: Mon, 8 May 2023 10:19:05 +0200 Subject: [PATCH 18/67] :rocket: fix: fix typo --- dev_gpt/options/generate/generator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev_gpt/options/generate/generator.py b/dev_gpt/options/generate/generator.py index f4dc278..0e4bfdb 100644 --- a/dev_gpt/options/generate/generator.py +++ b/dev_gpt/options/generate/generator.py @@ -421,7 +421,7 @@ pytest summarized_error=summarized_error, previous_errors=f'- "{os.linesep}"\n'.join(self.previous_errors), use_custom_system_message=False, - )['response.json'] + )['was_error_seen_before.json'] )['was_error_seen_before'].lower() == 'yes' suggested_solution = None @@ -436,7 +436,7 @@ pytest tried_solutions=f'- "{os.linesep}"\n'.join(self.previous_solutions), suggested_solution=_suggested_solution, use_custom_system_message=False, - )['response.json'] + )['will_lead_to_different_actions.json'] )['will_lead_to_different_actions'].lower() == 'no' if not was_solution_tried_before: suggested_solution = _suggested_solution From fdc8d0414c8268076e6b29cd3d67672db49fc942 Mon Sep 17 00:00:00 2001 From: Joschka Braun Date: Mon, 8 May 2023 10:39:54 +0200 Subject: [PATCH 19/67] :rocket: fix: fix typo --- dev_gpt/options/generate/generator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev_gpt/options/generate/generator.py b/dev_gpt/options/generate/generator.py index 0e4bfdb..63fe836 100644 --- a/dev_gpt/options/generate/generator.py +++ b/dev_gpt/options/generate/generator.py @@ -419,7 +419,7 @@ pytest template=template_was_error_seen_before, file_name_s=['was_error_seen_before.json'], summarized_error=summarized_error, - previous_errors=f'- "{os.linesep}"\n'.join(self.previous_errors), + previous_errors='- "' + f'"{os.linesep}- "'.join(self.previous_errors) + '"', use_custom_system_message=False, )['was_error_seen_before.json'] )['was_error_seen_before'].lower() == 'yes' @@ -433,7 +433,7 @@ pytest section_title='Check if solution was tried before', template=template_was_solution_tried_before, file_name_s=['will_lead_to_different_actions.json'], - tried_solutions=f'- "{os.linesep}"\n'.join(self.previous_solutions), + tried_solutions='- "' + f'"{os.linesep}- "'.join(self.previous_solutions) + '"', suggested_solution=_suggested_solution, use_custom_system_message=False, )['will_lead_to_different_actions.json'] From 5377b214dca46618a6feaf98355bddf5201b37a8 Mon Sep 17 00:00:00 2001 From: Joschka Braun Date: Mon, 8 May 2023 15:40:08 +0200 Subject: [PATCH 20/67] :rocket: feat: add explicit response format --- dev_gpt/options/generate/generator.py | 9 +++++++-- dev_gpt/options/generate/templates_user.py | 22 ++++++++++++++-------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/dev_gpt/options/generate/generator.py b/dev_gpt/options/generate/generator.py index 63fe836..044f3bc 100644 --- a/dev_gpt/options/generate/generator.py +++ b/dev_gpt/options/generate/generator.py @@ -27,7 +27,7 @@ from dev_gpt.options.generate.templates_user import template_generate_microservi template_chain_of_thought, template_summarize_error, \ template_solve_apt_get_dependency_issue, template_pm_task_iteration, \ template_pm_test_iteration, template_suggest_solutions_code_issue, template_was_error_seen_before, \ - template_was_solution_tried_before + template_was_solution_tried_before, response_format_was_error_seen_before, response_format_was_solution_tried_before from dev_gpt.options.generate.ui import get_random_employee from dev_gpt.utils.io import persist_file, get_all_microservice_files_with_content, get_microservice_path from dev_gpt.utils.string_tools import print_colored @@ -101,6 +101,7 @@ metas: file_name_s: List[str] = None, parse_result_fn: Callable = None, use_custom_system_message: bool = True, + response_format_example: str = None, **template_kwargs ): """This function generates file(s) using the given template and persists it/them in the given destination folder. @@ -148,7 +149,9 @@ metas: conversation = self.gpt_session.get_conversation( messages=[SystemMessage(content='You are a helpful assistant.'), AIMessage(content=content_raw)] ) - if len(file_name_s) == 1: + if response_format_example is not None: + file_wrapping_example = response_format_example + elif len(file_name_s) == 1: file_ending = file_name_s[0].split('.')[-1] if file_ending == 'py': tag = 'python' @@ -421,6 +424,7 @@ pytest summarized_error=summarized_error, previous_errors='- "' + f'"{os.linesep}- "'.join(self.previous_errors) + '"', use_custom_system_message=False, + response_format_example=response_format_was_error_seen_before, )['was_error_seen_before.json'] )['was_error_seen_before'].lower() == 'yes' @@ -436,6 +440,7 @@ pytest tried_solutions='- "' + f'"{os.linesep}- "'.join(self.previous_solutions) + '"', suggested_solution=_suggested_solution, use_custom_system_message=False, + response_format_example=response_format_was_solution_tried_before, )['will_lead_to_different_actions.json'] )['will_lead_to_different_actions'].lower() == 'no' if not was_solution_tried_before: diff --git a/dev_gpt/options/generate/templates_user.py b/dev_gpt/options/generate/templates_user.py index d51bb18..930c871 100644 --- a/dev_gpt/options/generate/templates_user.py +++ b/dev_gpt/options/generate/templates_user.py @@ -330,6 +330,12 @@ After thinking about the possible solutions, output them as JSON ranked from bes ) +response_format_was_error_seen_before = '''**was_error_seen_before.json** +```json +{{"was_error_seen_before": ""}} +```''' + + template_was_error_seen_before = PromptTemplate.from_template( '''Previously encountered error messages: {previous_errors} @@ -338,15 +344,18 @@ Now encountered error message: "{summarized_error}" Was this error message encountered before? Write down your final answer as json in the following format: -**was_error_seen_before.json** -```json -{{"was_error_seen_before": ""}} -``` +''' + response_format_was_error_seen_before + ''' Note that you must obey the double asterisk and triple backtick syntax from above. ''' ) +response_format_was_solution_tried_before = '''**will_lead_to_different_actions.json** +```json +{{"will_lead_to_different_actions": ""}} +```''' + + template_was_solution_tried_before = PromptTemplate.from_template( '''Previously tried solutions: {tried_solutions} @@ -356,10 +365,7 @@ Suggested solution: "{suggested_solution}" Will the suggested solution lead to different actions than the previously tried solutions? Write down your final answer as json in the following format: -**will_lead_to_different_actions.json** -```json -{{"will_lead_to_different_actions": ""}} -``` +''' + response_format_was_solution_tried_before + ''' Note that you must obey the double asterisk and triple backtick syntax from above.''' ) From 9ef6e82171598ac09b911154383786076483ccd1 Mon Sep 17 00:00:00 2001 From: Joschka Braun Date: Mon, 8 May 2023 16:45:28 +0200 Subject: [PATCH 21/67] :rocket: feat: add explicit response format for solutions --- dev_gpt/options/generate/generator.py | 4 +++- dev_gpt/options/generate/templates_user.py | 17 ++++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/dev_gpt/options/generate/generator.py b/dev_gpt/options/generate/generator.py index 044f3bc..7f08571 100644 --- a/dev_gpt/options/generate/generator.py +++ b/dev_gpt/options/generate/generator.py @@ -27,7 +27,8 @@ from dev_gpt.options.generate.templates_user import template_generate_microservi template_chain_of_thought, template_summarize_error, \ template_solve_apt_get_dependency_issue, template_pm_task_iteration, \ template_pm_test_iteration, template_suggest_solutions_code_issue, template_was_error_seen_before, \ - template_was_solution_tried_before, response_format_was_error_seen_before, response_format_was_solution_tried_before + template_was_solution_tried_before, response_format_was_error_seen_before, \ + response_format_was_solution_tried_before, response_format_suggest_solutions from dev_gpt.options.generate.ui import get_random_employee from dev_gpt.utils.io import persist_file, get_all_microservice_files_with_content, get_microservice_path from dev_gpt.utils.string_tools import print_colored @@ -412,6 +413,7 @@ pytest task_description=self.microservice_specification.task, test_description=self.microservice_specification.test, all_files_string=all_files_string, + response_format_example=response_format_suggest_solutions, )['solutions.json'] ) diff --git a/dev_gpt/options/generate/templates_user.py b/dev_gpt/options/generate/templates_user.py index 930c871..51867f5 100644 --- a/dev_gpt/options/generate/templates_user.py +++ b/dev_gpt/options/generate/templates_user.py @@ -294,6 +294,15 @@ Only output the apt-get-packages.json file. ) +response_format_suggest_solutions = '''**solutions.json** +```json +{{ + "1": "", + "2": "<2nd best solution>" +}} +```''' + + template_suggest_solutions_code_issue = PromptTemplate.from_template( '''General rules: ''' + not_allowed_function_string + ''' @@ -320,13 +329,7 @@ Note that any changes needed to make the test pass must be written under the con After thinking about the possible solutions, output them as JSON ranked from best to worst. Like this: -**solutions.json** -```json -{{ - "1": "", - "2": "<2nd best solution>" -}} -```''' +''' + response_format_suggest_solutions ) From cf671ffa8ce56fc1083af53eab1199189351f3f4 Mon Sep 17 00:00:00 2001 From: Joschka Braun Date: Tue, 9 May 2023 09:42:09 +0200 Subject: [PATCH 22/67] :rocket: feat: ensure json loads --- dev_gpt/options/generate/templates_user.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/dev_gpt/options/generate/templates_user.py b/dev_gpt/options/generate/templates_user.py index 51867f5..edb8371 100644 --- a/dev_gpt/options/generate/templates_user.py +++ b/dev_gpt/options/generate/templates_user.py @@ -289,7 +289,7 @@ The output is: {{"packages": [libgl1-mesa-glx]}} ``` Note that you must not output the content of any other files like the Dockerfile or requirements.txt. -Only output the apt-get-packages.json file. +Only output the apt-get-packages.json file. Ensure the response can be parsed by Python json.loads ''' ) @@ -329,7 +329,8 @@ Note that any changes needed to make the test pass must be written under the con After thinking about the possible solutions, output them as JSON ranked from best to worst. Like this: -''' + response_format_suggest_solutions +''' + response_format_suggest_solutions + ''' +Ensure the response can be parsed by Python json.loads''' ) @@ -348,7 +349,7 @@ Was this error message encountered before? Write down your final answer as json in the following format: ''' + response_format_was_error_seen_before + ''' -Note that you must obey the double asterisk and triple backtick syntax from above. +Note that you must obey the double asterisk and triple backtick syntax from above. Ensure the response can be parsed by Python json.loads ''' ) @@ -369,7 +370,7 @@ Will the suggested solution lead to different actions than the previously tried Write down your final answer as json in the following format: ''' + response_format_was_solution_tried_before + ''' -Note that you must obey the double asterisk and triple backtick syntax from above.''' +Note that you must obey the double asterisk and triple backtick syntax from above. Ensure the response can be parsed by Python json.loads''' ) From 82e1e88ea36976226981c852e8cc401a73fa32d0 Mon Sep 17 00:00:00 2001 From: Joschka Braun Date: Tue, 9 May 2023 10:09:22 +0200 Subject: [PATCH 23/67] :rocket: fix: don't output docker --- dev_gpt/options/generate/templates_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev_gpt/options/generate/templates_user.py b/dev_gpt/options/generate/templates_user.py index edb8371..2673cae 100644 --- a/dev_gpt/options/generate/templates_user.py +++ b/dev_gpt/options/generate/templates_user.py @@ -288,8 +288,8 @@ The output is: ```json {{"packages": [libgl1-mesa-glx]}} ``` -Note that you must not output the content of any other files like the Dockerfile or requirements.txt. Only output the apt-get-packages.json file. Ensure the response can be parsed by Python json.loads +Note that you must not output the content of any other. Especially don't output the Dockerfile or requirements.txt. ''' ) From 335518f848e0f6aeb545a3777ea861a9c29d8ee9 Mon Sep 17 00:00:00 2001 From: Joschka Braun Date: Tue, 9 May 2023 10:25:29 +0200 Subject: [PATCH 24/67] :rocket: fix: extraction --- dev_gpt/options/generate/generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev_gpt/options/generate/generator.py b/dev_gpt/options/generate/generator.py index 7f08571..b5f247a 100644 --- a/dev_gpt/options/generate/generator.py +++ b/dev_gpt/options/generate/generator.py @@ -170,7 +170,7 @@ metas: ```''' content_raw = conversation.chat( - 'You must add the content' + (f' for `{file_name_s[0]}`' if len(file_name_s) == 1 else '') + + 'Based on your previous response, only output the content' + (f' for `{file_name_s[0]}`' if len(file_name_s) == 1 else '') + '. You must wrap any file in triple backticks at the beginning and end of it. Like this:\n' + file_wrapping_example ) From d474082964b6d7221a017e41061f1b64637efff3 Mon Sep 17 00:00:00 2001 From: Joschka Braun Date: Tue, 9 May 2023 14:40:45 +0200 Subject: [PATCH 25/67] :rocket: fix: ci --- .github/workflows/ci.yml | 2 +- dev_gpt/options/generate/generator.py | 2 +- dev_gpt/options/generate/templates_user.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3b71b3..abfc233 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: strategy: fail-fast: false matrix: - group: [0, 1, 2, 3, 4, 5] + group: [0, 1, 2, 3, 4] steps: - uses: actions/checkout@v2 - name: Set up Python 3.8 diff --git a/dev_gpt/options/generate/generator.py b/dev_gpt/options/generate/generator.py index b5f247a..50057d8 100644 --- a/dev_gpt/options/generate/generator.py +++ b/dev_gpt/options/generate/generator.py @@ -171,7 +171,7 @@ metas: ```''' content_raw = conversation.chat( 'Based on your previous response, only output the content' + (f' for `{file_name_s[0]}`' if len(file_name_s) == 1 else '') + - '. You must wrap any file in triple backticks at the beginning and end of it. Like this:\n' + + '. Like this:\n' + file_wrapping_example ) content = parse_result_fn(content_raw) diff --git a/dev_gpt/options/generate/templates_user.py b/dev_gpt/options/generate/templates_user.py index 2673cae..e020faa 100644 --- a/dev_gpt/options/generate/templates_user.py +++ b/dev_gpt/options/generate/templates_user.py @@ -288,7 +288,7 @@ The output is: ```json {{"packages": [libgl1-mesa-glx]}} ``` -Only output the apt-get-packages.json file. Ensure the response can be parsed by Python json.loads +Only output content of the apt-get-packages.json file. Ensure the response can be parsed by Python json.loads Note that you must not output the content of any other. Especially don't output the Dockerfile or requirements.txt. ''' ) From 8a1b49281c8c23a5184608de4dfeaff0281e431e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Thu, 11 May 2023 09:35:04 +0200 Subject: [PATCH 26/67] =?UTF-8?q?=E2=9E=B0=20feat:=20avoid=20loops?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev_gpt/options/generate/generator.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/dev_gpt/options/generate/generator.py b/dev_gpt/options/generate/generator.py index 277986a..22fde4a 100644 --- a/dev_gpt/options/generate/generator.py +++ b/dev_gpt/options/generate/generator.py @@ -7,6 +7,7 @@ from typing import Callable from typing import List, Text, Optional from langchain import PromptTemplate +from langchain.schema import SystemMessage from pydantic.dataclasses import dataclass from dev_gpt.apis import gpt @@ -24,12 +25,10 @@ from dev_gpt.options.generate.templates_user import template_generate_microservi template_solve_pip_dependency_issue, template_is_dependency_issue, template_generate_playground, \ template_generate_function, template_generate_test, template_generate_requirements, \ template_chain_of_thought, template_summarize_error, \ - template_solve_apt_get_dependency_issue, template_pm_task_iteration, \ - template_pm_test_iteration, template_suggest_solutions_code_issue, template_was_error_seen_before, \ + template_solve_apt_get_dependency_issue, \ + template_suggest_solutions_code_issue, template_was_error_seen_before, \ template_was_solution_tried_before, response_format_was_error_seen_before, \ response_format_was_solution_tried_before, response_format_suggest_solutions -from dev_gpt.options.generate.ui import get_random_employee - template_solve_apt_get_dependency_issue from dev_gpt.utils.io import persist_file, get_all_microservice_files_with_content, get_microservice_path from dev_gpt.utils.string_tools import print_colored From 9931f97dde9cc4e4a2bd89a0e9a47ce7cda50480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Thu, 11 May 2023 09:48:00 +0200 Subject: [PATCH 27/67] =?UTF-8?q?=E2=9E=B0=20feat:=20avoid=20loops?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev_gpt/options/generate/generator.py | 2 +- test/integration/test_generator.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/dev_gpt/options/generate/generator.py b/dev_gpt/options/generate/generator.py index 22fde4a..22aabe7 100644 --- a/dev_gpt/options/generate/generator.py +++ b/dev_gpt/options/generate/generator.py @@ -7,7 +7,7 @@ from typing import Callable from typing import List, Text, Optional from langchain import PromptTemplate -from langchain.schema import SystemMessage +from langchain.schema import SystemMessage, AIMessage from pydantic.dataclasses import dataclass from dev_gpt.apis import gpt diff --git a/test/integration/test_generator.py b/test/integration/test_generator.py index e03f3ce..f4a8f7a 100644 --- a/test/integration/test_generator.py +++ b/test/integration/test_generator.py @@ -70,6 +70,26 @@ def test_generation_level_2(microservice_dir, mock_input_sequence): ) assert generator.generate() == 0 +@pytest.mark.parametrize('mock_input_sequence', [['y', 'https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png']], indirect=True) +def test_generation_level_2_svg(microservice_dir, mock_input_sequence): + """ + Requirements: + coding challenge: ✅ + pip packages: ✅ + environment: ❌ + GPT-3.5-turbo: ❌ + APIs: ❌ + Databases: ❌ + """ + os.environ['VERBOSE'] = 'true' + generator = Generator( + "Get a png as input and return a vectorized version as svg.", + str(microservice_dir), + 'gpt-3.5-turbo' + ) + assert generator.generate() == 0 + + @pytest.mark.parametrize('mock_input_sequence', [['y', 'yfinance.Ticker("MSFT").info']], indirect=True) def test_generation_level_3(microservice_dir, mock_input_sequence): """ From 6201b4a94265d0aa1143ce489341e85bf4994b87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Sat, 13 May 2023 21:34:15 +0200 Subject: [PATCH 28/67] =?UTF-8?q?=E2=9E=B0=20feat:=20avoid=20loop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev_gpt/options/generate/templates_user.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/dev_gpt/options/generate/templates_user.py b/dev_gpt/options/generate/templates_user.py index a726b17..45ad093 100644 --- a/dev_gpt/options/generate/templates_user.py +++ b/dev_gpt/options/generate/templates_user.py @@ -37,10 +37,15 @@ The executor name must fulfill the following criteria: - only consists of lower and upper case characters - end with Executor. -The output is a the raw string wrapped into ``` and starting with **name.txt** like this: +The name is witten in the following format: **name.txt** ``` -PDFParserExecutor + +``` +Example for: "Get a png as input and return a vectorized version as svg.": +**name.txt** +``` +PngToSvgExecutor ```''' ) From 2e3431e667df5dd543ea8e6ddba29119c9250242 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Sat, 13 May 2023 21:43:15 +0200 Subject: [PATCH 29/67] =?UTF-8?q?=E2=9E=B0=20feat:=20avoid=20loop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev_gpt/options/generate/templates_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev_gpt/options/generate/templates_user.py b/dev_gpt/options/generate/templates_user.py index 45ad093..3f6a07f 100644 --- a/dev_gpt/options/generate/templates_user.py +++ b/dev_gpt/options/generate/templates_user.py @@ -37,7 +37,7 @@ The executor name must fulfill the following criteria: - only consists of lower and upper case characters - end with Executor. -The name is witten in the following format: +Your response must exactly match the following block code format (double asterisks for the file name and triple backticks for the file block): **name.txt** ``` From 1b0c2d54612e025951d002282d456a78e1324d8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Mon, 15 May 2023 10:54:32 +0200 Subject: [PATCH 30/67] =?UTF-8?q?=F0=9F=8C=90=20feat:=20enable=20cors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev_gpt/options/generate/pm/pm.py | 2 +- dev_gpt/options/generate/static_files/gateway/custom_gateway.py | 1 + dev_gpt/options/generate/static_files/gateway/requirements.txt | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/dev_gpt/options/generate/pm/pm.py b/dev_gpt/options/generate/pm/pm.py index 7f3dfa5..65a7d43 100644 --- a/dev_gpt/options/generate/pm/pm.py +++ b/dev_gpt/options/generate/pm/pm.py @@ -61,7 +61,7 @@ Description of the microservice: context, microservice_description, condition_question='Does the microservice send requests to an API?', - question_gen='Generate a question that asks for the endpoint and an example of a request and response when interacting with the external API.', + question_gen='Generate a question that asks for the endpoint of the external API and an example of a request and response when interacting with the external API.', extension_name='Example of API usage', post_transformation_fn=translation(from_format='api instruction', to_format='python code snippet raw without formatting') ) diff --git a/dev_gpt/options/generate/static_files/gateway/custom_gateway.py b/dev_gpt/options/generate/static_files/gateway/custom_gateway.py index 766a6c4..c84e9f9 100644 --- a/dev_gpt/options/generate/static_files/gateway/custom_gateway.py +++ b/dev_gpt/options/generate/static_files/gateway/custom_gateway.py @@ -79,6 +79,7 @@ class CustomGateway(CompositeGateway): f'Please, let http port ({http_port}) be 8080 for nginx to work' ) kwargs['runtime_args']['port'][http_idx] = 8082 + kwargs['cors'] = True super().__init__(**kwargs) # remove potential clashing arguments from kwargs diff --git a/dev_gpt/options/generate/static_files/gateway/requirements.txt b/dev_gpt/options/generate/static_files/gateway/requirements.txt index 91d328c..a5ab956 100644 --- a/dev_gpt/options/generate/static_files/gateway/requirements.txt +++ b/dev_gpt/options/generate/static_files/gateway/requirements.txt @@ -1,3 +1,4 @@ streamlit==1.16.0 +altair==4.2.2 extra-streamlit-components==0.1.55 jina==3.15.1.dev14 \ No newline at end of file From e9f8ff41ca73a4d2bc91688562b79ea07688ad3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Mon, 15 May 2023 13:50:00 +0200 Subject: [PATCH 31/67] =?UTF-8?q?=F0=9F=8C=88=20docs:=20example=20rainbow?= =?UTF-8?q?=20tweet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rainbow_tweet/chrome_extension/content.js | 77 +++++++++ .../rainbow_tweet/chrome_extension/jina.png | Bin 0 -> 59591 bytes .../rainbow_tweet/chrome_extension/logo.png | Bin 0 -> 30817 bytes .../chrome_extension/manifest.json | 27 +++ .../rainbow_tweet/chrome_extension/popup.css | 37 +++++ .../rainbow_tweet/chrome_extension/popup.html | 31 ++++ .../rainbow_tweet/chrome_extension/popup.js | 35 ++++ .../rainbow_tweet/chrome_extension/styles.css | 84 ++++++++++ examples/rainbow_tweet/example_call.bash | 1 + .../0_gpt_3_5_turbo/__init__.py | 0 .../0_gpt_3_5_turbo/v1/Dockerfile | 29 ++++ .../0_gpt_3_5_turbo/v1/__init__.py | 15 ++ .../0_gpt_3_5_turbo/v1/apis.py | 23 +++ .../0_gpt_3_5_turbo/v1/config.yml | 5 + .../0_gpt_3_5_turbo/v1/microservice.py | 37 +++++ .../0_gpt_3_5_turbo/v1/requirements.txt | 4 + .../0_gpt_3_5_turbo/v1/test_microservice.py | 22 +++ .../0_gpt_3_5_turbo/v2/Dockerfile | 29 ++++ .../0_gpt_3_5_turbo/v2/__init__.py | 15 ++ .../0_gpt_3_5_turbo/v2/apis.py | 23 +++ .../0_gpt_3_5_turbo/v2/config.yml | 5 + .../0_gpt_3_5_turbo/v2/microservice.py | 41 +++++ .../0_gpt_3_5_turbo/v2/requirements.txt | 4 + .../0_gpt_3_5_turbo/v2/test_microservice.py | 22 +++ .../0_gpt_3_5_turbo/v3/Dockerfile | 29 ++++ .../0_gpt_3_5_turbo/v3/__init__.py | 15 ++ .../0_gpt_3_5_turbo/v3/apis.py | 23 +++ .../0_gpt_3_5_turbo/v3/config.yml | 5 + .../0_gpt_3_5_turbo/v3/flow.yml | 20 +++ .../0_gpt_3_5_turbo/v3/gateway/Dockerfile | 14 ++ .../0_gpt_3_5_turbo/v3/gateway/__init__.py | 0 .../0_gpt_3_5_turbo/v3/gateway/app.py | 58 +++++++ .../v3/gateway/app_config.toml | 4 + .../0_gpt_3_5_turbo/v3/gateway/config.yml | 5 + .../v3/gateway/custom_gateway.py | 154 ++++++++++++++++++ .../0_gpt_3_5_turbo/v3/gateway/nginx.conf | 62 +++++++ .../v3/gateway/requirements.txt | 4 + .../0_gpt_3_5_turbo/v3/microservice.py | 45 +++++ .../0_gpt_3_5_turbo/v3/requirements.txt | 4 + .../0_gpt_3_5_turbo/v3/run_flow.py | 5 + .../0_gpt_3_5_turbo/v3/test_microservice.py | 22 +++ .../__init__.py | 0 .../rainbow_tweet/microservice/__init__.py | 0 examples/rainbow_tweet/microservice/name.txt | 1 + .../microservice/strategies.json | 5 + examples/rainbow_tweet/screenshot.png | Bin 0 -> 86323 bytes 46 files changed, 1041 insertions(+) create mode 100644 examples/rainbow_tweet/chrome_extension/content.js create mode 100644 examples/rainbow_tweet/chrome_extension/jina.png create mode 100644 examples/rainbow_tweet/chrome_extension/logo.png create mode 100644 examples/rainbow_tweet/chrome_extension/manifest.json create mode 100644 examples/rainbow_tweet/chrome_extension/popup.css create mode 100644 examples/rainbow_tweet/chrome_extension/popup.html create mode 100644 examples/rainbow_tweet/chrome_extension/popup.js create mode 100644 examples/rainbow_tweet/chrome_extension/styles.css create mode 100644 examples/rainbow_tweet/example_call.bash create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/__init__.py create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v1/Dockerfile create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v1/__init__.py create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v1/apis.py create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v1/config.yml create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v1/microservice.py create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v1/requirements.txt create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v1/test_microservice.py create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v2/Dockerfile create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v2/__init__.py create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v2/apis.py create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v2/config.yml create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v2/microservice.py create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v2/requirements.txt create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v2/test_microservice.py create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/Dockerfile create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/__init__.py create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/apis.py create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/config.yml create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/flow.yml create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/gateway/Dockerfile create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/gateway/__init__.py create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/gateway/app.py create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/gateway/app_config.toml create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/gateway/config.yml create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/gateway/custom_gateway.py create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/gateway/nginx.conf create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/gateway/requirements.txt create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/microservice.py create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/requirements.txt create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/run_flow.py create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/test_microservice.py create mode 100644 examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/__init__.py create mode 100644 examples/rainbow_tweet/microservice/__init__.py create mode 100644 examples/rainbow_tweet/microservice/name.txt create mode 100644 examples/rainbow_tweet/microservice/strategies.json create mode 100644 examples/rainbow_tweet/screenshot.png diff --git a/examples/rainbow_tweet/chrome_extension/content.js b/examples/rainbow_tweet/chrome_extension/content.js new file mode 100644 index 0000000..32fb575 --- /dev/null +++ b/examples/rainbow_tweet/chrome_extension/content.js @@ -0,0 +1,77 @@ +console.log('Twitter Rewrite: Content script loaded'); +let openai_api_key = ''; + +// Get OPENAI_API_KEY from chrome storage +chrome.storage.sync.get({ + openai_api_key: '' +}, function(items) { + openai_api_key = items.openai_api_key; +}); +let observer = new MutationObserver((mutations) => { + console.log('Twitter Rewrite: DOM mutation detected'); + // For each mutation + mutations.forEach((mutation) => { + // If nodes were added + if (mutation.addedNodes) { + mutation.addedNodes.forEach((node) => { + // If the added node (or its descendants) contains a tweet + let tweets = node.querySelectorAll('[data-testid="tweetText"]'); + tweets.forEach((tweet) => { + // If the tweet doesn't already have a modify button + if (!tweet.querySelector('.modify-button')) { + // Create new button + let button = document.createElement('button'); + if (openai_api_key === '') { + button.innerText = 'Set OPENAI_API_KEY by clicking the extension icon'; + button.disabled = true; + } else { + button.innerText = '🦄'; + button.disabled = false; + } + button.className = 'modify-button'; + + // Add event listener for button click + button.addEventListener('click', function() { + // Send tweet to API + let originalTweet = tweet.innerText; + this.disabled = true; + this.innerText = 'Loading...'; + fetch('https://gptdeploy-61694dd6a3.wolf.jina.ai/post', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'accept': 'application/json' + }, + body: JSON.stringify({ + "data": [{"text": JSON.stringify({ + "tweet": originalTweet, + "OPENAI_API_KEY": openai_api_key + }) }] + }) + }) + .then(response => response.json()) + .then(data => { + let modifiedTweet = JSON.parse(data.data[0].text).positive_tweet; + let rainbowTweet = Array.from(modifiedTweet).map((char, i) => + `${char}` + ).join(''); + + // Create a new element node to contain the HTML + let newTweet = document.createElement('span'); + newTweet.innerHTML = rainbowTweet; + // Replace the old text node with the new element node + tweet.replaceWith(newTweet); + }); + }); + + // Inject button into tweet + tweet.appendChild(button); + } + }); + }); + } + }); +}); + +// Start observing the document with the configured parameters +observer.observe(document.body, { childList: true, subtree: true }); diff --git a/examples/rainbow_tweet/chrome_extension/jina.png b/examples/rainbow_tweet/chrome_extension/jina.png new file mode 100644 index 0000000000000000000000000000000000000000..6fe6d80f993cd4ff2472161705ba5e907ea39c41 GIT binary patch literal 59591 zcmeEui9b~P|NqfmNQ+QHLXe0d4+8(E5%B9OBJ0Z47ljOK`0fqiP~F9K^hwm0hPOfI9-P}dl>hmS zt=x)BT(aPs&l(9%`;%zTZq|pcJ_yBNJ#p=vA{k8pJ>_TtnQYUZo_W#qs{xR+k0)G(r zgTNmI{vhxNfjQeOTfeW`=iaD!i)|Dhx~?q_A5V;S8Xms9@n#&?agWS& zUQUN0R$KkiHPiEU2HpyprE88qx@^n_Bcvh_G8DRUbvE z2yE~1v{hfylju$2Fe(Qi> zp)DwW?;T8e*6+xv{N_@z^~IX!(afnApQ)M)UyGDK?6KTEFR>+D_06|FZv{F$^IHd! z*^@!Ew!Y6g7P!}QJf5?@^ALsCIi%^v`0lqhB=P&*I>Fw#^T3$#=(5MMPUdUfTTbIu zq>Ep<*6Ce(Y|&C;rTKYu-obnNYo1%eYECnaHU-{Z9vzSJ=c$&7dc_|6O8$VC!dO@{ zMG)i|X4>px0Rz`7oA+5)Ayi*gO(AYZ98Eqj2%N^tmLt=9%Qx$kLnWd*W5ohD6H>-^rIFVq3aAy=vcaty8w}^XfIvJb5|x zN=u)$DGGggTOg@=7M;1~Es_7cRPVeTW92y8NX`z*eNC=)LFTKG@<$C9lq9mA8^QiU zZN}@elzjBgr#xh0dbQWf(^(9`@zEpcSi7RaKqqiqqV5ZsH1CeBhu(e;Sq}WtnDzPb zHR*mPQ$?Nd=Hrx`>7MW1oK@-Ynd7*6Wjc$5k<-cBpIi6}GhK7wi@Qj9N;llpvj51( zHB*U*=c+e7MxT6{R*PspNx7YVjPPZ20i?XnFbh5*T>dZvxve0w`P9~ZOVNb1x60`D z2jF&r)cjQ5h}hO!X?|4zq(UcA$r(Sw6Ci?10U8Wr%MJn$)LU#trS`*b@(VG~TQ9e- zUey;(ly?p3*OxD%2yj%Y^3LHb9PRR)JDHJnf8kO?L3F(Z3+1+I;p2~DZ==_yt$&=% zSX^IbFa!RNY!;{7f5ixwm$x?T=>dW`>(j}0?`@{dM{?*X_jzwNpnbXTduAWYRZPox z6&v!#Luc(N#n0t?5Bh9;`qD_U{&F&--{{Gkk{s_2N|Ks%U1`X8lDXN(Q_YXNlbi9; zg7_*RkGZfw`F~0`k1#^Q4Sd%T-x3J|jmdy#08pzTPT3Cw=-6AFeIJDCBl-sVqN* z@f^LWW#?7<<<9MlR(19G3ozXO#(j{fV)R=Zqm%$%jFF(!Zjd@9vRl>j`o}6l`fiw+-zFsr-RlOW9Oez z%oKU^wrtRgtAK1kkK|@EI4{R6)6HL(LvcX)!zVF~RV|6aTH6mHYjjlAs((6Q{}%Iu zptM|3?MWSc3{ppmp(d8KS-6>KQAjSPWl$hhEndP z9}-1)YC1Re^8|2WRIi{(`^tfs^cJ^vyfwtf+6e)HoYv!TO8N@34TY3QyqOrYsz0EbZXISN}| zMd0`^_<=OeM+sW`Mf^YQr*wJ0!zb>qUy%F$EH{<4Tc$pT5>4f2gM90EK5uPpcfX9Y zp6&YPQpq#&a`!KuCP%Bj`r1Aj3`juo6}UxOP=d_Q{E$qCF*~e^B)7^nkR5E(M@G8- zCA(rTuR{-R=`%!Fa8UFgX1u&{SeZ_OTg7y9>RmC#Ug+F4&`*Di4q7L0T3u2`Uu}p{ z@29v2I{tv=+T`S40SQm+E$szoDeV#p(SWUOoUwkzX_Z|sruMm_S8pVSfR z`TW*6_1)zPXnKoyg7s5QtM2t#*nuN#6t3mBKa1#>eLip*CoU;o5aC8iHt5ko!yYXz zI%KP!y{p1!s}Z>^ibJNpeG=^uP(n()GtzC#T;<0?n|2bV&%{R(ch?nBto6Gzb-YC4 zzEc*vv0~JEK#NSB3NgxOLx3Hc}9!qWU=Vm)2C49AP zCQR;4 z5ZLqkDRN#|izc9TxM-33sj?fqBWx6vMeyU)9&;vWg_!SVLacb6U@(z&q_oI{C4>;+E!k;aJ0(v7ANx2bojTfTetd8 zQmATP;5qFcS^qjTad{CS;BCNgZ!<23Ci`ur@>Zpb9gUO zI`;m0{e0$@#}vVG^y~V^VeUE7u>o@&&zFfzWe-o=6ur@0Uv1M#=)n+jbq)x zBU>ZBh`Onwn^yc@q<+Z#h26hje``a|JEo$DyB`+)s9t8PESSPc`nb+OHlmgW@Y541aKV8ZW4Ph2`F8?1*-svzg5S1?2+= zm>tXS9^X-@s^uc7cw*|n745xy?@T*1WrQ?;S<#A^DNRnDDJdIRAjT;v`Xy}0%*tsk zb$mQ#v!+&+vt`4XhwVfOeD*``;P*)d2e#U-U&?_9FI1?2vLa45dgGD%GDZ38{$GFS zB-J0nh<9Y}LvkA0 z>fRzP72UlbYcUVGU0SS3P6RqiOuw+aeI8|=&CQ975${A>9Qehl{0eP-iBy8}AxzuV zwVvPkTna6dG6VwlTYECsi0%3HyvbYtaAM9+{;gWSan~ITw1{De>VHEY!Ofi#Ei@T( zXloMpd&0QCNlrj71ZZmzgOK=lcsP7IeP+G?&iPB2LR)jvqB|2Mpspumoh=aiNg?yq z>!NJXFY(YRjdNKGHK8MWkQ3QSD+>gHE&CL2n|ESQ)gQ#PneU!o7}BGZSdr%yfA!n^ zL(%Rn`nzy}6<-7_L$!W@@po+_777w!Mz>WQP``=BSspz3QzaYEwEqbHxKjGlhH2b? zEO>>r%{tDB?90QTxc?)z*$^Y3pr{ckA%i&bAGux}su5h=DXsIUrxpjdJhV@RRO<{d zl1_iQq4b+R8kU}M%JB2`QN)SgC4c>4toZAC_kXYJ`~#2*Y3cXtSs2t$!|XTi|IS`F zw#fSdjC5ww%E#ZI|GFsjQ%@i_^glp&ScJlc75JR{uf3a+x&Zp6UwOAB)RhkN@+V^#S1`gxt| z-=(%2A|ixf_@{|o|KSsXkTL|(u-_nNqviJ-j*gvHwXmLD6GXA$-vg^^?ZH2R^kR*%x%Wx2caXpkD|^A%iEql%{x&~e$RCkQrkZ_ z>^gIPPxcC+V}t-wQG?mgd3p1tvzL*a1XiJyi^<{V$V|;YVxEn8-)vi-g3^`m5IGh2>v<6Fl)(`^U+I%{e76Brs;I#B`b)uW0Sr`H(->#Z-1{*hn(cZ5ntXps9#E zD6z8azIABR@Gwgnb*=C^7=^xC&+-m;Q3|Wb;fkwcuZl^_w+Wa0x%)EbSu4y-G>87Z zFLp_+AWTS05jTArrSR_pd{;>~I$AmM3^(32w{Ws%=;ane%?gWAHBi)+?!3Q*vCl|y z^ZBs4==E7_Ym6Fwn6y$;dml*CSD6(a*4v^Cz0~Sulnn;Ff)0IORoSgs@vesM(bxkK z3n(Qf0dA||QYInku^;Xc6dyLEJHB4&rjHQdK5Zvq{M3R{g37PcGRG3;?$lnwNLss) z+S~)SD2lUQmGSE3K1**>Jdv5`E>3Ky|}xXHMkgh}Y1|t;=PHw3|FG=r%bhG7EDzI;+gyfYX7{ zKvCqxp{RZi-g;)t-J+zGNJ2X0y2B~+DicmUGMNdZLL@eQOHHzmpv~9%oU@8_b(b9 zXr}aHDCCB2lMbee4yoDSJHjc@#~HhY;PE$K8#Wya!LpWWFd1{Snc{S?$LY?L2u9UScDu`{S87QlezC!ruw~1bdIdQ<_P86ve4G`b=Dp?c0(8A&+uSR(@*F zZT$Y*aKm+0jCvYHygZ2O8(=c-p-|dlvO2tO!GXmj!)-SUCNsE=5hVU)Knuns%*cLZ zPf>gEQ>;0KF#iIaOCWZ#zjIo7bu0?e#=S(GE{&kz@a^Q_+ckxUNp33H%1l^(OUnAKZ!@4?OkNCWj&nYQKPRsF4k5xtMU$X7yd z+w)urwlB>=SoJMqK4$fLIc4uTY2{?&IZJnYC& zGlSXvrwMsm4?NtHW+8h!8BXs?2`fMSBucXf1(K z_ZFmBEApmdzA2IizA@2Gkl3m?4{7VyV(FQ#atG(dpx|IOMp4*Z|8T&SgCnY6rq%x- z(kunPIpA28qxuxP)pL`sd>EQ43P^IhmwoOe{+oU+;t)J%8#`@rcEwL+`nYo2}K zB!1l5)0i%8fgu>X)tr)UesG)-y?vgnk*#q%qkG3 zSSTk^Vsj}JJh0UyUK||!p4oaS)Hp^ULa}fuirAeMf#51l+F~Gjd%7X{(MZ2k=+48) zf*ZbZ#AhJvWy(IeVk&HGhm*PUCg;0oG)Ncp#~i4ECM;PE?JQ2KBLsf!bo($}~Z zb=a!IX5OY``UK@x!Rh0@F8Y?9m%F?&h~p@;@D`*zP_DlB z_6p&)`3#YsNh{$rKg#UGkREUK)>aK2&jACQ%^N(MQlxgmY0JEoR8Wyudntvs{M*BJ zLl4~DedL#V znuT9aswg?3*o5Ra*$G{{e(C3@a(MrbvO^JGQc3lKILQn=+dk0R7~oXdI`Q{YVb=CS zutX8-?zCL(wwG9AuBap82}ieh2p)kW5e)9N6+yXnbpr>5V#?IKT`48Oq-RfBNgSPu zuS+5Eq@B8oubpoHS`Oz!Hi8EwhyCn$Z@E`jR^Bhg2vjIW_xJML9+{eb+RR6R(bnd+ zx%(yECSzdg5?I;pEoRRX~@SIgEf3%D2IXw)cZF`QYnFz{io{BMa5`)a?tS? zjb7d(GqgZ)57ZmbGNfWTZ+P2d26N}Uf@W{;$n88g*F8n(pnrR}Bjf%uiQBV=`0S9R zcV!$U?XP)_C>$;*RD4>S-3N{mfU%pqXl0{{!TaJ~4NF(~)1(qgm_X8pgrqlo2QSR# zff|I{r7YJyBT<442=6D;!HtkqLWu@Q68XV-rNBS0|K;t)<%eJ_${vrO?ihHKM|?H+ zaxHXdU~Bd>q4f_JV?U(ry6LLhM85Z+io;Gzh2+1y^24q2>H_*PYU>p%Xd|HgK5FmJ zf#J3@R%ZIU%?HFJh(+Gp60^av8cbw%ibc@j6SoNIhb!xPo3dV!v0$ZEWAYcTo_EMC zt41+tyEZ-wXC45yQQQrk(p_yJBHz@y`EOs_3Cv4&{!3B8h?Mwsk#sJ7sfms`#_2xM zhO%@(*WbWnmo&;+w`*wtK5G$JvA-S}6b5Qyp3jNnpxNIZY2ba7weexKb1PV0hz<_5 z&b}vgc%*q8jTr0r+dsWBL-^K`kiEg4#-^N~gSoK`MI}FngFjPW5LLY%8XK`dW{dFB z;i*Tq>V_J#MfDHlk(;ofK{V6x3zr%YZ7B5ya=dcQA&(h8vPo}j^bh|S`6!&&e7SYQ zvggg6Csv2_$zx53{^k57^R$-MGVLbMJA0Jf9+UgFkdr(;lhpQZa`p6hKT^s9&JJ-I*1on7#79(DY*Ul z|Nk8XUUO|M_lX+lesIXQnEPh?hWN?$Mw5ls5-$A=$v~2iq@cYr@T|ScAg7Qxxs34A z=^;0!&R^JAG5qFBywOtw;^bvM%6elg-=@2?gXCjuOYXMlL6F=xY9b3bgq!M26E{~& zm&=OEHdmX{C(7i~(h$~@77r$*dJyF1X>zBY9a)rhQO%D0v6PEHu&A2+WeeLr*8Q_# zf=uY9T~+u;k2n*9S?fFs`=H!)b1o`Fiz=%iJ z+LD|3gHLipC>IJZzn*-DfxeuJgc3c&V(zrSf!c|AwT`$O5}qfuiob|A2FDgkc-D2S zYv$FRO`D9XV@jK?U!EzkAS(Y;nlE(L&G+OKl5^9i%FY?QLkY6p9Lp#ZRMOfnVb;Fs zzkIyyQQ4Eu*uM(NQOnn-no@@%9ejgcG3Rl_#R=YQHbRJ1PSj4Xtu@(-xI1chuIQoa zUmbA$tUw2Se>^>a^48Wu>!E^5v&j?7q#wdP@-YG@PiF|oclPIOPB*GGNlO{<#fBL= zeySx1|2pANFv~rjqbo0(R&QLpYec34bzhzo5bP`v+qx?R9Kx2fM}=%>`7@Pg z#GDt~N8aS43sx)~(cqo{x)*wasY{;r{8IT*$9F2CDzz3 z@|hqFr3yPTzjRHvbzwG-7dFzW8k734pC`DiMQRc8D!fWbJAAfjg4A!jcr)VSTJI9?DpZi3$q`PU zI?yC5aiJ$Ix?^l(-0ndtiM6L$+>Y5s59H{KKLe=|WMG1t_Zj{7j&-rTD)>A>N> zxoc(bL@8@_OD9@E?E;%%9lEptlyloRgLA4~mt5{@>uhwhF00u&9u~GLl31_qvAlTy z2I+B}^WO#{>!v1##Km>BUD{PTp_7U@*Qt5i8+6^1%jc)%CiW7{>BX$xdwOns>>jXl zEK%$!DI_OiP6TzmK9l{{{g!u1<#iUHQtfb#Z{NG0CB+vKH8u!_dWM@b>xWsd z4SZwktEYhs9BvPm>a~-f_U*zGen59 zZx@kZs{7c`GpaP8Z^&-F;iw&s`szz0Oc5sr35naGE<=-f!~G2My=H`+%+d3nI7Xs2cf}uBjUL&q`(ZER}~U-3+zlMZdQGq?kYWXein20 z-MmTuht({1+49d_MJur{3m!*?WE2v+*EJ8N4VB%kT=x>dZ!0f5yIyRy_>jv%@Ew=c zqPD*lK?amO)MGRw1U1CM>8>)rR2!U=pf?rZ%1%_0J!LXP_sFeoSJh6|SND)PU0TzV zJx2-e{GO;`38r>FCC-VYZI>3R9^ z@XdjGrnKbG3gCz&$zV@SA2H$qK_xpu4?8ovw0!(}dZpSP57hlk9;-K`Joh{0`BKN_ zO*WB<1doWeiz7AJ7qxQZ805ry3T8tHBYPmtHv(mH{2OI$rOSfJSbyA$)FAX7EwXu6 zMTO5W7ua}P%@<#67J56~*AzgKH4KZ}rPyAaiv{1orzco}ZKJtl=f>dg6Q; zCO@gLSe*4HwsC@F#pFTrf`yiLprzEeB)bGlw4LuhW#{U$7QSp2PQRX9t#AZ_PPL9_ zR1L8jBMTEJ<{5k*{AGvbo9T{i&-+1C!U=Yu^kkJx4308-;hgxbq8C$^%+qf#JzT}@ z^b?di9t&D;X1dTom#gK2PEo7LwECbP-+Vc&TM!z2UUNO*;!<>Na3*)3c6?megXuF5 zhqDWb1GOK+6M9mGnnD!z+0w@69F5aKg5I^NsJ(3bT&vw|Nckuvsbz>*oNGN+k5V$i zCQR!a`j%ugFfns5aae&OUkbcz5x>$C;^4>wz~0R5y|flL^nP2 zQyNDg8hmbJE9~MEw|r`Op;je>yRUy@o>ouK*GtyOL|HHl@nUUTe1e(PV&i;bV(1WI zn?aJ$0Opk@GE~Pk2vacFzJkZc&sK;x?+N4Gjwpff$978!5`jAQBxE1 z>r2?$wklKueCD(a&-li)DM4{fsq<#FHX-76?ISuJ2xH;EjjBknIobvnk{f+~<=WA$@us?1o;oeK~#U4bneiL{3 z81V{;Y&ACYl{R?i=4HbMl~#>gJ#o>y8_!GrjAg^vTVm0Oh9)dKyRkF`eGT5+Ksz{G zLX>`LOab5H#XB6Ld&K+MY`0|9&`@Q!le^}9As}mkQ6RrAixm4qRw*iDV!nDQ*)$j& zALrQI9vsNW(qe5kuFdOno}NhpgQBQ`06Uwp5-68OU|gWLSLCs}6~l^V`^Ffr5S%=w zj;U@nT?%ubx-d2|Py8XE6g614z3cU>W_Yqe(?UsY{Us|6b)>YO&cD)HJ9i=rT2w&n z;MW=Kwm}zoQ|HOHj$CJu!zn@o9B8)jTZRK4<-9t%VZPermEo4{G2Ng{O?PcrdouB) z5WlVWlo`YG*EnRN1Zt^`1*slP#QN*fC-9F`>nC$SZR>0=8BzwNF~mqu{#{9zY*??K z67JlB=2hGLPv8A(&xa)tT(u;R_n&4*e0_QG-ukYw8yJ#{anr7L4?V*HUUp9C`n$oo zhv`DTV5-B~imdRkDaZR)`>%I)puT~C&4Yd&XXiWYVxY=xv7+Y05JH``SP)Kz4$|yNOJNRMs z><-AU2YUwbtk+fv_aUvm9q_cc=77*WBE=1Kt2QDB6ZZVwbaUj>BApU0BbW4ffz*?8 zk=*8<0A&RSGg(f~$JE}6Y`Zr-cI&~=Q34M*#Vp7GwaxdWo0(_mX{6a~($7{J@cM92 zJA;GcE*>jlc-7d@!5uN~o0GFc8N)nmnl6M6cq}Ej)0nWrO>bJx_~qK0vsN}~L1neO z9w>Z{1Sed**&xgAo1I1)=G|Bt65SEQ(ngj$kHWNR!L6L-@An1*aW7uUA*FHGf?m0N z{E@MjCjTXf$>xcnJt5Q3$jX`mNcoW~=3#g#NVk5oM&ocsHI>V}$fQw;b_!O&Nh z{i+D)e-S^XkQzGeBjH^pa}@&M81vk=?|e^7nT(goK^%`GlHL`{hP7{!UxM|6(Rdz= zEm4DW_OtZ zCwiVm@HAcni>~B5U#HE}?f|OCat~@wNd*9vXlYl1YYsjQ-zk8Mj@v0vXhEZlbG5lq zDh1b0G{4_V36@?iuy8AnCMI;H3uyQ9(?AXTXmv69Z?lQZFv@!6nA|nJ^0!`0S=@Hb znKoV?+Kjx_X0#?rZJWvFE^9C8M-vjdW?1dUs=2m9epZ@{=$`L)Z(s`T;8q4)pAA%B z$nm?n0^{1$K4RYWdbQu(ndW*4T*_w3POojVWe9CSTsVRBZ< zej889KjAQKbGj+(=%#6n`ta;&$GY&DcrX9*PR3_Z$+Ea;cYP9+~EDAGVCIN+Co z@1tt4EyX(j!yuz@Jt&=Th8Zp9_ar~p`H}6763Ley7-#PGb`EnFpE38(K!FrElcMF3 z6U7r@)NrcY)iHP3)Fdcwf1!Al0G4s@m0dZiU_PkN0S>H=-v#+0#mD0cKhej5Xae2p zDYYxPbs|wQ?MpsGExSZSnF4w9a;92qy+aesCRwM)JQ0V`;1$NHVBma%mQo)#G&T8; zl`7YW>8CZU<@q|yh?ii2VQr8=9#eZTQIK;WB&Y^_MHg!N-rMwkC@)yskM|3*D~M>r zap238H69DtwrcbZM7J-+42$m0nr5teGAYI%YX&}p1JiWZq%kVeosk9-+1rFAU%vkU zu+U+2`=4kOm*7K0f%AnB1-A11_gRhu=|a(We32TF?IVexG<)Z@7EG0wsnV^uo0J|4t@OB1~6QiP&n4^S|%wx??^|X?G z3+CO-fc8+S4=JGu_6GXWr~e0rzIG7)@eC%K?N!S5zS73B@!VE6#~8P((e8v^?0^xw z3ZJ1-(}}5*SA3fG*cKjez2icbz+cBCOy3|+cR9Kzg9L%HV(0>ARULkCrwj)UeWs3{ z#_J=W;KL%>T{P07`s?XI2o;Ox+;;I3^FCd0tSErrlIf`e6VQ)Spt9MYljWoqpVO|> zv#~z*IsSl2dO)R@xyup*Yr_d-cR)A?IFBI___jkPGVeB$6Qvw6ZTI?fVl#g)295e4 z1zB&F#lTxF`xB@JoV;{5z2wJqaUpvO=X3P5BI#`27B5~)rkcV#?`iiBkHwP(z*f%iG<>VO1MQU{VJKmyB;JYP-#ig68Vg&ac zANsD%?op2;!Q9K3Hd0U@6?Se%HV7BWc#6a&Rx<#f}eJLG7(Q zNAJ5xYjRuRCdp&&KY;RGyQ)@_q^vP*R;3qJeilDsap}+EwMb?S+sG;LnmvkE@}mc* zfAU0EL)M1F$fyum9!3akINAdRpH!l_T{_3#fB6dIM~PK#9~+moLEC$T+k_80R++TO zYf@$YR?-UGR;j7JLhOG5xS8O`i&~hAU;hX1?svjiln<>5S3H*CsV|F23>!u~aS6rA z?hj}JE)q8JjW4-fx?Lzei&C<2f-Z)VEX5-1(dAFm0-bUSWph6@JWH^ejU0Zy2fEP7 zR0qzIs;^@T(}b|W3J0IY{MKqP_9YixjF>7y+`YGnS-z1LW9^Htak_-Ln|N_4jbOP0 zdae8u(t*eSLs(RJt1co!HBf<1B0G+}>Q zz0HvN4l}s1QR+fMkDkYgo}96M0?W?3<)%xQC)JGAMk~l>vqrB4iuOs92a2CXo>lh< zJn>&QqTfs9- zN&kBiK_PRUnT%V2OkP%H$xReG;kYz0+(;h_znyI>Ym0ms&t{1BT{EOS0(g9|H(mW= z8NW8OcrHBG&+cKXtM4cx5_~~ERXCrDh}8mwJ9{iqrf3FHzx%Ez(d%boXqaxq(L~e% zc8+~jb7cz5RWiv6sR=tLhH6))h{dQg8*972lbI_c6kSy1aUR_xC76 z`F(@h_?9zBOv3PK$(8QmO4`>6YDo&u+q`z^UhvtadnS&CshkjW+l#Rpt%%k|I$0FQhvHfwW9PEtq*)`v&{d-GZ3pR-@pm<3ud9W_bu-lYp|;=7Gi(0WKG zjZRsyjhDN=>lT|j|M*gi7v%?H&8!aj-bXVC`C;T^sR(^GG8W&3i_f{Db{7#Xj{2D$ z=?MlGxRJQ`0Lv!%d`Ky9(2UB|uVtB=ON$@E#lM<5m9@;cxve|Vl@p!}s^g!-;wJT( zXF`1}X~GzzfYbhC=7poAbC;}EU4}V&*E}{&mltS(;e+E^v&v)L?pyNlc zHy>3()X@$rB-CINd;|i<%n`{@*rHB+W8-9E%}>J9|9ImC6{s%GgKzK$1*|x%dyE@= zyjk8gX`JiwXhYE%`D&d4CpI(HFqP}{-bHGGQz z05y$Ya;08A6$;!pt43vDRK6>zbZBakFXHB#1NM0fVygr>Um==zV}UK#&I!uBdWZw!&>!kM9lNE9j zVy&ZQd)D+eG)OtS%S(GU&0ZFb*5Kj|Ko}VtGW$wyJln>1I>0TNN-Sw#WAruA{&RA% zJZu)x(NZ5KI)iX==b-?u0QAv#;37lA(B-jgG@swwiUF$NEhqKJ1tqs?;<)s2_ZVwJ zr!46G2JscoTSiBbcu^tXq(0MyxEuyPKa8on1yy~ayG+-tinc`7=-SRiZa$?Exn;PK zhFAYd^|K%gUjldo)X=8Hro}H2&JzA} zv#!3brI<}w8WDu)H+H++mFsAC0M1ATEq|8+kSj`5`CTq>HQmkCo)(@+Z!=F@KIi zWQzD+^E{#m&{g;+D9%s?#i$zdOdR_d4%a4L(7$$m)qB~ArhF)HaNM$+bitDcru2QU zS1%s|N*d}VRRk}LYsRyb$;sgo6W_5J=huL5mx6=>%GmAfW5bMSK`aCrGI!)KaQR^h z9l5CmYcd;6b6m2txdIdtbshj-#EGCjNoN_~n{wegRXJspp0Cp}S#v$%->*OU62L!m-_iXNd zq=Emf5AGE8z2x~>nYM+yR}eKo^LlM!3k3>LCC0&^X=SBpe(y z+JQfE5{bz@H%JmV(g)5Dh)y%%(lH`y zed2nBT9Qs62vH()^rJf*@^H~mp!WL*)758)z#C3FwhR`tXQ5m1bL)#rz`iIl@bW~p zFm-{eVya`E@@B3cGv|;4l9qQ;OIjUT?Z*>@ z>9bvly;GYz{cHq_IZN~b7NbSqh?ht$+i%>kjg6x=OgMl6ozxngpyMb0PBLniecG3t zG%F03wk~B0vq3GKoa#* zrJydmXt77`M-L59!#0dqjW8ux$T9uWLegR?jA*l|ECMDxo!^Y4ifSG)Uw4?OyDD_H zc0dz4r)u~CIzMLlphc5oDDDzZ*cEBth>=rVyI=E_ zxu@^@Z5OEL4X`6$(0K691|{Z&pm{AdcopByg~W&(gNNFKQBqX(GD4HifrIV*{rjGR zitUAqIM(UnM{!O4D$say$2%*Mi|0--kTt5^CO(#vzYot`(N(f>TJPKU0Ow+H>Cl?q7{ zTrW=vd&1kiLHaJQJy7E3xxtp_T)fGhz}L7r8!)8hf@)l6)PzJ4Ls3m8(kz z(76nzFM=HsdCd2xr(3BsgNh1AlUw;OozY+=o$w+{dXtngaPy%X@_aE8IFNQ zJo=K#&E+~lgRSwIG7=?13B#H6WsxNg&mc^tU`z=?dc=8{$@?Vx7?VgL6V%zSwd8m^2S}XoE-UTd8;>Cr_ z_quB6z@)sS<~WJKd@*&#vV!PCUzW0Z|gqBA;Ia z{!GCJe?WYE(nl*#7g%1JEAipf0u`9wqkMefBMzO% z&Us*6=p8oP=J%HsM}*OBI8JK85tHCkGWB}oXy-hQ{Ym1|_-0COo8KF-iY@L71v;wQ zEevEcu=rt45h|!fb+5JYRkJ2{KF+lOe%2`wTk`@+=Hx8|I|VJCfvK;Bu6~x2H-YN9 zeg=q6lMbtdJqtVF4sgIqJvEFjEpT!@S(MmBp@`otvfXw=0X8=9Ucs*dq>-aP z)2^(#)9_gm|EC@lJ|1kuYn}8_37zA}S=j{z2uU&^vEo41rZ9kTd2rhe9JjnYwUU{q z2JP`KCpVSv5qrA8pBs6=Y!qf>1;F0`M^m5#qp3zaC^LLG1T=RfnIQwg&Wzy+C>BH`c@r+rIL>B}e1uF@)9w z2O$rv523g=Ou$G0wu%3)0Z@%Vv3!Yk?q`?Az6J0HVMd#plXYH!+DL*EY~>4e2)f^5 zoPA{^sE;dLmdaV#4vnhNMdOd~g7FsRG98-3zyue9gVa$$7>`nJO}nbBPv_9o&j7tZ z#2x*)4LQL;gra)(zOyIJ{g0^6bK&s*Y$U9o4N`i#n6{^Dg9ci#?CzH+nSjyY{dt7d zOm(INh3GJ||D-CM%jxn06lWyKzbKMLe3h2pZ@Y1GBkO;}!Kq~v+JYIQq+6~Ny5${-xqDu}#B3RSan#EL*irsPW$wTx zP@9BXbN_DWg&SC3JV!tcZO7WgMftpg!Wb(bP{&iLZ7$-uk(mD9rwD|uA_!U#%8yLY ziRDIWY!^S-ydA1WVmzxqqFr*ZFV6!u*>|6Wg-YG_zz8jf$B zyhf|vQ!MI5g0jxZzclgz(cm%}Widl|zU_vpZ4=ef0h=x%ae;EvC&OOJIlpzYw6J>fM2Wm$rbk3YhP#N5Pw3&~+%1Y=DWhN?(}9jH`p1vJ_{ zvZrAh$LeMk>;+I_J>kKtYIKo0TA%V?Q&~{K=f&?#r5ElV{s`+f81qX^rd>8H#ue4$ zmEsJaQ~mQk07|?L4x(%nXm2w2N6I-ty0SVUQKPIl8*omZqAL1OPW}c|_c9j|Nv+(W z%DbM^mKT4{@v;NcQ!fF0QQ~Czl6_#JnSdKkhkqP(IahizNb6hkc&8mG3ts(D);m{} zp)M%N#@5l28|P^FQfODG<68-wWUFA=1uwk!P+U%@F;TBt#^beM>1{868#q|?izXAFjXvy$A+qMB!c+gRw?Y^6;*K}o0(0cAgAmbzPnF;&*LPAWL{I2{@Iat4s@E)}QN zMSU1I16$!*c_mOAql&1F%KJRmzjzGy%B7ZP;DwwHq|3zG=$5P#52`{}H(~ZJ2M@%f zQqf24rzmhHm!R`}^xXmbNI6YFWw>C5Z-=dtgxlpg5(8CtzMvAi{B-dQ?s(oo)%w`e zkU)@7dcr?qOuf!+@co2=`rXny_@vtCYE=BGLEfDI*WPzVHI;RL$6*F%2GnsB5ose* zr1vIG$3ciR5osdD0!R}n(jh1#Do6`e6tF}Pks?)UPz02!L8?L&M5G&PXvw>866R6l z`@7bE$rt@_?z#KyzJGh)9Q)r}ZCMD*on6S4WA}c(jdSqtT&t?-5G9d^P8B53 zNQ?D3=_e0GX}ox-y*=$b$>nPkFZtRpgRedP4*PLC=sVujvTNOrO3=D#74aXtf6Z8H zMnyC;+S80ax$M+E{d2JGR&Xq|_hdUsG$ZVu3NQJ_R$Zi1)2y_TW1lJ6wQfTtXtGB# zIM7d?FNpdx7@I!6Wi4Sv2lHH*&q zq@wb;5PQ1oGc@Ce=z*zoT5Mn(D(;OOajjbfIDL)|ETEG^kXjGkCewWuBT~r&Zn2>~ zGVNwyG{pXA__Z-??x6ETycP1)r>QY>cuKk&xMqwfX}Qmo+8RBm4oZ41lx~XsbbF^d zR!s+GCddqaG}BR9RdVUC4SOF#zw+C*a~|(0a@)*d0@wB$YD=mBN^Bbwbo-7KDJ&nD zI{@j@b?>zSa;|1#5{FyJa#2tF>IH~1Z2RK6)^DuHr1bGuCdhp`MLs;owZZ1bP8)Cm z4{N_%>D~yQ#&X_ut?IAjzowk`6L)xDo0p=d_1^&=`vxKO5TLLA-4lO8#^i)%I=4?v zz&{j~=ggW1=GBg*Zb(n)TTIJ_x~vboSJ(OssHN^kiLuj*z zDj9a4=nuy$f3^LorAuQWU?E_kYeL{y8r~pRVAGS~nFN2;gD5izx(LR}muY8?ed)bc z3zq~!$FnGAD$QCh{%^-PKoS2ZWN78DMN-Bx!uwMIYhiMM24?p(Q?TvnV9=t6J8F1s zdHDfQC69l13M6lXC$dM+rKxG4%oOM}x%e2;q}=vy-x_b%WdM2Xa69f|@sg9|@c-UH z15l9;(>J3J!GZEsVwK5quOP=CY~6xNxV`~OmWRh#C0o?BQHPTueOk+;42cR!vUmB5fy4fNR6>F++{OP{*9&=3@O*7(M-qX zoR#DnG4H&zv(*53ft0%BS1&-^OC*=_+E(<=OGsuEMnEA=Zba`GT#clvB20pA?OO23 z=2xiUXip6IpK0BUO1QRaJFPS^Bf|{e43wZk|B95DCz2s=y}2R55y`O` zLq$?QOD`KDyhP9rHI@HF<}w|II4}OpLtkb5A%9T#D(}iwr?(j$DknZb6iDDC%?65> zSOkZ`Un^5S6w*@op-3ZSS_@dycMF&aDy@X5wAg>1H~ExOGS9)IF~B9p7z`@(!xWry z|LC)vF#~3cN*^9&mWc^M5uh(-@WRUd&q0oh0Ly?=|GG!*2fEq2s#3H=k z!3f0_BD6PzfAO?l1DoHRwClZ1%1b7(Dljyxx+5u6S)WxJ@SBDToPaOl&f zh&K_I4vhX#jnHcKweKVSP$u$CC9x{Ynf_K<6( z@uI{w(844#{FS~_J!P$A`4sTcRz{!(sx;5Wx+>aanrIE~*GC`YhesK6a=~|`s*}^a zs2@BQmr*txs~JHLeR2AFiymHKRxhaP$<}Y`vu-q2c-PYj5E#{{K>Gs%-6C@S}VI}C?|?zeWGpaR@9CgbQ?I^ z7l%Xu58w56&2kA+_K!s6yz+JeA<6I(*y^2!n!L-b@z5g+QDzt z?1*QWqANo=E7x!?RL8*!g*tl){m+48HV79B6e#d>FRg;||@J1Nrs>3~i#WbR{6JD!iu zdPGYUx4uLw&|P$1C@i}mix0D6V%HISRd{oEw=4`F(KA?@uTEB6&tXhL6B?--cQ6t~ zXdogInXX%p#n!B@t*N3Yu|MfF?CeW{wcEG#t*fKq<81{)zoWwG=~cE%Uo360RoXiA zQUFxR|KDOH@e~g2T+e4w6%<`z=%KN@08U4bL_2Dz4K!ac?(|(W+G!(xNP5{EK_d(q z9`Vy866%hYeHmHjh~b8Sk0USy_ywfEgHWgk(;w@|`V=zgqg76PJUk7Q*f1&uf5dZ~ zdS<;2>Bnz*VX_4St5A8r(&b(DrHW?b5?tu-9bSq9mB(LO{z3x_yU# zIEaKcX_!M=o7sRa*;y)y`gcGpgJH4c^E4xLrt+`H=i*Y|5E$@mPE|xT!LSk3|FlSt zj2wOMm1@O{IlHHsgMH7?RH-8r!w7J-Yuq&Ans<}{f;8sKRIR#7^&T44FcPvhzk3sOv-RU|S=fvfBX(iFMahCO%;U(R7YJ1J= zu~Hy|URZ~JI8Dn7099D;gt>^{^+qKL{ijNL(#t2;TPdk9-;k<;YGR-!#S1=k#~-qu zDiNK%o;}z!*#N&~+{6NG-dAill;YjsJa~YpYGI8Z9D7u7PDSPXoOknPuI1G3MJ9|* zvHRSXR+H9+*WDLAFrOb%=Wh(XbMWdY^?ou!C6*2mPkD9J=%-lve0Vub)phtV+EX8a zAT5Z)1uzNYQ!INfx5^YEGlq z3osb}3oEv7MV(yV?aQN3QmE4&7NSlC1COUQPW#u}7n^hv%PaFKi`>-HLsZIGc6&%` z*Ku=9Qwx<6-_5bn;AV<%Ur9QZ<9?~Qf@GumAyn8d>N%)z*Ct_uFfs9AZ>WKND=+F3 z9k316{<4zjQ4GM zmB5vY1qCe&PrL8&28PVwZ_0f}inD$nq&!}Ge&3Ov47=4=X-+=Wm~yopA1dB_YZ^D+ zKFjW!{Rr$(>5l;@i%^ICd;x@yK#An&}p7!eV&A?$I?w_I&3zHC9F>90j z<5j18Jmz>%)zQF(do}JSuA#1 zjnF`dHc=G;G0n^6TE-On6DdDv-ch;zN7pbCqWU;k{ zhji6dJMEh1JA}Sk@@K#e*5=cVod6EO7a2DzSx{2Pz}7?GI1h#Rc`fz?mY8;0=K+oP zo&Wkr9*iLE?Y=y(;jyNT6tXi+z6P2Qq=B8s6k!x^9V#EOY-xg9Wp*Oz;K8$A9p)vy z$1SNvpkP}v4*nHtvl3VkUsT`PqDI{@To9XM1i||PtzG;b2U`GQ@EuyPPOaR6l0wqJ zxvXyAp%fX=$LhI;9%?~lSyg;g(9YzPAA;<^qx+(%YWjk=Uo8 zn5!5D(FiWSIS6T7rb$73NXgsdmYiTrfzq7QQYFQvof?(XComqxRY8q2H+0P5z%KBa z1E7fC)ci*va(M%ak2K-s;IM}kEtHoeK<{pX2(;Qro5N2JEm%D+Dx;Jm=47!HuO4w_;`(lW< zF7Y>{+Rt&CK&HCZ*oTg^s))ZN%0?UF(roWo_2Rrs?xPjAeN87zopWN=}p%lD927&U>pa;xTXDd+n<`={sJr&GcMK zmDkI$cOLjXf%b*}B71S~^PFBsggx0Vya}rtMV53wz0&9GH2YrDbR|;DpY^;=C#VNo zL3@ZnTJ|mP*cooZpNRc?eS~kazIQ8yZzpo63!DA(VHzY9@;LZ?lkzXmM&V#BzaZML zQ?KylLUs#@)P63%tExLNGo+_x3gfnq`$EeA;w;@4KcZes-`hJf^lU>s)!(jNGX zid$*E#X@w{RYyzqRmr`q(cIJ&-wC`P@qqXdWDmFHB_zRV{2D-F&5P@}P(t)o%y|Jr zL|u|VQAVpEr205 zZVX@B`J19_@K7@JWOa`ydxPCk_bWtRJPg60PhDktY*LS?*X)bX1Y>1LR)VO(Dm(~E z*pn45N%@pAKOoz^1k8R4%r;d7;m4LhOytLpvW4U=m7nA`TdM5_@O?4IQ6GH4Lr2~O zsE!_g4evohVVzs-duFZ*RUIT%jh$(2h8=QCOgpR+BW!Haz+|Z1WACP2W2y?0YY@5D zWv#=bnb{r~$lYFZv%s4f(&znhNvU|Wd^a@m|>15UeF3A5in8aW_M*!`3nAz->{^N?H%8FQiJ*YgA ztZjwLse7CIj&ncJZa(*BxGJY9-@jJ2nH3l<*PK&6p-Z~M6j3I@3q*1QKWiou^L%whA`T$415jv<;TAB1@%_;k1F%GCR( zOV+23*Y+5sxwxCK5vxd0|+W0_{R_Tv)$95|A*^=cD7{mIpH?32=7bGh?ui;EPaoxM@{jmSJe9r z<^Tiau;$%P33Zrbi8lkOp11zy%Le@N!a93c8a39hHK_6m;oFC zx87r)B%v-DX^8X9e%HQo!$+&@0K(9=2-3pE9z<2XR#?AwW;WG?Zt0%!tGw))cC z-o%6!9*HwqZoz!bfj|wZD(@g7g?oIM(*Z%Zz1HeR@jasdp+Q}kc!uwVZgV#j6?ue~ z&#&m)`tS{V+z)RJpAY2*;KE@S+lt23)-7)U5BOF`y|`4oCdgbXn0ODMfgfn-Hs#sp zdBrffvQS1TW@b?QOqQ*AD%<2AM&Q03c5Mn9svFwq2)6c(I8K#^^^eB#QJn{!ftF0x zJe%Td%yj$SX3A|DxtKyX2b1Ui&$lBCIJ_cMyWR$WE;bru-V#S+1>-?Mb7ozqIMTaa zO_>F;`sSt4D~=Tgy3pRiIe6lzz3VYE>EBsm^y^k@6J_{83f?E;wT^z|eQ-+}jV*@p)TVTI)46v_dt*H9;#0oYp|$=Z2^YV7a=_z4g?kGOKk74^N%?o+ONBP0`CYQNrHmbc55ZqR8r93T z&^XY>WCW%IlvsoDZTG{FRsqTz({mz8^RDm)wcvBka3wq9S}_NUxKnjItOqU}k; z7c&vd_}WbvUTJUF!3I&DHu%Q!JBV`FJayj7zJph8#FJ_p0=Jl=`qTsg zr8(MbDNFuDo~b^zmKhHk9Z6qsFAR>gz$mRfBpGtnH*Dv3T_~H(Wj(~NR|vXQG{#k? zQxT#Y4TthmNd+@vPvc4Y9T89~DjWG2{+xCuE<~#>|c>_Imq`>41uSV;`EmVg3CibAO$|F&Ogg1|mvBQ^qpePhQ12982#N z>q84d(swjlTtjWWSErw}9jqFd_BuVd43FP{$8R*KZRC|7@icgDcomm9d&7PAQi6|S zw;-Zg#RQCQfR~NqTDPm)7(?+E(hRZ~WIc@aEQ4Kq8$vct%IX$yD(w7MJK2t<+rOdG zpviSu&fw5*v&+P1QKePCUYKNxLWJP|BF?Ac4?L z%E{M6xH+0O5X)q=*6p6^2~*9z1=Drvbm4RaVa%uW{QEnkmi%96@8%%AyqNi2 z7~7UpIX8ih!DqR{=ugjHCb6@jQ`H|DLmj+Ma|LNTY9nl@SEuNcCfq~e{q60CDG$gW ztB-x>cZEZqGS%CEscX0q$1e=Xxk|2BW5J=6i8NzoUTkuE&V+02QX>bv|Cp9$5ML=} zF}#00UI;-5@(9@MnsZABjjfVn66^9E{Ch4o{>~NLA(M3TU6WGwh;neVE!^z)U^4p7 zXJ%}g(u!*BCpC1(@ei|ry|svxZ>(}ivMYHWa|LIo0ESnp>hndNBvF7y*mCyOWbF`h z`dS~Zxovx8AUY;W1b(v-Hw&(JC{9ObXtPQ;vF;*lqd0PgD%xkS@Dd>2wiB4h}APx>F{@`9U#8ZJ9qSF|}_0+E#SAF2KXUQ9av?X*SY1k6l2&pk)l^FLn7mTJrXF!X}3{PmP~R#FV(} z3!tYVKN-7jSVn%R^f_AY^!@Je{@zb=h&q+sonInlUgTcuJnuS~m`+B|Bw8CPXJK&* z&{;^&9TQW(BLmm@#K!w?A^)Ye4S78=cEE^91(NM=Eqq&cj#bk3g)Gq_W2{^n9N#m3 z<7R7w`O1+>{LOX)OUqUH9^`(X&QfkWz=Q!POr5#x!%QJvp(Z<+`WLIu^V>iwmxVJw6uM)JCiM9 za-O$NrIndR$=&%j(v6GUfIWO0j&_{z?oGk+VFCx2feZk)lOm_wx>J?9qTl31PO5)V zBTZ~Wy#Co0*XymUV>{RS?Si|VZatcteajYo)E_qRi}&AbDWh9pP#Y79OGto- zmRmLwNfLiQP21>hiKx1C-o3T_Y`WXt&Jr6$$#3vY;4gwa;DKqWsQ)?J+q*N{X~_ZO z`MZ1?Zc})^e+2<7|HewBvk)EKzQS(OQplef2;?zTz09VA3ldeb6Ye?oEu^z3zV zMRnwNcFG(=krlPKz`jX^0K#KivSeH%S1u4K!{4lR&yzG@JKvyEUvhE+ahCsROAFcNi6<+*#J}vGM)m%N*4NVw>8en#3dT+eai)cCP_>>9Rfwxld z^DX=?C4Be~A3KW=wyn(!&c^at@AS49yrqM+Qvt22kZGKNVCWigniY*?i|U3}fwP*V zh$JV_?qZAAn3Dm=Z*N^Ijl$*_%Dj*!_V^WDfkmHo+MJK&-LXX6p({wxZj=yB+j(3g#1>oQ9s4oH_^G z=&`Cf1CfzjxPQ-g$H@w!@}M=SjSV+|5Gx?We3a;A!~qw-jW%4a)ejK;lmIMriKp5I znB12B?i}4cJ-%qnf}-e<6;{pyT+hcxn8xPClHTW}%eaMeaS~O#tJpmn;V8w&tN7GK zU+o08d{Y~KI)I;lb7rM=fwT-PMGW)|%)Y{kO`iFJynB@iAsYEnkXxc&Fmav({lvajTN z>&lYI{C@14F$g{~aPW<_PcQQa-Z4z(mfXTM*DQ_*^SB{bq`~iUVQW>&w;TPHOxQIBlkU|U8VCXFYlAkTM5m3~V0uM_%Z7fddG8i!(ivaA z8|?@+Kwh8_VXWQl%x*Mif}La}{q3IVYzxL8s)ck;K?i zSwl5k(wHk(e*c|!ty;f@j3TH~ii0NAm?V7h&|0tel%1{85*u(} z=1;k6nBgy0CE!s5XTR~WbBm15TBSa60Yclr66uj!y{PvgnisSgZBSdPQr-;rg?mO2 zoXvQY0?Y%9J|Rhk>}czN>`xJE9si}bC4{9&+zX<4sUj9P3>Ipks9Rti2A7BT=ng)s zO1tXXZ~`KSryyHpAc^B*-FH05M56k~M$N$qZEQMZo6fyb&3$ZIY>MQ=YK9(n-=_Xvj9uAfqL-ayWQ}y7%-tDuKTHLI9xd1<9tuw``E-=?@4lT zVJaM=S>f}h{%t=+Agz_JF^Wsg<#RpdQIk!RrV}yznSEd#PN6K}t=EuS%VQfnCu0ZR z68u2972Y~h->$oZYzcvwg1}Jo&{{SCm6C^wkosI2M1j7iKZoyUY+y|JL=Mtwo8 z4@_=FYhWw}&p~+(%)C{aUqJr1A^LA@@sU!MB?D4~1GJvd8CfqhZgQ#lb*@XwQb$J% zCo#kR*7A@mDH3XIz_@OeLM_N2xfg!ya=dwfvkitZu==KtmsLo^!8Z`x;v06pRCfQk zVZeWODBfQX+_LDx{Bt38rOWOP!|x1}1M~O66|LEEl`2)T*&vBw&rN<&*9|r{{k&Gc zb*(VI(!x`mBJ+CK!XoBIB2U`!_uCshsotg=w_^)u8!9*Q{6S3a0G;xD7`ALl>JGlK zHs`$Lhny4lnc726?LA)V4Ta_CFDtp>tzzeYuPIy#cT?Ssew(Swo_p0*&k3Zm&sem! zwa%W&E_BuZg;CO+&(8r^aE*soBR%-Hk=cnv$YJxJ_Qrx*&Kip5`GTm;xZrZz;=={( zwI%6Aps>9`^)ye;DJ6>-UOAt})-=APCwv&gL8L(4wrNU#Iq4@ysn2SsmhGz*O(t&h z55HkssPB*c{np>UpI>nU#5LUD1A==E86hUX^*9n;#*Jqvd(}Clj20Um22bc5O<`Ne z=r{U#TW71ODEtgGF6C^r#=aok4f4W`1z5^dKY3DF{YYsz78fBpg%4{LKL5L|J<;Ql$xKq5cI=hsIN%$4h z)?)s4vlzUrx~7zy=43sC-V3u>vh**yXu}f;tbVP$RJ9lOqTWW>nbNSnRC2&i8B!42 zp1%fq(<^-gJdQ(cLR&Hf@0~M%!{;Xl&KE z8_bNBGQQXhSsZU>q#(b;pSBoI;w=+Uob94hTLiMQysx(Y(sXOq zsING=$RDFZ*-R=Xpw| zI|X(=hv(5q4Q4hoFP$d`&24(j-y8@D;J~)uZaur^Nt#lE`<&4x6IKD*^bHA#-X1bL z--)ZUNfm_k@w%LE5y(sp>l*}c{P^~jQIR1~0|$i{yqw6U!?RT)95c+JX4p^gyYs}_ zjIBzS^V?P=-NbgIV<7{Bn@H8NA$uLxv7kP5KO0M#G7=d&fgN!;`@HE5>y&ed>|QjX zaF?J~RG)m1>V{1-rPkIfk8m$syWUIa9JTP*3)p$#;=zilUaYtkUm^cVq6D%KRuLEiP=g_?SQ;s#&u9K!rc{gXRM&%b(8#YI95$^B3J+=GCvd zQ2i<1)XJCgpiONno$#?bGNmvQOkT3^xT>o9)>pr;1QfaV2{;h4XR7w7YaGeCTax9z zSpfa89I(^;GQULf2G5y8l7!1nCiQts1yZ|zgg8T4Qw!n>Gnhc za6^Kcl$?>2M4Y(@m6I6si^UPZu_ECr^vH(ZCA?zQfo#CT94h&&ueLH39@fAZ48GGL z;2ILCI)EvO&`??}0K1)%xRj@Y$!@nl}oizyKOeJCg1~? z=hZ|9yx+&hmmI(*ABmxklUs#{>W5gmaFFpllEFsf_90$a@qsaG9&RD$1u-6g^K${a zZpS0Y?L;uYORhz1e7rw`t~jo=y07$EC@7@?5}gArliH}iY+4{-9LOh7U*pp7O2ShH z#(lq`du~wLbG1WVz__W$IN;{@O%a5N(E-jkBFtul0MnVdTKf&hM?Hv_X8Rh{Ah;DD zRaHR%6qBhd5gG%B;$=kuvfuS_QXfDAGKs0_@hqk;AFClD?1#rpb>`Q5AH=2`#-#1U zA55$l`dx_$@V!!6hXh`w7sy|A5D|5=t0tx8jLY~fve^Et>=|C8QWgk{l;Mwf&*1c( z+gp3z-#Uyftn9eL*2!9>95jYn22*2+rMT~#5E$?tkd62L2RricMcN-}{O?%-I(g?E za`9jOcEW=;ZX^ePUh+U5VVO(xWQ4&_O_zfxgoZ%KJzN4`{ic^s$@(>Xc{aWP4&o)^ z=ijfjxq7?|Q2>mfrFPgLk%KxL+HjK(9Xpyy`t7IU+hb$CXSlSrkETG?eUT*YxQR*3 zLBElh5I5LOLI;<=nk4QKp4xJ3@-n|mK40rV+fNaBWsB_t(+iIqZo~tqy~(dNidPKBgUxxGgU?P0MegCxY-woeuNkm-0|*u& zpqOHP_o=GIOJ0R47D#UoLb3whXLzdD)oD()qbpX##+ zTw=REo0$9H-4@6lMyc4pTJZ|%La&sx7KtkA1tlgeP?6sp6@S^#aGx~ri_NJHJh#*R z`S-ZbE1rZ_>k*3+0x89O7=QQihLC=bpPY?uOkfUiY0(&0E3_(;tk2_N1{<2P&FBI4`iFbG?Vi@G}tQNO8bA6@q>oFiuwU z)0z#$%9UheJo;ukt+%IbviT`=xPN*;>2JW}kq#JnM-ikva;}wwA2BDH0}LNm@Fv!nU`oJkLNS=o6hX5DW=$UH*(p;uys%ToAx?ktcgMg9g4JyZASg zU6!IxCdh?kM~N&on(}K!S^bf_??^0Qp`i`HP)FxC!xTe?j-)M*@GDjBS()S}CPtY| zMB$XhFy;hMeIl=7{X)l?;CCD);5i zp(v_4T+v=J-$CN^@tD<%E8N4jfalkwmXWvvanxWguj&VFD@!yD<-Ff(T1Pi#Wnqj% z1*Lj?Z&+g;W`No-qK1_d>fJYyNx8$DT|{v)8^zl-;61T8WeJQ0;`7K`H%OW0lzvKm z2{saYfbtpld7N{;4Wfo9f`1rnKDM;LsZJH$tYI9eCE#+8vwnV55*yz?WDw+^VlDft zic^x^WZ1y3=!XT{QYNfx>16nR?!a}H18!68z&a&HasdM^Qm+a)KppM(K*3!yn_NRE zlBvt>MkIh1dH^ex753YRg%)Xl{ZFWjykf6<+}@mB`1=|{4h7;Gfy0_c&r_S0O!(Iw99tr4cYUAXr4@>>-Ls|5aW^j&z1JsbS^MO?#wxLjDnHssJvxp`&S1MO&Ws2u)l^E7fL01LLlFwDV%RL^0yD@PO zc;{X3x^M{tR}`S$x-}ZPt`F61R#~sR!2X zPLv=Z_t^NpkO8)hRnMV4Bbc?~%88j6=O*4n8fJG%)D?U5YfLYhO3_GLk~hKPxHF0V zTG;u%q`STGb;`%HC39j6vvA;F`3{_^MngcHbG2obK}~Lo=t#ajDEV3S+`|}5#nk?Y4lVA=zLh%f$t$sZm}gJYbUUb`GXV?A19(b4 zF4;mjHPHQx@%AgR2T}z@7iz$eb^48eK{ozNS-E;w9m1?`&~O{NRXN&gQZ)K6Fm9Mu za_AZkQa}XXK^UuKyg7o~0B)_T8)e)fe4YTHr8x&kG9rSQ;>~cZbH+1?1ICPP$clio z?nbuh%P(OpD86voeZ0s!%9L?|^n2p zdZ40(z76W;(TNfDee^Yz8>{3Qud@Ou8Sd}1BJE@B<$SnX?Ta3b*CrFno@mDVDw#u8 zNFu9ijxTi86NQn@>dr+@(o*q)cxyGdR1o>oefFTO(jg=UO3VttIOOT6X|uj2<<7?* zr%9e%#wDxO94aw(R25qOeJG_?BZ8jP7Z}DwAxBU{$Hb_IDyrx!P!=W=4XGBiG?Lbn*yPB@B$m0rwm=l3g%U~ny6S{@mZi=0Kv}zC z_b}ybK*(`GR2}_F1Odu@p2b36l_zdnHo4Rfnf1tncLh{VdQy6{&EK)2nDM^1CKBo- zf)b?0*AneYke*{5Jw5)U@FI%lv{qMclh!SWGC}IC?mjMhv6XBdgOP!ILq~jU8A>N; z7w2VN`qb=itYGKg^NZv;q;PuE#JtewW4XG-@k#WGpDj{lI?q$G2Hv*qV>UZFEiy%n zG=*Cbkw2UM22UcT0olc@^wr8m#x{k0*hksD(pJXwRl(Opp~ZQgOGlf)FIq5WMH+YD z23{$%EeHX;f~iQSFGCd}^EmL^F5+%d@XoU^00H-@z+?ov`N0b<@V;98Y&36zLOp1| zCg-omr3V}Cy5D`Ajwv`ETA1?apZ%G>+XH|ggOq7FK|D*hK^+Yw!KLJW#*3I+s0djZ zFlg&Z(Po#7y7wFOVFo8^uh7919Jd73CO_ZZp(2?|TvwP3`C=sm zg)>DyJ{6&>FF*Grk81Ar&u+AZh-JVjI%1h@rJ2i_`xkx5{ylLl8+nd%7-|!Ch|Eu{ zBr~>3nHr?-%7q)$kiY$>K~L?Y2(?l5yJ;AzoWDhY^>G;cdx*bFedaSuUxwOcp9F~g zX~1dk0s2r|a(@PgMBMR9wN3BvKGAu%OzOf6+t!ns@s;x02(-|J4$)cxJ^0* z`0z3$g?{>1`4$!HzcgK5~E9@7Ii5YH06iZTWuD@tfbnY z{OXK^aXc>Q!hDvs9eQQi&naUH(#STQu}*xHW3@AvVfy<;ukZ7>e*CoSZ)}xW@`_i0 z$|vw3g|$aI`5WUf#?Ydk zhT)QfTqm>%$RyS{sW(Xn%D@|QRk*zq+j9S;FWfJ>iG5B(B~pUlp^_HFI6MGP+!w2! zj36XF5sVWb&r&^4&Oe#l`{xyQ_%&w8>Pb^m2iX}NwwqZmT(zf`Z}zh>BIDUg7#_Ib z7O>l)#iENkMmA{@XA&Ep8LZqQ1k;?PMaHg82=Eqan4$8JRf@ma-oZ_Ws)Uf*QY$%9MbDh>s&aIoQPJ2%89&^Uw#my}%O)DfA%RFEdGSCxQ zmZl%H)D9R{q-)JP(7{s(1e1H!lp*Wj$A&$Dfn2uQ$x)!Gymg8f;md4{>jE=-{*dkp7$dL3;x4z{COqqWNetUi~Zdda?$T4jygPDPXTJ z32ueAcGR2ZB7lhxLupZ6`%9Oq_lQ6nKHKHZaOz%_Zl(c?>LCsrq+lz;b*gAs!Ihh| zKmC(Xf|+wUXgfNv^xh+Fdatw7#-VCy76;A6PKMxu+xE_nQR`-1YcpBp`HQk|Hujd| zWvVs={Kh2KY+vJM5ZTFKb+D|IZu!clx?R}g$B_EGV(DjQv*ePn<$?hZ>S>Y)SoapdO~Z-4`SZ6>ZQ*W_% zmAIl*7@NwAcuPu{)Nx_$9?JC22ht~s9IbivpaoEWZ!?3;?T|%lgokWK%My^m8@qgRa_0Z4#x-*vsfBkJ5SK$qauF+pgW2!-0>+R6k6R^b3AtHgnX0(if394V?L{)k%>7!W5vti4{xV@X}?=cjkdb4ojlo&qjNb;TKk*DL*)dc7H z*GT+Q?O?ogERQj&cF^|o?o4y>Gn2x+OJtQ=l_n0UYC=;2em7F2s}dN?<(?xTeM8s5 zqo*OAAi002J~+@j$O<~SLv*oIpt5g)q{Q&vucT@U%m37Z!8J(d)Mfk3i(%-jpxwUC zey)B%nwh@g?K?>GU;uhP{xB-n(~a~%8<9#l#g>INPfi80d%{wv&%X@eoPFYcl60U( zfBusaDyHz@{qBgtry=v#yyL}XstIDpKKi=PeEx+*W@x1_Vok5)7MsC7o8s~9fCS+`<30; z5isGZ%7C$f$?cQ;+iZgG3mSuiW3IeXcbc(&e|TING-VO9Qh00A<`zseWC20()Jj+?n4e<7NtY)@AY$^xx|`RI31|U z@zGp$)iV{Ifa;{J-|Qd48PT@DrGehl&tOq3+XAm(=LwHiAF);4-&S+kS$;Fj!ak5& z$|bcJGBJsb|D#EHd?wl(E&!#!*ro+ZsUrzv7?aRF*kU9n_8}ru!0DDS7lS}OJgaW* z(=ntLfpWE~ROYXk!uv>511<|mVV~S354!lKK_ltW4DYt6zvw%o_y_y9Ny3?2qYnOw zw(oclyQ0^v9NHQj>wx&zm6dh?&AsttPO`@|jX zg75n+Wt@4nR0UVf(*JA@BJ*ilDH9tNcP41>(;OxKW$Lxhdh6tNBM^q)F2Rr>N!mbV z3`2c`jYPoVLUvT&U;PhZyfC^l<6Y&F=z!&^!=RW8RGNiF`?iM5V-^q|^y2Sa z!$~>?81z{q*6ed72}Zv49`aFNkyS4YY+c=4To{>P23h~PQ3zXgWNyNJvM=|V_Z4B; zYQkLkg?Exdnr?2Yt4T?o_+TH?7BgyS+Q^rGk|!aN*!RYy^9^Bgcw3AGQY%)7j=={IF82>mCP>|-syspr${*Ci!bg|^g9}&r znoaK&E-mJJe3l?Z=7q54dD``=P7GlIqe1Q*rTyJD86}csN6uijZ4@R{AT!vHDi%jz z7SiSlI00F)Gz?BXkKv)JGfIDB#Rcw<3+7Xyq)qGe@ ziH89=pt`mq!@f11B;WXpVQ-!RM<0xNVifwAc0tfBAv$59@h%MkQ034h}Y&`+cz&MxP510>>gd7cn#j-l24t36w@Q$i8>s9Iqu_UL+@3e(`}kC zXb!VP9QWinO%914t$~kBN)l2k4Z?SO4gU-fD z1gUO5flG&RPz^H)E}yfijZMG8@84v;AgWVF2V#qg;PT~FvP^`LvICq}kCJChGMH86 zqZ0xB_erd>2U+uaAO*f4^MdLDzN?m3gYitENhzRWH|#Vt4i^l#&G5A7$0puE1+TW+c3r}h%xxDhdU8RC!ywc^C%8~rf$K-|~vzg1N&(Rs{ zq!Y+kKHU9E`wK2bM%z?ReCEk!J&6;Bb7&z#M`piyTldN~p|W5+Iir3M8VP{|Q4FvF zMXr#652R@>Rr=_P?WB6@#7Y!Nm+%v_nXDD1m}H%H3qkS7_ZX=gN`WSkC(+rd%L;cJ zv=1da!>0%8C%UMjgT|)4(y5xq;S2g5=ZztvcRSv&ch(V9KvJ%(W;b#RzuY zgdQXhS1XVB>0o)C6(fRmkRMX?E=#X!m&Fmd1W5{Jfgmipa9+2^WzXF>Ddg{nuu;{c z5SUf~?ML)$YV1dC>Xle%ArHd~$GQT0jlsXHsp^vKTS zBdyn}d|qhJx#nJX#I?zdoN*OvTiFtBAEs;*+Bgb$*P>VKAe>(_YL3)@(sMr-uk;o1Io#7u(CvOWiUfXRCGQHz~Hm#g*WBN|6HT;C4dUL~|D_ihKMT3$pY4W}Sa)bB6jSgR#oo5Y_ z`Q=gW)X4`K23H&__Kan={?(i}SvF(jxSD!u# zkZ`#k?(kvp^rAl#{>2@OYCnxP?E{uMEdC<}e;*!Q)LGS#JdvZGJM{LGv3*9iIlLg(a!jL!DfmL?&`O*REe4&DVG?PacVy{QZLQzrE$HzY4x!1X+~R z0g=X8J?XNC%?%mJsua96KbAL5A&R#tlD#REMYu6?GFSdlSV(rTvFYT5#^Em=Qry;C zM;7BKqN`*)LOV=<+A}xk!H(nW>-|KE2zshrbt8WGWTS6D(aXlSZ8mNP{l*^GEIIw2 zHL+jZroBDGvTuw+)vC!^OrP*`NYdTT&Y&hZL~B=szJBfbS7$toV{1E2Bilz4qc`6# z^j)rK?r+rFI*G1*#;aF)O3&==FOLlVmiOKBraktggP-?6sR1u zbe)_~ylmNi`Q-e^CaDyj44YN|miIfMC1sS*-!EK{X{gDSt=r@nHg$1OwW@0eDdkg% z#Mwd8K>AhJBJ+Sd<$Y=SQ?8AgVhERavFaZ_%+K{AA-&{EnV-U&wv2;nz9PC=LS;)@ zWlI>ZPDhi}ylwQ_0Q3{&q;!^DV_nPp;PdMzyFSq1a#5A}^6t{St_ejK+-zLRoKxL-}rO8;iSlZ=-G2lY(URd41Oxu-Q7 zFQpM*tChfgc?B!GRwf_`!i6 z9QeV39~}6>fgc?B!GRwf_`k&g>%xs!XlWlGG3 literal 0 HcmV?d00001 diff --git a/examples/rainbow_tweet/chrome_extension/logo.png b/examples/rainbow_tweet/chrome_extension/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..803fbeade898c39591e0df12c2ad58aaf8bcb48d GIT binary patch literal 30817 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 literal 0 HcmV?d00001 diff --git a/examples/rainbow_tweet/chrome_extension/manifest.json b/examples/rainbow_tweet/chrome_extension/manifest.json new file mode 100644 index 0000000..e2429db --- /dev/null +++ b/examples/rainbow_tweet/chrome_extension/manifest.json @@ -0,0 +1,27 @@ +{ + "manifest_version": 3, + "name": "Rainbow-Tweet", + "description": "The Rainbow-Tweet plugin allows the user to convert any tweet into positive language by clicking a button on the tweet.", + "version": "0.0.0.1", + "icons": { + "128": "logo.png" + }, + "action": { + "default_icon": { + "128": "logo.png" + }, + "default_title": "Configure API Key", + "default_popup": "popup.html" + }, + "permissions": [ + "storage" + ], + "content_scripts": [ + { + "matches": ["https://twitter.com/*"], + "js": ["content.js"], + "css": ["styles.css"], + "run_at": "document_end" + } + ] +} diff --git a/examples/rainbow_tweet/chrome_extension/popup.css b/examples/rainbow_tweet/chrome_extension/popup.css new file mode 100644 index 0000000..00e57df --- /dev/null +++ b/examples/rainbow_tweet/chrome_extension/popup.css @@ -0,0 +1,37 @@ +body { + font-family: Arial, sans-serif; +} + +.container { + width: 300px; + padding: 20px; +} + +h1 { + color: #444; +} + +.btn { + color: white; + background-color: #1da1f2; + border: none; + padding: 10px 20px; + margin-top: 10px; + cursor: pointer; +} +.footer { + margin-top: 20px; + text-align: center; +} +.btn:hover { + background-color: #0c84d2; +} + +.form-group { + margin-bottom: 15px; +} + +.form-text { + font-size: 0.875em; + color: #6c757d; +} diff --git a/examples/rainbow_tweet/chrome_extension/popup.html b/examples/rainbow_tweet/chrome_extension/popup.html new file mode 100644 index 0000000..378bf6b --- /dev/null +++ b/examples/rainbow_tweet/chrome_extension/popup.html @@ -0,0 +1,31 @@ + + + + Twitter Rewrite: Extension Options + + + +

+

Twitter Rewrite: Extension Options

+
+
+
+
+ Enter your OpenAI API Key to start using the plugin. If you + don't have one, create it here. +
+ +
+

+ +
+ + + diff --git a/examples/rainbow_tweet/chrome_extension/popup.js b/examples/rainbow_tweet/chrome_extension/popup.js new file mode 100644 index 0000000..c2f6b68 --- /dev/null +++ b/examples/rainbow_tweet/chrome_extension/popup.js @@ -0,0 +1,35 @@ +// Saving options to chrome.storage +function save_options() { + let openai_api_key = document.getElementById('openai_api_key').value; + chrome.storage.sync.set({ + openai_api_key: openai_api_key + }, function() { + // Update status to let user know options were saved. + let status = document.getElementById('status'); + status.textContent = 'Options saved.'; + setTimeout(function() { + status.textContent = ''; + }, 750); + }); +} + +// Restores options from chrome.storage +function restore_options() { + chrome.storage.sync.get({ + openai_api_key: '' + }, function(items) { + document.getElementById('openai_api_key').value = items.openai_api_key; + }); +} + +document.addEventListener('DOMContentLoaded', restore_options); +document.getElementById('optionForm').addEventListener('submit', function(event) { + event.preventDefault(); + save_options(); +}); + + + + + + diff --git a/examples/rainbow_tweet/chrome_extension/styles.css b/examples/rainbow_tweet/chrome_extension/styles.css new file mode 100644 index 0000000..b2bf658 --- /dev/null +++ b/examples/rainbow_tweet/chrome_extension/styles.css @@ -0,0 +1,84 @@ +.modify-button { + background-color: #00acee; /* Twitter Blue */ + color: white; + border: none; + padding: 5px 10px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 16px; + margin: 4px 2px; + cursor: pointer; + border-radius: 3px; + box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); /* Add some shadow */ +} + + +/*!* Dynamic rainbow colors for each letter *!*/ +/*@keyframes rainbow {*/ +/* 0% { color: hsl(0, 100%, 50%); }*/ +/* 14% { color: hsl(60, 100%, 50%); }*/ +/* 28% { color: hsl(120, 100%, 50%); }*/ +/* 42% { color: hsl(180, 100%, 50%); }*/ +/* 57% { color: hsl(240, 100%, 50%); }*/ +/* 71% { color: hsl(300, 100%, 50%); }*/ +/* 85% { color: hsl(360, 100%, 50%); }*/ +/* 100% { color: hsl(0, 100%, 50%); }*/ +/*}*/ + + + +/*.rainbow-text {*/ +/* animation: rainbow 7s linear infinite;*/ +/* animation-delay: calc(.07s * var(--i));*/ +/*}*/ + +/* Light mode colors (darker) */ +@keyframes rainbow-light { + 0% { color: hsl(0, 100%, 30%); } + 14% { color: hsl(60, 100%, 30%); } + 28% { color: hsl(120, 100%, 30%); } + 42% { color: hsl(180, 100%, 30%); } + 57% { color: hsl(240, 100%, 30%); } + 71% { color: hsl(300, 100%, 30%); } + 85% { color: hsl(360, 100%, 30%); } + 100% { color: hsl(0, 100%, 30%); } +} + +/* Dark mode colors (brighter) */ +@keyframes rainbow-dark { + 0% { color: hsl(0, 100%, 70%); } + 14% { color: hsl(60, 100%, 70%); } + 28% { color: hsl(120, 100%, 70%); } + 42% { color: hsl(180, 100%, 70%); } + 57% { color: hsl(240, 100%, 70%); } + 71% { color: hsl(300, 100%, 70%); } + 85% { color: hsl(360, 100%, 70%); } + 100% { color: hsl(0, 100%, 70%); } +} + +/* Apply light mode colors by default */ +.rainbow-text { + font-size: 200%; + animation: rainbow-light 7s linear infinite; + animation-delay: calc(.07s * var(--i)); +} + +/* Apply dark mode colors if user prefers dark mode */ +@media (prefers-color-scheme: dark) { + .rainbow-text { + animation: rainbow-dark 7s linear infinite; + animation-delay: calc(.07s * var(--i)); + } +} + + +/*!* Rainbow colors for each letter *!*/ +/*!* Rainbow colors for each letter *!*/ +/*.rainbow0 { color: red; background-color: cyan; mix-blend-mode: difference; }*/ +/*.rainbow1 { color: orange; background-color: blue; mix-blend-mode: difference; }*/ +/*.rainbow2 { color: yellow; background-color: purple; mix-blend-mode: difference; }*/ +/*.rainbow3 { color: green; background-color: magenta; mix-blend-mode: difference; }*/ +/*.rainbow4 { color: blue; background-color: orange; mix-blend-mode: difference; }*/ +/*.rainbow5 { color: indigo; background-color: yellow; mix-blend-mode: difference; }*/ +/*.rainbow6 { color: violet; background-color: green; mix-blend-mode: difference; }*/ diff --git a/examples/rainbow_tweet/example_call.bash b/examples/rainbow_tweet/example_call.bash new file mode 100644 index 0000000..9204f0b --- /dev/null +++ b/examples/rainbow_tweet/example_call.bash @@ -0,0 +1 @@ +curl -X 'POST' 'https://gptdeploy-02e02e4150.wolf.jina.ai/post' -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"data": [{"text": "{\"tweet\":\"today is a bad day i dont like it\"}"}]}' diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/__init__.py b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v1/Dockerfile b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v1/Dockerfile new file mode 100644 index 0000000..c752b8e --- /dev/null +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v1/Dockerfile @@ -0,0 +1,29 @@ +FROM jinaai/dev-gpt:0.0.6 + + + +RUN apt-get install --no-install-recommends -y + + + +## install requirements for the executor + +COPY requirements.txt . + +RUN pip -v install --compile -r requirements.txt + + + +# setup the workspace + +COPY . /workdir/ + +WORKDIR /workdir + + + +RUN pytest test_microservice.py + + + +ENTRYPOINT ["jina", "executor", "--uses", "config.yml"] \ No newline at end of file diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v1/__init__.py b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v1/__init__.py new file mode 100644 index 0000000..8a4eb04 --- /dev/null +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v1/__init__.py @@ -0,0 +1,15 @@ +from jina import Executor, requests as jina_requests, DocumentArray +import json + +from .microservice import func + + +class PositiveTweetModifierExecutor3163055(Executor): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + @jina_requests() + def endpoint(self, docs: DocumentArray, **kwargs) -> DocumentArray: + for d in docs: + d.text = func(d.text) + return docs diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v1/apis.py b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v1/apis.py new file mode 100644 index 0000000..24dcb01 --- /dev/null +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v1/apis.py @@ -0,0 +1,23 @@ +import os +import openai + + +openai.api_key = os.getenv("OPENAI_API_KEY") + + +class GPT_3_5_Turbo: + def __init__(self, system: str = ''): + self.system = system + + def __call__(self, prompt: str) -> str: + response = openai.ChatCompletion.create( + model="gpt-3.5-turbo", + messages=[{ + "role": 'system', + "content": self.system + }, { + "role": 'user', + "content": prompt + }] + ) + return response.choices[0]['message']['content'] diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v1/config.yml b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v1/config.yml new file mode 100644 index 0000000..36e015e --- /dev/null +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v1/config.yml @@ -0,0 +1,5 @@ +jtype: PositiveTweetModifierExecutor3163055 +py_modules: + - __init__.py +metas: + name: PositiveTweetModifierExecutor3163055 diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v1/microservice.py b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v1/microservice.py new file mode 100644 index 0000000..8d0b8d1 --- /dev/null +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v1/microservice.py @@ -0,0 +1,37 @@ +# This microservice receives an API key for OpenAI (OPENAI_API_KEY) and a tweet containing potentially passive aggressive language as input. +# It analyzes the input tweet using the OpenAI API to identify passive aggressive language and modifies the language to make it more positive without changing the meaning. +# The microservice then returns the updated, positive version of the tweet as output. + +from .apis import GPT_3_5_Turbo +import json + + +def func(input_json: str) -> str: + # Parse the input JSON string + input_data = json.loads(input_json) + + # Extract the OpenAI API key and tweet from the input data + openai_api_key = input_data["OPENAI_API_KEY"] + tweet = input_data["tweet"] + + # Initialize the GPT-3.5 Turbo API + gpt_3_5_turbo = GPT_3_5_Turbo( + system=f''' +You are an AI language model that can modify tweets to make them more positive without changing their meaning. +When you receive a tweet, you will return a JSON object containing the updated, positive version of the tweet. +Example: +Input tweet: "I can't believe you did that. It's so typical of you." +Output JSON: {{"positive_tweet": "I'm surprised you did that. It's just like you!"}} +''') + + # Generate the prompt for the GPT-3.5 Turbo API + prompt = f"Input tweet: {tweet}" + + # Call the GPT-3.5 Turbo API with the prompt + generated_string = gpt_3_5_turbo(prompt) + + # Parse the generated JSON string + output_data = json.loads(generated_string) + + # Return the output JSON string + return json.dumps(output_data) \ No newline at end of file diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v1/requirements.txt b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v1/requirements.txt new file mode 100644 index 0000000..054deb5 --- /dev/null +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v1/requirements.txt @@ -0,0 +1,4 @@ +jina==3.15.1.dev14 +docarray==0.21.0 +openai==0.27.5 +pytest \ No newline at end of file diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v1/test_microservice.py b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v1/test_microservice.py new file mode 100644 index 0000000..95c276f --- /dev/null +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v1/test_microservice.py @@ -0,0 +1,22 @@ +# This test case checks if the output of the microservice is of type 'str' for the positive_tweet property. +# Since the output of the GPT-3.5 Turbo model is not deterministic, we cannot check for the exact output. +# Instead, we will test if the output is a valid JSON string and if the 'positive_tweet' property is a string. + +from .microservice import func +import json + +def test_positive_tweet_type(): + # Define the input JSON string + input_json = json.dumps({ + "OPENAI_API_KEY": "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", + "tweet": "I can't believe you did that. It's so typical of you." + }) + + # Call the microservice function with the input JSON string + output_json = func(input_json) + + # Parse the output JSON string + output_data = json.loads(output_json) + + # Check if the 'positive_tweet' property is a string + assert isinstance(output_data["positive_tweet"], str) \ No newline at end of file diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v2/Dockerfile b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v2/Dockerfile new file mode 100644 index 0000000..c752b8e --- /dev/null +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v2/Dockerfile @@ -0,0 +1,29 @@ +FROM jinaai/dev-gpt:0.0.6 + + + +RUN apt-get install --no-install-recommends -y + + + +## install requirements for the executor + +COPY requirements.txt . + +RUN pip -v install --compile -r requirements.txt + + + +# setup the workspace + +COPY . /workdir/ + +WORKDIR /workdir + + + +RUN pytest test_microservice.py + + + +ENTRYPOINT ["jina", "executor", "--uses", "config.yml"] \ No newline at end of file diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v2/__init__.py b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v2/__init__.py new file mode 100644 index 0000000..8a4eb04 --- /dev/null +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v2/__init__.py @@ -0,0 +1,15 @@ +from jina import Executor, requests as jina_requests, DocumentArray +import json + +from .microservice import func + + +class PositiveTweetModifierExecutor3163055(Executor): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + @jina_requests() + def endpoint(self, docs: DocumentArray, **kwargs) -> DocumentArray: + for d in docs: + d.text = func(d.text) + return docs diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v2/apis.py b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v2/apis.py new file mode 100644 index 0000000..24dcb01 --- /dev/null +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v2/apis.py @@ -0,0 +1,23 @@ +import os +import openai + + +openai.api_key = os.getenv("OPENAI_API_KEY") + + +class GPT_3_5_Turbo: + def __init__(self, system: str = ''): + self.system = system + + def __call__(self, prompt: str) -> str: + response = openai.ChatCompletion.create( + model="gpt-3.5-turbo", + messages=[{ + "role": 'system', + "content": self.system + }, { + "role": 'user', + "content": prompt + }] + ) + return response.choices[0]['message']['content'] diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v2/config.yml b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v2/config.yml new file mode 100644 index 0000000..36e015e --- /dev/null +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v2/config.yml @@ -0,0 +1,5 @@ +jtype: PositiveTweetModifierExecutor3163055 +py_modules: + - __init__.py +metas: + name: PositiveTweetModifierExecutor3163055 diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v2/microservice.py b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v2/microservice.py new file mode 100644 index 0000000..06bd4a0 --- /dev/null +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v2/microservice.py @@ -0,0 +1,41 @@ +# This microservice receives an API key for OpenAI (OPENAI_API_KEY) and a tweet containing potentially passive aggressive language as input. +# It analyzes the input tweet using the OpenAI API to identify passive aggressive language and modifies the language to make it more positive without changing the meaning. +# The microservice then returns the updated, positive version of the tweet as output. + +from .apis import GPT_3_5_Turbo +import json + + +def func(input_json: str) -> str: + # Parse the input JSON string + input_data = json.loads(input_json) + + # Extract the OpenAI API key and tweet from the input data + openai_api_key = input_data["OPENAI_API_KEY"] + tweet = input_data["tweet"] + + # Initialize the GPT-3.5 Turbo API + gpt_3_5_turbo = GPT_3_5_Turbo( + system=f''' +You are an AI language model that can modify tweets to make them more positive without changing their meaning. +When you receive a tweet, you will return a JSON object containing the updated, positive version of the tweet. +Example: +Input tweet: "I can't believe you did that. It's so typical of you." +Output JSON: {{"positive_tweet": "I'm surprised you did that. It's just like you!"}} +''') + + # Generate the prompt for the GPT-3.5 Turbo API + prompt = f"Input tweet: {tweet}" + + # Call the GPT-3.5 Turbo API with the prompt + generated_string = gpt_3_5_turbo(prompt) + + # Check if the generated_string is a valid JSON string + try: + output_data = json.loads(generated_string) + except json.JSONDecodeError: + # If the generated_string is not a valid JSON string, return an error message + return json.dumps({"error": "Invalid JSON string generated by the GPT-3.5 Turbo API"}) + + # Return the output JSON string + return json.dumps(output_data) \ No newline at end of file diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v2/requirements.txt b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v2/requirements.txt new file mode 100644 index 0000000..054deb5 --- /dev/null +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v2/requirements.txt @@ -0,0 +1,4 @@ +jina==3.15.1.dev14 +docarray==0.21.0 +openai==0.27.5 +pytest \ No newline at end of file diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v2/test_microservice.py b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v2/test_microservice.py new file mode 100644 index 0000000..95c276f --- /dev/null +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v2/test_microservice.py @@ -0,0 +1,22 @@ +# This test case checks if the output of the microservice is of type 'str' for the positive_tweet property. +# Since the output of the GPT-3.5 Turbo model is not deterministic, we cannot check for the exact output. +# Instead, we will test if the output is a valid JSON string and if the 'positive_tweet' property is a string. + +from .microservice import func +import json + +def test_positive_tweet_type(): + # Define the input JSON string + input_json = json.dumps({ + "OPENAI_API_KEY": "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", + "tweet": "I can't believe you did that. It's so typical of you." + }) + + # Call the microservice function with the input JSON string + output_json = func(input_json) + + # Parse the output JSON string + output_data = json.loads(output_json) + + # Check if the 'positive_tweet' property is a string + assert isinstance(output_data["positive_tweet"], str) \ No newline at end of file diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/Dockerfile b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/Dockerfile new file mode 100644 index 0000000..c752b8e --- /dev/null +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/Dockerfile @@ -0,0 +1,29 @@ +FROM jinaai/dev-gpt:0.0.6 + + + +RUN apt-get install --no-install-recommends -y + + + +## install requirements for the executor + +COPY requirements.txt . + +RUN pip -v install --compile -r requirements.txt + + + +# setup the workspace + +COPY . /workdir/ + +WORKDIR /workdir + + + +RUN pytest test_microservice.py + + + +ENTRYPOINT ["jina", "executor", "--uses", "config.yml"] \ No newline at end of file diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/__init__.py b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/__init__.py new file mode 100644 index 0000000..8a4eb04 --- /dev/null +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/__init__.py @@ -0,0 +1,15 @@ +from jina import Executor, requests as jina_requests, DocumentArray +import json + +from .microservice import func + + +class PositiveTweetModifierExecutor3163055(Executor): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + @jina_requests() + def endpoint(self, docs: DocumentArray, **kwargs) -> DocumentArray: + for d in docs: + d.text = func(d.text) + return docs diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/apis.py b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/apis.py new file mode 100644 index 0000000..89a0768 --- /dev/null +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/apis.py @@ -0,0 +1,23 @@ +import os +import openai + + + + + +class GPT_3_5_Turbo: + def __init__(self, system: str = ''): + self.system = system + + def __call__(self, prompt: str) -> str: + response = openai.ChatCompletion.create( + model="gpt-3.5-turbo", + messages=[{ + "role": 'system', + "content": self.system + }, { + "role": 'user', + "content": prompt + }] + ) + return response.choices[0]['message']['content'] diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/config.yml b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/config.yml new file mode 100644 index 0000000..36e015e --- /dev/null +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/config.yml @@ -0,0 +1,5 @@ +jtype: PositiveTweetModifierExecutor3163055 +py_modules: + - __init__.py +metas: + name: PositiveTweetModifierExecutor3163055 diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/flow.yml b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/flow.yml new file mode 100644 index 0000000..0eda86a --- /dev/null +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/flow.yml @@ -0,0 +1,20 @@ +jtype: Flow +with: + port: 8080 + protocol: http +jcloud: + version: 3.15.1.dev14 + labels: + creator: microchain + name: gptdeploy +gateway: + uses: jinaai+docker://auth0-unified-448f11965ce142b6/GatewayPositiveTweetModifierExecutor3163055:latest + +executors: + - name: positivetweetmodifierexecutor3163055 + uses: jinaai+docker://auth0-unified-448f11965ce142b6/PositiveTweetModifierExecutor3163055:latest + + jcloud: + resources: + instance: C2 + capacity: spot diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/gateway/Dockerfile b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/gateway/Dockerfile new file mode 100644 index 0000000..660b5d7 --- /dev/null +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/gateway/Dockerfile @@ -0,0 +1,14 @@ +FROM jinaai/jina:3.15.1-dev14-py39-standard + + +RUN apt-get update && apt-get install --no-install-recommends -y git pip nginx && rm -rf /var/lib/apt/lists/* + +## install requirements for the executor +COPY requirements.txt . +RUN pip install --compile -r requirements.txt + +# setup the workspace +COPY . /workdir/ +WORKDIR /workdir + +ENTRYPOINT ["jina", "gateway", "--uses", "config.yml"] \ No newline at end of file diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/gateway/__init__.py b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/gateway/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/gateway/app.py b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/gateway/app.py new file mode 100644 index 0000000..bb2c11e --- /dev/null +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/gateway/app.py @@ -0,0 +1,58 @@ +import json +import os +import streamlit as st +from jina import Client, Document, DocumentArray + +# Set the favicon and title +st.set_page_config( + page_title="Positive Tweet Modifier", + page_icon=":smiley:", + layout="wide", +) + +# Define the input dictionary +INPUT_DICTIONARY = { + "OPENAI_API_KEY": "", + "tweet": "I can't believe you did that. It's so typical of you.", +} + +# Define the function to send a request to the microservice +def send_request(input_dict): + client = Client(host='http://localhost:8080') + d = Document(text=json.dumps(input_dict)) + response = client.post('/', inputs=DocumentArray([d])) + return response[0].text + +# Create the UI +st.title("Positive Tweet Modifier :speech_balloon:") +st.write("Transform negative tweets into positive ones using GPT-3.5 Turbo! :sunglasses:") + +# Input form +st.subheader("Input") +tweet = st.text_area("Enter a negative tweet:", value=INPUT_DICTIONARY["tweet"], height=100) +api_key = st.text_input("Enter your OPENAI_API_KEY:", value=INPUT_DICTIONARY["OPENAI_API_KEY"]) + +# Send request button +if st.button("Transform Tweet"): + INPUT_DICTIONARY["tweet"] = tweet + INPUT_DICTIONARY["OPENAI_API_KEY"] = api_key + response_text = send_request(INPUT_DICTIONARY) + response_data = json.loads(response_text) + + # Display the result + st.subheader("Result") + st.write(f"Positive Tweet: {response_data['positive_tweet']} :thumbsup:") + +# Deploy your own microservice +st.markdown( + "Want to deploy your own microservice? [Click here!](https://github.com/jina-ai/dev-gpt)" +) + +# Display the curl command +deployment_id = os.environ.get("K8S_NAMESPACE_NAME", "") +host = f'https://dev-gpt-{deployment_id.split("-")[1]}.wolf.jina.ai/post' if deployment_id else "http://localhost:8080/post" +with st.expander("See curl command"): + st.code( + f'curl -X \'POST\' \'{host}\' -H \'accept: application/json\' -H \'Content-Type: application/json\' -d \'{{"data": [{{"text": "hello, world!"}}]}}\'', + language='bash' + ) \ No newline at end of file diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/gateway/app_config.toml b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/gateway/app_config.toml new file mode 100644 index 0000000..24ef3ce --- /dev/null +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/gateway/app_config.toml @@ -0,0 +1,4 @@ +[server] + +baseUrlPath = "/playground" +headless = true \ No newline at end of file diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/gateway/config.yml b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/gateway/config.yml new file mode 100644 index 0000000..5357216 --- /dev/null +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/gateway/config.yml @@ -0,0 +1,5 @@ +jtype: GatewayPositiveTweetModifierExecutor3163055 +py_modules: + - custom_gateway.py +metas: + name: GatewayPositiveTweetModifierExecutor3163055 diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/gateway/custom_gateway.py b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/gateway/custom_gateway.py new file mode 100644 index 0000000..d6292f7 --- /dev/null +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/gateway/custom_gateway.py @@ -0,0 +1,154 @@ +import os +import shutil +import subprocess +from time import sleep +from typing import List, Tuple + +import streamlit.web.bootstrap +from jina import Gateway +from jina.serve.runtimes.gateway.composite import CompositeGateway +from streamlit.file_util import get_streamlit_file_path +from streamlit.web.server import Server as StreamlitServer + + +cur_dir = os.path.dirname(__file__) + + +def cmd(command, std_output=False, wait=True): + if isinstance(command, str): + command = command.split() + if not std_output: + process = subprocess.Popen( + command, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + else: + process = subprocess.Popen(command) + if wait: + output, error = process.communicate() + return output, error + + +class PlaygroundGateway(Gateway): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.streamlit_script = 'app.py' + # copy playground/config.toml to streamlit config.toml + streamlit_config_toml_src = os.path.join(cur_dir, 'app_config.toml') + streamlit_config_toml_dest = get_streamlit_file_path("config.toml") + # create streamlit_config_toml_dest if it doesn't exist + os.makedirs(os.path.dirname(streamlit_config_toml_dest), exist_ok=True) + shutil.copyfile(streamlit_config_toml_src, streamlit_config_toml_dest) + + async def setup_server(self): + streamlit.web.bootstrap._fix_sys_path(self.streamlit_script) + streamlit.web.bootstrap._fix_matplotlib_crash() + streamlit.web.bootstrap._fix_tornado_crash() + streamlit.web.bootstrap._fix_sys_argv(self.streamlit_script, ()) + streamlit.web.bootstrap._fix_pydeck_mapbox_api_warning() + streamlit_cmd = f'streamlit run {self.streamlit_script}' + + self.streamlit_server = StreamlitServer( + os.path.join(cur_dir, self.streamlit_script), streamlit_cmd + ) + + async def run_server(self): + await self.streamlit_server.start() + streamlit.web.bootstrap._on_server_start(self.streamlit_server) + streamlit.web.bootstrap._set_up_signal_handler(self.streamlit_server) + + async def shutdown(self): + self.streamlit_server.stop() + + +class GatewayPositiveTweetModifierExecutor3163055(CompositeGateway): + """The CustomGateway assumes that the gateway has been started with http on port 8080. + This is the port on which the nginx process listens. After nginx has been started, + it will start the playground on port 8501 and the actual HTTP gateway will start on port 8082. + + Nginx is configured to route the requests in the following way: + - /playground -> playground on port 8501 + - / -> HTTP gateway on port 8082 + """ + + def __init__(self, **kwargs): + # need to update port to 8082, as nginx will listen on 8080 + http_idx = 0 + http_port = kwargs['runtime_args']['port'][http_idx] + if kwargs['runtime_args']['port'][http_idx] != 8080: + raise ValueError( + f'Please, let http port ({http_port}) be 8080 for nginx to work' + ) + kwargs['runtime_args']['port'][http_idx] = 8082 + kwargs['cors'] = True + super().__init__(**kwargs) + + # remove potential clashing arguments from kwargs + kwargs.pop("port", None) + kwargs.pop("protocol", None) + + # note order is important + self._add_gateway( + PlaygroundGateway, + 8501, + **kwargs, + ) + + self.setup_nginx() + self.nginx_was_shutdown = False + + async def shutdown(self): + await super().shutdown() + if not self.nginx_was_shutdown: + self.shutdown_nginx() + self.nginx_was_shutdown = True + + def setup_nginx(self): + command = [ + 'nginx', + '-c', + os.path.join(cur_dir, '', 'nginx.conf'), + ] + output, error = self._run_nginx_command(command) + self.logger.info('Nginx started') + self.logger.info(f'nginx output: {output}') + self.logger.info(f'nginx error: {error}') + + def shutdown_nginx(self): + command = ['nginx', '-s', 'stop'] + output, error = self._run_nginx_command(command) + self.logger.info('Nginx stopped') + self.logger.info(f'nginx output: {output}') + self.logger.info(f'nginx error: {error}') + + def _run_nginx_command(self, command: List[str]) -> Tuple[bytes, bytes]: + self.logger.info(f'Running command: {command}') + output, error = cmd(command) + if error != b'': + # on CI we need to use sudo; using NOW_CI_RUN isn't good if running test locally + self.logger.info(f'nginx error: {error}') + command.insert(0, 'sudo') + self.logger.info(f'So running command: {command}') + output, error = cmd(command) + sleep(10) + return output, error + + def _add_gateway(self, gateway_cls, port, protocol='http', **kwargs): + # ignore metrics_registry since it is not copyable + runtime_args = self._deepcopy_with_ignore_attrs( + self.runtime_args, + [ + 'metrics_registry', + 'tracer_provider', + 'grpc_tracing_server_interceptors', + 'aio_tracing_client_interceptors', + 'tracing_client_interceptor', + 'monitoring', # disable it for fastapi gateway + ], + ) + runtime_args.port = [port] + runtime_args.protocol = [protocol] + gateway_kwargs = {k: v for k, v in kwargs.items() if k != 'runtime_args'} + gateway_kwargs['runtime_args'] = dict(vars(runtime_args)) + gateway = gateway_cls(**gateway_kwargs) + gateway.streamer = self.streamer + self.gateways.insert(0, gateway) diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/gateway/nginx.conf b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/gateway/nginx.conf new file mode 100644 index 0000000..e44f98d --- /dev/null +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/gateway/nginx.conf @@ -0,0 +1,62 @@ +events { + worker_connections 4096; ## Default: 1024 +} + +http { + server { + listen 8080; + server_name localhost; + + + # from https://medium.com/@dasirra/using-streamlit-nginx-docker-to-build-and-put-in-production-dashboards-in-aws-lightsail-781dab8f2836 + location ^~ /static { + proxy_pass http://localhost:8501/static/; + } + location ^~ /healthz { + proxy_pass http://localhost:8501/healthz; + } + location ^~ /vendor { + proxy_pass http://localhost:8501/vendor; + } + location ^~ /st-allowed-message-origins { + proxy_pass http://localhost:8501/st-allowed-message-origins; + } + + # for jcloud deployment, very important; actually talks via websocket + location ^~ /stream { + # inspired from https://discuss.streamlit.io/t/how-to-use-streamlit-with-nginx/378/7 + proxy_pass http://localhost:8501/stream; + proxy_http_version 1.1; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_read_timeout 86400; + } + location ^~ /favicon.png { + proxy_pass http://localhost:8501/favicon.png; + } + # to make extra components work + location ^~ /component { + proxy_pass http://localhost:8501/component; + } + + location /playground { + # streamlit specific from https://discuss.streamlit.io/t/streamlit-docker-nginx-ssl-https/2195 + proxy_http_version 1.1; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_read_timeout 86400; + proxy_pass http://localhost:8501; + client_max_body_size 50M; + } + + location / { + proxy_pass http://localhost:8082; + client_max_body_size 50M; + } + + } +} \ No newline at end of file diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/gateway/requirements.txt b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/gateway/requirements.txt new file mode 100644 index 0000000..a5ab956 --- /dev/null +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/gateway/requirements.txt @@ -0,0 +1,4 @@ +streamlit==1.16.0 +altair==4.2.2 +extra-streamlit-components==0.1.55 +jina==3.15.1.dev14 \ No newline at end of file diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/microservice.py b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/microservice.py new file mode 100644 index 0000000..a9cded0 --- /dev/null +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/microservice.py @@ -0,0 +1,45 @@ +# This microservice receives an API key for OpenAI (OPENAI_API_KEY) and a tweet containing potentially passive aggressive language as input. +# It analyzes the input tweet using the OpenAI API to identify passive aggressive language and modifies the language to make it more positive without changing the meaning. +# The microservice then returns the updated, positive version of the tweet as output. +import os + +import openai + +from .apis import GPT_3_5_Turbo +import json + + +def func(input_json: str) -> str: + # Parse the input JSON string + input_data = json.loads(input_json) + + # Extract the OpenAI API key and tweet from the input data + openai.api_key = input_data["OPENAI_API_KEY"] + print('key updated: ', input_data["OPENAI_API_KEY"]) + tweet = input_data["tweet"] + + # Initialize the GPT-3.5 Turbo API + gpt_3_5_turbo = GPT_3_5_Turbo( + system=f''' +You are an AI language model that can modify tweets to make them more positive without changing their meaning. +When you receive a tweet, you will return a JSON object containing the updated, positive version of the tweet. +Example: +Input tweet: "I can't believe you did that. It's so typical of you." +Output JSON: {{"positive_tweet": "I'm surprised you did that. It's just like you!"}} +''') + + # Generate the prompt for the GPT-3.5 Turbo API + prompt = f"Input tweet: {tweet}\nOutput JSON:" + + # Call the GPT-3.5 Turbo API with the prompt + generated_string = gpt_3_5_turbo(prompt) + + # Check if the generated_string is a valid JSON string + try: + output_data = json.loads(generated_string) + except json.JSONDecodeError: + # If the generated_string is not a valid JSON string, return an error message + return json.dumps({"error": "Invalid JSON string generated by the GPT-3.5 Turbo API"}) + + # Return the output JSON string + return json.dumps(output_data) \ No newline at end of file diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/requirements.txt b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/requirements.txt new file mode 100644 index 0000000..054deb5 --- /dev/null +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/requirements.txt @@ -0,0 +1,4 @@ +jina==3.15.1.dev14 +docarray==0.21.0 +openai==0.27.5 +pytest \ No newline at end of file diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/run_flow.py b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/run_flow.py new file mode 100644 index 0000000..560e20c --- /dev/null +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/run_flow.py @@ -0,0 +1,5 @@ +from jina import Flow + +flow = Flow.load_config('flow.yml') +with flow: + flow.block() \ No newline at end of file diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/test_microservice.py b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/test_microservice.py new file mode 100644 index 0000000..95c276f --- /dev/null +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/test_microservice.py @@ -0,0 +1,22 @@ +# This test case checks if the output of the microservice is of type 'str' for the positive_tweet property. +# Since the output of the GPT-3.5 Turbo model is not deterministic, we cannot check for the exact output. +# Instead, we will test if the output is a valid JSON string and if the 'positive_tweet' property is a string. + +from .microservice import func +import json + +def test_positive_tweet_type(): + # Define the input JSON string + input_json = json.dumps({ + "OPENAI_API_KEY": "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", + "tweet": "I can't believe you did that. It's so typical of you." + }) + + # Call the microservice function with the input JSON string + output_json = func(input_json) + + # Parse the output JSON string + output_data = json.loads(output_json) + + # Check if the 'positive_tweet' property is a string + assert isinstance(output_data["positive_tweet"], str) \ No newline at end of file diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/__init__.py b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/rainbow_tweet/microservice/__init__.py b/examples/rainbow_tweet/microservice/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/rainbow_tweet/microservice/name.txt b/examples/rainbow_tweet/microservice/name.txt new file mode 100644 index 0000000..87ac147 --- /dev/null +++ b/examples/rainbow_tweet/microservice/name.txt @@ -0,0 +1 @@ +PositiveTweetModifierExecutor \ No newline at end of file diff --git a/examples/rainbow_tweet/microservice/strategies.json b/examples/rainbow_tweet/microservice/strategies.json new file mode 100644 index 0000000..f2004cd --- /dev/null +++ b/examples/rainbow_tweet/microservice/strategies.json @@ -0,0 +1,5 @@ +[ + ["openai"], + ["openai", "transformers"], + ["openai", "textblob"] +] \ No newline at end of file diff --git a/examples/rainbow_tweet/screenshot.png b/examples/rainbow_tweet/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..767db0e2cb91cf0a16b02ec5fadc6ba60e3db2c1 GIT binary patch literal 86323 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

5vahi7FoR)#x2{oh4F4p< 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 Date: Mon, 15 May 2023 13:53:25 +0200 Subject: [PATCH 32/67] =?UTF-8?q?=F0=9F=8C=88=20docs:=20example=20rainbow?= =?UTF-8?q?=20tweet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/rainbow_tweet/README.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 examples/rainbow_tweet/README.md diff --git a/examples/rainbow_tweet/README.md b/examples/rainbow_tweet/README.md new file mode 100644 index 0000000..914d3f5 --- /dev/null +++ b/examples/rainbow_tweet/README.md @@ -0,0 +1,2 @@ +Plugin that lets you convert a negative tweet into a positive one. +![rainbow_tweet](screenshot.png) \ No newline at end of file From f1fcd1adcff3b2496e237e32b80efe36e90f29cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Mon, 15 May 2023 14:44:54 +0200 Subject: [PATCH 33/67] fix --- .../0_gpt_3_5_turbo/v1/test_microservice.py | 2 +- .../0_gpt_3_5_turbo/v2/test_microservice.py | 2 +- .../0_gpt_3_5_turbo/v3/test_microservice.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v1/test_microservice.py b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v1/test_microservice.py index 95c276f..f350248 100644 --- a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v1/test_microservice.py +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v1/test_microservice.py @@ -8,7 +8,7 @@ import json def test_positive_tweet_type(): # Define the input JSON string input_json = json.dumps({ - "OPENAI_API_KEY": "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", + "OPENAI_API_KEY": "", "tweet": "I can't believe you did that. It's so typical of you." }) diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v2/test_microservice.py b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v2/test_microservice.py index 95c276f..f350248 100644 --- a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v2/test_microservice.py +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v2/test_microservice.py @@ -8,7 +8,7 @@ import json def test_positive_tweet_type(): # Define the input JSON string input_json = json.dumps({ - "OPENAI_API_KEY": "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", + "OPENAI_API_KEY": "", "tweet": "I can't believe you did that. It's so typical of you." }) diff --git a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/test_microservice.py b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/test_microservice.py index 95c276f..f350248 100644 --- a/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/test_microservice.py +++ b/examples/rainbow_tweet/microservice/PositiveTweetModifierExecutor3163055/0_gpt_3_5_turbo/v3/test_microservice.py @@ -8,7 +8,7 @@ import json def test_positive_tweet_type(): # Define the input JSON string input_json = json.dumps({ - "OPENAI_API_KEY": "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", + "OPENAI_API_KEY": "", "tweet": "I can't believe you did that. It's so typical of you." }) From 5351fa5f12296ee7c6a7534f49b2a0ce004473b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Mon, 15 May 2023 16:00:54 +0200 Subject: [PATCH 34/67] =?UTF-8?q?=E2=9E=B0=20feat:=20avoid=20loop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chrome_extension/button-icon.svg | 79 +++++++++++++++++++ .../rainbow_tweet/chrome_extension/content.js | 16 ++-- .../rainbow_tweet/chrome_extension/styles.css | 3 +- 3 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 examples/rainbow_tweet/chrome_extension/button-icon.svg diff --git a/examples/rainbow_tweet/chrome_extension/button-icon.svg b/examples/rainbow_tweet/chrome_extension/button-icon.svg new file mode 100644 index 0000000..bec00b5 --- /dev/null +++ b/examples/rainbow_tweet/chrome_extension/button-icon.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/rainbow_tweet/chrome_extension/content.js b/examples/rainbow_tweet/chrome_extension/content.js index 32fb575..3bb910e 100644 --- a/examples/rainbow_tweet/chrome_extension/content.js +++ b/examples/rainbow_tweet/chrome_extension/content.js @@ -7,6 +7,7 @@ chrome.storage.sync.get({ }, function(items) { openai_api_key = items.openai_api_key; }); + let observer = new MutationObserver((mutations) => { console.log('Twitter Rewrite: DOM mutation detected'); // For each mutation @@ -15,7 +16,7 @@ let observer = new MutationObserver((mutations) => { if (mutation.addedNodes) { mutation.addedNodes.forEach((node) => { // If the added node (or its descendants) contains a tweet - let tweets = node.querySelectorAll('[data-testid="tweetText"]'); + let tweets = node.querySelectorAll('[data-testid="tweet"]'); tweets.forEach((tweet) => { // If the tweet doesn't already have a modify button if (!tweet.querySelector('.modify-button')) { @@ -32,8 +33,9 @@ let observer = new MutationObserver((mutations) => { // Add event listener for button click button.addEventListener('click', function() { + let thisButton = this; // Send tweet to API - let originalTweet = tweet.innerText; + let originalTweet = tweet.querySelector('[data-testid="tweetText"]').innerText; this.disabled = true; this.innerText = 'Loading...'; fetch('https://gptdeploy-61694dd6a3.wolf.jina.ai/post', { @@ -60,12 +62,16 @@ let observer = new MutationObserver((mutations) => { let newTweet = document.createElement('span'); newTweet.innerHTML = rainbowTweet; // Replace the old text node with the new element node - tweet.replaceWith(newTweet); + tweet.querySelector('[data-testid="tweetText"]').replaceWith(newTweet); + // Remove the button + thisButton.remove(); }); }); - // Inject button into tweet - tweet.appendChild(button); + // Find the actions container and inject the button into it + let actionGroups = tweet.querySelectorAll('div[role="group"]'); + let actionsContainer = actionGroups[actionGroups.length - 1]; + actionsContainer.appendChild(button); } }); }); diff --git a/examples/rainbow_tweet/chrome_extension/styles.css b/examples/rainbow_tweet/chrome_extension/styles.css index b2bf658..2169abf 100644 --- a/examples/rainbow_tweet/chrome_extension/styles.css +++ b/examples/rainbow_tweet/chrome_extension/styles.css @@ -1,5 +1,6 @@ .modify-button { - background-color: #00acee; /* Twitter Blue */ + /* transparent */ + background-color: #00000000; color: white; border: none; padding: 5px 10px; From 7ac392b907178039821704177ab75ae945a07f75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Mon, 15 May 2023 17:27:35 +0200 Subject: [PATCH 35/67] =?UTF-8?q?=F0=9F=A6=84=20docs:=20example=20rainbow?= =?UTF-8?q?=20tweet=20password=20type=20for=20key=20input?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/rainbow_tweet/chrome_extension/popup.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/rainbow_tweet/chrome_extension/popup.html b/examples/rainbow_tweet/chrome_extension/popup.html index 378bf6b..7b322aa 100644 --- a/examples/rainbow_tweet/chrome_extension/popup.html +++ b/examples/rainbow_tweet/chrome_extension/popup.html @@ -10,7 +10,7 @@


-
+
Enter your OpenAI API Key to start using the plugin. If you don't have one, create it here. From 9e706892c2690e018f1dad147ce88c9a737a9811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Mon, 15 May 2023 23:32:23 +0200 Subject: [PATCH 36/67] =?UTF-8?q?=F0=9F=94=8D=20refactor:=20add=20search?= =?UTF-8?q?=20function=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../static_files/microservice/search.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 dev_gpt/options/generate/static_files/microservice/search.py diff --git a/dev_gpt/options/generate/static_files/microservice/search.py b/dev_gpt/options/generate/static_files/microservice/search.py new file mode 100644 index 0000000..dd388f7 --- /dev/null +++ b/dev_gpt/options/generate/static_files/microservice/search.py @@ -0,0 +1,35 @@ +import os +from typing import Any, List, Optional + +from googleapiclient.discovery import build + +google_api_key: Optional[str] = os.environ['GOOGLE_API_KEY'] +google_cse_id: Optional[str] = os.environ['GOOGLE_CSE_ID'] +search_engine = build("customsearch", "v1", developerKey=google_api_key) + +k: int = 10 +siterestrict: bool = False + +def _google_search_results(search_term: str, **kwargs: Any) -> List[dict]: + cse = search_engine.cse() + if siterestrict: + cse = cse.siterestrict() + res = cse.list(q=search_term, cx=google_cse_id, **kwargs).execute() + return res.get("items", []) + +def run(query: str) -> str: + """Run query through GoogleSearch and parse result.""" + snippets = [] + results = _google_search_results(query, num=k) + if len(results) == 0: + return "No good Google Search Result was found" + for result in results: + if "snippet" in result: + snippets.append(result["snippet"]) + + return " ".join(snippets) + + +if __name__ == "__main__": + # google-api-python-client==2.86.0 + print(run("jina ai")) \ No newline at end of file From 8b6cb8af99d7e637883c59c58ef77d6d47dca005 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Tue, 16 May 2023 00:19:52 +0200 Subject: [PATCH 37/67] =?UTF-8?q?=F0=9F=94=8D=20feat:=20search=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 +- test/integration/test_generator.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index abfc233..b3dc304 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: strategy: fail-fast: false matrix: - group: [0, 1, 2, 3, 4] + group: [0, 1, 2, 3, 4, 5_company_logos] steps: - uses: actions/checkout@v2 - name: Set up Python 3.8 diff --git a/test/integration/test_generator.py b/test/integration/test_generator.py index e03f3ce..83b776d 100644 --- a/test/integration/test_generator.py +++ b/test/integration/test_generator.py @@ -139,6 +139,21 @@ def test_generation_level_4(microservice_dir, mock_input_sequence): ) assert generator.generate() == 0 +@pytest.mark.parametrize('mock_input_sequence', [['y']], indirect=True) +def test_generation_level_5_company_logos(microservice_dir, mock_input_sequence): + os.environ['VERBOSE'] = 'true' + generator = Generator( + f'''\ +Given a list of email addresses, get all company names from them. +For all companies, get the company logo. +All logos need to be arranged on a square. +The square is returned as png. +''', + str(microservice_dir), + 'gpt-3.5-turbo' + ) + assert generator.generate() == 0 + @pytest.mark.parametrize('mock_input_sequence', [['y', 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/PNG_transparency_demonstration_1.png/560px-PNG_transparency_demonstration_1.png']], indirect=True) def test_generation_level_5(microservice_dir, mock_input_sequence): """ From 56042201ec663f4dc65985b609c2567f8841bafa Mon Sep 17 00:00:00 2001 From: Han Xiao Date: Tue, 16 May 2023 11:46:42 +0200 Subject: [PATCH 38/67] chore: fix discord link --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 99c1292..61eb866 100644 --- a/README.md +++ b/README.md @@ -49,9 +49,8 @@ Your imagination is the limit! Downloads - - Discord Chat - + +

From 8017b7fa7484e1da5e1f04b79cfca971db7ee8c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Tue, 16 May 2023 14:18:25 +0200 Subject: [PATCH 39/67] =?UTF-8?q?=F0=9F=94=8D=20feat:=20search=20api=20-?= =?UTF-8?q?=20simplified?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../static_files/microservice/search.py | 56 +++++++++---------- test/unit/test_search.py | 13 +++++ 2 files changed, 38 insertions(+), 31 deletions(-) create mode 100644 test/unit/test_search.py diff --git a/dev_gpt/options/generate/static_files/microservice/search.py b/dev_gpt/options/generate/static_files/microservice/search.py index dd388f7..4c1c082 100644 --- a/dev_gpt/options/generate/static_files/microservice/search.py +++ b/dev_gpt/options/generate/static_files/microservice/search.py @@ -1,35 +1,29 @@ import os -from typing import Any, List, Optional +from typing import Optional -from googleapiclient.discovery import build - -google_api_key: Optional[str] = os.environ['GOOGLE_API_KEY'] -google_cse_id: Optional[str] = os.environ['GOOGLE_CSE_ID'] -search_engine = build("customsearch", "v1", developerKey=google_api_key) - -k: int = 10 -siterestrict: bool = False - -def _google_search_results(search_term: str, **kwargs: Any) -> List[dict]: - cse = search_engine.cse() - if siterestrict: - cse = cse.siterestrict() - res = cse.list(q=search_term, cx=google_cse_id, **kwargs).execute() - return res.get("items", []) - -def run(query: str) -> str: - """Run query through GoogleSearch and parse result.""" - snippets = [] - results = _google_search_results(query, num=k) - if len(results) == 0: - return "No good Google Search Result was found" - for result in results: - if "snippet" in result: - snippets.append(result["snippet"]) - - return " ".join(snippets) +import requests -if __name__ == "__main__": - # google-api-python-client==2.86.0 - print(run("jina ai")) \ No newline at end of file +def google_search(search_term, search_type, top_n): + google_api_key: Optional[str] = os.environ['GOOGLE_API_KEY'] + google_cse_id: Optional[str] = os.environ['GOOGLE_CSE_ID'] + url = "https://www.googleapis.com/customsearch/v1" + params = { + 'q': search_term, + 'key': google_api_key, + 'cx': google_cse_id, + 'searchType': search_type, + 'num': top_n + } + response = requests.get(url, params=params) + response.raise_for_status() + return response.json() + +def search_image(search_term, top_n): + response = google_search(search_term, search_type="image", top_n=top_n) + return [item["link"] for item in response["items"]] + +def search_web(search_term, top_n): + response = google_search(search_term, search_type="web", top_n=top_n) + return [item["snippet"] for item in response["items"]] + diff --git a/test/unit/test_search.py b/test/unit/test_search.py new file mode 100644 index 0000000..fe15dfd --- /dev/null +++ b/test/unit/test_search.py @@ -0,0 +1,13 @@ +from dev_gpt.options.generate.static_files.microservice.search import search_web, search_image + + +def test_web_search(): + results = search_web("jina", 10) + assert len(results) == 10 + assert "jina" in results[0] + assert not results[0].startswith("http") + +def test_image_search(): + results = search_image("jina", 10) + assert len(results) == 10 + assert results[0].startswith("http") From c8c9089d337d616c2bdab06f266c81c033bf8b31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Tue, 16 May 2023 14:20:15 +0200 Subject: [PATCH 40/67] =?UTF-8?q?=F0=9F=A6=84=20example:=20update=20descri?= =?UTF-8?q?ption?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/rainbow_tweet/chrome_extension/content.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/rainbow_tweet/chrome_extension/content.js b/examples/rainbow_tweet/chrome_extension/content.js index 3bb910e..1444931 100644 --- a/examples/rainbow_tweet/chrome_extension/content.js +++ b/examples/rainbow_tweet/chrome_extension/content.js @@ -23,7 +23,7 @@ let observer = new MutationObserver((mutations) => { // Create new button let button = document.createElement('button'); if (openai_api_key === '') { - button.innerText = 'Set OPENAI_API_KEY by clicking the extension icon'; + button.innerText = 'Set OPENAI_API_KEY by clicking the Rainbow-Tweet icon and reload the page'; button.disabled = true; } else { button.innerText = '🦄'; From f7971757fe9a714da0ffc6247756d14f4d683e0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Wed, 17 May 2023 00:21:25 +0200 Subject: [PATCH 41/67] =?UTF-8?q?=F0=9F=94=8E=20feat:=20search=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 11 +++- dev_gpt/apis/gpt.py | 2 +- dev_gpt/apis/jina_cloud.py | 6 ++- dev_gpt/cli.py | 13 +++-- dev_gpt/constants.py | 4 ++ dev_gpt/options/configure/key_handling.py | 50 ++++++++--------- .../chains/auto_refine_description.py | 6 ++- .../generate/chains/question_answering.py | 33 +++++++++--- dev_gpt/options/generate/generator.py | 8 +-- dev_gpt/options/generate/pm/pm.py | 3 +- .../static_files/gateway/app-template | 53 +++++++++++++++++++ .../static_files/microservice/apis.py | 32 +++++++++++ .../static_files/microservice/search.py | 29 ---------- dev_gpt/options/generate/templates_user.py | 34 +++++++++--- dev_gpt/options/generate/tools/__init__.py | 0 dev_gpt/options/generate/tools/tools.py | 9 ++++ test/unit/test_search.py | 4 +- test/unit/test_tools.py | 13 +++++ 18 files changed, 227 insertions(+), 83 deletions(-) create mode 100644 dev_gpt/options/generate/static_files/gateway/app-template delete mode 100644 dev_gpt/options/generate/static_files/microservice/search.py create mode 100644 dev_gpt/options/generate/tools/__init__.py create mode 100644 dev_gpt/options/generate/tools/tools.py create mode 100644 test/unit/test_tools.py diff --git a/README.md b/README.md index 61eb866..e9a29ec 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,9 @@ Your imagination is the limit!

-Welcome to Dev GPT, where we bring your ideas to life with the power of advanced artificial intelligence! Our automated development team is designed to create microservices tailored to your specific needs, making your software development process seamless and efficient. Comprised of a virtual Product Manager, Developer, and DevOps, our AI team ensures that every aspect of your project is covered, from concept to deployment. +Welcome to Dev-GPT, where we bring your ideas to life with the power of advanced artificial intelligence! +Our automated development team is designed to create microservices tailored to your specific needs, making your software development process seamless and efficient. +Comprised of a virtual Product Manager, Developer, and DevOps, our AI team ensures that every aspect of your project is covered, from concept to deployment. ## Quickstart @@ -65,8 +67,13 @@ dev-gpt generate ### Requirements - OpenAI key with access to gpt-3.5-turbo or gpt-4 +- if you want to enable your microservice to search for web content, +you need to set the GOOGLE_API_KEY and GOOGLE_CSE_ID environment variables. +More information can be found [here](https://developers.google.com/custom-search/v1/overview). ```bash -dev-gpt configure --key +dev-gpt configure --openai_api_key +dev-gpt configure --google_api_key (optional if you want to use google search) +dev-gpt configure --google_cse_id (optional if you want to use google search) ``` If you set the environment variable `OPENAI_API_KEY`, the configuration step can be skipped. diff --git a/dev_gpt/apis/gpt.py b/dev_gpt/apis/gpt.py index 44d329f..387649c 100644 --- a/dev_gpt/apis/gpt.py +++ b/dev_gpt/apis/gpt.py @@ -24,7 +24,7 @@ def configure_openai_api_key(): if 'OPENAI_API_KEY' not in os.environ: print_colored('You need to set OPENAI_API_KEY in your environment.', ''' Run: -dev-gpt configure --key +dev-gpt configure --openai_api_key If you have updated it already, please restart your terminal. ''', 'red') diff --git a/dev_gpt/apis/jina_cloud.py b/dev_gpt/apis/jina_cloud.py index c41b0fb..e7963db 100644 --- a/dev_gpt/apis/jina_cloud.py +++ b/dev_gpt/apis/jina_cloud.py @@ -98,7 +98,7 @@ def _push_executor(dir_path): 'public': 'True', 'private': 'False', 'verbose': 'True', - 'buildEnv': f'{{"OPENAI_API_KEY": "{os.environ["OPENAI_API_KEY"]}"}}', + 'buildEnv': f'{{"OPENAI_API_KEY": "{os.environ["OPENAI_API_KEY"]}", "GOOGLE_API_KEY": "{os.environ.get("GOOGLE_API_KEY","")}", "GOOGLE_CSE_ID": "{os.environ.get("GOOGLE_CSE_ID","")}"}}', 'md5sum': md5_digest, } with suppress_stdout(): @@ -251,7 +251,9 @@ executors: uses: {prefix}://{get_user_name(DEMO_TOKEN)}/{executor_name}:latest {"" if use_docker else "install-requirements: True"} env: - OPENAI_API_KEY: {os.environ['OPENAI_API_KEY']} + OPENAI_API_KEY: ${{{{ ENV.OPENAI_API_KEY }}}} + GOOGLE_API_KEY: ${{{{ ENV.GOOGLE_API_KEY }}}} + GOOGLE_CSE_ID: ${{{{ ENV.GOOGLE_CSE_ID }}}} jcloud: resources: instance: C2 diff --git a/dev_gpt/cli.py b/dev_gpt/cli.py index 08d2ada..094bcbd 100644 --- a/dev_gpt/cli.py +++ b/dev_gpt/cli.py @@ -92,9 +92,16 @@ def deploy(path): Deployer().deploy(path) @main.command() -@click.option('--key', required=True, help='Your OpenAI API key.') -def configure(key): - set_api_key(key) +@click.option('--openai-api-key', default=None, help='Your OpenAI API key.') +@click.option('--google-api-key', default=None, help='Your Google API key.') +@click.option('--google-cse-id', default=None, help='Your Google CSE ID.') +def configure(openai_api_key, google_api_key, google_cse_id): + if openai_api_key: + set_api_key('OPENAI_API_KEY', openai_api_key) + if google_api_key: + set_api_key('GOOGLE_API_KEY', google_api_key) + if google_cse_id: + set_api_key('GOOGLE_CSE_ID', google_cse_id) if __name__ == '__main__': diff --git a/dev_gpt/constants.py b/dev_gpt/constants.py index bb5910b..b50cef2 100644 --- a/dev_gpt/constants.py +++ b/dev_gpt/constants.py @@ -55,3 +55,7 @@ LANGUAGE_PACKAGES = [ 'vadersentiment' ] +SEARCH_PACKAGES = [ + 'googlesearch-python', 'google', 'googlesearch', 'google-api-python-client', 'pygooglenews', 'google-cloud' +] + diff --git a/dev_gpt/options/configure/key_handling.py b/dev_gpt/options/configure/key_handling.py index a2f8576..1012fab 100644 --- a/dev_gpt/options/configure/key_handling.py +++ b/dev_gpt/options/configure/key_handling.py @@ -40,26 +40,26 @@ def get_shell(): return None -def get_shell_config(key): +def get_shell_config(name, key): return { - "bash": {"config_file": "~/.bashrc", "export_line": f"export OPENAI_API_KEY={key}"}, - "zsh": {"config_file": "~/.zshrc", "export_line": f"export OPENAI_API_KEY={key}"}, - "sh": {"config_file": "~/.profile", "export_line": f"export OPENAI_API_KEY={key}"}, + "bash": {"config_file": "~/.bashrc", "export_line": f"export {name}={key}"}, + "zsh": {"config_file": "~/.zshrc", "export_line": f"export {name}={key}"}, + "sh": {"config_file": "~/.profile", "export_line": f"export {name}={key}"}, "fish": { "config_file": "~/.config/fish/config.fish", - "export_line": f"set -gx OPENAI_API_KEY {key}", + "export_line": f"set -gx {name} {key}", }, - "csh": {"config_file": "~/.cshrc", "export_line": f"setenv OPENAI_API_KEY {key}"}, - "tcsh": {"config_file": "~/.tcshrc", "export_line": f"setenv OPENAI_API_KEY {key}"}, - "ksh": {"config_file": "~/.kshrc", "export_line": f"export OPENAI_API_KEY={key}"}, - "dash": {"config_file": "~/.profile", "export_line": f"export OPENAI_API_KEY={key}"} + "csh": {"config_file": "~/.cshrc", "export_line": f"setenv {name} {key}"}, + "tcsh": {"config_file": "~/.tcshrc", "export_line": f"setenv {name} {key}"}, + "ksh": {"config_file": "~/.kshrc", "export_line": f"export {name}={key}"}, + "dash": {"config_file": "~/.profile", "export_line": f"export {name}={key}"} } -def set_env_variable(shell, key): - shell_config = get_shell_config(key) +def set_env_variable(shell, name, key): + shell_config = get_shell_config(name, key) if shell not in shell_config: - click.echo("Sorry, your shell is not supported. Please add the key OPENAI_API_KEY manually.") + click.echo(f"Sorry, your shell is not supported. Please add the key {name} manually.") return config_file = os.path.expanduser(shell_config[shell]["config_file"]) @@ -71,8 +71,8 @@ def set_env_variable(shell, key): export_line = shell_config[shell]['export_line'] # Update the existing API key if it exists, otherwise append it to the config file - if f"OPENAI_API_KEY" in content: - content = re.sub(r'OPENAI_API_KEY=.*', f'OPENAI_API_KEY={key}', content, flags=re.MULTILINE) + if f"{name}" in content: + content = re.sub(rf'{name}=.*', f'{name}={key}', content, flags=re.MULTILINE) with open(config_file, "w", encoding='utf-8') as file: file.write(content) @@ -81,7 +81,7 @@ def set_env_variable(shell, key): file.write(f"\n{export_line}\n") click.echo(f''' -✅ Success, OPENAI_API_KEY has been set in {config_file}. +✅ Success, {name} has been set in {config_file}. Please restart your shell to apply the changes or run: source {config_file} ''' @@ -91,21 +91,21 @@ source {config_file} click.echo(f"Error: {config_file} not found. Please set the environment variable manually.") -def set_api_key(key): +def set_api_key(name, key): system_platform = platform.system().lower() if system_platform == "windows": - set_env_variable_command = f'setx OPENAI_API_KEY "{key}"' + set_env_variable_command = f'setx {name} "{key}"' subprocess.call(set_env_variable_command, shell=True) - click.echo(''' -✅ Success, OPENAI_API_KEY has been set. + click.echo(f''' +✅ Success, {name} 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?"): + if f"{name}" in os.environ or is_key_set_in_config_file(key): + if not click.confirm(f"{name} is already set. Do you want to overwrite it?"): click.echo("Aborted.") return @@ -115,24 +115,24 @@ Please restart your Command Prompt to apply the changes. "Error: Unable to detect your shell or psutil is not available. Please set the environment variable manually.") return - set_env_variable(shell, key) + set_env_variable(shell, name, key) else: click.echo("Sorry, this platform is not supported.") -def is_key_set_in_config_file(key): +def is_key_set_in_config_file(name, key): shell = get_shell() if shell is None: return False - shell_config = get_shell_config(key) + shell_config = get_shell_config(name, key) config_file = os.path.expanduser(shell_config[shell]["config_file"]) try: with open(config_file, "r", encoding='utf-8') as file: content = file.read() - if f"OPENAI_API_KEY" in content: + if f"{name}" in content: return True except FileNotFoundError: pass diff --git a/dev_gpt/options/generate/chains/auto_refine_description.py b/dev_gpt/options/generate/chains/auto_refine_description.py index 09e9818..aef8e08 100644 --- a/dev_gpt/options/generate/chains/auto_refine_description.py +++ b/dev_gpt/options/generate/chains/auto_refine_description.py @@ -3,7 +3,7 @@ import json from dev_gpt.apis.gpt import ask_gpt from dev_gpt.options.generate.parser import identity_parser from dev_gpt.options.generate.prompt_factory import context_to_string - +from dev_gpt.options.generate.tools.tools import get_available_tools def auto_refine_description(context): @@ -36,7 +36,9 @@ def auto_refine_description(context): better_description_prompt = f'''{{context_string}} Update the description of the Microservice to make it more precise without adding or removing information. Note: the output must be a list of tasks the Microservice has to perform. -Example for the description: "return the average temperature of the 5 days weather forecast for a given location." +Note: you can uses two tools if necessary: +{get_available_tools()} +Example for the description: "return a description of the average temperature of the 5 days weather forecast for a given location." 1. get the 5 days weather forcast from the https://openweathermap.org/ API 2. extract the temperature from the response 3. calculate the average temperature''' diff --git a/dev_gpt/options/generate/chains/question_answering.py b/dev_gpt/options/generate/chains/question_answering.py index 2f191ce..eca3c35 100644 --- a/dev_gpt/options/generate/chains/question_answering.py +++ b/dev_gpt/options/generate/chains/question_answering.py @@ -1,25 +1,46 @@ from dev_gpt.apis.gpt import ask_gpt -from dev_gpt.options.generate.parser import boolean_parser +from dev_gpt.options.generate.parser import boolean_parser, identity_parser + def is_question_true(question): def fn(text): return answer_yes_no_question(text, question) + return fn + def is_question_false(question): return lambda context: not is_question_true(question)(context) def answer_yes_no_question(text, question): - prompt = question_prompt.format( - question=question, - text=text + pros_and_cons = ask_gpt( + pros_and_cons_prompt.format( + question=question, + text=text, + ), + identity_parser, ) - return ask_gpt(prompt, boolean_parser) + + return ask_gpt( + question_prompt.format( + text=text, + question=question, + pros_and_cons=pros_and_cons, + ), + boolean_parser) + +pros_and_cons_prompt = '''\ +# Context +{text} +# Question +{question} +Note: You must not answer the question. Instead, give up to 5 bullet points (10 words) arguing why the question should be answered with true or false.''' question_prompt = '''\ +# Context {text} +# Question {question} Note: You must answer "yes" or "no". ''' - diff --git a/dev_gpt/options/generate/generator.py b/dev_gpt/options/generate/generator.py index 22aabe7..b28d648 100644 --- a/dev_gpt/options/generate/generator.py +++ b/dev_gpt/options/generate/generator.py @@ -17,7 +17,7 @@ from dev_gpt.apis.pypi import is_package_on_pypi, clean_requirements_txt from dev_gpt.constants import FILE_AND_TAG_PAIRS, NUM_IMPLEMENTATION_STRATEGIES, MAX_DEBUGGING_ITERATIONS, \ BLACKLISTED_PACKAGES, EXECUTOR_FILE_NAME, TEST_EXECUTOR_FILE_NAME, TEST_EXECUTOR_FILE_TAG, \ REQUIREMENTS_FILE_NAME, REQUIREMENTS_FILE_TAG, DOCKER_FILE_NAME, IMPLEMENTATION_FILE_NAME, \ - IMPLEMENTATION_FILE_TAG, LANGUAGE_PACKAGES, UNNECESSARY_PACKAGES, DOCKER_BASE_IMAGE_VERSION + IMPLEMENTATION_FILE_TAG, LANGUAGE_PACKAGES, UNNECESSARY_PACKAGES, DOCKER_BASE_IMAGE_VERSION, SEARCH_PACKAGES from dev_gpt.options.generate.pm.pm import PM from dev_gpt.options.generate.templates_user import template_generate_microservice_name, \ template_generate_possible_packages, \ @@ -500,7 +500,7 @@ pytest description=self.microservice_specification.task )['strategies.json'] packages_list = [[pkg.strip().lower() for pkg in packages] for packages in json.loads(packages_json_string)] - packages_list = [[self.replace_with_gpt_3_5_turbo_if_possible(pkg) for pkg in packages] for packages in + packages_list = [[self.replace_with_tool_if_possible(pkg) for pkg in packages] for packages in packages_list] packages_list = self.filter_packages_list(packages_list) @@ -543,9 +543,11 @@ dev-gpt deploy --path {self.microservice_root_path} @staticmethod - def replace_with_gpt_3_5_turbo_if_possible(pkg): + def replace_with_tool_if_possible(pkg): if pkg in LANGUAGE_PACKAGES: return 'gpt_3_5_turbo' + if pkg in SEARCH_PACKAGES: + return 'google_custom_search' return pkg @staticmethod diff --git a/dev_gpt/options/generate/pm/pm.py b/dev_gpt/options/generate/pm/pm.py index 65a7d43..2156fac 100644 --- a/dev_gpt/options/generate/pm/pm.py +++ b/dev_gpt/options/generate/pm/pm.py @@ -60,7 +60,8 @@ Description of the microservice: microservice_description += self.user_input_extension_if_needed( context, microservice_description, - condition_question='Does the microservice send requests to an API?', + condition_question='''\ +Does the microservice send requests to an API beside the Google Custom Search API and gpt-3.5-turbo?''', question_gen='Generate a question that asks for the endpoint of the external API and an example of a request and response when interacting with the external API.', extension_name='Example of API usage', post_transformation_fn=translation(from_format='api instruction', to_format='python code snippet raw without formatting') diff --git a/dev_gpt/options/generate/static_files/gateway/app-template b/dev_gpt/options/generate/static_files/gateway/app-template new file mode 100644 index 0000000..cda8597 --- /dev/null +++ b/dev_gpt/options/generate/static_files/gateway/app-template @@ -0,0 +1,53 @@ +import json +import os +import base64 +import streamlit as st +from jina import Client, Document, DocumentArray + +st.set_page_config( + page_title="", + page_icon="", + layout="", + initial_sidebar_state="", +) + +st.title("
") +st.markdown( + "<10 word description here>" + "To deploy your own microservice, click [here](https://github.com/jina-ai/dev-gpt)." +) + +st.header(" Input Parameters") # only if input parameters are needed +with st.form(key="input_form"): + + +input_data = { + +} +input_json = json.dumps(input_data) + +# Process input and call microservice +if submit_button: + with st.spinner("Generating collage..."): + + + client = Client(host="http://localhost:8080") + d = Document(text=input_json) + response = client.post("/", inputs=DocumentArray([d])) + + output_data = json.loads(response[0].text) + + +# Display curl command +deployment_id = os.environ.get("K8S_NAMESPACE_NAME", "") +host = ( + f"https://dev-gpt-{deployment_id.split('-')[1]}.wolf.jina.ai/post" + if deployment_id + else "http://localhost:8080/post" +) +with st.expander("See curl command"): + st.markdown("You can use the following curl command to send a request to the microservice from the command line:") + st.code( + f'curl -X "POST" "{host}" -H "accept: application/json" -H "Content-Type: application/json" -d \'{{"data": [{{"text": "{input_json}"}}]}}\'', + language="bash", + ) \ No newline at end of file diff --git a/dev_gpt/options/generate/static_files/microservice/apis.py b/dev_gpt/options/generate/static_files/microservice/apis.py index 24dcb01..1c529da 100644 --- a/dev_gpt/options/generate/static_files/microservice/apis.py +++ b/dev_gpt/options/generate/static_files/microservice/apis.py @@ -21,3 +21,35 @@ class GPT_3_5_Turbo: }] ) return response.choices[0]['message']['content'] + + + +import os +from typing import Optional + +import requests + + +def google_search(search_term, search_type, top_n): + google_api_key: Optional[str] = os.environ['GOOGLE_API_KEY'] + google_cse_id: Optional[str] = os.environ['GOOGLE_CSE_ID'] + url = "https://www.googleapis.com/customsearch/v1" + params = { + 'q': search_term, + 'key': google_api_key, + 'cx': google_cse_id, + 'searchType': search_type, + 'num': top_n + } + response = requests.get(url, params=params) + response.raise_for_status() + return response.json() + +def search_images(search_term, top_n): + response = google_search(search_term, search_type="image", top_n=top_n) + return [item["link"] for item in response["items"]] + +def search_web(search_term, top_n): + response = google_search(search_term, search_type="web", top_n=top_n) + return [item["snippet"] for item in response["items"]] + diff --git a/dev_gpt/options/generate/static_files/microservice/search.py b/dev_gpt/options/generate/static_files/microservice/search.py deleted file mode 100644 index 4c1c082..0000000 --- a/dev_gpt/options/generate/static_files/microservice/search.py +++ /dev/null @@ -1,29 +0,0 @@ -import os -from typing import Optional - -import requests - - -def google_search(search_term, search_type, top_n): - google_api_key: Optional[str] = os.environ['GOOGLE_API_KEY'] - google_cse_id: Optional[str] = os.environ['GOOGLE_CSE_ID'] - url = "https://www.googleapis.com/customsearch/v1" - params = { - 'q': search_term, - 'key': google_api_key, - 'cx': google_cse_id, - 'searchType': search_type, - 'num': top_n - } - response = requests.get(url, params=params) - response.raise_for_status() - return response.json() - -def search_image(search_term, top_n): - response = google_search(search_term, search_type="image", top_n=top_n) - return [item["link"] for item in response["items"]] - -def search_web(search_term, top_n): - response = google_search(search_term, search_type="web", top_n=top_n) - return [item["snippet"] for item in response["items"]] - diff --git a/dev_gpt/options/generate/templates_user.py b/dev_gpt/options/generate/templates_user.py index 3f6a07f..2aaf0a4 100644 --- a/dev_gpt/options/generate/templates_user.py +++ b/dev_gpt/options/generate/templates_user.py @@ -91,7 +91,7 @@ Note that you must obey the double asterisk and triple backtick syntax from like You must provide the complete file with the exact same syntax to wrap the code.''' -gpt_35_turbo_usage_string = """If need to use gpt_3_5_turbo, then this is an example on how to use it: +gpt_35_turbo_usage_string = """If you need to use gpt_3_5_turbo, then use it like shown in the following example: ``` from .apis import GPT_3_5_Turbo @@ -106,17 +106,35 @@ generated_string = gpt(prompt) # fill-in the prompt (str); the output is a stri ``` """ +google_custom_search_usage_string = """If you need to use google_custom_search, then use it like shown in the following example: +a) when searching for text: +``` +from .apis import search_web + +# input: search term (str), top_n (int) +# output: list of strings +string_list = search_web('', top_n=10) +``` +b) when searching for images: +``` +from .apis import search_images + +# input: search term (str), top_n (int) +# output: list of image urls +image_url_list = search_images('', top_n=10) +``` +""" template_generate_function = PromptTemplate.from_template( - general_guidelines_string + ''' + general_guidelines_string + f''' Write a python function which receives as \ input json string (that can be parsed with the python function json.loads) and \ outputs a json string (that can be parsed with the python function json.loads). \ The function is called 'func'. -The function must fulfill the following description: '{microservice_description}'. -It will be tested with the following scenario: '{test_description}'. -For the implementation use the following package(s): '{packages}'. +The function must fulfill the following description: '{{microservice_description}}'. +It will be tested with the following scenario: '{{test_description}}'. +For the implementation use the following package(s): '{{packages}}'. The code must start with the following imports: ``` @@ -124,14 +142,16 @@ from .apis import GPT_3_5_Turbo import json ``` Obey the following rules: -''' + not_allowed_function_string + ''' +{not_allowed_function_string} Your approach: 1. Identify the core challenge when implementing the function. 2. Think about solutions for these challenges. 3. Decide for one of the solutions. 4. Write the code for the function. Don't write code for the test. -''' + gpt_35_turbo_usage_string + '\n' + template_code_wrapping_string +{gpt_35_turbo_usage_string} +{google_custom_search_usage_string} +{template_code_wrapping_string}''' ) diff --git a/dev_gpt/options/generate/tools/__init__.py b/dev_gpt/options/generate/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dev_gpt/options/generate/tools/tools.py b/dev_gpt/options/generate/tools/tools.py new file mode 100644 index 0000000..02eebda --- /dev/null +++ b/dev_gpt/options/generate/tools/tools.py @@ -0,0 +1,9 @@ +import os + + +def get_available_tools(): + tools = ['gpt-3.5-turbo (for any kind of text processing like summarization, paraphrasing, etc.)'] + if os.environ.get('GOOGLE_API_KEY') and os.environ.get('GOOGLE_CSE_ID'): + tools.append('Google Custom Search API') + chars = 'abcdefghijklmnopqrstuvwxyz' + return '\n'.join([f'{char}) {tool}' for tool, char in zip(tools, chars)]) \ No newline at end of file diff --git a/test/unit/test_search.py b/test/unit/test_search.py index fe15dfd..2215939 100644 --- a/test/unit/test_search.py +++ b/test/unit/test_search.py @@ -1,4 +1,4 @@ -from dev_gpt.options.generate.static_files.microservice.search import search_web, search_image +from dev_gpt.options.generate.static_files.microservice.search import search_web, search_images def test_web_search(): @@ -8,6 +8,6 @@ def test_web_search(): assert not results[0].startswith("http") def test_image_search(): - results = search_image("jina", 10) + results = search_images("jina", 10) assert len(results) == 10 assert results[0].startswith("http") diff --git a/test/unit/test_tools.py b/test/unit/test_tools.py new file mode 100644 index 0000000..692c9c8 --- /dev/null +++ b/test/unit/test_tools.py @@ -0,0 +1,13 @@ +import os + +from dev_gpt.options.generate.tools.tools import get_available_tools + + +def test_all_tools(): + tool_lines = get_available_tools().split('\n') + assert len(tool_lines) == 2 + +def test_no_search(): + os.environ['GOOGLE_API_KEY'] = '' + tool_lines = get_available_tools().split('\n') + assert len(tool_lines) == 1 \ No newline at end of file From 0df787026ce5a9078aefa181be44965293e1acd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Wed, 17 May 2023 00:34:15 +0200 Subject: [PATCH 42/67] =?UTF-8?q?=F0=9F=94=8E=20feat:=20search=20api=20-?= =?UTF-8?q?=20fix=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/unit/test_search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/test_search.py b/test/unit/test_search.py index 2215939..7f16179 100644 --- a/test/unit/test_search.py +++ b/test/unit/test_search.py @@ -1,4 +1,4 @@ -from dev_gpt.options.generate.static_files.microservice.search import search_web, search_images +from dev_gpt.options.generate.static_files.microservice.apis import search_web, search_images def test_web_search(): From 7b5ec7e8fb380c66356477dfc67e93f09b124372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Thu, 18 May 2023 19:12:55 +0200 Subject: [PATCH 43/67] =?UTF-8?q?=F0=9F=94=8E=20feat:=20search=20api=20-?= =?UTF-8?q?=20fix=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev_gpt/constants.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dev_gpt/constants.py b/dev_gpt/constants.py index b50cef2..6888758 100644 --- a/dev_gpt/constants.py +++ b/dev_gpt/constants.py @@ -43,6 +43,7 @@ DEMO_TOKEN = '45372338e04f5a41af949024db929d46' BLACKLISTED_PACKAGES = [ 'moderngl', 'pyopengl', 'pyglet', 'pythreejs', 'panda3d', # because they need a screen, 'tika', # because it needs java + 'clearbit' # because of installation issues on latest version ] UNNECESSARY_PACKAGES = [ 'fastapi', 'uvicorn', 'starlette' # because the wrappers are used instead From 2f91cb4804585b8c04afe2766549a342d5a723ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Thu, 18 May 2023 19:14:24 +0200 Subject: [PATCH 44/67] =?UTF-8?q?=F0=9F=8C=88=20docs:=20example=20light=20?= =?UTF-8?q?mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chrome_extension/manifest.json | 2 +- .../rainbow_tweet/chrome_extension/popup.html | 4 +-- .../rainbow_tweet/chrome_extension/styles.css | 35 ++++++++----------- 3 files changed, 17 insertions(+), 24 deletions(-) diff --git a/examples/rainbow_tweet/chrome_extension/manifest.json b/examples/rainbow_tweet/chrome_extension/manifest.json index e2429db..2b9be91 100644 --- a/examples/rainbow_tweet/chrome_extension/manifest.json +++ b/examples/rainbow_tweet/chrome_extension/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Rainbow-Tweet", "description": "The Rainbow-Tweet plugin allows the user to convert any tweet into positive language by clicking a button on the tweet.", - "version": "0.0.0.1", + "version": "0.0.1.0", "icons": { "128": "logo.png" }, diff --git a/examples/rainbow_tweet/chrome_extension/popup.html b/examples/rainbow_tweet/chrome_extension/popup.html index 7b322aa..f3259fe 100644 --- a/examples/rainbow_tweet/chrome_extension/popup.html +++ b/examples/rainbow_tweet/chrome_extension/popup.html @@ -6,14 +6,14 @@
-

Twitter Rewrite: Extension Options

+

Rainbow-Tweet: Extension Options



Enter your OpenAI API Key to start using the plugin. If you don't have one, create it here. + target="_blank">here. After inserting the key, you need to reload the page
diff --git a/examples/rainbow_tweet/chrome_extension/styles.css b/examples/rainbow_tweet/chrome_extension/styles.css index 2169abf..bf6f9c9 100644 --- a/examples/rainbow_tweet/chrome_extension/styles.css +++ b/examples/rainbow_tweet/chrome_extension/styles.css @@ -1,7 +1,5 @@ .modify-button { - /* transparent */ - background-color: #00000000; - color: white; + /* common styles */ border: none; padding: 5px 10px; text-align: center; @@ -11,29 +9,24 @@ margin: 4px 2px; cursor: pointer; border-radius: 3px; - box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); /* Add some shadow */ + background: transparent; /* Make the button transparent */ } +/* Dark mode */ +@media (prefers-color-scheme: dark) { + .modify-button { + color: white; /* Light text for dark mode */ + } +} -/*!* Dynamic rainbow colors for each letter *!*/ -/*@keyframes rainbow {*/ -/* 0% { color: hsl(0, 100%, 50%); }*/ -/* 14% { color: hsl(60, 100%, 50%); }*/ -/* 28% { color: hsl(120, 100%, 50%); }*/ -/* 42% { color: hsl(180, 100%, 50%); }*/ -/* 57% { color: hsl(240, 100%, 50%); }*/ -/* 71% { color: hsl(300, 100%, 50%); }*/ -/* 85% { color: hsl(360, 100%, 50%); }*/ -/* 100% { color: hsl(0, 100%, 50%); }*/ -/*}*/ +/* Light mode */ +@media (prefers-color-scheme: light) { + .modify-button { + color: #000; /* Dark text for light mode */ + } +} - -/*.rainbow-text {*/ -/* animation: rainbow 7s linear infinite;*/ -/* animation-delay: calc(.07s * var(--i));*/ -/*}*/ - /* Light mode colors (darker) */ @keyframes rainbow-light { 0% { color: hsl(0, 100%, 30%); } From 65e6791db40bb02613693b6d83f150f39dec6235 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Thu, 18 May 2023 19:17:31 +0200 Subject: [PATCH 45/67] =?UTF-8?q?=F0=9F=94=8E=20feat:=20search=20api=20-?= =?UTF-8?q?=20ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3dc304..87855dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,8 @@ jobs: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} SCENEX_API_KEY: ${{ secrets.SCENEX_API_KEY }} WHISPER_API_KEY: ${{ secrets.WHISPER_API_KEY }} + GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} + GOOGLE_CSE_ID: ${{ secrets.GOOGLE_CSE_ID }} test_unit: runs-on: ubuntu-latest From 3101bd1978f01eae71d46e56c166d5537d06573e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Thu, 18 May 2023 19:18:01 +0200 Subject: [PATCH 46/67] =?UTF-8?q?=F0=9F=94=8E=20feat:=20search=20api=20-?= =?UTF-8?q?=20ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 87855dd..af6a700 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,6 +60,8 @@ jobs: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} SCENEX_API_KEY: ${{ secrets.SCENEX_API_KEY }} WHISPER_API_KEY: ${{ secrets.WHISPER_API_KEY }} + GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} + GOOGLE_CSE_ID: ${{ secrets.GOOGLE_CSE_ID }} base-image-push: runs-on: ubuntu-latest From e1f0205663c819aee4c8232adc218b973509e572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Thu, 18 May 2023 20:08:41 +0200 Subject: [PATCH 47/67] =?UTF-8?q?=F0=9F=94=8E=20feat:=20search=20fix=20web?= =?UTF-8?q?=20search?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev_gpt/options/generate/static_files/microservice/apis.py | 2 +- test/unit/test_search.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dev_gpt/options/generate/static_files/microservice/apis.py b/dev_gpt/options/generate/static_files/microservice/apis.py index 1c529da..527d13a 100644 --- a/dev_gpt/options/generate/static_files/microservice/apis.py +++ b/dev_gpt/options/generate/static_files/microservice/apis.py @@ -38,7 +38,7 @@ def google_search(search_term, search_type, top_n): 'q': search_term, 'key': google_api_key, 'cx': google_cse_id, - 'searchType': search_type, + **({'searchType': search_type} if search_type == 'image' else {}), 'num': top_n } response = requests.get(url, params=params) diff --git a/test/unit/test_search.py b/test/unit/test_search.py index 7f16179..b5839fd 100644 --- a/test/unit/test_search.py +++ b/test/unit/test_search.py @@ -4,7 +4,7 @@ from dev_gpt.options.generate.static_files.microservice.apis import search_web, def test_web_search(): results = search_web("jina", 10) assert len(results) == 10 - assert "jina" in results[0] + assert "jina" in results[0].lower() assert not results[0].startswith("http") def test_image_search(): From 2475cacd94ea68f4efce983b476fafc895664799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Fri, 19 May 2023 13:47:44 +0200 Subject: [PATCH 48/67] =?UTF-8?q?=F0=9F=94=8E=20feat:=20search=20fix=20web?= =?UTF-8?q?=20search?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev_gpt/options/generate/templates_user.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dev_gpt/options/generate/templates_user.py b/dev_gpt/options/generate/templates_user.py index 2aaf0a4..8a6f0e0 100644 --- a/dev_gpt/options/generate/templates_user.py +++ b/dev_gpt/options/generate/templates_user.py @@ -16,7 +16,8 @@ The Dockerfile must not attach a virtual display when running test_microservice. not_allowed_function_string = '''The implemented function and the test must not use the GPU. The implemented function and the test must not access a database. The implemented function and the test must not access a display. -The implemented function and the test must not access external apis except unless it is explicitly mentioned in the description or test case (e.g. by mentioning the api that should be used or by providing a URL to access the data). +The implemented function and the test must not access external apis unless it is explicitly mentioned. +The implemented function and the test must not be based on a large collection of hard-coded strings. The implemented function and the test must not load data from the local file system unless it was created by the implemented function itself. The implemented function and the test must not use a pre-trained model unless it is explicitly mentioned in the description. The implemented function and the test must not train a model. From 3bf687eec19a654c500a794a7e0bd4403cbaf268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Fri, 19 May 2023 14:22:04 +0200 Subject: [PATCH 49/67] =?UTF-8?q?=F0=9F=94=8E=20feat:=20search=20fix=20web?= =?UTF-8?q?=20search?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev_gpt/options/generate/templates_user.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dev_gpt/options/generate/templates_user.py b/dev_gpt/options/generate/templates_user.py index 8a6f0e0..ba9eb34 100644 --- a/dev_gpt/options/generate/templates_user.py +++ b/dev_gpt/options/generate/templates_user.py @@ -103,7 +103,7 @@ When you get asked something like 'Who was having a date with ?', then you an You must not answer something else - only the json. \'\'\') -generated_string = gpt(prompt) # fill-in the prompt (str); the output is a string +generated_string = gpt_3_5_turbo("example user prompt") # prompt is a string; generated_string is a string ``` """ @@ -141,6 +141,7 @@ The code must start with the following imports: ``` from .apis import GPT_3_5_Turbo import json +import requests ``` Obey the following rules: {not_allowed_function_string} From 513ce588fe44e3d5d2846fc220550d84c1064185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Fri, 19 May 2023 14:34:12 +0200 Subject: [PATCH 50/67] =?UTF-8?q?=F0=9F=94=8E=20feat:=20search=20fix=20web?= =?UTF-8?q?=20search?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev_gpt/options/generate/templates_user.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dev_gpt/options/generate/templates_user.py b/dev_gpt/options/generate/templates_user.py index ba9eb34..aa70f4d 100644 --- a/dev_gpt/options/generate/templates_user.py +++ b/dev_gpt/options/generate/templates_user.py @@ -363,9 +363,10 @@ Note that any changes needed to make the test pass must be written under the con ''' + f'{not_allowed_function_string}\n{not_allowed_docker_string}\n{gpt_35_turbo_usage_string}' + ''' -After thinking about the possible solutions, output them as JSON ranked from best to worst. Like this: +After thinking about the possible solutions, output them as JSON ranked from best to worst. +You must use the following format: ''' + response_format_suggest_solutions + ''' -Ensure the response can be parsed by Python json.loads''' +Ensure the response starts with **solutions.json** and can be parsed by Python json.loads''' ) From 328eee7f18b2acc45d8d3e7fee6cd49f59596269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Fri, 19 May 2023 15:10:15 +0200 Subject: [PATCH 51/67] =?UTF-8?q?=F0=9F=94=8E=20feat:=20search=20fix=20web?= =?UTF-8?q?=20search?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev_gpt/options/generate/generator.py | 6 +- dev_gpt/options/generate/pm/pm.py | 211 +++++++++--------- .../options/generate/pm/task_tree_schema.py | 42 ++-- dev_gpt/options/generate/templates_user.py | 9 +- 4 files changed, 136 insertions(+), 132 deletions(-) diff --git a/dev_gpt/options/generate/generator.py b/dev_gpt/options/generate/generator.py index b28d648..661cdce 100644 --- a/dev_gpt/options/generate/generator.py +++ b/dev_gpt/options/generate/generator.py @@ -23,7 +23,7 @@ from dev_gpt.options.generate.templates_user import template_generate_microservi template_generate_possible_packages, \ template_implement_solution_code_issue, \ template_solve_pip_dependency_issue, template_is_dependency_issue, template_generate_playground, \ - template_generate_function, template_generate_test, template_generate_requirements, \ + template_generate_function_constructor, template_generate_test, template_generate_requirements, \ template_chain_of_thought, template_summarize_error, \ template_solve_apt_get_dependency_issue, \ template_suggest_solutions_code_issue, template_was_error_seen_before, \ @@ -197,9 +197,11 @@ metas: with open(os.path.join(os.path.dirname(__file__), 'static_files', 'microservice', 'apis.py'), 'r', encoding='utf-8') as f: persist_file(f.read(), os.path.join(self.cur_microservice_path, 'apis.py')) + is_using_gpt_3_5_turbo = 'gpt-3-5-turbo' in packages + is_using_google_custom_search = 'google-custom-search' in packages microservice_content = self.generate_and_persist_file( section_title='Microservice', - template=template_generate_function, + template=template_generate_function_constructor(is_using_gpt_3_5_turbo, is_using_google_custom_search), microservice_description=self.microservice_specification.task, test_description=self.microservice_specification.test, packages=packages, diff --git a/dev_gpt/options/generate/pm/pm.py b/dev_gpt/options/generate/pm/pm.py index 2156fac..83a3d98 100644 --- a/dev_gpt/options/generate/pm/pm.py +++ b/dev_gpt/options/generate/pm/pm.py @@ -5,7 +5,8 @@ from dev_gpt.options.generate.chains.question_answering import is_question_true from dev_gpt.options.generate.chains.translation import translation from dev_gpt.options.generate.chains.user_confirmation_feedback_loop import user_feedback_loop from dev_gpt.options.generate.chains.get_user_input_if_needed import get_user_input_if_needed -from dev_gpt.options.generate.parser import identity_parser +from dev_gpt.options.generate.parser import identity_parser, json_parser +from dev_gpt.options.generate.pm.task_tree_schema import TaskTree from dev_gpt.options.generate.prompt_factory import make_prompt_friendly from dev_gpt.options.generate.ui import get_random_employee @@ -35,9 +36,9 @@ Description of the microservice: def refine(self, microservice_description): microservice_description, test_description = self.refine_description(microservice_description) - return microservice_description, test_description - # sub_task_tree = self.construct_sub_task_tree(microservice_description) + # sub_task_tree = construct_sub_task_tree(microservice_description) # return sub_task_tree + return microservice_description, test_description def refine_description(self, microservice_description): context = {'microservice_description': microservice_description} @@ -128,44 +129,44 @@ Example: # microservice_description=microservice_description # ) # -# def construct_sub_task_tree(self, microservice_description): -# """ -# takes a microservice description and recursively constructs a tree of sub-tasks that need to be done to implement the microservice -# """ -# # -# # nlp_fns = self.get_nlp_fns( -# # microservice_description -# # ) -# -# sub_task_tree_dict = ask_gpt( -# construct_sub_task_tree_prompt, json_parser, -# microservice_description=microservice_description, -# # nlp_fns=nlp_fns -# ) -# reflections = ask_gpt( -# sub_task_tree_reflections_prompt, identity_parser, -# microservice_description=microservice_description, -# # nlp_fns=nlp_fns, -# sub_task_tree=sub_task_tree_dict, -# ) -# solutions = ask_gpt( -# sub_task_tree_solutions_prompt, identity_parser, -# # nlp_fns=nlp_fns, -# microservice_description=microservice_description, sub_task_tree=sub_task_tree_dict, -# reflections=reflections, -# ) -# sub_task_tree_updated = ask_gpt( -# sub_task_tree_update_prompt, -# json_parser, -# microservice_description=microservice_description, -# # nlp_fns=nlp_fns, -# sub_task_tree=sub_task_tree_dict, solutions=solutions -# ) -# # for task_dict in self.iterate_over_sub_tasks(sub_task_tree_updated): -# # task_dict.update(self.get_additional_task_info(task_dict['task'])) -# -# sub_task_tree = TaskTree.parse_obj(sub_task_tree_updated) -# return sub_task_tree +def construct_sub_task_tree(self, microservice_description): + """ + takes a microservice description and recursively constructs a tree of sub-tasks that need to be done to implement the microservice + """ + # + # nlp_fns = self.get_nlp_fns( + # microservice_description + # ) + + sub_task_tree_dict = ask_gpt( + construct_sub_task_tree_prompt, json_parser, + microservice_description=microservice_description, + # nlp_fns=nlp_fns + ) + reflections = ask_gpt( + sub_task_tree_reflections_prompt, identity_parser, + microservice_description=microservice_description, + # nlp_fns=nlp_fns, + sub_task_tree=sub_task_tree_dict, + ) + solutions = ask_gpt( + sub_task_tree_solutions_prompt, identity_parser, + # nlp_fns=nlp_fns, + microservice_description=microservice_description, sub_task_tree=sub_task_tree_dict, + reflections=reflections, + ) + sub_task_tree_updated = ask_gpt( + sub_task_tree_update_prompt, + json_parser, + microservice_description=microservice_description, + # nlp_fns=nlp_fns, + sub_task_tree=sub_task_tree_dict, solutions=solutions + ) + # for task_dict in self.iterate_over_sub_tasks(sub_task_tree_updated): + # task_dict.update(self.get_additional_task_info(task_dict['task'])) + + sub_task_tree = TaskTree.parse_obj(sub_task_tree_updated) + return sub_task_tree # def get_additional_task_info(self, sub_task_description): # additional_info_dict = self.get_additional_infos( @@ -281,71 +282,71 @@ Example: # Note: You must ignore facts that are unknown. # Note: You must ignore facts that are unclear.''' -# construct_sub_task_tree_prompt = client_description + ''' -# Recursively constructs a tree of functions that need to be implemented for the endpoint_function that retrieves a json string and returns a json string. -# Example: -# Input: "Input: list of integers, Output: Audio file of short story where each number is mentioned exactly once." -# Output: -# {{ -# "description": "Create an audio file containing a short story in which each integer from the provided list is seamlessly incorporated, ensuring that every integer is mentioned exactly once.", -# "python_fn_signature": "def generate_integer_story_audio(numbers: List[int]) -> str:", -# "sub_fns": [ -# {{ -# "description": "Generate sentence from integer.", -# "python_fn_signature": "def generate_sentence_from_integer(number: int) -> int:", -# "sub_fns": [] -# }}, -# {{ -# "description": "Convert the story into an audio file.", -# "python_fn_signature": "def convert_story_to_audio(story: str) -> bytes:", -# "sub_fns": [] -# }} -# ] -# }} -# -# Note: you must only output the json string - nothing else. -# Note: you must pretty print the json string.''' +construct_sub_task_tree_prompt = client_description + ''' +Recursively constructs a tree of functions that need to be implemented for the endpoint_function that retrieves a json string and returns a json string. +Example: +Input: "Input: list of integers, Output: Audio file of short story where each number is mentioned exactly once." +Output: +{{ + "description": "Create an audio file containing a short story in which each integer from the provided list is seamlessly incorporated, ensuring that every integer is mentioned exactly once.", + "python_fn_signature": "def generate_integer_story_audio(numbers: List[int]) -> str:", + "sub_fns": [ + {{ + "description": "Generate sentence from integer.", + "python_fn_signature": "def generate_sentence_from_integer(number: int) -> int:", + "sub_fns": [] + }}, + {{ + "description": "Convert the story into an audio file.", + "python_fn_signature": "def convert_story_to_audio(story: str) -> bytes:", + "sub_fns": [] + }} + ] +}} -# sub_task_tree_reflections_prompt = client_description + ''' -# Sub task tree: -# ``` -# {sub_task_tree} -# ``` -# Write down 3 arguments why the sub task tree might not perfectly represents the information mentioned in the microservice description. (5 words per argument)''' -# -# sub_task_tree_solutions_prompt = client_description + ''' -# Sub task tree: -# ``` -# {sub_task_tree} -# ``` -# Reflections: -# ``` -# {reflections} -# ``` -# For each constructive criticism, write a solution (5 words) that address the criticism.''' -# -# sub_task_tree_update_prompt = client_description + ''' -# Sub task tree: -# ``` -# {sub_task_tree} -# ``` -# Solutions: -# ``` -# {solutions} -# ``` -# Update the sub task tree by applying the solutions. (pretty print the json string)''' -# -# ask_questions_prompt = client_description + ''' -# Request json schema: -# ``` -# {request_schema} -# ``` -# Response json schema: -# ``` -# {response_schema} -# ``` -# Ask the user up to 5 unique detailed questions (5 words) about the microservice description that are not yet answered. -# ''' +Note: you must only output the json string - nothing else. +Note: you must pretty print the json string.''' + +sub_task_tree_reflections_prompt = client_description + ''' +Sub task tree: +``` +{sub_task_tree} +``` +Write down 3 arguments why the sub task tree might not perfectly represents the information mentioned in the microservice description. (5 words per argument)''' + +sub_task_tree_solutions_prompt = client_description + ''' +Sub task tree: +``` +{sub_task_tree} +``` +Reflections: +``` +{reflections} +``` +For each constructive criticism, write a solution (5 words) that address the criticism.''' + +sub_task_tree_update_prompt = client_description + ''' +Sub task tree: +``` +{sub_task_tree} +``` +Solutions: +``` +{solutions} +``` +Update the sub task tree by applying the solutions. (pretty print the json string)''' + +ask_questions_prompt = client_description + ''' +Request json schema: +``` +{request_schema} +``` +Response json schema: +``` +{response_schema} +``` +Ask the user up to 5 unique detailed questions (5 words) about the microservice description that are not yet answered. +''' # answer_questions_prompt = client_description + ''' # Request json schema: diff --git a/dev_gpt/options/generate/pm/task_tree_schema.py b/dev_gpt/options/generate/pm/task_tree_schema.py index 41035fc..39f3518 100644 --- a/dev_gpt/options/generate/pm/task_tree_schema.py +++ b/dev_gpt/options/generate/pm/task_tree_schema.py @@ -1,22 +1,22 @@ -# from typing import Dict, List, Union, Optional -# from pydantic import BaseModel, Field -# -# class JSONSchema(BaseModel): -# type: str -# format: Union[str, None] = None -# items: Union['JSONSchema', None] = None -# properties: Dict[str, 'JSONSchema'] = Field(default_factory=dict) -# additionalProperties: Union[bool, 'JSONSchema'] = True -# required: List[str] = Field(default_factory=list) -# -# class Config: -# arbitrary_types_allowed = True -# -# class TaskTree(BaseModel): -# description: Optional[str] -# python_fn_signature: str -# sub_fns: List['TaskTree'] -# -# JSONSchema.update_forward_refs() -# TaskTree.update_forward_refs() +from typing import Dict, List, Union, Optional +from pydantic import BaseModel, Field + +class JSONSchema(BaseModel): + type: str + format: Union[str, None] = None + items: Union['JSONSchema', None] = None + properties: Dict[str, 'JSONSchema'] = Field(default_factory=dict) + additionalProperties: Union[bool, 'JSONSchema'] = True + required: List[str] = Field(default_factory=list) + + class Config: + arbitrary_types_allowed = True + +class TaskTree(BaseModel): + description: Optional[str] + python_fn_signature: str + sub_fns: List['TaskTree'] + +JSONSchema.update_forward_refs() +TaskTree.update_forward_refs() # diff --git a/dev_gpt/options/generate/templates_user.py b/dev_gpt/options/generate/templates_user.py index aa70f4d..5b0dd66 100644 --- a/dev_gpt/options/generate/templates_user.py +++ b/dev_gpt/options/generate/templates_user.py @@ -126,7 +126,8 @@ image_url_list = search_images('', top_n=10) ``` """ -template_generate_function = PromptTemplate.from_template( +def template_generate_function_constructor(is_using_gpt_3_5_turbo, is_using_google_custom_search): + return PromptTemplate.from_template( general_guidelines_string + f''' Write a python function which receives as \ @@ -151,10 +152,10 @@ Your approach: 2. Think about solutions for these challenges. 3. Decide for one of the solutions. 4. Write the code for the function. Don't write code for the test. -{gpt_35_turbo_usage_string} -{google_custom_search_usage_string} +{gpt_35_turbo_usage_string if is_using_gpt_3_5_turbo else ''} +{google_custom_search_usage_string if is_using_google_custom_search else ''} {template_code_wrapping_string}''' -) + ) template_generate_test = PromptTemplate.from_template( From 1c375f3d50e24ddae6192fe1b257b23d08b56460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Fri, 19 May 2023 15:26:38 +0200 Subject: [PATCH 52/67] =?UTF-8?q?=F0=9F=94=8E=20feat:=20search=20fix=20web?= =?UTF-8?q?=20search?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev_gpt/options/generate/generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev_gpt/options/generate/generator.py b/dev_gpt/options/generate/generator.py index 661cdce..b33a7ed 100644 --- a/dev_gpt/options/generate/generator.py +++ b/dev_gpt/options/generate/generator.py @@ -197,7 +197,7 @@ metas: with open(os.path.join(os.path.dirname(__file__), 'static_files', 'microservice', 'apis.py'), 'r', encoding='utf-8') as f: persist_file(f.read(), os.path.join(self.cur_microservice_path, 'apis.py')) - is_using_gpt_3_5_turbo = 'gpt-3-5-turbo' in packages + is_using_gpt_3_5_turbo = 'gpt_3_5_turbo' in packages is_using_google_custom_search = 'google-custom-search' in packages microservice_content = self.generate_and_persist_file( section_title='Microservice', From 4a70183a6f42cdd31423a6dfd7a6ea11578d6979 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Fri, 19 May 2023 15:48:11 +0200 Subject: [PATCH 53/67] =?UTF-8?q?=F0=9F=94=8E=20feat:=20search=20fix=20web?= =?UTF-8?q?=20search?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev_gpt/options/generate/templates_user.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dev_gpt/options/generate/templates_user.py b/dev_gpt/options/generate/templates_user.py index 5b0dd66..3078e23 100644 --- a/dev_gpt/options/generate/templates_user.py +++ b/dev_gpt/options/generate/templates_user.py @@ -170,6 +170,7 @@ The test must start with the following imports: ``` from .microservice import func import json +import requests ``` ''' + not_allowed_function_string + ''' The test must not open local files. From e20952141e1a3c1a8a51a8f1835f5be0006631ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Fri, 19 May 2023 16:12:26 +0200 Subject: [PATCH 54/67] =?UTF-8?q?=F0=9F=94=8E=20feat:=20search=20fix=20web?= =?UTF-8?q?=20search?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev_gpt/constants.py | 2 +- dev_gpt/options/generate/templates_user.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dev_gpt/constants.py b/dev_gpt/constants.py index 6888758..4326065 100644 --- a/dev_gpt/constants.py +++ b/dev_gpt/constants.py @@ -50,7 +50,7 @@ UNNECESSARY_PACKAGES = [ ] LANGUAGE_PACKAGES = [ - 'allennlp', 'bertopic', 'fasttext', 'flair', 'gensim', 'nltk', 'openai', + 'allennlp', 'bertopic', 'GPT-3', 'fasttext', 'flair', 'gensim', 'nltk', 'openai', 'pattern', 'polyglot', 'pytorch-transformers', 'rasa', 'sentence-transformers', 'spacy', 'stanza', 'summarizer', 'sumy', 'textblob', 'textstat', 'transformers', 'vadersentiment' diff --git a/dev_gpt/options/generate/templates_user.py b/dev_gpt/options/generate/templates_user.py index 3078e23..ab36ada 100644 --- a/dev_gpt/options/generate/templates_user.py +++ b/dev_gpt/options/generate/templates_user.py @@ -89,7 +89,7 @@ Note that you must obey the double asterisk and triple backtick syntax from like ```{tag_name} ...code... ``` -You must provide the complete file with the exact same syntax to wrap the code.''' +You must provide the complete {file_name} wrapped with the exact syntax shown above.''' gpt_35_turbo_usage_string = """If you need to use gpt_3_5_turbo, then use it like shown in the following example: From cda99f80d259fdec7040e335f779b5cfd490652a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Fri, 19 May 2023 16:45:28 +0200 Subject: [PATCH 55/67] =?UTF-8?q?=F0=9F=94=8E=20feat:=20search=20fix=20web?= =?UTF-8?q?=20search?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev_gpt/options/generate/templates_user.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev_gpt/options/generate/templates_user.py b/dev_gpt/options/generate/templates_user.py index ab36ada..5b61064 100644 --- a/dev_gpt/options/generate/templates_user.py +++ b/dev_gpt/options/generate/templates_user.py @@ -126,6 +126,7 @@ image_url_list = search_images('', top_n=10) ``` """ +linebreak = '\n' def template_generate_function_constructor(is_using_gpt_3_5_turbo, is_using_google_custom_search): return PromptTemplate.from_template( general_guidelines_string + f''' @@ -139,8 +140,7 @@ It will be tested with the following scenario: '{{test_description}}'. For the implementation use the following package(s): '{{packages}}'. The code must start with the following imports: -``` -from .apis import GPT_3_5_Turbo +```{linebreak +'from .apis import GPT_3_5_Turbo' if is_using_gpt_3_5_turbo else ""} import json import requests ``` From 077f03b049bbb31a8a1349c10500fed82a883033 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Fri, 19 May 2023 18:09:13 +0200 Subject: [PATCH 56/67] feat: search fix web search --- dev_gpt/options/generate/generator.py | 9 +++++--- dev_gpt/options/generate/templates_user.py | 3 ++- test/integration/test_generator.py | 24 ++++++++++++++-------- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/dev_gpt/options/generate/generator.py b/dev_gpt/options/generate/generator.py index b33a7ed..f856579 100644 --- a/dev_gpt/options/generate/generator.py +++ b/dev_gpt/options/generate/generator.py @@ -40,9 +40,10 @@ class TaskSpecification: class Generator: - def __init__(self, task_description, path, model='gpt-4'): + def __init__(self, task_description, path, model='gpt-4', self_healing=True): self.gpt_session = gpt.GPTSession(model=model) self.microservice_specification = TaskSpecification(task=task_description, test=None) + self.self_healing = self_healing self.microservice_root_path = path self.microservice_name = None self.previous_microservice_path = None @@ -325,7 +326,7 @@ pytest if not is_executor_in_hub(gateway_name): raise Exception(f'{self.microservice_name} not in hub. Hubble logs: {hubble_log}') - def debug_microservice(self, num_approach, packages): + def debug_microservice(self, num_approach, packages, self_healing): for i in range(1, MAX_DEBUGGING_ITERATIONS): print('Debugging iteration', i) print('Trying to debug the microservice. Might take a while...') @@ -333,6 +334,8 @@ pytest log_hubble = push_executor(self.cur_microservice_path) error = process_error_message(log_hubble) if error: + if not self_healing: + raise Exception('Self-healing is disabled. Please fix the error manually.', error) print('An error occurred during the build process. Feeding the error back to the assistant...') self.previous_microservice_path = self.cur_microservice_path self.cur_microservice_path = get_microservice_path( @@ -520,7 +523,7 @@ pytest for num_approach, packages in enumerate(packages_list): try: self.generate_microservice(packages, num_approach) - self.debug_microservice(num_approach, packages) + self.debug_microservice(num_approach, packages, self.self_healing) self.generate_playground() except self.MaxDebugTimeReachedException: print('Could not debug the Microservice with the approach:', packages) diff --git a/dev_gpt/options/generate/templates_user.py b/dev_gpt/options/generate/templates_user.py index 5b61064..f207bd8 100644 --- a/dev_gpt/options/generate/templates_user.py +++ b/dev_gpt/options/generate/templates_user.py @@ -140,7 +140,8 @@ It will be tested with the following scenario: '{{test_description}}'. For the implementation use the following package(s): '{{packages}}'. The code must start with the following imports: -```{linebreak +'from .apis import GPT_3_5_Turbo' if is_using_gpt_3_5_turbo else ""} +```{linebreak +'from .apis import GPT_3_5_Turbo' if is_using_gpt_3_5_turbo else ""}{linebreak +'from .apis import search_web, search_images' if is_using_google_custom_search else ""}{linebreak} + import json import requests ``` diff --git a/test/integration/test_generator.py b/test/integration/test_generator.py index 58f38f2..e17e71d 100644 --- a/test/integration/test_generator.py +++ b/test/integration/test_generator.py @@ -22,7 +22,8 @@ def test_generation_level_0(microservice_dir, mock_input_sequence): generator = Generator( "The microservice is very simple, it does not take anything as input and only outputs the word 'test'", microservice_dir, - 'gpt-3.5-turbo' + 'gpt-3.5-turbo', + self_healing=False, ) assert generator.generate() == 0 @@ -46,7 +47,8 @@ Example tweet: \'When your coworker microwaves fish in the break room... AGAIN. 🐟🤢 But hey, at least SOMEONE's enjoying their lunch. #officelife\'''', str(microservice_dir), - 'gpt-3.5-turbo' + 'gpt-3.5-turbo', + self_healing=False, ) assert generator.generate() == 0 @@ -66,7 +68,8 @@ def test_generation_level_2(microservice_dir, mock_input_sequence): generator = Generator( "The input is a PDF and the output the summarized text (50 words).", str(microservice_dir), - 'gpt-3.5-turbo' + 'gpt-3.5-turbo', + self_healing=False, ) assert generator.generate() == 0 @@ -85,7 +88,8 @@ def test_generation_level_2_svg(microservice_dir, mock_input_sequence): generator = Generator( "Get a png as input and return a vectorized version as svg.", str(microservice_dir), - 'gpt-3.5-turbo' + 'gpt-3.5-turbo', + self_healing=False, ) assert generator.generate() == 0 @@ -111,7 +115,8 @@ def test_generation_level_3(microservice_dir, mock_input_sequence): Example input: 'AAPL' ''', str(microservice_dir), - 'gpt-3.5-turbo' + 'gpt-3.5-turbo', + self_healing=False, ) assert generator.generate() == 0 @@ -155,7 +160,8 @@ def test_generation_level_4(microservice_dir, mock_input_sequence): 4. Return the the audio file as base64 encoded binary. ''', str(microservice_dir), - 'gpt-4' + 'gpt-4', + self_healing=False, ) assert generator.generate() == 0 @@ -170,7 +176,8 @@ All logos need to be arranged on a square. The square is returned as png. ''', str(microservice_dir), - 'gpt-3.5-turbo' + 'gpt-3.5-turbo', + self_healing=False, ) assert generator.generate() == 0 @@ -209,7 +216,8 @@ The joke is the put on the image. The output is the image with the joke on it. ''', str(microservice_dir), - 'gpt-3.5-turbo' + 'gpt-3.5-turbo', + self_healing=False, ) assert generator.generate() == 0 From 8746749f0f33f76f3078880be025f1ce0e682c83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Fri, 19 May 2023 22:56:36 +0200 Subject: [PATCH 57/67] feat: search fix web search --- dev_gpt/options/generate/generator.py | 7 ++++--- dev_gpt/options/generate/templates_user.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/dev_gpt/options/generate/generator.py b/dev_gpt/options/generate/generator.py index f856579..fd0ab99 100644 --- a/dev_gpt/options/generate/generator.py +++ b/dev_gpt/options/generate/generator.py @@ -198,8 +198,8 @@ metas: with open(os.path.join(os.path.dirname(__file__), 'static_files', 'microservice', 'apis.py'), 'r', encoding='utf-8') as f: persist_file(f.read(), os.path.join(self.cur_microservice_path, 'apis.py')) - is_using_gpt_3_5_turbo = 'gpt_3_5_turbo' in packages - is_using_google_custom_search = 'google-custom-search' in packages + is_using_gpt_3_5_turbo = 'gpt_3_5_turbo' in packages or 'gpt-3-5-turbo' in packages + is_using_google_custom_search = 'google_custom_search' in packages or 'google-custom-search' in packages microservice_content = self.generate_and_persist_file( section_title='Microservice', template=template_generate_function_constructor(is_using_gpt_3_5_turbo, is_using_google_custom_search), @@ -335,7 +335,8 @@ pytest error = process_error_message(log_hubble) if error: if not self_healing: - raise Exception('Self-healing is disabled. Please fix the error manually.', error) + print(error) + raise Exception('Self-healing is disabled. Please fix the error manually.') print('An error occurred during the build process. Feeding the error back to the assistant...') self.previous_microservice_path = self.cur_microservice_path self.cur_microservice_path = get_microservice_path( diff --git a/dev_gpt/options/generate/templates_user.py b/dev_gpt/options/generate/templates_user.py index f207bd8..ae31daf 100644 --- a/dev_gpt/options/generate/templates_user.py +++ b/dev_gpt/options/generate/templates_user.py @@ -140,7 +140,7 @@ It will be tested with the following scenario: '{{test_description}}'. For the implementation use the following package(s): '{{packages}}'. The code must start with the following imports: -```{linebreak +'from .apis import GPT_3_5_Turbo' if is_using_gpt_3_5_turbo else ""}{linebreak +'from .apis import search_web, search_images' if is_using_google_custom_search else ""}{linebreak} +```{linebreak +'from .apis import GPT_3_5_Turbo' if is_using_gpt_3_5_turbo else ""}{linebreak + 'from .apis import search_web, search_images' if is_using_google_custom_search else ""}{linebreak} import json import requests From e25a625f0873e650022ce4b6181366693ba996d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Fri, 19 May 2023 23:30:59 +0200 Subject: [PATCH 58/67] feat: search fix web search --- dev_gpt/constants.py | 4 ++++ dev_gpt/options/generate/generator.py | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/dev_gpt/constants.py b/dev_gpt/constants.py index 4326065..20181c8 100644 --- a/dev_gpt/constants.py +++ b/dev_gpt/constants.py @@ -26,6 +26,10 @@ FILE_AND_TAG_PAIRS = [ (STREAMLIT_FILE_NAME, STREAMLIT_FILE_TAG) ] +INDICATOR_TO_IMPORT_STATEMENT = { + 'BytesIO': 'from io import BytesIO', +} + FLOW_URL_PLACEHOLDER = 'jcloud.jina.ai' PRICING_GPT4_PROMPT = 0.03 diff --git a/dev_gpt/options/generate/generator.py b/dev_gpt/options/generate/generator.py index fd0ab99..52e0dc0 100644 --- a/dev_gpt/options/generate/generator.py +++ b/dev_gpt/options/generate/generator.py @@ -17,7 +17,8 @@ from dev_gpt.apis.pypi import is_package_on_pypi, clean_requirements_txt from dev_gpt.constants import FILE_AND_TAG_PAIRS, NUM_IMPLEMENTATION_STRATEGIES, MAX_DEBUGGING_ITERATIONS, \ BLACKLISTED_PACKAGES, EXECUTOR_FILE_NAME, TEST_EXECUTOR_FILE_NAME, TEST_EXECUTOR_FILE_TAG, \ REQUIREMENTS_FILE_NAME, REQUIREMENTS_FILE_TAG, DOCKER_FILE_NAME, IMPLEMENTATION_FILE_NAME, \ - IMPLEMENTATION_FILE_TAG, LANGUAGE_PACKAGES, UNNECESSARY_PACKAGES, DOCKER_BASE_IMAGE_VERSION, SEARCH_PACKAGES + IMPLEMENTATION_FILE_TAG, LANGUAGE_PACKAGES, UNNECESSARY_PACKAGES, DOCKER_BASE_IMAGE_VERSION, SEARCH_PACKAGES, \ + INDICATOR_TO_IMPORT_STATEMENT from dev_gpt.options.generate.pm.pm import PM from dev_gpt.options.generate.templates_user import template_generate_microservice_name, \ template_generate_possible_packages, \ @@ -103,6 +104,7 @@ metas: parse_result_fn: Callable = None, use_custom_system_message: bool = True, response_format_example: str = None, + post_process_fn: Callable = None, **template_kwargs ): """This function generates file(s) using the given template and persists it/them in the given destination folder. @@ -146,6 +148,8 @@ metas: ) ) content = parse_result_fn(content_raw) + if post_process_fn is not None: + content = post_process_fn(content) if content == {}: conversation = self.gpt_session.get_conversation( messages=[SystemMessage(content='You are a helpful assistant.'), AIMessage(content=content_raw)] @@ -209,6 +213,7 @@ metas: file_name_purpose=IMPLEMENTATION_FILE_NAME, tag_name=IMPLEMENTATION_FILE_TAG, file_name_s=[IMPLEMENTATION_FILE_NAME], + post_process_fn=self.add_missing_imports_post_process_fn, )[IMPLEMENTATION_FILE_NAME] test_microservice_content = self.generate_and_persist_file( @@ -221,6 +226,7 @@ metas: file_name_purpose=TEST_EXECUTOR_FILE_NAME, tag_name=TEST_EXECUTOR_FILE_TAG, file_name_s=[TEST_EXECUTOR_FILE_NAME], + post_process_fn=self.add_missing_imports_post_process_fn, )[TEST_EXECUTOR_FILE_NAME] self.generate_and_persist_file( @@ -250,6 +256,13 @@ metas: print('\nFirst version of the microservice generated. Start iterating on it to make the tests pass...') + + def add_missing_imports_post_process_fn(self, content_raw: str): + for indicator, import_statement in INDICATOR_TO_IMPORT_STATEMENT.items(): + if indicator in content_raw and import_statement not in content_raw: + content_raw = f'{import_statement}\n{content_raw}' + + @staticmethod def read_docker_template(): with open(os.path.join(os.path.dirname(__file__), 'static_files', 'microservice', 'Dockerfile'), 'r', encoding='utf-8') as f: From b2f1ce4489cec7c701ee6903061a0eba93b894d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Fri, 19 May 2023 23:59:42 +0200 Subject: [PATCH 59/67] feat: search fix web search --- dev_gpt/options/generate/generator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dev_gpt/options/generate/generator.py b/dev_gpt/options/generate/generator.py index 52e0dc0..ef11b35 100644 --- a/dev_gpt/options/generate/generator.py +++ b/dev_gpt/options/generate/generator.py @@ -261,6 +261,7 @@ metas: for indicator, import_statement in INDICATOR_TO_IMPORT_STATEMENT.items(): if indicator in content_raw and import_statement not in content_raw: content_raw = f'{import_statement}\n{content_raw}' + return content_raw @staticmethod From 16f1f7b6afb091336c16c7c5cba1b6d09b3f5ff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Sat, 20 May 2023 01:05:09 +0200 Subject: [PATCH 60/67] feat: search fix web search --- dev_gpt/options/generate/generator.py | 9 +++-- .../static_files/microservice/apis.py | 8 ++-- dev_gpt/options/generate/templates_user.py | 4 +- test/integration/test_generator.py | 38 +++++++++++-------- 4 files changed, 34 insertions(+), 25 deletions(-) diff --git a/dev_gpt/options/generate/generator.py b/dev_gpt/options/generate/generator.py index ef11b35..e37c8fe 100644 --- a/dev_gpt/options/generate/generator.py +++ b/dev_gpt/options/generate/generator.py @@ -257,11 +257,12 @@ metas: print('\nFirst version of the microservice generated. Start iterating on it to make the tests pass...') - def add_missing_imports_post_process_fn(self, content_raw: str): + def add_missing_imports_post_process_fn(self, content_dict: dict): for indicator, import_statement in INDICATOR_TO_IMPORT_STATEMENT.items(): - if indicator in content_raw and import_statement not in content_raw: - content_raw = f'{import_statement}\n{content_raw}' - return content_raw + for file_name, file_content in content_dict.items(): + if indicator in file_content and import_statement not in file_content: + content_dict[file_name] = f'{import_statement}\n{file_content}' + return content_dict @staticmethod diff --git a/dev_gpt/options/generate/static_files/microservice/apis.py b/dev_gpt/options/generate/static_files/microservice/apis.py index 527d13a..def8830 100644 --- a/dev_gpt/options/generate/static_files/microservice/apis.py +++ b/dev_gpt/options/generate/static_files/microservice/apis.py @@ -6,10 +6,10 @@ openai.api_key = os.getenv("OPENAI_API_KEY") class GPT_3_5_Turbo: - def __init__(self, system: str = ''): - self.system = system + def __init__(self, system_string: str = ''): + self.system = system_string - def __call__(self, prompt: str) -> str: + def __call__(self, prompt_string: str) -> str: response = openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=[{ @@ -17,7 +17,7 @@ class GPT_3_5_Turbo: "content": self.system }, { "role": 'user', - "content": prompt + "content": prompt_string }] ) return response.choices[0]['message']['content'] diff --git a/dev_gpt/options/generate/templates_user.py b/dev_gpt/options/generate/templates_user.py index ae31daf..b2908a4 100644 --- a/dev_gpt/options/generate/templates_user.py +++ b/dev_gpt/options/generate/templates_user.py @@ -97,13 +97,13 @@ gpt_35_turbo_usage_string = """If you need to use gpt_3_5_turbo, then use it lik from .apis import GPT_3_5_Turbo gpt_3_5_turbo = GPT_3_5_Turbo( - system=\'\'\' + system_string=\'\'\' You are a tv-reporter who is specialized in C-list celebrities. When you get asked something like 'Who was having a date with ?', then you answer with a json like '{{"dates": ["", ""]}}'. You must not answer something else - only the json. \'\'\') -generated_string = gpt_3_5_turbo("example user prompt") # prompt is a string; generated_string is a string +generated_string = gpt_3_5_turbo(prompt_string="example user prompt") # prompt_string is the only parameter ``` """ diff --git a/test/integration/test_generator.py b/test/integration/test_generator.py index e17e71d..58d1b77 100644 --- a/test/integration/test_generator.py +++ b/test/integration/test_generator.py @@ -28,7 +28,6 @@ def test_generation_level_0(microservice_dir, mock_input_sequence): assert generator.generate() == 0 - @pytest.mark.parametrize('mock_input_sequence', [['y']], indirect=True) def test_generation_level_1(microservice_dir, mock_input_sequence): """ @@ -48,12 +47,13 @@ Example tweet: But hey, at least SOMEONE's enjoying their lunch. #officelife\'''', str(microservice_dir), 'gpt-3.5-turbo', - self_healing=False, + # self_healing=False, ) assert generator.generate() == 0 -@pytest.mark.parametrize('mock_input_sequence', [['y', 'https://www.africau.edu/images/default/sample.pdf']], indirect=True) +@pytest.mark.parametrize('mock_input_sequence', [['y', 'https://www.africau.edu/images/default/sample.pdf']], + indirect=True) def test_generation_level_2(microservice_dir, mock_input_sequence): """ Requirements: @@ -69,11 +69,13 @@ def test_generation_level_2(microservice_dir, mock_input_sequence): "The input is a PDF and the output the summarized text (50 words).", str(microservice_dir), 'gpt-3.5-turbo', - self_healing=False, + # self_healing=False, ) assert generator.generate() == 0 -@pytest.mark.parametrize('mock_input_sequence', [['y', 'https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png']], indirect=True) + +@pytest.mark.parametrize('mock_input_sequence', [ + ['y', 'https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png']], indirect=True) def test_generation_level_2_svg(microservice_dir, mock_input_sequence): """ Requirements: @@ -89,7 +91,7 @@ def test_generation_level_2_svg(microservice_dir, mock_input_sequence): "Get a png as input and return a vectorized version as svg.", str(microservice_dir), 'gpt-3.5-turbo', - self_healing=False, + # self_healing=False, ) assert generator.generate() == 0 @@ -116,10 +118,11 @@ Example input: 'AAPL' ''', str(microservice_dir), 'gpt-3.5-turbo', - self_healing=False, + # self_healing=False, ) assert generator.generate() == 0 + @pytest.mark.parametrize( 'mock_input_sequence', [ [ @@ -161,10 +164,11 @@ def test_generation_level_4(microservice_dir, mock_input_sequence): ''', str(microservice_dir), 'gpt-4', - self_healing=False, + # self_healing=False, ) assert generator.generate() == 0 + @pytest.mark.parametrize('mock_input_sequence', [['y']], indirect=True) def test_generation_level_5_company_logos(microservice_dir, mock_input_sequence): os.environ['VERBOSE'] = 'true' @@ -177,11 +181,14 @@ The square is returned as png. ''', str(microservice_dir), 'gpt-3.5-turbo', - self_healing=False, + # self_healing=False, ) assert generator.generate() == 0 -@pytest.mark.parametrize('mock_input_sequence', [['y', 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/PNG_transparency_demonstration_1.png/560px-PNG_transparency_demonstration_1.png']], indirect=True) + +@pytest.mark.parametrize('mock_input_sequence', [['y', + 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/PNG_transparency_demonstration_1.png/560px-PNG_transparency_demonstration_1.png']], + indirect=True) def test_generation_level_5(microservice_dir, mock_input_sequence): """ Requirements: @@ -193,7 +200,8 @@ def test_generation_level_5(microservice_dir, mock_input_sequence): Databases: ❌ """ os.environ['VERBOSE'] = 'true' - generator = Generator(f''' + generator = Generator( + f''' The input is an image. Use the following api to get the description of the image: Request: @@ -215,10 +223,10 @@ The description is then used to generate a joke. The joke is the put on the image. The output is the image with the joke on it. ''', - str(microservice_dir), - 'gpt-3.5-turbo', - self_healing=False, - ) + str(microservice_dir), + 'gpt-3.5-turbo', + # self_healing=False, + ) assert generator.generate() == 0 # @pytest.fixture From 206cffd6ffc0bb8f9083f0d395a5ef80073d6741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Sat, 20 May 2023 02:18:32 +0200 Subject: [PATCH 61/67] feat: search fix web search --- .../{apis.py => google_custom_search.py} | 27 ------------------- .../microservice/gpt_3_5_turbo.py | 24 +++++++++++++++++ dev_gpt/options/generate/templates_user.py | 8 +++--- test/unit/test_search.py | 2 +- 4 files changed, 29 insertions(+), 32 deletions(-) rename dev_gpt/options/generate/static_files/microservice/{apis.py => google_custom_search.py} (61%) create mode 100644 dev_gpt/options/generate/static_files/microservice/gpt_3_5_turbo.py diff --git a/dev_gpt/options/generate/static_files/microservice/apis.py b/dev_gpt/options/generate/static_files/microservice/google_custom_search.py similarity index 61% rename from dev_gpt/options/generate/static_files/microservice/apis.py rename to dev_gpt/options/generate/static_files/microservice/google_custom_search.py index def8830..f112129 100644 --- a/dev_gpt/options/generate/static_files/microservice/apis.py +++ b/dev_gpt/options/generate/static_files/microservice/google_custom_search.py @@ -1,29 +1,3 @@ -import os -import openai - - -openai.api_key = os.getenv("OPENAI_API_KEY") - - -class GPT_3_5_Turbo: - def __init__(self, system_string: str = ''): - self.system = system_string - - def __call__(self, prompt_string: str) -> str: - response = openai.ChatCompletion.create( - model="gpt-3.5-turbo", - messages=[{ - "role": 'system', - "content": self.system - }, { - "role": 'user', - "content": prompt_string - }] - ) - return response.choices[0]['message']['content'] - - - import os from typing import Optional @@ -52,4 +26,3 @@ def search_images(search_term, top_n): def search_web(search_term, top_n): response = google_search(search_term, search_type="web", top_n=top_n) return [item["snippet"] for item in response["items"]] - diff --git a/dev_gpt/options/generate/static_files/microservice/gpt_3_5_turbo.py b/dev_gpt/options/generate/static_files/microservice/gpt_3_5_turbo.py new file mode 100644 index 0000000..8b618ef --- /dev/null +++ b/dev_gpt/options/generate/static_files/microservice/gpt_3_5_turbo.py @@ -0,0 +1,24 @@ +import os +import openai + + +openai.api_key = os.getenv("OPENAI_API_KEY") + + +class GPT_3_5_Turbo: + def __init__(self, system_string: str = ''): + self.system = system_string + + def __call__(self, prompt_string: str) -> str: + response = openai.ChatCompletion.create( + model="gpt-3.5-turbo", + messages=[{ + "role": 'system', + "content": self.system + }, { + "role": 'user', + "content": prompt_string + }] + ) + return response.choices[0]['message']['content'] + diff --git a/dev_gpt/options/generate/templates_user.py b/dev_gpt/options/generate/templates_user.py index b2908a4..300a3c1 100644 --- a/dev_gpt/options/generate/templates_user.py +++ b/dev_gpt/options/generate/templates_user.py @@ -94,7 +94,7 @@ You must provide the complete {file_name} wrapped with the exact syntax shown ab gpt_35_turbo_usage_string = """If you need to use gpt_3_5_turbo, then use it like shown in the following example: ``` -from .apis import GPT_3_5_Turbo +from .gpt_3_5_turbo import GPT_3_5_Turbo gpt_3_5_turbo = GPT_3_5_Turbo( system_string=\'\'\' @@ -110,7 +110,7 @@ generated_string = gpt_3_5_turbo(prompt_string="example user prompt") # prompt_s google_custom_search_usage_string = """If you need to use google_custom_search, then use it like shown in the following example: a) when searching for text: ``` -from .apis import search_web +from .google_custom_search import search_web # input: search term (str), top_n (int) # output: list of strings @@ -118,7 +118,7 @@ string_list = search_web('', top_n=10) ``` b) when searching for images: ``` -from .apis import search_images +from .google_custom_search import search_images # input: search term (str), top_n (int) # output: list of image urls @@ -140,7 +140,7 @@ It will be tested with the following scenario: '{{test_description}}'. For the implementation use the following package(s): '{{packages}}'. The code must start with the following imports: -```{linebreak +'from .apis import GPT_3_5_Turbo' if is_using_gpt_3_5_turbo else ""}{linebreak + 'from .apis import search_web, search_images' if is_using_google_custom_search else ""}{linebreak} +```{linebreak +'from .gpt_3_5_turbo import GPT_3_5_Turbo' if is_using_gpt_3_5_turbo else ""}{linebreak + 'from .google_custom_search import search_web, search_images' if is_using_google_custom_search else ""}{linebreak} import json import requests diff --git a/test/unit/test_search.py b/test/unit/test_search.py index b5839fd..fdd38fb 100644 --- a/test/unit/test_search.py +++ b/test/unit/test_search.py @@ -1,4 +1,4 @@ -from dev_gpt.options.generate.static_files.microservice.apis import search_web, search_images +from dev_gpt.options.generate.static_files.microservice.google_custom_search import search_web, search_images def test_web_search(): From fb1793c51f08c8adf46c48d244ae3b519ace0860 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Sat, 20 May 2023 02:24:35 +0200 Subject: [PATCH 62/67] feat: search fix web search --- dev_gpt/options/generate/generator.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dev_gpt/options/generate/generator.py b/dev_gpt/options/generate/generator.py index e37c8fe..700b7d9 100644 --- a/dev_gpt/options/generate/generator.py +++ b/dev_gpt/options/generate/generator.py @@ -199,8 +199,9 @@ metas: .replace('class DevGPTExecutor(Executor):', f'class {self.microservice_name}(Executor):') persist_file(microservice_executor_code, os.path.join(self.cur_microservice_path, EXECUTOR_FILE_NAME)) - with open(os.path.join(os.path.dirname(__file__), 'static_files', 'microservice', 'apis.py'), 'r', encoding='utf-8') as f: - persist_file(f.read(), os.path.join(self.cur_microservice_path, 'apis.py')) + for additional_file in ['google_custom_search.py', 'gpt_3_5_turbo.py']: + with open(os.path.join(os.path.dirname(__file__), 'static_files', 'microservice', additional_file), 'r', encoding='utf-8') as f: + persist_file(f.read(), os.path.join(self.cur_microservice_path, additional_file)) is_using_gpt_3_5_turbo = 'gpt_3_5_turbo' in packages or 'gpt-3-5-turbo' in packages is_using_google_custom_search = 'google_custom_search' in packages or 'google-custom-search' in packages From 889c2271e16bacddc195076f81d9c31ec265db2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Sat, 20 May 2023 02:27:38 +0200 Subject: [PATCH 63/67] feat: search fix web search --- dev_gpt/options/generate/templates_user.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dev_gpt/options/generate/templates_user.py b/dev_gpt/options/generate/templates_user.py index 300a3c1..ee19cbf 100644 --- a/dev_gpt/options/generate/templates_user.py +++ b/dev_gpt/options/generate/templates_user.py @@ -140,8 +140,7 @@ It will be tested with the following scenario: '{{test_description}}'. For the implementation use the following package(s): '{{packages}}'. The code must start with the following imports: -```{linebreak +'from .gpt_3_5_turbo import GPT_3_5_Turbo' if is_using_gpt_3_5_turbo else ""}{linebreak + 'from .google_custom_search import search_web, search_images' if is_using_google_custom_search else ""}{linebreak} - +```{linebreak +'from .gpt_3_5_turbo import GPT_3_5_Turbo' if is_using_gpt_3_5_turbo else ""}{linebreak + 'from .google_custom_search import search_web, search_images' if is_using_google_custom_search else ""} import json import requests ``` From e420aee0beb5cd1467da20fd99fd9c9a74d390c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Mon, 22 May 2023 10:59:57 +0200 Subject: [PATCH 64/67] =?UTF-8?q?=F0=9F=A7=AE=20fix:=20m1=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev_gpt/apis/jina_cloud.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dev_gpt/apis/jina_cloud.py b/dev_gpt/apis/jina_cloud.py index e7963db..ac0953b 100644 --- a/dev_gpt/apis/jina_cloud.py +++ b/dev_gpt/apis/jina_cloud.py @@ -103,6 +103,11 @@ def _push_executor(dir_path): } with suppress_stdout(): headers = get_request_header() + headers['jinameta-platform'] = 'Darwin' + headers['jinameta-platform-release'] = '21.1.0' + headers['jinameta-platform-version'] = 'Darwin Kernel Version 21.1.0: Wed Oct 13 17:33:23 PDT 2021; root:xnu-8019.41.5~1/RELEASE_X86_64' + headers['jinameta-architecture'] = 'x86_64' + headers['jinameta-processor'] = 'i386' resp = upload_file( 'https://api.hubble.jina.ai/v2/rpc/executor.push', From 5792225ea11cb86c81769c20216ba804a8074b82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Mon, 22 May 2023 11:48:28 +0200 Subject: [PATCH 65/67] =?UTF-8?q?=E2=9A=BD=20refactor:=20playground=20more?= =?UTF-8?q?=20stable=20generation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev_gpt/constants.py | 2 + .../chains/auto_refine_description.py | 3 +- dev_gpt/options/generate/generator.py | 20 ++++-- .../gateway/{app-template => app_template.py} | 40 ++++++------ dev_gpt/options/generate/templates_user.py | 64 +++++++------------ 5 files changed, 60 insertions(+), 69 deletions(-) rename dev_gpt/options/generate/static_files/gateway/{app-template => app_template.py} (51%) diff --git a/dev_gpt/constants.py b/dev_gpt/constants.py index 20181c8..facfeaf 100644 --- a/dev_gpt/constants.py +++ b/dev_gpt/constants.py @@ -27,7 +27,9 @@ FILE_AND_TAG_PAIRS = [ ] INDICATOR_TO_IMPORT_STATEMENT = { + 'io.BytesIO': 'import io', 'BytesIO': 'from io import BytesIO', + 'base64': 'import base64', } FLOW_URL_PLACEHOLDER = 'jcloud.jina.ai' diff --git a/dev_gpt/options/generate/chains/auto_refine_description.py b/dev_gpt/options/generate/chains/auto_refine_description.py index aef8e08..085295a 100644 --- a/dev_gpt/options/generate/chains/auto_refine_description.py +++ b/dev_gpt/options/generate/chains/auto_refine_description.py @@ -49,7 +49,8 @@ Note: If you are not sure about the details, then come up with the minimal numbe generate_output_schema_prompt = '''{context_string} Generate the lean response json schema for the Microservice. -Note: If you are not sure about the details, then come up with the minimal number of parameters possible.''' +Note: If you are not sure about the details, then come up with the minimal number of parameters possible. +Note: If you can decide to return files as URLs or as base64 encoded strings, then choose the base64 encoded strings.''' summarize_description_and_schemas_prompt = '''{context_string} Write an updated microservice description by incorporating information about the request and response parameters in a concise way without losing any information. diff --git a/dev_gpt/options/generate/generator.py b/dev_gpt/options/generate/generator.py index 700b7d9..5bd0f69 100644 --- a/dev_gpt/options/generate/generator.py +++ b/dev_gpt/options/generate/generator.py @@ -250,7 +250,7 @@ metas: line.replace('{{APT_GET_PACKAGES}}', '').replace('{{DOCKER_BASE_IMAGE_VERSION}}', DOCKER_BASE_IMAGE_VERSION) for line in docker_file_template_lines ] - docker_file_content = '\n'.join(docker_file_template_lines) + docker_file_content = ''.join(docker_file_template_lines) persist_file(docker_file_content, os.path.join(self.cur_microservice_path, 'Dockerfile')) self.write_config_yml(self.microservice_name, self.cur_microservice_path) @@ -259,12 +259,16 @@ metas: def add_missing_imports_post_process_fn(self, content_dict: dict): - for indicator, import_statement in INDICATOR_TO_IMPORT_STATEMENT.items(): - for file_name, file_content in content_dict.items(): - if indicator in file_content and import_statement not in file_content: - content_dict[file_name] = f'{import_statement}\n{file_content}' + for file_name, file_content in content_dict.items(): + file_content = self.add_missing_imports_for_file(file_content) + content_dict[file_name] = file_content return content_dict + def add_missing_imports_for_file(self, file_content): + for indicator, import_statement in INDICATOR_TO_IMPORT_STATEMENT.items(): + if indicator in file_content and import_statement not in file_content: + file_content = f'{import_statement}\n{file_content}' + return file_content @staticmethod def read_docker_template(): @@ -295,12 +299,15 @@ pytest def generate_playground(self): print_colored('', '\n\n############# Playground #############', 'blue') + with open(os.path.join(os.path.dirname(__file__), 'static_files', 'gateway', 'app_template.py'), 'r', encoding='utf-8') as f: + playground_template = f.read() file_name_to_content = get_all_microservice_files_with_content(self.cur_microservice_path) conversation = self.gpt_session.get_conversation() conversation.chat( template_generate_playground.format( - code_files_wrapped=self.files_to_string(file_name_to_content, ['test_microservice.py']), + code_files_wrapped=self.files_to_string(file_name_to_content, ['test_microservice.py', 'microservice.py']), microservice_name=self.microservice_name, + playground_template=playground_template, ) ) playground_content_raw = conversation.chat( @@ -316,6 +323,7 @@ pytest playground_content = self.extract_content_from_result( content_raw, 'app.py', match_single_block=True ) + playground_content = self.add_missing_imports_for_file(playground_content) gateway_path = os.path.join(self.cur_microservice_path, 'gateway') shutil.copytree(os.path.join(os.path.dirname(__file__), 'static_files', 'gateway'), gateway_path) diff --git a/dev_gpt/options/generate/static_files/gateway/app-template b/dev_gpt/options/generate/static_files/gateway/app_template.py similarity index 51% rename from dev_gpt/options/generate/static_files/gateway/app-template rename to dev_gpt/options/generate/static_files/gateway/app_template.py index cda8597..a9ec3cd 100644 --- a/dev_gpt/options/generate/static_files/gateway/app-template +++ b/dev_gpt/options/generate/static_files/gateway/app_template.py @@ -1,53 +1,53 @@ import json import os -import base64 + import streamlit as st from jina import Client, Document, DocumentArray +import io st.set_page_config( page_title="", page_icon="", - layout="", - initial_sidebar_state="", + layout="centered", + initial_sidebar_state="auto", ) st.title("
") st.markdown( "<10 word description here>" - "To deploy your own microservice, click [here](https://github.com/jina-ai/dev-gpt)." + "To generate and deploy your own microservice, click [here](https://github.com/jina-ai/dev-gpt)." ) - -st.header(" Input Parameters") # only if input parameters are needed +st.subheader(" ") # only if input parameters are needed with st.form(key="input_form"): - + # + input_json_dict = {} # -input_data = { - -} -input_json = json.dumps(input_data) + input_json_dict_string = json.dumps(input_json_dict) + submitted = st.form_submit_button("") # Process input and call microservice -if submit_button: - with st.spinner("Generating collage..."): - - +if submitted: + with st.spinner("..."): client = Client(host="http://localhost:8080") - d = Document(text=input_json) + d = Document(text=input_json_dict_string) response = client.post("/", inputs=DocumentArray([d])) output_data = json.loads(response[0].text) - + # # Display curl command deployment_id = os.environ.get("K8S_NAMESPACE_NAME", "") -host = ( +api_endpoint = ( f"https://dev-gpt-{deployment_id.split('-')[1]}.wolf.jina.ai/post" if deployment_id else "http://localhost:8080/post" ) + with st.expander("See curl command"): st.markdown("You can use the following curl command to send a request to the microservice from the command line:") + escaped_input_json_dict_string = input_json_dict_string.replace('"', '\\"') + st.code( - f'curl -X "POST" "{host}" -H "accept: application/json" -H "Content-Type: application/json" -d \'{{"data": [{{"text": "{input_json}"}}]}}\'', + f'curl -X "POST" "{api_endpoint}" -H "accept: application/json" -H "Content-Type: application/json" -d \'{{"data": [{{"text": "{escaped_input_json_dict_string}"}}]}}\'', language="bash", - ) \ No newline at end of file + ) diff --git a/dev_gpt/options/generate/templates_user.py b/dev_gpt/options/generate/templates_user.py index ee19cbf..1ae01a5 100644 --- a/dev_gpt/options/generate/templates_user.py +++ b/dev_gpt/options/generate/templates_user.py @@ -132,9 +132,10 @@ def template_generate_function_constructor(is_using_gpt_3_5_turbo, is_using_goog general_guidelines_string + f''' Write a python function which receives as \ -input json string (that can be parsed with the python function json.loads) and \ -outputs a json string (that can be parsed with the python function json.loads). \ -The function is called 'func'. +input json dictionary string (that can be parsed with the python function json.loads) and \ +outputs a json dictionary string (that can be parsed with the python function json.loads). \ +The function is called 'func' and has the following signature: +def func(input_json_dict_string: str) -> str: The function must fulfill the following description: '{{microservice_description}}'. It will be tested with the following scenario: '{{test_description}}'. For the implementation use the following package(s): '{{packages}}'. @@ -433,13 +434,12 @@ Use the exact following syntax to wrap the code: ``` Example: - **implementation.py** ```python import json -def func(json_input: str) -> str: - return json_input['img_base64'] +def func(input_json_dict_string: str) -> str: + return json.dumps('output_param1': input_json_dict_string['img_base64']) ```''' ) @@ -449,50 +449,30 @@ template_generate_playground = PromptTemplate.from_template( {code_files_wrapped} -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. -Don't mention the word Playground in the title. -The playground contains many emojis that fit the theme of the playground and has an emoji as favicon. -The playground encourages the user to deploy their own microservice by clicking on this link: https://github.com/jina-ai/dev-gpt -The playground uses the following code to send a request to the microservice: +1. Write down the json request model required by microservice.py. +2. Generate a playground for the microservice {microservice_name} using the following streamlit template by replacing all the placeholders (<...>) with the correct values: +**app_template.py** +```python +{playground_template} ``` -from jina import Client, Document, DocumentArray -client = Client(host='http://localhost:8080') -d = Document(text=json.dumps(INPUT_DICTIONARY)) # fill-in dictionary which takes input -response = client.post('/', inputs=DocumentArray([d])) # always use '/' -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 -The playground displays a code block containing the microservice specific curl code that can be used to send the request to the microservice. -While the exact payload in the curl might change, the host and deployment ID always stay the same. Example: -``` -deployment_id = os.environ.get("K8S_NAMESPACE_NAME", "") -host = f'https://dev-gpt-{{deployment_id.split("-")[1]}}.wolf.jina.ai/post' if deployment_id else "http://localhost:8080/post" -with st.expander("See curl command"): - st.code( - f'curl -X \\'POST\\' \\'host\\' -H \\'accept: application/json\\' -H \\'Content-Type: application/json\\' -d \\'{{{{"data": [{{{{"text": "hello, world!"}}}}]}}}}\\'', - language='bash' - ) -``` -You must provide the complete app.py file using the following syntax to wrap the code: +Note: Don't mention the word Playground in the title. +Most importantly: You must generate the complete app.py file using the following syntax to wrap the code: **app.py** ```python ... -``` -The playground (app.py) must always use the host on http://localhost:8080 and must not let the user configure the host on the UI. -The playground (app.py) must not import the executor. -''' +```''' ) template_chain_of_thought = PromptTemplate.from_template( - '''First, write down an extensive list of obvious and non-obvious observations about {file_name_purpose} that could need an adjustment. Explain why. -Think if all the changes are required and finally decide for the changes you want to make, but you are not allowed disregard the instructions in the previous message. -Be very hesitant to change the code. Only make a change if you are sure that it is necessary. - -Output only {file_name_purpose} -Write the whole content of {file_name_purpose} - even if you decided to change only a small thing or even nothing. + '''\ +1. write down an extensive list (5 words per item) of obvious and non-obvious observations about {file_name_purpose} that could need an adjustment. +2. Explain why. (5 words per item) +3. Think if all the changes are required +4. decide for the changes you want to make, but you are not allowed disregard the instructions in the previous message. +5. Write the whole content of {file_name_purpose} - even if you decided to change only a small thing or even nothing. +Note: Be very hesitant to change the code. Only make a change if you are sure that it is necessary. +Note: Output only {file_name_purpose} ''' + '\n' + template_code_wrapping_string + ''' Remember: From 67302b95430d713cf7dbf1287744197454954039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Mon, 22 May 2023 13:24:41 +0200 Subject: [PATCH 66/67] =?UTF-8?q?=E2=9A=BD=20refactor:=20playground=20more?= =?UTF-8?q?=20stable=20generation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 +- dev_gpt/options/generate/templates_user.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af6a700..85d34e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: id: test run: | pytest -vs test/integration/test_generator.py::test_generation_level_${{ matrix.group }} - timeout-minutes: 15 + timeout-minutes: 17 env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} SCENEX_API_KEY: ${{ secrets.SCENEX_API_KEY }} diff --git a/dev_gpt/options/generate/templates_user.py b/dev_gpt/options/generate/templates_user.py index 1ae01a5..7f4bb86 100644 --- a/dev_gpt/options/generate/templates_user.py +++ b/dev_gpt/options/generate/templates_user.py @@ -99,7 +99,7 @@ from .gpt_3_5_turbo import GPT_3_5_Turbo gpt_3_5_turbo = GPT_3_5_Turbo( system_string=\'\'\' You are a tv-reporter who is specialized in C-list celebrities. -When you get asked something like 'Who was having a date with ?', then you answer with a json like '{{"dates": ["", ""]}}'. +When you get asked something like 'Who was having a date with ?', then you answer with a string like ", were having a date with "'. You must not answer something else - only the json. \'\'\') From 70b6a9b7c79e89ca8e35533d952d69a3e9a8f0df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Ho=CC=88nicke?= Date: Mon, 22 May 2023 16:53:42 +0200 Subject: [PATCH 67/67] =?UTF-8?q?=F0=9F=93=97=20feat:=20prompt=20logging?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev_gpt/apis/gpt.py | 15 +++++++--- .../options/generate/conversation_logger.py | 28 +++++++++++++++++++ dev_gpt/options/generate/generator.py | 4 +-- 3 files changed, 41 insertions(+), 6 deletions(-) create mode 100644 dev_gpt/options/generate/conversation_logger.py diff --git a/dev_gpt/apis/gpt.py b/dev_gpt/apis/gpt.py index 387649c..335eab0 100644 --- a/dev_gpt/apis/gpt.py +++ b/dev_gpt/apis/gpt.py @@ -16,6 +16,7 @@ from urllib3.exceptions import InvalidChunkLength from dev_gpt.constants import PRICING_GPT4_PROMPT, PRICING_GPT4_GENERATION, PRICING_GPT3_5_TURBO_PROMPT, \ PRICING_GPT3_5_TURBO_GENERATION, CHARS_PER_TOKEN +from dev_gpt.options.generate.conversation_logger import ConversationLogger from dev_gpt.options.generate.templates_system import template_system_message_base from dev_gpt.utils.string_tools import print_colored, get_template_parameters @@ -41,9 +42,10 @@ class GPTSession: cls._instance = super(GPTSession, cls).__new__(cls) return cls._instance - def __init__(self, model: str = 'gpt-4', ): + def __init__(self, log_file_path: str, model: str = 'gpt-4', ): if GPTSession._initialized: return + self.conversation_logger = ConversationLogger(log_file_path) if model == 'gpt-4' and self.is_gpt4_available(): self.pricing_prompt = PRICING_GPT4_PROMPT self.pricing_generation = PRICING_GPT4_GENERATION @@ -58,10 +60,13 @@ class GPTSession: self.chars_generation_so_far = 0 GPTSession._initialized = True + + + def get_conversation(self, messages: List[BaseMessage] = [], print_stream: bool = True, print_costs: bool = True): messages = deepcopy(messages) return _GPTConversation( - self.model_name, self.cost_callback, messages, print_stream, print_costs + self.model_name, self.cost_callback, messages, print_stream, print_costs, self.conversation_logger ) @staticmethod @@ -107,7 +112,7 @@ class AssistantStreamingStdOutCallbackHandler(StreamingStdOutCallbackHandler): class _GPTConversation: - def __init__(self, model: str, cost_callback, messages: List[BaseMessage], print_stream, print_costs): + def __init__(self, model: str, cost_callback, messages: List[BaseMessage], print_stream, print_costs, conversation_logger: ConversationLogger = None): self._chat = ChatOpenAI( model_name=model, streaming=True, @@ -119,6 +124,7 @@ class _GPTConversation: self.messages = messages self.print_stream = print_stream self.print_costs = print_costs + self.conversation_logger = conversation_logger def print_messages(self, messages): for i, message in enumerate(messages): @@ -141,6 +147,7 @@ class _GPTConversation: for i in range(10): try: response = self._chat(self.messages) + self.conversation_logger.log(self.messages, response) break except (ConnectionError, InvalidChunkLength, ChunkedEncodingError) as e: print('There was a connection error. Retrying...') @@ -173,7 +180,7 @@ def ask_gpt(prompt_template, parser, **kwargs): if isinstance(value, dict): kwargs[key] = json.dumps(value, indent=4) prompt = prompt_template.format(**kwargs) - conversation = GPTSession().get_conversation( + conversation = GPTSession._instance.get_conversation( [], print_stream=os.environ['VERBOSE'].lower() == 'true', print_costs=False diff --git a/dev_gpt/options/generate/conversation_logger.py b/dev_gpt/options/generate/conversation_logger.py new file mode 100644 index 0000000..cbb3577 --- /dev/null +++ b/dev_gpt/options/generate/conversation_logger.py @@ -0,0 +1,28 @@ +import json +from typing import List + +from langchain.schema import BaseMessage + + +class ConversationLogger: + def __init__(self, log_file_path): + self.log_file_path = log_file_path + self.log_file = [] + + def log(self, prompt_message_list: List[BaseMessage], response: str): + prompt_list_json = [ + { + 'role': f'{message.type}', + 'content': f'{message.content}' + } + for message in prompt_message_list + ] + self.log_file.append({ + 'prompt': prompt_list_json, + 'response': f'{response}' + }) + with open(self.log_file_path, 'w') as f: + f.write(json.dumps(self.log_file, indent=2)) + + + diff --git a/dev_gpt/options/generate/generator.py b/dev_gpt/options/generate/generator.py index 5bd0f69..e10fe68 100644 --- a/dev_gpt/options/generate/generator.py +++ b/dev_gpt/options/generate/generator.py @@ -42,7 +42,7 @@ class TaskSpecification: class Generator: def __init__(self, task_description, path, model='gpt-4', self_healing=True): - self.gpt_session = gpt.GPTSession(model=model) + self.gpt_session = gpt.GPTSession(os.path.join(path, 'log.json'), model=model) self.microservice_specification = TaskSpecification(task=task_description, test=None) self.self_healing = self_healing self.microservice_root_path = path @@ -540,8 +540,8 @@ pytest # '/private/var/folders/f5/whmffl4d7q79s29jpyb6719m0000gn/T/pytest-of-florianhonicke/pytest-128/test_generation_level_0_mock_i0' # '/private/var/folders/f5/whmffl4d7q79s29jpyb6719m0000gn/T/pytest-of-florianhonicke/pytest-129/test_generation_level_0_mock_i0' def generate(self): - self.microservice_specification.task, self.microservice_specification.test = PM().refine_specification(self.microservice_specification.task) os.makedirs(self.microservice_root_path) + self.microservice_specification.task, self.microservice_specification.test = PM().refine_specification(self.microservice_specification.task) generated_name = self.generate_microservice_name(self.microservice_specification.task) self.microservice_name = f'{generated_name}{random.randint(0, 10_000_000)}' packages_list = self.get_possible_packages()