From c92799119405babd15b63624086f7783c462ee5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Mon, 15 Jan 2024 15:41:18 +0800 Subject: [PATCH 01/21] update get_choice_function_arguments. --- metagpt/provider/base_llm.py | 48 +++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/metagpt/provider/base_llm.py b/metagpt/provider/base_llm.py index dbef15fa1..c482aaf35 100644 --- a/metagpt/provider/base_llm.py +++ b/metagpt/provider/base_llm.py @@ -6,10 +6,14 @@ @File : base_llm.py @Desc : mashenquan, 2023/8/22. + try catch """ +import re import json from abc import ABC, abstractmethod from typing import Optional +from metagpt.logs import logger +from metagpt.utils.common import CodeParser + class BaseLLM(ABC): """LLM API abstract class, requiring all inheritors to provide a series of standard capabilities""" @@ -118,6 +122,30 @@ class BaseLLM(ABC): """ return rsp.get("choices")[0]["message"]["tool_calls"][0]["function"] + def _parse_arguments(self, arguments: str) -> dict: + """parse arguments in openai function call""" + if 'langugae' not in arguments and 'code' not in arguments: + logger.warning(f"Not found `code`, `language`, We assume it is pure code:\n {arguments}\n. ") + return {'language': 'python', 'code': arguments} + + # 匹配language + language_pattern = re.compile(r'[\"\']?language[\"\']?\s*:\s*["\']([^"\']+?)["\']', re.DOTALL) + language_match = language_pattern.search(arguments) + language_value = language_match.group(1) if language_match else None + + # 匹配code + code_pattern = r'(["\']{3}|["])([\s\S]*?)\1' + try: + code_value = re.findall(code_pattern, arguments)[-1][-1] + except Exception as e: + logger.error(f"{e}, when re.findall({code_pattern}, {arguments})") + code_value = None + + if code_value is None: + raise ValueError(f"Parse code error for {arguments}") + # arguments只有code的情况 + return {'language': language_value, 'code': code_value} + def get_choice_function_arguments(self, rsp: dict) -> dict: """Required to provide the first function arguments of choice. @@ -125,7 +153,25 @@ class BaseLLM(ABC): :return dict: return the first function arguments of choice, for example, {'language': 'python', 'code': "print('Hello, World!')"} """ - return json.loads(self.get_choice_function(rsp)["arguments"], strict=False) + try: + arguments: str = self.get_choice_function(rsp)["arguments"] + return json.loads(arguments, strict=False) + except json.decoder.JSONDecodeError as e: + logger.debug(f"Got JSONDecodeError for {arguments}, we will use RegExp to parse code, \n {e}") + return self._parse_arguments(arguments) + except KeyError as e: + if 'tool_calls' in e.args: + txt_rsp = self.get_choice_text(rsp) + # find code + code = CodeParser.parse_code(None, txt_rsp, lang='python') + if code != txt_rsp: + return {'language': 'python', 'code': code} + # no code + return {'language': 'markdown', 'code': txt_rsp} + raise e + except Exception as e: + logger.error(f"Got error `{e}` for parsing\n {rsp}\n") + return {} def messages_to_prompt(self, messages: list[dict]): """[{"role": "user", "content": msg}] to user: etc.""" From bb356fbc02a666d970799798a8f7d921252ec703 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Mon, 15 Jan 2024 15:49:07 +0800 Subject: [PATCH 02/21] update truncate. --- metagpt/actions/execute_code.py | 34 +++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/metagpt/actions/execute_code.py b/metagpt/actions/execute_code.py index c75711e75..458dc0898 100644 --- a/metagpt/actions/execute_code.py +++ b/metagpt/actions/execute_code.py @@ -212,26 +212,40 @@ class ExecutePyCode(ExecuteCode, Action): cell_index = len(self.nb.cells) - 1 success, error_message = await self.run_cell(self.nb.cells[-1], cell_index) - if success: - outputs = self.parse_outputs(self.nb.cells[-1].outputs) - return truncate(remove_escape_and_color_codes(outputs)), True - else: - return error_message, False + if not success: + return truncate(remove_escape_and_color_codes(error_message), is_success=success) + + # code success + outputs = self.parse_outputs(self.nb.cells[-1].outputs) + return truncate(remove_escape_and_color_codes(outputs), is_success=success) else: # TODO: markdown raise NotImplementedError(f"Not support this code type : {language}, Only support code!") -def truncate(result: str, keep_len: int = 2000) -> str: - desc = f"Truncated to show only the last {keep_len} characters\n" +def truncate(result: str, keep_len: int = 2000, is_success: bool = True) -> str | bool: + desc = f"Executed code {'successfully' if is_success else 'failed, please reflect the cause of bug and then debug'}" + if is_success: + desc += f"Truncated to show only {keep_len} characters\n" + else: + desc += "Show complete information for you." + if result.startswith(desc): result = result[len(desc) :] if len(result) > keep_len: - result = result[-keep_len:] - return desc + result + result = result[-keep_len:] if not is_success else result + if not result: + result = 'No output about your code. Only when importing packages it is normal case. Recap and go ahead.' + return result, False - return result + if result.strip().startswith(" Date: Mon, 15 Jan 2024 15:51:36 +0800 Subject: [PATCH 03/21] support for markdown. --- metagpt/actions/execute_code.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/metagpt/actions/execute_code.py b/metagpt/actions/execute_code.py index 458dc0898..1a97e49d6 100644 --- a/metagpt/actions/execute_code.py +++ b/metagpt/actions/execute_code.py @@ -15,7 +15,7 @@ import nbformat from nbclient import NotebookClient from nbclient.exceptions import CellTimeoutError, DeadKernelError from nbformat import NotebookNode -from nbformat.v4 import new_code_cell, new_output +from nbformat.v4 import new_code_cell, new_output, new_markdown_cell from rich.console import Console from rich.syntax import Syntax @@ -91,6 +91,9 @@ class ExecutePyCode(ExecuteCode, Action): def add_code_cell(self, code): self.nb.cells.append(new_code_cell(source=code)) + def add_markdown_cell(self, markdown): + self.nb.cells.append(new_markdown_cell(source=markdown)) + def _display(self, code, language: str = "python"): if language == "python": code = Syntax(code, "python", theme="paraiso-dark", line_numbers=True) @@ -219,8 +222,9 @@ class ExecutePyCode(ExecuteCode, Action): outputs = self.parse_outputs(self.nb.cells[-1].outputs) return truncate(remove_escape_and_color_codes(outputs), is_success=success) else: - # TODO: markdown - raise NotImplementedError(f"Not support this code type : {language}, Only support code!") + # markdown + self.add_markdown_cell(code) + return code, True def truncate(result: str, keep_len: int = 2000, is_success: bool = True) -> str | bool: From b69f2be165a2c76343f5f7b9b03c18321aa5d588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Mon, 15 Jan 2024 16:51:25 +0800 Subject: [PATCH 04/21] delete type. --- metagpt/actions/execute_code.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/actions/execute_code.py b/metagpt/actions/execute_code.py index 1a97e49d6..fb0ecd893 100644 --- a/metagpt/actions/execute_code.py +++ b/metagpt/actions/execute_code.py @@ -227,7 +227,7 @@ class ExecutePyCode(ExecuteCode, Action): return code, True -def truncate(result: str, keep_len: int = 2000, is_success: bool = True) -> str | bool: +def truncate(result: str, keep_len: int = 2000, is_success: bool = True): desc = f"Executed code {'successfully' if is_success else 'failed, please reflect the cause of bug and then debug'}" if is_success: desc += f"Truncated to show only {keep_len} characters\n" From 4f93c5fad3f03fd0302e3a93760216fc9ca58ffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Mon, 15 Jan 2024 16:59:49 +0800 Subject: [PATCH 05/21] add only_code arg for WriteCodeByGenerate. --- metagpt/actions/write_analysis_code.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/metagpt/actions/write_analysis_code.py b/metagpt/actions/write_analysis_code.py index 04cad34a5..76d47ba28 100644 --- a/metagpt/actions/write_analysis_code.py +++ b/metagpt/actions/write_analysis_code.py @@ -88,8 +88,14 @@ class WriteCodeByGenerate(BaseWriteAnalysisCode): ) -> str: # context.append(Message(content=self.REUSE_CODE_INSTRUCTION, role="user")) prompt = self.process_msg(context, system_msg) + is_only_code = kwargs.pop("only_code", True) + code_content = await self.llm.aask_code(prompt, **kwargs) - return code_content["code"] + if is_only_code: + return code_content["code"] + else: + return code_content + class WriteCodeWithTools(BaseWriteAnalysisCode): From 00f7f93234d0c19286aca3d16233367be2d5fd2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Mon, 15 Jan 2024 18:09:56 +0800 Subject: [PATCH 06/21] add scrape_web. --- metagpt/tools/__init__.py | 6 ++++++ .../tools/functions/schemas/scrape_web.yml | 21 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 metagpt/tools/functions/schemas/scrape_web.yml diff --git a/metagpt/tools/__init__.py b/metagpt/tools/__init__.py index 41c8708b2..c24dc6fce 100644 --- a/metagpt/tools/__init__.py +++ b/metagpt/tools/__init__.py @@ -76,6 +76,12 @@ TOOL_TYPE_MAPPINGS = { desc="Related to text2image, image2image using stable diffusion model.", usage_prompt="", ), + "scrape_web": ToolType( + name="scrape_web", + module="metagpt.tools.scrape_web", + desc="Scrape data from web page.", + usage_prompt="", + ), "other": ToolType( name="other", module="", diff --git a/metagpt/tools/functions/schemas/scrape_web.yml b/metagpt/tools/functions/schemas/scrape_web.yml new file mode 100644 index 000000000..ecca3fbed --- /dev/null +++ b/metagpt/tools/functions/schemas/scrape_web.yml @@ -0,0 +1,21 @@ +scrape_web: + type: async funciton + description: "Scrape and save the HTML structure and inner text content of a web page using Playwright." + parameters: + properties: + url: + type: str + description: "web url" + \*url: + type: Non-Keyword Arguments + description: "other web urls, you can assagin sub url link to it." + required: + - url + returns: + inner_text: + type: str + description: The inner text content of the web page. + html: + type: str + description: The html structure of the web page. + From 75628caf4d68b7519c63a84c0203326ea05ace5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Mon, 15 Jan 2024 18:10:57 +0800 Subject: [PATCH 07/21] add scrape_web.py --- .../functions/libs/scrape_web/__init__.py | 1 + .../functions/libs/scrape_web/scrape_web.py | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 metagpt/tools/functions/libs/scrape_web/__init__.py create mode 100644 metagpt/tools/functions/libs/scrape_web/scrape_web.py diff --git a/metagpt/tools/functions/libs/scrape_web/__init__.py b/metagpt/tools/functions/libs/scrape_web/__init__.py new file mode 100644 index 000000000..d5cd1524b --- /dev/null +++ b/metagpt/tools/functions/libs/scrape_web/__init__.py @@ -0,0 +1 @@ +from metagpt.tools.functions.libs.scrape_web.scrape_web import scrape_web diff --git a/metagpt/tools/functions/libs/scrape_web/scrape_web.py b/metagpt/tools/functions/libs/scrape_web/scrape_web.py new file mode 100644 index 000000000..5cd984f4d --- /dev/null +++ b/metagpt/tools/functions/libs/scrape_web/scrape_web.py @@ -0,0 +1,26 @@ +import asyncio + +from metagpt.tools.web_browser_engine_playwright import PlaywrightWrapper + + +async def scrape_web(url, *urls): + """ + Scrape and save the HTML structure and inner text content of a web page using Playwright. + + Args: + url (str): The main URL to fetch inner text from. + *urls (str): Additional URLs to fetch inner text from. + + Returns: + (dict): The inner text content and html structure of the web page, key are : 'inner_text', 'html'. + + Raises: + Any exceptions that may occur during the Playwright operation. + """ + # Create a PlaywrightWrapper instance for the Chromium browser + web = await PlaywrightWrapper("chromium").run(url, *urls) + + # Return the inner text content of the web page + return {"inner_text": web.inner_text, "html": web.html} + +# 需要改三个地方: yaml, 对应路径下init, MetaGPT/metagpt/prompts/ml_engineer.py中ML_MODULE_MAP From a7e088845e508464281daed1301ce32c0acc0797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Mon, 15 Jan 2024 18:11:22 +0800 Subject: [PATCH 08/21] update scrape_web docstring. --- metagpt/tools/functions/libs/scrape_web/scrape_web.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/metagpt/tools/functions/libs/scrape_web/scrape_web.py b/metagpt/tools/functions/libs/scrape_web/scrape_web.py index 5cd984f4d..e68ce0e64 100644 --- a/metagpt/tools/functions/libs/scrape_web/scrape_web.py +++ b/metagpt/tools/functions/libs/scrape_web/scrape_web.py @@ -13,9 +13,6 @@ async def scrape_web(url, *urls): Returns: (dict): The inner text content and html structure of the web page, key are : 'inner_text', 'html'. - - Raises: - Any exceptions that may occur during the Playwright operation. """ # Create a PlaywrightWrapper instance for the Chromium browser web = await PlaywrightWrapper("chromium").run(url, *urls) From 559a1604ad1e273d9de50fd466b1d0ac2a045d56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Mon, 15 Jan 2024 18:26:32 +0800 Subject: [PATCH 09/21] restore. --- metagpt/provider/base_llm.py | 48 +----------------------------------- 1 file changed, 1 insertion(+), 47 deletions(-) diff --git a/metagpt/provider/base_llm.py b/metagpt/provider/base_llm.py index c482aaf35..dbef15fa1 100644 --- a/metagpt/provider/base_llm.py +++ b/metagpt/provider/base_llm.py @@ -6,14 +6,10 @@ @File : base_llm.py @Desc : mashenquan, 2023/8/22. + try catch """ -import re import json from abc import ABC, abstractmethod from typing import Optional -from metagpt.logs import logger -from metagpt.utils.common import CodeParser - class BaseLLM(ABC): """LLM API abstract class, requiring all inheritors to provide a series of standard capabilities""" @@ -122,30 +118,6 @@ class BaseLLM(ABC): """ return rsp.get("choices")[0]["message"]["tool_calls"][0]["function"] - def _parse_arguments(self, arguments: str) -> dict: - """parse arguments in openai function call""" - if 'langugae' not in arguments and 'code' not in arguments: - logger.warning(f"Not found `code`, `language`, We assume it is pure code:\n {arguments}\n. ") - return {'language': 'python', 'code': arguments} - - # 匹配language - language_pattern = re.compile(r'[\"\']?language[\"\']?\s*:\s*["\']([^"\']+?)["\']', re.DOTALL) - language_match = language_pattern.search(arguments) - language_value = language_match.group(1) if language_match else None - - # 匹配code - code_pattern = r'(["\']{3}|["])([\s\S]*?)\1' - try: - code_value = re.findall(code_pattern, arguments)[-1][-1] - except Exception as e: - logger.error(f"{e}, when re.findall({code_pattern}, {arguments})") - code_value = None - - if code_value is None: - raise ValueError(f"Parse code error for {arguments}") - # arguments只有code的情况 - return {'language': language_value, 'code': code_value} - def get_choice_function_arguments(self, rsp: dict) -> dict: """Required to provide the first function arguments of choice. @@ -153,25 +125,7 @@ class BaseLLM(ABC): :return dict: return the first function arguments of choice, for example, {'language': 'python', 'code': "print('Hello, World!')"} """ - try: - arguments: str = self.get_choice_function(rsp)["arguments"] - return json.loads(arguments, strict=False) - except json.decoder.JSONDecodeError as e: - logger.debug(f"Got JSONDecodeError for {arguments}, we will use RegExp to parse code, \n {e}") - return self._parse_arguments(arguments) - except KeyError as e: - if 'tool_calls' in e.args: - txt_rsp = self.get_choice_text(rsp) - # find code - code = CodeParser.parse_code(None, txt_rsp, lang='python') - if code != txt_rsp: - return {'language': 'python', 'code': code} - # no code - return {'language': 'markdown', 'code': txt_rsp} - raise e - except Exception as e: - logger.error(f"Got error `{e}` for parsing\n {rsp}\n") - return {} + return json.loads(self.get_choice_function(rsp)["arguments"], strict=False) def messages_to_prompt(self, messages: list[dict]): """[{"role": "user", "content": msg}] to user: etc.""" From b430e2c88fe6db7104faf9dc44cad639f95965c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Mon, 15 Jan 2024 19:02:37 +0800 Subject: [PATCH 10/21] update scrape_web module. --- metagpt/tools/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/tools/__init__.py b/metagpt/tools/__init__.py index c24dc6fce..2f8941fdb 100644 --- a/metagpt/tools/__init__.py +++ b/metagpt/tools/__init__.py @@ -78,7 +78,7 @@ TOOL_TYPE_MAPPINGS = { ), "scrape_web": ToolType( name="scrape_web", - module="metagpt.tools.scrape_web", + module=str(TOOL_LIBS_PATH / "scrape_web"), desc="Scrape data from web page.", usage_prompt="", ), From d1666c3307289edd0fcb53c8ba881574ee0dca19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Mon, 15 Jan 2024 21:17:01 +0800 Subject: [PATCH 11/21] update get_choice_function_arguments. --- metagpt/provider/openai_api.py | 71 +++++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 23 deletions(-) diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py index 747e36480..66d215eda 100644 --- a/metagpt/provider/openai_api.py +++ b/metagpt/provider/openai_api.py @@ -9,6 +9,7 @@ @Modified By: mashenquan, 2023/12/1. Fix bug: Unclosed connection caused by openai 0.x. """ +import re import json from typing import AsyncIterator, Union @@ -37,6 +38,7 @@ from metagpt.utils.token_counter import ( count_string_tokens, get_max_completion_tokens, ) +from metagpt.utils.common import CodeParser def log_and_reraise(retry_state): @@ -147,10 +149,7 @@ class OpenAILLM(BaseLLM): def _func_configs(self, messages: list[dict], timeout=3, **kwargs) -> dict: """Note: Keep kwargs consistent with https://platform.openai.com/docs/api-reference/chat/create""" if "tools" not in kwargs: - configs = { - "tools": [{"type": "function", "function": GENERAL_FUNCTION_SCHEMA}], - "tool_choice": GENERAL_TOOL_CHOICE, - } + configs = {"tools": [{"type": "function", "function": GENERAL_FUNCTION_SCHEMA}]} kwargs.update(configs) return self._cons_kwargs(messages=messages, timeout=timeout, **kwargs) @@ -161,23 +160,7 @@ class OpenAILLM(BaseLLM): self._update_costs(rsp.usage) return rsp - def _process_message(self, messages: Union[str, Message, list[dict], list[Message], list[str]]) -> list[dict]: - """convert messages to list[dict].""" - if isinstance(messages, list): - messages = [Message(content=msg) if isinstance(msg, str) else msg for msg in messages] - return [msg if isinstance(msg, dict) else msg.to_dict() for msg in messages] - - if isinstance(messages, Message): - messages = [messages.to_dict()] - elif isinstance(messages, str): - messages = [{"role": "user", "content": messages}] - else: - raise ValueError( - f"Only support messages type are: str, Message, list[dict], but got {type(messages).__name__}!" - ) - return messages - - async def aask_code(self, messages: Union[str, Message, list[dict]], **kwargs) -> dict: + async def aask_code(self, messages: list[dict], **kwargs) -> dict: """Use function of tools to ask a code. Note: Keep kwargs consistent with https://platform.openai.com/docs/api-reference/chat/create @@ -187,18 +170,60 @@ class OpenAILLM(BaseLLM): >>> rsp = await llm.aask_code(msg) # -> {'language': 'python', 'code': "print('Hello, World!')"} """ - messages = self._process_message(messages) rsp = await self._achat_completion_function(messages, **kwargs) return self.get_choice_function_arguments(rsp) + def _parse_arguments(self, arguments: str) -> dict: + """parse arguments in openai function call""" + if 'langugae' not in arguments and 'code' not in arguments: + logger.warning(f"Not found `code`, `language`, We assume it is pure code:\n {arguments}\n. ") + return {'language': 'python', 'code': arguments} + + # 匹配language + language_pattern = re.compile(r'[\"\']?language[\"\']?\s*:\s*["\']([^"\']+?)["\']', re.DOTALL) + language_match = language_pattern.search(arguments) + language_value = language_match.group(1) if language_match else None + + # 匹配code + code_pattern = r'(["\'`]{3}|["\'`])([\s\S]*?)\1' + try: + code_value = re.findall(code_pattern, arguments)[-1][-1] + except Exception as e: + logger.error(f"{e}, when re.findall({code_pattern}, {arguments})") + code_value = None + + if code_value is None: + raise ValueError(f"Parse code error for {arguments}") + # arguments只有code的情况 + return {'language': language_value, 'code': code_value} + @handle_exception def get_choice_function_arguments(self, rsp: ChatCompletion) -> dict: """Required to provide the first function arguments of choice. + :param dict rsp: same as in self.get_choice_function(rsp) :return dict: return the first function arguments of choice, for example, {'language': 'python', 'code': "print('Hello, World!')"} """ - return json.loads(rsp.choices[0].message.tool_calls[0].function.arguments) + message = rsp.choices[0].message + if ( + message.tool_calls is not None and + message.tool_calls[0].function is not None and + message.tool_calls[0].function.arguments is not None + ): + # reponse is code + try: + return json.loads(message.tool_calls[0].function.arguments, strict=False) + except json.decoder.JSONDecodeError as e: + logger.debug(f"Got JSONDecodeError for {message.tool_calls[0].function.arguments},\ + we will use RegExp to parse code, \n {e}") + return {'language': 'python', 'code': self._parse_arguments(message.tool_calls[0].function.arguments)} + elif message.tool_calls is None and message.content is not None: + # reponse is message + return {'language': 'markdown', 'code': self.get_choice_text(rsp)} + else: + logger.error(f"Failed to parse \n {rsp}\n") + raise Exception(f"Failed to parse \n {rsp}\n") def get_choice_text(self, rsp: ChatCompletion) -> str: """Required to provide the first text of choice""" From f9b1cce654e36ac764acd7db0b7d5c74404dc877 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Mon, 15 Jan 2024 22:21:56 +0800 Subject: [PATCH 12/21] update code-intepreter by auto aask. --- metagpt/actions/write_analysis_code.py | 2 +- metagpt/provider/openai_api.py | 23 +++++++++++++++++++++++ metagpt/roles/code_interpreter.py | 9 ++++++--- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/metagpt/actions/write_analysis_code.py b/metagpt/actions/write_analysis_code.py index 76d47ba28..bceb100b1 100644 --- a/metagpt/actions/write_analysis_code.py +++ b/metagpt/actions/write_analysis_code.py @@ -88,7 +88,7 @@ class WriteCodeByGenerate(BaseWriteAnalysisCode): ) -> str: # context.append(Message(content=self.REUSE_CODE_INSTRUCTION, role="user")) prompt = self.process_msg(context, system_msg) - is_only_code = kwargs.pop("only_code", True) + is_only_code = kwargs.pop("only_code", False) code_content = await self.llm.aask_code(prompt, **kwargs) if is_only_code: diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py index 66d215eda..7bdb4bfbe 100644 --- a/metagpt/provider/openai_api.py +++ b/metagpt/provider/openai_api.py @@ -154,7 +154,30 @@ class OpenAILLM(BaseLLM): return self._cons_kwargs(messages=messages, timeout=timeout, **kwargs) + def _process_message(self, messages: Union[str, Message, list[dict], list[Message], list[str]]) -> list[dict]: + """convert messages to list[dict].""" + # 全部转成list + if not isinstance(messages, list): + messages = [messages] + + # 转成list[dict] + processed_messages = [] + for msg in messages: + if isinstance(msg, str): + processed_messages.append({"role": "user", "content": msg}) + elif isinstance(msg, dict): + assert set(msg.keys()) == set(['role', 'content']) + processed_messages.append(msg) + elif isinstance(msg, Message): + processed_messages.append(msg.to_dict()) + else: + raise ValueError( + f"Only support message type are: str, Message, dict, but got {type(messages).__name__}!" + ) + return processed_messages + async def _achat_completion_function(self, messages: list[dict], timeout=3, **chat_configs) -> ChatCompletion: + messages = self._process_message(messages) kwargs = self._func_configs(messages=messages, timeout=timeout, **chat_configs) rsp: ChatCompletion = await self.aclient.chat.completions.create(**kwargs) self._update_costs(rsp.usage) diff --git a/metagpt/roles/code_interpreter.py b/metagpt/roles/code_interpreter.py index 164c7cb12..afd51a575 100644 --- a/metagpt/roles/code_interpreter.py +++ b/metagpt/roles/code_interpreter.py @@ -52,7 +52,7 @@ class CodeInterpreter(Role): async def _act_on_task(self, current_task: Task) -> TaskResult: code, result, is_success = await self._write_and_exec_code() - task_result = TaskResult(code=code, result=result, is_success=is_success) + task_result = TaskResult(code=code['code'], result=result, is_success=is_success) return task_result async def _write_and_exec_code(self, max_retry: int = 3): @@ -63,10 +63,10 @@ class CodeInterpreter(Role): ### write code ### code, cause_by = await self._write_code() - self.working_memory.add(Message(content=code, role="assistant", cause_by=cause_by)) + self.working_memory.add(Message(content=code['code'], role="assistant", cause_by=cause_by)) ### execute code ### - result, success = await self.execute_code.run(code) + result, success = await self.execute_code.run(**code) print(result) self.working_memory.add(Message(content=result, role="user", cause_by=ExecutePyCode)) @@ -91,6 +91,9 @@ class CodeInterpreter(Role): context = self.planner.get_useful_memories() code = await todo.run(context=context, plan=self.planner.plan, temperature=0.0) + # 暂时在这里转换 WriteCodeWithTools 的输出 + if isinstance(code, str): + code = {'code': code, 'language': 'python'} return code, todo From 29fd7117ef5cf187506e53727437881068118113 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Tue, 16 Jan 2024 11:57:08 +0800 Subject: [PATCH 13/21] update module. --- metagpt/tools/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/tools/__init__.py b/metagpt/tools/__init__.py index 2f8941fdb..73de03156 100644 --- a/metagpt/tools/__init__.py +++ b/metagpt/tools/__init__.py @@ -78,7 +78,7 @@ TOOL_TYPE_MAPPINGS = { ), "scrape_web": ToolType( name="scrape_web", - module=str(TOOL_LIBS_PATH / "scrape_web"), + module="metagpt.tools.functions.libs.scrape_web.scrape_web", desc="Scrape data from web page.", usage_prompt="", ), From eef77d1628bf48657ecf307ba69ab21f21e2d71e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Tue, 16 Jan 2024 12:29:52 +0800 Subject: [PATCH 14/21] display markdown. --- metagpt/actions/execute_code.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/metagpt/actions/execute_code.py b/metagpt/actions/execute_code.py index fb0ecd893..9fadd0acd 100644 --- a/metagpt/actions/execute_code.py +++ b/metagpt/actions/execute_code.py @@ -18,6 +18,8 @@ from nbformat import NotebookNode from nbformat.v4 import new_code_cell, new_output, new_markdown_cell from rich.console import Console from rich.syntax import Syntax +from rich.markdown import Markdown + from metagpt.actions import Action from metagpt.logs import logger @@ -97,8 +99,12 @@ class ExecutePyCode(ExecuteCode, Action): def _display(self, code, language: str = "python"): if language == "python": code = Syntax(code, "python", theme="paraiso-dark", line_numbers=True) - self.console.print("\n") self.console.print(code) + elif language == "markdown": + code = Markdown(code, inline_code_theme="paraiso-dark") + self.console.print(code) + else: + raise ValueError(f"Only support for python, markdown, but got {language}") def add_output_to_cell(self, cell, output): if "outputs" not in cell: @@ -221,10 +227,12 @@ class ExecutePyCode(ExecuteCode, Action): # code success outputs = self.parse_outputs(self.nb.cells[-1].outputs) return truncate(remove_escape_and_color_codes(outputs), is_success=success) - else: + elif language == 'markdown': # markdown self.add_markdown_cell(code) return code, True + else: + raise ValueError(f"Only support for language: python, markdown, but got {language}, ") def truncate(result: str, keep_len: int = 2000, is_success: bool = True): From 95ce190f32a88d59455bb8bb982b64dd3a5018c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Tue, 16 Jan 2024 14:30:07 +0800 Subject: [PATCH 15/21] feature: display markdown content. --- metagpt/actions/execute_code.py | 36 ++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/metagpt/actions/execute_code.py b/metagpt/actions/execute_code.py index 9fadd0acd..6d9135ec3 100644 --- a/metagpt/actions/execute_code.py +++ b/metagpt/actions/execute_code.py @@ -19,7 +19,10 @@ from nbformat.v4 import new_code_cell, new_output, new_markdown_cell from rich.console import Console from rich.syntax import Syntax from rich.markdown import Markdown - +from rich.panel import Panel +from rich.box import MINIMAL +from rich.live import Live +from rich.console import Group from metagpt.actions import Action from metagpt.logs import logger @@ -101,8 +104,7 @@ class ExecutePyCode(ExecuteCode, Action): code = Syntax(code, "python", theme="paraiso-dark", line_numbers=True) self.console.print(code) elif language == "markdown": - code = Markdown(code, inline_code_theme="paraiso-dark") - self.console.print(code) + _display_markdown(code) else: raise ValueError(f"Only support for python, markdown, but got {language}") @@ -265,3 +267,31 @@ def remove_escape_and_color_codes(input_str): pattern = re.compile(r"\x1b\[[0-9;]*[mK]") result = pattern.sub("", input_str) return result + + +def _display_markdown(content: str): + # 使用正则表达式逐个匹配代码块 + matches = re.finditer(r'```(.+?)```', content, re.DOTALL) + start_index = 0 + content_panels = [] + # 逐个打印匹配到的文本和代码 + for match in matches: + text_content = content[start_index:match.start()].strip() + code_content = match.group(0).strip()[3:-3] # Remove triple backticks + + if text_content: + content_panels.append(Panel(Markdown(text_content), box=MINIMAL)) + + if code_content: + content_panels.append(Panel(Markdown(f"```{code_content}"), box=MINIMAL)) + start_index = match.end() + + # 打印剩余文本(如果有) + remaining_text = content[start_index:].strip() + if remaining_text: + content_panels.append(Panel(Markdown(remaining_text), box=MINIMAL)) + + # 在Live模式中显示所有Panel + with Live(auto_refresh=False, console=Console(), vertical_overflow="visible") as live: + live.update(Group(*content_panels)) + live.refresh() From 43558c208ebd1dbf6bf980f0a271abaed4557a7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Tue, 16 Jan 2024 15:03:12 +0800 Subject: [PATCH 16/21] doc: add ipywidgets. --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 7ef6d884e..016c2f5d5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -65,3 +65,4 @@ networkx~=3.2.1 google-generativeai==0.3.2 # playwright==1.40.0 # playwright extras require anytree +ipywidgets==8.1.1 From ff10c9bdda72cce908da673c29a5980105129797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Wed, 17 Jan 2024 18:10:30 +0800 Subject: [PATCH 17/21] change name: _display_markdown -> display_markdown. --- metagpt/actions/execute_code.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/metagpt/actions/execute_code.py b/metagpt/actions/execute_code.py index 6d9135ec3..5b6cba57d 100644 --- a/metagpt/actions/execute_code.py +++ b/metagpt/actions/execute_code.py @@ -104,7 +104,7 @@ class ExecutePyCode(ExecuteCode, Action): code = Syntax(code, "python", theme="paraiso-dark", line_numbers=True) self.console.print(code) elif language == "markdown": - _display_markdown(code) + display_markdown(code) else: raise ValueError(f"Only support for python, markdown, but got {language}") @@ -269,7 +269,7 @@ def remove_escape_and_color_codes(input_str): return result -def _display_markdown(content: str): +def display_markdown(content: str): # 使用正则表达式逐个匹配代码块 matches = re.finditer(r'```(.+?)```', content, re.DOTALL) start_index = 0 From 20f31fa027b32181cbcddce8fc7b24cdb2bb0a0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Wed, 17 Jan 2024 18:17:52 +0800 Subject: [PATCH 18/21] pre-commit. --- metagpt/provider/openai_api.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py index 7bdb4bfbe..3edd89835 100644 --- a/metagpt/provider/openai_api.py +++ b/metagpt/provider/openai_api.py @@ -9,8 +9,8 @@ @Modified By: mashenquan, 2023/12/1. Fix bug: Unclosed connection caused by openai 0.x. """ -import re import json +import re from typing import AsyncIterator, Union from openai import APIConnectionError, AsyncOpenAI, AsyncStream @@ -28,7 +28,7 @@ from tenacity import ( from metagpt.config import CONFIG, Config, LLMProviderEnum from metagpt.logs import log_llm_stream, logger from metagpt.provider.base_llm import BaseLLM -from metagpt.provider.constant import GENERAL_FUNCTION_SCHEMA, GENERAL_TOOL_CHOICE +from metagpt.provider.constant import GENERAL_FUNCTION_SCHEMA from metagpt.provider.llm_provider_registry import register_provider from metagpt.schema import Message from metagpt.utils.cost_manager import Costs @@ -38,7 +38,6 @@ from metagpt.utils.token_counter import ( count_string_tokens, get_max_completion_tokens, ) -from metagpt.utils.common import CodeParser def log_and_reraise(retry_state): @@ -166,7 +165,7 @@ class OpenAILLM(BaseLLM): if isinstance(msg, str): processed_messages.append({"role": "user", "content": msg}) elif isinstance(msg, dict): - assert set(msg.keys()) == set(['role', 'content']) + assert set(msg.keys()) == set(["role", "content"]) processed_messages.append(msg) elif isinstance(msg, Message): processed_messages.append(msg.to_dict()) @@ -198,9 +197,9 @@ class OpenAILLM(BaseLLM): def _parse_arguments(self, arguments: str) -> dict: """parse arguments in openai function call""" - if 'langugae' not in arguments and 'code' not in arguments: + if "langugae" not in arguments and "code" not in arguments: logger.warning(f"Not found `code`, `language`, We assume it is pure code:\n {arguments}\n. ") - return {'language': 'python', 'code': arguments} + return {"language": "python", "code": arguments} # 匹配language language_pattern = re.compile(r'[\"\']?language[\"\']?\s*:\s*["\']([^"\']+?)["\']', re.DOTALL) @@ -218,7 +217,7 @@ class OpenAILLM(BaseLLM): if code_value is None: raise ValueError(f"Parse code error for {arguments}") # arguments只有code的情况 - return {'language': language_value, 'code': code_value} + return {"language": language_value, "code": code_value} @handle_exception def get_choice_function_arguments(self, rsp: ChatCompletion) -> dict: @@ -230,20 +229,22 @@ class OpenAILLM(BaseLLM): """ message = rsp.choices[0].message if ( - message.tool_calls is not None and - message.tool_calls[0].function is not None and - message.tool_calls[0].function.arguments is not None + message.tool_calls is not None + and message.tool_calls[0].function is not None + and message.tool_calls[0].function.arguments is not None ): # reponse is code try: return json.loads(message.tool_calls[0].function.arguments, strict=False) except json.decoder.JSONDecodeError as e: - logger.debug(f"Got JSONDecodeError for {message.tool_calls[0].function.arguments},\ - we will use RegExp to parse code, \n {e}") - return {'language': 'python', 'code': self._parse_arguments(message.tool_calls[0].function.arguments)} + logger.debug( + f"Got JSONDecodeError for {message.tool_calls[0].function.arguments},\ + we will use RegExp to parse code, \n {e}" + ) + return {"language": "python", "code": self._parse_arguments(message.tool_calls[0].function.arguments)} elif message.tool_calls is None and message.content is not None: # reponse is message - return {'language': 'markdown', 'code': self.get_choice_text(rsp)} + return {"language": "markdown", "code": self.get_choice_text(rsp)} else: logger.error(f"Failed to parse \n {rsp}\n") raise Exception(f"Failed to parse \n {rsp}\n") From 10129c6ecf431b9f40b1dd2a349eb2cc0de5c024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Thu, 18 Jan 2024 12:07:31 +0800 Subject: [PATCH 19/21] update scrape_web. --- metagpt/tools/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/metagpt/tools/__init__.py b/metagpt/tools/__init__.py index 95872940f..222edf312 100644 --- a/metagpt/tools/__init__.py +++ b/metagpt/tools/__init__.py @@ -16,7 +16,7 @@ from metagpt.prompts.tool_type import ( FEATURE_ENGINEERING_PROMPT, MODEL_TRAIN_PROMPT, MODEL_EVALUATE_PROMPT, - VISION_PROMPT + VISION_PROMPT, ) @@ -81,7 +81,8 @@ TOOL_TYPE_MAPPINGS = { name="scrape_web", module="metagpt.tools.functions.libs.scrape_web.scrape_web", desc="Scrape data from web page.", - usage_prompt=""), + usage_prompt="", + ), "vision": ToolType( name="vision", module=str(TOOL_LIBS_PATH / "vision"), From f3612f8123941abfa5e8aae221ba5c1ab69512a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Thu, 18 Jan 2024 12:10:37 +0800 Subject: [PATCH 20/21] add only_code arg for WriteCodeByGenerate. --- metagpt/roles/ml_engineer_simple.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/roles/ml_engineer_simple.py b/metagpt/roles/ml_engineer_simple.py index 3f10af8d0..9ff1c9880 100644 --- a/metagpt/roles/ml_engineer_simple.py +++ b/metagpt/roles/ml_engineer_simple.py @@ -75,7 +75,7 @@ class MLEngineerSimple(Role): context = self.get_useful_memories() print(f"memories数量:{len(context)}") # print("===\n" +str(context) + "\n===") - code = await WriteCodeByGenerate().run(context=context, temperature=0.0) + code = await WriteCodeByGenerate().run(context=context, temperature=0.0, only_code=True) cause_by = WriteCodeByGenerate self.working_memory.add(Message(content=code, role="assistant", cause_by=cause_by)) From d78db8994c6cb6c05f40c30891987358dcafd242 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Thu, 18 Jan 2024 20:57:43 +0800 Subject: [PATCH 21/21] delete arg only_code. --- metagpt/actions/write_analysis_code.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/metagpt/actions/write_analysis_code.py b/metagpt/actions/write_analysis_code.py index bceb100b1..9104fdf82 100644 --- a/metagpt/actions/write_analysis_code.py +++ b/metagpt/actions/write_analysis_code.py @@ -85,17 +85,11 @@ class WriteCodeByGenerate(BaseWriteAnalysisCode): plan: Plan = None, system_msg: str = None, **kwargs, - ) -> str: + ) -> dict: # context.append(Message(content=self.REUSE_CODE_INSTRUCTION, role="user")) prompt = self.process_msg(context, system_msg) - is_only_code = kwargs.pop("only_code", False) - code_content = await self.llm.aask_code(prompt, **kwargs) - if is_only_code: - return code_content["code"] - else: - return code_content - + return code_content class WriteCodeWithTools(BaseWriteAnalysisCode):