From f20064a5786540f7364a5cf60b2fa237af3ef4e5 Mon Sep 17 00:00:00 2001 From: eukub Date: Thu, 28 Dec 2023 20:10:33 +0300 Subject: [PATCH 001/131] =?UTF-8?q?=D1=81hanged=20concatenation=20of=20str?= =?UTF-8?q?ings=20to=20f-strings=20to=20improve=20readability=20and=20unif?= =?UTF-8?q?y=20with=20the=20rest=20of=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- metagpt/actions/action_node.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/metagpt/actions/action_node.py b/metagpt/actions/action_node.py index 3529942c3..e44e5e96e 100644 --- a/metagpt/actions/action_node.py +++ b/metagpt/actions/action_node.py @@ -195,10 +195,7 @@ class ActionNode: def tagging(self, text, schema, tag="") -> str: if not tag: return text - if schema == "json": - return f"[{tag}]\n" + text + f"\n[/{tag}]" - else: # markdown - return f"[{tag}]\n" + text + f"\n[/{tag}]" + return f"[{tag}]\n{text}\n[/{tag}]" def _compile_f(self, schema, mode, tag, format_func, kv_sep) -> str: nodes = self.to_dict(format_func=format_func, mode=mode) @@ -230,7 +227,7 @@ class ActionNode: schema="markdown": 编译context, example(markdown), instruction(markdown), constraint, action """ if schema == "raw": - return context + "\n\n## Actions\n" + LANGUAGE_CONSTRAINT + "\n" + self.instruction + return f"{context}\n\n## Actions\n{LANGUAGE_CONSTRAINT}\n{self.instruction}" # FIXME: json instruction会带来格式问题,如:"Project name": "web_2048 # 项目名称使用下划线", # compile example暂时不支持markdown From 711c5e68094d09092abc642455336c5ced246eba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Sun, 18 Feb 2024 19:16:42 +0800 Subject: [PATCH 002/131] set `block on white` style for markdown block. --- metagpt/actions/mi/execute_nb_code.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/metagpt/actions/mi/execute_nb_code.py b/metagpt/actions/mi/execute_nb_code.py index a8c9c3085..cb3c80352 100644 --- a/metagpt/actions/mi/execute_nb_code.py +++ b/metagpt/actions/mi/execute_nb_code.py @@ -226,22 +226,24 @@ def display_markdown(content: str): matches = re.finditer(r"```(.+?)```", content, re.DOTALL) start_index = 0 content_panels = [] + # 文本背景色和文字颜色设置 + style = "black on white" # 逐个打印匹配到的文本和代码 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)) + content_panels.append(Panel(Markdown(text_content), style=style, box=MINIMAL)) if code_content: - content_panels.append(Panel(Markdown(f"```{code_content}"), box=MINIMAL)) + content_panels.append(Panel(Markdown(f"```{code_content}"), style=style, box=MINIMAL)) start_index = match.end() # 打印剩余文本(如果有) remaining_text = content[start_index:].strip() if remaining_text: - content_panels.append(Panel(Markdown(remaining_text), box=MINIMAL)) + content_panels.append(Panel(Markdown(remaining_text), style=style, box=MINIMAL)) # 在Live模式中显示所有Panel with Live(auto_refresh=False, console=Console(), vertical_overflow="visible") as live: From e1eb69b07d4cec1b3714279d3fe866f397f7109a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Mon, 19 Feb 2024 10:21:59 +0800 Subject: [PATCH 003/131] chore: annotated in English. --- metagpt/actions/mi/execute_nb_code.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/metagpt/actions/mi/execute_nb_code.py b/metagpt/actions/mi/execute_nb_code.py index cb3c80352..8e8e997b8 100644 --- a/metagpt/actions/mi/execute_nb_code.py +++ b/metagpt/actions/mi/execute_nb_code.py @@ -215,20 +215,21 @@ def truncate(result: str, keep_len: int = 2000, is_success: bool = True): def remove_escape_and_color_codes(input_str: str): - # 使用正则表达式去除转义字符和颜色代码 + # 使用正则表达式去除jupyter notebook输出结果中的转义字符和颜色代码 + # Use regular expressions to get rid of escape characters and color codes in jupyter notebook output. pattern = re.compile(r"\x1b\[[0-9;]*[mK]") result = pattern.sub("", input_str) return result def display_markdown(content: str): - # 使用正则表达式逐个匹配代码块 + # Use regular expressions to match blocks of code one by one. matches = re.finditer(r"```(.+?)```", content, re.DOTALL) start_index = 0 content_panels = [] - # 文本背景色和文字颜色设置 + # Set the text background color and text color. style = "black on white" - # 逐个打印匹配到的文本和代码 + # Print the matching text and code one by one. for match in matches: text_content = content[start_index : match.start()].strip() code_content = match.group(0).strip()[3:-3] # Remove triple backticks @@ -240,12 +241,12 @@ def display_markdown(content: str): content_panels.append(Panel(Markdown(f"```{code_content}"), style=style, box=MINIMAL)) start_index = match.end() - # 打印剩余文本(如果有) + # Print remaining text (if any). remaining_text = content[start_index:].strip() if remaining_text: content_panels.append(Panel(Markdown(remaining_text), style=style, box=MINIMAL)) - # 在Live模式中显示所有Panel + # Display all panels in Live mode. with Live(auto_refresh=False, console=Console(), vertical_overflow="visible") as live: live.update(Group(*content_panels)) live.refresh() From bcf5f051bb6d9b7853a0e969fe3c98713094b84a Mon Sep 17 00:00:00 2001 From: yzlin Date: Tue, 20 Feb 2024 21:13:34 +0800 Subject: [PATCH 004/131] fix human interact bug & add example --- examples/mi/machine_learning.py | 10 +++++----- metagpt/strategy/planner.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/mi/machine_learning.py b/examples/mi/machine_learning.py index ee3b47f26..a8ab5051e 100644 --- a/examples/mi/machine_learning.py +++ b/examples/mi/machine_learning.py @@ -1,13 +1,13 @@ -import asyncio +import fire from metagpt.roles.mi.interpreter import Interpreter -async def main(requirement: str): - mi = Interpreter(auto_run=True, use_tools=False) +async def main(auto_run: bool = True): + requirement = "Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy." + mi = Interpreter(auto_run=auto_run) await mi.run(requirement) if __name__ == "__main__": - requirement = "Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy." - asyncio.run(main(requirement)) + fire.Fire(main) diff --git a/metagpt/strategy/planner.py b/metagpt/strategy/planner.py index 851d45893..99d16f78b 100644 --- a/metagpt/strategy/planner.py +++ b/metagpt/strategy/planner.py @@ -122,7 +122,7 @@ class Planner(BaseModel): ) # "confirm, ... (more content, such as changing downstream tasks)" if confirmed_and_more: self.working_memory.add(Message(content=review, role="user", cause_by=AskReview)) - await self.update_plan(review) + await self.update_plan() def get_useful_memories(self, task_exclude_field=None) -> list[Message]: """find useful memories only to reduce context length and improve performance""" From 0eda2e6581561938063f8a4389210cf260daff1e Mon Sep 17 00:00:00 2001 From: yzlin Date: Tue, 20 Feb 2024 22:56:50 +0800 Subject: [PATCH 005/131] fix tool path bug --- metagpt/tools/tool_registry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/metagpt/tools/tool_registry.py b/metagpt/tools/tool_registry.py index 5fbd39421..02eecb0f3 100644 --- a/metagpt/tools/tool_registry.py +++ b/metagpt/tools/tool_registry.py @@ -9,7 +9,6 @@ from __future__ import annotations import inspect import os -import re from collections import defaultdict import yaml @@ -109,7 +108,8 @@ def register_tool(tool_type: str = "other", schema_path: str = "", **kwargs): # Get the file path where the function / class is defined and the source code file_path = inspect.getfile(cls) if "metagpt" in file_path: - file_path = re.search("metagpt.+", file_path).group(0) + # split to handle ../metagpt/metagpt/tools/... where only metapgt/tools/... is needed + file_path = "metagpt" + file_path.split("metagpt")[-1] source_code = inspect.getsource(cls) TOOL_REGISTRY.register_tool( From 3dde4664f40f9ae9f92441369a7cab0c88f7e68d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Wed, 21 Feb 2024 11:03:32 +0800 Subject: [PATCH 006/131] add sales_forecast in machine_learning. --- examples/mi/machine_learning.py | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/examples/mi/machine_learning.py b/examples/mi/machine_learning.py index a8ab5051e..a76561a37 100644 --- a/examples/mi/machine_learning.py +++ b/examples/mi/machine_learning.py @@ -3,10 +3,36 @@ import fire from metagpt.roles.mi.interpreter import Interpreter -async def main(auto_run: bool = True): - requirement = "Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy." +DATA_DIR = "examples/mi/data" +requirements = { + "wine": "Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.", + + # sales_forecast data from https://www.kaggle.com/datasets/aslanahmedov/walmart-sales-forecast/data + "sales_forecast": f""" + # Goal + Use time series regression machine learning to make predictions for Dept sales of the stores as accurate as possible. + + # Datasets Available + - train_data: {DATA_DIR}/WalmartSalesForecast/new_train.csv + - test_data: {DATA_DIR}/WalmartSalesForecast/new_test.csv + - additional data: {DATA_DIR}/WalmartSalesForecast/features.csv; To merge on train, test data. + - stores data: {DATA_DIR}/WalmartSalesForecast/stores.csv; To merge on train, test data. + + # Metric + The metric of the competition is weighted mean absolute error (WMAE) for test data. + + # Notice + - *print* key variables to get more information for next task step. + - Perform data analysis by plotting sales trends, holiday effects, distribution of sales across stores/departments using box/violin on the train data. + - Make sure the DataFrame.dtypes must be int, float or bool, and drop date column. + - Plot scatter plots of groud truth and predictions on test data. + """ +} + + +async def main(auto_run: bool = True, use_case: str = 'wine'): mi = Interpreter(auto_run=auto_run) - await mi.run(requirement) + await mi.run(requirements[use_case]) if __name__ == "__main__": From 247fa13e864b22848e33656b21752e64db30cc11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Wed, 21 Feb 2024 11:03:48 +0800 Subject: [PATCH 007/131] fix: pip error. --- metagpt/actions/mi/execute_nb_code.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/actions/mi/execute_nb_code.py b/metagpt/actions/mi/execute_nb_code.py index 8e8e997b8..0e4563a37 100644 --- a/metagpt/actions/mi/execute_nb_code.py +++ b/metagpt/actions/mi/execute_nb_code.py @@ -182,7 +182,7 @@ class ExecuteNbCode(Action): outputs = self.parse_outputs(self.nb.cells[-1].outputs) outputs, success = truncate(remove_escape_and_color_codes(outputs), is_success=success) - if "!pip" in outputs: + if "!pip" in code: success = False return outputs, success From 662fbd7e5554f426cc34e42402e48fc5ab407621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Wed, 21 Feb 2024 11:43:02 +0800 Subject: [PATCH 008/131] chore. --- examples/mi/machine_learning.py | 44 ++++++++++++++++----------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/examples/mi/machine_learning.py b/examples/mi/machine_learning.py index a76561a37..689335db3 100644 --- a/examples/mi/machine_learning.py +++ b/examples/mi/machine_learning.py @@ -2,35 +2,35 @@ import fire from metagpt.roles.mi.interpreter import Interpreter - DATA_DIR = "examples/mi/data" -requirements = { - "wine": "Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.", +WINE_REQ = "Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy." - # sales_forecast data from https://www.kaggle.com/datasets/aslanahmedov/walmart-sales-forecast/data - "sales_forecast": f""" - # Goal - Use time series regression machine learning to make predictions for Dept sales of the stores as accurate as possible. +# sales_forecast data from https://www.kaggle.com/datasets/aslanahmedov/walmart-sales-forecast/data, +# new_train, new_test from train.csv. +SALES_FORECAST_REQ = f""" +# Goal +Use time series regression machine learning to make predictions for Dept sales of the stores as accurate as possible. - # Datasets Available - - train_data: {DATA_DIR}/WalmartSalesForecast/new_train.csv - - test_data: {DATA_DIR}/WalmartSalesForecast/new_test.csv - - additional data: {DATA_DIR}/WalmartSalesForecast/features.csv; To merge on train, test data. - - stores data: {DATA_DIR}/WalmartSalesForecast/stores.csv; To merge on train, test data. +# Datasets Available +- train_data: {DATA_DIR}/WalmartSalesForecast/new_train.csv +- test_data: {DATA_DIR}/WalmartSalesForecast/new_test.csv +- additional data: {DATA_DIR}/WalmartSalesForecast/features.csv; To merge on train, test data. +- stores data: {DATA_DIR}/WalmartSalesForecast/stores.csv; To merge on train, test data. - # Metric - The metric of the competition is weighted mean absolute error (WMAE) for test data. +# Metric +The metric of the competition is weighted mean absolute error (WMAE) for test data. - # Notice - - *print* key variables to get more information for next task step. - - Perform data analysis by plotting sales trends, holiday effects, distribution of sales across stores/departments using box/violin on the train data. - - Make sure the DataFrame.dtypes must be int, float or bool, and drop date column. - - Plot scatter plots of groud truth and predictions on test data. - """ -} +# Notice +- *print* key variables to get more information for next task step. +- Perform data analysis by plotting sales trends, holiday effects, distribution of sales across stores/departments using box/violin on the train data. +- Make sure the DataFrame.dtypes must be int, float or bool, and drop date column. +- Plot scatter plots of groud truth and predictions on test data. +""" + +requirements = {"wine": WINE_REQ, "sales_forecast": SALES_FORECAST_REQ} -async def main(auto_run: bool = True, use_case: str = 'wine'): +async def main(auto_run: bool = True, use_case: str = "wine"): mi = Interpreter(auto_run=auto_run) await mi.run(requirements[use_case]) From fc4017480205104f281f0367ef83acc433375a59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Wed, 21 Feb 2024 21:33:11 +0800 Subject: [PATCH 009/131] chore. --- examples/mi/machine_learning.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/examples/mi/machine_learning.py b/examples/mi/machine_learning.py index 689335db3..5f9d5b0cd 100644 --- a/examples/mi/machine_learning.py +++ b/examples/mi/machine_learning.py @@ -2,29 +2,30 @@ import fire from metagpt.roles.mi.interpreter import Interpreter -DATA_DIR = "examples/mi/data" WINE_REQ = "Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy." -# sales_forecast data from https://www.kaggle.com/datasets/aslanahmedov/walmart-sales-forecast/data, -# new_train, new_test from train.csv. +# DATA_DIR = "your/path/to/data" +DATA_DIR = "examples/mi/data/WalmartSalesForecast2" +# sales_forecast data from https://www.kaggle.com/datasets/aslanahmedov/walmart-sales-forecast/data SALES_FORECAST_REQ = f""" # Goal -Use time series regression machine learning to make predictions for Dept sales of the stores as accurate as possible. +Train a model to predict sales for each department in every store (split the last 40 weeks records as validation dataset, +the others is train dataset), include plot sales trends, holiday effects, distribution of sales across stores/departments, +using box on the train dataset, print metric and plot scatter plots of groud truth and predictions on validation data. +save predictions on test data. # Datasets Available -- train_data: {DATA_DIR}/WalmartSalesForecast/new_train.csv -- test_data: {DATA_DIR}/WalmartSalesForecast/new_test.csv -- additional data: {DATA_DIR}/WalmartSalesForecast/features.csv; To merge on train, test data. -- stores data: {DATA_DIR}/WalmartSalesForecast/stores.csv; To merge on train, test data. +- train_data: {DATA_DIR}/train.csv +- test_data: {DATA_DIR}/test.csv, no label data. +- additional data: {DATA_DIR}/features.csv +- stores data: {DATA_DIR}/stores.csv # Metric The metric of the competition is weighted mean absolute error (WMAE) for test data. # Notice - *print* key variables to get more information for next task step. -- Perform data analysis by plotting sales trends, holiday effects, distribution of sales across stores/departments using box/violin on the train data. -- Make sure the DataFrame.dtypes must be int, float or bool, and drop date column. -- Plot scatter plots of groud truth and predictions on test data. +- Only When you fit the model, make the DataFrame.dtypes to be int, float or bool, and drop date column. """ requirements = {"wine": WINE_REQ, "sales_forecast": SALES_FORECAST_REQ} @@ -32,7 +33,12 @@ requirements = {"wine": WINE_REQ, "sales_forecast": SALES_FORECAST_REQ} async def main(auto_run: bool = True, use_case: str = "wine"): mi = Interpreter(auto_run=auto_run) - await mi.run(requirements[use_case]) + if use_case == "wine": + requirement = requirements[use_case] + else: + assert DATA_DIR != "your/path/to/data", f"Please set DATA_DIR for the use_case: {use_case}!" + requirement = requirements[use_case] + await mi.run(requirement) if __name__ == "__main__": From 6652aa09ce8e2e19ba4a8ffd89013fae2fccb23f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Wed, 21 Feb 2024 22:52:18 +0800 Subject: [PATCH 010/131] delete feature.csv, store.csv, test.csv. --- examples/mi/machine_learning.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/examples/mi/machine_learning.py b/examples/mi/machine_learning.py index 5f9d5b0cd..652e7c908 100644 --- a/examples/mi/machine_learning.py +++ b/examples/mi/machine_learning.py @@ -10,22 +10,17 @@ DATA_DIR = "examples/mi/data/WalmartSalesForecast2" SALES_FORECAST_REQ = f""" # Goal Train a model to predict sales for each department in every store (split the last 40 weeks records as validation dataset, -the others is train dataset), include plot sales trends, holiday effects, distribution of sales across stores/departments, -using box on the train dataset, print metric and plot scatter plots of groud truth and predictions on validation data. -save predictions on test data. +the others is train dataset), include plot sales trends, print metric and plot scatter plots of +groud truth and predictions on validation data. # Datasets Available - train_data: {DATA_DIR}/train.csv -- test_data: {DATA_DIR}/test.csv, no label data. -- additional data: {DATA_DIR}/features.csv -- stores data: {DATA_DIR}/stores.csv # Metric The metric of the competition is weighted mean absolute error (WMAE) for test data. # Notice - *print* key variables to get more information for next task step. -- Only When you fit the model, make the DataFrame.dtypes to be int, float or bool, and drop date column. """ requirements = {"wine": WINE_REQ, "sales_forecast": SALES_FORECAST_REQ} @@ -36,6 +31,7 @@ async def main(auto_run: bool = True, use_case: str = "wine"): if use_case == "wine": requirement = requirements[use_case] else: + mi.use_tools = True assert DATA_DIR != "your/path/to/data", f"Please set DATA_DIR for the use_case: {use_case}!" requirement = requirements[use_case] await mi.run(requirement) From 2d8906091824263236db19ade7c1a9edfb5146af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Wed, 21 Feb 2024 23:03:36 +0800 Subject: [PATCH 011/131] chore. --- examples/mi/machine_learning.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/mi/machine_learning.py b/examples/mi/machine_learning.py index 652e7c908..9e4045bbb 100644 --- a/examples/mi/machine_learning.py +++ b/examples/mi/machine_learning.py @@ -23,17 +23,17 @@ The metric of the competition is weighted mean absolute error (WMAE) for test da - *print* key variables to get more information for next task step. """ -requirements = {"wine": WINE_REQ, "sales_forecast": SALES_FORECAST_REQ} +REQUIREMENTS = {"wine": WINE_REQ, "sales_forecast": SALES_FORECAST_REQ} async def main(auto_run: bool = True, use_case: str = "wine"): mi = Interpreter(auto_run=auto_run) if use_case == "wine": - requirement = requirements[use_case] + requirement = REQUIREMENTS[use_case] else: mi.use_tools = True assert DATA_DIR != "your/path/to/data", f"Please set DATA_DIR for the use_case: {use_case}!" - requirement = requirements[use_case] + requirement = REQUIREMENTS[use_case] await mi.run(requirement) From bd994adca2ec4b8199d75ea99365881f69ebb557 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Wed, 21 Feb 2024 23:07:42 +0800 Subject: [PATCH 012/131] chore. --- examples/mi/machine_learning.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/mi/machine_learning.py b/examples/mi/machine_learning.py index 9e4045bbb..43f1a1d3a 100644 --- a/examples/mi/machine_learning.py +++ b/examples/mi/machine_learning.py @@ -4,8 +4,7 @@ from metagpt.roles.mi.interpreter import Interpreter WINE_REQ = "Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy." -# DATA_DIR = "your/path/to/data" -DATA_DIR = "examples/mi/data/WalmartSalesForecast2" +DATA_DIR = "your/path/to/data" # sales_forecast data from https://www.kaggle.com/datasets/aslanahmedov/walmart-sales-forecast/data SALES_FORECAST_REQ = f""" # Goal From ec95cedb368c5014c2978d65966e1aaee7639669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Thu, 22 Feb 2024 21:07:32 +0800 Subject: [PATCH 013/131] update SALES_FORECAST_REQ. --- examples/mi/machine_learning.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/examples/mi/machine_learning.py b/examples/mi/machine_learning.py index 43f1a1d3a..d67a42712 100644 --- a/examples/mi/machine_learning.py +++ b/examples/mi/machine_learning.py @@ -6,20 +6,8 @@ WINE_REQ = "Run data analysis on sklearn Wine recognition dataset, include a plo DATA_DIR = "your/path/to/data" # sales_forecast data from https://www.kaggle.com/datasets/aslanahmedov/walmart-sales-forecast/data -SALES_FORECAST_REQ = f""" -# Goal -Train a model to predict sales for each department in every store (split the last 40 weeks records as validation dataset, -the others is train dataset), include plot sales trends, print metric and plot scatter plots of -groud truth and predictions on validation data. - -# Datasets Available -- train_data: {DATA_DIR}/train.csv - -# Metric -The metric of the competition is weighted mean absolute error (WMAE) for test data. - -# Notice -- *print* key variables to get more information for next task step. +SALES_FORECAST_REQ = f"""Train a model to predict sales for each department in every store (split the last 40 weeks records as validation dataset, the others is train dataset), include plot sales trends, print metric and plot scatter plots of +groud truth and predictions on validation data. Datasets Available is train_data: {DATA_DIR}/train.csv, The metric of the competition is weighted mean absolute error (WMAE) for test data. Notice: *print* key variables to get more information for next task step. """ REQUIREMENTS = {"wine": WINE_REQ, "sales_forecast": SALES_FORECAST_REQ} From 08e00b4dc2894dc233bbb2802009f9f070092b3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Thu, 22 Feb 2024 21:48:05 +0800 Subject: [PATCH 014/131] chore: simplify code. --- examples/mi/machine_learning.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/examples/mi/machine_learning.py b/examples/mi/machine_learning.py index d67a42712..53168e0e9 100644 --- a/examples/mi/machine_learning.py +++ b/examples/mi/machine_learning.py @@ -4,10 +4,10 @@ from metagpt.roles.mi.interpreter import Interpreter WINE_REQ = "Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy." -DATA_DIR = "your/path/to/data" +DATA_DIR = "examples/mi/data/WalmartSalesForecast2" # sales_forecast data from https://www.kaggle.com/datasets/aslanahmedov/walmart-sales-forecast/data -SALES_FORECAST_REQ = f"""Train a model to predict sales for each department in every store (split the last 40 weeks records as validation dataset, the others is train dataset), include plot sales trends, print metric and plot scatter plots of -groud truth and predictions on validation data. Datasets Available is train_data: {DATA_DIR}/train.csv, The metric of the competition is weighted mean absolute error (WMAE) for test data. Notice: *print* key variables to get more information for next task step. +SALES_FORECAST_REQ = f"""Train a model to predict sales for each department in every store (split the last 40 weeks records as validation dataset, the others is train dataset), include plot total sales trends, print metric and plot scatter plots of +groud truth and predictions on validation data. Dataset is {DATA_DIR}/train.csv, the metric is weighted mean absolute error (WMAE) for test data. Notice: *print* key variables to get more information for next task step. """ REQUIREMENTS = {"wine": WINE_REQ, "sales_forecast": SALES_FORECAST_REQ} @@ -15,12 +15,7 @@ REQUIREMENTS = {"wine": WINE_REQ, "sales_forecast": SALES_FORECAST_REQ} async def main(auto_run: bool = True, use_case: str = "wine"): mi = Interpreter(auto_run=auto_run) - if use_case == "wine": - requirement = REQUIREMENTS[use_case] - else: - mi.use_tools = True - assert DATA_DIR != "your/path/to/data", f"Please set DATA_DIR for the use_case: {use_case}!" - requirement = REQUIREMENTS[use_case] + requirement = REQUIREMENTS[use_case] await mi.run(requirement) From 041a2d61097bad45b5b7e28a880b37268a285c5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Thu, 22 Feb 2024 21:56:02 +0800 Subject: [PATCH 015/131] chore. --- examples/mi/machine_learning.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/mi/machine_learning.py b/examples/mi/machine_learning.py index 53168e0e9..56c68f69e 100644 --- a/examples/mi/machine_learning.py +++ b/examples/mi/machine_learning.py @@ -4,7 +4,7 @@ from metagpt.roles.mi.interpreter import Interpreter WINE_REQ = "Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy." -DATA_DIR = "examples/mi/data/WalmartSalesForecast2" +DATA_DIR = "path/to/your/data" # sales_forecast data from https://www.kaggle.com/datasets/aslanahmedov/walmart-sales-forecast/data SALES_FORECAST_REQ = f"""Train a model to predict sales for each department in every store (split the last 40 weeks records as validation dataset, the others is train dataset), include plot total sales trends, print metric and plot scatter plots of groud truth and predictions on validation data. Dataset is {DATA_DIR}/train.csv, the metric is weighted mean absolute error (WMAE) for test data. Notice: *print* key variables to get more information for next task step. From 791ab749adab3d527c6a700bfd89d855a2ba347a Mon Sep 17 00:00:00 2001 From: geekan Date: Wed, 21 Feb 2024 16:49:59 +0800 Subject: [PATCH 016/131] refine code to avoid config error --- metagpt/software_company.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/metagpt/software_company.py b/metagpt/software_company.py index 26bb29cd1..f290d497a 100644 --- a/metagpt/software_company.py +++ b/metagpt/software_company.py @@ -2,14 +2,11 @@ # -*- coding: utf-8 -*- import asyncio -import shutil from pathlib import Path import typer -from metagpt.config2 import config -from metagpt.const import CONFIG_ROOT, METAGPT_ROOT -from metagpt.context import Context +from metagpt.const import CONFIG_ROOT from metagpt.utils.project_repo import ProjectRepo app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False) @@ -30,6 +27,8 @@ def generate_repo( recover_path=None, ) -> ProjectRepo: """Run the startup logic. Can be called from CLI or other Python scripts.""" + from metagpt.config2 import config + from metagpt.context import Context from metagpt.roles import ( Architect, Engineer, @@ -122,7 +121,17 @@ def startup( ) -def copy_config_to(config_path=METAGPT_ROOT / "config" / "config2.yaml"): +DEFAULT_CONFIG = """# Full Example: https://github.com/geekan/MetaGPT/blob/main/config/config2.example.yaml +# Reflected Code: https://github.com/geekan/MetaGPT/blob/main/metagpt/config2.py +llm: + api_type: "openai" # or azure / ollama / open_llm etc. Check LLMType for more options + model: "gpt-4-turbo-preview" # or gpt-3.5-turbo-1106 / gpt-4-1106-preview + base_url: "https://api.openai.com/v1" # or forward url / other llm url + api_key: "YOUR_API_KEY" +""" + + +def copy_config_to(): """Initialize the configuration file for MetaGPT.""" target_path = CONFIG_ROOT / "config2.yaml" @@ -136,7 +145,7 @@ def copy_config_to(config_path=METAGPT_ROOT / "config" / "config2.yaml"): print(f"Existing configuration file backed up at {backup_path}") # 复制文件 - shutil.copy(str(config_path), target_path) + target_path.write_text(DEFAULT_CONFIG, encoding="utf-8") print(f"Configuration file initialized at {target_path}") From 0a6dc8f7e7d888f7556a0c009c422d61933c0406 Mon Sep 17 00:00:00 2001 From: shenchucheng Date: Fri, 23 Feb 2024 10:15:49 +0800 Subject: [PATCH 017/131] update version to 0.7.3 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ed3d5f78f..f5d880ac9 100644 --- a/setup.py +++ b/setup.py @@ -57,7 +57,7 @@ extras_require["dev"] = (["pylint~=3.0.3", "black~=23.3.0", "isort~=5.12.0", "pr setup( name="metagpt", - version="0.7.2", + version="0.7.3", description="The Multi-Agent Framework", long_description=long_description, long_description_content_type="text/markdown", From 219d361ca6d377f749631cca9794133984e98c19 Mon Sep 17 00:00:00 2001 From: yzlin Date: Mon, 26 Feb 2024 21:10:16 +0800 Subject: [PATCH 018/131] rm repetitive tool config for writing code; rm WriteCodeWithoutTools --- metagpt/actions/__init__.py | 3 +- metagpt/actions/mi/debug_code.py | 4 +- metagpt/actions/mi/ml_action.py | 41 ++++++--------- metagpt/actions/mi/write_analysis_code.py | 62 +++++++---------------- metagpt/actions/mi/write_plan.py | 36 +++---------- metagpt/prompts/mi/ml_action.py | 46 ++++++----------- metagpt/prompts/mi/write_analysis_code.py | 41 --------------- metagpt/provider/base_llm.py | 5 +- metagpt/provider/openai_api.py | 25 ++++----- metagpt/roles/mi/interpreter.py | 7 +-- metagpt/strategy/planner.py | 3 +- 11 files changed, 77 insertions(+), 196 deletions(-) diff --git a/metagpt/actions/__init__.py b/metagpt/actions/__init__.py index 19a7c10d5..31de8c04a 100644 --- a/metagpt/actions/__init__.py +++ b/metagpt/actions/__init__.py @@ -23,7 +23,7 @@ from metagpt.actions.write_prd import WritePRD from metagpt.actions.write_prd_review import WritePRDReview from metagpt.actions.write_test import WriteTest from metagpt.actions.mi.execute_nb_code import ExecuteNbCode -from metagpt.actions.mi.write_analysis_code import WriteCodeWithoutTools, WriteCodeWithTools +from metagpt.actions.mi.write_analysis_code import WriteCodeWithTools from metagpt.actions.mi.write_plan import WritePlan @@ -46,7 +46,6 @@ class ActionType(Enum): WEB_BROWSE_AND_SUMMARIZE = WebBrowseAndSummarize CONDUCT_RESEARCH = ConductResearch EXECUTE_NB_CODE = ExecuteNbCode - WRITE_CODE_WITHOUT_TOOLS = WriteCodeWithoutTools WRITE_CODE_WITH_TOOLS = WriteCodeWithTools WRITE_PLAN = WritePlan diff --git a/metagpt/actions/mi/debug_code.py b/metagpt/actions/mi/debug_code.py index db3b7a9db..93446bc70 100644 --- a/metagpt/actions/mi/debug_code.py +++ b/metagpt/actions/mi/debug_code.py @@ -1,6 +1,6 @@ from __future__ import annotations -from metagpt.actions.mi.write_analysis_code import BaseWriteAnalysisCode +from metagpt.actions import Action from metagpt.logs import logger from metagpt.schema import Message from metagpt.utils.common import create_func_call_config @@ -72,7 +72,7 @@ CODE_REFLECTION = { } -class DebugCode(BaseWriteAnalysisCode): +class DebugCode(Action): async def run( self, context: list[Message] = None, diff --git a/metagpt/actions/mi/ml_action.py b/metagpt/actions/mi/ml_action.py index 60b2fb547..1bed6c95b 100644 --- a/metagpt/actions/mi/ml_action.py +++ b/metagpt/actions/mi/ml_action.py @@ -5,14 +5,13 @@ from typing import Tuple from metagpt.actions import Action from metagpt.actions.mi.write_analysis_code import WriteCodeWithTools from metagpt.prompts.mi.ml_action import ( - ML_GENERATE_CODE_PROMPT, - ML_TOOL_USAGE_PROMPT, - PRINT_DATA_COLUMNS, + ML_PROMPT, UPDATE_DATA_COLUMNS, + USE_NO_TOOLS_EXAMPLE, + USE_TOOLS_EXAMPLE, ) -from metagpt.prompts.mi.write_analysis_code import CODE_GENERATOR_WITH_TOOLS from metagpt.schema import Message, Plan -from metagpt.utils.common import create_func_call_config, remove_comments +from metagpt.utils.common import remove_comments class WriteCodeWithToolsML(WriteCodeWithTools): @@ -32,26 +31,17 @@ class WriteCodeWithToolsML(WriteCodeWithTools): code_context = "\n\n".join(code_context) # prepare prompt depending on tool availability & LLM call - if tool_schemas: - prompt = ML_TOOL_USAGE_PROMPT.format( - user_requirement=plan.goal, - history_code=code_context, - current_task=plan.current_task.instruction, - column_info=column_info, - tool_type_usage_prompt=tool_type_usage_prompt, - tool_schemas=tool_schemas, - ) + prompt = ML_PROMPT.format( + user_requirement=plan.goal, + history_code=code_context, + current_task=plan.current_task.instruction, + column_info=column_info, + tool_type_usage_prompt=tool_type_usage_prompt, + tool_schemas=tool_schemas, + examples=USE_TOOLS_EXAMPLE if tool_schemas else USE_NO_TOOLS_EXAMPLE, + ) - else: - prompt = ML_GENERATE_CODE_PROMPT.format( - user_requirement=plan.goal, - history_code=code_context, - current_task=plan.current_task.instruction, - column_info=column_info, - tool_type_usage_prompt=tool_type_usage_prompt, - ) - tool_config = create_func_call_config(CODE_GENERATOR_WITH_TOOLS) - rsp = await self.llm.aask_code(prompt, **tool_config) + rsp = await self.llm.aask_code(prompt, language="python") # Extra output to be used for potential debugging context = [Message(content=prompt, role="user")] @@ -65,6 +55,5 @@ class UpdateDataColumns(Action): code_context = [remove_comments(task.code) for task in finished_tasks] code_context = "\n\n".join(code_context) prompt = UPDATE_DATA_COLUMNS.format(history_code=code_context) - tool_config = create_func_call_config(PRINT_DATA_COLUMNS) - rsp = await self.llm.aask_code(prompt, **tool_config) + rsp = await self.llm.aask_code(prompt, language="python") return rsp diff --git a/metagpt/actions/mi/write_analysis_code.py b/metagpt/actions/mi/write_analysis_code.py index b3d0632b6..33e55336f 100644 --- a/metagpt/actions/mi/write_analysis_code.py +++ b/metagpt/actions/mi/write_analysis_code.py @@ -11,7 +11,6 @@ from typing import Tuple from metagpt.actions import Action from metagpt.logs import logger from metagpt.prompts.mi.write_analysis_code import ( - CODE_GENERATOR_WITH_TOOLS, SELECT_FUNCTION_TOOLS, TOOL_RECOMMENDATION_PROMPT, TOOL_USAGE_PROMPT, @@ -22,43 +21,19 @@ from metagpt.tools.tool_registry import validate_tool_names from metagpt.utils.common import create_func_call_config -class BaseWriteAnalysisCode(Action): - DEFAULT_SYSTEM_MSG: str = """You are Code Interpreter, a world-class programmer that can complete any goal by executing code. Strictly follow the plan and generate code step by step. Each step of the code will be executed on the user's machine, and the user will provide the code execution results to you.**Notice: The code for the next step depends on the code for the previous step. Must reuse variables in the lastest other code directly, dont creat it again, it is very import for you. Use !pip install in a standalone block to install missing packages.Usually the libraries you need are already installed.Dont check if packages already imported.**""" # prompt reference: https://github.com/KillianLucas/open-interpreter/blob/v0.1.4/interpreter/system_message.txt - # REUSE_CODE_INSTRUCTION = """ATTENTION: DONT include codes from previous tasks in your current code block, include new codes only, DONT repeat codes!""" +class WriteCodeWithTools(Action): + """Write code with help of local available tools. Choose tools first, then generate code to use the tools""" - def insert_system_message(self, context: list[Message], system_msg: str = None): + use_tools: bool = True + # selected tools to choose from, listed by their names. An empty list means selection from all tools. + selected_tools: list[str] = [] + DEFAULT_SYSTEM_MSG: str = """You are Code Interpreter, a world-class programmer that can complete any goal by executing code. Strictly follow the plan and generate code step by step. Each step of the code will be executed on the user's machine, and the user will provide the code execution results to you.**Notice: The code for the next step depends on the code for the previous step. Must reuse variables in the lastest other code directly, dont creat it again, it is very import for you. Use !pip install in a standalone block to install missing packages.Usually the libraries you need are already installed.Dont check if packages already imported.**""" # prompt reference: https://github.com/KillianLucas/open-interpreter/blob/v0.1.4/interpreter/system_message.txt + + def _insert_system_message(self, context: list[Message], system_msg: str = None): system_msg = system_msg or self.DEFAULT_SYSTEM_MSG context.insert(0, SystemMessage(content=system_msg)) if context[0].role != "system" else None return context - async def run(self, context: list[Message], plan: Plan = None) -> dict: - """Run of a code writing action, used in data analysis or modeling - - Args: - context (list[Message]): Action output history, source action denoted by Message.cause_by - plan (Plan, optional): Overall plan. Defaults to None. - - Returns: - dict: code result in the format of {"code": "print('hello world')", "language": "python"} - """ - raise NotImplementedError - - -class WriteCodeWithoutTools(BaseWriteAnalysisCode): - """Ask LLM to generate codes purely by itself without local user-defined tools""" - - async def run(self, context: list[Message], plan: Plan = None, system_msg: str = None, **kwargs) -> dict: - messages = self.insert_system_message(context, system_msg) - rsp = await self.llm.aask_code(messages, **kwargs) - return rsp - - -class WriteCodeWithTools(BaseWriteAnalysisCode): - """Write code with help of local available tools. Choose tools first, then generate code to use the tools""" - - # selected tools to choose from, listed by their names. An empty list means selection from all tools. - selected_tools: list[str] = [] - def _get_tools_by_type(self, tool_type: str) -> dict: """ Retreive tools by tool type from registry, but filtered by pre-selected tool list @@ -138,18 +113,19 @@ class WriteCodeWithTools(BaseWriteAnalysisCode): plan: Plan, **kwargs, ) -> str: - # prepare tool schemas and tool-type-specific instruction - tool_schemas, tool_type_usage_prompt = await self._prepare_tools(plan=plan) + if self.use_tools: + # prepare tool schemas and tool-type-specific instruction + tool_schemas, tool_type_usage_prompt = await self._prepare_tools(plan=plan) - # form a complete tool usage instruction and include it as a message in context - tools_instruction = TOOL_USAGE_PROMPT.format( - tool_schemas=tool_schemas, tool_type_usage_prompt=tool_type_usage_prompt - ) - context.append(Message(content=tools_instruction, role="user")) + # form a complete tool usage instruction and include it as a message in context + tools_instruction = TOOL_USAGE_PROMPT.format( + tool_schemas=tool_schemas, tool_type_usage_prompt=tool_type_usage_prompt + ) + context.append(Message(content=tools_instruction, role="user")) # prepare prompt & LLM call - prompt = self.insert_system_message(context) - tool_config = create_func_call_config(CODE_GENERATOR_WITH_TOOLS) - rsp = await self.llm.aask_code(prompt, **tool_config) + prompt = self._insert_system_message(context) + + rsp = await self.llm.aask_code(prompt, language="python") return rsp diff --git a/metagpt/actions/mi/write_plan.py b/metagpt/actions/mi/write_plan.py index 8067d7b87..c20773674 100644 --- a/metagpt/actions/mi/write_plan.py +++ b/metagpt/actions/mi/write_plan.py @@ -12,19 +12,17 @@ from typing import Tuple from metagpt.actions import Action from metagpt.logs import logger -from metagpt.prompts.mi.write_analysis_code import ( - ASSIGN_TASK_TYPE_CONFIG, - ASSIGN_TASK_TYPE_PROMPT, -) from metagpt.schema import Message, Plan, Task from metagpt.tools import TOOL_REGISTRY -from metagpt.utils.common import CodeParser, create_func_call_config +from metagpt.utils.common import CodeParser class WritePlan(Action): PROMPT_TEMPLATE: str = """ # Context: __context__ + # Available Task Types: + __task_type_desc__ # Task: Based on the context, write a plan or modify an existing plan of what you should do to achieve the goal. A plan consists of one to __max_tasks__ tasks. If you are modifying an existing plan, carefully follow the instruction, don't make unnecessary changes. Give the whole plan unless instructed to modify only one task of the plan. @@ -36,46 +34,24 @@ class WritePlan(Action): "task_id": str = "unique identifier for a task in plan, can be an ordinal", "dependent_task_ids": list[str] = "ids of tasks prerequisite to this task", "instruction": "what you should do in this task, one short phrase or sentence", + "task_type": "type of this task, should be one of Available Task Types", }, ... ] ``` """ - async def assign_task_type(self, tasks: list[dict]) -> str: - """Assign task type to each task in tasks - - Args: - tasks (list[dict]): tasks to be assigned task type - - Returns: - str: tasks with task type assigned in a json string - """ - task_info = "\n".join([f"Task {task['task_id']}: {task['instruction']}" for task in tasks]) + async def run(self, context: list[Message], max_tasks: int = 5, use_tools: bool = False) -> str: task_type_desc = "\n".join( [f"- **{tool_type.name}**: {tool_type.desc}" for tool_type in TOOL_REGISTRY.get_tool_types().values()] ) # task type are binded with tool type now, should be improved in the future - prompt = ASSIGN_TASK_TYPE_PROMPT.format( - task_info=task_info, task_type_desc=task_type_desc - ) # task types are set to be the same as tool types, for now - tool_config = create_func_call_config(ASSIGN_TASK_TYPE_CONFIG) - rsp = await self.llm.aask_code(prompt, **tool_config) - task_type_list = rsp["task_type"] - logger.info(f"assigned task types: {task_type_list}") - for task, task_type in zip(tasks, task_type_list): - task["task_type"] = task_type - return json.dumps(tasks) - - async def run(self, context: list[Message], max_tasks: int = 5, use_tools: bool = False) -> str: prompt = ( self.PROMPT_TEMPLATE.replace("__context__", "\n".join([str(ct) for ct in context])) # .replace("__current_plan__", current_plan) - .replace("__max_tasks__", str(max_tasks)) + .replace("__max_tasks__", str(max_tasks)).replace("__task_type_desc__", task_type_desc) ) rsp = await self._aask(prompt) rsp = CodeParser.parse_code(block=None, text=rsp) - if use_tools: - rsp = await self.assign_task_type(json.loads(rsp)) return rsp diff --git a/metagpt/prompts/mi/ml_action.py b/metagpt/prompts/mi/ml_action.py index 5d27c7ff0..ed7ea59e4 100644 --- a/metagpt/prompts/mi/ml_action.py +++ b/metagpt/prompts/mi/ml_action.py @@ -7,13 +7,13 @@ UPDATE_DATA_COLUMNS = """ # Background Keep dataset column information updated before model train. -## Done Tasks +## Tasks Done ```python {history_code} ```end # Task -Update and print the dataset's column information only if the train or test data has changed. Use the following code: +Print the the latest column information after 'Tasks Done' code. Use the following code: ```python from metagpt.tools.libs.data_preprocess import get_column_info @@ -23,26 +23,11 @@ print(column_info) ```end # Constraints: -- Use the DataFrame variable from 'Done Tasks' in place of df. -- Import `get_column_info` only if it's not already imported. +- Use the DataFrame variable from 'Tasks Done' in place of df. +- Your code is to be added to a new cell in jupyter. """ -PRINT_DATA_COLUMNS = { - "name": "print_column_info", - "description": "Print the latest column information after 'Done Tasks' code if first read or data changed.", - "parameters": { - "type": "object", - "properties": { - "code": { - "type": "string", - "description": "The code to be added to a new cell in jupyter.", - }, - }, - "required": ["code"], - }, -} - -ML_COMMON_PROMPT = """ +ML_PROMPT = """ # Background As a data scientist, you need to help user to achieve their goal [{user_requirement}] step-by-step in an continuous Jupyter notebook. @@ -61,6 +46,16 @@ Latest data info after previous tasks: # Task Write complete code for 'Current Task'. And avoid duplicating code from 'Done Tasks', such as repeated import of packages, reading data, etc. Specifically, {tool_type_usage_prompt} + +# Capabilities +- You can utilize pre-defined tools in any code lines from 'Available Tools' in the form of Python Class. +- You can freely combine the use of any other public packages, like sklearn, numpy, pandas, etc.. + +# Available Tools: +Each Class tool is described in JSON format. When you call a tool, import the tool from its path first. +{tool_schemas} + +{examples} """ USE_NO_TOOLS_EXAMPLE = """ @@ -86,14 +81,6 @@ model.fit(train, y_train) """ USE_TOOLS_EXAMPLE = """ -# Capabilities -- You can utilize pre-defined tools in any code lines from 'Available Tools' in the form of Python Class. -- You can freely combine the use of any other public packages, like sklearn, numpy, pandas, etc.. - -# Available Tools: -Each Class tool is described in JSON format. When you call a tool, import the tool from its path first. -{tool_schemas} - # Output Example: when current task is "do data preprocess, like fill missing value, handle outliers, etc.", the code can be like: ```python @@ -123,6 +110,3 @@ for col in num_cols: - Always prioritize using pre-defined tools for the same functionality. - Always copy the DataFrame before processing it and use the copy to process. """ - -ML_GENERATE_CODE_PROMPT = ML_COMMON_PROMPT + USE_NO_TOOLS_EXAMPLE -ML_TOOL_USAGE_PROMPT = ML_COMMON_PROMPT + USE_TOOLS_EXAMPLE diff --git a/metagpt/prompts/mi/write_analysis_code.py b/metagpt/prompts/mi/write_analysis_code.py index 4eccefcd1..3ad7725d4 100644 --- a/metagpt/prompts/mi/write_analysis_code.py +++ b/metagpt/prompts/mi/write_analysis_code.py @@ -1,29 +1,3 @@ -ASSIGN_TASK_TYPE_PROMPT = """ -Please assign a task type to each task in the list below from the given categories: -{task_info} - -## All Task Type: -{task_type_desc} -""" - -ASSIGN_TASK_TYPE_CONFIG = { - "name": "assign_task_type", - "description": "Assign task type to each task by order.", - "parameters": { - "type": "object", - "properties": { - "task_type": { - "type": "array", - "description": "List of task type. The length should as long as task list", - "items": { - "type": "string", - }, - }, - }, - "required": ["task_type"], - }, -} - TOOL_RECOMMENDATION_PROMPT = """ ## User Requirement: {current_task} @@ -59,21 +33,6 @@ SELECT_FUNCTION_TOOLS = { }, } -CODE_GENERATOR_WITH_TOOLS = { - "name": "add_subtask_code", - "description": "Add new code cell of current task to the end of an active Jupyter notebook.", - "parameters": { - "type": "object", - "properties": { - "code": { - "type": "string", - "description": "The code to be added to a new cell in jupyter.", - }, - }, - "required": ["code"], - }, -} - TOOL_USAGE_PROMPT = """ # Instruction Write complete code for 'Current Task'. And avoid duplicating code from finished tasks, such as repeated import of packages, reading data, etc. diff --git a/metagpt/provider/base_llm.py b/metagpt/provider/base_llm.py index b144471b5..2137bb2dc 100644 --- a/metagpt/provider/base_llm.py +++ b/metagpt/provider/base_llm.py @@ -102,8 +102,9 @@ class BaseLLM(ABC): context.append(self._assistant_msg(rsp_text)) return self._extract_assistant_rsp(context) - async def aask_code(self, messages: Union[str, Message, list[dict]], timeout=3) -> dict: - """FIXME: No code segment filtering has been done here, and all results are actually displayed""" + async def aask_code( + self, messages: Union[str, Message, list[dict]], timeout=3, language: str = "", **kwargs + ) -> dict: raise NotImplementedError @abstractmethod diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py index 28abed752..de6355815 100644 --- a/metagpt/provider/openai_api.py +++ b/metagpt/provider/openai_api.py @@ -9,6 +9,7 @@ import json import re +from copy import deepcopy from typing import AsyncIterator, Optional, Union from openai import APIConnectionError, AsyncOpenAI, AsyncStream @@ -145,14 +146,6 @@ class OpenAILLM(BaseLLM): rsp = await self._achat_completion(messages, timeout=timeout) return self.get_choice_text(rsp) - 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}]} - kwargs.update(configs) - - 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 @@ -175,14 +168,16 @@ class OpenAILLM(BaseLLM): ) return processed_messages - async def _achat_completion_function(self, messages: list[dict], timeout=3, **chat_configs) -> ChatCompletion: + async def _achat_completion_function( + self, messages: list[dict], timeout: int = 3, **chat_configs + ) -> ChatCompletion: messages = self._process_message(messages) - kwargs = self._func_configs(messages=messages, timeout=timeout, **chat_configs) + kwargs = self._cons_kwargs(messages=messages, timeout=timeout, **chat_configs) rsp: ChatCompletion = await self.aclient.chat.completions.create(**kwargs) self._update_costs(rsp.usage) return rsp - async def aask_code(self, messages: list[dict], **kwargs) -> dict: + async def aask_code(self, messages: list[dict], timeout: int = 3, language: str = "", **kwargs) -> dict: """Use function of tools to ask a code. Note: Keep kwargs consistent with https://platform.openai.com/docs/api-reference/chat/create @@ -192,12 +187,18 @@ class OpenAILLM(BaseLLM): >>> rsp = await llm.aask_code(msg) # -> {'language': 'python', 'code': "print('Hello, World!')"} """ + if "tools" not in kwargs: + function_schema = deepcopy(GENERAL_FUNCTION_SCHEMA) + if language: + function_schema["parameters"]["properties"]["language"]["enum"] = [language] + configs = {"tools": [{"type": "function", "function": function_schema}]} + kwargs.update(configs) 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: + if "language" 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} diff --git a/metagpt/roles/mi/interpreter.py b/metagpt/roles/mi/interpreter.py index fa50098e9..d5607bfe3 100644 --- a/metagpt/roles/mi/interpreter.py +++ b/metagpt/roles/mi/interpreter.py @@ -4,10 +4,7 @@ from pydantic import Field from metagpt.actions.mi.ask_review import ReviewConst from metagpt.actions.mi.execute_nb_code import ExecuteNbCode -from metagpt.actions.mi.write_analysis_code import ( - WriteCodeWithoutTools, - WriteCodeWithTools, -) +from metagpt.actions.mi.write_analysis_code import WriteCodeWithTools from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message, Task, TaskResult @@ -75,7 +72,7 @@ class Interpreter(Role): return code["code"], result, success async def _write_code(self): - todo = WriteCodeWithoutTools() if not self.use_tools else WriteCodeWithTools(selected_tools=self.tools) + todo = WriteCodeWithTools(use_tools=self.use_tools, selected_tools=self.tools) logger.info(f"ready to {todo.name}") context = self.planner.get_useful_memories() diff --git a/metagpt/strategy/planner.py b/metagpt/strategy/planner.py index 99d16f78b..a0f73f70c 100644 --- a/metagpt/strategy/planner.py +++ b/metagpt/strategy/planner.py @@ -32,7 +32,6 @@ class Planner(BaseModel): default_factory=Memory ) # memory for working on each task, discarded each time a task is done auto_run: bool = False - use_tools: bool = False def __init__(self, goal: str = "", plan: Plan = None, **kwargs): plan = plan or Plan(goal=goal) @@ -53,7 +52,7 @@ class Planner(BaseModel): plan_confirmed = False while not plan_confirmed: context = self.get_useful_memories() - rsp = await WritePlan().run(context, max_tasks=max_tasks, use_tools=self.use_tools) + rsp = await WritePlan().run(context, max_tasks=max_tasks) self.working_memory.add(Message(content=rsp, role="assistant", cause_by=WritePlan)) # precheck plan before asking reviews From a2b85641c327406b8efc7d5f0636fe9faa7d0c95 Mon Sep 17 00:00:00 2001 From: yzlin Date: Tue, 27 Feb 2024 17:59:50 +0800 Subject: [PATCH 019/131] rm unnecessary function call --- metagpt/actions/mi/debug_code.py | 42 +++++++---------------- metagpt/actions/mi/ml_action.py | 4 +-- metagpt/actions/mi/write_analysis_code.py | 12 +++---- metagpt/actions/mi/write_plan.py | 16 ++++----- metagpt/prompts/mi/write_analysis_code.py | 21 +++--------- metagpt/provider/constant.py | 17 +++++++++ metagpt/provider/openai_api.py | 11 +++--- metagpt/roles/mi/ml_engineer.py | 1 - metagpt/utils/common.py | 10 ------ 9 files changed, 54 insertions(+), 80 deletions(-) diff --git a/metagpt/actions/mi/debug_code.py b/metagpt/actions/mi/debug_code.py index 93446bc70..8259d3de1 100644 --- a/metagpt/actions/mi/debug_code.py +++ b/metagpt/actions/mi/debug_code.py @@ -1,9 +1,10 @@ from __future__ import annotations +import json + from metagpt.actions import Action -from metagpt.logs import logger from metagpt.schema import Message -from metagpt.utils.common import create_func_call_config +from metagpt.utils.common import CodeParser DEBUG_REFLECTION_EXAMPLE = ''' Example 1: @@ -48,28 +49,15 @@ Here is an example for you. {runtime_result} Analysis the error step by step, provide me improve method and code. Remember to follow [context] requirement. Don't forget write code for steps behind the error step. -[reflection on previous impl]: -xxx -""" -CODE_REFLECTION = { - "name": "execute_reflection_code", - "description": "Execute reflection code.", - "parameters": { - "type": "object", - "properties": { - "reflection": { - "type": "string", - "description": "Reflection on previous impl.", - }, - "improved_impl": { - "type": "string", - "description": "Refined code after reflection.", - }, - }, - "required": ["reflection", "improved_impl"], - }, -} +Output a json following the format: +```json +{{ + "reflection": str = "Reflection on previous implementation", + "improved_impl": str = "Refined code after reflection.", +}} +``` +""" class DebugCode(Action): @@ -91,7 +79,6 @@ class DebugCode(Action): str: The improved implementation based on the debugging process. """ - info = [] reflection_prompt = REFLECTION_PROMPT.format( debug_example=DEBUG_REFLECTION_EXAMPLE, context=context, @@ -99,11 +86,8 @@ class DebugCode(Action): runtime_result=runtime_result, ) system_prompt = "You are an AI Python assistant. You will be given your previous implementation code of a task, runtime error results, and a hint to change the implementation appropriately. Write your full implementation " - info.append(Message(role="system", content=system_prompt)) - info.append(Message(role="user", content=reflection_prompt)) - tool_config = create_func_call_config(CODE_REFLECTION) - reflection = await self.llm.aask_code(messages=info, **tool_config) - logger.info(f"reflection is {reflection}") + rsp = await self._aask(reflection_prompt, system_msgs=[system_prompt]) + reflection = json.loads(CodeParser.parse_code(block=None, text=rsp)) return {"code": reflection["improved_impl"]} diff --git a/metagpt/actions/mi/ml_action.py b/metagpt/actions/mi/ml_action.py index 1bed6c95b..3577c76e7 100644 --- a/metagpt/actions/mi/ml_action.py +++ b/metagpt/actions/mi/ml_action.py @@ -41,7 +41,7 @@ class WriteCodeWithToolsML(WriteCodeWithTools): examples=USE_TOOLS_EXAMPLE if tool_schemas else USE_NO_TOOLS_EXAMPLE, ) - rsp = await self.llm.aask_code(prompt, language="python") + rsp = await self.llm.aask_code(prompt) # Extra output to be used for potential debugging context = [Message(content=prompt, role="user")] @@ -55,5 +55,5 @@ class UpdateDataColumns(Action): code_context = [remove_comments(task.code) for task in finished_tasks] code_context = "\n\n".join(code_context) prompt = UPDATE_DATA_COLUMNS.format(history_code=code_context) - rsp = await self.llm.aask_code(prompt, language="python") + rsp = await self.llm.aask_code(prompt) return rsp diff --git a/metagpt/actions/mi/write_analysis_code.py b/metagpt/actions/mi/write_analysis_code.py index 33e55336f..9337487c2 100644 --- a/metagpt/actions/mi/write_analysis_code.py +++ b/metagpt/actions/mi/write_analysis_code.py @@ -6,19 +6,19 @@ """ from __future__ import annotations +import json from typing import Tuple from metagpt.actions import Action from metagpt.logs import logger from metagpt.prompts.mi.write_analysis_code import ( - SELECT_FUNCTION_TOOLS, TOOL_RECOMMENDATION_PROMPT, TOOL_USAGE_PROMPT, ) from metagpt.schema import Message, Plan, SystemMessage from metagpt.tools import TOOL_REGISTRY from metagpt.tools.tool_registry import validate_tool_names -from metagpt.utils.common import create_func_call_config +from metagpt.utils.common import CodeParser class WriteCodeWithTools(Action): @@ -69,9 +69,9 @@ class WriteCodeWithTools(Action): current_task=task, available_tools=available_tools, ) - tool_config = create_func_call_config(SELECT_FUNCTION_TOOLS) - rsp = await self.llm.aask_code(prompt, **tool_config) - recommend_tools = rsp["recommend_tools"] + rsp = await self._aask(prompt) + rsp = CodeParser.parse_code(block=None, text=rsp) + recommend_tools = json.loads(rsp) logger.info(f"Recommended tools: \n{recommend_tools}") # Parses and validates the recommended tools, for LLM might hallucinate and recommend non-existing tools @@ -126,6 +126,6 @@ class WriteCodeWithTools(Action): # prepare prompt & LLM call prompt = self._insert_system_message(context) - rsp = await self.llm.aask_code(prompt, language="python") + rsp = await self.llm.aask_code(prompt) return rsp diff --git a/metagpt/actions/mi/write_plan.py b/metagpt/actions/mi/write_plan.py index c20773674..9e468c30f 100644 --- a/metagpt/actions/mi/write_plan.py +++ b/metagpt/actions/mi/write_plan.py @@ -20,22 +20,22 @@ from metagpt.utils.common import CodeParser class WritePlan(Action): PROMPT_TEMPLATE: str = """ # Context: - __context__ + {context} # Available Task Types: - __task_type_desc__ + {task_type_desc} # Task: - Based on the context, write a plan or modify an existing plan of what you should do to achieve the goal. A plan consists of one to __max_tasks__ tasks. + Based on the context, write a plan or modify an existing plan of what you should do to achieve the goal. A plan consists of one to {max_tasks} tasks. If you are modifying an existing plan, carefully follow the instruction, don't make unnecessary changes. Give the whole plan unless instructed to modify only one task of the plan. If you encounter errors on the current task, revise and output the current single task only. Output a list of jsons following the format: ```json [ - { + {{ "task_id": str = "unique identifier for a task in plan, can be an ordinal", "dependent_task_ids": list[str] = "ids of tasks prerequisite to this task", "instruction": "what you should do in this task, one short phrase or sentence", "task_type": "type of this task, should be one of Available Task Types", - }, + }}, ... ] ``` @@ -45,10 +45,8 @@ class WritePlan(Action): task_type_desc = "\n".join( [f"- **{tool_type.name}**: {tool_type.desc}" for tool_type in TOOL_REGISTRY.get_tool_types().values()] ) # task type are binded with tool type now, should be improved in the future - prompt = ( - self.PROMPT_TEMPLATE.replace("__context__", "\n".join([str(ct) for ct in context])) - # .replace("__current_plan__", current_plan) - .replace("__max_tasks__", str(max_tasks)).replace("__task_type_desc__", task_type_desc) + prompt = self.PROMPT_TEMPLATE.format( + context="\n".join([str(ct) for ct in context]), max_tasks=max_tasks, task_type_desc=task_type_desc ) rsp = await self._aask(prompt) rsp = CodeParser.parse_code(block=None, text=rsp) diff --git a/metagpt/prompts/mi/write_analysis_code.py b/metagpt/prompts/mi/write_analysis_code.py index 3ad7725d4..6cae15e24 100644 --- a/metagpt/prompts/mi/write_analysis_code.py +++ b/metagpt/prompts/mi/write_analysis_code.py @@ -13,25 +13,12 @@ Recommend up to five tools from 'Available Tools' that can help solve the 'User - If you believe that no tools are suitable, indicate with an empty list. - Only list the names of the tools, not the full schema of each tool. - Ensure selected tools are listed in 'Available Tools'. +- Output a json list of tool names: +```json +["tool_name1", "tool_name2", ...] +``` """ -SELECT_FUNCTION_TOOLS = { - "name": "select_function_tools", - "description": "For current task, select suitable tools for it.", - "parameters": { - "type": "object", - "properties": { - "recommend_tools": { - "type": "array", - "description": "List of tool names. Empty list if no tool is suitable.", - "items": { - "type": "string", - }, - }, - }, - "required": ["recommend_tools"], - }, -} TOOL_USAGE_PROMPT = """ # Instruction diff --git a/metagpt/provider/constant.py b/metagpt/provider/constant.py index db67847a8..cfece6a79 100644 --- a/metagpt/provider/constant.py +++ b/metagpt/provider/constant.py @@ -25,6 +25,23 @@ GENERAL_FUNCTION_SCHEMA = { }, } + +CODE_ONLY_FUNCTION_SCHEMA = { + "name": "add_new_code", + "description": "Add new code cell of current task to the end of an active Jupyter notebook.", + "parameters": { + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "The code to be added to a new cell in jupyter.", + }, + }, + "required": ["code"], + }, +} + + # tool_choice value for general_function_schema # https://platform.openai.com/docs/api-reference/chat/create#chat-create-tool_choice GENERAL_TOOL_CHOICE = {"type": "function", "function": {"name": "execute"}} diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py index de6355815..f1d53c75a 100644 --- a/metagpt/provider/openai_api.py +++ b/metagpt/provider/openai_api.py @@ -9,7 +9,6 @@ import json import re -from copy import deepcopy from typing import AsyncIterator, Optional, Union from openai import APIConnectionError, AsyncOpenAI, AsyncStream @@ -27,7 +26,7 @@ from tenacity import ( from metagpt.configs.llm_config import LLMConfig, LLMType from metagpt.logs import log_llm_stream, logger from metagpt.provider.base_llm import BaseLLM -from metagpt.provider.constant import GENERAL_FUNCTION_SCHEMA +from metagpt.provider.constant import CODE_ONLY_FUNCTION_SCHEMA, GENERAL_FUNCTION_SCHEMA from metagpt.provider.llm_provider_registry import register_provider from metagpt.schema import Message from metagpt.utils.common import CodeParser, decode_image @@ -177,7 +176,7 @@ class OpenAILLM(BaseLLM): self._update_costs(rsp.usage) return rsp - async def aask_code(self, messages: list[dict], timeout: int = 3, language: str = "", **kwargs) -> dict: + async def aask_code(self, messages: list[dict], timeout: int = 3, include_language: bool = False, **kwargs) -> dict: """Use function of tools to ask a code. Note: Keep kwargs consistent with https://platform.openai.com/docs/api-reference/chat/create @@ -185,12 +184,12 @@ class OpenAILLM(BaseLLM): >>> llm = OpenAILLM() >>> msg = [{'role': 'user', 'content': "Write a python hello world code."}] >>> rsp = await llm.aask_code(msg) + # -> {'code': "print('Hello, World!')"} + >>> rsp = await llm.aask_code(msg, include_language=True) # -> {'language': 'python', 'code': "print('Hello, World!')"} """ if "tools" not in kwargs: - function_schema = deepcopy(GENERAL_FUNCTION_SCHEMA) - if language: - function_schema["parameters"]["properties"]["language"]["enum"] = [language] + function_schema = GENERAL_FUNCTION_SCHEMA if include_language else CODE_ONLY_FUNCTION_SCHEMA configs = {"tools": [{"type": "function", "function": function_schema}]} kwargs.update(configs) rsp = await self._achat_completion_function(messages, **kwargs) diff --git a/metagpt/roles/mi/ml_engineer.py b/metagpt/roles/mi/ml_engineer.py index 78d605d3e..7d7676eef 100644 --- a/metagpt/roles/mi/ml_engineer.py +++ b/metagpt/roles/mi/ml_engineer.py @@ -41,7 +41,6 @@ class MLEngineer(Interpreter): runtime_result=self.working_memory.get(), context=self.debug_context, ) - logger.info(f"new code \n{code}") cause_by = DebugCode self.latest_code = code["code"] diff --git a/metagpt/utils/common.py b/metagpt/utils/common.py index 015902c3d..f829c1289 100644 --- a/metagpt/utils/common.py +++ b/metagpt/utils/common.py @@ -361,16 +361,6 @@ def parse_recipient(text): return "" -def create_func_call_config(func_schema: dict) -> dict: - """Create new function call config""" - tools = [{"type": "function", "function": func_schema}] - tool_choice = {"type": "function", "function": {"name": func_schema["name"]}} - return { - "tools": tools, - "tool_choice": tool_choice, - } - - def remove_comments(code_str: str) -> str: """Remove comments from code.""" pattern = r"(\".*?\"|\'.*?\')|(\#.*?$)" From 4c1ee630f648f1877efea708e429771ee0b7e1a6 Mon Sep 17 00:00:00 2001 From: yzlin Date: Thu, 29 Feb 2024 18:23:26 +0800 Subject: [PATCH 020/131] merge mle & interpreter --- metagpt/actions/mi/debug_code.py | 93 -------------- metagpt/actions/mi/execute_nb_code.py | 2 +- metagpt/actions/mi/ml_action.py | 59 --------- metagpt/actions/mi/write_analysis_code.py | 93 +++++++++++--- metagpt/prompts/mi/ml_action.py | 60 +-------- metagpt/prompts/mi/write_analysis_code.py | 146 +++++++++++++++++++--- metagpt/provider/base_llm.py | 9 +- metagpt/provider/constant.py | 16 --- metagpt/provider/openai_api.py | 27 +--- metagpt/roles/mi/interpreter.py | 44 +++++-- metagpt/roles/mi/ml_engineer.py | 63 ---------- metagpt/tools/tool_type.py | 3 +- metagpt/utils/common.py | 23 ++++ 13 files changed, 273 insertions(+), 365 deletions(-) delete mode 100644 metagpt/actions/mi/debug_code.py delete mode 100644 metagpt/actions/mi/ml_action.py delete mode 100644 metagpt/roles/mi/ml_engineer.py diff --git a/metagpt/actions/mi/debug_code.py b/metagpt/actions/mi/debug_code.py deleted file mode 100644 index 8259d3de1..000000000 --- a/metagpt/actions/mi/debug_code.py +++ /dev/null @@ -1,93 +0,0 @@ -from __future__ import annotations - -import json - -from metagpt.actions import Action -from metagpt.schema import Message -from metagpt.utils.common import CodeParser - -DEBUG_REFLECTION_EXAMPLE = ''' -Example 1: -[previous impl]: -```python -def add(a: int, b: int) -> int: - """ - Given integers a and b, return the total value of a and b. - """ - return a - b -``` - -[runtime Error]: -Tested passed: - -Tests failed: -assert add(1, 2) == 3 # output: -1 -assert add(1, 2) == 4 # output: -1 - -[reflection on previous impl]: -The implementation failed the test cases where the input integers are 1 and 2. The issue arises because the code does not add the two integers together, but instead subtracts the second integer from the first. To fix this issue, we should change the operator from `-` to `+` in the return statement. This will ensure that the function returns the correct output for the given input. - -[improved impl]: -```python -def add(a: int, b: int) -> int: - """ - Given integers a and b, return the total value of a and b. - """ - return a + b -``` -''' - -REFLECTION_PROMPT = """ -Here is an example for you. -{debug_example} -[context] -{context} - -[previous impl] -{code} -[runtime Error] -{runtime_result} - -Analysis the error step by step, provide me improve method and code. Remember to follow [context] requirement. Don't forget write code for steps behind the error step. - -Output a json following the format: -```json -{{ - "reflection": str = "Reflection on previous implementation", - "improved_impl": str = "Refined code after reflection.", -}} -``` -""" - - -class DebugCode(Action): - async def run( - self, - context: list[Message] = None, - code: str = "", - runtime_result: str = "", - ) -> str: - """ - Execute the debugging process based on the provided context, code, and runtime_result. - - Args: - context (list[Message]): A list of Message objects representing the context. - code (str): The code to be debugged. - runtime_result (str): The result of the code execution. - - Returns: - str: The improved implementation based on the debugging process. - """ - - reflection_prompt = REFLECTION_PROMPT.format( - debug_example=DEBUG_REFLECTION_EXAMPLE, - context=context, - code=code, - runtime_result=runtime_result, - ) - system_prompt = "You are an AI Python assistant. You will be given your previous implementation code of a task, runtime error results, and a hint to change the implementation appropriately. Write your full implementation " - - rsp = await self._aask(reflection_prompt, system_msgs=[system_prompt]) - reflection = json.loads(CodeParser.parse_code(block=None, text=rsp)) - - return {"code": reflection["improved_impl"]} diff --git a/metagpt/actions/mi/execute_nb_code.py b/metagpt/actions/mi/execute_nb_code.py index a8c9c3085..02b1fb168 100644 --- a/metagpt/actions/mi/execute_nb_code.py +++ b/metagpt/actions/mi/execute_nb_code.py @@ -201,7 +201,7 @@ def truncate(result: str, keep_len: int = 2000, is_success: bool = True): if is_success: desc = f"Executed code successfully. Truncated to show only first {keep_len} characters\n" else: - desc = f"Executed code failed, please reflect the cause of bug and then debug. Truncated to show only last {keep_len} characters\n" + desc = f"Executed code failed, please reflect on the cause of bug and then debug. Truncated to show only last {keep_len} characters\n" if result.strip().startswith(" Tuple[list[Message], str]: - # prepare tool schemas and tool-type-specific instruction - tool_schemas, tool_type_usage_prompt = await self._prepare_tools(plan=plan) - - # ML-specific variables to be used in prompt - finished_tasks = plan.get_finished_tasks() - code_context = [remove_comments(task.code) for task in finished_tasks] - code_context = "\n\n".join(code_context) - - # prepare prompt depending on tool availability & LLM call - prompt = ML_PROMPT.format( - user_requirement=plan.goal, - history_code=code_context, - current_task=plan.current_task.instruction, - column_info=column_info, - tool_type_usage_prompt=tool_type_usage_prompt, - tool_schemas=tool_schemas, - examples=USE_TOOLS_EXAMPLE if tool_schemas else USE_NO_TOOLS_EXAMPLE, - ) - - rsp = await self.llm.aask_code(prompt) - - # Extra output to be used for potential debugging - context = [Message(content=prompt, role="user")] - - return context, rsp - - -class UpdateDataColumns(Action): - async def run(self, plan: Plan = None) -> dict: - finished_tasks = plan.get_finished_tasks() - code_context = [remove_comments(task.code) for task in finished_tasks] - code_context = "\n\n".join(code_context) - prompt = UPDATE_DATA_COLUMNS.format(history_code=code_context) - rsp = await self.llm.aask_code(prompt) - return rsp diff --git a/metagpt/actions/mi/write_analysis_code.py b/metagpt/actions/mi/write_analysis_code.py index 9337487c2..aa2ced892 100644 --- a/metagpt/actions/mi/write_analysis_code.py +++ b/metagpt/actions/mi/write_analysis_code.py @@ -11,14 +11,19 @@ from typing import Tuple from metagpt.actions import Action from metagpt.logs import logger +from metagpt.prompts.mi.ml_action import MODEL_TRAIN_EXAMPLE, USE_ML_TOOLS_EXAMPLE from metagpt.prompts.mi.write_analysis_code import ( + CHECK_DATA_PROMPT, + DEBUG_REFLECTION_EXAMPLE, + REFLECTION_PROMPT, + STRUCTUAL_PROMPT, TOOL_RECOMMENDATION_PROMPT, - TOOL_USAGE_PROMPT, ) from metagpt.schema import Message, Plan, SystemMessage from metagpt.tools import TOOL_REGISTRY from metagpt.tools.tool_registry import validate_tool_names -from metagpt.utils.common import CodeParser +from metagpt.tools.tool_type import ToolType +from metagpt.utils.common import CodeParser, process_message, remove_comments class WriteCodeWithTools(Action): @@ -27,7 +32,6 @@ class WriteCodeWithTools(Action): use_tools: bool = True # selected tools to choose from, listed by their names. An empty list means selection from all tools. selected_tools: list[str] = [] - DEFAULT_SYSTEM_MSG: str = """You are Code Interpreter, a world-class programmer that can complete any goal by executing code. Strictly follow the plan and generate code step by step. Each step of the code will be executed on the user's machine, and the user will provide the code execution results to you.**Notice: The code for the next step depends on the code for the previous step. Must reuse variables in the lastest other code directly, dont creat it again, it is very import for you. Use !pip install in a standalone block to install missing packages.Usually the libraries you need are already installed.Dont check if packages already imported.**""" # prompt reference: https://github.com/KillianLucas/open-interpreter/blob/v0.1.4/interpreter/system_message.txt def _insert_system_message(self, context: list[Message], system_msg: str = None): system_msg = system_msg or self.DEFAULT_SYSTEM_MSG @@ -98,6 +102,16 @@ class WriteCodeWithTools(Action): TOOL_REGISTRY.get_tool_type(tool_type).usage_prompt if TOOL_REGISTRY.has_tool_type(tool_type) else "" ) + # ML-specific tool usage examples + examples = "" + if plan.current_task.task_type in [ + ToolType.DATA_PREPROCESS.type_name, + ToolType.FEATURE_ENGINEERING.type_name, + ]: + examples = USE_ML_TOOLS_EXAMPLE + elif plan.current_task.task_type in [ToolType.MODEL_TRAIN.type_name]: + examples = MODEL_TRAIN_EXAMPLE + # prepare schemas of available tools tool_schemas = {} available_tools = self._get_tools_by_type(tool_type) @@ -105,27 +119,72 @@ class WriteCodeWithTools(Action): available_tools = {tool_name: tool.schemas["description"] for tool_name, tool in available_tools.items()} tool_schemas = await self._recommend_tool(plan.current_task.instruction, available_tools) - return tool_schemas, tool_type_usage_prompt + return tool_schemas, tool_type_usage_prompt, examples + + async def _debug_with_reflection(self, context: list[Message], working_memory: list[Message]): + reflection_prompt = REFLECTION_PROMPT.format( + debug_example=DEBUG_REFLECTION_EXAMPLE, + context=context, + previous_impl=working_memory, + ) + # print(reflection_prompt) + system_prompt = "You are an AI Python assistant. You will be given your previous implementation code of a task, runtime error results, and a hint to change the implementation appropriately. Write your full implementation " + + rsp = await self._aask(reflection_prompt, system_msgs=[system_prompt]) + reflection = json.loads(CodeParser.parse_code(block=None, text=rsp)) + + return reflection["improved_impl"] async def run( self, - context: list[Message], plan: Plan, + working_memory: list[Message] = [], + use_reflection: bool = False, **kwargs, ) -> str: - if self.use_tools: - # prepare tool schemas and tool-type-specific instruction - tool_schemas, tool_type_usage_prompt = await self._prepare_tools(plan=plan) + # prepare tool schemas and tool-type-specific instruction + tool_schemas, tool_type_usage_prompt, examples = await self._prepare_tools(plan=plan) - # form a complete tool usage instruction and include it as a message in context - tools_instruction = TOOL_USAGE_PROMPT.format( - tool_schemas=tool_schemas, tool_type_usage_prompt=tool_type_usage_prompt - ) - context.append(Message(content=tools_instruction, role="user")) + # necessary components to be used in prompt + finished_tasks = plan.get_finished_tasks() + code_written = [remove_comments(task.code) for task in finished_tasks] + code_written = "\n\n".join(code_written) + task_results = [task.result for task in finished_tasks] + task_results = "\n\n".join(task_results) - # prepare prompt & LLM call - prompt = self._insert_system_message(context) + # structure prompt + structual_prompt = STRUCTUAL_PROMPT.format( + user_requirement=plan.goal, + code_written=code_written, + task_results=task_results, + current_task=plan.current_task.instruction, + tool_type_usage_prompt=tool_type_usage_prompt, + tool_schemas=tool_schemas, + examples=examples, + ) + context = [Message(content=structual_prompt, role="user")] + working_memory + context = process_message(context) - rsp = await self.llm.aask_code(prompt) + # temp = context + working_memory + # print(*temp, sep="***\n\n***") - return rsp + # LLM call + if not use_reflection: + rsp = await self.llm.aask(context, **kwargs) + code = CodeParser.parse_code(block=None, text=rsp) + + else: + code = await self._debug_with_reflection(context=context, working_memory=working_memory) + + return code + + +class CheckData(Action): + async def run(self, plan: Plan = None) -> dict: + finished_tasks = plan.get_finished_tasks() + code_written = [remove_comments(task.code) for task in finished_tasks] + code_written = "\n\n".join(code_written) + prompt = CHECK_DATA_PROMPT.format(code_written=code_written) + rsp = await self._aask(prompt) + code = CodeParser.parse_code(block=None, text=rsp) + return code diff --git a/metagpt/prompts/mi/ml_action.py b/metagpt/prompts/mi/ml_action.py index ed7ea59e4..4d769379a 100644 --- a/metagpt/prompts/mi/ml_action.py +++ b/metagpt/prompts/mi/ml_action.py @@ -4,62 +4,7 @@ # @Author : lidanyang # @File : ml_action # @Desc : -UPDATE_DATA_COLUMNS = """ -# Background -Keep dataset column information updated before model train. -## Tasks Done -```python -{history_code} -```end - -# Task -Print the the latest column information after 'Tasks Done' code. Use the following code: -```python -from metagpt.tools.libs.data_preprocess import get_column_info - -column_info = get_column_info(df) -print("column_info") -print(column_info) -```end - -# Constraints: -- Use the DataFrame variable from 'Tasks Done' in place of df. -- Your code is to be added to a new cell in jupyter. -""" - -ML_PROMPT = """ -# Background -As a data scientist, you need to help user to achieve their goal [{user_requirement}] step-by-step in an continuous Jupyter notebook. - -## Done Tasks -```python -{history_code} -```end - -## Current Task -{current_task} - -# Latest Data Info -Latest data info after previous tasks: -{column_info} - -# Task -Write complete code for 'Current Task'. And avoid duplicating code from 'Done Tasks', such as repeated import of packages, reading data, etc. -Specifically, {tool_type_usage_prompt} - -# Capabilities -- You can utilize pre-defined tools in any code lines from 'Available Tools' in the form of Python Class. -- You can freely combine the use of any other public packages, like sklearn, numpy, pandas, etc.. - -# Available Tools: -Each Class tool is described in JSON format. When you call a tool, import the tool from its path first. -{tool_schemas} - -{examples} -""" - -USE_NO_TOOLS_EXAMPLE = """ -# Output Example: +MODEL_TRAIN_EXAMPLE = """ when current task is "train a lightgbm model on training data", the code can be like: ```python # Step 1: check data type and convert to numeric @@ -80,8 +25,7 @@ model.fit(train, y_train) - Ensure the output new code is executable in the same Jupyter notebook with previous tasks code have been executed. """ -USE_TOOLS_EXAMPLE = """ -# Output Example: +USE_ML_TOOLS_EXAMPLE = """ when current task is "do data preprocess, like fill missing value, handle outliers, etc.", the code can be like: ```python # Step 1: fill missing value diff --git a/metagpt/prompts/mi/write_analysis_code.py b/metagpt/prompts/mi/write_analysis_code.py index 6cae15e24..0974f368f 100644 --- a/metagpt/prompts/mi/write_analysis_code.py +++ b/metagpt/prompts/mi/write_analysis_code.py @@ -1,3 +1,130 @@ +STRUCTUAL_PROMPT = """ +# Background +As a data scientist, you need to help user to achieve their goal [{user_requirement}] step-by-step in an continuous Jupyter notebook. Since it is a notebook environment, don't use asyncio.run. Instead, use await if you need to call an async function. + +# Finished Tasks +## code +```python +{code_written} +``` + +## execution result +{task_results} + +# Current Task +{current_task} + +# Instruction +Write complete code for 'Current Task'. And avoid duplicating code from 'Finished Tasks', such as repeated import of packages, reading data, etc. +Specifically, {tool_type_usage_prompt} + +# Capabilities +- You can utilize pre-defined tools in any code lines from 'Available Tools' in the form of Python class or function. +- You can freely combine the use of any other public packages, like sklearn, numpy, pandas, etc.. + +# Available Tools: +Each tool is described in JSON format. When you call a tool, import the tool from its path first. +{tool_schemas} + +# Examples +{examples} + +# Output +Output code in the following format: +```python +your code +``` +""" + +DEBUG_REFLECTION_EXAMPLE = ''' +[previous impl]: +assistant: +```python +def add(a: int, b: int) -> int: + """ + Given integers a and b, return the total value of a and b. + """ + return a - b +``` + +user: +Tests failed: +assert add(1, 2) == 3 # output: -1 +assert add(1, 2) == 4 # output: -1 + +[reflection on previous impl]: +The implementation failed the test cases where the input integers are 1 and 2. The issue arises because the code does not add the two integers together, but instead subtracts the second integer from the first. To fix this issue, we should change the operator from `-` to `+` in the return statement. This will ensure that the function returns the correct output for the given input. + +[improved impl]: +```python +def add(a: int, b: int) -> int: + """ + Given integers a and b, return the total value of a and b. + """ + return a + b +``` +''' + +REFLECTION_PROMPT = """ +[example] +Here is an example of debugging with reflection. +{debug_example} +[/example] + +[context] +{context} + +[previous impl]: +{previous_impl} + +[instruction] +Analyze your previous code and error in [context] step by step, provide me with improved method and code. Remember to follow [context] requirement. Don't forget to write code for steps behind the error step. +Output a json following the format: +```json +{{ + "reflection": str = "Reflection on previous implementation", + "improved_impl": str = "Refined code after reflection.", +}} +``` +""" + +CHECK_DATA_PROMPT = """ +# Background +Check latest data info to guide subsequent tasks. + +## Finished Tasks +```python +{code_written} +```end + +# Task +Check code in finished tasks, print key variables to guide your following actions. +Specifically, if it is a data analysis or machine learning task, print the the latest column information using the following code, with DataFrame variable from 'Finished Tasks' in place of df: +```python +from metagpt.tools.libs.data_preprocess import get_column_info + +column_info = get_column_info(df) +print("column_info") +print(column_info) +```end +Otherwise, you may write any codes you see fit. Return an empty string if you think there is no important data to check. + +# Constraints: +- Your code is to be added to a new cell in jupyter. + +# Instruction +Output code following the format: +```python +your code +``` +""" + +DATA_INFO = """ +# Latest Data Info +Latest data info after previous tasks: +{info} +""" + TOOL_RECOMMENDATION_PROMPT = """ ## User Requirement: {current_task} @@ -18,22 +145,3 @@ Recommend up to five tools from 'Available Tools' that can help solve the 'User ["tool_name1", "tool_name2", ...] ``` """ - - -TOOL_USAGE_PROMPT = """ -# Instruction -Write complete code for 'Current Task'. And avoid duplicating code from finished tasks, such as repeated import of packages, reading data, etc. -Specifically, {tool_type_usage_prompt} - -# Capabilities -- You can utilize pre-defined tools in any code lines from 'Available Tools' in the form of Python Class. -- You can freely combine the use of any other public packages, like sklearn, numpy, pandas, etc.. - -# Available Tools (can be empty): -Each Class tool is described in JSON format. When you call a tool, import the tool first. -{tool_schemas} - -# Constraints: -- Ensure the output new code is executable in the same Jupyter notebook with previous tasks code have been executed. -- Always prioritize using pre-defined tools for the same functionality. -""" diff --git a/metagpt/provider/base_llm.py b/metagpt/provider/base_llm.py index 2137bb2dc..966a9fcd6 100644 --- a/metagpt/provider/base_llm.py +++ b/metagpt/provider/base_llm.py @@ -69,7 +69,7 @@ class BaseLLM(ABC): async def aask( self, - msg: str, + msg: Union[str, list[dict[str, str]]], system_msgs: Optional[list[str]] = None, format_msgs: Optional[list[dict[str, str]]] = None, images: Optional[Union[str, list[str]]] = None, @@ -84,7 +84,10 @@ class BaseLLM(ABC): message = [] if format_msgs: message.extend(format_msgs) - message.append(self._user_msg(msg, images=images)) + if isinstance(msg, str): + message.append(self._user_msg(msg, images=images)) + else: + message.extend(msg) logger.debug(message) rsp = await self.acompletion_text(message, stream=stream, timeout=timeout) return rsp @@ -103,7 +106,7 @@ class BaseLLM(ABC): return self._extract_assistant_rsp(context) async def aask_code( - self, messages: Union[str, Message, list[dict]], timeout=3, language: str = "", **kwargs + self, messages: Union[str, Message, list[dict]], timeout=3, include_language: bool = False, **kwargs ) -> dict: raise NotImplementedError diff --git a/metagpt/provider/constant.py b/metagpt/provider/constant.py index cfece6a79..dee78dc3b 100644 --- a/metagpt/provider/constant.py +++ b/metagpt/provider/constant.py @@ -26,22 +26,6 @@ GENERAL_FUNCTION_SCHEMA = { } -CODE_ONLY_FUNCTION_SCHEMA = { - "name": "add_new_code", - "description": "Add new code cell of current task to the end of an active Jupyter notebook.", - "parameters": { - "type": "object", - "properties": { - "code": { - "type": "string", - "description": "The code to be added to a new cell in jupyter.", - }, - }, - "required": ["code"], - }, -} - - # tool_choice value for general_function_schema # https://platform.openai.com/docs/api-reference/chat/create#chat-create-tool_choice GENERAL_TOOL_CHOICE = {"type": "function", "function": {"name": "execute"}} diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py index f1d53c75a..388149256 100644 --- a/metagpt/provider/openai_api.py +++ b/metagpt/provider/openai_api.py @@ -28,8 +28,7 @@ from metagpt.logs import log_llm_stream, logger from metagpt.provider.base_llm import BaseLLM from metagpt.provider.constant import CODE_ONLY_FUNCTION_SCHEMA, GENERAL_FUNCTION_SCHEMA from metagpt.provider.llm_provider_registry import register_provider -from metagpt.schema import Message -from metagpt.utils.common import CodeParser, decode_image +from metagpt.utils.common import CodeParser, decode_image, process_message from metagpt.utils.cost_manager import CostManager, Costs from metagpt.utils.exceptions import handle_exception from metagpt.utils.token_counter import ( @@ -145,32 +144,10 @@ class OpenAILLM(BaseLLM): rsp = await self._achat_completion(messages, timeout=timeout) return self.get_choice_text(rsp) - 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: int = 3, **chat_configs ) -> ChatCompletion: - messages = self._process_message(messages) + messages = process_message(messages) kwargs = self._cons_kwargs(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/mi/interpreter.py b/metagpt/roles/mi/interpreter.py index d5607bfe3..0e26dd71a 100644 --- a/metagpt/roles/mi/interpreter.py +++ b/metagpt/roles/mi/interpreter.py @@ -4,10 +4,12 @@ from pydantic import Field from metagpt.actions.mi.ask_review import ReviewConst from metagpt.actions.mi.execute_nb_code import ExecuteNbCode -from metagpt.actions.mi.write_analysis_code import WriteCodeWithTools +from metagpt.actions.mi.write_analysis_code import CheckData, WriteCodeWithTools from metagpt.logs import logger +from metagpt.prompts.mi.write_analysis_code import DATA_INFO from metagpt.roles import Role from metagpt.schema import Message, Task, TaskResult +from metagpt.tools.tool_type import ToolType class Interpreter(Role): @@ -15,6 +17,7 @@ class Interpreter(Role): profile: str = "Interpreter" auto_run: bool = True use_tools: bool = False + use_reflection: bool = False execute_code: ExecuteNbCode = Field(default_factory=ExecuteNbCode, exclude=True) tools: list[str] = [] @@ -48,14 +51,16 @@ class Interpreter(Role): counter = 0 success = False + await self._check_data() + while not success and counter < max_retry: ### write code ### - code, cause_by = await self._write_code() + code, cause_by = await self._write_code(counter) - self.working_memory.add(Message(content=code["code"], role="assistant", cause_by=cause_by)) + self.working_memory.add(Message(content=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=ExecuteNbCode)) @@ -69,14 +74,33 @@ class Interpreter(Role): if ReviewConst.CHANGE_WORDS[0] in review: counter = 0 # redo the task again with help of human suggestions - return code["code"], result, success + return code, result, success - async def _write_code(self): + async def _write_code(self, counter): todo = WriteCodeWithTools(use_tools=self.use_tools, selected_tools=self.tools) logger.info(f"ready to {todo.name}") - - context = self.planner.get_useful_memories() - # print(*context, sep="\n***\n") - code = await todo.run(context=context, plan=self.planner.plan, temperature=0.0) + use_reflection = counter > 0 and self.use_reflection + code = await todo.run( + plan=self.planner.plan, working_memory=self.working_memory.get(), use_reflection=use_reflection + ) return code, todo + + async def _check_data(self): + current_task = self.planner.plan.current_task + if current_task.task_type not in [ + ToolType.DATA_PREPROCESS.type_name, + ToolType.FEATURE_ENGINEERING.type_name, + ToolType.MODEL_TRAIN.type_name, + ]: + return + logger.info("Check updated data") + code = await CheckData().run(self.planner.plan) + if not code: + return + success = False + result, success = await self.execute_code.run(code) + if success: + print(result) + data_info = DATA_INFO.format(info=result) + self.working_memory.add(Message(content=data_info, role="user", cause_by=CheckData)) diff --git a/metagpt/roles/mi/ml_engineer.py b/metagpt/roles/mi/ml_engineer.py deleted file mode 100644 index 7d7676eef..000000000 --- a/metagpt/roles/mi/ml_engineer.py +++ /dev/null @@ -1,63 +0,0 @@ -from metagpt.actions.mi.debug_code import DebugCode -from metagpt.actions.mi.execute_nb_code import ExecuteNbCode -from metagpt.actions.mi.ml_action import UpdateDataColumns, WriteCodeWithToolsML -from metagpt.logs import logger -from metagpt.roles.mi.interpreter import Interpreter -from metagpt.tools.tool_type import ToolType -from metagpt.utils.common import any_to_str - - -class MLEngineer(Interpreter): - name: str = "Mark" - profile: str = "MLEngineer" - debug_context: list = [] - latest_code: str = "" - - async def _write_code(self): - if not self.use_tools: - return await super()._write_code() - - # In a trial and errors settings, check whether this is our first attempt to tackle the task. If there is no code execution before, then it is. - is_first_trial = any_to_str(ExecuteNbCode) not in [msg.cause_by for msg in self.working_memory.get()] - - if is_first_trial: - # For the first trial, write task code from scratch - column_info = await self._update_data_columns() - - logger.info("Write code with tools") - tool_context, code = await WriteCodeWithToolsML(selected_tools=self.tools).run( - context=[], # context assembled inside the Action - plan=self.planner.plan, - column_info=column_info, - ) - self.debug_context = tool_context - cause_by = WriteCodeWithToolsML - - else: - # Previous trials resulted in error, debug and rewrite the code - logger.warning("We got a bug, now start to debug...") - code = await DebugCode().run( - code=self.latest_code, - runtime_result=self.working_memory.get(), - context=self.debug_context, - ) - cause_by = DebugCode - - self.latest_code = code["code"] - - return code, cause_by - - async def _update_data_columns(self): - current_task = self.planner.plan.current_task - if current_task.task_type not in [ - ToolType.DATA_PREPROCESS.type_name, - ToolType.FEATURE_ENGINEERING.type_name, - ToolType.MODEL_TRAIN.type_name, - ]: - return "" - logger.info("Check columns in updated data") - code = await UpdateDataColumns().run(self.planner.plan) - success = False - result, success = await self.execute_code.run(**code) - print(result) - return result if success else "" diff --git a/metagpt/tools/tool_type.py b/metagpt/tools/tool_type.py index e9c9a3482..093a69bf7 100644 --- a/metagpt/tools/tool_type.py +++ b/metagpt/tools/tool_type.py @@ -19,7 +19,8 @@ class ToolType(Enum): ) DATA_PREPROCESS = ToolTypeDef( name="data_preprocess", - desc="Only for changing value inplace.", + desc="For preprocessing dataset in a data analysis or machine learning task ONLY," + "general data operation doesn't fall into this type", usage_prompt=DATA_PREPROCESS_PROMPT, ) EMAIL_LOGIN = ToolTypeDef( diff --git a/metagpt/utils/common.py b/metagpt/utils/common.py index f829c1289..4f86deab1 100644 --- a/metagpt/utils/common.py +++ b/metagpt/utils/common.py @@ -666,3 +666,26 @@ def decode_image(img_url_or_b64: str) -> Image: img_data = BytesIO(base64.b64decode(b64_data)) img = Image.open(img_data) return img + + +def process_message(messages: Union[str, Message, list[dict], list[Message], list[str]]) -> list[dict]: + """convert messages to list[dict].""" + from metagpt.schema import Message + + # 全部转成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 From 08bf756ae8cc81bfa483d5fcfbb9a45b49decca6 Mon Sep 17 00:00:00 2001 From: yzlin Date: Fri, 1 Mar 2024 18:33:49 +0800 Subject: [PATCH 021/131] rename interpreter --- README.md | 4 +++- examples/di/README.md | 18 ++++++++++++++++++ examples/{mi => di}/crawl_webpage.py | 6 +++--- examples/{mi => di}/data_visualization.py | 6 +++--- examples/{mi => di}/email_summary.py | 6 +++--- examples/{mi => di}/imitate_webpage.py | 6 +++--- examples/{mi => di}/machine_learning.py | 6 +++--- examples/{mi => di}/ml_engineer_with_tools.py | 2 +- examples/{mi => di}/ocr_receipt.py | 6 +++--- examples/{mi => di}/rm_image_background.py | 6 +++--- examples/{mi => di}/sd_tool_usage.py | 6 +++--- examples/{mi => di}/solve_math_problems.py | 6 +++--- examples/mi/README.md | 18 ------------------ metagpt/actions/__init__.py | 6 +++--- metagpt/actions/{mi => di}/__init__.py | 0 metagpt/actions/{mi => di}/ask_review.py | 0 metagpt/actions/{mi => di}/debug_code.py | 2 +- metagpt/actions/{mi => di}/execute_nb_code.py | 0 metagpt/actions/{mi => di}/ml_action.py | 6 +++--- .../actions/{mi => di}/write_analysis_code.py | 2 +- metagpt/actions/{mi => di}/write_plan.py | 2 +- metagpt/prompts/{mi => di}/__init__.py | 0 metagpt/prompts/{mi => di}/ml_action.py | 0 .../prompts/{mi => di}/write_analysis_code.py | 0 metagpt/roles/{mi => di}/__init__.py | 0 .../interpreter.py => di/data_interpreter.py} | 12 ++++++------ metagpt/roles/{mi => di}/ml_engineer.py | 10 +++++----- metagpt/strategy/planner.py | 4 ++-- metagpt/strategy/solver.py | 4 ++-- setup.py | 2 +- .../actions/{mi => di}/test_ask_review.py | 2 +- .../actions/{mi => di}/test_debug_code.py | 2 +- .../actions/{mi => di}/test_execute_nb_code.py | 2 +- .../actions/{mi => di}/test_ml_action.py | 2 +- .../{mi => di}/test_write_analysis_code.py | 4 ++-- .../actions/{mi => di}/test_write_plan.py | 2 +- .../test_data_interpreter.py} | 10 +++++----- .../roles/{mi => di}/test_ml_engineer.py | 6 +++--- tests/metagpt/utils/test_save_code.py | 2 +- 39 files changed, 90 insertions(+), 88 deletions(-) create mode 100644 examples/di/README.md rename examples/{mi => di}/crawl_webpage.py (78%) rename examples/{mi => di}/data_visualization.py (59%) rename examples/{mi => di}/email_summary.py (90%) rename examples/{mi => di}/imitate_webpage.py (81%) rename examples/{mi => di}/machine_learning.py (67%) rename examples/{mi => di}/ml_engineer_with_tools.py (94%) rename examples/{mi => di}/ocr_receipt.py (81%) rename examples/{mi => di}/rm_image_background.py (75%) rename examples/{mi => di}/sd_tool_usage.py (73%) rename examples/{mi => di}/solve_math_problems.py (70%) delete mode 100644 examples/mi/README.md rename metagpt/actions/{mi => di}/__init__.py (100%) rename metagpt/actions/{mi => di}/ask_review.py (100%) rename metagpt/actions/{mi => di}/debug_code.py (98%) rename metagpt/actions/{mi => di}/execute_nb_code.py (100%) rename metagpt/actions/{mi => di}/ml_action.py (93%) rename metagpt/actions/{mi => di}/write_analysis_code.py (99%) rename metagpt/actions/{mi => di}/write_plan.py (98%) rename metagpt/prompts/{mi => di}/__init__.py (100%) rename metagpt/prompts/{mi => di}/ml_action.py (100%) rename metagpt/prompts/{mi => di}/write_analysis_code.py (100%) rename metagpt/roles/{mi => di}/__init__.py (100%) rename metagpt/roles/{mi/interpreter.py => di/data_interpreter.py} (91%) rename metagpt/roles/{mi => di}/ml_engineer.py (89%) rename tests/metagpt/actions/{mi => di}/test_ask_review.py (84%) rename tests/metagpt/actions/{mi => di}/test_debug_code.py (96%) rename tests/metagpt/actions/{mi => di}/test_execute_nb_code.py (98%) rename tests/metagpt/actions/{mi => di}/test_ml_action.py (95%) rename tests/metagpt/actions/{mi => di}/test_write_analysis_code.py (99%) rename tests/metagpt/actions/{mi => di}/test_write_plan.py (95%) rename tests/metagpt/roles/{mi/test_interpreter.py => di/test_data_interpreter.py} (65%) rename tests/metagpt/roles/{mi => di}/test_ml_engineer.py (94%) diff --git a/README.md b/README.md index a1aa5ded8..197c387fd 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,9 @@ # MetaGPT: The Multi-Agent Framework

## News -🚀 Feb. 08, 2024: [v0.7.0](https://github.com/geekan/MetaGPT/releases/tag/v0.7.0) released, supporting assigning different LLMs to different Roles. We also introduced [Interpreter](https://github.com/geekan/MetaGPT/blob/main/examples/mi/README.md), a powerful agent capable of solving a wide range of real-world problems. +🚀 March. 01, 2024: Our Data Interpreter paper is on arxiv. Find all design and benchmark details [here](https://arxiv.org/abs/2402.18679)! + +🚀 Feb. 08, 2024: [v0.7.0](https://github.com/geekan/MetaGPT/releases/tag/v0.7.0) released, supporting assigning different LLMs to different Roles. We also introduced [Data Interpreter](https://github.com/geekan/MetaGPT/blob/main/examples/di/README.md), a powerful agent capable of solving a wide range of real-world problems. 🚀 Jan. 16, 2024: Our paper [MetaGPT: Meta Programming for A Multi-Agent Collaborative Framework ](https://arxiv.org/abs/2308.00352) accepted for oral presentation **(top 1.2%)** at ICLR 2024, **ranking #1** in the LLM-based Agent category. diff --git a/examples/di/README.md b/examples/di/README.md new file mode 100644 index 000000000..fb4e8044b --- /dev/null +++ b/examples/di/README.md @@ -0,0 +1,18 @@ +# Data Interpreter (DI) + +## What is Data Interpreter +Data Interpreter is an agent who solves problems through codes. It understands user requirements, makes plans, writes codes for execution, and uses tools if necessary. These capabilities enable it to tackle a wide range of scenarios, please check out the examples below. + +## Example List +- Data visualization +- Machine learning modeling +- Image background removal +- Solve math problems +- Receipt OCR +- Tool usage: web page imitation +- Tool usage: web crawling +- Tool usage: text2image +- Tool usage: email summarization and response +- More on the way! + +Please see [here](https://docs.deepwisdom.ai/main/en/guide/use_cases/agent/interpreter/intro.html) for detailed explanation. \ No newline at end of file diff --git a/examples/mi/crawl_webpage.py b/examples/di/crawl_webpage.py similarity index 78% rename from examples/mi/crawl_webpage.py rename to examples/di/crawl_webpage.py index b5d2fb3d0..f06b85d9b 100644 --- a/examples/mi/crawl_webpage.py +++ b/examples/di/crawl_webpage.py @@ -5,15 +5,15 @@ @File : crawl_webpage.py """ -from metagpt.roles.mi.interpreter import Interpreter +from metagpt.roles.di.data_interpreter import DataInterpreter async def main(): prompt = """Get data from `paperlist` table in https://papercopilot.com/statistics/iclr-statistics/iclr-2024-statistics/, and save it to a csv file. paper title must include `multiagent` or `large language model`. *notice: print key variables*""" - mi = Interpreter(use_tools=True) + di = DataInterpreter(use_tools=True) - await mi.run(prompt) + await di.run(prompt) if __name__ == "__main__": diff --git a/examples/mi/data_visualization.py b/examples/di/data_visualization.py similarity index 59% rename from examples/mi/data_visualization.py rename to examples/di/data_visualization.py index 2e4acc9b4..9af72dc42 100644 --- a/examples/mi/data_visualization.py +++ b/examples/di/data_visualization.py @@ -1,11 +1,11 @@ import asyncio -from metagpt.roles.mi.interpreter import Interpreter +from metagpt.roles.di.data_interpreter import DataInterpreter async def main(requirement: str = ""): - mi = Interpreter(use_tools=False) - await mi.run(requirement) + di = DataInterpreter(use_tools=False) + await di.run(requirement) if __name__ == "__main__": diff --git a/examples/mi/email_summary.py b/examples/di/email_summary.py similarity index 90% rename from examples/mi/email_summary.py rename to examples/di/email_summary.py index e1511c5b0..af081fee2 100644 --- a/examples/mi/email_summary.py +++ b/examples/di/email_summary.py @@ -6,7 +6,7 @@ """ import os -from metagpt.roles.mi.interpreter import Interpreter +from metagpt.roles.di.data_interpreter import DataInterpreter async def main(): @@ -22,9 +22,9 @@ async def main(): Firstly, Please help me fetch the latest 5 senders and full letter contents. Then, summarize each of the 5 emails into one sentence (you can do this by yourself, no need to import other models to do this) and output them in a markdown format.""" - mi = Interpreter(use_tools=True) + di = DataInterpreter(use_tools=True) - await mi.run(prompt) + await di.run(prompt) if __name__ == "__main__": diff --git a/examples/mi/imitate_webpage.py b/examples/di/imitate_webpage.py similarity index 81% rename from examples/mi/imitate_webpage.py rename to examples/di/imitate_webpage.py index 0e9ca731d..e2c99b874 100644 --- a/examples/mi/imitate_webpage.py +++ b/examples/di/imitate_webpage.py @@ -5,7 +5,7 @@ @Author : mannaandpoem @File : imitate_webpage.py """ -from metagpt.roles.mi.interpreter import Interpreter +from metagpt.roles.di.data_interpreter import DataInterpreter async def main(): @@ -15,9 +15,9 @@ Firstly, utilize Selenium and WebDriver for rendering. Secondly, convert image to a webpage including HTML, CSS and JS in one go. Finally, save webpage in a text file. Note: All required dependencies and environments have been fully installed and configured.""" - mi = Interpreter(use_tools=True) + di = DataInterpreter(use_tools=True) - await mi.run(prompt) + await di.run(prompt) if __name__ == "__main__": diff --git a/examples/mi/machine_learning.py b/examples/di/machine_learning.py similarity index 67% rename from examples/mi/machine_learning.py rename to examples/di/machine_learning.py index a8ab5051e..a58735831 100644 --- a/examples/mi/machine_learning.py +++ b/examples/di/machine_learning.py @@ -1,12 +1,12 @@ import fire -from metagpt.roles.mi.interpreter import Interpreter +from metagpt.roles.di.data_interpreter import DataInterpreter async def main(auto_run: bool = True): requirement = "Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy." - mi = Interpreter(auto_run=auto_run) - await mi.run(requirement) + di = DataInterpreter(auto_run=auto_run) + await di.run(requirement) if __name__ == "__main__": diff --git a/examples/mi/ml_engineer_with_tools.py b/examples/di/ml_engineer_with_tools.py similarity index 94% rename from examples/mi/ml_engineer_with_tools.py rename to examples/di/ml_engineer_with_tools.py index 9d0e7f951..6119ad843 100644 --- a/examples/mi/ml_engineer_with_tools.py +++ b/examples/di/ml_engineer_with_tools.py @@ -1,6 +1,6 @@ import asyncio -from metagpt.roles.mi.ml_engineer import MLEngineer +from metagpt.roles.di.ml_engineer import MLEngineer async def main(requirement: str): diff --git a/examples/mi/ocr_receipt.py b/examples/di/ocr_receipt.py similarity index 81% rename from examples/mi/ocr_receipt.py rename to examples/di/ocr_receipt.py index ffa5cff05..8b48be4f1 100644 --- a/examples/mi/ocr_receipt.py +++ b/examples/di/ocr_receipt.py @@ -1,4 +1,4 @@ -from metagpt.roles.mi.interpreter import Interpreter +from metagpt.roles.di.data_interpreter import DataInterpreter async def main(): @@ -8,9 +8,9 @@ async def main(): requirement = f"""This is a {language} receipt image. Your goal is to perform OCR on images using PaddleOCR, then extract the total amount from ocr text results, and finally save as table. Image path: {image_path}. NOTE: The environments for Paddle and PaddleOCR are all ready and has been fully installed.""" - mi = Interpreter() + di = DataInterpreter() - await mi.run(requirement) + await di.run(requirement) if __name__ == "__main__": diff --git a/examples/mi/rm_image_background.py b/examples/di/rm_image_background.py similarity index 75% rename from examples/mi/rm_image_background.py rename to examples/di/rm_image_background.py index 57e89b103..b74a79eeb 100644 --- a/examples/mi/rm_image_background.py +++ b/examples/di/rm_image_background.py @@ -1,11 +1,11 @@ import asyncio -from metagpt.roles.mi.interpreter import Interpreter +from metagpt.roles.di.data_interpreter import DataInterpreter async def main(requirement: str = ""): - mi = Interpreter(use_tools=False) - await mi.run(requirement) + di = DataInterpreter(use_tools=False) + await di.run(requirement) if __name__ == "__main__": diff --git a/examples/mi/sd_tool_usage.py b/examples/di/sd_tool_usage.py similarity index 73% rename from examples/mi/sd_tool_usage.py rename to examples/di/sd_tool_usage.py index f8507a411..69c7df5bd 100644 --- a/examples/mi/sd_tool_usage.py +++ b/examples/di/sd_tool_usage.py @@ -4,12 +4,12 @@ # @Desc : import asyncio -from metagpt.roles.mi.interpreter import Interpreter +from metagpt.roles.di.data_interpreter import DataInterpreter async def main(requirement: str = ""): - mi = Interpreter(use_tools=True, goal=requirement) - await mi.run(requirement) + di = DataInterpreter(use_tools=True, goal=requirement) + await di.run(requirement) if __name__ == "__main__": diff --git a/examples/mi/solve_math_problems.py b/examples/di/solve_math_problems.py similarity index 70% rename from examples/mi/solve_math_problems.py rename to examples/di/solve_math_problems.py index dce2edb00..35a69c953 100644 --- a/examples/mi/solve_math_problems.py +++ b/examples/di/solve_math_problems.py @@ -1,11 +1,11 @@ import asyncio -from metagpt.roles.mi.interpreter import Interpreter +from metagpt.roles.di.data_interpreter import DataInterpreter async def main(requirement: str = ""): - mi = Interpreter(use_tools=False) - await mi.run(requirement) + di = DataInterpreter(use_tools=False) + await di.run(requirement) if __name__ == "__main__": diff --git a/examples/mi/README.md b/examples/mi/README.md deleted file mode 100644 index 1734ba388..000000000 --- a/examples/mi/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# MetaGPT Interpreter (MI) - -## What is Interpreter -Interpreter is an agent who solves problems through codes. It understands user requirements, makes plans, writes codes for execution, and uses tools if necessary. These capabilities enable it to tackle a wide range of scenarios, please check out the examples below. - -## Example List -- Data visualization -- Machine learning modeling -- Image background removal -- Solve math problems -- Receipt OCR -- Tool usage: web page imitation -- Tool usage: web crawling -- Tool usage: text2image -- Tool usage: email summarization and response -- More on the way! - -Please see [here](https://docs.deepwisdom.ai/main/en/guide/use_cases/agent/interpreter/mi_intro.html) for detailed explanation. \ No newline at end of file diff --git a/metagpt/actions/__init__.py b/metagpt/actions/__init__.py index 19a7c10d5..29af1dad1 100644 --- a/metagpt/actions/__init__.py +++ b/metagpt/actions/__init__.py @@ -22,9 +22,9 @@ from metagpt.actions.write_code_review import WriteCodeReview from metagpt.actions.write_prd import WritePRD from metagpt.actions.write_prd_review import WritePRDReview from metagpt.actions.write_test import WriteTest -from metagpt.actions.mi.execute_nb_code import ExecuteNbCode -from metagpt.actions.mi.write_analysis_code import WriteCodeWithoutTools, WriteCodeWithTools -from metagpt.actions.mi.write_plan import WritePlan +from metagpt.actions.di.execute_nb_code import ExecuteNbCode +from metagpt.actions.di.write_analysis_code import WriteCodeWithoutTools, WriteCodeWithTools +from metagpt.actions.di.write_plan import WritePlan class ActionType(Enum): diff --git a/metagpt/actions/mi/__init__.py b/metagpt/actions/di/__init__.py similarity index 100% rename from metagpt/actions/mi/__init__.py rename to metagpt/actions/di/__init__.py diff --git a/metagpt/actions/mi/ask_review.py b/metagpt/actions/di/ask_review.py similarity index 100% rename from metagpt/actions/mi/ask_review.py rename to metagpt/actions/di/ask_review.py diff --git a/metagpt/actions/mi/debug_code.py b/metagpt/actions/di/debug_code.py similarity index 98% rename from metagpt/actions/mi/debug_code.py rename to metagpt/actions/di/debug_code.py index db3b7a9db..f589436f5 100644 --- a/metagpt/actions/mi/debug_code.py +++ b/metagpt/actions/di/debug_code.py @@ -1,6 +1,6 @@ from __future__ import annotations -from metagpt.actions.mi.write_analysis_code import BaseWriteAnalysisCode +from metagpt.actions.di.write_analysis_code import BaseWriteAnalysisCode from metagpt.logs import logger from metagpt.schema import Message from metagpt.utils.common import create_func_call_config diff --git a/metagpt/actions/mi/execute_nb_code.py b/metagpt/actions/di/execute_nb_code.py similarity index 100% rename from metagpt/actions/mi/execute_nb_code.py rename to metagpt/actions/di/execute_nb_code.py diff --git a/metagpt/actions/mi/ml_action.py b/metagpt/actions/di/ml_action.py similarity index 93% rename from metagpt/actions/mi/ml_action.py rename to metagpt/actions/di/ml_action.py index 60b2fb547..d49b7b67d 100644 --- a/metagpt/actions/mi/ml_action.py +++ b/metagpt/actions/di/ml_action.py @@ -3,14 +3,14 @@ from __future__ import annotations from typing import Tuple from metagpt.actions import Action -from metagpt.actions.mi.write_analysis_code import WriteCodeWithTools -from metagpt.prompts.mi.ml_action import ( +from metagpt.actions.di.write_analysis_code import WriteCodeWithTools +from metagpt.prompts.di.ml_action import ( ML_GENERATE_CODE_PROMPT, ML_TOOL_USAGE_PROMPT, PRINT_DATA_COLUMNS, UPDATE_DATA_COLUMNS, ) -from metagpt.prompts.mi.write_analysis_code import CODE_GENERATOR_WITH_TOOLS +from metagpt.prompts.di.write_analysis_code import CODE_GENERATOR_WITH_TOOLS from metagpt.schema import Message, Plan from metagpt.utils.common import create_func_call_config, remove_comments diff --git a/metagpt/actions/mi/write_analysis_code.py b/metagpt/actions/di/write_analysis_code.py similarity index 99% rename from metagpt/actions/mi/write_analysis_code.py rename to metagpt/actions/di/write_analysis_code.py index b3d0632b6..0c4980c2b 100644 --- a/metagpt/actions/mi/write_analysis_code.py +++ b/metagpt/actions/di/write_analysis_code.py @@ -10,7 +10,7 @@ from typing import Tuple from metagpt.actions import Action from metagpt.logs import logger -from metagpt.prompts.mi.write_analysis_code import ( +from metagpt.prompts.di.write_analysis_code import ( CODE_GENERATOR_WITH_TOOLS, SELECT_FUNCTION_TOOLS, TOOL_RECOMMENDATION_PROMPT, diff --git a/metagpt/actions/mi/write_plan.py b/metagpt/actions/di/write_plan.py similarity index 98% rename from metagpt/actions/mi/write_plan.py rename to metagpt/actions/di/write_plan.py index 8067d7b87..518dfb6c6 100644 --- a/metagpt/actions/mi/write_plan.py +++ b/metagpt/actions/di/write_plan.py @@ -12,7 +12,7 @@ from typing import Tuple from metagpt.actions import Action from metagpt.logs import logger -from metagpt.prompts.mi.write_analysis_code import ( +from metagpt.prompts.di.write_analysis_code import ( ASSIGN_TASK_TYPE_CONFIG, ASSIGN_TASK_TYPE_PROMPT, ) diff --git a/metagpt/prompts/mi/__init__.py b/metagpt/prompts/di/__init__.py similarity index 100% rename from metagpt/prompts/mi/__init__.py rename to metagpt/prompts/di/__init__.py diff --git a/metagpt/prompts/mi/ml_action.py b/metagpt/prompts/di/ml_action.py similarity index 100% rename from metagpt/prompts/mi/ml_action.py rename to metagpt/prompts/di/ml_action.py diff --git a/metagpt/prompts/mi/write_analysis_code.py b/metagpt/prompts/di/write_analysis_code.py similarity index 100% rename from metagpt/prompts/mi/write_analysis_code.py rename to metagpt/prompts/di/write_analysis_code.py diff --git a/metagpt/roles/mi/__init__.py b/metagpt/roles/di/__init__.py similarity index 100% rename from metagpt/roles/mi/__init__.py rename to metagpt/roles/di/__init__.py diff --git a/metagpt/roles/mi/interpreter.py b/metagpt/roles/di/data_interpreter.py similarity index 91% rename from metagpt/roles/mi/interpreter.py rename to metagpt/roles/di/data_interpreter.py index fa50098e9..b3a2e789f 100644 --- a/metagpt/roles/mi/interpreter.py +++ b/metagpt/roles/di/data_interpreter.py @@ -2,9 +2,9 @@ from __future__ import annotations from pydantic import Field -from metagpt.actions.mi.ask_review import ReviewConst -from metagpt.actions.mi.execute_nb_code import ExecuteNbCode -from metagpt.actions.mi.write_analysis_code import ( +from metagpt.actions.di.ask_review import ReviewConst +from metagpt.actions.di.execute_nb_code import ExecuteNbCode +from metagpt.actions.di.write_analysis_code import ( WriteCodeWithoutTools, WriteCodeWithTools, ) @@ -13,9 +13,9 @@ from metagpt.roles import Role from metagpt.schema import Message, Task, TaskResult -class Interpreter(Role): - name: str = "Ivy" - profile: str = "Interpreter" +class DataInterpreter(Role): + name: str = "David" + profile: str = "DataInterpreter" auto_run: bool = True use_tools: bool = False execute_code: ExecuteNbCode = Field(default_factory=ExecuteNbCode, exclude=True) diff --git a/metagpt/roles/mi/ml_engineer.py b/metagpt/roles/di/ml_engineer.py similarity index 89% rename from metagpt/roles/mi/ml_engineer.py rename to metagpt/roles/di/ml_engineer.py index 78d605d3e..b33b166cf 100644 --- a/metagpt/roles/mi/ml_engineer.py +++ b/metagpt/roles/di/ml_engineer.py @@ -1,13 +1,13 @@ -from metagpt.actions.mi.debug_code import DebugCode -from metagpt.actions.mi.execute_nb_code import ExecuteNbCode -from metagpt.actions.mi.ml_action import UpdateDataColumns, WriteCodeWithToolsML +from metagpt.actions.di.debug_code import DebugCode +from metagpt.actions.di.execute_nb_code import ExecuteNbCode +from metagpt.actions.di.ml_action import UpdateDataColumns, WriteCodeWithToolsML from metagpt.logs import logger -from metagpt.roles.mi.interpreter import Interpreter +from metagpt.roles.di.data_interpreter import DataInterpreter from metagpt.tools.tool_type import ToolType from metagpt.utils.common import any_to_str -class MLEngineer(Interpreter): +class MLEngineer(DataInterpreter): name: str = "Mark" profile: str = "MLEngineer" debug_context: list = [] diff --git a/metagpt/strategy/planner.py b/metagpt/strategy/planner.py index 99d16f78b..44294be00 100644 --- a/metagpt/strategy/planner.py +++ b/metagpt/strategy/planner.py @@ -4,8 +4,8 @@ import json from pydantic import BaseModel, Field -from metagpt.actions.mi.ask_review import AskReview, ReviewConst -from metagpt.actions.mi.write_plan import ( +from metagpt.actions.di.ask_review import AskReview, ReviewConst +from metagpt.actions.di.write_plan import ( WritePlan, precheck_update_plan_from_rsp, update_plan_from_rsp, diff --git a/metagpt/strategy/solver.py b/metagpt/strategy/solver.py index ab16d82bd..e532f736b 100644 --- a/metagpt/strategy/solver.py +++ b/metagpt/strategy/solver.py @@ -49,8 +49,8 @@ class TOTSolver(BaseSolver): raise NotImplementedError -class InterpreterSolver(BaseSolver): - """InterpreterSolver: Write&Run code in the graph""" +class DataInterpreterSolver(BaseSolver): + """DataInterpreterSolver: Write&Run code in the graph""" async def solve(self): raise NotImplementedError diff --git a/setup.py b/setup.py index f5d880ac9..2044e577a 100644 --- a/setup.py +++ b/setup.py @@ -57,7 +57,7 @@ extras_require["dev"] = (["pylint~=3.0.3", "black~=23.3.0", "isort~=5.12.0", "pr setup( name="metagpt", - version="0.7.3", + version="0.7.4", description="The Multi-Agent Framework", long_description=long_description, long_description_content_type="text/markdown", diff --git a/tests/metagpt/actions/mi/test_ask_review.py b/tests/metagpt/actions/di/test_ask_review.py similarity index 84% rename from tests/metagpt/actions/mi/test_ask_review.py rename to tests/metagpt/actions/di/test_ask_review.py index 92e8bd046..6bb1accf5 100644 --- a/tests/metagpt/actions/mi/test_ask_review.py +++ b/tests/metagpt/actions/di/test_ask_review.py @@ -1,6 +1,6 @@ import pytest -from metagpt.actions.mi.ask_review import AskReview +from metagpt.actions.di.ask_review import AskReview @pytest.mark.asyncio diff --git a/tests/metagpt/actions/mi/test_debug_code.py b/tests/metagpt/actions/di/test_debug_code.py similarity index 96% rename from tests/metagpt/actions/mi/test_debug_code.py rename to tests/metagpt/actions/di/test_debug_code.py index 24cfcef10..67f72ad63 100644 --- a/tests/metagpt/actions/mi/test_debug_code.py +++ b/tests/metagpt/actions/di/test_debug_code.py @@ -5,7 +5,7 @@ import pytest -from metagpt.actions.mi.debug_code import DebugCode +from metagpt.actions.di.debug_code import DebugCode from metagpt.schema import Message ErrorStr = """Tested passed: diff --git a/tests/metagpt/actions/mi/test_execute_nb_code.py b/tests/metagpt/actions/di/test_execute_nb_code.py similarity index 98% rename from tests/metagpt/actions/mi/test_execute_nb_code.py rename to tests/metagpt/actions/di/test_execute_nb_code.py index 59a814054..b491dd212 100644 --- a/tests/metagpt/actions/mi/test_execute_nb_code.py +++ b/tests/metagpt/actions/di/test_execute_nb_code.py @@ -1,6 +1,6 @@ import pytest -from metagpt.actions.mi.execute_nb_code import ExecuteNbCode, truncate +from metagpt.actions.di.execute_nb_code import ExecuteNbCode, truncate @pytest.mark.asyncio diff --git a/tests/metagpt/actions/mi/test_ml_action.py b/tests/metagpt/actions/di/test_ml_action.py similarity index 95% rename from tests/metagpt/actions/mi/test_ml_action.py rename to tests/metagpt/actions/di/test_ml_action.py index 27d47b0e3..826a7fcf2 100644 --- a/tests/metagpt/actions/mi/test_ml_action.py +++ b/tests/metagpt/actions/di/test_ml_action.py @@ -1,6 +1,6 @@ import pytest -from metagpt.actions.mi.ml_action import WriteCodeWithToolsML +from metagpt.actions.di.ml_action import WriteCodeWithToolsML from metagpt.schema import Plan, Task diff --git a/tests/metagpt/actions/mi/test_write_analysis_code.py b/tests/metagpt/actions/di/test_write_analysis_code.py similarity index 99% rename from tests/metagpt/actions/mi/test_write_analysis_code.py rename to tests/metagpt/actions/di/test_write_analysis_code.py index 6c2228222..d6bbae1c6 100644 --- a/tests/metagpt/actions/mi/test_write_analysis_code.py +++ b/tests/metagpt/actions/di/test_write_analysis_code.py @@ -2,8 +2,8 @@ import asyncio import pytest -from metagpt.actions.mi.execute_nb_code import ExecuteNbCode -from metagpt.actions.mi.write_analysis_code import ( +from metagpt.actions.di.execute_nb_code import ExecuteNbCode +from metagpt.actions.di.write_analysis_code import ( WriteCodeWithoutTools, WriteCodeWithTools, ) diff --git a/tests/metagpt/actions/mi/test_write_plan.py b/tests/metagpt/actions/di/test_write_plan.py similarity index 95% rename from tests/metagpt/actions/mi/test_write_plan.py rename to tests/metagpt/actions/di/test_write_plan.py index 97632ea44..80b3399b8 100644 --- a/tests/metagpt/actions/mi/test_write_plan.py +++ b/tests/metagpt/actions/di/test_write_plan.py @@ -1,6 +1,6 @@ import pytest -from metagpt.actions.mi.write_plan import ( +from metagpt.actions.di.write_plan import ( Plan, Task, WritePlan, diff --git a/tests/metagpt/roles/mi/test_interpreter.py b/tests/metagpt/roles/di/test_data_interpreter.py similarity index 65% rename from tests/metagpt/roles/mi/test_interpreter.py rename to tests/metagpt/roles/di/test_data_interpreter.py index 3bae4a1ac..ba50f473b 100644 --- a/tests/metagpt/roles/mi/test_interpreter.py +++ b/tests/metagpt/roles/di/test_data_interpreter.py @@ -1,23 +1,23 @@ import pytest from metagpt.logs import logger -from metagpt.roles.mi.interpreter import Interpreter +from metagpt.roles.di.data_interpreter import DataInterpreter @pytest.mark.asyncio @pytest.mark.parametrize("auto_run", [(True), (False)]) async def test_interpreter(mocker, auto_run): - mocker.patch("metagpt.actions.mi.execute_nb_code.ExecuteNbCode.run", return_value=("a successful run", True)) + mocker.patch("metagpt.actions.di.execute_nb_code.ExecuteNbCode.run", return_value=("a successful run", True)) mocker.patch("builtins.input", return_value="confirm") requirement = "Run data analysis on sklearn Iris dataset, include a plot" tools = [] - mi = Interpreter(auto_run=auto_run, use_tools=True, tools=tools) - rsp = await mi.run(requirement) + di = DataInterpreter(auto_run=auto_run, use_tools=True, tools=tools) + rsp = await di.run(requirement) logger.info(rsp) assert len(rsp.content) > 0 - finished_tasks = mi.planner.plan.get_finished_tasks() + finished_tasks = di.planner.plan.get_finished_tasks() assert len(finished_tasks) > 0 assert len(finished_tasks[0].code) > 0 # check one task to see if code is recorded diff --git a/tests/metagpt/roles/mi/test_ml_engineer.py b/tests/metagpt/roles/di/test_ml_engineer.py similarity index 94% rename from tests/metagpt/roles/mi/test_ml_engineer.py rename to tests/metagpt/roles/di/test_ml_engineer.py index 921ac8822..08b92cd27 100644 --- a/tests/metagpt/roles/mi/test_ml_engineer.py +++ b/tests/metagpt/roles/di/test_ml_engineer.py @@ -1,11 +1,11 @@ import pytest -from metagpt.actions.mi.execute_nb_code import ExecuteNbCode +from metagpt.actions.di.execute_nb_code import ExecuteNbCode from metagpt.logs import logger -from metagpt.roles.mi.ml_engineer import MLEngineer +from metagpt.roles.di.ml_engineer import MLEngineer from metagpt.schema import Message, Plan, Task from metagpt.tools.tool_type import ToolType -from tests.metagpt.actions.mi.test_debug_code import CODE, DebugContext, ErrorStr +from tests.metagpt.actions.di.test_debug_code import CODE, DebugContext, ErrorStr def test_mle_init(): diff --git a/tests/metagpt/utils/test_save_code.py b/tests/metagpt/utils/test_save_code.py index 9df2650f3..aceecec3b 100644 --- a/tests/metagpt/utils/test_save_code.py +++ b/tests/metagpt/utils/test_save_code.py @@ -6,7 +6,7 @@ import nbformat import pytest -from metagpt.actions.mi.execute_nb_code import ExecuteNbCode +from metagpt.actions.di.execute_nb_code import ExecuteNbCode from metagpt.utils.common import read_json_file from metagpt.utils.save_code import DATA_PATH, save_code_file From 0a2273c7a071490f8bb6cbab3d8e0e20b9f86a63 Mon Sep 17 00:00:00 2001 From: yzlin Date: Mon, 4 Mar 2024 10:45:00 +0800 Subject: [PATCH 022/131] small fixes --- metagpt/actions/mi/write_analysis_code.py | 14 ++++++-------- metagpt/prompts/mi/write_analysis_code.py | 4 +--- metagpt/provider/base_llm.py | 4 +--- metagpt/provider/openai_api.py | 9 +++------ metagpt/roles/mi/interpreter.py | 2 +- 5 files changed, 12 insertions(+), 21 deletions(-) diff --git a/metagpt/actions/mi/write_analysis_code.py b/metagpt/actions/mi/write_analysis_code.py index aa2ced892..64e58920a 100644 --- a/metagpt/actions/mi/write_analysis_code.py +++ b/metagpt/actions/mi/write_analysis_code.py @@ -19,7 +19,7 @@ from metagpt.prompts.mi.write_analysis_code import ( STRUCTUAL_PROMPT, TOOL_RECOMMENDATION_PROMPT, ) -from metagpt.schema import Message, Plan, SystemMessage +from metagpt.schema import Message, Plan from metagpt.tools import TOOL_REGISTRY from metagpt.tools.tool_registry import validate_tool_names from metagpt.tools.tool_type import ToolType @@ -33,11 +33,6 @@ class WriteCodeWithTools(Action): # selected tools to choose from, listed by their names. An empty list means selection from all tools. selected_tools: list[str] = [] - def _insert_system_message(self, context: list[Message], system_msg: str = None): - system_msg = system_msg or self.DEFAULT_SYSTEM_MSG - context.insert(0, SystemMessage(content=system_msg)) if context[0].role != "system" else None - return context - def _get_tools_by_type(self, tool_type: str) -> dict: """ Retreive tools by tool type from registry, but filtered by pre-selected tool list @@ -85,15 +80,18 @@ class WriteCodeWithTools(Action): return tool_schemas - async def _prepare_tools(self, plan: Plan) -> Tuple[dict, str]: + async def _prepare_tools(self, plan: Plan) -> Tuple[dict, str, str]: """Prepare tool schemas and usage instructions according to current task Args: plan (Plan): The overall plan containing task information. Returns: - Tuple[dict, str]: A tool schemas ({tool_name: tool_schema_dict}) and a usage prompt for the type of tools selected + Tuple[dict, str, str]: A tool schemas ({tool_name: tool_schema_dict}), a usage prompt for the type of tools selected, and examples of using the tools """ + if not self.use_tools: + return {}, "", "" + # find tool type from task type through exact match, can extend to retrieval in the future tool_type = plan.current_task.task_type diff --git a/metagpt/prompts/mi/write_analysis_code.py b/metagpt/prompts/mi/write_analysis_code.py index 0974f368f..12ca9d4ae 100644 --- a/metagpt/prompts/mi/write_analysis_code.py +++ b/metagpt/prompts/mi/write_analysis_code.py @@ -56,13 +56,11 @@ assert add(1, 2) == 4 # output: -1 The implementation failed the test cases where the input integers are 1 and 2. The issue arises because the code does not add the two integers together, but instead subtracts the second integer from the first. To fix this issue, we should change the operator from `-` to `+` in the return statement. This will ensure that the function returns the correct output for the given input. [improved impl]: -```python def add(a: int, b: int) -> int: """ Given integers a and b, return the total value of a and b. """ return a + b -``` ''' REFLECTION_PROMPT = """ @@ -107,7 +105,7 @@ column_info = get_column_info(df) print("column_info") print(column_info) ```end -Otherwise, you may write any codes you see fit. Return an empty string if you think there is no important data to check. +Otherwise, print out any key variables you see fit. Return an empty string if you think there is no important data to check. # Constraints: - Your code is to be added to a new cell in jupyter. diff --git a/metagpt/provider/base_llm.py b/metagpt/provider/base_llm.py index 966a9fcd6..140ed3570 100644 --- a/metagpt/provider/base_llm.py +++ b/metagpt/provider/base_llm.py @@ -105,9 +105,7 @@ class BaseLLM(ABC): context.append(self._assistant_msg(rsp_text)) return self._extract_assistant_rsp(context) - async def aask_code( - self, messages: Union[str, Message, list[dict]], timeout=3, include_language: bool = False, **kwargs - ) -> dict: + async def aask_code(self, messages: Union[str, Message, list[dict]], timeout=3, **kwargs) -> dict: raise NotImplementedError @abstractmethod diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py index 388149256..3616debc4 100644 --- a/metagpt/provider/openai_api.py +++ b/metagpt/provider/openai_api.py @@ -26,7 +26,7 @@ from tenacity import ( from metagpt.configs.llm_config import LLMConfig, LLMType from metagpt.logs import log_llm_stream, logger from metagpt.provider.base_llm import BaseLLM -from metagpt.provider.constant import CODE_ONLY_FUNCTION_SCHEMA, GENERAL_FUNCTION_SCHEMA +from metagpt.provider.constant import GENERAL_FUNCTION_SCHEMA from metagpt.provider.llm_provider_registry import register_provider from metagpt.utils.common import CodeParser, decode_image, process_message from metagpt.utils.cost_manager import CostManager, Costs @@ -153,7 +153,7 @@ class OpenAILLM(BaseLLM): self._update_costs(rsp.usage) return rsp - async def aask_code(self, messages: list[dict], timeout: int = 3, include_language: bool = False, **kwargs) -> dict: + async def aask_code(self, messages: list[dict], timeout: int = 3, **kwargs) -> dict: """Use function of tools to ask a code. Note: Keep kwargs consistent with https://platform.openai.com/docs/api-reference/chat/create @@ -161,13 +161,10 @@ class OpenAILLM(BaseLLM): >>> llm = OpenAILLM() >>> msg = [{'role': 'user', 'content': "Write a python hello world code."}] >>> rsp = await llm.aask_code(msg) - # -> {'code': "print('Hello, World!')"} - >>> rsp = await llm.aask_code(msg, include_language=True) # -> {'language': 'python', 'code': "print('Hello, World!')"} """ if "tools" not in kwargs: - function_schema = GENERAL_FUNCTION_SCHEMA if include_language else CODE_ONLY_FUNCTION_SCHEMA - configs = {"tools": [{"type": "function", "function": function_schema}]} + configs = {"tools": [{"type": "function", "function": GENERAL_FUNCTION_SCHEMA}]} kwargs.update(configs) rsp = await self._achat_completion_function(messages, **kwargs) return self.get_choice_function_arguments(rsp) diff --git a/metagpt/roles/mi/interpreter.py b/metagpt/roles/mi/interpreter.py index 0e26dd71a..0a99c0e69 100644 --- a/metagpt/roles/mi/interpreter.py +++ b/metagpt/roles/mi/interpreter.py @@ -96,7 +96,7 @@ class Interpreter(Role): return logger.info("Check updated data") code = await CheckData().run(self.planner.plan) - if not code: + if not code.strip(): return success = False result, success = await self.execute_code.run(code) From a7eaf6666c43b42d26cf20fd7e09cc62602e7f50 Mon Sep 17 00:00:00 2001 From: Abhishek0075 Date: Tue, 5 Mar 2024 23:33:10 +0530 Subject: [PATCH 023/131] change in two examples file --- examples/debate.py | 2 +- examples/search_with_specific_engine.py | 31 ++++++++++++++++++------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/examples/debate.py b/examples/debate.py index 72ab8796d..22cfbf39e 100644 --- a/examples/debate.py +++ b/examples/debate.py @@ -105,4 +105,4 @@ def main(idea: str, investment: float = 3.0, n_round: int = 10): if __name__ == "__main__": - fire.Fire(main) + fire.Fire(main) # run as python debate.py --idea="TOPIC" --investment=3.0 --n_round=5 diff --git a/examples/search_with_specific_engine.py b/examples/search_with_specific_engine.py index 97b1378ee..c9fa0395e 100644 --- a/examples/search_with_specific_engine.py +++ b/examples/search_with_specific_engine.py @@ -6,19 +6,32 @@ import asyncio from metagpt.roles import Searcher from metagpt.tools.search_engine import SearchEngine, SearchEngineType - +from metagpt.config2 import Config async def main(): question = "What are the most interesting human facts?" - kwargs = {"api_key": "", "cse_id": "", "proxy": None} - # Serper API - # await Searcher(search_engine=SearchEngine(engine=SearchEngineType.SERPER_GOOGLE, **kwargs)).run(question) - # SerpAPI - # await Searcher(search_engine=SearchEngine(engine=SearchEngineType.SERPAPI_GOOGLE, **kwargs)).run(question) - # Google API - # await Searcher(search_engine=SearchEngine(engine=SearchEngineType.DIRECT_GOOGLE, **kwargs)).run(question) + + search = Config.default().search + kwargs = {"api_key": search.api_key, "cse_id": search.cse_id, "proxy": None} + + if(search.api_type == SearchEngineType.DIRECT_GOOGLE): + # Google API + await Searcher(search_engine=SearchEngine(engine=SearchEngineType.DIRECT_GOOGLE, **kwargs)).run(question) + elif(search.api_type == SearchEngineType.SERPER_GOOGLE): + # Serper API + await Searcher(search_engine=SearchEngine(engine=SearchEngineType.SERPER_GOOGLE, **kwargs)).run(question) + elif(search.api_type == SearchEngineType.SERPAPI_GOOGLE): + # SerpAPI + await Searcher(search_engine=SearchEngine(engine=SearchEngineType.SERPAPI_GOOGLE, **kwargs)).run(question) + else: + # DDG API + await Searcher(search_engine=SearchEngine(engine=SearchEngineType.DUCK_DUCK_GO, **kwargs)).run(question) + + + + # # DDG API - await Searcher(search_engine=SearchEngine(engine=SearchEngineType.DUCK_DUCK_GO, **kwargs)).run(question) + # await Searcher(search_engine=SearchEngine(engine=SearchEngineType.DUCK_DUCK_GO, **kwargs)).run(question) if __name__ == "__main__": From 5caa03d6952b7d69f0cf76df65753181b0b08172 Mon Sep 17 00:00:00 2001 From: Abhishek0075 Date: Wed, 6 Mar 2024 01:41:13 +0530 Subject: [PATCH 024/131] small change --- examples/search_with_specific_engine.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/examples/search_with_specific_engine.py b/examples/search_with_specific_engine.py index c9fa0395e..5fc17824b 100644 --- a/examples/search_with_specific_engine.py +++ b/examples/search_with_specific_engine.py @@ -27,12 +27,5 @@ async def main(): # DDG API await Searcher(search_engine=SearchEngine(engine=SearchEngineType.DUCK_DUCK_GO, **kwargs)).run(question) - - - # - # DDG API - # await Searcher(search_engine=SearchEngine(engine=SearchEngineType.DUCK_DUCK_GO, **kwargs)).run(question) - - if __name__ == "__main__": asyncio.run(main()) From 6ffa22ea335196864aaf57ebbaf32a20519d41bf Mon Sep 17 00:00:00 2001 From: Abhishek0075 Date: Wed, 6 Mar 2024 10:50:31 +0530 Subject: [PATCH 025/131] black reformatting done --- examples/search_with_specific_engine.py | 30 ++++++++++++++++--------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/examples/search_with_specific_engine.py b/examples/search_with_specific_engine.py index 5fc17824b..11c9b2315 100644 --- a/examples/search_with_specific_engine.py +++ b/examples/search_with_specific_engine.py @@ -8,24 +8,34 @@ from metagpt.roles import Searcher from metagpt.tools.search_engine import SearchEngine, SearchEngineType from metagpt.config2 import Config + async def main(): question = "What are the most interesting human facts?" - + search = Config.default().search kwargs = {"api_key": search.api_key, "cse_id": search.cse_id, "proxy": None} - - if(search.api_type == SearchEngineType.DIRECT_GOOGLE): + + if search.api_type == SearchEngineType.DIRECT_GOOGLE: # Google API - await Searcher(search_engine=SearchEngine(engine=SearchEngineType.DIRECT_GOOGLE, **kwargs)).run(question) - elif(search.api_type == SearchEngineType.SERPER_GOOGLE): + await Searcher( + search_engine=SearchEngine(engine=SearchEngineType.DIRECT_GOOGLE, **kwargs) + ).run(question) + elif search.api_type == SearchEngineType.SERPER_GOOGLE: # Serper API - await Searcher(search_engine=SearchEngine(engine=SearchEngineType.SERPER_GOOGLE, **kwargs)).run(question) - elif(search.api_type == SearchEngineType.SERPAPI_GOOGLE): + await Searcher( + search_engine=SearchEngine(engine=SearchEngineType.SERPER_GOOGLE, **kwargs) + ).run(question) + elif search.api_type == SearchEngineType.SERPAPI_GOOGLE: # SerpAPI - await Searcher(search_engine=SearchEngine(engine=SearchEngineType.SERPAPI_GOOGLE, **kwargs)).run(question) + await Searcher( + search_engine=SearchEngine(engine=SearchEngineType.SERPAPI_GOOGLE, **kwargs) + ).run(question) else: # DDG API - await Searcher(search_engine=SearchEngine(engine=SearchEngineType.DUCK_DUCK_GO, **kwargs)).run(question) - + await Searcher( + search_engine=SearchEngine(engine=SearchEngineType.DUCK_DUCK_GO, **kwargs) + ).run(question) + + if __name__ == "__main__": asyncio.run(main()) From 3fee7a5368fff9218576710ff4f0b85d7f3bb8cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 6 Mar 2024 19:30:06 +0800 Subject: [PATCH 026/131] feat: +rfc197 example --- examples/reverse_engineering.py | 72 ++++++++++++++++++ metagpt/actions/rebuild_class_view.py | 2 +- metagpt/actions/rebuild_sequence_view.py | 75 ++++++++----------- metagpt/repo_parser.py | 13 +++- .../actions/test_rebuild_class_view.py | 1 - .../actions/test_rebuild_sequence_view.py | 2 + 6 files changed, 118 insertions(+), 47 deletions(-) create mode 100644 examples/reverse_engineering.py diff --git a/examples/reverse_engineering.py b/examples/reverse_engineering.py new file mode 100644 index 000000000..f80fc09e6 --- /dev/null +++ b/examples/reverse_engineering.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import asyncio +import shutil +from pathlib import Path + +import typer + +from metagpt.actions.rebuild_class_view import RebuildClassView +from metagpt.actions.rebuild_sequence_view import RebuildSequenceView +from metagpt.context import Context +from metagpt.llm import LLM +from metagpt.logs import logger +from metagpt.utils.git_repository import GitRepository +from metagpt.utils.project_repo import ProjectRepo + +app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False) + + +@app.command("", help="Python project reverse engineering.") +def startup( + project_root: str = typer.Argument( + default="", + help="Specify the root directory of the existing project for reverse engineering.", + ), + output_dir: str = typer.Option(default="", help="Specify the output directory path for reverse engineering."), +): + package_root = Path(project_root) + if not package_root.exists(): + raise FileNotFoundError(f"{project_root} not exists") + if not _is_python_package_root(package_root): + raise FileNotFoundError(f'There are no "*.py" files under "{project_root}".') + init_file = package_root / "__init__.py" # used by pyreverse + init_file_exists = init_file.exists() + if not init_file_exists: + init_file.touch() + + if not output_dir: + output_dir = package_root / "../reverse_engineering_output" + logger.info(f"output dir:{output_dir}") + try: + asyncio.run(reverse_engineering(package_root, Path(output_dir))) + finally: + if not init_file_exists: + init_file.unlink(missing_ok=True) + tmp_dir = package_root / "__dot__" + if tmp_dir.exists(): + shutil.rmtree(tmp_dir, ignore_errors=True) + + +def _is_python_package_root(package_root: Path) -> bool: + for file_path in package_root.iterdir(): + if file_path.is_file(): + if file_path.suffix == ".py": + return True + return False + + +async def reverse_engineering(package_root: Path, output_dir: Path): + ctx = Context() + ctx.git_repo = GitRepository(output_dir) + ctx.repo = ProjectRepo(ctx.git_repo) + action = RebuildClassView(name="ReverseEngineering", i_context=str(package_root), llm=LLM(), context=ctx) + await action.run() + + action = RebuildSequenceView(name="ReverseEngineering", llm=LLM(), context=ctx) + await action.run() + + +if __name__ == "__main__": + app() diff --git a/metagpt/actions/rebuild_class_view.py b/metagpt/actions/rebuild_class_view.py index 6dd5690b6..ff030ec87 100644 --- a/metagpt/actions/rebuild_class_view.py +++ b/metagpt/actions/rebuild_class_view.py @@ -76,7 +76,7 @@ class RebuildClassView(Action): path = self.context.git_repo.workdir / DATA_API_DESIGN_FILE_REPO path.mkdir(parents=True, exist_ok=True) pathname = path / self.context.git_repo.workdir.name - filename = str(pathname.with_suffix(".mmd")) + filename = str(pathname.with_suffix(".class_diagram.mmd")) async with aiofiles.open(filename, mode="w", encoding="utf-8") as writer: content = "classDiagram\n" logger.debug(content) diff --git a/metagpt/actions/rebuild_sequence_view.py b/metagpt/actions/rebuild_sequence_view.py index 227d29872..0e67de908 100644 --- a/metagpt/actions/rebuild_sequence_view.py +++ b/metagpt/actions/rebuild_sequence_view.py @@ -12,7 +12,7 @@ from __future__ import annotations import re from datetime import datetime from pathlib import Path -from typing import List, Optional +from typing import List, Optional, Set from pydantic import BaseModel from tenacity import retry, stop_after_attempt, wait_random_exponential @@ -125,7 +125,7 @@ class RebuildSequenceView(Action): if prefix in r.subject: classes.append(r) await self._rebuild_use_case(r.subject) - participants = set() + participants = await self._search_participants(split_namespace(entry.subject)[0]) class_details = [] class_views = [] for c in classes: @@ -171,7 +171,8 @@ class RebuildSequenceView(Action): sequence_view = rsp.removeprefix("```mermaid").removesuffix("```") rows = await self.graph_db.select(subject=entry.subject, predicate=GraphKeyword.HAS_SEQUENCE_VIEW) for r in rows: - await self.graph_db.delete(subject=r.subject, predicate=r.predicate, object_=r.object_) + if r.predicate == GraphKeyword.HAS_SEQUENCE_VIEW: + await self.graph_db.delete(subject=r.subject, predicate=r.predicate, object_=r.object_) await self.graph_db.insert( subject=entry.subject, predicate=GraphKeyword.HAS_SEQUENCE_VIEW, object_=sequence_view ) @@ -184,7 +185,7 @@ class RebuildSequenceView(Action): await self.graph_db.insert( subject=entry.subject, predicate=GraphKeyword.HAS_PARTICIPANT, object_=auto_namespace(c.subject) ) - await self.graph_db.save() + await self._save_sequence_view(subject=entry.subject, content=sequence_view) async def _merge_sequence_view(self, entry: SPO) -> bool: """ @@ -267,38 +268,6 @@ class RebuildSequenceView(Action): prompt_blocks.append(block) prompt = "\n---\n".join(prompt_blocks) - # class _UseCase(BaseModel): - # description: str = Field(default="...", description="Describes about what the use case to do") - # inputs: List[str] = Field(default=["input name 1", "input name 2"], - # description="Lists the input names of the use case from external sources") - # outputs: List[str] = Field(default=["output name 1", "output name 2"], - # description="Lists the output names of the use case to external sources") - # actors: List[str] = Field(default=["actor name 1", "actor name 2"], - # description="Lists the participant actors of the use case") - # steps: List[str] = Field(default=["Step 1", "Step 2"], - # description="Lists the steps about how the use case works step by step") - # reason: str = Field(default="Because ...", - # description="Explaining under what circumstances would the external system execute this use case.") - # - # - # class _UseCaseList(BaseModel): - # description: str = Field(default="...", - # description="A summary explains what the whole source code want to do") - # use_cases: List[_UseCase] = Field(default=[ - # { - # "description": "Describes about what the use case to do", - # "inputs": ["input name 1", "input name 2"], - # "outputs": ["output name 1", "output name 2"], - # "actors": ["actor name 1", "actor name 2"], - # "steps": ["Step 1", "Step 2"], - # "reason": "Because ..." - # } - # ], description="List all use cases.") - # relationship: List[str] = Field(default=["use case 1 ..."], - # description="Lists all the descriptions of relationship among these use cases") - - # rsp = await ActionNode.from_pydantic(_UseCaseList).fill(context=prompt, llm=self.llm) - rsp = await self.llm.aask( msg=prompt, system_msgs=[ @@ -327,7 +296,6 @@ class RebuildSequenceView(Action): await self.graph_db.insert( subject=ns_class_name, predicate=GraphKeyword.HAS_CLASS_USE_CASE, object_=detail.model_dump_json() ) - await self.graph_db.save() @retry( wait=wait_random_exponential(min=1, max=20), @@ -347,7 +315,6 @@ class RebuildSequenceView(Action): use_case_markdown = await self._get_class_use_cases(ns_class_name) if not use_case_markdown: # external class await self.graph_db.insert(subject=ns_class_name, predicate=GraphKeyword.HAS_SEQUENCE_VIEW, object_="") - await self.graph_db.save() return block = f"## Use Cases\n{use_case_markdown}" prompts_blocks.append(block) @@ -382,7 +349,6 @@ class RebuildSequenceView(Action): await self.graph_db.insert( subject=ns_class_name, predicate=GraphKeyword.HAS_SEQUENCE_VIEW, object_=sequence_view ) - await self.graph_db.save() async def _get_participants(self, ns_class_name: str) -> List[str]: """ @@ -574,14 +540,12 @@ class RebuildSequenceView(Action): await self.graph_db.insert( subject=entry.subject, predicate=GraphKeyword.HAS_PARTICIPANT, object_=concat_namespace("?", class_name) ) - await self.graph_db.save() return if len(participants) > 1: for r in participants: await self.graph_db.insert( subject=entry.subject, predicate=GraphKeyword.HAS_PARTICIPANT, object_=auto_namespace(r.subject) ) - await self.graph_db.save() return participant = participants[0] @@ -619,4 +583,31 @@ class RebuildSequenceView(Action): await self.graph_db.insert( subject=entry.subject, predicate=GraphKeyword.HAS_PARTICIPANT, object_=auto_namespace(participant.subject) ) - await self.graph_db.save() + await self._save_sequence_view(subject=entry.subject, content=sequence_view) + + async def _save_sequence_view(self, subject: str, content: str): + pattern = re.compile(r"[^a-zA-Z0-9]") + name = re.sub(pattern, "_", subject) + filename = Path(name).with_suffix(".sequence_diagram.mmd") + await self.context.repo.resources.data_api_design.save(filename=str(filename), content=content) + + async def _search_participants(self, filename: str) -> Set: + content = await self._get_source_code(filename) + + rsp = await self.llm.aask( + msg=content, + system_msgs=[ + "You are a tool for listing all class names used in a source file.", + "Return a markdown JSON object with: " + '- a "class_names" key containing the list of class names used in the file; ' + '- a "reasons" key lists all reason objects, each object containing a "class_name" key for class name, a "reference" key explaining the line where the class has been used.', + ], + ) + + class _Data(BaseModel): + class_names: List[str] + reasons: List + + json_blocks = parse_json_code_block(rsp) + data = _Data.model_validate_json(json_blocks[0]) + return set(data.class_names) diff --git a/metagpt/repo_parser.py b/metagpt/repo_parser.py index 15842fdfb..bc3bae662 100644 --- a/metagpt/repo_parser.py +++ b/metagpt/repo_parser.py @@ -722,14 +722,19 @@ class RepoParser(BaseModel): path = Path(path) if not path.exists(): return + init_file = path / "__init__.py" + if not init_file.exists(): + raise ValueError("Failed to import module __init__ with error:No module named __init__.") command = f"pyreverse {str(path)} -o dot" - result = subprocess.run(command, shell=True, check=True, cwd=str(path)) + output_dir = path / "__dot__" + output_dir.mkdir(parents=True, exist_ok=True) + result = subprocess.run(command, shell=True, check=True, cwd=str(output_dir)) if result.returncode != 0: raise ValueError(f"{result}") - class_view_pathname = path / "classes.dot" + class_view_pathname = output_dir / "classes.dot" class_views = await self._parse_classes(class_view_pathname) relationship_views = await self._parse_class_relationships(class_view_pathname) - packages_pathname = path / "packages.dot" + packages_pathname = output_dir / "packages.dot" class_views, relationship_views, package_root = RepoParser._repair_namespaces( class_views=class_views, relationship_views=relationship_views, path=path ) @@ -975,6 +980,8 @@ class RepoParser(BaseModel): file_ns = file_ns[0:ix] continue break + if file_ns == "": + return "" internal_ns = package[ix + 1 :] ns = mappings[file_ns] + ":" + internal_ns.replace(".", ":") return ns diff --git a/tests/metagpt/actions/test_rebuild_class_view.py b/tests/metagpt/actions/test_rebuild_class_view.py index 4414c2000..3731cd598 100644 --- a/tests/metagpt/actions/test_rebuild_class_view.py +++ b/tests/metagpt/actions/test_rebuild_class_view.py @@ -14,7 +14,6 @@ from metagpt.actions.rebuild_class_view import RebuildClassView from metagpt.llm import LLM -@pytest.mark.skip @pytest.mark.asyncio async def test_rebuild(context): action = RebuildClassView( diff --git a/tests/metagpt/actions/test_rebuild_sequence_view.py b/tests/metagpt/actions/test_rebuild_sequence_view.py index 1daea22a4..0e10e3776 100644 --- a/tests/metagpt/actions/test_rebuild_sequence_view.py +++ b/tests/metagpt/actions/test_rebuild_sequence_view.py @@ -47,6 +47,8 @@ async def test_rebuild(context, mocker): context=context, ) await action.run() + rows = await action.graph_db.select() + assert rows assert context.repo.docs.graph_repo.changed_files From 97bf135da6d78eed0a740beb8a87ca6a6fadc58e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 7 Mar 2024 12:20:58 +0800 Subject: [PATCH 027/131] feat: gemini + proxy --- metagpt/provider/google_gemini_api.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/metagpt/provider/google_gemini_api.py b/metagpt/provider/google_gemini_api.py index 09e554205..8f8f5f707 100644 --- a/metagpt/provider/google_gemini_api.py +++ b/metagpt/provider/google_gemini_api.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # @Desc : Google Gemini LLM from https://ai.google.dev/tutorials/python_quickstart +import os from typing import Optional, Union import google.generativeai as genai @@ -15,7 +16,7 @@ from google.generativeai.types.generation_types import ( ) from metagpt.configs.llm_config import LLMConfig, LLMType -from metagpt.logs import log_llm_stream +from metagpt.logs import log_llm_stream, logger from metagpt.provider.base_llm import BaseLLM from metagpt.provider.llm_provider_registry import register_provider @@ -51,6 +52,10 @@ class GeminiLLM(BaseLLM): self.llm = GeminiGenerativeModel(model_name=self.model) def __init_gemini(self, config: LLMConfig): + if config.proxy: + logger.info(f"Use proxy: {config.proxy}") + os.environ["HTTP_PROXY"] = config.proxy + os.environ["HTTP_PROXYS"] = config.proxy genai.configure(api_key=config.api_key) def _user_msg(self, msg: str, images: Optional[Union[str, list[str]]] = None) -> dict[str, str]: From f6260ec084443d9ae96dd6aec02afc595c709ce6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 7 Mar 2024 12:27:28 +0800 Subject: [PATCH 028/131] feat: gemini + proxy --- metagpt/provider/google_gemini_api.py | 6 +++++- setup.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/metagpt/provider/google_gemini_api.py b/metagpt/provider/google_gemini_api.py index 2647ab16b..bdbf7acd6 100644 --- a/metagpt/provider/google_gemini_api.py +++ b/metagpt/provider/google_gemini_api.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Desc : Google Gemini LLM from https://ai.google.dev/tutorials/python_quickstart - +import os from typing import Optional, Union import google.generativeai as genai @@ -58,6 +58,10 @@ class GeminiLLM(BaseLLM): self.llm = GeminiGenerativeModel(model_name=self.model) def __init_gemini(self, config: LLMConfig): + if config.proxy: + logger.info(f"Use proxy: {config.proxy}") + os.environ["HTTP_PROXY"] = config.proxy + os.environ["HTTP_PROXYS"] = config.proxy genai.configure(api_key=config.api_key) def _user_msg(self, msg: str, images: Optional[Union[str, list[str]]] = None) -> dict[str, str]: diff --git a/setup.py b/setup.py index 2044e577a..072237e8c 100644 --- a/setup.py +++ b/setup.py @@ -57,7 +57,7 @@ extras_require["dev"] = (["pylint~=3.0.3", "black~=23.3.0", "isort~=5.12.0", "pr setup( name="metagpt", - version="0.7.4", + version="0.7.5", description="The Multi-Agent Framework", long_description=long_description, long_description_content_type="text/markdown", From a5f278c297499fcf596f09d84bb09043ad827dc5 Mon Sep 17 00:00:00 2001 From: Abhishek0075 Date: Thu, 7 Mar 2024 10:51:35 +0530 Subject: [PATCH 029/131] Applied precommit hooks --- examples/debate.py | 18 +++++++++++++----- examples/search_with_specific_engine.py | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/examples/debate.py b/examples/debate.py index 22cfbf39e..f6c0f1313 100644 --- a/examples/debate.py +++ b/examples/debate.py @@ -5,12 +5,12 @@ Author: garylin2099 @Modified By: mashenquan, 2023-11-1. In accordance with Chapter 2.1.3 of RFC 116, modify the data type of the `send_to` value of the `Message` object; modify the argument type of `get_by_actions`. """ + import asyncio import platform from typing import Any import fire - from metagpt.actions import Action, UserRequirement from metagpt.logs import logger from metagpt.roles import Role @@ -34,7 +34,9 @@ class SpeakAloud(Action): name: str = "SpeakAloud" async def run(self, context: str, name: str, opponent_name: str): - prompt = self.PROMPT_TEMPLATE.format(context=context, name=name, opponent_name=opponent_name) + prompt = self.PROMPT_TEMPLATE.format( + context=context, name=name, opponent_name=opponent_name + ) # logger.info(prompt) rsp = await self._aask(prompt) @@ -66,7 +68,9 @@ class Debator(Role): context = "\n".join(f"{msg.sent_from}: {msg.content}" for msg in memories) # print(context) - rsp = await todo.run(context=context, name=self.name, opponent_name=self.opponent_name) + rsp = await todo.run( + context=context, name=self.name, opponent_name=self.opponent_name + ) msg = Message( content=rsp, @@ -87,7 +91,9 @@ async def debate(idea: str, investment: float = 3.0, n_round: int = 5): team = Team() team.hire([Biden, Trump]) team.invest(investment) - team.run_project(idea, send_to="Biden") # send debate topic to Biden and let him speak first + team.run_project( + idea, send_to="Biden" + ) # send debate topic to Biden and let him speak first await team.run(n_round=n_round) @@ -105,4 +111,6 @@ def main(idea: str, investment: float = 3.0, n_round: int = 10): if __name__ == "__main__": - fire.Fire(main) # run as python debate.py --idea="TOPIC" --investment=3.0 --n_round=5 + fire.Fire( + main + ) # run as python debate.py --idea="TOPIC" --investment=3.0 --n_round=5 diff --git a/examples/search_with_specific_engine.py b/examples/search_with_specific_engine.py index 11c9b2315..64d5c6e5f 100644 --- a/examples/search_with_specific_engine.py +++ b/examples/search_with_specific_engine.py @@ -4,9 +4,9 @@ """ import asyncio +from metagpt.config2 import Config from metagpt.roles import Searcher from metagpt.tools.search_engine import SearchEngine, SearchEngineType -from metagpt.config2 import Config async def main(): From 0867dad4d7b66ad098934dd9d39b3cdb261c0c00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 7 Mar 2024 17:04:44 +0800 Subject: [PATCH 030/131] fixbug: gbk UnicodeEncodeError --- metagpt/learn/skill_loader.py | 5 ++--- metagpt/utils/common.py | 16 +++++++++++---- metagpt/utils/dependency_file.py | 7 ++----- metagpt/utils/file_repository.py | 7 ++----- metagpt/utils/mermaid.py | 8 ++------ setup.py | 2 +- .../metagpt/roles/test_tutorial_assistant.py | 7 +++---- tests/metagpt/utils/test_common.py | 20 ++++++++++++++----- tests/metagpt/utils/test_git_repository.py | 5 ++--- tests/metagpt/utils/test_s3.py | 8 ++------ 10 files changed, 43 insertions(+), 42 deletions(-) diff --git a/metagpt/learn/skill_loader.py b/metagpt/learn/skill_loader.py index bcf28bb87..e98f73cf9 100644 --- a/metagpt/learn/skill_loader.py +++ b/metagpt/learn/skill_loader.py @@ -9,11 +9,11 @@ from pathlib import Path from typing import Dict, List, Optional -import aiofiles import yaml from pydantic import BaseModel, Field from metagpt.context import Context +from metagpt.utils.common import aread class Example(BaseModel): @@ -68,8 +68,7 @@ class SkillsDeclaration(BaseModel): async def load(skill_yaml_file_name: Path = None) -> "SkillsDeclaration": if not skill_yaml_file_name: skill_yaml_file_name = Path(__file__).parent.parent.parent / "docs/.well-known/skills.yaml" - async with aiofiles.open(str(skill_yaml_file_name), mode="r") as reader: - data = await reader.read(-1) + data = await aread(filename=skill_yaml_file_name) skill_data = yaml.safe_load(data) return SkillsDeclaration(**skill_data) diff --git a/metagpt/utils/common.py b/metagpt/utils/common.py index 015902c3d..aba75fbec 100644 --- a/metagpt/utils/common.py +++ b/metagpt/utils/common.py @@ -29,6 +29,7 @@ from pathlib import Path from typing import Any, Callable, List, Tuple, Union import aiofiles +import chardet import loguru import requests from PIL import Image @@ -587,14 +588,21 @@ def role_raise_decorator(func): @handle_exception -async def aread(filename: str | Path, encoding=None) -> str: +async def aread(filename: str | Path, encoding="utf-8") -> str: """Read file asynchronously.""" - async with aiofiles.open(str(filename), mode="r", encoding=encoding) as reader: - content = await reader.read() + try: + async with aiofiles.open(str(filename), mode="r", encoding=encoding) as reader: + content = await reader.read() + except UnicodeDecodeError: + async with aiofiles.open(str(filename), mode="rb") as reader: + raw = await reader.read() + result = chardet.detect(raw) + detected_encoding = result["encoding"] + content = raw.decode(detected_encoding) return content -async def awrite(filename: str | Path, data: str, encoding=None): +async def awrite(filename: str | Path, data: str, encoding="utf-8"): """Write file asynchronously.""" pathname = Path(filename) pathname.parent.mkdir(parents=True, exist_ok=True) diff --git a/metagpt/utils/dependency_file.py b/metagpt/utils/dependency_file.py index d3add1171..0a375051c 100644 --- a/metagpt/utils/dependency_file.py +++ b/metagpt/utils/dependency_file.py @@ -13,9 +13,7 @@ import re from pathlib import Path from typing import Set -import aiofiles - -from metagpt.utils.common import aread +from metagpt.utils.common import aread, awrite from metagpt.utils.exceptions import handle_exception @@ -45,8 +43,7 @@ class DependencyFile: async def save(self): """Save dependencies to the file asynchronously.""" data = json.dumps(self._dependencies) - async with aiofiles.open(str(self._filename), mode="w") as writer: - await writer.write(data) + await awrite(filename=self._filename, data=data) async def update(self, filename: Path | str, dependencies: Set[Path | str], persist=True): """Update dependencies for a file asynchronously. diff --git a/metagpt/utils/file_repository.py b/metagpt/utils/file_repository.py index d2a06963a..d19f2b705 100644 --- a/metagpt/utils/file_repository.py +++ b/metagpt/utils/file_repository.py @@ -14,11 +14,9 @@ from datetime import datetime from pathlib import Path from typing import Dict, List, Set -import aiofiles - from metagpt.logs import logger from metagpt.schema import Document -from metagpt.utils.common import aread +from metagpt.utils.common import aread, awrite from metagpt.utils.json_to_markdown import json_to_markdown @@ -55,8 +53,7 @@ class FileRepository: pathname = self.workdir / filename pathname.parent.mkdir(parents=True, exist_ok=True) content = content if content else "" # avoid `argument must be str, not None` to make it continue - async with aiofiles.open(str(pathname), mode="w") as writer: - await writer.write(content) + await awrite(filename=str(pathname), data=content) logger.info(f"save to: {str(pathname)}") if dependencies is not None: diff --git a/metagpt/utils/mermaid.py b/metagpt/utils/mermaid.py index ae3c5118f..e1d140e84 100644 --- a/metagpt/utils/mermaid.py +++ b/metagpt/utils/mermaid.py @@ -9,11 +9,9 @@ import asyncio import os from pathlib import Path -import aiofiles - from metagpt.config2 import config from metagpt.logs import logger -from metagpt.utils.common import check_cmd_exists +from metagpt.utils.common import awrite, check_cmd_exists async def mermaid_to_file(engine, mermaid_code, output_file_without_suffix, width=2048, height=2048) -> int: @@ -30,9 +28,7 @@ async def mermaid_to_file(engine, mermaid_code, output_file_without_suffix, widt if dir_name and not os.path.exists(dir_name): os.makedirs(dir_name) tmp = Path(f"{output_file_without_suffix}.mmd") - async with aiofiles.open(tmp, "w", encoding="utf-8") as f: - await f.write(mermaid_code) - # tmp.write_text(mermaid_code, encoding="utf-8") + await awrite(filename=tmp, data=mermaid_code) if engine == "nodejs": if check_cmd_exists(config.mermaid.path) != 0: diff --git a/setup.py b/setup.py index 072237e8c..813d2410c 100644 --- a/setup.py +++ b/setup.py @@ -57,7 +57,7 @@ extras_require["dev"] = (["pylint~=3.0.3", "black~=23.3.0", "isort~=5.12.0", "pr setup( name="metagpt", - version="0.7.5", + version="0.7.6", description="The Multi-Agent Framework", long_description=long_description, long_description_content_type="text/markdown", diff --git a/tests/metagpt/roles/test_tutorial_assistant.py b/tests/metagpt/roles/test_tutorial_assistant.py index c12c2b26e..732f346fd 100644 --- a/tests/metagpt/roles/test_tutorial_assistant.py +++ b/tests/metagpt/roles/test_tutorial_assistant.py @@ -6,11 +6,11 @@ @File : test_tutorial_assistant.py """ -import aiofiles import pytest from metagpt.const import TUTORIAL_PATH from metagpt.roles.tutorial_assistant import TutorialAssistant +from metagpt.utils.common import aread @pytest.mark.asyncio @@ -20,9 +20,8 @@ async def test_tutorial_assistant(language: str, topic: str, context): msg = await role.run(topic) assert TUTORIAL_PATH.exists() filename = msg.content - async with aiofiles.open(filename, mode="r", encoding="utf-8") as reader: - content = await reader.read() - assert "pip" in content + content = await aread(filename=filename) + assert "pip" in content if __name__ == "__main__": diff --git a/tests/metagpt/utils/test_common.py b/tests/metagpt/utils/test_common.py index 9b1fa878e..7c59b8072 100644 --- a/tests/metagpt/utils/test_common.py +++ b/tests/metagpt/utils/test_common.py @@ -13,7 +13,6 @@ import uuid from pathlib import Path from typing import Any, Set -import aiofiles import pytest from pydantic import BaseModel @@ -125,9 +124,7 @@ class TestGetProjectRoot: async def test_parse_data_exception(self, filename, want): pathname = Path(__file__).parent.parent.parent / "data/output_parser" / filename assert pathname.exists() - async with aiofiles.open(str(pathname), mode="r") as reader: - data = await reader.read() - + data = await aread(filename=pathname) result = OutputParser.parse_data(data=data) assert want in result @@ -198,12 +195,25 @@ class TestGetProjectRoot: @pytest.mark.asyncio async def test_read_write(self): - pathname = Path(__file__).parent / uuid.uuid4().hex / "test.tmp" + pathname = Path(__file__).parent / f"../../../workspace/unittest/{uuid.uuid4().hex}" / "test.tmp" await awrite(pathname, "ABC") data = await aread(pathname) assert data == "ABC" pathname.unlink(missing_ok=True) + @pytest.mark.asyncio + async def test_read_write_error_charset(self): + pathname = Path(__file__).parent / f"../../../workspace/unittest/{uuid.uuid4().hex}" / "test.txt" + content = "中国abc123\u27f6" + await awrite(filename=pathname, data=content) + data = await aread(filename=pathname) + assert data == content + + content = "GB18030 是中国国家标准局发布的新一代中文字符集标准,是 GBK 的升级版,支持更广泛的字符范围。" + await awrite(filename=pathname, data=content, encoding="gb2312") + data = await aread(filename=pathname, encoding="utf-8") + assert data == content + if __name__ == "__main__": pytest.main([__file__, "-s"]) diff --git a/tests/metagpt/utils/test_git_repository.py b/tests/metagpt/utils/test_git_repository.py index ea28b8f0b..480a22e24 100644 --- a/tests/metagpt/utils/test_git_repository.py +++ b/tests/metagpt/utils/test_git_repository.py @@ -10,15 +10,14 @@ import shutil from pathlib import Path -import aiofiles import pytest +from metagpt.utils.common import awrite from metagpt.utils.git_repository import GitRepository async def mock_file(filename, content=""): - async with aiofiles.open(str(filename), mode="w") as file: - await file.write(content) + await awrite(filename=filename, data=content) async def mock_repo(local_path) -> (GitRepository, Path): diff --git a/tests/metagpt/utils/test_s3.py b/tests/metagpt/utils/test_s3.py index b26ebe94d..c1a85f4ff 100644 --- a/tests/metagpt/utils/test_s3.py +++ b/tests/metagpt/utils/test_s3.py @@ -9,7 +9,6 @@ import uuid from pathlib import Path import aioboto3 -import aiofiles import pytest from metagpt.config2 import Config @@ -37,7 +36,7 @@ async def test_s3(mocker): conn = S3(s3) object_name = "unittest.bak" await conn.upload_file(bucket=s3.bucket, local_path=__file__, object_name=object_name) - pathname = (Path(__file__).parent / uuid.uuid4().hex).with_suffix(".bak") + pathname = (Path(__file__).parent / "../../../workspace/unittest" / uuid.uuid4().hex).with_suffix(".bak") pathname.unlink(missing_ok=True) await conn.download_file(bucket=s3.bucket, object_name=object_name, local_path=str(pathname)) assert pathname.exists() @@ -45,8 +44,7 @@ async def test_s3(mocker): assert url bin_data = await conn.get_object(bucket=s3.bucket, object_name=object_name) assert bin_data - async with aiofiles.open(__file__, mode="r", encoding="utf-8") as reader: - data = await reader.read() + data = await aread(filename=__file__) res = await conn.cache(data, ".bak", "script") assert "http" in res @@ -60,8 +58,6 @@ async def test_s3(mocker): except Exception: pass - await reader.close() - if __name__ == "__main__": pytest.main([__file__, "-s"]) From 0116de01b9201af7affe1ab46c79cfb76cc39df4 Mon Sep 17 00:00:00 2001 From: yzlin Date: Thu, 7 Mar 2024 21:22:44 +0800 Subject: [PATCH 031/131] disentangle planner and tool module, optimize tool module, add react mode --- metagpt/actions/mi/write_analysis_code.py | 137 ++---------- metagpt/actions/mi/write_plan.py | 4 +- metagpt/prompts/mi/ml_action.py | 56 ----- metagpt/prompts/mi/write_analysis_code.py | 61 ++---- .../prompts/{tool_types.py => task_type.py} | 14 +- metagpt/roles/mi/interpreter.py | 126 ++++++++--- metagpt/roles/role.py | 6 +- metagpt/strategy/planner.py | 40 ++++ metagpt/strategy/task_type.py | 57 +++++ metagpt/tools/libs/data_preprocess.py | 19 +- metagpt/tools/libs/email_login.py | 3 +- metagpt/tools/libs/feature_engineering.py | 25 ++- metagpt/tools/libs/gpt_v_generator.py | 5 +- metagpt/tools/libs/sd_engine.py | 3 +- metagpt/tools/libs/web_scraping.py | 3 +- metagpt/tools/tool_convert.py | 2 +- metagpt/tools/tool_data_type.py | 7 +- metagpt/tools/tool_recommend.py | 196 ++++++++++++++++++ metagpt/tools/tool_registry.py | 78 +++---- tests/metagpt/tools/test_tool_recommend.py | 66 ++++++ 20 files changed, 554 insertions(+), 354 deletions(-) delete mode 100644 metagpt/prompts/mi/ml_action.py rename metagpt/prompts/{tool_types.py => task_type.py} (87%) create mode 100644 metagpt/strategy/task_type.py create mode 100644 metagpt/tools/tool_recommend.py create mode 100644 tests/metagpt/tools/test_tool_recommend.py diff --git a/metagpt/actions/mi/write_analysis_code.py b/metagpt/actions/mi/write_analysis_code.py index 64e58920a..55a9a5eb0 100644 --- a/metagpt/actions/mi/write_analysis_code.py +++ b/metagpt/actions/mi/write_analysis_code.py @@ -7,168 +7,57 @@ from __future__ import annotations import json -from typing import Tuple from metagpt.actions import Action -from metagpt.logs import logger -from metagpt.prompts.mi.ml_action import MODEL_TRAIN_EXAMPLE, USE_ML_TOOLS_EXAMPLE from metagpt.prompts.mi.write_analysis_code import ( CHECK_DATA_PROMPT, DEBUG_REFLECTION_EXAMPLE, + INTERPRETER_SYSTEM_MSG, REFLECTION_PROMPT, + REFLECTION_SYSTEM_MSG, STRUCTUAL_PROMPT, - TOOL_RECOMMENDATION_PROMPT, ) from metagpt.schema import Message, Plan -from metagpt.tools import TOOL_REGISTRY -from metagpt.tools.tool_registry import validate_tool_names -from metagpt.tools.tool_type import ToolType from metagpt.utils.common import CodeParser, process_message, remove_comments class WriteCodeWithTools(Action): """Write code with help of local available tools. Choose tools first, then generate code to use the tools""" - use_tools: bool = True - # selected tools to choose from, listed by their names. An empty list means selection from all tools. - selected_tools: list[str] = [] - - def _get_tools_by_type(self, tool_type: str) -> dict: - """ - Retreive tools by tool type from registry, but filtered by pre-selected tool list - - Args: - tool_type (str): Tool type to retrieve from the registry - - Returns: - dict: A dict of tool name to Tool object, representing available tools under the type - """ - candidate_tools = TOOL_REGISTRY.get_tools_by_type(tool_type) - if self.selected_tools: - candidate_tool_names = set(self.selected_tools) & candidate_tools.keys() - candidate_tools = {tool_name: candidate_tools[tool_name] for tool_name in candidate_tool_names} - return candidate_tools - - async def _recommend_tool( - self, - task: str, - available_tools: dict, - ) -> dict: - """ - Recommend tools for the specified task. - - Args: - task (str): the task to recommend tools for - available_tools (dict): the available tools description - - Returns: - dict: schemas of recommended tools for the specified task - """ - prompt = TOOL_RECOMMENDATION_PROMPT.format( - current_task=task, - available_tools=available_tools, - ) - rsp = await self._aask(prompt) - rsp = CodeParser.parse_code(block=None, text=rsp) - recommend_tools = json.loads(rsp) - logger.info(f"Recommended tools: \n{recommend_tools}") - - # Parses and validates the recommended tools, for LLM might hallucinate and recommend non-existing tools - valid_tools = validate_tool_names(recommend_tools, return_tool_object=True) - - tool_schemas = {tool.name: tool.schemas for tool in valid_tools} - - return tool_schemas - - async def _prepare_tools(self, plan: Plan) -> Tuple[dict, str, str]: - """Prepare tool schemas and usage instructions according to current task - - Args: - plan (Plan): The overall plan containing task information. - - Returns: - Tuple[dict, str, str]: A tool schemas ({tool_name: tool_schema_dict}), a usage prompt for the type of tools selected, and examples of using the tools - """ - if not self.use_tools: - return {}, "", "" - - # find tool type from task type through exact match, can extend to retrieval in the future - tool_type = plan.current_task.task_type - - # prepare tool-type-specific instruction - tool_type_usage_prompt = ( - TOOL_REGISTRY.get_tool_type(tool_type).usage_prompt if TOOL_REGISTRY.has_tool_type(tool_type) else "" - ) - - # ML-specific tool usage examples - examples = "" - if plan.current_task.task_type in [ - ToolType.DATA_PREPROCESS.type_name, - ToolType.FEATURE_ENGINEERING.type_name, - ]: - examples = USE_ML_TOOLS_EXAMPLE - elif plan.current_task.task_type in [ToolType.MODEL_TRAIN.type_name]: - examples = MODEL_TRAIN_EXAMPLE - - # prepare schemas of available tools - tool_schemas = {} - available_tools = self._get_tools_by_type(tool_type) - if available_tools: - available_tools = {tool_name: tool.schemas["description"] for tool_name, tool in available_tools.items()} - tool_schemas = await self._recommend_tool(plan.current_task.instruction, available_tools) - - return tool_schemas, tool_type_usage_prompt, examples - async def _debug_with_reflection(self, context: list[Message], working_memory: list[Message]): reflection_prompt = REFLECTION_PROMPT.format( debug_example=DEBUG_REFLECTION_EXAMPLE, context=context, previous_impl=working_memory, ) - # print(reflection_prompt) - system_prompt = "You are an AI Python assistant. You will be given your previous implementation code of a task, runtime error results, and a hint to change the implementation appropriately. Write your full implementation " - rsp = await self._aask(reflection_prompt, system_msgs=[system_prompt]) + rsp = await self._aask(reflection_prompt, system_msgs=[REFLECTION_SYSTEM_MSG]) reflection = json.loads(CodeParser.parse_code(block=None, text=rsp)) return reflection["improved_impl"] async def run( self, - plan: Plan, - working_memory: list[Message] = [], + user_requirement: str, + plan_status: str = "", + tool_info: str = "", + working_memory: list[Message] = None, use_reflection: bool = False, **kwargs, ) -> str: - # prepare tool schemas and tool-type-specific instruction - tool_schemas, tool_type_usage_prompt, examples = await self._prepare_tools(plan=plan) - - # necessary components to be used in prompt - finished_tasks = plan.get_finished_tasks() - code_written = [remove_comments(task.code) for task in finished_tasks] - code_written = "\n\n".join(code_written) - task_results = [task.result for task in finished_tasks] - task_results = "\n\n".join(task_results) - - # structure prompt structual_prompt = STRUCTUAL_PROMPT.format( - user_requirement=plan.goal, - code_written=code_written, - task_results=task_results, - current_task=plan.current_task.instruction, - tool_type_usage_prompt=tool_type_usage_prompt, - tool_schemas=tool_schemas, - examples=examples, + user_requirement=user_requirement, + plan_status=plan_status, + tool_info=tool_info, ) + + working_memory = working_memory or [] context = [Message(content=structual_prompt, role="user")] + working_memory context = process_message(context) - # temp = context + working_memory - # print(*temp, sep="***\n\n***") - # LLM call if not use_reflection: - rsp = await self.llm.aask(context, **kwargs) + rsp = await self.llm.aask(context, system_msgs=[INTERPRETER_SYSTEM_MSG], **kwargs) code = CodeParser.parse_code(block=None, text=rsp) else: diff --git a/metagpt/actions/mi/write_plan.py b/metagpt/actions/mi/write_plan.py index 9e468c30f..1839de0f9 100644 --- a/metagpt/actions/mi/write_plan.py +++ b/metagpt/actions/mi/write_plan.py @@ -13,7 +13,7 @@ from typing import Tuple from metagpt.actions import Action from metagpt.logs import logger from metagpt.schema import Message, Plan, Task -from metagpt.tools import TOOL_REGISTRY +from metagpt.strategy.task_type import TaskType from metagpt.utils.common import CodeParser @@ -43,7 +43,7 @@ class WritePlan(Action): async def run(self, context: list[Message], max_tasks: int = 5, use_tools: bool = False) -> str: task_type_desc = "\n".join( - [f"- **{tool_type.name}**: {tool_type.desc}" for tool_type in TOOL_REGISTRY.get_tool_types().values()] + [f"- **{tt.type_name}**: {tt.value.desc}" for tt in TaskType] ) # task type are binded with tool type now, should be improved in the future prompt = self.PROMPT_TEMPLATE.format( context="\n".join([str(ct) for ct in context]), max_tasks=max_tasks, task_type_desc=task_type_desc diff --git a/metagpt/prompts/mi/ml_action.py b/metagpt/prompts/mi/ml_action.py deleted file mode 100644 index 4d769379a..000000000 --- a/metagpt/prompts/mi/ml_action.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# @Time : 2023/11/24 15:43 -# @Author : lidanyang -# @File : ml_action -# @Desc : -MODEL_TRAIN_EXAMPLE = """ -when current task is "train a lightgbm model on training data", the code can be like: -```python -# Step 1: check data type and convert to numeric -obj_cols = train.select_dtypes(include='object').columns.tolist() - -for col in obj_cols: - encoder = LabelEncoder() - train[col] = encoder.fit_transform(train[col].unique().tolist() + ['unknown']) - test[col] = test[col].apply(lambda x: x if x in encoder.classes_ else 'unknown') - test[col] = encoder.transform(test[col]) - -# Step 2: train lightgbm model -model = LGBMClassifier() -model.fit(train, y_train) -```end - -# Constraints: -- Ensure the output new code is executable in the same Jupyter notebook with previous tasks code have been executed. -""" - -USE_ML_TOOLS_EXAMPLE = """ -when current task is "do data preprocess, like fill missing value, handle outliers, etc.", the code can be like: -```python -# Step 1: fill missing value -# Tools used: ['FillMissingValue'] -from metagpt.tools.libs.data_preprocess import FillMissingValue - -train_processed = train.copy() -test_processed = test.copy() -num_cols = train_processed.select_dtypes(include='number').columns.tolist() -if 'label' in num_cols: - num_cols.remove('label') -fill_missing_value = FillMissingValue(features=num_cols, strategy='mean') -fill_missing_value.fit(train_processed) -train_processed = fill_missing_value.transform(train_processed) -test_processed = fill_missing_value.transform(test_processed) - -# Step 2: handle outliers -for col in num_cols: - low, high = train_processed[col].quantile([0.01, 0.99]) - train_processed[col] = train_processed[col].clip(low, high) - test_processed[col] = test_processed[col].clip(low, high) -```end - -# Constraints: -- Ensure the output new code is executable in the same Jupyter notebook with previous tasks code have been executed. -- Always prioritize using pre-defined tools for the same functionality. -- Always copy the DataFrame before processing it and use the copy to process. -""" diff --git a/metagpt/prompts/mi/write_analysis_code.py b/metagpt/prompts/mi/write_analysis_code.py index 12ca9d4ae..e8ffedce4 100644 --- a/metagpt/prompts/mi/write_analysis_code.py +++ b/metagpt/prompts/mi/write_analysis_code.py @@ -1,33 +1,19 @@ +INTERPRETER_SYSTEM_MSG = """As a data scientist, you need to help user to achieve their goal step by step in a continuous Jupyter notebook. Since it is a notebook environment, don't use asyncio.run. Instead, use await if you need to call an async function.""" + STRUCTUAL_PROMPT = """ -# Background -As a data scientist, you need to help user to achieve their goal [{user_requirement}] step-by-step in an continuous Jupyter notebook. Since it is a notebook environment, don't use asyncio.run. Instead, use await if you need to call an async function. +# User Requirement +{user_requirement} -# Finished Tasks -## code -```python -{code_written} -``` +# Plan Status +{plan_status} -## execution result -{task_results} +# Tool Info +{tool_info} -# Current Task -{current_task} - -# Instruction -Write complete code for 'Current Task'. And avoid duplicating code from 'Finished Tasks', such as repeated import of packages, reading data, etc. -Specifically, {tool_type_usage_prompt} - -# Capabilities -- You can utilize pre-defined tools in any code lines from 'Available Tools' in the form of Python class or function. -- You can freely combine the use of any other public packages, like sklearn, numpy, pandas, etc.. - -# Available Tools: -Each tool is described in JSON format. When you call a tool, import the tool from its path first. -{tool_schemas} - -# Examples -{examples} +# Constraints +- Take on Current Task if it is in Plan Status, otherwise, tackle User Requirement directly. +- Ensure the output new code is executable in the same Jupyter notebook as the previous executed code. +- Always prioritize using pre-defined tools for the same functionality. # Output Output code in the following format: @@ -36,6 +22,8 @@ your code ``` """ +REFLECTION_SYSTEM_MSG = """You are an AI Python assistant. You will be given your previous implementation code of a task, runtime error results, and a hint to change the implementation appropriately. Write your full implementation.""" + DEBUG_REFLECTION_EXAMPLE = ''' [previous impl]: assistant: @@ -122,24 +110,3 @@ DATA_INFO = """ Latest data info after previous tasks: {info} """ - -TOOL_RECOMMENDATION_PROMPT = """ -## User Requirement: -{current_task} - -## Task -Recommend up to five tools from 'Available Tools' that can help solve the 'User Requirement'. - -## Available Tools: -{available_tools} - -## Tool Selection and Instructions: -- Select tools most relevant to completing the 'User Requirement'. -- If you believe that no tools are suitable, indicate with an empty list. -- Only list the names of the tools, not the full schema of each tool. -- Ensure selected tools are listed in 'Available Tools'. -- Output a json list of tool names: -```json -["tool_name1", "tool_name2", ...] -``` -""" diff --git a/metagpt/prompts/tool_types.py b/metagpt/prompts/task_type.py similarity index 87% rename from metagpt/prompts/tool_types.py rename to metagpt/prompts/task_type.py index 8728d22f3..5b1ffc744 100644 --- a/metagpt/prompts/tool_types.py +++ b/metagpt/prompts/task_type.py @@ -1,11 +1,11 @@ -# Prompt for using tools of "eda" type +# Prompt for taking on "eda" tasks EDA_PROMPT = """ The current task is about exploratory data analysis, please note the following: - Distinguish column types with `select_dtypes` for tailored analysis and visualization, such as correlation. - Remember to `import numpy as np` before using Numpy functions. """ -# Prompt for using tools of "data_preprocess" type +# Prompt for taking on "data_preprocess" tasks DATA_PREPROCESS_PROMPT = """ The current task is about data preprocessing, please note the following: - Monitor data types per column, applying appropriate methods. @@ -15,9 +15,10 @@ The current task is about data preprocessing, please note the following: - Prefer alternatives to one-hot encoding for categorical data. - Only encode or scale necessary columns to allow for potential feature-specific engineering tasks (like time_extract, binning, extraction, etc.) later. - Each step do data preprocessing to train, must do same for test separately at the same time. +- Always copy the DataFrame before processing it and use the copy to process. """ -# Prompt for using tools of "feature_engineering" type +# Prompt for taking on "feature_engineering" tasks FEATURE_ENGINEERING_PROMPT = """ The current task is about feature engineering. when performing it, please adhere to the following principles: - Generate as diverse features as possible to improve the model's performance step-by-step. @@ -27,9 +28,10 @@ The current task is about feature engineering. when performing it, please adhere - Each feature engineering operation performed on the train set must also applies to the test separately at the same time. - Avoid using the label column to create features, except for cat encoding. - Use the data from previous task result if exist, do not mock or reload data yourself. +- Always copy the DataFrame before processing it and use the copy to process. """ -# Prompt for using tools of "model_train" type +# Prompt for taking on "model_train" tasks MODEL_TRAIN_PROMPT = """ The current task is about training a model, please ensure high performance: - Keep in mind that your user prioritizes results and is highly focused on model performance. So, when needed, feel free to use models of any complexity to improve effectiveness, such as XGBoost, CatBoost, etc. @@ -38,14 +40,14 @@ The current task is about training a model, please ensure high performance: - Set suitable hyperparameters for the model, make metrics as high as possible. """ -# Prompt for using tools of "model_evaluate" type +# Prompt for taking on "model_evaluate" tasks MODEL_EVALUATE_PROMPT = """ The current task is about evaluating a model, please note the following: - Ensure that the evaluated data is same processed as the training data. If not, remember use object in 'Done Tasks' to transform the data. - Use trained model from previous task result directly, do not mock or reload model yourself. """ -# Prompt for using tools of "vision" type +# Prompt for taking on "image2webpage" tasks IMAGE2WEBPAGE_PROMPT = """ The current task is about converting image into webpage code. please note the following: - Single-Step Code Generation: Execute the entire code generation process in a single step, encompassing HTML, CSS, and JavaScript. Avoid fragmenting the code generation into multiple separate steps to maintain consistency and simplify the development workflow. diff --git a/metagpt/roles/mi/interpreter.py b/metagpt/roles/mi/interpreter.py index 0a99c0e69..58b38ac43 100644 --- a/metagpt/roles/mi/interpreter.py +++ b/metagpt/roles/mi/interpreter.py @@ -1,6 +1,9 @@ from __future__ import annotations -from pydantic import Field +import json +from typing import Literal, Union + +from pydantic import Field, model_validator from metagpt.actions.mi.ask_review import ReviewConst from metagpt.actions.mi.execute_nb_code import ExecuteNbCode @@ -9,40 +12,80 @@ from metagpt.logs import logger from metagpt.prompts.mi.write_analysis_code import DATA_INFO from metagpt.roles import Role from metagpt.schema import Message, Task, TaskResult -from metagpt.tools.tool_type import ToolType +from metagpt.strategy.task_type import TaskType +from metagpt.tools.tool_recommend import BM25ToolRecommender, ToolRecommender +from metagpt.utils.common import CodeParser + +REACT_THINK_PROMPT = """ +# User Requirement +{user_requirement} +# Context +{context} + +Output a json following the format: +```json +{{ + "thoughts": str = "Thoughts on current situation, reflect on how you should proceed to fulfill the user requirement", + "state": bool = "Decide whether you need to take more actions to complete the user requirement. Return true if you think so. Return false if you think the requirement has been completely fulfilled." +}} +``` +""" class Interpreter(Role): name: str = "Ivy" profile: str = "Interpreter" auto_run: bool = True - use_tools: bool = False + use_plan: bool = True use_reflection: bool = False execute_code: ExecuteNbCode = Field(default_factory=ExecuteNbCode, exclude=True) - tools: list[str] = [] + tools: Union[str, list[str]] = [] + tool_recommender: ToolRecommender = None + react_mode: Literal["plan_and_act", "react"] = "plan_and_act" + max_react_loop: int = 10 # used for react mode - def __init__( - self, - auto_run=True, - use_tools=False, - tools=[], - **kwargs, - ): - super().__init__(auto_run=auto_run, use_tools=use_tools, tools=tools, **kwargs) - self._set_react_mode(react_mode="plan_and_act", auto_run=auto_run, use_tools=use_tools) - if use_tools and tools: - from metagpt.tools.tool_registry import ( - validate_tool_names, # import upon use - ) - - self.tools = validate_tool_names(tools) - logger.info(f"will only use {self.tools} as tools") + @model_validator(mode="after") + def set_plan_and_tool(self) -> "Interpreter": + self._set_react_mode(react_mode=self.react_mode, max_react_loop=self.max_react_loop, auto_run=self.auto_run) + self.use_plan = ( + self.react_mode == "plan_and_act" + ) # create a flag for convenience, overwrite any passed-in value + if self.tools: + self.tool_recommender = BM25ToolRecommender(tools=self.tools) + self.set_actions([WriteCodeWithTools]) + return self @property def working_memory(self): return self.rc.working_memory + async def _think(self) -> bool: + """Useful in 'react' mode. Use LLM to decide whether and what to do next.""" + user_requirement = self.get_memories()[0].content + context = self.working_memory.get() + + if not context: + # just started the run, we need action certainly + self.working_memory.add(self.get_memories()[0]) # add user requirement to working memory + self._set_state(0) + return True + + prompt = REACT_THINK_PROMPT.format(user_requirement=user_requirement, context=context) + rsp = await self.llm.aask(prompt) + rsp_dict = json.loads(CodeParser.parse_code(block=None, text=rsp)) + self.working_memory.add(Message(content=rsp_dict["thoughts"], role="assistant")) + need_action = rsp_dict["state"] + self._set_state(0) if need_action else self._set_state(-1) + + return need_action + + async def _act(self) -> Message: + """Useful in 'react' mode. Return a Message conforming to Role._act interface.""" + code, _, _ = await self._write_and_exec_code() + return Message(content=code, role="assistant", cause_by=WriteCodeWithTools) + async def _act_on_task(self, current_task: Task) -> TaskResult: + """Useful in 'plan_and_act' mode. Wrap the output in a TaskResult for review and confirmation.""" code, result, is_success = await self._write_and_exec_code() task_result = TaskResult(code=code, result=result, is_success=is_success) return task_result @@ -51,11 +94,25 @@ class Interpreter(Role): counter = 0 success = False + # plan info + plan_status = self.planner.get_plan_status() if self.use_plan else "" + + # tool info + if self.tools: + context = ( + self.working_memory.get()[-1].content if self.working_memory.get() else "" + ) # thoughts from _think stage in 'react' mode + plan = self.planner.plan if self.use_plan else None + tool_info = await self.tool_recommender.get_recommended_tool_info(context=context, plan=plan) + else: + tool_info = "" + + # data info await self._check_data() while not success and counter < max_retry: ### write code ### - code, cause_by = await self._write_code(counter) + code, cause_by = await self._write_code(counter, plan_status, tool_info) self.working_memory.add(Message(content=code, role="assistant", cause_by=cause_by)) @@ -76,22 +133,33 @@ class Interpreter(Role): return code, result, success - async def _write_code(self, counter): - todo = WriteCodeWithTools(use_tools=self.use_tools, selected_tools=self.tools) + async def _write_code( + self, + counter, + plan_status="", + tool_info="", + ): + todo = WriteCodeWithTools() logger.info(f"ready to {todo.name}") use_reflection = counter > 0 and self.use_reflection + + user_requirement = self.get_memories()[0].content + code = await todo.run( - plan=self.planner.plan, working_memory=self.working_memory.get(), use_reflection=use_reflection + user_requirement=user_requirement, + plan_status=plan_status, + tool_info=tool_info, + working_memory=self.working_memory.get(), + use_reflection=use_reflection, ) return code, todo async def _check_data(self): - current_task = self.planner.plan.current_task - if current_task.task_type not in [ - ToolType.DATA_PREPROCESS.type_name, - ToolType.FEATURE_ENGINEERING.type_name, - ToolType.MODEL_TRAIN.type_name, + if not self.use_plan or self.planner.plan.current_task.task_type not in [ + TaskType.DATA_PREPROCESS.type_name, + TaskType.FEATURE_ENGINEERING.type_name, + TaskType.MODEL_TRAIN.type_name, ]: return logger.info("Check updated data") diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index 3938664ba..e0c33f899 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -283,7 +283,7 @@ class Role(SerializationMixin, ContextMixin, BaseModel): self.actions.append(i) self.states.append(f"{len(self.actions)}. {action}") - def _set_react_mode(self, react_mode: str, max_react_loop: int = 1, auto_run: bool = True, use_tools: bool = False): + def _set_react_mode(self, react_mode: str, max_react_loop: int = 1, auto_run: bool = True): """Set strategy of the Role reacting to observed Message. Variation lies in how this Role elects action to perform during the _think stage, especially if it is capable of multiple Actions. @@ -304,9 +304,7 @@ class Role(SerializationMixin, ContextMixin, BaseModel): if react_mode == RoleReactMode.REACT: self.rc.max_react_loop = max_react_loop elif react_mode == RoleReactMode.PLAN_AND_ACT: - self.planner = Planner( - goal=self.goal, working_memory=self.rc.working_memory, auto_run=auto_run, use_tools=use_tools - ) + self.planner = Planner(goal=self.goal, working_memory=self.rc.working_memory, auto_run=auto_run) def _watch(self, actions: Iterable[Type[Action]] | Iterable[Action]): """Watch Actions of interest. Role will select Messages caused by these Actions from its personal message diff --git a/metagpt/strategy/planner.py b/metagpt/strategy/planner.py index a0f73f70c..35f2fbdf2 100644 --- a/metagpt/strategy/planner.py +++ b/metagpt/strategy/planner.py @@ -13,6 +13,8 @@ from metagpt.actions.mi.write_plan import ( from metagpt.logs import logger from metagpt.memory import Memory from metagpt.schema import Message, Plan, Task, TaskResult +from metagpt.strategy.task_type import TaskType +from metagpt.utils.common import remove_comments STRUCTURAL_CONTEXT = """ ## User Requirement @@ -25,6 +27,24 @@ STRUCTURAL_CONTEXT = """ {current_task} """ +PLAN_STATUS = """ +## Finished Tasks +### code +```python +{code_written} +``` + +### execution result +{task_results} + +## Current Task +{current_task} + +## Task Guidance +Write complete code for 'Current Task'. And avoid duplicating code from 'Finished Tasks', such as repeated import of packages, reading data, etc. +Specifically, {guidance} +""" + class Planner(BaseModel): plan: Plan @@ -136,3 +156,23 @@ class Planner(BaseModel): context_msg = [Message(content=context, role="user")] return context_msg + self.working_memory.get() + + def get_plan_status(self) -> str: + # prepare components of a plan status + finished_tasks = self.plan.get_finished_tasks() + code_written = [remove_comments(task.code) for task in finished_tasks] + code_written = "\n\n".join(code_written) + task_results = [task.result for task in finished_tasks] + task_results = "\n\n".join(task_results) + task_type_name = self.current_task.task_type.upper() + guidance = TaskType[task_type_name].value.guidance if hasattr(TaskType, task_type_name) else "" + + # combine components in a prompt + prompt = PLAN_STATUS.format( + code_written=code_written, + task_results=task_results, + current_task=self.current_task.instruction, + guidance=guidance, + ) + + return prompt diff --git a/metagpt/strategy/task_type.py b/metagpt/strategy/task_type.py new file mode 100644 index 000000000..28a86f100 --- /dev/null +++ b/metagpt/strategy/task_type.py @@ -0,0 +1,57 @@ +from enum import Enum + +from pydantic import BaseModel + +from metagpt.prompts.task_type import ( + DATA_PREPROCESS_PROMPT, + EDA_PROMPT, + FEATURE_ENGINEERING_PROMPT, + IMAGE2WEBPAGE_PROMPT, + MODEL_EVALUATE_PROMPT, + MODEL_TRAIN_PROMPT, +) + + +class TaskTypeDef(BaseModel): + name: str + desc: str = "" + guidance: str = "" + + +class TaskType(Enum): + EDA = TaskTypeDef( + name="eda", + desc="For performing exploratory data analysis", + guidance=EDA_PROMPT, + ) + DATA_PREPROCESS = TaskTypeDef( + name="data_preprocess", + desc="For preprocessing dataset in a data analysis or machine learning task ONLY," + "general data operation doesn't fall into this type", + guidance=DATA_PREPROCESS_PROMPT, + ) + FEATURE_ENGINEERING = TaskTypeDef( + name="feature_engineering", + desc="Only for creating new columns for input data.", + guidance=FEATURE_ENGINEERING_PROMPT, + ) + MODEL_TRAIN = TaskTypeDef( + name="model_train", + desc="Only for training model.", + guidance=MODEL_TRAIN_PROMPT, + ) + MODEL_EVALUATE = TaskTypeDef( + name="model_evaluate", + desc="Only for evaluating model.", + guidance=MODEL_EVALUATE_PROMPT, + ) + IMAGE2WEBPAGE = TaskTypeDef( + name="image2webpage", + desc="For converting image into webpage code.", + guidance=IMAGE2WEBPAGE_PROMPT, + ) + OTHER = TaskTypeDef(name="other", desc="Any tasks not in the defined categories") + + @property + def type_name(self): + return self.value.name diff --git a/metagpt/tools/libs/data_preprocess.py b/metagpt/tools/libs/data_preprocess.py index 7a3d019bf..3c8ebcc02 100644 --- a/metagpt/tools/libs/data_preprocess.py +++ b/metagpt/tools/libs/data_preprocess.py @@ -16,9 +16,8 @@ from sklearn.preprocessing import ( ) from metagpt.tools.tool_registry import register_tool -from metagpt.tools.tool_type import ToolType -TOOL_TYPE = ToolType.DATA_PREPROCESS.type_name +TAGS = ["data preprocessing", "machine learning"] class MLProcess: @@ -85,7 +84,7 @@ class DataPreprocessTool(MLProcess): return new_df -@register_tool(tool_type=TOOL_TYPE) +@register_tool(tags=TAGS) class FillMissingValue(DataPreprocessTool): """ Completing missing values with simple strategies. @@ -106,7 +105,7 @@ class FillMissingValue(DataPreprocessTool): self.model = SimpleImputer(strategy=strategy, fill_value=fill_value) -@register_tool(tool_type=TOOL_TYPE) +@register_tool(tags=TAGS) class MinMaxScale(DataPreprocessTool): """ Transform features by scaling each feature to a range, which is (0, 1). @@ -117,7 +116,7 @@ class MinMaxScale(DataPreprocessTool): self.model = MinMaxScaler() -@register_tool(tool_type=TOOL_TYPE) +@register_tool(tags=TAGS) class StandardScale(DataPreprocessTool): """ Standardize features by removing the mean and scaling to unit variance. @@ -128,7 +127,7 @@ class StandardScale(DataPreprocessTool): self.model = StandardScaler() -@register_tool(tool_type=TOOL_TYPE) +@register_tool(tags=TAGS) class MaxAbsScale(DataPreprocessTool): """ Scale each feature by its maximum absolute value. @@ -139,7 +138,7 @@ class MaxAbsScale(DataPreprocessTool): self.model = MaxAbsScaler() -@register_tool(tool_type=TOOL_TYPE) +@register_tool(tags=TAGS) class RobustScale(DataPreprocessTool): """ Apply the RobustScaler to scale features using statistics that are robust to outliers. @@ -150,7 +149,7 @@ class RobustScale(DataPreprocessTool): self.model = RobustScaler() -@register_tool(tool_type=TOOL_TYPE) +@register_tool(tags=TAGS) class OrdinalEncode(DataPreprocessTool): """ Encode categorical features as ordinal integers. @@ -161,7 +160,7 @@ class OrdinalEncode(DataPreprocessTool): self.model = OrdinalEncoder() -@register_tool(tool_type=TOOL_TYPE) +@register_tool(tags=TAGS) class OneHotEncode(DataPreprocessTool): """ Apply one-hot encoding to specified categorical columns, the original columns will be dropped. @@ -180,7 +179,7 @@ class OneHotEncode(DataPreprocessTool): return new_df -@register_tool(tool_type=TOOL_TYPE) +@register_tool(tags=TAGS) class LabelEncode(DataPreprocessTool): """ Apply label encoding to specified categorical columns in-place. diff --git a/metagpt/tools/libs/email_login.py b/metagpt/tools/libs/email_login.py index 79734665e..757ac2b87 100644 --- a/metagpt/tools/libs/email_login.py +++ b/metagpt/tools/libs/email_login.py @@ -1,7 +1,6 @@ from imap_tools import MailBox from metagpt.tools.tool_registry import register_tool -from metagpt.tools.tool_type import ToolType # Define a dictionary mapping email domains to their IMAP server addresses IMAP_SERVERS = { @@ -24,7 +23,7 @@ IMAP_SERVERS = { } -@register_tool(tool_type=ToolType.EMAIL_LOGIN.type_name) +@register_tool() def email_login_imap(email_address, email_password): """ Use imap_tools package to log in to your email (the email that supports IMAP protocol) to verify and return the account object. diff --git a/metagpt/tools/libs/feature_engineering.py b/metagpt/tools/libs/feature_engineering.py index 40bfb2fc7..3013e1594 100644 --- a/metagpt/tools/libs/feature_engineering.py +++ b/metagpt/tools/libs/feature_engineering.py @@ -19,12 +19,11 @@ from sklearn.preprocessing import KBinsDiscretizer, PolynomialFeatures from metagpt.tools.libs.data_preprocess import MLProcess from metagpt.tools.tool_registry import register_tool -from metagpt.tools.tool_type import ToolType -TOOL_TYPE = ToolType.FEATURE_ENGINEERING.type_name +TAGS = ["feature engineering", "machine learning"] -@register_tool(tool_type=TOOL_TYPE) +@register_tool(tags=TAGS) class PolynomialExpansion(MLProcess): """ Add polynomial and interaction features from selected numeric columns to input DataFrame. @@ -67,7 +66,7 @@ class PolynomialExpansion(MLProcess): return new_df -@register_tool(tool_type=TOOL_TYPE) +@register_tool(tags=TAGS) class CatCount(MLProcess): """ Add value counts of a categorical column as new feature. @@ -92,7 +91,7 @@ class CatCount(MLProcess): return new_df -@register_tool(tool_type=TOOL_TYPE) +@register_tool(tags=TAGS) class TargetMeanEncoder(MLProcess): """ Encode a categorical column by the mean of the label column, and adds the result as a new feature. @@ -119,7 +118,7 @@ class TargetMeanEncoder(MLProcess): return new_df -@register_tool(tool_type=TOOL_TYPE) +@register_tool(tags=TAGS) class KFoldTargetMeanEncoder(MLProcess): """ Add a new feature to the DataFrame by k-fold mean encoding of a categorical column using the label column. @@ -159,7 +158,7 @@ class KFoldTargetMeanEncoder(MLProcess): return new_df -@register_tool(tool_type=TOOL_TYPE) +@register_tool(tags=TAGS) class CatCross(MLProcess): """ Add pairwise crossed features and convert them to numerical features. @@ -216,7 +215,7 @@ class CatCross(MLProcess): return new_df -@register_tool(tool_type=TOOL_TYPE) +@register_tool(tags=TAGS) class GroupStat(MLProcess): """ Aggregate specified column in a DataFrame grouped by another column, adding new features named '__by_'. @@ -248,7 +247,7 @@ class GroupStat(MLProcess): return new_df -@register_tool(tool_type=TOOL_TYPE) +@register_tool(tags=TAGS) class SplitBins(MLProcess): """ Inplace binning of continuous data into intervals, returning integer-encoded bin identifiers directly. @@ -276,7 +275,7 @@ class SplitBins(MLProcess): return new_df -# @register_tool(tool_type=TOOL_TYPE) +# @register_tool(tags=TAGS) class ExtractTimeComps(MLProcess): """ Extract time components from a datetime column and add them as new features. @@ -316,7 +315,7 @@ class ExtractTimeComps(MLProcess): return new_df -@register_tool(tool_type=TOOL_TYPE) +@register_tool(tags=TAGS) class GeneralSelection(MLProcess): """ Drop all nan feats and feats with only one unique value. @@ -349,7 +348,7 @@ class GeneralSelection(MLProcess): # skip for now because lgb is needed -# @register_tool(tool_type=TOOL_TYPE) +# @register_tool(tags=TAGS) class TreeBasedSelection(MLProcess): """ Select features based on tree-based model and remove features with low importance. @@ -403,7 +402,7 @@ class TreeBasedSelection(MLProcess): return new_df -@register_tool(tool_type=TOOL_TYPE) +@register_tool(tags=TAGS) class VarianceBasedSelection(MLProcess): """ Select features based on variance and remove features with low variance. diff --git a/metagpt/tools/libs/gpt_v_generator.py b/metagpt/tools/libs/gpt_v_generator.py index 3b17fc596..abf5d1986 100644 --- a/metagpt/tools/libs/gpt_v_generator.py +++ b/metagpt/tools/libs/gpt_v_generator.py @@ -10,7 +10,6 @@ from pathlib import Path from metagpt.const import DEFAULT_WORKSPACE_ROOT from metagpt.tools.tool_registry import register_tool -from metagpt.tools.tool_type import ToolType from metagpt.utils.common import encode_image ANALYZE_LAYOUT_PROMPT = """You are now a UI/UX designer, please generate layout information for this image: @@ -28,9 +27,7 @@ As the design pays tribute to large companies, sometimes it is normal for some c Now, please generate the corresponding webpage code including HTML, CSS and JavaScript:""" -@register_tool( - tool_type=ToolType.IMAGE2WEBPAGE.type_name, include_functions=["__init__", "generate_webpages", "save_webpages"] -) +@register_tool(include_functions=["__init__", "generate_webpages", "save_webpages"]) class GPTvGenerator: """Class for generating webpages at once. diff --git a/metagpt/tools/libs/sd_engine.py b/metagpt/tools/libs/sd_engine.py index 347f4a430..0e3042f5c 100644 --- a/metagpt/tools/libs/sd_engine.py +++ b/metagpt/tools/libs/sd_engine.py @@ -18,7 +18,6 @@ from PIL import Image, PngImagePlugin from metagpt.const import SD_OUTPUT_FILE_REPO, SOURCE_ROOT from metagpt.logs import logger from metagpt.tools.tool_registry import register_tool -from metagpt.tools.tool_type import ToolType payload = { "prompt": "", @@ -55,7 +54,7 @@ default_negative_prompt = "(easynegative:0.8),black, dark,Low resolution" @register_tool( - tool_type=ToolType.STABLE_DIFFUSION.type_name, + tags=["text2image", "multimodal"], include_functions=["__init__", "simple_run_t2i", "run_t2i", "construct_payload", "save"], ) class SDEngine: diff --git a/metagpt/tools/libs/web_scraping.py b/metagpt/tools/libs/web_scraping.py index d01e69d09..bc34b1306 100644 --- a/metagpt/tools/libs/web_scraping.py +++ b/metagpt/tools/libs/web_scraping.py @@ -1,9 +1,8 @@ from metagpt.tools.tool_registry import register_tool -from metagpt.tools.tool_type import ToolType from metagpt.tools.web_browser_engine_playwright import PlaywrightWrapper -@register_tool(tool_type=ToolType.WEBSCRAPING.type_name) +@register_tool(tags=["web scraping", "web"]) async def scrape_web_playwright(url): """ Asynchronously Scrape and save the HTML structure and inner text content of a web page using Playwright. diff --git a/metagpt/tools/tool_convert.py b/metagpt/tools/tool_convert.py index fc29d0693..b837d8288 100644 --- a/metagpt/tools/tool_convert.py +++ b/metagpt/tools/tool_convert.py @@ -3,7 +3,7 @@ import inspect from metagpt.utils.parse_docstring import GoogleDocstringParser, remove_spaces -def convert_code_to_tool_schema(obj, include: list[str] = []): +def convert_code_to_tool_schema(obj, include: list[str] = None): docstring = inspect.getdoc(obj) assert docstring, "no docstring found for the objects, skip registering" diff --git a/metagpt/tools/tool_data_type.py b/metagpt/tools/tool_data_type.py index 0ae46fa5c..1a31b03e7 100644 --- a/metagpt/tools/tool_data_type.py +++ b/metagpt/tools/tool_data_type.py @@ -1,12 +1,6 @@ from pydantic import BaseModel -class ToolTypeDef(BaseModel): - name: str - desc: str = "" - usage_prompt: str = "" - - class ToolSchema(BaseModel): description: str @@ -16,3 +10,4 @@ class Tool(BaseModel): path: str schemas: dict = {} code: str = "" + tags: list[str] = [] diff --git a/metagpt/tools/tool_recommend.py b/metagpt/tools/tool_recommend.py new file mode 100644 index 000000000..9e06a67b4 --- /dev/null +++ b/metagpt/tools/tool_recommend.py @@ -0,0 +1,196 @@ +from __future__ import annotations + +import json +from typing import Any + +import jieba +import numpy as np +from pydantic import BaseModel, field_validator +from rank_bm25 import BM25Okapi + +from metagpt.actions import Action +from metagpt.logs import logger +from metagpt.schema import Plan +from metagpt.tools import TOOL_REGISTRY +from metagpt.tools.tool_data_type import Tool +from metagpt.tools.tool_registry import validate_tool_names +from metagpt.utils.common import CodeParser + +TOOL_INFO_PROMPT = """ +## Capabilities +- You can utilize pre-defined tools in any code lines from 'Available Tools' in the form of Python class or function. +- You can freely combine the use of any other public packages, like sklearn, numpy, pandas, etc.. + +## Available Tools: +Each tool is described in JSON format. When you call a tool, import the tool from its path first. +{tool_schemas} +""" + + +TOOL_RECOMMENDATION_PROMPT = """ +## User Requirement: +{current_task} + +## Task +Recommend up to {topk} tools from 'Available Tools' that can help solve the 'User Requirement'. + +## Available Tools: +{available_tools} + +## Tool Selection and Instructions: +- Select tools most relevant to completing the 'User Requirement'. +- If you believe that no tools are suitable, indicate with an empty list. +- Only list the names of the tools, not the full schema of each tool. +- Ensure selected tools are listed in 'Available Tools'. +- Output a json list of tool names: +```json +["tool_name1", "tool_name2", ...] +``` +""" + + +class RecommendTool(Action): + async def run(self, prompt): + return await self._aask(prompt) + + +class ToolRecommender(BaseModel): + """ + The default ToolRecommender: + 1. Recall: If plan exists, use exact match between task type and tool type to recall tools; + If plan doesn't exist (e.g. we use ReAct), return all user-specified tools; + 2. Rank: Use LLM to select final candidates from recalled set. + """ + + tools: dict[str, Tool] = {} + force: bool = False + + @field_validator("tools", mode="before") + @classmethod + def validate_tools(cls, v: list[str]) -> dict[str, Tool]: + if v == [""]: + return TOOL_REGISTRY.get_all_tools() + else: + return validate_tool_names(v) + + async def recommend_tools( + self, context: str = "", plan: Plan = None, recall_topk: int = 20, topk: int = 5 + ) -> list[Tool]: + """ + Recommends a list of tools based on the given context and plan. The recommendation process includes two stages: recall from a large pool and rank the recalled tools to select the final set. + + Args: + context (str): The context for tool recommendation. + plan (Plan): The plan for tool recommendation. + recall_topk (int): The number of tools to recall in the initial step. + topk (int): The number of tools to return after rank as final recommendations. + + Returns: + list[Tool]: A list of recommended tools. + """ + + if not self.tools: + return [] + + if self.force or (not context and not plan): + # directly use what users have specified as result for forced recommendation; + # directly use the whole set if there is no useful information + return list(self.tools.values()) + + recalled_tools = await self.recall_tools(context=context, plan=plan, topk=recall_topk) + if not recalled_tools: + return [] + + ranked_tools = await self.rank_tools(recalled_tools=recalled_tools, context=context, plan=plan, topk=topk) + + logger.info(f"Recommended tools: \n{[tool.name for tool in ranked_tools]}") + + return ranked_tools + + async def get_recommended_tool_info(self, **kwargs) -> str: + """ + Wrap recommended tools with their info in a string, which can be used directly in a prompt. + """ + recommended_tools = await self.recommend_tools(**kwargs) + if not recommended_tools: + return "" + tool_schemas = {tool.name: tool.schemas for tool in recommended_tools} + return TOOL_INFO_PROMPT.format(tool_schemas=tool_schemas) + + async def recall_tools(self, context: str = "", plan: Plan = None, topk: int = 20) -> list[Tool]: + """ + Retrieves a list of relevant tools from a large pool, based on the given context and plan. + """ + raise NotImplementedError + + async def rank_tools( + self, recalled_tools: list[Tool], context: str = "", plan: Plan = None, topk: int = 5 + ) -> list[Tool]: + """ + Default rank methods for a ToolRecommender. Use LLM to rank the recalled tools based on the given context, plan, and topk value. + """ + current_task = plan.current_task.instruction if plan else context + + available_tools = {tool.name: tool.schemas["description"] for tool in recalled_tools} + prompt = TOOL_RECOMMENDATION_PROMPT.format( + current_task=current_task, + available_tools=available_tools, + topk=topk, + ) + rsp = await RecommendTool().run(prompt) + rsp = CodeParser.parse_code(block=None, text=rsp) + ranked_tools = json.loads(rsp) + + valid_tools = validate_tool_names(ranked_tools) + + return list(valid_tools.values())[:topk] + + +class BM25ToolRecommender(ToolRecommender): + """ + A ToolRecommender using BM25 at the recall stage: + 1. Recall: Querying tool descriptions with task instruction if plan exists. Otherwise, return all user-specified tools; + 2. Rank: LLM rank, the same as the default ToolRecommender. + """ + + bm25: Any = None + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._init_corpus() + + def _init_corpus(self): + corpus = [f"{tool.name} {tool.tags}: {tool.schemas['description']}" for tool in self.tools.values()] + tokenized_corpus = [self._tokenize(doc) for doc in corpus] + self.bm25 = BM25Okapi(tokenized_corpus) + + def _tokenize(self, text): + return jieba.lcut(text) # FIXME: needs more sophisticated tokenization + + async def recall_tools(self, context: str = "", plan: Plan = None, topk: int = 20) -> list[Tool]: + query = plan.current_task.instruction if plan else context + + query_tokens = self._tokenize(query) + doc_scores = self.bm25.get_scores(query_tokens) + top_indexes = np.argsort(doc_scores)[::-1][:topk] + recalled_tools = [list(self.tools.values())[index] for index in top_indexes] + print([doc_scores[index] for index in top_indexes]) + print([recalled_tools[i].name for i in range(len(recalled_tools))]) + print([recalled_tools[i].schemas["description"] for i in range(len(recalled_tools))]) + + return recalled_tools + + +class EmbeddingToolRecommender(ToolRecommender): + """ + NOTE: To be implemented. + A ToolRecommender using embeddings at the recall stage: + 1. Recall: Use embeddings to calculate the similarity between query and tool info; + 2. Rank: LLM rank, the same as the default ToolRecommender. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + async def recall_tools(self, context: str = "", plan: Plan = None, topk: int = 20) -> list[Tool]: + pass diff --git a/metagpt/tools/tool_registry.py b/metagpt/tools/tool_registry.py index 02eecb0f3..24c286c26 100644 --- a/metagpt/tools/tool_registry.py +++ b/metagpt/tools/tool_registry.py @@ -10,26 +10,20 @@ from __future__ import annotations import inspect import os from collections import defaultdict +from typing import Union import yaml -from pydantic import BaseModel, field_validator +from pydantic import BaseModel from metagpt.const import TOOL_SCHEMA_PATH from metagpt.logs import logger from metagpt.tools.tool_convert import convert_code_to_tool_schema -from metagpt.tools.tool_data_type import Tool, ToolSchema, ToolTypeDef -from metagpt.tools.tool_type import ToolType +from metagpt.tools.tool_data_type import Tool, ToolSchema class ToolRegistry(BaseModel): tools: dict = {} - tool_types: dict = {} - tools_by_types: dict = defaultdict(dict) # two-layer k-v, {tool_type: {tool_name: {...}, ...}, ...} - - @field_validator("tool_types", mode="before") - @classmethod - def init_tool_types(cls, tool_types: ToolType): - return {tool_type.type_name: tool_type.value for tool_type in tool_types} + tools_by_tags: dict = defaultdict(dict) # two-layer k-v, {tag: {tool_name: {...}, ...}, ...} def register_tool( self, @@ -37,25 +31,15 @@ class ToolRegistry(BaseModel): tool_path, schema_path="", tool_code="", - tool_type="other", + tags=None, tool_source_object=None, - include_functions=[], + include_functions=None, verbose=False, ): if self.has_tool(tool_name): return - if tool_type not in self.tool_types: - # register new tool type on the fly - logger.warning( - f"{tool_type} not previously defined, will create a temporary tool type with just a name. This tool type is only effective during this runtime. You may consider add this tool type with more configs permanently at metagpt.tools.tool_type" - ) - temp_tool_type_obj = ToolTypeDef(name=tool_type) - self.tool_types[tool_type] = temp_tool_type_obj - if verbose: - logger.info(f"tool type {tool_type} registered") - - schema_path = schema_path or TOOL_SCHEMA_PATH / tool_type / f"{tool_name}.yml" + schema_path = schema_path or TOOL_SCHEMA_PATH / f"{tool_name}.yml" schemas = make_schema(tool_source_object, include_functions, schema_path) @@ -70,10 +54,11 @@ class ToolRegistry(BaseModel): # logger.warning( # f"{tool_name} schema not conforms to required format, but will be used anyway. Mismatch: {e}" # ) - - tool = Tool(name=tool_name, path=tool_path, schemas=schemas, code=tool_code) + tags = tags or [] + tool = Tool(name=tool_name, path=tool_path, schemas=schemas, code=tool_code, tags=tags) self.tools[tool_name] = tool - self.tools_by_types[tool_type][tool_name] = tool + for tag in tags: + self.tools_by_tags[tag].update({tool_name: tool}) if verbose: logger.info(f"{tool_name} registered") logger.info(f"schema made at {str(schema_path)}, can be used for checking") @@ -84,24 +69,24 @@ class ToolRegistry(BaseModel): def get_tool(self, key) -> Tool: return self.tools.get(key) - def get_tools_by_type(self, key) -> dict[str, Tool]: - return self.tools_by_types.get(key, {}) + def get_tools_by_tag(self, key) -> dict[str, Tool]: + return self.tools_by_tags.get(key, {}) - def has_tool_type(self, key) -> bool: - return key in self.tool_types + def get_all_tools(self) -> dict[str, Tool]: + return self.tools - def get_tool_type(self, key) -> ToolType: - return self.tool_types.get(key) + def has_tool_tag(self, key) -> bool: + return key in self.tools_by_tags - def get_tool_types(self) -> dict[str, ToolType]: - return self.tool_types + def get_tool_tags(self) -> list[str]: + return list(self.tools_by_tags.keys()) # Registry instance -TOOL_REGISTRY = ToolRegistry(tool_types=ToolType) +TOOL_REGISTRY = ToolRegistry() -def register_tool(tool_type: str = "other", schema_path: str = "", **kwargs): +def register_tool(tags: list[str] = None, schema_path: str = "", **kwargs): """register a tool to registry""" def decorator(cls): @@ -117,7 +102,7 @@ def register_tool(tool_type: str = "other", schema_path: str = "", **kwargs): tool_path=file_path, schema_path=schema_path, tool_code=source_code, - tool_type=tool_type, + tags=tags, tool_source_object=cls, **kwargs, ) @@ -142,14 +127,15 @@ def make_schema(tool_source_object, include, path): return schema -def validate_tool_names(tools: list[str], return_tool_object=False) -> list[str]: - valid_tools = [] - for tool_name in tools: - if not TOOL_REGISTRY.has_tool(tool_name): - logger.warning( - f"Specified tool {tool_name} not found and was skipped. Check if you have registered it properly" - ) +def validate_tool_names(tools: Union[list[str], str]) -> str: + assert isinstance(tools, list), "tools must be a list of str" + valid_tools = {} + for key in tools: + # one can define either tool names or tool type names, take union to get the whole set + if TOOL_REGISTRY.has_tool(key): + valid_tools.update({key: TOOL_REGISTRY.get_tool(key)}) + elif TOOL_REGISTRY.tool_tool_tag(key): + valid_tools.update(TOOL_REGISTRY.get_tools_by_tag(key)) else: - valid_tool = TOOL_REGISTRY.get_tool(tool_name) if return_tool_object else tool_name - valid_tools.append(valid_tool) + logger.warning(f"invalid tool name or tool type name: {key}, skipped") return valid_tools diff --git a/tests/metagpt/tools/test_tool_recommend.py b/tests/metagpt/tools/test_tool_recommend.py new file mode 100644 index 000000000..1359d5834 --- /dev/null +++ b/tests/metagpt/tools/test_tool_recommend.py @@ -0,0 +1,66 @@ +import pytest + +from metagpt.schema import Plan, Task +from metagpt.tools import TOOL_REGISTRY +from metagpt.tools.tool_recommend import BM25ToolRecommender, ToolRecommender + + +@pytest.fixture +def mock_plan(mocker): + task_map = { + "1": Task( + task_id="1", + instruction="conduct feature engineering, add new features on the dataset", + task_type="feature_engineering", + ) + } + plan = Plan( + goal="test requirement", + tasks=list(task_map.values()), + task_map=task_map, + current_task_id="1", + ) + return plan + + +def test_tr_init(): + tr = ToolRecommender(tools=["FillMissingValue", "PolynomialExpansion", "web_scraping", "non-existing tool"]) + # web_scraping is a tool type, it has one tool scrape_web_playwright + assert list(tr.tools.keys()) == [ + "FillMissingValue", + "PolynomialExpansion", + "scrape_web_playwright", + ] + + +def test_tr_init_default_tools_value(): + tr = ToolRecommender() + assert tr.tools == {} + + +def test_tr_init_tools_all(): + tr = ToolRecommender(tools="") + assert list(tr.tools.keys()) == list(TOOL_REGISTRY.get_all_tools().keys()) + + +@pytest.mark.asyncio +async def test_tr_recall_with_plan(mock_plan): + tr = ToolRecommender( + tools=[ + "FillMissingValue", + "PolynomialExpansion", + "web_scraping", + ] + ) + result = await tr.recall_tools(plan=mock_plan) + assert len(result) == 1 + assert result[0].name == "PolynomialExpansion" + + +@pytest.mark.asyncio +async def test_bm25_tr_recall(mock_plan): + tr = BM25ToolRecommender(tools=["FillMissingValue", "PolynomialExpansion", "web_scraping"]) + result = await tr.recall_tools(plan=mock_plan) + # print(result) + assert len(result) == 3 + assert result[0].name == "PolynomialExpansion" From cbfbecb48767b86beb939fe83e116ecdeb0c4865 Mon Sep 17 00:00:00 2001 From: Abhishek <95518276+Abhishek0075@users.noreply.github.com> Date: Fri, 8 Mar 2024 08:15:13 +0000 Subject: [PATCH 032/131] reformatting done --- examples/debate.py | 17 +++++------------ examples/search_with_specific_engine.py | 24 ++---------------------- 2 files changed, 7 insertions(+), 34 deletions(-) diff --git a/examples/debate.py b/examples/debate.py index f6c0f1313..56df16b4f 100644 --- a/examples/debate.py +++ b/examples/debate.py @@ -11,6 +11,7 @@ import platform from typing import Any import fire + from metagpt.actions import Action, UserRequirement from metagpt.logs import logger from metagpt.roles import Role @@ -34,9 +35,7 @@ class SpeakAloud(Action): name: str = "SpeakAloud" async def run(self, context: str, name: str, opponent_name: str): - prompt = self.PROMPT_TEMPLATE.format( - context=context, name=name, opponent_name=opponent_name - ) + prompt = self.PROMPT_TEMPLATE.format(context=context, name=name, opponent_name=opponent_name) # logger.info(prompt) rsp = await self._aask(prompt) @@ -68,9 +67,7 @@ class Debator(Role): context = "\n".join(f"{msg.sent_from}: {msg.content}" for msg in memories) # print(context) - rsp = await todo.run( - context=context, name=self.name, opponent_name=self.opponent_name - ) + rsp = await todo.run(context=context, name=self.name, opponent_name=self.opponent_name) msg = Message( content=rsp, @@ -91,9 +88,7 @@ async def debate(idea: str, investment: float = 3.0, n_round: int = 5): team = Team() team.hire([Biden, Trump]) team.invest(investment) - team.run_project( - idea, send_to="Biden" - ) # send debate topic to Biden and let him speak first + team.run_project(idea, send_to="Biden") # send debate topic to Biden and let him speak first await team.run(n_round=n_round) @@ -111,6 +106,4 @@ def main(idea: str, investment: float = 3.0, n_round: int = 10): if __name__ == "__main__": - fire.Fire( - main - ) # run as python debate.py --idea="TOPIC" --investment=3.0 --n_round=5 + fire.Fire(main) # run as python debate.py --idea="TOPIC" --investment=3.0 --n_round=5 diff --git a/examples/search_with_specific_engine.py b/examples/search_with_specific_engine.py index 64d5c6e5f..1eee762d5 100644 --- a/examples/search_with_specific_engine.py +++ b/examples/search_with_specific_engine.py @@ -6,7 +6,7 @@ import asyncio from metagpt.config2 import Config from metagpt.roles import Searcher -from metagpt.tools.search_engine import SearchEngine, SearchEngineType +from metagpt.tools.search_engine import SearchEngine async def main(): @@ -14,27 +14,7 @@ async def main(): search = Config.default().search kwargs = {"api_key": search.api_key, "cse_id": search.cse_id, "proxy": None} - - if search.api_type == SearchEngineType.DIRECT_GOOGLE: - # Google API - await Searcher( - search_engine=SearchEngine(engine=SearchEngineType.DIRECT_GOOGLE, **kwargs) - ).run(question) - elif search.api_type == SearchEngineType.SERPER_GOOGLE: - # Serper API - await Searcher( - search_engine=SearchEngine(engine=SearchEngineType.SERPER_GOOGLE, **kwargs) - ).run(question) - elif search.api_type == SearchEngineType.SERPAPI_GOOGLE: - # SerpAPI - await Searcher( - search_engine=SearchEngine(engine=SearchEngineType.SERPAPI_GOOGLE, **kwargs) - ).run(question) - else: - # DDG API - await Searcher( - search_engine=SearchEngine(engine=SearchEngineType.DUCK_DUCK_GO, **kwargs) - ).run(question) + await Searcher(search_engine=SearchEngine(engine=search.api_type, **kwargs)).run(question) if __name__ == "__main__": From b5a534d617f9eed45ccd8c36c2ddabe22dfce286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Sun, 10 Mar 2024 22:18:13 +0800 Subject: [PATCH 033/131] fix: ipykernel_error that is `Too many open files in system` --- metagpt/roles/role.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index 3938664ba..73edfc52b 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -503,6 +503,9 @@ class Role(SerializationMixin, ContextMixin, BaseModel): self.rc.memory.add(rsp) # add to persistent memory + if hasattr(self, "execute_code") and hasattr(self.execute_code, "terminate"): + await self.execute_code.terminate() + return rsp async def _act_on_task(self, current_task: Task) -> TaskResult: From 1e04e3410346efafc4c8efcf35700650a2ad5608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Sun, 10 Mar 2024 23:52:19 +0800 Subject: [PATCH 034/131] add test. --- tests/metagpt/actions/mi/test_execute_nb_code.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/metagpt/actions/mi/test_execute_nb_code.py b/tests/metagpt/actions/mi/test_execute_nb_code.py index 59a814054..3059ad3ae 100644 --- a/tests/metagpt/actions/mi/test_execute_nb_code.py +++ b/tests/metagpt/actions/mi/test_execute_nb_code.py @@ -104,6 +104,15 @@ async def test_terminate(): time.sleep(2) assert executor.nb_client.km is None + for _ in range(200): + executor = ExecuteNbCode() + await executor.run(code='print("This is a code!")', language="python") + is_kernel_alive = await executor.nb_client.km.is_alive() + assert is_kernel_alive + await executor.terminate() + assert executor.nb_client.km is None + assert executor.nb_client.kc is None + await executor.terminate() @pytest.mark.asyncio From 803359a7588f5d955395222368320e450cc27bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Mon, 11 Mar 2024 11:25:23 +0800 Subject: [PATCH 035/131] refine: add terminate in Interpreter instead of Role. --- metagpt/actions/mi/execute_nb_code.py | 3 ++- metagpt/roles/mi/interpreter.py | 4 ++++ metagpt/roles/role.py | 3 --- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/metagpt/actions/mi/execute_nb_code.py b/metagpt/actions/mi/execute_nb_code.py index 0e4563a37..3a64a8bec 100644 --- a/metagpt/actions/mi/execute_nb_code.py +++ b/metagpt/actions/mi/execute_nb_code.py @@ -58,7 +58,8 @@ class ExecuteNbCode(Action): async def terminate(self): """kill NotebookClient""" - await self.nb_client._async_cleanup_kernel() + if self.nb_client.km is not None: + await self.nb_client._async_cleanup_kernel() async def reset(self): """reset NotebookClient""" diff --git a/metagpt/roles/mi/interpreter.py b/metagpt/roles/mi/interpreter.py index fa50098e9..2392863a0 100644 --- a/metagpt/roles/mi/interpreter.py +++ b/metagpt/roles/mi/interpreter.py @@ -42,6 +42,10 @@ class Interpreter(Role): def working_memory(self): return self.rc.working_memory + async def _plan_and_act(self) -> Message: + await super()._plan_and_act() + await self.execute_code.terminate() + 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) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index 73edfc52b..3938664ba 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -503,9 +503,6 @@ class Role(SerializationMixin, ContextMixin, BaseModel): self.rc.memory.add(rsp) # add to persistent memory - if hasattr(self, "execute_code") and hasattr(self.execute_code, "terminate"): - await self.execute_code.terminate() - return rsp async def _act_on_task(self, current_task: Task) -> TaskResult: From bf4b13ec4a6d39eb69a5838523a44c55ba5ec03f Mon Sep 17 00:00:00 2001 From: yzlin Date: Mon, 11 Mar 2024 12:30:00 +0800 Subject: [PATCH 036/131] simplify tool convert --- metagpt/prompts/mi/write_analysis_code.py | 2 +- metagpt/tools/libs/data_preprocess.py | 9 ++- metagpt/tools/tool_convert.py | 71 +++++++------------- metagpt/utils/parse_docstring.py | 82 ++++++----------------- 4 files changed, 51 insertions(+), 113 deletions(-) diff --git a/metagpt/prompts/mi/write_analysis_code.py b/metagpt/prompts/mi/write_analysis_code.py index e8ffedce4..e5663d498 100644 --- a/metagpt/prompts/mi/write_analysis_code.py +++ b/metagpt/prompts/mi/write_analysis_code.py @@ -16,7 +16,7 @@ STRUCTUAL_PROMPT = """ - Always prioritize using pre-defined tools for the same functionality. # Output -Output code in the following format: +While some concise thoughts are helpful, code is absolutely required. Always output one and only one code block in your response. Output code in the following format: ```python your code ``` diff --git a/metagpt/tools/libs/data_preprocess.py b/metagpt/tools/libs/data_preprocess.py index 3c8ebcc02..aa9070689 100644 --- a/metagpt/tools/libs/data_preprocess.py +++ b/metagpt/tools/libs/data_preprocess.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +from typing import Literal import numpy as np import pandas as pd @@ -90,14 +91,16 @@ class FillMissingValue(DataPreprocessTool): Completing missing values with simple strategies. """ - def __init__(self, features: list, strategy: str = "mean", fill_value=None): + def __init__( + self, features: list, strategy: Literal["mean", "median", "most_frequent", "constant"] = "mean", fill_value=None + ): """ Initialize self. Args: features (list): Columns to be processed. - strategy (str, optional): The imputation strategy, notice 'mean' and 'median' can only - be used for numeric features. Enum: ['mean', 'median', 'most_frequent', 'constant']. Defaults to 'mean'. + strategy (Literal["mean", "median", "most_frequent", "constant"], optional): The imputation strategy, notice 'mean' and 'median' can only + be used for numeric features. Defaults to 'mean'. fill_value (int, optional): Fill_value is used to replace all occurrences of missing_values. Defaults to None. """ diff --git a/metagpt/tools/tool_convert.py b/metagpt/tools/tool_convert.py index b837d8288..3d6a49769 100644 --- a/metagpt/tools/tool_convert.py +++ b/metagpt/tools/tool_convert.py @@ -2,6 +2,8 @@ import inspect from metagpt.utils.parse_docstring import GoogleDocstringParser, remove_spaces +PARSER = GoogleDocstringParser + def convert_code_to_tool_schema(obj, include: list[str] = None): docstring = inspect.getdoc(obj) @@ -23,54 +25,31 @@ def convert_code_to_tool_schema(obj, include: list[str] = None): return schema -def function_docstring_to_schema(fn_obj, docstring): +def function_docstring_to_schema(fn_obj, docstring) -> dict: + """ + Converts a function's docstring into a schema dictionary. + + Args: + fn_obj: The function object. + docstring: The docstring of the function. + + Returns: + A dictionary representing the schema of the function's docstring. + The dictionary contains the following keys: + - 'type': The type of the function ('function' or 'async_function'). + - 'description': The first section of the docstring describing the function overall. Provided to LLMs for both recommending and using the function. + - 'signature': The signature of the function, which helps LLMs understand how to call the function. + - 'parameters': Docstring section describing parameters including args and returns, served as extra details for LLM perception. + """ + signature = inspect.signature(fn_obj) + + docstring = remove_spaces(docstring) + + overall_desc, param_desc = PARSER.parse(docstring) + function_type = "function" if not inspect.iscoroutinefunction(fn_obj) else "async_function" - return {"type": function_type, **docstring_to_schema(docstring)} - -def docstring_to_schema(docstring: str): - if docstring is None: - return {} - - parser = GoogleDocstringParser(docstring=docstring) - - # 匹配简介部分 - description = parser.parse_desc() - - # 匹配Args部分 - params = parser.parse_params() - parameter_schema = {"properties": {}, "required": []} - for param in params: - param_name, param_type, param_desc = param - # check required or optional - is_optional, param_type = parser.check_and_parse_optional(param_type) - if not is_optional: - parameter_schema["required"].append(param_name) - # type and desc - param_dict = {"type": param_type, "description": remove_spaces(param_desc)} - # match Default for optional args - has_default_val, default_val = parser.check_and_parse_default_value(param_desc) - if has_default_val: - param_dict["default"] = default_val - # match Enum - has_enum, enum_vals = parser.check_and_parse_enum(param_desc) - if has_enum: - param_dict["enum"] = enum_vals - # add to parameter schema - parameter_schema["properties"].update({param_name: param_dict}) - - # 匹配Returns部分 - returns = parser.parse_returns() - - # 构建YAML字典 - schema = { - "description": description, - "parameters": parameter_schema, - } - if returns: - schema["returns"] = [{"type": ret[0], "description": remove_spaces(ret[1])} for ret in returns] - - return schema + return {"type": function_type, "description": overall_desc, "signature": str(signature), "parameters": param_desc} def get_class_method_docstring(cls, method_name): diff --git a/metagpt/utils/parse_docstring.py b/metagpt/utils/parse_docstring.py index e91be8e75..63c0e6890 100644 --- a/metagpt/utils/parse_docstring.py +++ b/metagpt/utils/parse_docstring.py @@ -1,45 +1,23 @@ import re from typing import Tuple -from pydantic import BaseModel - def remove_spaces(text): return re.sub(r"\s+", " ", text).strip() -class DocstringParser(BaseModel): - docstring: str +class DocstringParser: + @staticmethod + def parse(docstring: str) -> Tuple[str, str]: + """Parse the docstring and return the overall description and the parameter description. - def parse_desc(self) -> str: - """Parse and return the description from the docstring.""" - - def parse_params(self) -> list[Tuple[str, str, str]]: - """Parse and return the parameters from the docstring. + Args: + docstring (str): The docstring to be parsed. Returns: - list[Tuple[str, str, str]]: A list of input paramter info. Each info is a triple of (param name, param type, param description) + Tuple[str, str]: A tuple of (overall description, parameter description) """ - def parse_returns(self) -> list[Tuple[str, str]]: - """Parse and return the output information from the docstring. - - Returns: - list[Tuple[str, str]]: A list of output info. Each info is a tuple of (return type, return description) - """ - - @staticmethod - def check_and_parse_optional(param_type: str) -> Tuple[bool, str]: - """Check if a parameter is optional and return a processed param_type rid of the optionality info if so""" - - @staticmethod - def check_and_parse_default_value(param_desc: str) -> Tuple[bool, str]: - """Check if a parameter has a default value and return the default value if so""" - - @staticmethod - def check_and_parse_enum(param_desc: str) -> Tuple[bool, str]: - """Check if a parameter description includes an enum and return enum values if so""" - class reSTDocstringParser(DocstringParser): """A parser for reStructuredText (reST) docstring""" @@ -48,40 +26,18 @@ class reSTDocstringParser(DocstringParser): class GoogleDocstringParser(DocstringParser): """A parser for Google-stype docstring""" - docstring: str - - def parse_desc(self) -> str: - description_match = re.search(r"^(.*?)(?:Args:|Returns:|Raises:|$)", self.docstring, re.DOTALL) - description = remove_spaces(description_match.group(1)) if description_match else "" - return description - - def parse_params(self) -> list[Tuple[str, str, str]]: - args_match = re.search(r"Args:\s*(.*?)(?:Returns:|Raises:|$)", self.docstring, re.DOTALL) - _args = args_match.group(1).strip() if args_match else "" - # variable_pattern = re.compile(r"(\w+)\s*\((.*?)\):\s*(.*)") - variable_pattern = re.compile( - r"(\w+)\s*\((.*?)\):\s*(.*?)(?=\n\s*\w+\s*\(|\Z)", re.DOTALL - ) # (?=\n\w+\s*\(|\Z) is to assert that what follows is either the start of the next parameter (indicated by a newline, some word characters, and an opening parenthesis) or the end of the string (\Z). - params = variable_pattern.findall(_args) - return params - - def parse_returns(self) -> list[Tuple[str, str]]: - returns_match = re.search(r"Returns:\s*(.*?)(?:Raises:|$)", self.docstring, re.DOTALL) - returns = returns_match.group(1).strip() if returns_match else "" - return_pattern = re.compile(r"^(.*)\s*:\s*(.*)$") - returns = return_pattern.findall(returns) - return returns - @staticmethod - def check_and_parse_optional(param_type: str) -> Tuple[bool, str]: - return "optional" in param_type, param_type.replace(", optional", "") + def parse(docstring: str) -> Tuple[str, str]: + if not docstring: + return "", "" - @staticmethod - def check_and_parse_default_value(param_desc: str) -> Tuple[bool, str]: - default_val = re.search(r"Defaults to (.+?)\.", param_desc) - return (True, default_val.group(1)) if default_val else (False, "") + docstring = remove_spaces(docstring) - @staticmethod - def check_and_parse_enum(param_desc: str) -> Tuple[bool, str]: - enum_val = re.search(r"Enum: \[(.+?)\]", param_desc) - return (True, [e.strip() for e in enum_val.group(1).split(",")]) if enum_val else (False, []) + if "Args:" in docstring: + overall_desc, param_desc = docstring.split("Args:") + param_desc = "Args:" + param_desc + else: + overall_desc = docstring + param_desc = "" + + return overall_desc, param_desc From bcde5171e0ccf86f8a51d4c7bb28ef18093fe255 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Mon, 11 Mar 2024 13:57:25 +0800 Subject: [PATCH 037/131] refine parse_outputs in ExecuteNbCode. --- metagpt/actions/mi/execute_nb_code.py | 31 ++++++++++--------- .../actions/mi/test_execute_nb_code.py | 17 ++++++++++ 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/metagpt/actions/mi/execute_nb_code.py b/metagpt/actions/mi/execute_nb_code.py index 3a64a8bec..4644ef5d5 100644 --- a/metagpt/actions/mi/execute_nb_code.py +++ b/metagpt/actions/mi/execute_nb_code.py @@ -9,7 +9,6 @@ from __future__ import annotations import asyncio import base64 import re -import traceback from typing import Literal, Tuple import nbformat @@ -92,17 +91,17 @@ class ExecuteNbCode(Action): else: cell["outputs"].append(new_output(output_type="stream", name="stdout", text=str(output))) - def parse_outputs(self, outputs: list[str]) -> str: + def parse_outputs(self, outputs: list[str], keep_len: int = 2000) -> Tuple[bool, str]: """Parses the outputs received from notebook execution.""" assert isinstance(outputs, list) - parsed_output = "" - + parsed_output, is_success = [], True for i, output in enumerate(outputs): + is_success = "traceback" not in output.keys() if output["output_type"] == "stream" and not any( tag in output["text"] for tag in ["| INFO | metagpt", "| ERROR | metagpt", "| WARNING | metagpt", "DEBUG"] ): - parsed_output += output["text"] + ioutput, is_success = truncate(remove_escape_and_color_codes(output["text"]), keep_len, is_success) elif output["output_type"] == "display_data": if "image/png" in output["data"]: self.show_bytes_figure(output["data"]["image/png"], self.interaction) @@ -110,9 +109,15 @@ class ExecuteNbCode(Action): logger.info( f"{i}th output['data'] from nbclient outputs dont have image/png, continue next output ..." ) + ioutput, is_success = "", True elif output["output_type"] == "execute_result": - parsed_output += output["data"]["text/plain"] - return parsed_output + no_escape_color_output = remove_escape_and_color_codes(output["data"]["text/plain"]) + ioutput, is_success = truncate(no_escape_color_output, keep_len, is_success) + elif output["output_type"] == "error": + no_escape_color_output = remove_escape_and_color_codes("\n".join(output["traceback"])) + ioutput, is_success = truncate(no_escape_color_output, keep_len, is_success) + parsed_output.append(ioutput) + return is_success, ",".join(parsed_output) def show_bytes_figure(self, image_base64: str, interaction_type: Literal["ipython", None]): image_bytes = base64.b64decode(image_base64) @@ -157,7 +162,7 @@ class ExecuteNbCode(Action): await self.reset() return False, "DeadKernelError" except Exception: - return False, f"{traceback.format_exc()}" + return False, "" async def run(self, code: str, language: Literal["python", "markdown"] = "python") -> Tuple[str, bool]: """ @@ -175,13 +180,9 @@ class ExecuteNbCode(Action): # run code cell_index = len(self.nb.cells) - 1 success, error_message = await self.run_cell(self.nb.cells[-1], cell_index) - - 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) - outputs, success = truncate(remove_escape_and_color_codes(outputs), is_success=success) + success, outputs = self.parse_outputs(self.nb.cells[-1].outputs) + if error_message: + outputs = error_message + outputs if "!pip" in code: success = False diff --git a/tests/metagpt/actions/mi/test_execute_nb_code.py b/tests/metagpt/actions/mi/test_execute_nb_code.py index 3059ad3ae..98c2e5cc3 100644 --- a/tests/metagpt/actions/mi/test_execute_nb_code.py +++ b/tests/metagpt/actions/mi/test_execute_nb_code.py @@ -100,6 +100,7 @@ async def test_terminate(): is_kernel_alive = await executor.nb_client.km.is_alive() assert is_kernel_alive await executor.terminate() + import time time.sleep(2) @@ -123,3 +124,19 @@ async def test_reset(): assert is_kernel_alive await executor.reset() assert executor.nb_client.km is None + + +@pytest.mark.asyncio +async def test_parse_outputs(): + executor = ExecuteNbCode() + code = """ + import pandas as pd + df = pd.DataFrame({'ID': [1,2,3], 'NAME': ['a', 'b', 'c']}) + print(df.columns) + print(df['DUMMPY_ID']) + """ + output, is_success = await executor.run(code) + assert not is_success + assert "Index(['ID', 'NAME'], dtype='object')" in output + assert "Executed code failed," in output + assert "KeyError: 'DUMMPY_ID'" in output From bff3ef02bc29b58e5d6b20fc6a2003e10fc1c088 Mon Sep 17 00:00:00 2001 From: yzlin Date: Mon, 11 Mar 2024 13:58:19 +0800 Subject: [PATCH 038/131] rm tool types --- metagpt/tools/tool_type.py | 65 -------------------------------------- 1 file changed, 65 deletions(-) delete mode 100644 metagpt/tools/tool_type.py diff --git a/metagpt/tools/tool_type.py b/metagpt/tools/tool_type.py deleted file mode 100644 index 093a69bf7..000000000 --- a/metagpt/tools/tool_type.py +++ /dev/null @@ -1,65 +0,0 @@ -from enum import Enum - -from metagpt.prompts.tool_types import ( - DATA_PREPROCESS_PROMPT, - EDA_PROMPT, - FEATURE_ENGINEERING_PROMPT, - IMAGE2WEBPAGE_PROMPT, - MODEL_EVALUATE_PROMPT, - MODEL_TRAIN_PROMPT, -) -from metagpt.tools.tool_data_type import ToolTypeDef - - -class ToolType(Enum): - EDA = ToolTypeDef( - name="eda", - desc="For performing exploratory data analysis", - usage_prompt=EDA_PROMPT, - ) - DATA_PREPROCESS = ToolTypeDef( - name="data_preprocess", - desc="For preprocessing dataset in a data analysis or machine learning task ONLY," - "general data operation doesn't fall into this type", - usage_prompt=DATA_PREPROCESS_PROMPT, - ) - EMAIL_LOGIN = ToolTypeDef( - name="email_login", - desc="For logging to an email.", - ) - FEATURE_ENGINEERING = ToolTypeDef( - name="feature_engineering", - desc="Only for creating new columns for input data.", - usage_prompt=FEATURE_ENGINEERING_PROMPT, - ) - MODEL_TRAIN = ToolTypeDef( - name="model_train", - desc="Only for training model.", - usage_prompt=MODEL_TRAIN_PROMPT, - ) - MODEL_EVALUATE = ToolTypeDef( - name="model_evaluate", - desc="Only for evaluating model.", - usage_prompt=MODEL_EVALUATE_PROMPT, - ) - STABLE_DIFFUSION = ToolTypeDef( - name="stable_diffusion", - desc="Related to text2image, image2image using stable diffusion model.", - ) - IMAGE2WEBPAGE = ToolTypeDef( - name="image2webpage", - desc="For converting image into webpage code.", - usage_prompt=IMAGE2WEBPAGE_PROMPT, - ) - WEBSCRAPING = ToolTypeDef( - name="web_scraping", - desc="For scraping data from web pages.", - ) - OTHER = ToolTypeDef(name="other", desc="Any tools not in the defined categories") - - def __missing__(self, key): - return self.OTHER - - @property - def type_name(self): - return self.value.name From 9db705f20f79de8d77192bbd8adbc7dc37b25174 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Mon, 11 Mar 2024 14:43:00 +0800 Subject: [PATCH 039/131] refine: rm truncate. --- metagpt/actions/mi/execute_nb_code.py | 38 +++++++++---------- .../actions/mi/test_execute_nb_code.py | 17 +-------- 2 files changed, 18 insertions(+), 37 deletions(-) diff --git a/metagpt/actions/mi/execute_nb_code.py b/metagpt/actions/mi/execute_nb_code.py index 4644ef5d5..2e22a7d0c 100644 --- a/metagpt/actions/mi/execute_nb_code.py +++ b/metagpt/actions/mi/execute_nb_code.py @@ -101,7 +101,7 @@ class ExecuteNbCode(Action): tag in output["text"] for tag in ["| INFO | metagpt", "| ERROR | metagpt", "| WARNING | metagpt", "DEBUG"] ): - ioutput, is_success = truncate(remove_escape_and_color_codes(output["text"]), keep_len, is_success) + ioutput, is_success = remove_escape_and_color_codes(output["text"]), True elif output["output_type"] == "display_data": if "image/png" in output["data"]: self.show_bytes_figure(output["data"]["image/png"], self.interaction) @@ -112,10 +112,24 @@ class ExecuteNbCode(Action): ioutput, is_success = "", True elif output["output_type"] == "execute_result": no_escape_color_output = remove_escape_and_color_codes(output["data"]["text/plain"]) - ioutput, is_success = truncate(no_escape_color_output, keep_len, is_success) + ioutput, is_success = no_escape_color_output, True elif output["output_type"] == "error": no_escape_color_output = remove_escape_and_color_codes("\n".join(output["traceback"])) - ioutput, is_success = truncate(no_escape_color_output, keep_len, is_success) + ioutput, is_success = no_escape_color_output, False + + # handle coroutines that are not executed asynchronously + if ioutput.strip().startswith(" keep_len and is_success: + prefix = f"Executed code successfully. Truncated to show only first {keep_len} characters\n" + ioutput = prefix + ioutput[:keep_len] + elif len(ioutput) > keep_len and not is_success: + prefix = f"Executed code failed, please reflect the cause of bug and then debug. Truncated to show only last {keep_len} characters\n" + ioutput = prefix + ioutput[-keep_len:] + parsed_output.append(ioutput) return is_success, ",".join(parsed_output) @@ -198,24 +212,6 @@ class ExecuteNbCode(Action): raise ValueError(f"Only support for language: python, markdown, but got {language}, ") -def truncate(result: str, keep_len: int = 2000, is_success: bool = True): - """对于超出keep_len个字符的result: 执行失败的代码, 展示result后keep_len个字符; 执行成功的代码, 展示result前keep_len个字符。""" - if is_success: - desc = f"Executed code successfully. Truncated to show only first {keep_len} characters\n" - else: - desc = f"Executed code failed, please reflect the cause of bug and then debug. Truncated to show only last {keep_len} characters\n" - - if result.strip().startswith(" keep_len: - result = result[-keep_len:] if not is_success else result[:keep_len] - return desc + result, is_success - - return result, is_success - - def remove_escape_and_color_codes(input_str: str): # 使用正则表达式去除jupyter notebook输出结果中的转义字符和颜色代码 # Use regular expressions to get rid of escape characters and color codes in jupyter notebook output. diff --git a/tests/metagpt/actions/mi/test_execute_nb_code.py b/tests/metagpt/actions/mi/test_execute_nb_code.py index 98c2e5cc3..4b90289ea 100644 --- a/tests/metagpt/actions/mi/test_execute_nb_code.py +++ b/tests/metagpt/actions/mi/test_execute_nb_code.py @@ -1,6 +1,6 @@ import pytest -from metagpt.actions.mi.execute_nb_code import ExecuteNbCode, truncate +from metagpt.actions.mi.execute_nb_code import ExecuteNbCode @pytest.mark.asyncio @@ -54,21 +54,6 @@ async def test_plotting_code(): assert is_success -def test_truncate(): - # 代码执行成功 - output, is_success = truncate("hello world", 5, True) - assert "Truncated to show only first 5 characters\nhello" in output - assert is_success - # 代码执行失败 - output, is_success = truncate("hello world", 5, False) - assert "Truncated to show only last 5 characters\nworld" in output - assert not is_success - # 异步 - output, is_success = truncate(" Date: Mon, 11 Mar 2024 14:51:02 +0800 Subject: [PATCH 040/131] refine: rm `is_success = "traceback" not in output.keys()` --- metagpt/actions/mi/execute_nb_code.py | 1 - 1 file changed, 1 deletion(-) diff --git a/metagpt/actions/mi/execute_nb_code.py b/metagpt/actions/mi/execute_nb_code.py index 2e22a7d0c..632f0076c 100644 --- a/metagpt/actions/mi/execute_nb_code.py +++ b/metagpt/actions/mi/execute_nb_code.py @@ -96,7 +96,6 @@ class ExecuteNbCode(Action): assert isinstance(outputs, list) parsed_output, is_success = [], True for i, output in enumerate(outputs): - is_success = "traceback" not in output.keys() if output["output_type"] == "stream" and not any( tag in output["text"] for tag in ["| INFO | metagpt", "| ERROR | metagpt", "| WARNING | metagpt", "DEBUG"] From b5af9ccde6e0e87af40eca3b45fcb8569d588633 Mon Sep 17 00:00:00 2001 From: yzlin Date: Mon, 11 Mar 2024 16:18:28 +0800 Subject: [PATCH 041/131] fix unit tests for tool module --- metagpt/actions/mi/write_plan.py | 4 +- metagpt/roles/mi/interpreter.py | 15 ++- metagpt/tools/tool_recommend.py | 7 +- metagpt/tools/tool_registry.py | 2 +- tests/metagpt/tools/test_tool_convert.py | 106 ++++++--------------- tests/metagpt/tools/test_tool_recommend.py | 46 +++++---- tests/metagpt/tools/test_tool_registry.py | 52 +++------- 7 files changed, 91 insertions(+), 141 deletions(-) diff --git a/metagpt/actions/mi/write_plan.py b/metagpt/actions/mi/write_plan.py index 1839de0f9..b190733fc 100644 --- a/metagpt/actions/mi/write_plan.py +++ b/metagpt/actions/mi/write_plan.py @@ -42,9 +42,7 @@ class WritePlan(Action): """ async def run(self, context: list[Message], max_tasks: int = 5, use_tools: bool = False) -> str: - task_type_desc = "\n".join( - [f"- **{tt.type_name}**: {tt.value.desc}" for tt in TaskType] - ) # task type are binded with tool type now, should be improved in the future + task_type_desc = "\n".join([f"- **{tt.type_name}**: {tt.value.desc}" for tt in TaskType]) prompt = self.PROMPT_TEMPLATE.format( context="\n".join([str(ct) for ct in context]), max_tasks=max_tasks, task_type_desc=task_type_desc ) diff --git a/metagpt/roles/mi/interpreter.py b/metagpt/roles/mi/interpreter.py index 58b38ac43..e71514b62 100644 --- a/metagpt/roles/mi/interpreter.py +++ b/metagpt/roles/mi/interpreter.py @@ -156,11 +156,16 @@ class Interpreter(Role): return code, todo async def _check_data(self): - if not self.use_plan or self.planner.plan.current_task.task_type not in [ - TaskType.DATA_PREPROCESS.type_name, - TaskType.FEATURE_ENGINEERING.type_name, - TaskType.MODEL_TRAIN.type_name, - ]: + if ( + not self.use_plan + or not self.planner.plan.get_finished_tasks() + or self.planner.plan.current_task.task_type + not in [ + TaskType.DATA_PREPROCESS.type_name, + TaskType.FEATURE_ENGINEERING.type_name, + TaskType.MODEL_TRAIN.type_name, + ] + ): return logger.info("Check updated data") code = await CheckData().run(self.planner.plan) diff --git a/metagpt/tools/tool_recommend.py b/metagpt/tools/tool_recommend.py index 9e06a67b4..fcdbc4254 100644 --- a/metagpt/tools/tool_recommend.py +++ b/metagpt/tools/tool_recommend.py @@ -174,9 +174,10 @@ class BM25ToolRecommender(ToolRecommender): doc_scores = self.bm25.get_scores(query_tokens) top_indexes = np.argsort(doc_scores)[::-1][:topk] recalled_tools = [list(self.tools.values())[index] for index in top_indexes] - print([doc_scores[index] for index in top_indexes]) - print([recalled_tools[i].name for i in range(len(recalled_tools))]) - print([recalled_tools[i].schemas["description"] for i in range(len(recalled_tools))]) + + logger.info( + f"Recalled tools: \n{[tool.name for tool in recalled_tools]}; Scores: {[doc_scores[index] for index in top_indexes]}" + ) return recalled_tools diff --git a/metagpt/tools/tool_registry.py b/metagpt/tools/tool_registry.py index 24c286c26..11269cb0f 100644 --- a/metagpt/tools/tool_registry.py +++ b/metagpt/tools/tool_registry.py @@ -134,7 +134,7 @@ def validate_tool_names(tools: Union[list[str], str]) -> str: # one can define either tool names or tool type names, take union to get the whole set if TOOL_REGISTRY.has_tool(key): valid_tools.update({key: TOOL_REGISTRY.get_tool(key)}) - elif TOOL_REGISTRY.tool_tool_tag(key): + elif TOOL_REGISTRY.has_tool_tag(key): valid_tools.update(TOOL_REGISTRY.get_tools_by_tag(key)) else: logger.warning(f"invalid tool name or tool type name: {key}, skipped") diff --git a/tests/metagpt/tools/test_tool_convert.py b/tests/metagpt/tools/test_tool_convert.py index 8f26a211c..f85b84b71 100644 --- a/tests/metagpt/tools/test_tool_convert.py +++ b/tests/metagpt/tools/test_tool_convert.py @@ -1,44 +1,8 @@ +from typing import Literal, Union + import pandas as pd -from metagpt.tools.tool_convert import convert_code_to_tool_schema, docstring_to_schema - - -def test_docstring_to_schema(): - docstring = """ - Some test desc. - - Args: - features (list): Columns to be processed. - strategy (str, optional): The imputation strategy, notice 'mean' and 'median' can only be - used for numeric features. Enum: ['mean', 'median', 'most_frequent', 'constant']. Defaults to 'mean'. - fill_value (int, optional): Fill_value is used to replace all occurrences of missing_values. - Defaults to None. - Returns: - pd.DataFrame: The transformed DataFrame. - """ - expected = { - "description": "Some test desc.", - "parameters": { - "properties": { - "features": {"type": "list", "description": "Columns to be processed."}, - "strategy": { - "type": "str", - "description": "The imputation strategy, notice 'mean' and 'median' can only be used for numeric features. Enum: ['mean', 'median', 'most_frequent', 'constant']. Defaults to 'mean'.", - "default": "'mean'", - "enum": ["'mean'", "'median'", "'most_frequent'", "'constant'"], - }, - "fill_value": { - "type": "int", - "description": "Fill_value is used to replace all occurrences of missing_values. Defaults to None.", - "default": "None", - }, - }, - "required": ["features"], - }, - "returns": [{"type": "pd.DataFrame", "description": "The transformed DataFrame."}], - } - schema = docstring_to_schema(docstring) - assert schema == expected +from metagpt.tools.tool_convert import convert_code_to_tool_schema class DummyClass: @@ -81,12 +45,26 @@ class DummyClass: pass -def dummy_fn(df: pd.DataFrame) -> dict: +# def dummy_fn(df: pd.DataFrame, s: str, k: int = 5, type: Literal["a", "b", "c"] = "a") -> dict: +def dummy_fn( + df: pd.DataFrame, + s: str, + k: int = 5, + type: Literal["a", "b", "c"] = "a", + test_dict: dict[str, int] = None, + test_union: Union[str, list[str]] = "", +) -> dict: """ Analyzes a DataFrame and categorizes its columns based on data types. Args: - df (pd.DataFrame): The DataFrame to be analyzed. + df: The DataFrame to be analyzed. + Another line for df. + s (str): Some test string param. + Another line for s. + k (int, optional): Some test integer param. Defaults to 5. + type (Literal["a", "b", "c"], optional): Some test type. Defaults to 'a'. + more_args: will be omitted here for testing Returns: dict: A dictionary with four keys ('Category', 'Numeric', 'Datetime', 'Others'). @@ -115,41 +93,21 @@ def test_convert_code_to_tool_schema_class(): "methods": { "__init__": { "type": "function", - "description": "Initialize self.", - "parameters": { - "properties": { - "features": {"type": "list", "description": "Columns to be processed."}, - "strategy": { - "type": "str", - "description": "The imputation strategy, notice 'mean' and 'median' can only be used for numeric features. Enum: ['mean', 'median', 'most_frequent', 'constant']. Defaults to 'mean'.", - "default": "'mean'", - "enum": ["'mean'", "'median'", "'most_frequent'", "'constant'"], - }, - "fill_value": { - "type": "int", - "description": "Fill_value is used to replace all occurrences of missing_values. Defaults to None.", - "default": "None", - }, - }, - "required": ["features"], - }, + "description": "Initialize self. ", + "signature": "(self, features: list, strategy: str = 'mean', fill_value=None)", + "parameters": "Args: features (list): Columns to be processed. strategy (str, optional): The imputation strategy, notice 'mean' and 'median' can only be used for numeric features. Enum: ['mean', 'median', 'most_frequent', 'constant']. Defaults to 'mean'. fill_value (int, optional): Fill_value is used to replace all occurrences of missing_values. Defaults to None.", }, "fit": { "type": "function", - "description": "Fit the FillMissingValue model.", - "parameters": { - "properties": {"df": {"type": "pd.DataFrame", "description": "The input DataFrame."}}, - "required": ["df"], - }, + "description": "Fit the FillMissingValue model. ", + "signature": "(self, df: pandas.core.frame.DataFrame)", + "parameters": "Args: df (pd.DataFrame): The input DataFrame.", }, "transform": { "type": "function", - "description": "Transform the input DataFrame with the fitted model.", - "parameters": { - "properties": {"df": {"type": "pd.DataFrame", "description": "The input DataFrame."}}, - "required": ["df"], - }, - "returns": [{"type": "pd.DataFrame", "description": "The transformed DataFrame."}], + "description": "Transform the input DataFrame with the fitted model. ", + "signature": "(self, df: pandas.core.frame.DataFrame) -> pandas.core.frame.DataFrame", + "parameters": "Args: df (pd.DataFrame): The input DataFrame. Returns: pd.DataFrame: The transformed DataFrame.", }, }, } @@ -160,11 +118,9 @@ def test_convert_code_to_tool_schema_class(): def test_convert_code_to_tool_schema_function(): expected = { "type": "function", - "description": "Analyzes a DataFrame and categorizes its columns based on data types.", - "parameters": { - "properties": {"df": {"type": "pd.DataFrame", "description": "The DataFrame to be analyzed."}}, - "required": ["df"], - }, + "description": "Analyzes a DataFrame and categorizes its columns based on data types. ", + "signature": "(df: pandas.core.frame.DataFrame, s: str, k: int = 5, type: Literal['a', 'b', 'c'] = 'a', test_dict: dict[str, int] = None, test_union: Union[str, list[str]] = '') -> dict", + "parameters": "Args: df: The DataFrame to be analyzed. Another line for df. s (str): Some test string param. Another line for s. k (int, optional): Some test integer param. Defaults to 5. type (Literal[\"a\", \"b\", \"c\"], optional): Some test type. Defaults to 'a'. more_args: will be omitted here for testing Returns: dict: A dictionary with four keys ('Category', 'Numeric', 'Datetime', 'Others'). Each key corresponds to a list of column names belonging to that category.", } schema = convert_code_to_tool_schema(dummy_fn) assert schema == expected diff --git a/tests/metagpt/tools/test_tool_recommend.py b/tests/metagpt/tools/test_tool_recommend.py index 1359d5834..2fb3f9348 100644 --- a/tests/metagpt/tools/test_tool_recommend.py +++ b/tests/metagpt/tools/test_tool_recommend.py @@ -23,9 +23,15 @@ def mock_plan(mocker): return plan +@pytest.fixture +def mock_bm25_tr(mocker): + tr = BM25ToolRecommender(tools=["FillMissingValue", "PolynomialExpansion", "web scraping"]) + return tr + + def test_tr_init(): - tr = ToolRecommender(tools=["FillMissingValue", "PolynomialExpansion", "web_scraping", "non-existing tool"]) - # web_scraping is a tool type, it has one tool scrape_web_playwright + tr = ToolRecommender(tools=["FillMissingValue", "PolynomialExpansion", "web scraping", "non-existing tool"]) + # web_scraping is a tool tag, it has one tool scrape_web_playwright assert list(tr.tools.keys()) == [ "FillMissingValue", "PolynomialExpansion", @@ -39,28 +45,34 @@ def test_tr_init_default_tools_value(): def test_tr_init_tools_all(): - tr = ToolRecommender(tools="") + tr = ToolRecommender(tools=[""]) assert list(tr.tools.keys()) == list(TOOL_REGISTRY.get_all_tools().keys()) @pytest.mark.asyncio -async def test_tr_recall_with_plan(mock_plan): - tr = ToolRecommender( - tools=[ - "FillMissingValue", - "PolynomialExpansion", - "web_scraping", - ] - ) - result = await tr.recall_tools(plan=mock_plan) - assert len(result) == 1 +async def test_bm25_tr_recall_with_plan(mock_plan, mock_bm25_tr): + result = await mock_bm25_tr.recall_tools(plan=mock_plan) + assert len(result) == 3 assert result[0].name == "PolynomialExpansion" @pytest.mark.asyncio -async def test_bm25_tr_recall(mock_plan): - tr = BM25ToolRecommender(tools=["FillMissingValue", "PolynomialExpansion", "web_scraping"]) - result = await tr.recall_tools(plan=mock_plan) - # print(result) +async def test_bm25_tr_recall_no_plan(mock_plan, mock_bm25_tr): + result = await mock_bm25_tr.recall_tools( + context="conduct feature engineering, add new features on the dataset", plan=None + ) assert len(result) == 3 assert result[0].name == "PolynomialExpansion" + + +@pytest.mark.asyncio +async def test_bm25_recommend_tools(mock_bm25_tr): + result = await mock_bm25_tr.recommend_tools(context="conduct feature engineering, add new features on the dataset") + assert len(result) == 2 # web scraping tool should be filtered out at rank stage + assert result[0].name == "PolynomialExpansion" + + +@pytest.mark.asyncio +async def test_get_recommended_tool_info(mock_plan, mock_bm25_tr): + result = await mock_bm25_tr.get_recommended_tool_info(plan=mock_plan) + assert isinstance(result, str) diff --git a/tests/metagpt/tools/test_tool_registry.py b/tests/metagpt/tools/test_tool_registry.py index 2fd487fb7..f44dfea0b 100644 --- a/tests/metagpt/tools/test_tool_registry.py +++ b/tests/metagpt/tools/test_tool_registry.py @@ -1,7 +1,6 @@ import pytest from metagpt.tools.tool_registry import ToolRegistry -from metagpt.tools.tool_type import ToolType @pytest.fixture @@ -9,25 +8,11 @@ def tool_registry(): return ToolRegistry() -@pytest.fixture -def tool_registry_full(): - return ToolRegistry(tool_types=ToolType) - - # Test Initialization def test_initialization(tool_registry): assert isinstance(tool_registry, ToolRegistry) assert tool_registry.tools == {} - assert tool_registry.tool_types == {} - assert tool_registry.tools_by_types == {} - - -# Test Initialization with tool types -def test_initialize_with_tool_types(tool_registry_full): - assert isinstance(tool_registry_full, ToolRegistry) - assert tool_registry_full.tools == {} - assert tool_registry_full.tools_by_types == {} - assert "data_preprocess" in tool_registry_full.tool_types + assert tool_registry.tools_by_tags == {} class TestClassTool: @@ -72,31 +57,24 @@ def test_get_tool(tool_registry): assert "description" in tool.schemas -# Similar tests for has_tool_type, get_tool_type, get_tools_by_type -def test_has_tool_type(tool_registry_full): - assert tool_registry_full.has_tool_type("data_preprocess") - assert not tool_registry_full.has_tool_type("NonexistentType") +def test_has_tool_tag(tool_registry): + tool_registry.register_tool( + "TestClassTool", "/path/to/tool", tool_source_object=TestClassTool, tags=["machine learning", "test"] + ) + assert tool_registry.has_tool_tag("test") + assert not tool_registry.has_tool_tag("Non-existent tag") -def test_get_tool_type(tool_registry_full): - retrieved_type = tool_registry_full.get_tool_type("data_preprocess") - assert retrieved_type is not None - assert retrieved_type.name == "data_preprocess" - - -def test_get_tools_by_type(tool_registry): - tool_type_name = "TestType" +def test_get_tools_by_tag(tool_registry): + tool_tag_name = "Test Tag" tool_name = "TestTool" tool_path = "/path/to/tool" - tool_registry.register_tool(tool_name, tool_path, tool_type=tool_type_name, tool_source_object=TestClassTool) + tool_registry.register_tool(tool_name, tool_path, tags=[tool_tag_name], tool_source_object=TestClassTool) - tools_by_type = tool_registry.get_tools_by_type(tool_type_name) - assert tools_by_type is not None - assert tool_name in tools_by_type + tools_by_tag = tool_registry.get_tools_by_tag(tool_tag_name) + assert tools_by_tag is not None + assert tool_name in tools_by_tag - -# Test case for when the tool type does not exist -def test_get_tools_by_nonexistent_type(tool_registry): - tools_by_type = tool_registry.get_tools_by_type("NonexistentType") - assert not tools_by_type + tools_by_tag_non_existent = tool_registry.get_tools_by_tag("Non-existent Tag") + assert not tools_by_tag_non_existent From cc00161b989eed48c94c4425a89279f534ed942d Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Mon, 11 Mar 2024 16:25:02 +0800 Subject: [PATCH 042/131] simplify save_webpages in gpt_v_generator.py --- metagpt/tools/libs/gpt_v_generator.py | 57 +++++++------------ .../tools/libs/test_gpt_v_generator.py | 4 +- 2 files changed, 22 insertions(+), 39 deletions(-) diff --git a/metagpt/tools/libs/gpt_v_generator.py b/metagpt/tools/libs/gpt_v_generator.py index 3b17fc596..bcca0df7b 100644 --- a/metagpt/tools/libs/gpt_v_generator.py +++ b/metagpt/tools/libs/gpt_v_generator.py @@ -6,12 +6,13 @@ @File : gpt_v_generator.py """ import os +import re from pathlib import Path from metagpt.const import DEFAULT_WORKSPACE_ROOT from metagpt.tools.tool_registry import register_tool -from metagpt.tools.tool_type import ToolType -from metagpt.utils.common import encode_image +from metagpt.utils.common import encode_image, CodeParser +from metagpt.logs import logger ANALYZE_LAYOUT_PROMPT = """You are now a UI/UX designer, please generate layout information for this image: @@ -28,11 +29,9 @@ As the design pays tribute to large companies, sometimes it is normal for some c Now, please generate the corresponding webpage code including HTML, CSS and JavaScript:""" -@register_tool( - tool_type=ToolType.IMAGE2WEBPAGE.type_name, include_functions=["__init__", "generate_webpages", "save_webpages"] -) +@register_tool(include_functions=["__init__", "generate_webpages", "save_webpages"]) class GPTvGenerator: - """Class for generating webpages at once. + """Class for generating webpage code from a given webpage screenshot. This class provides methods to generate webpages including all code (HTML, CSS, and JavaScript) based on an image. It utilizes a vision model to analyze the layout from an image and generate webpage codes accordingly. @@ -75,50 +74,34 @@ class GPTvGenerator: return await self.llm.aask(msg=prompt, images=[encode_image(image_path)]) @staticmethod - def save_webpages(image_path: str, webpages: str) -> Path: + def save_webpages(webpages: str, save_folder_name: str = "example") -> Path: """Save webpages including all code (HTML, CSS, and JavaScript) at once. Args: - image_path (str): The path of the image file. webpages (str): The generated webpages content. + save_folder_name (str, optional): The name of the folder to save the webpages. Defaults to 'example'. Returns: Path: The path of the saved webpages. """ # Create a folder called webpages in the workspace directory to store HTML, CSS, and JavaScript files - webpages_path = DEFAULT_WORKSPACE_ROOT / "webpages" / Path(image_path).stem + webpages_path = DEFAULT_WORKSPACE_ROOT / "webpages" / save_folder_name + logger.info(f"code will be saved at {webpages_path}") os.makedirs(webpages_path, exist_ok=True) index_path = webpages_path / "index.html" - try: - index = webpages.split("```html")[1].split("```")[0] - style_path = None - if "styles.css" in index: - style_path = webpages_path / "styles.css" - elif "style.css" in index: - style_path = webpages_path / "style.css" - style = webpages.split("```css")[1].split("```")[0] if style_path else "" + index_path.write_text(CodeParser.parse_code(block=None, text=webpages, lang="html")) - js_path = None - if "scripts.js" in index: - js_path = webpages_path / "scripts.js" - elif "script.js" in index: - js_path = webpages_path / "script.js" + extract_and_save_code(folder=webpages_path, text=webpages, pattern="styles?.css", language="css") - js = webpages.split("```javascript")[1].split("```")[0] if js_path else "" - except IndexError: - raise ValueError(f"No html or css or js code found in the result. \nWebpages: {webpages}") - - try: - with open(index_path, "w", encoding="utf-8") as f: - f.write(index) - if style_path: - with open(style_path, "w", encoding="utf-8") as f: - f.write(style) - if js_path: - with open(js_path, "w", encoding="utf-8") as f: - f.write(js) - except FileNotFoundError as e: - raise FileNotFoundError(f"Cannot save the webpages to {str(webpages_path)}") from e + extract_and_save_code(folder=webpages_path, text=webpages, pattern="scripts?.js", language="javascript") return webpages_path + + +def extract_and_save_code(folder, text, pattern, language): + word = re.search(pattern, text) + if word: + path = folder / word.group(0) + code = CodeParser.parse_code(block=None, text=text, lang=language) + path.write_text(code, encoding="utf-8") diff --git a/tests/metagpt/tools/libs/test_gpt_v_generator.py b/tests/metagpt/tools/libs/test_gpt_v_generator.py index 907006765..96b61ccca 100644 --- a/tests/metagpt/tools/libs/test_gpt_v_generator.py +++ b/tests/metagpt/tools/libs/test_gpt_v_generator.py @@ -60,7 +60,7 @@ async def test_generate_webpages(mock_webpage_filename_with_styles_and_scripts, async def test_save_webpages_with_styles_and_scripts(mock_webpage_filename_with_styles_and_scripts, image_path): generator = GPTvGenerator() webpages = await generator.generate_webpages(image_path) - webpages_dir = generator.save_webpages(image_path=image_path, webpages=webpages) + webpages_dir = generator.save_webpages(webpages=webpages, save_folder_name="test_1") logs.logger.info(webpages_dir) assert webpages_dir.exists() @@ -69,7 +69,7 @@ async def test_save_webpages_with_styles_and_scripts(mock_webpage_filename_with_ async def test_save_webpages_with_style_and_script(mock_webpage_filename_with_style_and_script, image_path): generator = GPTvGenerator() webpages = await generator.generate_webpages(image_path) - webpages_dir = generator.save_webpages(image_path=image_path, webpages=webpages) + webpages_dir = generator.save_webpages(webpages=webpages, save_folder_name="test_2") logs.logger.info(webpages_dir) assert webpages_dir.exists() From 478ba13b5f1e25948d39c634b98bc9c188e80b8c Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Mon, 11 Mar 2024 16:56:47 +0800 Subject: [PATCH 043/131] simplify save_webpages in gpt_v_generator.py --- metagpt/tools/libs/gpt_v_generator.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/metagpt/tools/libs/gpt_v_generator.py b/metagpt/tools/libs/gpt_v_generator.py index bcca0df7b..f24a473bf 100644 --- a/metagpt/tools/libs/gpt_v_generator.py +++ b/metagpt/tools/libs/gpt_v_generator.py @@ -5,7 +5,6 @@ @Author : mannaandpoem @File : gpt_v_generator.py """ -import os import re from pathlib import Path @@ -87,7 +86,7 @@ class GPTvGenerator: # Create a folder called webpages in the workspace directory to store HTML, CSS, and JavaScript files webpages_path = DEFAULT_WORKSPACE_ROOT / "webpages" / save_folder_name logger.info(f"code will be saved at {webpages_path}") - os.makedirs(webpages_path, exist_ok=True) + webpages_path.mkdir(parents=True, exist_ok=True) index_path = webpages_path / "index.html" index_path.write_text(CodeParser.parse_code(block=None, text=webpages, lang="html")) From 09e3a8e1fa98a6d03507f6f4db9ba2a0064d15fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Mon, 11 Mar 2024 17:00:44 +0800 Subject: [PATCH 044/131] refine run_cell and parse_outputs. --- metagpt/actions/mi/execute_nb_code.py | 36 ++++++++----------- .../actions/mi/test_execute_nb_code.py | 5 +-- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/metagpt/actions/mi/execute_nb_code.py b/metagpt/actions/mi/execute_nb_code.py index 632f0076c..217fc8ddc 100644 --- a/metagpt/actions/mi/execute_nb_code.py +++ b/metagpt/actions/mi/execute_nb_code.py @@ -96,11 +96,12 @@ class ExecuteNbCode(Action): assert isinstance(outputs, list) parsed_output, is_success = [], True for i, output in enumerate(outputs): + output_text = "" if output["output_type"] == "stream" and not any( tag in output["text"] for tag in ["| INFO | metagpt", "| ERROR | metagpt", "| WARNING | metagpt", "DEBUG"] ): - ioutput, is_success = remove_escape_and_color_codes(output["text"]), True + output_text = output["text"] elif output["output_type"] == "display_data": if "image/png" in output["data"]: self.show_bytes_figure(output["data"]["image/png"], self.interaction) @@ -108,28 +109,22 @@ class ExecuteNbCode(Action): logger.info( f"{i}th output['data'] from nbclient outputs dont have image/png, continue next output ..." ) - ioutput, is_success = "", True elif output["output_type"] == "execute_result": - no_escape_color_output = remove_escape_and_color_codes(output["data"]["text/plain"]) - ioutput, is_success = no_escape_color_output, True + output_text = output["data"]["text/plain"] elif output["output_type"] == "error": - no_escape_color_output = remove_escape_and_color_codes("\n".join(output["traceback"])) - ioutput, is_success = no_escape_color_output, False + output_text, is_success = "\n".join(output["traceback"]), False # handle coroutines that are not executed asynchronously - if ioutput.strip().startswith(" keep_len and is_success: - prefix = f"Executed code successfully. Truncated to show only first {keep_len} characters\n" - ioutput = prefix + ioutput[:keep_len] - elif len(ioutput) > keep_len and not is_success: - prefix = f"Executed code failed, please reflect the cause of bug and then debug. Truncated to show only last {keep_len} characters\n" - ioutput = prefix + ioutput[-keep_len:] + output_text = remove_escape_and_color_codes(output_text) + # The valid information of the exception is at the end, + # the valid information of Normal output is at the begining. + output_text = output_text[:keep_len] if is_success else output_text[-keep_len:] - parsed_output.append(ioutput) + parsed_output.append(output_text) return is_success, ",".join(parsed_output) def show_bytes_figure(self, image_base64: str, interaction_type: Literal["ipython", None]): @@ -164,7 +159,7 @@ class ExecuteNbCode(Action): """ try: await self.nb_client.async_execute_cell(cell, cell_index) - return True, "" + return self.parse_outputs(self.nb.cells[-1].outputs) except CellTimeoutError: assert self.nb_client.km is not None await self.nb_client.km.interrupt_kernel() @@ -175,7 +170,7 @@ class ExecuteNbCode(Action): await self.reset() return False, "DeadKernelError" except Exception: - return False, "" + return self.parse_outputs(self.nb.cells[-1].outputs) async def run(self, code: str, language: Literal["python", "markdown"] = "python") -> Tuple[str, bool]: """ @@ -192,10 +187,7 @@ class ExecuteNbCode(Action): # run code cell_index = len(self.nb.cells) - 1 - success, error_message = await self.run_cell(self.nb.cells[-1], cell_index) - success, outputs = self.parse_outputs(self.nb.cells[-1].outputs) - if error_message: - outputs = error_message + outputs + success, outputs = await self.run_cell(self.nb.cells[-1], cell_index) if "!pip" in code: success = False diff --git a/tests/metagpt/actions/mi/test_execute_nb_code.py b/tests/metagpt/actions/mi/test_execute_nb_code.py index 4b90289ea..2ecfbd2a2 100644 --- a/tests/metagpt/actions/mi/test_execute_nb_code.py +++ b/tests/metagpt/actions/mi/test_execute_nb_code.py @@ -68,7 +68,7 @@ async def test_run_code_text(): executor = ExecuteNbCode() message, success = await executor.run(code='print("This is a code!")', language="python") assert success - assert message == "This is a code!\n" + assert "This is a code!" in message message, success = await executor.run(code="# This is a code!", language="markdown") assert success assert message == "# This is a code!" @@ -118,10 +118,11 @@ async def test_parse_outputs(): import pandas as pd df = pd.DataFrame({'ID': [1,2,3], 'NAME': ['a', 'b', 'c']}) print(df.columns) + print(f"columns num:{len(df.columns)}") print(df['DUMMPY_ID']) """ output, is_success = await executor.run(code) assert not is_success assert "Index(['ID', 'NAME'], dtype='object')" in output - assert "Executed code failed," in output assert "KeyError: 'DUMMPY_ID'" in output + assert "columns num:2" in output From 980851136fb1e63dc6bd04c33046f68f167b4b6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Mon, 11 Mar 2024 17:07:54 +0800 Subject: [PATCH 045/131] chore --- metagpt/actions/mi/execute_nb_code.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/metagpt/actions/mi/execute_nb_code.py b/metagpt/actions/mi/execute_nb_code.py index 217fc8ddc..f6a8defbd 100644 --- a/metagpt/actions/mi/execute_nb_code.py +++ b/metagpt/actions/mi/execute_nb_code.py @@ -120,8 +120,8 @@ class ExecuteNbCode(Action): is_success = False output_text = remove_escape_and_color_codes(output_text) - # The valid information of the exception is at the end, - # the valid information of Normal output is at the begining. + # The useful information of the exception is at the end, + # the useful information of normal output is at the begining. output_text = output_text[:keep_len] if is_success else output_text[-keep_len:] parsed_output.append(output_text) From 363c1b7ffb0fe9f497dfc83e59cc711067bbf36a Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Mon, 11 Mar 2024 18:15:02 +0800 Subject: [PATCH 046/131] simplify save_webpages in gpt_v_generator.py --- tests/metagpt/tools/libs/test_gpt_v_generator.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/metagpt/tools/libs/test_gpt_v_generator.py b/tests/metagpt/tools/libs/test_gpt_v_generator.py index 96b61ccca..4a2e68682 100644 --- a/tests/metagpt/tools/libs/test_gpt_v_generator.py +++ b/tests/metagpt/tools/libs/test_gpt_v_generator.py @@ -63,6 +63,9 @@ async def test_save_webpages_with_styles_and_scripts(mock_webpage_filename_with_ webpages_dir = generator.save_webpages(webpages=webpages, save_folder_name="test_1") logs.logger.info(webpages_dir) assert webpages_dir.exists() + assert (webpages_dir / "index.html").exists() + assert (webpages_dir / "styles.css").exists() + assert (webpages_dir / "scripts.js").exists() @pytest.mark.asyncio @@ -72,6 +75,9 @@ async def test_save_webpages_with_style_and_script(mock_webpage_filename_with_st webpages_dir = generator.save_webpages(webpages=webpages, save_folder_name="test_2") logs.logger.info(webpages_dir) assert webpages_dir.exists() + assert (webpages_dir / "index.html").exists() + assert (webpages_dir / "style.css").exists() + assert (webpages_dir / "script.js").exists() @pytest.mark.asyncio From bbb9645f7c60c35177922d10ccc7ed4b90d261c3 Mon Sep 17 00:00:00 2001 From: geekan Date: Mon, 11 Mar 2024 19:18:27 +0800 Subject: [PATCH 047/131] readd startup.py --- metagpt/startup.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 metagpt/startup.py diff --git a/metagpt/startup.py b/metagpt/startup.py new file mode 100644 index 000000000..bb6f6abf2 --- /dev/null +++ b/metagpt/startup.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@Time : 2024/3/11 19:16 +@Author : alexanderwu +@File : startup.py +""" + +# DEPRECATED: This file is deprecated and will be removed in the future. +# The startup.py implementation has been moved to software_company.py From 9ad2f22c16247a159186a90c3335125acacd41da Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Mon, 11 Mar 2024 19:59:12 +0800 Subject: [PATCH 048/131] simplify save_webpages in gpt_v_generator.py --- metagpt/tools/libs/gpt_v_generator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/metagpt/tools/libs/gpt_v_generator.py b/metagpt/tools/libs/gpt_v_generator.py index f24a473bf..0e9f34770 100644 --- a/metagpt/tools/libs/gpt_v_generator.py +++ b/metagpt/tools/libs/gpt_v_generator.py @@ -9,9 +9,9 @@ import re from pathlib import Path from metagpt.const import DEFAULT_WORKSPACE_ROOT -from metagpt.tools.tool_registry import register_tool -from metagpt.utils.common import encode_image, CodeParser from metagpt.logs import logger +from metagpt.tools.tool_registry import register_tool +from metagpt.utils.common import CodeParser, encode_image ANALYZE_LAYOUT_PROMPT = """You are now a UI/UX designer, please generate layout information for this image: From e39cafdd580ebcf1b587b6de40a51b5deae3fef7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Mon, 11 Mar 2024 22:25:38 +0800 Subject: [PATCH 049/131] feat: + tree command --- metagpt/utils/tree.py | 99 ++++++++++++++++++++++++++++++++ tests/metagpt/utils/test_tree.py | 54 +++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 metagpt/utils/tree.py create mode 100644 tests/metagpt/utils/test_tree.py diff --git a/metagpt/utils/tree.py b/metagpt/utils/tree.py new file mode 100644 index 000000000..49b5634c6 --- /dev/null +++ b/metagpt/utils/tree.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@Time : 2024/3/11 +@Author : mashenquan +@File : tree.py +@Desc : Implement the same functionality as the `tree` command. +Example: + root + +-- dir1 + | +-- file1.txt + | +-- file2.txt + +-- dir2 + | +-- subdir1 + | | +-- file1.txt + | | +-- file2.txt + | +-- subdir2 + | +-- file1.txt + | +-- file2.txt + +-- file.txt +""" +from __future__ import annotations + +from pathlib import Path +from typing import Callable, Dict, List + +from anthropic import BaseModel +from pydantic import Field + + +class Tree(BaseModel): + """ + Represents a directory tree structure. + + Attributes: + root (str): The root directory of the tree. + tree (Dict[str, Dict]): The tree structure as a dictionary. + + Methods: + print: Print the directory tree structure. + + """ + + root: str + tree: Dict[str, Dict] = Field(default_factory=dict) + + def print(self, git_ignore_rules: Callable = None) -> str: + """ + Recursively traverses the directory structure and prints it out in a tree-like format. + + Args: + git_ignore_rules (Callable): Optional. A function to filter files to ignore. + + Returns: + str: A string representation of the directory tree. + + """ + root = Path(self.root).resolve() + self.tree[root.name] = self._list_children(root=root, git_ignore_rules=git_ignore_rules) + v = self._print_tree(self.tree) + return "\n".join(v) + + @staticmethod + def _list_children(root: Path, git_ignore_rules: Callable) -> Dict[str, Dict]: + tree = {} + for i in root.iterdir(): + if git_ignore_rules and git_ignore_rules(str(i)): + continue + if i.is_file(): + tree[i.name] = {} + else: + tree[i.name] = Tree._list_children(root=i, git_ignore_rules=git_ignore_rules) + return tree + + @staticmethod + def _print_tree(tree: Dict[str:Dict], indent: int = 0) -> List[str]: + ret = [] + for name, children in tree.items(): + ret.append(name) + if not children: + continue + lines = Tree._print_tree(tree=children, indent=indent + 1) + for j, v in enumerate(lines): + if v[0] not in ["+", " ", "|"]: + ret = Tree._add_line(ret) + row = f"+-- {v}" + else: + row = f" {v}" + ret.append(row) + return ret + + @staticmethod + def _add_line(rows: List[str]) -> List[str]: + for i in range(len(rows) - 1, -1, -1): + v = rows[i] + if v[0] != " ": + return rows + rows[i] = "|" + v[1:] + return rows diff --git a/tests/metagpt/utils/test_tree.py b/tests/metagpt/utils/test_tree.py new file mode 100644 index 000000000..0d48f7ce3 --- /dev/null +++ b/tests/metagpt/utils/test_tree.py @@ -0,0 +1,54 @@ +from pathlib import Path +from typing import List + +import pytest +from gitignore_parser import parse_gitignore + +from metagpt.utils.tree import Tree + + +@pytest.mark.parametrize( + ("root", "rules"), + [ + (str(Path(__file__).parent / "../.."), None), + (str(Path(__file__).parent / "../.."), str(Path(__file__).parent / "../../../.gitignore")), + ], +) +def test_tree(root: str, rules: str): + gitignore_rules = parse_gitignore(full_path=rules) if rules else None + tree = Tree(root=root).print(git_ignore_rules=gitignore_rules) + assert tree + + +@pytest.mark.parametrize( + ("tree", "want"), + [ + ({"a": {"b": {}, "c": {}}}, ["a", "+-- b", "+-- c"]), + ({"a": {"b": {}, "c": {"d": {}}}}, ["a", "+-- b", "+-- c", " +-- d"]), + ( + {"a": {"b": {"e": {"f": {}, "g": {}}}, "c": {"d": {}}}}, + ["a", "+-- b", "| +-- e", "| +-- f", "| +-- g", "+-- c", " +-- d"], + ), + ( + {"h": {"a": {"b": {"e": {"f": {}, "g": {}}}, "c": {"d": {}}}, "i": {}}}, + [ + "h", + "+-- a", + "| +-- b", + "| | +-- e", + "| | +-- f", + "| | +-- g", + "| +-- c", + "| +-- d", + "+-- i", + ], + ), + ], +) +def test__print_tree(tree: dict, want: List[str]): + v = Tree._print_tree(tree) + assert v == want + + +if __name__ == "__main__": + pytest.main([__file__, "-s"]) From 6487ae84b0dd1a55053ea396d6fbd3444cda4072 Mon Sep 17 00:00:00 2001 From: Azure Wang Date: Mon, 11 Mar 2024 22:27:59 +0800 Subject: [PATCH 050/131] - updated MAX-TOKEN according to openai document - minior optimization of code style - fixed issues `RuntimeError: fail to reduce message length` --- metagpt/actions/research.py | 2 +- metagpt/config2.py | 3 +-- metagpt/utils/token_counter.py | 33 ++++++++++++++++----------------- tests/mock/mock_llm.py | 3 ++- 4 files changed, 20 insertions(+), 21 deletions(-) diff --git a/metagpt/actions/research.py b/metagpt/actions/research.py index ce8d8a967..65962de31 100644 --- a/metagpt/actions/research.py +++ b/metagpt/actions/research.py @@ -134,7 +134,7 @@ class CollectLinks(Action): break model_name = config.llm.model - prompt = reduce_message_length(gen_msg(), model_name, system_text, 4096) + prompt = reduce_message_length(gen_msg(), model_name, system_text, 0) logger.debug(prompt) queries = await self._aask(prompt, [system_text]) try: diff --git a/metagpt/config2.py b/metagpt/config2.py index bc6af18c6..25569bd93 100644 --- a/metagpt/config2.py +++ b/metagpt/config2.py @@ -92,7 +92,7 @@ class Config(CLIParams, YamlModel): """ default_config_paths: List[Path] = [ METAGPT_ROOT / "config/config2.yaml", - Path.home() / ".metagpt/config2.yaml", + CONFIG_ROOT / "config2.yaml", ] dicts = [dict(os.environ)] @@ -134,4 +134,3 @@ def merge_dict(dicts: Iterable[Dict]) -> Dict: return result -config = Config.default() diff --git a/metagpt/utils/token_counter.py b/metagpt/utils/token_counter.py index f7c53706b..bb53cfe5e 100644 --- a/metagpt/utils/token_counter.py +++ b/metagpt/utils/token_counter.py @@ -140,25 +140,24 @@ FIREWORKS_GRADE_TOKEN_COSTS = { "mixtral-8x7b": {"prompt": 0.4, "completion": 1.6}, } +# https://platform.openai.com/docs/models/gpt-4-and-gpt-4-turbo TOKEN_MAX = { - "gpt-3.5-turbo": 4096, - "gpt-3.5-turbo-0301": 4096, - "gpt-3.5-turbo-0613": 4096, - "gpt-3.5-turbo-16k": 16384, - "gpt-3.5-turbo-16k-0613": 16384, - "gpt-35-turbo": 4096, - "gpt-35-turbo-16k": 16384, - "gpt-3.5-turbo-1106": 16384, - "gpt-4-0314": 8192, - "gpt-4": 8192, - "gpt-4-32k": 32768, - "gpt-4-32k-0314": 32768, - "gpt-4-0613": 8192, - "gpt-4-turbo-preview": 128000, "gpt-4-0125-preview": 128000, + "gpt-4-turbo-preview": 128000, "gpt-4-1106-preview": 128000, "gpt-4-vision-preview": 128000, "gpt-4-1106-vision-preview": 128000, + "gpt-4": 8192, + "gpt-4-0613": 8192, + "gpt-4-32k": 32768, + "gpt-4-32k-0613": 32768, + "gpt-3.5-turbo-0125": 16385, + "gpt-3.5-turbo": 16385, + "gpt-3.5-turbo-1106": 16385, + "gpt-3.5-turbo-instruct": 4096, + "gpt-3.5-turbo-16k": 16385, + "gpt-3.5-turbo-0613": 4096, + "gpt-3.5-turbo-16k-0613": 16385, "text-embedding-ada-002": 8192, "glm-3-turbo": 128000, "glm-4": 128000, @@ -179,7 +178,7 @@ TOKEN_MAX = { } -def count_message_tokens(messages, model="gpt-3.5-turbo-0613"): +def count_message_tokens(messages, model="gpt-3.5-turbo-0125"): """Return the number of tokens used by a list of messages.""" try: encoding = tiktoken.encoding_for_model(model) @@ -209,8 +208,8 @@ def count_message_tokens(messages, model="gpt-3.5-turbo-0613"): tokens_per_message = 4 # every message follows <|start|>{role/name}\n{content}<|end|>\n tokens_per_name = -1 # if there's a name, the role is omitted elif "gpt-3.5-turbo" == model: - print("Warning: gpt-3.5-turbo may update over time. Returning num tokens assuming gpt-3.5-turbo-0613.") - return count_message_tokens(messages, model="gpt-3.5-turbo-0613") + print("Warning: gpt-3.5-turbo may update over time. Returning num tokens assuming gpt-3.5-turbo-0125.") + return count_message_tokens(messages, model="gpt-3.5-turbo-0125") elif "gpt-4" == model: print("Warning: gpt-4 may update over time. Returning num tokens assuming gpt-4-0613.") return count_message_tokens(messages, model="gpt-4-0613") diff --git a/tests/mock/mock_llm.py b/tests/mock/mock_llm.py index b2052e2b3..9c159b4ed 100644 --- a/tests/mock/mock_llm.py +++ b/tests/mock/mock_llm.py @@ -1,13 +1,14 @@ import json from typing import Optional, Union -from metagpt.config2 import config +from metagpt.config2 import Config from metagpt.configs.llm_config import LLMType from metagpt.logs import logger from metagpt.provider.azure_openai_api import AzureOpenAILLM from metagpt.provider.openai_api import OpenAILLM from metagpt.schema import Message +config = Config.default() OriginalLLM = OpenAILLM if config.llm.api_type == LLMType.OPENAI else AzureOpenAILLM From 7d32f9efe6958bf9b76b4309d426c590e3268e76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Tue, 12 Mar 2024 11:10:09 +0800 Subject: [PATCH 051/131] refactor: Replace Tree class with tree() --- metagpt/utils/tree.py | 218 ++++++++++++++++++++----------- tests/metagpt/utils/test_tree.py | 8 +- 2 files changed, 149 insertions(+), 77 deletions(-) diff --git a/metagpt/utils/tree.py b/metagpt/utils/tree.py index 49b5634c6..ad3373f5f 100644 --- a/metagpt/utils/tree.py +++ b/metagpt/utils/tree.py @@ -6,94 +6,166 @@ @File : tree.py @Desc : Implement the same functionality as the `tree` command. Example: - root - +-- dir1 - | +-- file1.txt - | +-- file2.txt - +-- dir2 - | +-- subdir1 - | | +-- file1.txt - | | +-- file2.txt - | +-- subdir2 - | +-- file1.txt - | +-- file2.txt - +-- file.txt + Usage: + >>> print_tree(".") + utils + +-- serialize.py + +-- project_repo.py + +-- tree.py + +-- mmdc_playwright.py + +-- dependency_file.py + +-- index.html + +-- make_sk_kernel.py + +-- token_counter.py + +-- embedding.py + +-- repair_llm_raw_output.py + +-- mermaid.py + +-- parse_html.py + +-- visual_graph_repo.py + +-- special_tokens.py + +-- ahttp_client.py + +-- __init__.py + +-- mmdc_ink.py + +-- di_graph_repository.py + +-- yaml_model.py + +-- cost_manager.py + +-- __pycache__ + | +-- __init__.cpython-39.pyc + | +-- redis.cpython-39.pyc + | +-- singleton.cpython-39.pyc + | +-- mmdc_ink.cpython-39.pyc + | +-- read_document.cpython-39.pyc + | +-- mermaid.cpython-39.pyc + | +-- parse_html.cpython-39.pyc + | +-- human_interaction.cpython-39.pyc + | +-- cost_manager.cpython-39.pyc + | +-- json_to_markdown.cpython-39.pyc + | +-- graph_repository.cpython-39.pyc + | +-- ahttp_client.cpython-39.pyc + | +-- visual_graph_repo.cpython-39.pyc + | +-- file.cpython-39.pyc + | +-- di_graph_repository.cpython-39.pyc + | +-- pycst.cpython-39.pyc + | +-- save_code.cpython-39.pyc + | +-- dependency_file.cpython-39.pyc + | +-- text.cpython-39.pyc + | +-- token_counter.cpython-39.pyc + | +-- project_repo.cpython-39.pyc + | +-- yaml_model.cpython-39.pyc + | +-- serialize.cpython-39.pyc + | +-- git_repository.cpython-39.pyc + | +-- custom_decoder.cpython-39.pyc + | +-- parse_docstring.cpython-39.pyc + | +-- common.cpython-39.pyc + | +-- exceptions.cpython-39.pyc + | +-- repair_llm_raw_output.cpython-39.pyc + | +-- s3.cpython-39.pyc + | +-- embedding.cpython-39.pyc + | +-- make_sk_kernel.cpython-39.pyc + | +-- file_repository.cpython-39.pyc + +-- file.py + +-- save_code.py + +-- common.py + +-- redis.py + +-- text.py + +-- graph_repository.py + +-- singleton.py + +-- recovery_util.py + +-- file_repository.py + +-- pycst.py + +-- exceptions.py + +-- human_interaction.py + +-- highlight.py + +-- mmdc_pyppeteer.py + +-- s3.py + +-- json_to_markdown.py + +-- custom_decoder.py + +-- git_repository.py + +-- read_document.py + +-- parse_docstring.py """ from __future__ import annotations from pathlib import Path from typing import Callable, Dict, List -from anthropic import BaseModel -from pydantic import Field - -class Tree(BaseModel): +def tree(root: str | Path, git_ignore_rules: Callable = None) -> str: """ - Represents a directory tree structure. + Recursively traverses the directory structure and prints it out in a tree-like format. - Attributes: - root (str): The root directory of the tree. - tree (Dict[str, Dict]): The tree structure as a dictionary. + Args: + root (str or Path): The root directory from which to start traversing. + git_ignore_rules (Callable): Optional. A function to filter files to ignore. - Methods: - print: Print the directory tree structure. + Returns: + str: A string representation of the directory tree. + + Example: + >>> tree(".") + utils + +-- serialize.py + +-- project_repo.py + +-- tree.py + +-- mmdc_playwright.py + +-- __pycache__ + | +-- __init__.cpython-39.pyc + | +-- redis.cpython-39.pyc + | +-- singleton.cpython-39.pyc + +-- parse_docstring.py + + >>> from gitignore_parser import parse_gitignore + >>> tree(".", git_ignore_rules=parse_gitignore(full_path="../../.gitignore")) + utils + +-- serialize.py + +-- project_repo.py + +-- tree.py + +-- mmdc_playwright.py + +-- parse_docstring.py """ + root = Path(root).resolve() + dir_ = {root.name: _list_children(root=root, git_ignore_rules=git_ignore_rules)} + v = _print_tree(dir_) + return "\n".join(v) - root: str - tree: Dict[str, Dict] = Field(default_factory=dict) - def print(self, git_ignore_rules: Callable = None) -> str: - """ - Recursively traverses the directory structure and prints it out in a tree-like format. - - Args: - git_ignore_rules (Callable): Optional. A function to filter files to ignore. - - Returns: - str: A string representation of the directory tree. - - """ - root = Path(self.root).resolve() - self.tree[root.name] = self._list_children(root=root, git_ignore_rules=git_ignore_rules) - v = self._print_tree(self.tree) - return "\n".join(v) - - @staticmethod - def _list_children(root: Path, git_ignore_rules: Callable) -> Dict[str, Dict]: - tree = {} - for i in root.iterdir(): - if git_ignore_rules and git_ignore_rules(str(i)): - continue +def _list_children(root: Path, git_ignore_rules: Callable) -> Dict[str, Dict]: + dir_ = {} + for i in root.iterdir(): + if git_ignore_rules and git_ignore_rules(str(i)): + continue + try: if i.is_file(): - tree[i.name] = {} + dir_[i.name] = {} else: - tree[i.name] = Tree._list_children(root=i, git_ignore_rules=git_ignore_rules) - return tree + dir_[i.name] = _list_children(root=i, git_ignore_rules=git_ignore_rules) + except (FileNotFoundError, PermissionError, OSError): + dir_[i.name] = {} + return dir_ - @staticmethod - def _print_tree(tree: Dict[str:Dict], indent: int = 0) -> List[str]: - ret = [] - for name, children in tree.items(): - ret.append(name) - if not children: - continue - lines = Tree._print_tree(tree=children, indent=indent + 1) - for j, v in enumerate(lines): - if v[0] not in ["+", " ", "|"]: - ret = Tree._add_line(ret) - row = f"+-- {v}" - else: - row = f" {v}" - ret.append(row) - return ret - @staticmethod - def _add_line(rows: List[str]) -> List[str]: - for i in range(len(rows) - 1, -1, -1): - v = rows[i] - if v[0] != " ": - return rows - rows[i] = "|" + v[1:] - return rows +def _print_tree(dir_: Dict[str:Dict]) -> List[str]: + ret = [] + for name, children in dir_.items(): + ret.append(name) + if not children: + continue + lines = _print_tree(children) + for j, v in enumerate(lines): + if v[0] not in ["+", " ", "|"]: + ret = _add_line(ret) + row = f"+-- {v}" + else: + row = f" {v}" + ret.append(row) + return ret + + +def _add_line(rows: List[str]) -> List[str]: + for i in range(len(rows) - 1, -1, -1): + v = rows[i] + if v[0] != " ": + return rows + rows[i] = "|" + v[1:] + return rows diff --git a/tests/metagpt/utils/test_tree.py b/tests/metagpt/utils/test_tree.py index 0d48f7ce3..34eae10cf 100644 --- a/tests/metagpt/utils/test_tree.py +++ b/tests/metagpt/utils/test_tree.py @@ -4,7 +4,7 @@ from typing import List import pytest from gitignore_parser import parse_gitignore -from metagpt.utils.tree import Tree +from metagpt.utils.tree import _print_tree, tree @pytest.mark.parametrize( @@ -16,8 +16,8 @@ from metagpt.utils.tree import Tree ) def test_tree(root: str, rules: str): gitignore_rules = parse_gitignore(full_path=rules) if rules else None - tree = Tree(root=root).print(git_ignore_rules=gitignore_rules) - assert tree + v = tree(root=root, git_ignore_rules=gitignore_rules) + assert v @pytest.mark.parametrize( @@ -46,7 +46,7 @@ def test_tree(root: str, rules: str): ], ) def test__print_tree(tree: dict, want: List[str]): - v = Tree._print_tree(tree) + v = _print_tree(tree) assert v == want From 6a8699cd4a8f0f2f6f3a8095c67bc72abe775b35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Tue, 12 Mar 2024 11:14:35 +0800 Subject: [PATCH 052/131] refactor: Replace Tree class with tree() --- metagpt/utils/tree.py | 61 +------------------------------------------ 1 file changed, 1 insertion(+), 60 deletions(-) diff --git a/metagpt/utils/tree.py b/metagpt/utils/tree.py index ad3373f5f..1c0060842 100644 --- a/metagpt/utils/tree.py +++ b/metagpt/utils/tree.py @@ -5,61 +5,18 @@ @Author : mashenquan @File : tree.py @Desc : Implement the same functionality as the `tree` command. -Example: - Usage: + Example: >>> print_tree(".") utils +-- serialize.py +-- project_repo.py +-- tree.py +-- mmdc_playwright.py - +-- dependency_file.py - +-- index.html - +-- make_sk_kernel.py - +-- token_counter.py - +-- embedding.py - +-- repair_llm_raw_output.py - +-- mermaid.py - +-- parse_html.py - +-- visual_graph_repo.py - +-- special_tokens.py - +-- ahttp_client.py - +-- __init__.py - +-- mmdc_ink.py - +-- di_graph_repository.py - +-- yaml_model.py +-- cost_manager.py +-- __pycache__ | +-- __init__.cpython-39.pyc | +-- redis.cpython-39.pyc | +-- singleton.cpython-39.pyc - | +-- mmdc_ink.cpython-39.pyc - | +-- read_document.cpython-39.pyc - | +-- mermaid.cpython-39.pyc - | +-- parse_html.cpython-39.pyc - | +-- human_interaction.cpython-39.pyc - | +-- cost_manager.cpython-39.pyc - | +-- json_to_markdown.cpython-39.pyc - | +-- graph_repository.cpython-39.pyc - | +-- ahttp_client.cpython-39.pyc - | +-- visual_graph_repo.cpython-39.pyc - | +-- file.cpython-39.pyc - | +-- di_graph_repository.cpython-39.pyc - | +-- pycst.cpython-39.pyc - | +-- save_code.cpython-39.pyc - | +-- dependency_file.cpython-39.pyc - | +-- text.cpython-39.pyc - | +-- token_counter.cpython-39.pyc - | +-- project_repo.cpython-39.pyc - | +-- yaml_model.cpython-39.pyc - | +-- serialize.cpython-39.pyc - | +-- git_repository.cpython-39.pyc - | +-- custom_decoder.cpython-39.pyc - | +-- parse_docstring.cpython-39.pyc - | +-- common.cpython-39.pyc - | +-- exceptions.cpython-39.pyc - | +-- repair_llm_raw_output.cpython-39.pyc - | +-- s3.cpython-39.pyc | +-- embedding.cpython-39.pyc | +-- make_sk_kernel.cpython-39.pyc | +-- file_repository.cpython-39.pyc @@ -67,22 +24,6 @@ Example: +-- save_code.py +-- common.py +-- redis.py - +-- text.py - +-- graph_repository.py - +-- singleton.py - +-- recovery_util.py - +-- file_repository.py - +-- pycst.py - +-- exceptions.py - +-- human_interaction.py - +-- highlight.py - +-- mmdc_pyppeteer.py - +-- s3.py - +-- json_to_markdown.py - +-- custom_decoder.py - +-- git_repository.py - +-- read_document.py - +-- parse_docstring.py """ from __future__ import annotations From a680a1a82fd16501c47f89c6efda0b756705a415 Mon Sep 17 00:00:00 2001 From: yzlin Date: Tue, 12 Mar 2024 16:11:57 +0800 Subject: [PATCH 053/131] update examples --- examples/di/crawl_webpage.py | 26 ++++++++++++++++--- examples/di/data_visualization.py | 2 +- examples/di/email_summary.py | 2 +- examples/di/imitate_webpage.py | 2 +- ...ools.py => machine_learning_with_tools.py} | 4 +-- examples/di/rm_image_background.py | 2 +- examples/di/sd_tool_usage.py | 2 +- examples/di/solve_math_problems.py | 2 +- metagpt/roles/di/data_interpreter.py | 2 +- metagpt/tools/libs/sd_engine.py | 1 - metagpt/tools/tool_recommend.py | 3 +-- 11 files changed, 32 insertions(+), 16 deletions(-) rename examples/di/{ml_engineer_with_tools.py => machine_learning_with_tools.py} (88%) diff --git a/examples/di/crawl_webpage.py b/examples/di/crawl_webpage.py index f06b85d9b..b8226f4f4 100644 --- a/examples/di/crawl_webpage.py +++ b/examples/di/crawl_webpage.py @@ -7,13 +7,31 @@ from metagpt.roles.di.data_interpreter import DataInterpreter +PAPER_LIST_REQ = """" +Get data from `paperlist` table in https://papercopilot.com/statistics/iclr-statistics/iclr-2024-statistics/, +and save it to a csv file. paper title must include `multiagent` or `large language model`. *notice: print key variables* +""" + +ECOMMERCE_REQ = """ +Get products data from website https://scrapeme.live/shop/ and save it as a csv file. +**Notice: Firstly parse the web page encoding and the text HTML structure; +The first page product name, price, product URL, and image URL must be saved in the csv;** +""" + +NEWS_36KR_REQ = """从36kr创投平台https://pitchhub.36kr.com/financing-flash 所有初创企业融资的信息, **注意: 这是一个中文网站**; +下面是一个大致流程, 你会根据每一步的运行结果对当前计划中的任务做出适当调整: +1. 爬取并本地保存html结构; +2. 直接打印第7个*`快讯`*关键词后2000个字符的html内容, 作为*快讯的html内容示例*; +3. 反思*快讯的html内容示例*中的规律, 设计正则匹配表达式来获取*`快讯`*的标题、链接、时间; +4. 筛选最近3天的初创企业融资*`快讯`*, 以list[dict]形式打印前5个。 +5. 将全部结果存在本地csv中 +""" + async def main(): - prompt = """Get data from `paperlist` table in https://papercopilot.com/statistics/iclr-statistics/iclr-2024-statistics/, - and save it to a csv file. paper title must include `multiagent` or `large language model`. *notice: print key variables*""" - di = DataInterpreter(use_tools=True) + di = DataInterpreter(tools=["scrape_web_playwright"]) - await di.run(prompt) + await di.run(ECOMMERCE_REQ) if __name__ == "__main__": diff --git a/examples/di/data_visualization.py b/examples/di/data_visualization.py index 9af72dc42..1a21ab7cb 100644 --- a/examples/di/data_visualization.py +++ b/examples/di/data_visualization.py @@ -4,7 +4,7 @@ from metagpt.roles.di.data_interpreter import DataInterpreter async def main(requirement: str = ""): - di = DataInterpreter(use_tools=False) + di = DataInterpreter() await di.run(requirement) diff --git a/examples/di/email_summary.py b/examples/di/email_summary.py index af081fee2..7c112767c 100644 --- a/examples/di/email_summary.py +++ b/examples/di/email_summary.py @@ -22,7 +22,7 @@ async def main(): Firstly, Please help me fetch the latest 5 senders and full letter contents. Then, summarize each of the 5 emails into one sentence (you can do this by yourself, no need to import other models to do this) and output them in a markdown format.""" - di = DataInterpreter(use_tools=True) + di = DataInterpreter() await di.run(prompt) diff --git a/examples/di/imitate_webpage.py b/examples/di/imitate_webpage.py index e2c99b874..b00157d9d 100644 --- a/examples/di/imitate_webpage.py +++ b/examples/di/imitate_webpage.py @@ -15,7 +15,7 @@ Firstly, utilize Selenium and WebDriver for rendering. Secondly, convert image to a webpage including HTML, CSS and JS in one go. Finally, save webpage in a text file. Note: All required dependencies and environments have been fully installed and configured.""" - di = DataInterpreter(use_tools=True) + di = DataInterpreter(tools=["GPTvGenerator"]) await di.run(prompt) diff --git a/examples/di/ml_engineer_with_tools.py b/examples/di/machine_learning_with_tools.py similarity index 88% rename from examples/di/ml_engineer_with_tools.py rename to examples/di/machine_learning_with_tools.py index 6119ad843..42c0ef55b 100644 --- a/examples/di/ml_engineer_with_tools.py +++ b/examples/di/machine_learning_with_tools.py @@ -1,10 +1,10 @@ import asyncio -from metagpt.roles.di.ml_engineer import MLEngineer +from metagpt.roles.di.data_interpreter import DataInterpreter async def main(requirement: str): - role = MLEngineer(auto_run=True, use_tools=True) + role = DataInterpreter(tools=[""]) await role.run(requirement) diff --git a/examples/di/rm_image_background.py b/examples/di/rm_image_background.py index b74a79eeb..cb7900a0a 100644 --- a/examples/di/rm_image_background.py +++ b/examples/di/rm_image_background.py @@ -4,7 +4,7 @@ from metagpt.roles.di.data_interpreter import DataInterpreter async def main(requirement: str = ""): - di = DataInterpreter(use_tools=False) + di = DataInterpreter() await di.run(requirement) diff --git a/examples/di/sd_tool_usage.py b/examples/di/sd_tool_usage.py index 69c7df5bd..b373a6251 100644 --- a/examples/di/sd_tool_usage.py +++ b/examples/di/sd_tool_usage.py @@ -8,7 +8,7 @@ from metagpt.roles.di.data_interpreter import DataInterpreter async def main(requirement: str = ""): - di = DataInterpreter(use_tools=True, goal=requirement) + di = DataInterpreter(tools=["SDEngine"]) await di.run(requirement) diff --git a/examples/di/solve_math_problems.py b/examples/di/solve_math_problems.py index 35a69c953..ae2c1b942 100644 --- a/examples/di/solve_math_problems.py +++ b/examples/di/solve_math_problems.py @@ -4,7 +4,7 @@ from metagpt.roles.di.data_interpreter import DataInterpreter async def main(requirement: str = ""): - di = DataInterpreter(use_tools=False) + di = DataInterpreter() await di.run(requirement) diff --git a/metagpt/roles/di/data_interpreter.py b/metagpt/roles/di/data_interpreter.py index 0a149e901..7eb5b9e8a 100644 --- a/metagpt/roles/di/data_interpreter.py +++ b/metagpt/roles/di/data_interpreter.py @@ -9,7 +9,7 @@ from metagpt.actions.di.ask_review import ReviewConst from metagpt.actions.di.execute_nb_code import ExecuteNbCode from metagpt.actions.di.write_analysis_code import CheckData, WriteCodeWithTools from metagpt.logs import logger -from metagpt.prompts.mi.write_analysis_code import DATA_INFO +from metagpt.prompts.di.write_analysis_code import DATA_INFO from metagpt.roles import Role from metagpt.schema import Message, Task, TaskResult from metagpt.strategy.task_type import TaskType diff --git a/metagpt/tools/libs/sd_engine.py b/metagpt/tools/libs/sd_engine.py index 0e3042f5c..b62e39db8 100644 --- a/metagpt/tools/libs/sd_engine.py +++ b/metagpt/tools/libs/sd_engine.py @@ -14,7 +14,6 @@ import requests from aiohttp import ClientSession from PIL import Image, PngImagePlugin -# from metagpt.const import SD_OUTPUT_FILE_REPO, SOURCE_ROOT from metagpt.logs import logger from metagpt.tools.tool_registry import register_tool diff --git a/metagpt/tools/tool_recommend.py b/metagpt/tools/tool_recommend.py index fcdbc4254..9e9bf4a01 100644 --- a/metagpt/tools/tool_recommend.py +++ b/metagpt/tools/tool_recommend.py @@ -57,8 +57,7 @@ class RecommendTool(Action): class ToolRecommender(BaseModel): """ The default ToolRecommender: - 1. Recall: If plan exists, use exact match between task type and tool type to recall tools; - If plan doesn't exist (e.g. we use ReAct), return all user-specified tools; + 1. Recall: To be implemented in subclasses. Recall tools based on the given context and plan. 2. Rank: Use LLM to select final candidates from recalled set. """ From e960ac8dc8eafca456e902890d4825b7622a2677 Mon Sep 17 00:00:00 2001 From: yzlin Date: Tue, 12 Mar 2024 16:43:24 +0800 Subject: [PATCH 054/131] update requirement and example, recover legacy code --- ...h_tools.py => machine_learning_complex.py} | 2 +- metagpt/strategy/task_type.py | 10 +++++---- metagpt/tools/tool_recommend.py | 22 ++++++++++++++++++- requirements.txt | 2 ++ 4 files changed, 30 insertions(+), 6 deletions(-) rename examples/di/{machine_learning_with_tools.py => machine_learning_complex.py} (92%) diff --git a/examples/di/machine_learning_with_tools.py b/examples/di/machine_learning_complex.py similarity index 92% rename from examples/di/machine_learning_with_tools.py rename to examples/di/machine_learning_complex.py index 42c0ef55b..42059ac4f 100644 --- a/examples/di/machine_learning_with_tools.py +++ b/examples/di/machine_learning_complex.py @@ -4,7 +4,7 @@ from metagpt.roles.di.data_interpreter import DataInterpreter async def main(requirement: str): - role = DataInterpreter(tools=[""]) + role = DataInterpreter(use_reflection=True, tools=[""]) await role.run(requirement) diff --git a/metagpt/strategy/task_type.py b/metagpt/strategy/task_type.py index 28a86f100..9eeeb79ce 100644 --- a/metagpt/strategy/task_type.py +++ b/metagpt/strategy/task_type.py @@ -19,29 +19,31 @@ class TaskTypeDef(BaseModel): class TaskType(Enum): + """By identifying specific types of tasks, we can inject human priors (guidance) to help task solving""" + EDA = TaskTypeDef( name="eda", desc="For performing exploratory data analysis", guidance=EDA_PROMPT, ) DATA_PREPROCESS = TaskTypeDef( - name="data_preprocess", + name="data preprocessing", desc="For preprocessing dataset in a data analysis or machine learning task ONLY," "general data operation doesn't fall into this type", guidance=DATA_PREPROCESS_PROMPT, ) FEATURE_ENGINEERING = TaskTypeDef( - name="feature_engineering", + name="feature engineering", desc="Only for creating new columns for input data.", guidance=FEATURE_ENGINEERING_PROMPT, ) MODEL_TRAIN = TaskTypeDef( - name="model_train", + name="model train", desc="Only for training model.", guidance=MODEL_TRAIN_PROMPT, ) MODEL_EVALUATE = TaskTypeDef( - name="model_evaluate", + name="model evaluate", desc="Only for evaluating model.", guidance=MODEL_EVALUATE_PROMPT, ) diff --git a/metagpt/tools/tool_recommend.py b/metagpt/tools/tool_recommend.py index 9e9bf4a01..9b00a7379 100644 --- a/metagpt/tools/tool_recommend.py +++ b/metagpt/tools/tool_recommend.py @@ -62,7 +62,7 @@ class ToolRecommender(BaseModel): """ tools: dict[str, Tool] = {} - force: bool = False + force: bool = False # whether to forcedly recommend the specified tools @field_validator("tools", mode="before") @classmethod @@ -145,6 +145,26 @@ class ToolRecommender(BaseModel): return list(valid_tools.values())[:topk] +class TypeMatchToolRecommender(ToolRecommender): + """ + A legacy ToolRecommender using task type matching at the recall stage: + 1. Recall: Find tools based on exact match between task type and tool tag; + 2. Rank: LLM rank, the same as the default ToolRecommender. + """ + + async def recall_tools(self, context: str = "", plan: Plan = None, topk: int = 20) -> list[Tool]: + if not plan: + return list(self.tools.values())[:topk] + + # find tools based on exact match between task type and tool tag + task_type = plan.current_task.task_type + candidate_tools = TOOL_REGISTRY.get_tools_by_tag(task_type) + candidate_tool_names = set(self.tools.keys()) & candidate_tools.keys() + recalled_tools = [candidate_tools[tool_name] for tool_name in candidate_tool_names] + + return recalled_tools[:topk] + + class BM25ToolRecommender(ToolRecommender): """ A ToolRecommender using BM25 at the recall stage: diff --git a/requirements.txt b/requirements.txt index 64b174913..d0ee8c95c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -71,3 +71,5 @@ Pillow imap_tools==1.5.0 # Used by metagpt/tools/libs/email_login.py qianfan==0.3.2 dashscope==1.14.1 +rank-bm25==0.2.2 # for tool recommendation +jieba==0.42.1 # for tool recommendation \ No newline at end of file From 73d0d29e81d2bcde9a6e1f9f511be7877a619a01 Mon Sep 17 00:00:00 2001 From: Azure Wang Date: Tue, 12 Mar 2024 16:49:04 +0800 Subject: [PATCH 055/131] - change reserved parameter back to `config.llm.max_token` --- metagpt/actions/research.py | 2 +- metagpt/config2.py | 1 + tests/mock/mock_llm.py | 3 +-- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/metagpt/actions/research.py b/metagpt/actions/research.py index 65962de31..2a99a8d99 100644 --- a/metagpt/actions/research.py +++ b/metagpt/actions/research.py @@ -134,7 +134,7 @@ class CollectLinks(Action): break model_name = config.llm.model - prompt = reduce_message_length(gen_msg(), model_name, system_text, 0) + prompt = reduce_message_length(gen_msg(), model_name, system_text, config.llm.max_token) logger.debug(prompt) queries = await self._aask(prompt, [system_text]) try: diff --git a/metagpt/config2.py b/metagpt/config2.py index 25569bd93..ed1d23fa5 100644 --- a/metagpt/config2.py +++ b/metagpt/config2.py @@ -134,3 +134,4 @@ def merge_dict(dicts: Iterable[Dict]) -> Dict: return result +config = Config.default() diff --git a/tests/mock/mock_llm.py b/tests/mock/mock_llm.py index 9c159b4ed..b2052e2b3 100644 --- a/tests/mock/mock_llm.py +++ b/tests/mock/mock_llm.py @@ -1,14 +1,13 @@ import json from typing import Optional, Union -from metagpt.config2 import Config +from metagpt.config2 import config from metagpt.configs.llm_config import LLMType from metagpt.logs import logger from metagpt.provider.azure_openai_api import AzureOpenAILLM from metagpt.provider.openai_api import OpenAILLM from metagpt.schema import Message -config = Config.default() OriginalLLM = OpenAILLM if config.llm.api_type == LLMType.OPENAI else AzureOpenAILLM From a585ef52a60b3e2f4dfff19d512d9835cddf377e Mon Sep 17 00:00:00 2001 From: yzlin Date: Tue, 12 Mar 2024 17:50:10 +0800 Subject: [PATCH 056/131] update examples --- examples/di/imitate_webpage.py | 3 +-- examples/di/solve_math_problems.py | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/di/imitate_webpage.py b/examples/di/imitate_webpage.py index b00157d9d..60ebab389 100644 --- a/examples/di/imitate_webpage.py +++ b/examples/di/imitate_webpage.py @@ -12,8 +12,7 @@ async def main(): web_url = "https://pytorch.org/" prompt = f"""This is a URL of webpage: '{web_url}' . Firstly, utilize Selenium and WebDriver for rendering. -Secondly, convert image to a webpage including HTML, CSS and JS in one go. -Finally, save webpage in a text file. +Secondly, convert image to a webpage including HTML, CSS and JS in one go. Note: All required dependencies and environments have been fully installed and configured.""" di = DataInterpreter(tools=["GPTvGenerator"]) diff --git a/examples/di/solve_math_problems.py b/examples/di/solve_math_problems.py index ae2c1b942..f7fd3d4e3 100644 --- a/examples/di/solve_math_problems.py +++ b/examples/di/solve_math_problems.py @@ -10,4 +10,5 @@ async def main(requirement: str = ""): if __name__ == "__main__": requirement = "Solve this math problem: The greatest common divisor of positive integers m and n is 6. The least common multiple of m and n is 126. What is the least possible value of m + n?" + # answer: 60 (m = 18, n = 42) asyncio.run(main(requirement)) From 19c0db88218933333689654cfb8452d692aed868 Mon Sep 17 00:00:00 2001 From: yzlin Date: Tue, 12 Mar 2024 18:05:50 +0800 Subject: [PATCH 057/131] fix empty choices error when using azure --- metagpt/provider/openai_api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py index 2126505c7..8f3b71c42 100644 --- a/metagpt/provider/openai_api.py +++ b/metagpt/provider/openai_api.py @@ -87,7 +87,9 @@ class OpenAILLM(BaseLLM): collected_messages = [] async for chunk in response: chunk_message = chunk.choices[0].delta.content or "" if chunk.choices else "" # extract the message - finish_reason = chunk.choices[0].finish_reason if hasattr(chunk.choices[0], "finish_reason") else None + finish_reason = ( + chunk.choices[0].finish_reason if chunk.choices and hasattr(chunk.choices[0], "finish_reason") else None + ) log_llm_stream(chunk_message) collected_messages.append(chunk_message) if finish_reason: From 3b001572d905490e2f98e871d0745772e60b199c Mon Sep 17 00:00:00 2001 From: yzlin Date: Tue, 12 Mar 2024 21:19:25 +0800 Subject: [PATCH 058/131] update tests, rm unused --- metagpt/actions/__init__.py | 4 +- metagpt/actions/di/write_analysis_code.py | 6 +- metagpt/actions/di/write_plan.py | 8 +- metagpt/roles/di/data_interpreter.py | 11 +- tests/metagpt/actions/di/test_debug_code.py | 51 --- tests/metagpt/actions/di/test_ml_action.py | 46 --- .../actions/di/test_write_analysis_code.py | 329 ++---------------- tests/metagpt/actions/di/test_write_plan.py | 6 +- .../metagpt/roles/di/test_data_interpreter.py | 17 +- tests/metagpt/roles/di/test_ml_engineer.py | 90 ----- tests/mock/mock_llm.py | 20 +- 11 files changed, 74 insertions(+), 514 deletions(-) delete mode 100644 tests/metagpt/actions/di/test_debug_code.py delete mode 100644 tests/metagpt/actions/di/test_ml_action.py delete mode 100644 tests/metagpt/roles/di/test_ml_engineer.py diff --git a/metagpt/actions/__init__.py b/metagpt/actions/__init__.py index 18140b726..495ed4031 100644 --- a/metagpt/actions/__init__.py +++ b/metagpt/actions/__init__.py @@ -23,7 +23,7 @@ from metagpt.actions.write_prd import WritePRD from metagpt.actions.write_prd_review import WritePRDReview from metagpt.actions.write_test import WriteTest from metagpt.actions.di.execute_nb_code import ExecuteNbCode -from metagpt.actions.di.write_analysis_code import WriteCodeWithTools +from metagpt.actions.di.write_analysis_code import WriteAnalysisCode from metagpt.actions.di.write_plan import WritePlan @@ -46,7 +46,7 @@ class ActionType(Enum): WEB_BROWSE_AND_SUMMARIZE = WebBrowseAndSummarize CONDUCT_RESEARCH = ConductResearch EXECUTE_NB_CODE = ExecuteNbCode - WRITE_CODE_WITH_TOOLS = WriteCodeWithTools + WRITE_ANALYSIS_CODE = WriteAnalysisCode WRITE_PLAN = WritePlan diff --git a/metagpt/actions/di/write_analysis_code.py b/metagpt/actions/di/write_analysis_code.py index d704bef24..97eb81def 100644 --- a/metagpt/actions/di/write_analysis_code.py +++ b/metagpt/actions/di/write_analysis_code.py @@ -21,9 +21,7 @@ from metagpt.schema import Message, Plan from metagpt.utils.common import CodeParser, process_message, remove_comments -class WriteCodeWithTools(Action): - """Write code with help of local available tools. Choose tools first, then generate code to use the tools""" - +class WriteAnalysisCode(Action): async def _debug_with_reflection(self, context: list[Message], working_memory: list[Message]): reflection_prompt = REFLECTION_PROMPT.format( debug_example=DEBUG_REFLECTION_EXAMPLE, @@ -67,7 +65,7 @@ class WriteCodeWithTools(Action): class CheckData(Action): - async def run(self, plan: Plan = None) -> dict: + async def run(self, plan: Plan) -> dict: finished_tasks = plan.get_finished_tasks() code_written = [remove_comments(task.code) for task in finished_tasks] code_written = "\n\n".join(code_written) diff --git a/metagpt/actions/di/write_plan.py b/metagpt/actions/di/write_plan.py index b190733fc..2dbe3f0e7 100644 --- a/metagpt/actions/di/write_plan.py +++ b/metagpt/actions/di/write_plan.py @@ -41,7 +41,7 @@ class WritePlan(Action): ``` """ - async def run(self, context: list[Message], max_tasks: int = 5, use_tools: bool = False) -> str: + async def run(self, context: list[Message], max_tasks: int = 5) -> str: task_type_desc = "\n".join([f"- **{tt.type_name}**: {tt.value.desc}" for tt in TaskType]) prompt = self.PROMPT_TEMPLATE.format( context="\n".join([str(ct) for ct in context]), max_tasks=max_tasks, task_type_desc=task_type_desc @@ -51,14 +51,10 @@ class WritePlan(Action): return rsp -def rsp_to_tasks(rsp: str) -> list[Task]: +def update_plan_from_rsp(rsp: str, current_plan: Plan): rsp = json.loads(rsp) tasks = [Task(**task_config) for task_config in rsp] - return tasks - -def update_plan_from_rsp(rsp: str, current_plan: Plan): - tasks = rsp_to_tasks(rsp) if len(tasks) == 1 or tasks[0].dependent_task_ids: if tasks[0].dependent_task_ids and len(tasks) > 1: # tasks[0].dependent_task_ids means the generated tasks are not a complete plan diff --git a/metagpt/roles/di/data_interpreter.py b/metagpt/roles/di/data_interpreter.py index 7eb5b9e8a..c24c78a90 100644 --- a/metagpt/roles/di/data_interpreter.py +++ b/metagpt/roles/di/data_interpreter.py @@ -7,7 +7,7 @@ from pydantic import Field, model_validator from metagpt.actions.di.ask_review import ReviewConst from metagpt.actions.di.execute_nb_code import ExecuteNbCode -from metagpt.actions.di.write_analysis_code import CheckData, WriteCodeWithTools +from metagpt.actions.di.write_analysis_code import CheckData, WriteAnalysisCode from metagpt.logs import logger from metagpt.prompts.di.write_analysis_code import DATA_INFO from metagpt.roles import Role @@ -52,7 +52,7 @@ class DataInterpreter(Role): ) # create a flag for convenience, overwrite any passed-in value if self.tools: self.tool_recommender = BM25ToolRecommender(tools=self.tools) - self.set_actions([WriteCodeWithTools]) + self.set_actions([WriteAnalysisCode]) return self @property @@ -82,11 +82,12 @@ class DataInterpreter(Role): async def _act(self) -> Message: """Useful in 'react' mode. Return a Message conforming to Role._act interface.""" code, _, _ = await self._write_and_exec_code() - return Message(content=code, role="assistant", cause_by=WriteCodeWithTools) + return Message(content=code, role="assistant", cause_by=WriteAnalysisCode) async def _plan_and_act(self) -> Message: - await super()._plan_and_act() + rsp = await super()._plan_and_act() await self.execute_code.terminate() + return rsp async def _act_on_task(self, current_task: Task) -> TaskResult: """Useful in 'plan_and_act' mode. Wrap the output in a TaskResult for review and confirmation.""" @@ -143,7 +144,7 @@ class DataInterpreter(Role): plan_status="", tool_info="", ): - todo = WriteCodeWithTools() + todo = WriteAnalysisCode() logger.info(f"ready to {todo.name}") use_reflection = counter > 0 and self.use_reflection diff --git a/tests/metagpt/actions/di/test_debug_code.py b/tests/metagpt/actions/di/test_debug_code.py deleted file mode 100644 index 67f72ad63..000000000 --- a/tests/metagpt/actions/di/test_debug_code.py +++ /dev/null @@ -1,51 +0,0 @@ -# -*- coding: utf-8 -*- -# @Date : 1/11/2024 8:51 PM -# @Author : stellahong (stellahong@fuzhi.ai) -# @Desc : - -import pytest - -from metagpt.actions.di.debug_code import DebugCode -from metagpt.schema import Message - -ErrorStr = """Tested passed: - -Tests failed: -assert sort_array([1, 5, 2, 3, 4]) == [1, 2, 3, 4, 5] # output: [1, 2, 4, 3, 5] -""" - -CODE = """ -def sort_array(arr): - # Helper function to count the number of ones in the binary representation - def count_ones(n): - return bin(n).count('1') - - # Sort the array using a custom key function - # The key function returns a tuple (number of ones, value) for each element - # This ensures that if two elements have the same number of ones, they are sorted by their value - sorted_arr = sorted(arr, key=lambda x: (count_ones(x), x)) - - return sorted_arr -``` -""" - -DebugContext = '''Solve the problem in Python: -def sort_array(arr): - """ - In this Kata, you have to sort an array of non-negative integers according to - number of ones in their binary representation in ascending order. - For similar number of ones, sort based on decimal value. - - It must be implemented like this: - >>> sort_array([1, 5, 2, 3, 4]) == [1, 2, 3, 4, 5] - >>> sort_array([-2, -3, -4, -5, -6]) == [-6, -5, -4, -3, -2] - >>> sort_array([1, 0, 2, 3, 4]) [0, 1, 2, 3, 4] - """ -''' - - -@pytest.mark.asyncio -async def test_debug_code(): - debug_context = Message(content=DebugContext) - new_code = await DebugCode().run(context=debug_context, code=CODE, runtime_result=ErrorStr) - assert "def sort_array(arr)" in new_code["code"] diff --git a/tests/metagpt/actions/di/test_ml_action.py b/tests/metagpt/actions/di/test_ml_action.py deleted file mode 100644 index 826a7fcf2..000000000 --- a/tests/metagpt/actions/di/test_ml_action.py +++ /dev/null @@ -1,46 +0,0 @@ -import pytest - -from metagpt.actions.di.ml_action import WriteCodeWithToolsML -from metagpt.schema import Plan, Task - - -@pytest.mark.asyncio -async def test_write_code_with_tools(): - write_code_ml = WriteCodeWithToolsML() - - task_map = { - "1": Task( - task_id="1", - instruction="随机生成一个pandas DataFrame数据集", - task_type="other", - dependent_task_ids=[], - code=""" - import pandas as pd - df = pd.DataFrame({ - 'a': [1, 2, 3, 4, 5], - 'b': [1.1, 2.2, 3.3, 4.4, np.nan], - 'c': ['aa', 'bb', 'cc', 'dd', 'ee'], - 'd': [1, 2, 3, 4, 5] - }) - """, - is_finished=True, - ), - "2": Task( - task_id="2", - instruction="对数据集进行数据清洗", - task_type="data_preprocess", - dependent_task_ids=["1"], - ), - } - plan = Plan( - goal="构造数据集并进行数据清洗", - tasks=list(task_map.values()), - task_map=task_map, - current_task_id="2", - ) - column_info = "" - - _, code_with_ml = await write_code_ml.run([], plan, column_info) - code_with_ml = code_with_ml["code"] - assert len(code_with_ml) > 0 - print(code_with_ml) diff --git a/tests/metagpt/actions/di/test_write_analysis_code.py b/tests/metagpt/actions/di/test_write_analysis_code.py index d6bbae1c6..b1e51d8ce 100644 --- a/tests/metagpt/actions/di/test_write_analysis_code.py +++ b/tests/metagpt/actions/di/test_write_analysis_code.py @@ -1,134 +1,41 @@ -import asyncio - import pytest -from metagpt.actions.di.execute_nb_code import ExecuteNbCode -from metagpt.actions.di.write_analysis_code import ( - WriteCodeWithoutTools, - WriteCodeWithTools, -) -from metagpt.logs import logger -from metagpt.schema import Message, Plan, Task -from metagpt.strategy.planner import STRUCTURAL_CONTEXT - - -@pytest.mark.skip -@pytest.mark.asyncio -async def test_write_code_by_list_plan(): - write_code = WriteCodeWithoutTools() - execute_code = ExecuteNbCode() - messages = [] - plan = ["随机生成一个pandas DataFrame时间序列", "绘制这个时间序列的直方图", "回顾已完成的任务", "求均值", "总结"] - for task in plan: - print(f"\n任务: {task}\n\n") - messages.append(Message(task, role="assistant")) - code = await write_code.run(messages) - if task.startswith(("回顾", "总结")): - assert code["language"] == "markdown" - else: - assert code["language"] == "python" - messages.append(Message(code["code"], role="assistant")) - assert len(code) > 0 - output, _ = await execute_code.run(**code) - print(f"\n[Output]: 任务{task}的执行结果是: \n{output}\n") - messages.append(output) +from metagpt.actions.di.write_analysis_code import WriteAnalysisCode +from metagpt.schema import Message @pytest.mark.asyncio -async def test_tool_recommendation(): - task = "clean and preprocess the data" - available_tools = { - "FillMissingValue": "Filling missing values", - "SplitBins": "Bin continuous data into intervals and return the bin identifier encoded as an integer value", - } - write_code = WriteCodeWithTools() - tools = await write_code._recommend_tool(task, available_tools) +async def test_write_code(): + write_code = WriteAnalysisCode() - assert len(tools) == 1 - assert "FillMissingValue" in tools + user_requirement = "Run data analysis on sklearn Iris dataset, include a plot" + plan_status = "\n## Finished Tasks\n### code\n```python\n\n```\n\n### execution result\n\n\n## Current Task\nLoad the sklearn Iris dataset and perform exploratory data analysis\n\n## Task Guidance\nWrite complete code for 'Current Task'. And avoid duplicating code from 'Finished Tasks', such as repeated import of packages, reading data, etc.\nSpecifically, \nThe current task is about exploratory data analysis, please note the following:\n- Distinguish column types with `select_dtypes` for tailored analysis and visualization, such as correlation.\n- Remember to `import numpy as np` before using Numpy functions.\n\n" - -@pytest.mark.asyncio -async def test_write_code_with_tools(): - write_code = WriteCodeWithTools() - - requirement = "构造数据集并进行数据清洗" - task_map = { - "1": Task( - task_id="1", - instruction="随机生成一个pandas DataFrame数据集", - task_type="other", - dependent_task_ids=[], - code=""" - import pandas as pd - df = pd.DataFrame({ - 'a': [1, 2, 3, 4, 5], - 'b': [1.1, 2.2, 3.3, 4.4, np.nan], - 'c': ['aa', 'bb', 'cc', 'dd', 'ee'], - 'd': [1, 2, 3, 4, 5] - }) - """, - is_finished=True, - ), - "2": Task( - task_id="2", - instruction="对数据集进行数据清洗", - task_type="data_preprocess", - dependent_task_ids=["1"], - ), - } - plan = Plan( - goal="构造数据集并进行数据清洗", - tasks=list(task_map.values()), - task_map=task_map, - current_task_id="2", - ) - - context = STRUCTURAL_CONTEXT.format( - user_requirement=requirement, - context=plan.context, - tasks=list(task_map.values()), - current_task=plan.current_task.model_dump_json(), - ) - context_msg = [Message(content=context, role="user")] - - code = await write_code.run(context_msg, plan) - code = code["code"] + code = await write_code.run(user_requirement=user_requirement, plan_status=plan_status) assert len(code) > 0 - print(code) + assert "sklearn" in code @pytest.mark.asyncio -async def test_write_code_to_correct_error(): - structural_context = """ - ## User Requirement - read a dataset test.csv and print its head - ## Current Plan - [ - { - "task_id": "1", - "dependent_task_ids": [], - "instruction": "import pandas and load the dataset from 'test.csv'.", - "task_type": "", - "code": "", - "result": "", - "is_finished": false - }, - { - "task_id": "2", - "dependent_task_ids": [ - "1" - ], - "instruction": "Print the head of the dataset to display the first few rows.", - "task_type": "", - "code": "", - "result": "", - "is_finished": false - } - ] +async def test_debug_with_reflection(): + user_requirement = "Run data analysis on sklearn Iris dataset, include a plot" + + plan_status = """ + ## Finished Tasks + ### code + ```python + ``` + + ### execution result + ## Current Task - {"task_id": "1", "dependent_task_ids": [], "instruction": "import pandas and load the dataset from 'test.csv'.", "task_type": "", "code": "", "result": "", "is_finished": false} + import pandas and load the dataset from 'test.csv'. + + ## Task Guidance + Write complete code for 'Current Task'. And avoid duplicating code from 'Finished Tasks', such as repeated import of packages, reading data, etc. + Specifically, """ + wrong_code = """import pandas as pd\ndata = pd.read_excel('test.csv')\ndata""" # use read_excel to read a csv error = """ Traceback (most recent call last): @@ -139,186 +46,14 @@ async def test_write_code_to_correct_error(): raise ValueError( ValueError: Excel file format cannot be determined, you must specify an engine manually. """ - context = [ - Message(content=structural_context, role="user"), + working_memory = [ Message(content=wrong_code, role="assistant"), Message(content=error, role="user"), ] - new_code = await WriteCodeWithoutTools().run(context=context) - new_code = new_code["code"] - print(new_code) + new_code = await WriteAnalysisCode().run( + user_requirement=user_requirement, + plan_status=plan_status, + working_memory=working_memory, + use_reflection=True, + ) assert "read_csv" in new_code # should correct read_excel to read_csv - - -@pytest.mark.asyncio -async def test_write_code_reuse_code_simple(): - structural_context = """ - ## User Requirement - read a dataset test.csv and print its head - ## Current Plan - [ - { - "task_id": "1", - "dependent_task_ids": [], - "instruction": "import pandas and load the dataset from 'test.csv'.", - "task_type": "", - "code": "import pandas as pd\ndata = pd.read_csv('test.csv')", - "result": "", - "is_finished": true - }, - { - "task_id": "2", - "dependent_task_ids": [ - "1" - ], - "instruction": "Print the head of the dataset to display the first few rows.", - "task_type": "", - "code": "", - "result": "", - "is_finished": false - } - ] - ## Current Task - {"task_id": "2", "dependent_task_ids": ["1"], "instruction": "Print the head of the dataset to display the first few rows.", "task_type": "", "code": "", "result": "", "is_finished": false} - """ - context = [ - Message(content=structural_context, role="user"), - ] - code = await WriteCodeWithoutTools().run(context=context) - code = code["code"] - print(code) - assert "pandas" not in code and "read_csv" not in code # should reuse import and read statement from previous one - - -@pytest.mark.skip -@pytest.mark.asyncio -async def test_write_code_reuse_code_long(): - """test code reuse for long context""" - - structural_context = """ - ## User Requirement - Run data analysis on sklearn Iris dataset, include a plot - ## Current Plan - [ - { - "task_id": "1", - "dependent_task_ids": [], - "instruction": "Load the Iris dataset from sklearn.", - "task_type": "", - "code": "from sklearn.datasets import load_iris\niris_data = load_iris()\niris_data['data'][0:5], iris_data['target'][0:5]", - "result": "(array([[5.1, 3.5, 1.4, 0.2],\n [4.9, 3. , 1.4, 0.2],\n [4.7, 3.2, 1.3, 0.2],\n [4.6, 3.1, 1.5, 0.2],\n [5. , 3.6, 1.4, 0.2]]),\n array([0, 0, 0, 0, 0]))", - "is_finished": true - }, - { - "task_id": "2", - "dependent_task_ids": [ - "1" - ], - "instruction": "Perform exploratory data analysis on the Iris dataset.", - "task_type": "", - "code": "", - "result": "", - "is_finished": false - }, - { - "task_id": "3", - "dependent_task_ids": [ - "2" - ], - "instruction": "Create a plot visualizing the Iris dataset features.", - "task_type": "", - "code": "", - "result": "", - "is_finished": false - } - ] - ## Current Task - {"task_id": "2", "dependent_task_ids": ["1"], "instruction": "Perform exploratory data analysis on the Iris dataset.", "task_type": "", "code": "", "result": "", "is_finished": false} - """ - context = [ - Message(content=structural_context, role="user"), - ] - trials_num = 5 - trials = [WriteCodeWithoutTools().run(context=context, temperature=0.0) for _ in range(trials_num)] - trial_results = await asyncio.gather(*trials) - print(*trial_results, sep="\n\n***\n\n") - success = [ - "load_iris" not in result["code"] and "iris_data" in result["code"] for result in trial_results - ] # should reuse iris_data from previous tasks - success_rate = sum(success) / trials_num - logger.info(f"success rate: {success_rate :.2f}") - assert success_rate >= 0.8 - - -@pytest.mark.skip -@pytest.mark.asyncio -async def test_write_code_reuse_code_long_for_wine(): - """test code reuse for long context""" - - structural_context = """ - ## User Requirement - Run data analysis on sklearn Wisconsin Breast Cancer dataset, include a plot, train a model to predict targets (20% as validation), and show validation accuracy - ## Current Plan - [ - { - "task_id": "1", - "dependent_task_ids": [], - "instruction": "Load the sklearn Wine recognition dataset and perform exploratory data analysis." - "task_type": "", - "code": "from sklearn.datasets import load_wine\n# Load the Wine recognition dataset\nwine_data = load_wine()\n# Perform exploratory data analysis\nwine_data.keys()", - "result": "Truncated to show only the last 1000 characters\ndict_keys(['data', 'target', 'frame', 'target_names', 'DESCR', 'feature_names'])", - "is_finished": true - }, - { - "task_id": "2", - "dependent_task_ids": ["1"], - "instruction": "Create a plot to visualize some aspect of the wine dataset." - "task_type": "", - "code": "", - "result": "", - "is_finished": false - }, - { - "task_id": "3", - "dependent_task_ids": ["1"], - "instruction": "Split the dataset into training and validation sets with a 20% validation size.", - "task_type": "", - "code": "", - "result": "", - "is_finished": false - }, - { - "task_id": "4", - "dependent_task_ids": ["3"], - "instruction": "Train a model on the training set to predict wine class.", - "task_type": "", - "code": "", - "result": "", - "is_finished": false - }, - { - "task_id": "5", - "dependent_task_ids": ["4"], - "instruction": "Evaluate the model on the validation set and report the accuracy.", - "task_type": "", - "code": "", - "result": "", - "is_finished": false - } - ] - ## Current Task - {"task_id": "2", "dependent_task_ids": ["1"], "instruction": "Create a plot to visualize some aspect of the Wine dataset.", "task_type": "", "code": "", "result": "", "is_finished": false} - """ - context = [ - Message(content=structural_context, role="user"), - ] - trials_num = 5 - trials = [WriteCodeWithoutTools().run(context=context, temperature=0.0) for _ in range(trials_num)] - trial_results = await asyncio.gather(*trials) - print(*trial_results, sep="\n\n***\n\n") - success = [ - "load_wine" not in result["code"] and "wine_data" in result["code"] for result in trial_results - ] # should reuse iris_data from previous tasks - success_rate = sum(success) / trials_num - logger.info(f"success rate: {success_rate :.2f}") - assert success_rate >= 0.8 diff --git a/tests/metagpt/actions/di/test_write_plan.py b/tests/metagpt/actions/di/test_write_plan.py index 80b3399b8..cad0c8a71 100644 --- a/tests/metagpt/actions/di/test_write_plan.py +++ b/tests/metagpt/actions/di/test_write_plan.py @@ -23,12 +23,10 @@ def test_precheck_update_plan_from_rsp(): @pytest.mark.asyncio -@pytest.mark.parametrize("use_tools", [(False), (True)]) -async def test_write_plan(use_tools): +async def test_write_plan(): rsp = await WritePlan().run( - context=[Message("run analysis on sklearn iris dataset", role="user")], use_tools=use_tools + context=[Message("Run data analysis on sklearn Iris dataset, include a plot", role="user")] ) assert "task_id" in rsp assert "instruction" in rsp - assert "json" not in rsp # the output should be the content inside ```json ``` diff --git a/tests/metagpt/roles/di/test_data_interpreter.py b/tests/metagpt/roles/di/test_data_interpreter.py index ba50f473b..f51f5bbfc 100644 --- a/tests/metagpt/roles/di/test_data_interpreter.py +++ b/tests/metagpt/roles/di/test_data_interpreter.py @@ -10,10 +10,9 @@ async def test_interpreter(mocker, auto_run): mocker.patch("metagpt.actions.di.execute_nb_code.ExecuteNbCode.run", return_value=("a successful run", True)) mocker.patch("builtins.input", return_value="confirm") - requirement = "Run data analysis on sklearn Iris dataset, include a plot" - tools = [] + requirement = "Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy." - di = DataInterpreter(auto_run=auto_run, use_tools=True, tools=tools) + di = DataInterpreter(auto_run=auto_run) rsp = await di.run(requirement) logger.info(rsp) assert len(rsp.content) > 0 @@ -21,3 +20,15 @@ async def test_interpreter(mocker, auto_run): finished_tasks = di.planner.plan.get_finished_tasks() assert len(finished_tasks) > 0 assert len(finished_tasks[0].code) > 0 # check one task to see if code is recorded + + +async def test_interpreter_react_mode(mocker): + mocker.patch("metagpt.actions.di.execute_nb_code.ExecuteNbCode.run", return_value=("a successful run", True)) + mocker.patch("builtins.input", return_value="confirm") + + requirement = "Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy." + + di = DataInterpreter(react_mode="react") + rsp = await di.run(requirement) + logger.info(rsp) + assert len(rsp.content) > 0 diff --git a/tests/metagpt/roles/di/test_ml_engineer.py b/tests/metagpt/roles/di/test_ml_engineer.py deleted file mode 100644 index 08b92cd27..000000000 --- a/tests/metagpt/roles/di/test_ml_engineer.py +++ /dev/null @@ -1,90 +0,0 @@ -import pytest - -from metagpt.actions.di.execute_nb_code import ExecuteNbCode -from metagpt.logs import logger -from metagpt.roles.di.ml_engineer import MLEngineer -from metagpt.schema import Message, Plan, Task -from metagpt.tools.tool_type import ToolType -from tests.metagpt.actions.di.test_debug_code import CODE, DebugContext, ErrorStr - - -def test_mle_init(): - mle = MLEngineer(goal="test", auto_run=True, use_tools=True, tools=["tool1", "tool2"]) - assert mle.tools == [] - - -MockPlan = Plan( - goal="This is a titanic passenger survival dataset, your goal is to predict passenger survival outcome. The target column is Survived. Perform data analysis, data preprocessing, feature engineering, and modeling to predict the target. Report accuracy on the eval data. Train data path: 'tests/data/ml_datasets/titanic/split_train.csv', eval data path: 'tests/data/ml_datasets/titanic/split_eval.csv'.", - context="", - tasks=[ - Task( - task_id="1", - dependent_task_ids=[], - instruction="Perform exploratory data analysis on the train dataset to understand the features and target variable.", - task_type="eda", - code="", - result="", - is_success=False, - is_finished=False, - ) - ], - task_map={ - "1": Task( - task_id="1", - dependent_task_ids=[], - instruction="Perform exploratory data analysis on the train dataset to understand the features and target variable.", - task_type="eda", - code="", - result="", - is_success=False, - is_finished=False, - ) - }, - current_task_id="1", -) - - -@pytest.mark.asyncio -async def test_mle_write_code(mocker): - data_path = "tests/data/ml_datasets/titanic" - - mle = MLEngineer(auto_run=True, use_tools=True) - mle.planner.plan = MockPlan - - code, _ = await mle._write_code() - assert data_path in code["code"] - - -@pytest.mark.asyncio -async def test_mle_update_data_columns(mocker): - mle = MLEngineer(auto_run=True, use_tools=True) - mle.planner.plan = MockPlan - - # manually update task type to test update - mle.planner.plan.current_task.task_type = ToolType.DATA_PREPROCESS.value - - result = await mle._update_data_columns() - assert result is not None - - -@pytest.mark.asyncio -async def test_mle_debug_code(mocker): - mle = MLEngineer(auto_run=True, use_tools=True) - mle.working_memory.add(Message(content=ErrorStr, cause_by=ExecuteNbCode)) - mle.latest_code = CODE - mle.debug_context = DebugContext - code, _ = await mle._write_code() - assert len(code) > 0 - - -@pytest.mark.skip -@pytest.mark.asyncio -async def test_ml_engineer(): - data_path = "tests/data/ml_datasets/titanic" - requirement = f"This is a titanic passenger survival dataset, your goal is to predict passenger survival outcome. The target column is Survived. Perform data analysis, data preprocessing, feature engineering, and modeling to predict the target. Report accuracy on the eval data. Train data path: '{data_path}/split_train.csv', eval data path: '{data_path}/split_eval.csv'." - tools = ["FillMissingValue", "CatCross", "dummy_tool"] - - mle = MLEngineer(auto_run=True, use_tools=True, tools=tools) - rsp = await mle.run(requirement) - logger.info(rsp) - assert len(rsp.content) > 0 diff --git a/tests/mock/mock_llm.py b/tests/mock/mock_llm.py index b2052e2b3..48154840a 100644 --- a/tests/mock/mock_llm.py +++ b/tests/mock/mock_llm.py @@ -32,14 +32,13 @@ class MockLLM(OriginalLLM): async def original_aask( self, - msg: str, + msg: Union[str, list[dict[str, str]]], system_msgs: Optional[list[str]] = None, format_msgs: Optional[list[dict[str, str]]] = None, images: Optional[Union[str, list[str]]] = None, timeout=3, stream=True, - ): - """A copy of metagpt.provider.base_llm.BaseLLM.aask, we can't use super().aask because it will be mocked""" + ) -> str: if system_msgs: message = self._system_msgs(system_msgs) else: @@ -48,7 +47,11 @@ class MockLLM(OriginalLLM): message = [] if format_msgs: message.extend(format_msgs) - message.append(self._user_msg(msg, images=images)) + if isinstance(msg, str): + message.append(self._user_msg(msg, images=images)) + else: + message.extend(msg) + logger.debug(message) rsp = await self.acompletion_text(message, stream=stream, timeout=timeout) return rsp @@ -72,14 +75,19 @@ class MockLLM(OriginalLLM): async def aask( self, - msg: str, + msg: Union[str, list[dict[str, str]]], system_msgs: Optional[list[str]] = None, format_msgs: Optional[list[dict[str, str]]] = None, images: Optional[Union[str, list[str]]] = None, timeout=3, stream=True, ) -> str: - msg_key = msg # used to identify it a message has been called before + # used to identify it a message has been called before + if isinstance(msg, list): + msg_key = "#MSG_SEP#".join([m["content"] for m in msg]) + else: + msg_key = msg + if system_msgs: joined_system_msg = "#MSG_SEP#".join(system_msgs) + "#SYSTEM_MSG_END#" msg_key = joined_system_msg + msg_key From 9d453c5c1018b243b837b4bb1a1740fed81f37f4 Mon Sep 17 00:00:00 2001 From: yzlin Date: Tue, 12 Mar 2024 22:59:07 +0800 Subject: [PATCH 059/131] avoid private fn registry, add some tests --- metagpt/tools/tool_convert.py | 2 ++ .../actions/di/test_write_analysis_code.py | 24 +++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/metagpt/tools/tool_convert.py b/metagpt/tools/tool_convert.py index 3d6a49769..48c945d9b 100644 --- a/metagpt/tools/tool_convert.py +++ b/metagpt/tools/tool_convert.py @@ -12,6 +12,8 @@ def convert_code_to_tool_schema(obj, include: list[str] = None): if inspect.isclass(obj): schema = {"type": "class", "description": remove_spaces(docstring), "methods": {}} for name, method in inspect.getmembers(obj, inspect.isfunction): + if name.startswith("_"): # skip private methodss + continue if include and name not in include: continue # method_doc = inspect.getdoc(method) diff --git a/tests/metagpt/actions/di/test_write_analysis_code.py b/tests/metagpt/actions/di/test_write_analysis_code.py index b1e51d8ce..2996f31f7 100644 --- a/tests/metagpt/actions/di/test_write_analysis_code.py +++ b/tests/metagpt/actions/di/test_write_analysis_code.py @@ -5,7 +5,7 @@ from metagpt.schema import Message @pytest.mark.asyncio -async def test_write_code(): +async def test_write_code_with_plan(): write_code = WriteAnalysisCode() user_requirement = "Run data analysis on sklearn Iris dataset, include a plot" @@ -16,9 +16,29 @@ async def test_write_code(): assert "sklearn" in code +@pytest.mark.asyncio +async def test_write_code_with_tools(): + write_code = WriteAnalysisCode() + + user_requirement = "Preprocess sklearn Wine recognition dataset and train a model to predict wine class (20% as validation), and show validation accuracy." + tool_info = """ + ## Capabilities + - You can utilize pre-defined tools in any code lines from 'Available Tools' in the form of Python class or function. + - You can freely combine the use of any other public packages, like sklearn, numpy, pandas, etc.. + + ## Available Tools: + Each tool is described in JSON format. When you call a tool, import the tool from its path first. + {'FillMissingValue': {'type': 'class', 'description': 'Completing missing values with simple strategies.', 'methods': {'__init__': {'type': 'function', 'description': 'Initialize self. ', 'signature': '(self, features: \'list\', strategy: "Literal[\'mean\', \'median\', \'most_frequent\', \'constant\']" = \'mean\', fill_value=None)', 'parameters': 'Args: features (list): Columns to be processed. strategy (Literal["mean", "median", "most_frequent", "constant"], optional): The imputation strategy, notice \'mean\' and \'median\' can only be used for numeric features. Defaults to \'mean\'. fill_value (int, optional): Fill_value is used to replace all occurrences of missing_values. Defaults to None.'}, 'fit': {'type': 'function', 'description': 'Fit a model to be used in subsequent transform. ', 'signature': "(self, df: 'pd.DataFrame')", 'parameters': 'Args: df (pd.DataFrame): The input DataFrame.'}, 'fit_transform': {'type': 'function', 'description': 'Fit and transform the input DataFrame. ', 'signature': "(self, df: 'pd.DataFrame') -> 'pd.DataFrame'", 'parameters': 'Args: df (pd.DataFrame): The input DataFrame. Returns: pd.DataFrame: The transformed DataFrame.'}, 'transform': {'type': 'function', 'description': 'Transform the input DataFrame with the fitted model. ', 'signature': "(self, df: 'pd.DataFrame') -> 'pd.DataFrame'", 'parameters': 'Args: df (pd.DataFrame): The input DataFrame. Returns: pd.DataFrame: The transformed DataFrame.'}}, 'tool_path': 'metagpt/tools/libs/data_preprocess.py'} + """ + + code = await write_code.run(user_requirement=user_requirement, tool_info=tool_info) + assert len(code) > 0 + assert "metagpt.tools.libs" in code + + @pytest.mark.asyncio async def test_debug_with_reflection(): - user_requirement = "Run data analysis on sklearn Iris dataset, include a plot" + user_requirement = "read a dataset test.csv and print its head" plan_status = """ ## Finished Tasks From e1123bae82c2ed9f8062af0095edb800a6aef986 Mon Sep 17 00:00:00 2001 From: liujun Date: Wed, 13 Mar 2024 09:59:30 +0800 Subject: [PATCH 060/131] feat:add user llm config --- metagpt/config2.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/metagpt/config2.py b/metagpt/config2.py index bc6af18c6..4b3d82593 100644 --- a/metagpt/config2.py +++ b/metagpt/config2.py @@ -99,7 +99,20 @@ class Config(CLIParams, YamlModel): dicts += [Config.read_yaml(path) for path in default_config_paths] final = merge_dict(dicts) return Config(**final) - + + @classmethod + def config(cls,config:dict): + """user config llm + example: + llm_config = {"llm": {"api_type": "xxx", "api_key": "xxx", "model": "xxx"} + gpt4 = Config.config(llm_config) + A = Role(name="A", profile="Democratic candidate", goal="Win the election", actions=[a1], watch=[a2], config=gpt4) + """ + dicts = [dict(os.environ)] + dicts += [config] + final = merge_dict(dicts) + return Config(**final) + def update_via_cli(self, project_path, project_name, inc, reqa_file, max_auto_summarize_code): """update config via cli""" From 684730e94f7c5fe7f5bac8a31ed8fac1937b6d43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 13 Mar 2024 15:06:40 +0800 Subject: [PATCH 061/131] feat: +`tree` command --- metagpt/utils/tree.py | 36 ++++++++++++++++++++++++++++---- tests/metagpt/utils/test_tree.py | 16 +++++++++++--- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/metagpt/utils/tree.py b/metagpt/utils/tree.py index 1c0060842..c0386d822 100644 --- a/metagpt/utils/tree.py +++ b/metagpt/utils/tree.py @@ -27,17 +27,22 @@ """ from __future__ import annotations +import subprocess from pathlib import Path from typing import Callable, Dict, List +from gitignore_parser import parse_gitignore -def tree(root: str | Path, git_ignore_rules: Callable = None) -> str: + +def tree(root: str | Path, gitignore: str | Path = None, run_command: bool = True) -> str: """ Recursively traverses the directory structure and prints it out in a tree-like format. Args: root (str or Path): The root directory from which to start traversing. - git_ignore_rules (Callable): Optional. A function to filter files to ignore. + gitignore (str or Path): The filename of gitignore file. + run_command (bool): Whether to execute `tree` command. Execute the `tree` command and return the result if True, + otherwise execute python code instead. Returns: str: A string representation of the directory tree. @@ -55,8 +60,7 @@ def tree(root: str | Path, git_ignore_rules: Callable = None) -> str: | +-- singleton.cpython-39.pyc +-- parse_docstring.py - >>> from gitignore_parser import parse_gitignore - >>> tree(".", git_ignore_rules=parse_gitignore(full_path="../../.gitignore")) + >>> tree(".", gitignore="../../.gitignore") utils +-- serialize.py +-- project_repo.py @@ -64,8 +68,21 @@ def tree(root: str | Path, git_ignore_rules: Callable = None) -> str: +-- mmdc_playwright.py +-- parse_docstring.py + >>> tree(".", gitignore="../../.gitignore", run_command=True) + utils + ├── serialize.py + ├── project_repo.py + ├── tree.py + ├── mmdc_playwright.py + └── parse_docstring.py + + """ root = Path(root).resolve() + if run_command: + return _execute_tree(root, gitignore) + + git_ignore_rules = parse_gitignore(gitignore) if gitignore else None dir_ = {root.name: _list_children(root=root, git_ignore_rules=git_ignore_rules)} v = _print_tree(dir_) return "\n".join(v) @@ -110,3 +127,14 @@ def _add_line(rows: List[str]) -> List[str]: return rows rows[i] = "|" + v[1:] return rows + + +def _execute_tree(root: Path, gitignore: str | Path) -> str: + args = ["--gitignore", str(gitignore)] if gitignore else [] + try: + result = subprocess.run(["tree"] + args + [str(root)], capture_output=True, text=True, check=True) + if result.returncode != 0: + raise ValueError(f"tree exits with code {result.returncode}") + return result.stdout + except subprocess.CalledProcessError as e: + raise e diff --git a/tests/metagpt/utils/test_tree.py b/tests/metagpt/utils/test_tree.py index 34eae10cf..03a2a5606 100644 --- a/tests/metagpt/utils/test_tree.py +++ b/tests/metagpt/utils/test_tree.py @@ -2,7 +2,6 @@ from pathlib import Path from typing import List import pytest -from gitignore_parser import parse_gitignore from metagpt.utils.tree import _print_tree, tree @@ -15,8 +14,19 @@ from metagpt.utils.tree import _print_tree, tree ], ) def test_tree(root: str, rules: str): - gitignore_rules = parse_gitignore(full_path=rules) if rules else None - v = tree(root=root, git_ignore_rules=gitignore_rules) + v = tree(root=root, gitignore=rules) + assert v + + +@pytest.mark.parametrize( + ("root", "rules"), + [ + (str(Path(__file__).parent / "../.."), None), + (str(Path(__file__).parent / "../.."), str(Path(__file__).parent / "../../../.gitignore")), + ], +) +def test_tree_command(root: str, rules: str): + v = tree(root=root, gitignore=rules, run_command=True) assert v From 6f4d30825f84d90c35dab04e639fc1b483e1a823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 13 Mar 2024 15:10:13 +0800 Subject: [PATCH 062/131] feat: +`tree` command --- metagpt/utils/tree.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/utils/tree.py b/metagpt/utils/tree.py index c0386d822..fbf085e48 100644 --- a/metagpt/utils/tree.py +++ b/metagpt/utils/tree.py @@ -34,7 +34,7 @@ from typing import Callable, Dict, List from gitignore_parser import parse_gitignore -def tree(root: str | Path, gitignore: str | Path = None, run_command: bool = True) -> str: +def tree(root: str | Path, gitignore: str | Path = None, run_command: bool = False) -> str: """ Recursively traverses the directory structure and prints it out in a tree-like format. From 367324b84200a89db9de74c8bc13100913b6cdd7 Mon Sep 17 00:00:00 2001 From: geekan Date: Wed, 13 Mar 2024 16:16:41 +0800 Subject: [PATCH 063/131] update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 72ce03412..4c1d75489 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ # MetaGPT: The Multi-Agent Framework

## News -🚀 March. 01, 2024: Our Data Interpreter paper is on arxiv. Find all design and benchmark details [here](https://arxiv.org/abs/2402.18679)! +🚀 Mar. 01, 2024: Our Data Interpreter paper is on [arxiv](https://arxiv.org/abs/2402.18679). Check the [example](https://docs.deepwisdom.ai/main/en/DataInterpreter/) and [code](https://github.com/geekan/MetaGPT/tree/main/examples/di)! 🚀 Feb. 08, 2024: [v0.7.0](https://github.com/geekan/MetaGPT/releases/tag/v0.7.0) released, supporting assigning different LLMs to different Roles. We also introduced [Data Interpreter](https://github.com/geekan/MetaGPT/blob/main/examples/di/README.md), a powerful agent capable of solving a wide range of real-world problems. From 77eacec1b85fdf6e33684dd3c4c5a3d3555dec17 Mon Sep 17 00:00:00 2001 From: yzlin Date: Wed, 13 Mar 2024 16:29:28 +0800 Subject: [PATCH 064/131] recover legacy task type --- metagpt/strategy/task_type.py | 14 ++++++++++++++ metagpt/tools/libs/email_login.py | 2 +- metagpt/tools/libs/gpt_v_generator.py | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/metagpt/strategy/task_type.py b/metagpt/strategy/task_type.py index 9eeeb79ce..7c88817cc 100644 --- a/metagpt/strategy/task_type.py +++ b/metagpt/strategy/task_type.py @@ -54,6 +54,20 @@ class TaskType(Enum): ) OTHER = TaskTypeDef(name="other", desc="Any tasks not in the defined categories") + # Legacy TaskType to support tool recommendation using type match. You don't need to define task types if you have no human priors to inject. + TEXT2IMAGE = TaskTypeDef( + name="text2image", + desc="Related to text2image, image2image using stable diffusion model.", + ) + WEBSCRAPING = TaskTypeDef( + name="web scraping", + desc="For scraping data from web pages.", + ) + EMAIL_LOGIN = TaskTypeDef( + name="email login", + desc="For logging to an email.", + ) + @property def type_name(self): return self.value.name diff --git a/metagpt/tools/libs/email_login.py b/metagpt/tools/libs/email_login.py index 757ac2b87..32626ac55 100644 --- a/metagpt/tools/libs/email_login.py +++ b/metagpt/tools/libs/email_login.py @@ -23,7 +23,7 @@ IMAP_SERVERS = { } -@register_tool() +@register_tool(tags=["email login"]) def email_login_imap(email_address, email_password): """ Use imap_tools package to log in to your email (the email that supports IMAP protocol) to verify and return the account object. diff --git a/metagpt/tools/libs/gpt_v_generator.py b/metagpt/tools/libs/gpt_v_generator.py index 0e9f34770..4eba3d5ee 100644 --- a/metagpt/tools/libs/gpt_v_generator.py +++ b/metagpt/tools/libs/gpt_v_generator.py @@ -28,7 +28,7 @@ As the design pays tribute to large companies, sometimes it is normal for some c Now, please generate the corresponding webpage code including HTML, CSS and JavaScript:""" -@register_tool(include_functions=["__init__", "generate_webpages", "save_webpages"]) +@register_tool(tags=["image2webpage"], include_functions=["__init__", "generate_webpages", "save_webpages"]) class GPTvGenerator: """Class for generating webpage code from a given webpage screenshot. From 93663784ab2866cc083ce5ac633a61fdf9616d8d Mon Sep 17 00:00:00 2001 From: yzlin Date: Wed, 13 Mar 2024 16:36:34 +0800 Subject: [PATCH 065/131] small bug fixes --- metagpt/tools/tool_convert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/tools/tool_convert.py b/metagpt/tools/tool_convert.py index 48c945d9b..42c65b9e7 100644 --- a/metagpt/tools/tool_convert.py +++ b/metagpt/tools/tool_convert.py @@ -12,7 +12,7 @@ def convert_code_to_tool_schema(obj, include: list[str] = None): if inspect.isclass(obj): schema = {"type": "class", "description": remove_spaces(docstring), "methods": {}} for name, method in inspect.getmembers(obj, inspect.isfunction): - if name.startswith("_"): # skip private methodss + if name.startswith("_") and name != "__init__": # skip private methodss continue if include and name not in include: continue From 5fc711ae8250bf83374c2163cf9c9c792ea5a11d Mon Sep 17 00:00:00 2001 From: yzlin Date: Wed, 13 Mar 2024 16:50:19 +0800 Subject: [PATCH 066/131] minor updates --- metagpt/actions/di/write_analysis_code.py | 10 ++++------ metagpt/roles/di/data_interpreter.py | 14 +++++++------- metagpt/tools/tool_recommend.py | 16 +++++++--------- tests/metagpt/roles/di/test_data_interpreter.py | 1 + tests/metagpt/tools/test_tool_convert.py | 1 - 5 files changed, 19 insertions(+), 23 deletions(-) diff --git a/metagpt/actions/di/write_analysis_code.py b/metagpt/actions/di/write_analysis_code.py index 97eb81def..185926e31 100644 --- a/metagpt/actions/di/write_analysis_code.py +++ b/metagpt/actions/di/write_analysis_code.py @@ -50,17 +50,15 @@ class WriteAnalysisCode(Action): ) working_memory = working_memory or [] - context = [Message(content=structual_prompt, role="user")] + working_memory - context = process_message(context) + context = process_message([Message(content=structual_prompt, role="user")] + working_memory) # LLM call - if not use_reflection: + if use_reflection: + code = await self._debug_with_reflection(context=context, working_memory=working_memory) + else: rsp = await self.llm.aask(context, system_msgs=[INTERPRETER_SYSTEM_MSG], **kwargs) code = CodeParser.parse_code(block=None, text=rsp) - else: - code = await self._debug_with_reflection(context=context, working_memory=working_memory) - return code diff --git a/metagpt/roles/di/data_interpreter.py b/metagpt/roles/di/data_interpreter.py index c24c78a90..a8534b710 100644 --- a/metagpt/roles/di/data_interpreter.py +++ b/metagpt/roles/di/data_interpreter.py @@ -39,7 +39,7 @@ class DataInterpreter(Role): use_plan: bool = True use_reflection: bool = False execute_code: ExecuteNbCode = Field(default_factory=ExecuteNbCode, exclude=True) - tools: Union[str, list[str]] = [] + tools: Union[str, list[str]] = [] # Use special symbol [""] to indicate use of all registered tools tool_recommender: ToolRecommender = None react_mode: Literal["plan_and_act", "react"] = "plan_and_act" max_react_loop: int = 10 # used for react mode @@ -53,6 +53,7 @@ class DataInterpreter(Role): if self.tools: self.tool_recommender = BM25ToolRecommender(tools=self.tools) self.set_actions([WriteAnalysisCode]) + self._set_state(0) return self @property @@ -140,13 +141,13 @@ class DataInterpreter(Role): async def _write_code( self, - counter, - plan_status="", - tool_info="", + counter: int, + plan_status: str = "", + tool_info: str = "", ): - todo = WriteAnalysisCode() + todo = self.rc.todo # todo is WriteAnalysisCode logger.info(f"ready to {todo.name}") - use_reflection = counter > 0 and self.use_reflection + use_reflection = counter > 0 and self.use_reflection # only use reflection after the first trial user_requirement = self.get_memories()[0].content @@ -176,7 +177,6 @@ class DataInterpreter(Role): code = await CheckData().run(self.planner.plan) if not code.strip(): return - success = False result, success = await self.execute_code.run(code) if success: print(result) diff --git a/metagpt/tools/tool_recommend.py b/metagpt/tools/tool_recommend.py index 9b00a7379..69b9a4b5d 100644 --- a/metagpt/tools/tool_recommend.py +++ b/metagpt/tools/tool_recommend.py @@ -8,7 +8,7 @@ import numpy as np from pydantic import BaseModel, field_validator from rank_bm25 import BM25Okapi -from metagpt.actions import Action +from metagpt.llm import LLM from metagpt.logs import logger from metagpt.schema import Plan from metagpt.tools import TOOL_REGISTRY @@ -49,11 +49,6 @@ Recommend up to {topk} tools from 'Available Tools' that can help solve the 'Use """ -class RecommendTool(Action): - async def run(self, prompt): - return await self._aask(prompt) - - class ToolRecommender(BaseModel): """ The default ToolRecommender: @@ -67,6 +62,7 @@ class ToolRecommender(BaseModel): @field_validator("tools", mode="before") @classmethod def validate_tools(cls, v: list[str]) -> dict[str, Tool]: + # One can use special symbol [""] to indicate use of all registered tools if v == [""]: return TOOL_REGISTRY.get_all_tools() else: @@ -136,7 +132,7 @@ class ToolRecommender(BaseModel): available_tools=available_tools, topk=topk, ) - rsp = await RecommendTool().run(prompt) + rsp = await LLM().aask(prompt) rsp = CodeParser.parse_code(block=None, text=rsp) ranked_tools = json.loads(rsp) @@ -160,9 +156,11 @@ class TypeMatchToolRecommender(ToolRecommender): task_type = plan.current_task.task_type candidate_tools = TOOL_REGISTRY.get_tools_by_tag(task_type) candidate_tool_names = set(self.tools.keys()) & candidate_tools.keys() - recalled_tools = [candidate_tools[tool_name] for tool_name in candidate_tool_names] + recalled_tools = [candidate_tools[tool_name] for tool_name in candidate_tool_names][:topk] - return recalled_tools[:topk] + logger.info(f"Recalled tools: \n{[tool.name for tool in recalled_tools]}") + + return recalled_tools class BM25ToolRecommender(ToolRecommender): diff --git a/tests/metagpt/roles/di/test_data_interpreter.py b/tests/metagpt/roles/di/test_data_interpreter.py index f51f5bbfc..d25e5a099 100644 --- a/tests/metagpt/roles/di/test_data_interpreter.py +++ b/tests/metagpt/roles/di/test_data_interpreter.py @@ -22,6 +22,7 @@ async def test_interpreter(mocker, auto_run): assert len(finished_tasks[0].code) > 0 # check one task to see if code is recorded +@pytest.mark.asyncio async def test_interpreter_react_mode(mocker): mocker.patch("metagpt.actions.di.execute_nb_code.ExecuteNbCode.run", return_value=("a successful run", True)) mocker.patch("builtins.input", return_value="confirm") diff --git a/tests/metagpt/tools/test_tool_convert.py b/tests/metagpt/tools/test_tool_convert.py index f85b84b71..061a619ce 100644 --- a/tests/metagpt/tools/test_tool_convert.py +++ b/tests/metagpt/tools/test_tool_convert.py @@ -45,7 +45,6 @@ class DummyClass: pass -# def dummy_fn(df: pd.DataFrame, s: str, k: int = 5, type: Literal["a", "b", "c"] = "a") -> dict: def dummy_fn( df: pd.DataFrame, s: str, From 8dd2df56bc243b2f4a3c3b07f5b533e678d95b3d Mon Sep 17 00:00:00 2001 From: yzlin Date: Wed, 13 Mar 2024 16:52:30 +0800 Subject: [PATCH 067/131] cite paper --- examples/di/README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/di/README.md b/examples/di/README.md index fb4e8044b..f63795f13 100644 --- a/examples/di/README.md +++ b/examples/di/README.md @@ -1,7 +1,7 @@ # Data Interpreter (DI) ## What is Data Interpreter -Data Interpreter is an agent who solves problems through codes. It understands user requirements, makes plans, writes codes for execution, and uses tools if necessary. These capabilities enable it to tackle a wide range of scenarios, please check out the examples below. +Data Interpreter is an agent who solves data-related problems through codes. It understands user requirements, makes plans, writes codes for execution, and uses tools if necessary. These capabilities enable it to tackle a wide range of scenarios, please check out the examples below. For overall design and technical details, please see our [paper](https://arxiv.org/abs/2402.18679). ## Example List - Data visualization @@ -12,7 +12,9 @@ ## Example List - Tool usage: web page imitation - Tool usage: web crawling - Tool usage: text2image -- Tool usage: email summarization and response +- Tool usage: email summarization and response\ - More on the way! -Please see [here](https://docs.deepwisdom.ai/main/en/guide/use_cases/agent/interpreter/intro.html) for detailed explanation. \ No newline at end of file +Please see the [docs](https://docs.deepwisdom.ai/main/en/guide/use_cases/agent/interpreter/intro.html) for more explanation. + +We are continuously releasing codes, stay tuned! From 60fd49ea601a2e37c3d00b1531605924c4e116a0 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 13 Mar 2024 16:52:53 +0800 Subject: [PATCH 068/131] update method of terminate --- metagpt/actions/di/execute_nb_code.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/metagpt/actions/di/execute_nb_code.py b/metagpt/actions/di/execute_nb_code.py index f6a8defbd..b16da221f 100644 --- a/metagpt/actions/di/execute_nb_code.py +++ b/metagpt/actions/di/execute_nb_code.py @@ -57,8 +57,23 @@ class ExecuteNbCode(Action): async def terminate(self): """kill NotebookClient""" - if self.nb_client.km is not None: - await self.nb_client._async_cleanup_kernel() + if self.nb_client.km is not None and await self.nb_client.km.is_alive(): + await self.nb_client.km.shutdown_kernel(now=True) + await self.nb_client.km.cleanup_resources() + + # Stops all the running channels for this kernel + # The stdin_channel is the channel for handling standard input to the kernel. + if self.nb_client.kc.stdin_channel.is_alive(): + self.nb_client.kc.stdin_channel.stop() + # The hb_channel is the channel for heartbeat communication between the kernel and client. + if self.nb_client.kc.hb_channel.is_alive(): + self.nb_client.kc.hb_channel.stop() + # The control_channel is the channel for controlling the kernel. + if self.nb_client.kc.control_channel.is_alive(): + self.nb_client.kc.control_channel.stop() + + self.nb_client.kc = None + self.nb_client.km = None async def reset(self): """reset NotebookClient""" From 88cea9418c94b9d4607fdd06dc995780b5eeb7e3 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Wed, 13 Mar 2024 17:08:58 +0800 Subject: [PATCH 069/131] update method of terminate --- metagpt/actions/di/execute_nb_code.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/metagpt/actions/di/execute_nb_code.py b/metagpt/actions/di/execute_nb_code.py index b16da221f..0cf16b70f 100644 --- a/metagpt/actions/di/execute_nb_code.py +++ b/metagpt/actions/di/execute_nb_code.py @@ -61,16 +61,16 @@ class ExecuteNbCode(Action): await self.nb_client.km.shutdown_kernel(now=True) await self.nb_client.km.cleanup_resources() + channels = [ + self.nb_client.kc.stdin_channel, # The channel for handling standard input to the kernel. + self.nb_client.kc.hb_channel, # The channel for heartbeat communication between the kernel and client. + self.nb_client.kc.control_channel, # The channel for controlling the kernel. + ] + # Stops all the running channels for this kernel - # The stdin_channel is the channel for handling standard input to the kernel. - if self.nb_client.kc.stdin_channel.is_alive(): - self.nb_client.kc.stdin_channel.stop() - # The hb_channel is the channel for heartbeat communication between the kernel and client. - if self.nb_client.kc.hb_channel.is_alive(): - self.nb_client.kc.hb_channel.stop() - # The control_channel is the channel for controlling the kernel. - if self.nb_client.kc.control_channel.is_alive(): - self.nb_client.kc.control_channel.stop() + for channel in channels: + if channel.is_alive(): + channel.stop() self.nb_client.kc = None self.nb_client.km = None From f9683c0276dc0ca6d83e2f46f8f34eaf27654239 Mon Sep 17 00:00:00 2001 From: yzlin Date: Wed, 13 Mar 2024 21:17:24 +0800 Subject: [PATCH 070/131] test type match recommender --- tests/metagpt/tools/test_tool_recommend.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/metagpt/tools/test_tool_recommend.py b/tests/metagpt/tools/test_tool_recommend.py index 2fb3f9348..fafe0a638 100644 --- a/tests/metagpt/tools/test_tool_recommend.py +++ b/tests/metagpt/tools/test_tool_recommend.py @@ -2,7 +2,11 @@ import pytest from metagpt.schema import Plan, Task from metagpt.tools import TOOL_REGISTRY -from metagpt.tools.tool_recommend import BM25ToolRecommender, ToolRecommender +from metagpt.tools.tool_recommend import ( + BM25ToolRecommender, + ToolRecommender, + TypeMatchToolRecommender, +) @pytest.fixture @@ -11,7 +15,7 @@ def mock_plan(mocker): "1": Task( task_id="1", instruction="conduct feature engineering, add new features on the dataset", - task_type="feature_engineering", + task_type="feature engineering", ) } plan = Plan( @@ -76,3 +80,11 @@ async def test_bm25_recommend_tools(mock_bm25_tr): async def test_get_recommended_tool_info(mock_plan, mock_bm25_tr): result = await mock_bm25_tr.get_recommended_tool_info(plan=mock_plan) assert isinstance(result, str) + + +@pytest.mark.asyncio +async def test_tm_tr_recall_with_plan(mock_plan, mock_bm25_tr): + tr = TypeMatchToolRecommender(tools=["FillMissingValue", "PolynomialExpansion", "web scraping"]) + result = await tr.recall_tools(plan=mock_plan) + assert len(result) == 1 + assert result[0].name == "PolynomialExpansion" From e73ff53de60e49f0a52bfb6cee1e73f93f3d1cfc Mon Sep 17 00:00:00 2001 From: yzlin Date: Wed, 13 Mar 2024 21:49:21 +0800 Subject: [PATCH 071/131] terminate kernel properly in unit tests --- .../actions/di/test_execute_nb_code.py | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/metagpt/actions/di/test_execute_nb_code.py b/tests/metagpt/actions/di/test_execute_nb_code.py index 74757555f..b206046d7 100644 --- a/tests/metagpt/actions/di/test_execute_nb_code.py +++ b/tests/metagpt/actions/di/test_execute_nb_code.py @@ -8,6 +8,7 @@ async def test_code_running(): executor = ExecuteNbCode() output, is_success = await executor.run("print('hello world!')") assert is_success + await executor.terminate() @pytest.mark.asyncio @@ -17,6 +18,7 @@ async def test_split_code_running(): _ = await executor.run("z=x+y") output, is_success = await executor.run("assert z==3") assert is_success + await executor.terminate() @pytest.mark.asyncio @@ -24,6 +26,7 @@ async def test_execute_error(): executor = ExecuteNbCode() output, is_success = await executor.run("z=1/0") assert not is_success + await executor.terminate() PLOT_CODE = """ @@ -52,6 +55,7 @@ async def test_plotting_code(): executor = ExecuteNbCode() output, is_success = await executor.run(PLOT_CODE) assert is_success + await executor.terminate() @pytest.mark.asyncio @@ -61,6 +65,7 @@ async def test_run_with_timeout(): message, success = await executor.run(code) assert not success assert message.startswith("Cell execution timed out") + await executor.terminate() @pytest.mark.asyncio @@ -76,21 +81,15 @@ async def test_run_code_text(): message, success = await executor.run(code=mix_text, language="markdown") assert success assert message == mix_text + await executor.terminate() @pytest.mark.asyncio -async def test_terminate(): - executor = ExecuteNbCode() - await executor.run(code='print("This is a code!")', language="python") - is_kernel_alive = await executor.nb_client.km.is_alive() - assert is_kernel_alive - await executor.terminate() - - import time - - time.sleep(2) - assert executor.nb_client.km is None - for _ in range(200): +@pytest.mark.parametrize( + "k", [(1), (5)] +) # k=1 to test a single regular terminate, k>1 to test terminate under continuous run +async def test_terminate(k): + for _ in range(k): executor = ExecuteNbCode() await executor.run(code='print("This is a code!")', language="python") is_kernel_alive = await executor.nb_client.km.is_alive() @@ -98,7 +97,6 @@ async def test_terminate(): await executor.terminate() assert executor.nb_client.km is None assert executor.nb_client.kc is None - await executor.terminate() @pytest.mark.asyncio @@ -109,6 +107,7 @@ async def test_reset(): assert is_kernel_alive await executor.reset() assert executor.nb_client.km is None + await executor.terminate() @pytest.mark.asyncio @@ -126,3 +125,4 @@ async def test_parse_outputs(): assert "Index(['ID', 'NAME'], dtype='object')" in output assert "KeyError: 'DUMMPY_ID'" in output assert "columns num:2" in output + await executor.terminate() From 247f7849979349eda793040ea0bf1b2ee10b4c21 Mon Sep 17 00:00:00 2001 From: yzlin Date: Wed, 13 Mar 2024 21:51:05 +0800 Subject: [PATCH 072/131] update cache --- tests/data/rsp_cache.json | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/data/rsp_cache.json b/tests/data/rsp_cache.json index ba144ae39..8f0567c56 100644 --- a/tests/data/rsp_cache.json +++ b/tests/data/rsp_cache.json @@ -401,5 +401,24 @@ "\n# System\nRole: You are a professional software engineer, and your main task is to review and revise the code. You need to ensure that the code conforms to the google-style standards, is elegantly designed and modularized, easy to read and maintain.\nLanguage: Please use the same language as the user requirement, but the title and code should be still in English. For example, if the user speaks Chinese, the specific text of your answer should also be in Chinese.\nATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenced \"Format example\".\n\n# Context\n## User New Requirements\nNone\n\n## Code Plan And Change\n\n def add(a, b):\n- return a + \n+ return a + b\n \n\n## System Design\n编写一个从a加b的函数,返回a+b\n\n## Task\n\n\n## Code Files\n\n\n\n## Code to be Reviewed: \n```Code\ndef add(a, b):\n return a + b\n\n```\n\n\n\n# Format example 1\n## Code Review: \n1. No, we should fix the logic of class A due to ...\n2. ...\n3. ...\n4. No, function B is not implemented, ...\n5. ...\n6. ...\n\n## Actions\n1. Fix the `handle_events` method to update the game state only if a move is successful.\n ```python\n def handle_events(self):\n for event in pygame.event.get():\n if event.type == pygame.QUIT:\n return False\n if event.type == pygame.KEYDOWN:\n moved = False\n if event.key == pygame.K_UP:\n moved = self.game.move('UP')\n elif event.key == pygame.K_DOWN:\n moved = self.game.move('DOWN')\n elif event.key == pygame.K_LEFT:\n moved = self.game.move('LEFT')\n elif event.key == pygame.K_RIGHT:\n moved = self.game.move('RIGHT')\n if moved:\n # Update the game state only if a move was successful\n self.render()\n return True\n ```\n2. Implement function B\n\n## Code Review Result\nLBTM\n\n# Format example 2\n## Code Review: \n1. Yes.\n2. Yes.\n3. Yes.\n4. Yes.\n5. Yes.\n6. Yes.\n\n## Actions\npass\n\n## Code Review Result\nLGTM\n\n\n\n# Instruction: Based on the actual code situation, follow one of the \"Format example\". Return only 1 file under review.\n\n## Code Review: Ordered List. Based on the \"Code to be Reviewed\", provide key, clear, concise, and specific answer. If any answer is no, explain how to fix it step by step.\n1. Is the code implemented as per the requirements? If not, how to achieve it? Analyse it step by step.\n2. Is the code logic completely correct? If there are errors, please indicate how to correct them.\n3. Does the existing code follow the \"Data structures and interfaces\"?\n4. Are all functions implemented? If there is no implementation, please indicate how to achieve it step by step.\n5. Have all necessary pre-dependencies been imported? If not, indicate which ones need to be imported\n6. Are methods from other files being reused correctly?\n\n## Actions: Ordered List. Things that should be done after CR, such as implementing class A and function B\n\n## Code Review Result: str. If the code doesn't have bugs, we don't need to rewrite it, so answer LGTM and stop. ONLY ANSWER LGTM/LBTM.\nLGTM/LBTM\n\n": "## Code Review: \n1. 是的,代码按照要求实现了从a加b的功能。\n2. 是的,代码逻辑完全正确。\n3. 是的,现有代码遵循了“数据结构和接口”原则。\n4. 是的,所有功能都已实现。\n5. 是的,此代码片段不需要额外的依赖导入,因为它使用的是Python的内置操作。\n6. 是的,此代码片段没有从其他文件重用方法,因此不适用。\n\n## Actions\npass\n\n## Code Review Result\nLGTM", "\n## context\nPlease fix the bug in the code.\n\n-----\n\n## format example\n[CONTENT]\n{\n \"issue_type\": \"BUG\",\n \"reason\": \"...\"\n}\n[/CONTENT]\n\n## nodes: \": # \"\n- issue_type: # Answer BUG/REQUIREMENT. If it is a bugfix, answer BUG, otherwise answer Requirement\n- reason: # Explain the reasoning process from question to answer\n\n\n## constraint\nLanguage: Please use the same language as Human INPUT.\nFormat: output wrapped inside [CONTENT][/CONTENT] like format example, nothing else.\n\n## action\nFollow instructions of nodes, generate output and make sure it follows the format example.\n": "[CONTENT]\n{\n \"issue_type\": \"BUG\",\n \"reason\": \"The user has requested to fix a bug in the code, but has not provided the code or described the bug. Without this information, it is not possible to identify or fix the bug.\"\n}\n[/CONTENT]", "\n## context\n\n### Legacy Content\n\n## Language\n\nen_us\n\n## Programming Language\n\nPython\n\n## Original Requirements\n\nMake a simple number guessing game\n\n## Product Goals\n\n- Ensure a user-friendly interface for the game\n- Provide a challenging yet enjoyable game experience\n- Design the game to be easily extendable for future features\n\n## User Stories\n\n- As a player, I want to guess numbers and receive feedback on whether my guess is too high or too low\n- As a player, I want to be able to set the difficulty level by choosing the range of possible numbers\n- As a player, I want to see my previous guesses to strategize my next guess\n- As a player, I want to know how many attempts it took me to guess the number once I get it right\n\n## Competitive Analysis\n\n- Guess The Number Game A: Basic text interface, no difficulty levels\n- Number Master B: Has difficulty levels, but cluttered interface\n- Quick Guess C: Sleek design, but lacks performance tracking\n- NumGuess D: Good performance tracking, but not mobile-friendly\n- GuessIt E: Mobile-friendly, but too many ads\n- Perfect Guess F: Offers hints, but the hints are not very helpful\n- SmartGuesser G: Has a learning mode, but lacks a competitive edge\n\n## Competitive Quadrant Chart\n\nquadrantChart\n title \"User Engagement and Game Complexity\"\n x-axis \"Low Complexity\" --> \"High Complexity\"\n y-axis \"Low Engagement\" --> \"High Engagement\"\n quadrant-1 \"Too Simple\"\n quadrant-2 \"Niche Appeal\"\n quadrant-3 \"Complex & Unengaging\"\n quadrant-4 \"Sweet Spot\"\n \"Guess The Number Game A\": [0.2, 0.4]\n \"Number Master B\": [0.5, 0.3]\n \"Quick Guess C\": [0.6, 0.7]\n \"NumGuess D\": [0.4, 0.6]\n \"GuessIt E\": [0.7, 0.5]\n \"Perfect Guess F\": [0.6, 0.4]\n \"SmartGuesser G\": [0.8, 0.6]\n \"Our Target Product\": [0.5, 0.8]\n\n## Requirement Analysis\n\nThe game should be simple yet engaging, allowing players of different skill levels to enjoy it. It should provide immediate feedback and track the player's performance. The game should also be designed with a clean and intuitive interface, and it should be easy to add new features in the future.\n\n## Requirement Pool\n\n- ['P0', 'Implement the core game logic to randomly select a number and allow the user to guess it']\n- ['P0', 'Design a user interface that displays the game status and results clearly']\n- ['P1', 'Add difficulty levels by varying the range of possible numbers']\n- ['P1', 'Keep track of and display the number of attempts for each game session']\n- ['P2', \"Store and show the history of the player's guesses during a game session\"]\n\n## UI Design draft\n\nThe UI will feature a clean and minimalist design with a number input field, submit button, and messages area to provide feedback. There will be options to select the difficulty level and a display showing the number of attempts and history of past guesses.\n\n## Anything UNCLEAR\n\n### New Requirements\n\nAdding graphical interface functionality to enhance the user experience in the number-guessing game. The existing number-guessing game currently relies on command-line input for numbers. The goal is to introduce a graphical interface to improve the game's usability and visual appeal\n\n\n\n-----\n\n## format example\n[CONTENT]\n{\n \"is_relative\": \"YES\",\n \"reason\": \"...\"\n}\n[/CONTENT]\n\n## nodes: \": # \"\n- is_relative: # Answer YES/NO. If the requirement is related to the old PRD, answer YES, otherwise NO\n- reason: # Explain the reasoning process from question to answer\n\n\n## constraint\nLanguage: Please use the same language as Human INPUT.\nFormat: output wrapped inside [CONTENT][/CONTENT] like format example, nothing else.\n\n## action\nFollow instructions of nodes, generate output and make sure it follows the format example.\n": "[CONTENT]\n{\n \"is_relative\": \"YES\",\n \"reason\": \"The new requirement to add a graphical interface functionality is related to the original PRD's goal of ensuring a user-friendly interface for the game. The addition of a graphical interface aligns with the original product goals of providing a challenging yet enjoyable game experience and designing the game to be easily extendable for future features. It specifically addresses the competitive analysis findings by enhancing usability and visual appeal, which were identified as shortcomings in some of the competitive products.\"\n}\n[/CONTENT]", - "\n## context\n\n### Legacy Content\n\n## Language\n\nen_us\n\n## Programming Language\n\nPython\n\n## Original Requirements\n\nMake a simple number guessing game\n\n## Product Goals\n\n- Ensure a user-friendly interface for the game\n- Provide a challenging yet enjoyable game experience\n- Design the game to be easily extendable for future features\n\n## User Stories\n\n- As a player, I want to guess numbers and receive feedback on whether my guess is too high or too low\n- As a player, I want to be able to set the difficulty level by choosing the range of possible numbers\n- As a player, I want to see my previous guesses to strategize my next guess\n- As a player, I want to know how many attempts it took me to guess the number once I get it right\n\n## Competitive Analysis\n\n- Guess The Number Game A: Basic text interface, no difficulty levels\n- Number Master B: Has difficulty levels, but cluttered interface\n- Quick Guess C: Sleek design, but lacks performance tracking\n- NumGuess D: Good performance tracking, but not mobile-friendly\n- GuessIt E: Mobile-friendly, but too many ads\n- Perfect Guess F: Offers hints, but the hints are not very helpful\n- SmartGuesser G: Has a learning mode, but lacks a competitive edge\n\n## Competitive Quadrant Chart\n\nquadrantChart\n title \"User Engagement and Game Complexity\"\n x-axis \"Low Complexity\" --> \"High Complexity\"\n y-axis \"Low Engagement\" --> \"High Engagement\"\n quadrant-1 \"Too Simple\"\n quadrant-2 \"Niche Appeal\"\n quadrant-3 \"Complex & Unengaging\"\n quadrant-4 \"Sweet Spot\"\n \"Guess The Number Game A\": [0.2, 0.4]\n \"Number Master B\": [0.5, 0.3]\n \"Quick Guess C\": [0.6, 0.7]\n \"NumGuess D\": [0.4, 0.6]\n \"GuessIt E\": [0.7, 0.5]\n \"Perfect Guess F\": [0.6, 0.4]\n \"SmartGuesser G\": [0.8, 0.6]\n \"Our Target Product\": [0.5, 0.8]\n\n## Requirement Analysis\n\nThe game should be simple yet engaging, allowing players of different skill levels to enjoy it. It should provide immediate feedback and track the player's performance. The game should also be designed with a clean and intuitive interface, and it should be easy to add new features in the future.\n\n## Requirement Pool\n\n- ['P0', 'Implement the core game logic to randomly select a number and allow the user to guess it']\n- ['P0', 'Design a user interface that displays the game status and results clearly']\n- ['P1', 'Add difficulty levels by varying the range of possible numbers']\n- ['P1', 'Keep track of and display the number of attempts for each game session']\n- ['P2', \"Store and show the history of the player's guesses during a game session\"]\n\n## UI Design draft\n\nThe UI will feature a clean and minimalist design with a number input field, submit button, and messages area to provide feedback. There will be options to select the difficulty level and a display showing the number of attempts and history of past guesses.\n\n## Anything UNCLEAR\n\n### New Requirements\n\nAdding graphical interface functionality to enhance the user experience in the number-guessing game. The existing number-guessing game currently relies on command-line input for numbers. The goal is to introduce a graphical interface to improve the game's usability and visual appeal\n\n\n\n-----\n\n## format example\n[CONTENT]\n{\n \"Language\": \"en_us\",\n \"Programming Language\": \"Python\",\n \"Refined Requirements\": \"Create a 2048 game with a new feature that ...\",\n \"Project Name\": \"game_2048\",\n \"Refined Product Goals\": [\n \"Enhance user engagement through new features\",\n \"Optimize performance for scalability\",\n \"Integrate innovative UI enhancements\"\n ],\n \"Refined User Stories\": [\n \"As a player, I want to choose difficulty levels to challenge my skills\",\n \"As a player, I want a visually appealing score display after each game for a better gaming experience\",\n \"As a player, I want a convenient restart button displayed when I lose to quickly start a new game\",\n \"As a player, I want an enhanced and aesthetically pleasing UI to elevate the overall gaming experience\",\n \"As a player, I want the ability to play the game seamlessly on my mobile phone for on-the-go entertainment\"\n ],\n \"Competitive Analysis\": [\n \"2048 Game A: Simple interface, lacks responsive features\",\n \"play2048.co: Beautiful and responsive UI with my best score shown\",\n \"2048game.com: Responsive UI with my best score shown, but many ads\"\n ],\n \"Competitive Quadrant Chart\": \"quadrantChart\\n title \\\"Reach and engagement of campaigns\\\"\\n x-axis \\\"Low Reach\\\" --> \\\"High Reach\\\"\\n y-axis \\\"Low Engagement\\\" --> \\\"High Engagement\\\"\\n quadrant-1 \\\"We should expand\\\"\\n quadrant-2 \\\"Need to promote\\\"\\n quadrant-3 \\\"Re-evaluate\\\"\\n quadrant-4 \\\"May be improved\\\"\\n \\\"Campaign A\\\": [0.3, 0.6]\\n \\\"Campaign B\\\": [0.45, 0.23]\\n \\\"Campaign C\\\": [0.57, 0.69]\\n \\\"Campaign D\\\": [0.78, 0.34]\\n \\\"Campaign E\\\": [0.40, 0.34]\\n \\\"Campaign F\\\": [0.35, 0.78]\\n \\\"Our Target Product\\\": [0.5, 0.6]\",\n \"Refined Requirement Analysis\": [\n \"Require add/update/modify ...\"\n ],\n \"Refined Requirement Pool\": [\n [\n \"P0\",\n \"The main code ...\"\n ],\n [\n \"P0\",\n \"The game algorithm ...\"\n ]\n ],\n \"UI Design draft\": \"Basic function description with a simple style and layout.\",\n \"Anything UNCLEAR\": \"\"\n}\n[/CONTENT]\n\n## nodes: \": # \"\n- Language: # Provide the language used in the project, typically matching the user's requirement language.\n- Programming Language: # Python/JavaScript or other mainstream programming language.\n- Refined Requirements: # Place the New user's original requirements here.\n- Project Name: # According to the content of \"Original Requirements,\" name the project using snake case style , like 'game_2048' or 'simple_crm.\n- Refined Product Goals: typing.List[str] # Update and expand the original product goals to reflect the evolving needs due to incremental development. Ensure that the refined goals align with the current project direction and contribute to its success.\n- Refined User Stories: typing.List[str] # Update and expand the original scenario-based user stories to reflect the evolving needs due to incremental development. Ensure that the refined user stories capture incremental features and improvements. \n- Competitive Analysis: typing.List[str] # Provide 5 to 7 competitive products.\n- Competitive Quadrant Chart: # Use mermaid quadrantChart syntax. Distribute scores evenly between 0 and 1\n- Refined Requirement Analysis: typing.List[str] # Review and refine the existing requirement analysis to align with the evolving needs of the project due to incremental development. Ensure the analysis comprehensively covers the new features and enhancements required for the refined project scope.\n- Refined Requirement Pool: typing.List[typing.List[str]] # List down the top 5 to 7 requirements with their priority (P0, P1, P2). Cover both legacy content and incremental content. Retain content unrelated to incremental development\n- UI Design draft: # Provide a simple description of UI elements, functions, style, and layout.\n- Anything UNCLEAR: # Mention any aspects of the project that are unclear and try to clarify them.\n\n\n## constraint\nLanguage: Please use the same language as Human INPUT.\nFormat: output wrapped inside [CONTENT][/CONTENT] like format example, nothing else.\n\n## action\nFollow instructions of nodes, generate output and make sure it follows the format example.\n": "[CONTENT]\n{\n \"Language\": \"en_us\",\n \"Programming Language\": \"Python\",\n \"Refined Requirements\": \"Adding graphical interface functionality to enhance the user experience in the number-guessing game. The existing number-guessing game currently relies on command-line input for numbers. The goal is to introduce a graphical interface to improve the game's usability and visual appeal.\",\n \"Project Name\": \"graphical_number_guesser\",\n \"Refined Product Goals\": [\n \"Introduce a graphical user interface to make the game more accessible and visually appealing\",\n \"Maintain a user-friendly experience while transitioning from a command-line interface to a graphical interface\",\n \"Ensure the game remains challenging and enjoyable with the addition of the new interface\",\n \"Design the graphical interface to be easily extendable for future enhancements\"\n ],\n \"Refined User Stories\": [\n \"As a player, I want to interact with a graphical interface to input my guesses and receive feedback\",\n \"As a player, I want to easily select difficulty levels through the graphical interface\",\n \"As a player, I want to see a visual representation of my previous guesses and the number of attempts\",\n \"As a player, I want to enjoy a visually engaging experience that enhances the overall gameplay\"\n ],\n \"Competitive Analysis\": [\n \"Guess The Number Game A: Basic text interface, no difficulty levels\",\n \"Number Master B: Has difficulty levels, but cluttered interface\",\n \"Quick Guess C: Sleek design, but lacks performance tracking\",\n \"NumGuess D: Good performance tracking, but not mobile-friendly\",\n \"GuessIt E: Mobile-friendly, but too many ads\",\n \"Perfect Guess F: Offers hints, but the hints are not very helpful\",\n \"SmartGuesser G: Has a learning mode, but lacks a competitive edge\",\n \"Graphical Number Guesser: Our new game with a graphical interface and user-friendly design\"\n ],\n \"Competitive Quadrant Chart\": \"quadrantChart\\n title \\\"User Engagement and Game Complexity\\\"\\n x-axis \\\"Low Complexity\\\" --> \\\"High Complexity\\\"\\n y-axis \\\"Low Engagement\\\" --> \\\"High Engagement\\\"\\n quadrant-1 \\\"Too Simple\\\"\\n quadrant-2 \\\"Niche Appeal\\\"\\n quadrant-3 \\\"Complex & Unengaging\\\"\\n quadrant-4 \\\"Sweet Spot\\\"\\n \\\"Guess The Number Game A\\\": [0.2, 0.4]\\n \\\"Number Master B\\\": [0.5, 0.3]\\n \\\"Quick Guess C\\\": [0.6, 0.7]\\n \\\"NumGuess D\\\": [0.4, 0.6]\\n \\\"GuessIt E\\\": [0.7, 0.5]\\n \\\"Perfect Guess F\\\": [0.6, 0.4]\\n \\\"SmartGuesser G\\\": [0.8, 0.6]\\n \\\"Graphical Number Guesser\\\": [0.5, 0.8]\",\n \"Refined Requirement Analysis\": [\n \"The game must provide an intuitive graphical interface for user interaction\",\n \"The difficulty level selection should be seamlessly integrated into the graphical interface\",\n \"Performance tracking must be visually represented within the game interface\",\n \"The game should be designed to allow for easy updates and feature additions\"\n ],\n \"Refined Requirement Pool\": [\n [\"P0\", \"Implement a graphical user interface for the game\"],\n [\"P0\", \"Ensure the graphical interface provides feedback on guesses\"],\n [\"P1\", \"Incorporate difficulty level selection into the graphical interface\"],\n [\"P1\", \"Display a visual representation of guess history and attempt count\"],\n [\"P2\", \"Design the interface to be extensible for future game features\"]\n ],\n \"UI Design draft\": \"The graphical interface will feature a main game window with an input field for number guesses, buttons for submitting guesses and selecting difficulty, and panels for displaying guess history, attempt count, and feedback messages.\",\n \"Anything UNCLEAR\": \"\"\n}\n[/CONTENT]" + "\n## context\n\n### Legacy Content\n\n## Language\n\nen_us\n\n## Programming Language\n\nPython\n\n## Original Requirements\n\nMake a simple number guessing game\n\n## Product Goals\n\n- Ensure a user-friendly interface for the game\n- Provide a challenging yet enjoyable game experience\n- Design the game to be easily extendable for future features\n\n## User Stories\n\n- As a player, I want to guess numbers and receive feedback on whether my guess is too high or too low\n- As a player, I want to be able to set the difficulty level by choosing the range of possible numbers\n- As a player, I want to see my previous guesses to strategize my next guess\n- As a player, I want to know how many attempts it took me to guess the number once I get it right\n\n## Competitive Analysis\n\n- Guess The Number Game A: Basic text interface, no difficulty levels\n- Number Master B: Has difficulty levels, but cluttered interface\n- Quick Guess C: Sleek design, but lacks performance tracking\n- NumGuess D: Good performance tracking, but not mobile-friendly\n- GuessIt E: Mobile-friendly, but too many ads\n- Perfect Guess F: Offers hints, but the hints are not very helpful\n- SmartGuesser G: Has a learning mode, but lacks a competitive edge\n\n## Competitive Quadrant Chart\n\nquadrantChart\n title \"User Engagement and Game Complexity\"\n x-axis \"Low Complexity\" --> \"High Complexity\"\n y-axis \"Low Engagement\" --> \"High Engagement\"\n quadrant-1 \"Too Simple\"\n quadrant-2 \"Niche Appeal\"\n quadrant-3 \"Complex & Unengaging\"\n quadrant-4 \"Sweet Spot\"\n \"Guess The Number Game A\": [0.2, 0.4]\n \"Number Master B\": [0.5, 0.3]\n \"Quick Guess C\": [0.6, 0.7]\n \"NumGuess D\": [0.4, 0.6]\n \"GuessIt E\": [0.7, 0.5]\n \"Perfect Guess F\": [0.6, 0.4]\n \"SmartGuesser G\": [0.8, 0.6]\n \"Our Target Product\": [0.5, 0.8]\n\n## Requirement Analysis\n\nThe game should be simple yet engaging, allowing players of different skill levels to enjoy it. It should provide immediate feedback and track the player's performance. The game should also be designed with a clean and intuitive interface, and it should be easy to add new features in the future.\n\n## Requirement Pool\n\n- ['P0', 'Implement the core game logic to randomly select a number and allow the user to guess it']\n- ['P0', 'Design a user interface that displays the game status and results clearly']\n- ['P1', 'Add difficulty levels by varying the range of possible numbers']\n- ['P1', 'Keep track of and display the number of attempts for each game session']\n- ['P2', \"Store and show the history of the player's guesses during a game session\"]\n\n## UI Design draft\n\nThe UI will feature a clean and minimalist design with a number input field, submit button, and messages area to provide feedback. There will be options to select the difficulty level and a display showing the number of attempts and history of past guesses.\n\n## Anything UNCLEAR\n\n### New Requirements\n\nAdding graphical interface functionality to enhance the user experience in the number-guessing game. The existing number-guessing game currently relies on command-line input for numbers. The goal is to introduce a graphical interface to improve the game's usability and visual appeal\n\n\n\n-----\n\n## format example\n[CONTENT]\n{\n \"Language\": \"en_us\",\n \"Programming Language\": \"Python\",\n \"Refined Requirements\": \"Create a 2048 game with a new feature that ...\",\n \"Project Name\": \"game_2048\",\n \"Refined Product Goals\": [\n \"Enhance user engagement through new features\",\n \"Optimize performance for scalability\",\n \"Integrate innovative UI enhancements\"\n ],\n \"Refined User Stories\": [\n \"As a player, I want to choose difficulty levels to challenge my skills\",\n \"As a player, I want a visually appealing score display after each game for a better gaming experience\",\n \"As a player, I want a convenient restart button displayed when I lose to quickly start a new game\",\n \"As a player, I want an enhanced and aesthetically pleasing UI to elevate the overall gaming experience\",\n \"As a player, I want the ability to play the game seamlessly on my mobile phone for on-the-go entertainment\"\n ],\n \"Competitive Analysis\": [\n \"2048 Game A: Simple interface, lacks responsive features\",\n \"play2048.co: Beautiful and responsive UI with my best score shown\",\n \"2048game.com: Responsive UI with my best score shown, but many ads\"\n ],\n \"Competitive Quadrant Chart\": \"quadrantChart\\n title \\\"Reach and engagement of campaigns\\\"\\n x-axis \\\"Low Reach\\\" --> \\\"High Reach\\\"\\n y-axis \\\"Low Engagement\\\" --> \\\"High Engagement\\\"\\n quadrant-1 \\\"We should expand\\\"\\n quadrant-2 \\\"Need to promote\\\"\\n quadrant-3 \\\"Re-evaluate\\\"\\n quadrant-4 \\\"May be improved\\\"\\n \\\"Campaign A\\\": [0.3, 0.6]\\n \\\"Campaign B\\\": [0.45, 0.23]\\n \\\"Campaign C\\\": [0.57, 0.69]\\n \\\"Campaign D\\\": [0.78, 0.34]\\n \\\"Campaign E\\\": [0.40, 0.34]\\n \\\"Campaign F\\\": [0.35, 0.78]\\n \\\"Our Target Product\\\": [0.5, 0.6]\",\n \"Refined Requirement Analysis\": [\n \"Require add/update/modify ...\"\n ],\n \"Refined Requirement Pool\": [\n [\n \"P0\",\n \"The main code ...\"\n ],\n [\n \"P0\",\n \"The game algorithm ...\"\n ]\n ],\n \"UI Design draft\": \"Basic function description with a simple style and layout.\",\n \"Anything UNCLEAR\": \"\"\n}\n[/CONTENT]\n\n## nodes: \": # \"\n- Language: # Provide the language used in the project, typically matching the user's requirement language.\n- Programming Language: # Python/JavaScript or other mainstream programming language.\n- Refined Requirements: # Place the New user's original requirements here.\n- Project Name: # According to the content of \"Original Requirements,\" name the project using snake case style , like 'game_2048' or 'simple_crm.\n- Refined Product Goals: typing.List[str] # Update and expand the original product goals to reflect the evolving needs due to incremental development. Ensure that the refined goals align with the current project direction and contribute to its success.\n- Refined User Stories: typing.List[str] # Update and expand the original scenario-based user stories to reflect the evolving needs due to incremental development. Ensure that the refined user stories capture incremental features and improvements. \n- Competitive Analysis: typing.List[str] # Provide 5 to 7 competitive products.\n- Competitive Quadrant Chart: # Use mermaid quadrantChart syntax. Distribute scores evenly between 0 and 1\n- Refined Requirement Analysis: typing.List[str] # Review and refine the existing requirement analysis to align with the evolving needs of the project due to incremental development. Ensure the analysis comprehensively covers the new features and enhancements required for the refined project scope.\n- Refined Requirement Pool: typing.List[typing.List[str]] # List down the top 5 to 7 requirements with their priority (P0, P1, P2). Cover both legacy content and incremental content. Retain content unrelated to incremental development\n- UI Design draft: # Provide a simple description of UI elements, functions, style, and layout.\n- Anything UNCLEAR: # Mention any aspects of the project that are unclear and try to clarify them.\n\n\n## constraint\nLanguage: Please use the same language as Human INPUT.\nFormat: output wrapped inside [CONTENT][/CONTENT] like format example, nothing else.\n\n## action\nFollow instructions of nodes, generate output and make sure it follows the format example.\n": "[CONTENT]\n{\n \"Language\": \"en_us\",\n \"Programming Language\": \"Python\",\n \"Refined Requirements\": \"Adding graphical interface functionality to enhance the user experience in the number-guessing game. The existing number-guessing game currently relies on command-line input for numbers. The goal is to introduce a graphical interface to improve the game's usability and visual appeal.\",\n \"Project Name\": \"graphical_number_guesser\",\n \"Refined Product Goals\": [\n \"Introduce a graphical user interface to make the game more accessible and visually appealing\",\n \"Maintain a user-friendly experience while transitioning from a command-line interface to a graphical interface\",\n \"Ensure the game remains challenging and enjoyable with the addition of the new interface\",\n \"Design the graphical interface to be easily extendable for future enhancements\"\n ],\n \"Refined User Stories\": [\n \"As a player, I want to interact with a graphical interface to input my guesses and receive feedback\",\n \"As a player, I want to easily select difficulty levels through the graphical interface\",\n \"As a player, I want to see a visual representation of my previous guesses and the number of attempts\",\n \"As a player, I want to enjoy a visually engaging experience that enhances the overall gameplay\"\n ],\n \"Competitive Analysis\": [\n \"Guess The Number Game A: Basic text interface, no difficulty levels\",\n \"Number Master B: Has difficulty levels, but cluttered interface\",\n \"Quick Guess C: Sleek design, but lacks performance tracking\",\n \"NumGuess D: Good performance tracking, but not mobile-friendly\",\n \"GuessIt E: Mobile-friendly, but too many ads\",\n \"Perfect Guess F: Offers hints, but the hints are not very helpful\",\n \"SmartGuesser G: Has a learning mode, but lacks a competitive edge\",\n \"Graphical Number Guesser: Our new game with a graphical interface and user-friendly design\"\n ],\n \"Competitive Quadrant Chart\": \"quadrantChart\\n title \\\"User Engagement and Game Complexity\\\"\\n x-axis \\\"Low Complexity\\\" --> \\\"High Complexity\\\"\\n y-axis \\\"Low Engagement\\\" --> \\\"High Engagement\\\"\\n quadrant-1 \\\"Too Simple\\\"\\n quadrant-2 \\\"Niche Appeal\\\"\\n quadrant-3 \\\"Complex & Unengaging\\\"\\n quadrant-4 \\\"Sweet Spot\\\"\\n \\\"Guess The Number Game A\\\": [0.2, 0.4]\\n \\\"Number Master B\\\": [0.5, 0.3]\\n \\\"Quick Guess C\\\": [0.6, 0.7]\\n \\\"NumGuess D\\\": [0.4, 0.6]\\n \\\"GuessIt E\\\": [0.7, 0.5]\\n \\\"Perfect Guess F\\\": [0.6, 0.4]\\n \\\"SmartGuesser G\\\": [0.8, 0.6]\\n \\\"Graphical Number Guesser\\\": [0.5, 0.8]\",\n \"Refined Requirement Analysis\": [\n \"The game must provide an intuitive graphical interface for user interaction\",\n \"The difficulty level selection should be seamlessly integrated into the graphical interface\",\n \"Performance tracking must be visually represented within the game interface\",\n \"The game should be designed to allow for easy updates and feature additions\"\n ],\n \"Refined Requirement Pool\": [\n [\"P0\", \"Implement a graphical user interface for the game\"],\n [\"P0\", \"Ensure the graphical interface provides feedback on guesses\"],\n [\"P1\", \"Incorporate difficulty level selection into the graphical interface\"],\n [\"P1\", \"Display a visual representation of guess history and attempt count\"],\n [\"P2\", \"Design the interface to be extensible for future game features\"]\n ],\n \"UI Design draft\": \"The graphical interface will feature a main game window with an input field for number guesses, buttons for submitting guesses and selecting difficulty, and panels for displaying guess history, attempt count, and feedback messages.\",\n \"Anything UNCLEAR\": \"\"\n}\n[/CONTENT]", + "As a data scientist, you need to help user to achieve their goal step by step in a continuous Jupyter notebook. Since it is a notebook environment, don't use asyncio.run. Instead, use await if you need to call an async function.#SYSTEM_MSG_END#\n# User Requirement\nRun data analysis on sklearn Iris dataset, include a plot\n\n# Plan Status\n\n## Finished Tasks\n### code\n```python\n\n```\n\n### execution result\n\n\n## Current Task\nLoad the sklearn Iris dataset and perform exploratory data analysis\n\n## Task Guidance\nWrite complete code for 'Current Task'. And avoid duplicating code from 'Finished Tasks', such as repeated import of packages, reading data, etc.\nSpecifically, \nThe current task is about exploratory data analysis, please note the following:\n- Distinguish column types with `select_dtypes` for tailored analysis and visualization, such as correlation.\n- Remember to `import numpy as np` before using Numpy functions.\n\n\n\n# Tool Info\n\n\n# Constraints\n- Take on Current Task if it is in Plan Status, otherwise, tackle User Requirement directly.\n- Ensure the output new code is executable in the same Jupyter notebook as the previous executed code.\n- Always prioritize using pre-defined tools for the same functionality.\n\n# Output\nWhile some concise thoughts are helpful, code is absolutely required. Always output one and only one code block in your response. Output code in the following format:\n```python\nyour code\n```\n": "```python\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom sklearn import datasets\n\n# Load the Iris dataset\niris = datasets.load_iris()\niris_df = pd.DataFrame(data= np.c_[iris['data'], iris['target']],\n columns= iris['feature_names'] + ['target'])\n\n# Perform exploratory data analysis\n# Distinguish column types\nnumerical_cols = iris_df.select_dtypes(include=[np.number]).columns.tolist()\ncategorical_cols = iris_df.select_dtypes(include=[object]).columns.tolist()\n\n# Display basic statistics for numerical columns\nprint(iris_df[numerical_cols].describe())\n\n# Plotting histograms for numerical features\nfig, axs = plt.subplots(2, 2, figsize=(12, 8))\nfor i, col in enumerate(numerical_cols[:-1]): # Exclude the target column for histogram\n ax = axs[i//2, i%2]\n iris_df[col].hist(ax=ax)\n ax.set_title(col)\n ax.set_xlabel('Measurement (cm)')\n ax.set_ylabel('Frequency')\n\nplt.tight_layout()\nplt.show()\n\n# Plotting a scatter matrix for numerical features\npd.plotting.scatter_matrix(iris_df[numerical_cols], figsize=(12, 12), diagonal='kde')\nplt.show()\n\n# Correlation matrix heatmap\nplt.figure(figsize=(8, 6))\nsns.heatmap(iris_df[numerical_cols].corr(), annot=True, fmt=\".2f\")\nplt.show()\n```", + "As a data scientist, you need to help user to achieve their goal step by step in a continuous Jupyter notebook. Since it is a notebook environment, don't use asyncio.run. Instead, use await if you need to call an async function.#SYSTEM_MSG_END#\n# User Requirement\nPreprocess sklearn Wine recognition dataset and train a model to predict wine class (20% as validation), and show validation accuracy.\n\n# Plan Status\n\n\n# Tool Info\n\n ## Capabilities\n - You can utilize pre-defined tools in any code lines from 'Available Tools' in the form of Python class or function.\n - You can freely combine the use of any other public packages, like sklearn, numpy, pandas, etc..\n\n ## Available Tools:\n Each tool is described in JSON format. When you call a tool, import the tool from its path first.\n {'FillMissingValue': {'type': 'class', 'description': 'Completing missing values with simple strategies.', 'methods': {'__init__': {'type': 'function', 'description': 'Initialize self. ', 'signature': '(self, features: 'list', strategy: \"Literal['mean', 'median', 'most_frequent', 'constant']\" = 'mean', fill_value=None)', 'parameters': 'Args: features (list): Columns to be processed. strategy (Literal[\"mean\", \"median\", \"most_frequent\", \"constant\"], optional): The imputation strategy, notice 'mean' and 'median' can only be used for numeric features. Defaults to 'mean'. fill_value (int, optional): Fill_value is used to replace all occurrences of missing_values. Defaults to None.'}, 'fit': {'type': 'function', 'description': 'Fit a model to be used in subsequent transform. ', 'signature': \"(self, df: 'pd.DataFrame')\", 'parameters': 'Args: df (pd.DataFrame): The input DataFrame.'}, 'fit_transform': {'type': 'function', 'description': 'Fit and transform the input DataFrame. ', 'signature': \"(self, df: 'pd.DataFrame') -> 'pd.DataFrame'\", 'parameters': 'Args: df (pd.DataFrame): The input DataFrame. Returns: pd.DataFrame: The transformed DataFrame.'}, 'transform': {'type': 'function', 'description': 'Transform the input DataFrame with the fitted model. ', 'signature': \"(self, df: 'pd.DataFrame') -> 'pd.DataFrame'\", 'parameters': 'Args: df (pd.DataFrame): The input DataFrame. Returns: pd.DataFrame: The transformed DataFrame.'}}, 'tool_path': 'metagpt/tools/libs/data_preprocess.py'}\n \n\n# Constraints\n- Take on Current Task if it is in Plan Status, otherwise, tackle User Requirement directly.\n- Ensure the output new code is executable in the same Jupyter notebook as the previous executed code.\n- Always prioritize using pre-defined tools for the same functionality.\n\n# Output\nWhile some concise thoughts are helpful, code is absolutely required. Always output one and only one code block in your response. Output code in the following format:\n```python\nyour code\n```\n": "To achieve the goal of preprocessing the sklearn Wine recognition dataset, training a model, and showing validation accuracy, we will follow these steps:\n\n1. Load the Wine recognition dataset from `sklearn.datasets`.\n2. Preprocess the dataset by handling any missing values using the `FillMissingValue` tool.\n3. Split the dataset into training and validation sets.\n4. Train a classification model using the training set.\n5. Evaluate the model on the validation set and output the accuracy.\n\nLet's start by loading the dataset and checking for any missing values.\n\n```python\nfrom sklearn.datasets import load_wine\nimport pandas as pd\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.ensemble import RandomForestClassifier\nfrom sklearn.metrics import accuracy_score\n\n# Load the wine dataset\ndata = load_wine()\ndf = pd.DataFrame(data.data, columns=data.feature_names)\ndf['target'] = data.target\n\n# Check for missing values\nmissing_values = df.isnull().sum()\nprint(\"Missing values in each column:\\n\", missing_values)\n\n# If there are missing values, we will use the FillMissingValue tool to handle them\nif missing_values.sum() > 0:\n from metagpt.tools.libs.data_preprocess import FillMissingValue\n fill_missing = FillMissingValue(features=list(df.columns), strategy='mean')\n df = fill_missing.fit_transform(df)\n\n# Split the dataset into training and validation sets (80% train, 20% validation)\nX_train, X_val, y_train, y_val = train_test_split(df.drop('target', axis=1), df['target'], test_size=0.2, random_state=42)\n\n# Train a RandomForestClassifier\nmodel = RandomForestClassifier(random_state=42)\nmodel.fit(X_train, y_train)\n\n# Predict on the validation set\ny_pred = model.predict(X_val)\n\n# Calculate and print the validation accuracy\naccuracy = accuracy_score(y_val, y_pred)\nprint(f\"Validation accuracy: {accuracy:.2f}\")\n```", + "You are an AI Python assistant. You will be given your previous implementation code of a task, runtime error results, and a hint to change the implementation appropriately. Write your full implementation.#SYSTEM_MSG_END#\n[example]\nHere is an example of debugging with reflection.\n\n[previous impl]:\nassistant:\n```python\ndef add(a: int, b: int) -> int:\n \"\"\"\n Given integers a and b, return the total value of a and b.\n \"\"\"\n return a - b\n```\n\nuser:\nTests failed:\nassert add(1, 2) == 3 # output: -1\nassert add(1, 2) == 4 # output: -1\n\n[reflection on previous impl]:\nThe implementation failed the test cases where the input integers are 1 and 2. The issue arises because the code does not add the two integers together, but instead subtracts the second integer from the first. To fix this issue, we should change the operator from `-` to `+` in the return statement. This will ensure that the function returns the correct output for the given input.\n\n[improved impl]:\ndef add(a: int, b: int) -> int:\n \"\"\"\n Given integers a and b, return the total value of a and b.\n \"\"\"\n return a + b\n\n[/example]\n\n[context]\n[{'role': 'user', 'content': \"\\n# User Requirement\\nread a dataset test.csv and print its head\\n\\n# Plan Status\\n\\n ## Finished Tasks\\n ### code\\n ```python\\n ```\\n\\n ### execution result\\n\\n ## Current Task\\n import pandas and load the dataset from 'test.csv'.\\n\\n ## Task Guidance\\n Write complete code for 'Current Task'. And avoid duplicating code from 'Finished Tasks', such as repeated import of packages, reading data, etc.\\n Specifically, \\n \\n\\n# Tool Info\\n\\n\\n# Constraints\\n- Take on Current Task if it is in Plan Status, otherwise, tackle User Requirement directly.\\n- Ensure the output new code is executable in the same Jupyter notebook as the previous executed code.\\n- Always prioritize using pre-defined tools for the same functionality.\\n\\n# Output\\nWhile some concise thoughts are helpful, code is absolutely required. Always output one and only one code block in your response. Output code in the following format:\\n```python\\nyour code\\n```\\n\"}, {'role': 'assistant', 'content': \"import pandas as pd\\ndata = pd.read_excel('test.csv')\\ndata\"}, {'role': 'user', 'content': '\\n Traceback (most recent call last):\\n File \"\", line 2, in \\n File \"/Users/gary/miniconda3/envs/py39_scratch/lib/python3.9/site-packages/pandas/io/excel/_base.py\", line 478, in read_excel\\n io = ExcelFile(io, storage_options=storage_options, engine=engine)\\n File \"/Users/gary/miniconda3/envs/py39_scratch/lib/python3.9/site-packages/pandas/io/excel/_base.py\", line 1500, in __init__\\n raise ValueError(\\n ValueError: Excel file format cannot be determined, you must specify an engine manually.\\n '}]\n\n[previous impl]:\n[assistant: import pandas as pd\ndata = pd.read_excel('test.csv')\ndata, user: \n Traceback (most recent call last):\n File \"\", line 2, in \n File \"/Users/gary/miniconda3/envs/py39_scratch/lib/python3.9/site-packages/pandas/io/excel/_base.py\", line 478, in read_excel\n io = ExcelFile(io, storage_options=storage_options, engine=engine)\n File \"/Users/gary/miniconda3/envs/py39_scratch/lib/python3.9/site-packages/pandas/io/excel/_base.py\", line 1500, in __init__\n raise ValueError(\n ValueError: Excel file format cannot be determined, you must specify an engine manually.\n ]\n\n[instruction]\nAnalyze your previous code and error in [context] step by step, provide me with improved method and code. Remember to follow [context] requirement. Don't forget to write code for steps behind the error step.\nOutput a json following the format:\n```json\n{\n \"reflection\": str = \"Reflection on previous implementation\",\n \"improved_impl\": str = \"Refined code after reflection.\",\n}\n```\n": "```json\n{\n \"reflection\": \"The previous implementation attempted to read a CSV file using the `pd.read_excel` function, which is intended for Excel files, not CSV files. The error message indicates that the file format cannot be determined and an engine must be specified manually. However, the real issue is that the wrong function is being used for a CSV file. The correct function to use for reading CSV files is `pd.read_csv`.\",\n \"improved_impl\": \"import pandas as pd\\ndata = pd.read_csv('test.csv')\\nprint(data.head())\"\n}\n```", + "\n # Context:\n user: Run data analysis on sklearn Iris dataset, include a plot\n # Available Task Types:\n - **eda**: For performing exploratory data analysis\n- **data preprocessing**: For preprocessing dataset in a data analysis or machine learning task ONLY,general data operation doesn't fall into this type\n- **feature engineering**: Only for creating new columns for input data.\n- **model train**: Only for training model.\n- **model evaluate**: Only for evaluating model.\n- **image2webpage**: For converting image into webpage code.\n- **other**: Any tasks not in the defined categories\n- **text2image**: Related to text2image, image2image using stable diffusion model.\n- **web scraping**: For scraping data from web pages.\n- **email login**: For logging to an email.\n # Task:\n Based on the context, write a plan or modify an existing plan of what you should do to achieve the goal. A plan consists of one to 5 tasks.\n If you are modifying an existing plan, carefully follow the instruction, don't make unnecessary changes. Give the whole plan unless instructed to modify only one task of the plan.\n If you encounter errors on the current task, revise and output the current single task only.\n Output a list of jsons following the format:\n ```json\n [\n {\n \"task_id\": str = \"unique identifier for a task in plan, can be an ordinal\",\n \"dependent_task_ids\": list[str] = \"ids of tasks prerequisite to this task\",\n \"instruction\": \"what you should do in this task, one short phrase or sentence\",\n \"task_type\": \"type of this task, should be one of Available Task Types\",\n },\n ...\n ]\n ```\n ": "```json\n[\n {\n \"task_id\": \"1\",\n \"dependent_task_ids\": [],\n \"instruction\": \"Load the sklearn Iris dataset and display the first few rows.\",\n \"task_type\": \"eda\"\n },\n {\n \"task_id\": \"2\",\n \"dependent_task_ids\": [\"1\"],\n \"instruction\": \"Perform exploratory data analysis on the Iris dataset to summarize the main characteristics.\",\n \"task_type\": \"eda\"\n },\n {\n \"task_id\": \"3\",\n \"dependent_task_ids\": [\"2\"],\n \"instruction\": \"Create a scatter plot of the Iris dataset showing the relationship between sepal length and sepal width, color-coded by species.\",\n \"task_type\": \"eda\"\n }\n]\n```", + "\n # Context:\n user: \n## User Requirement\nRun data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.\n## Context\n\n## Current Plan\n[]\n## Current Task\n{}\n\n # Available Task Types:\n - **eda**: For performing exploratory data analysis\n- **data preprocessing**: For preprocessing dataset in a data analysis or machine learning task ONLY,general data operation doesn't fall into this type\n- **feature engineering**: Only for creating new columns for input data.\n- **model train**: Only for training model.\n- **model evaluate**: Only for evaluating model.\n- **image2webpage**: For converting image into webpage code.\n- **other**: Any tasks not in the defined categories\n- **text2image**: Related to text2image, image2image using stable diffusion model.\n- **web scraping**: For scraping data from web pages.\n- **email login**: For logging to an email.\n # Task:\n Based on the context, write a plan or modify an existing plan of what you should do to achieve the goal. A plan consists of one to 3 tasks.\n If you are modifying an existing plan, carefully follow the instruction, don't make unnecessary changes. Give the whole plan unless instructed to modify only one task of the plan.\n If you encounter errors on the current task, revise and output the current single task only.\n Output a list of jsons following the format:\n ```json\n [\n {\n \"task_id\": str = \"unique identifier for a task in plan, can be an ordinal\",\n \"dependent_task_ids\": list[str] = \"ids of tasks prerequisite to this task\",\n \"instruction\": \"what you should do in this task, one short phrase or sentence\",\n \"task_type\": \"type of this task, should be one of Available Task Types\",\n },\n ...\n ]\n ```\n ": "```json\n[\n {\n \"task_id\": \"1\",\n \"dependent_task_ids\": [],\n \"instruction\": \"Perform exploratory data analysis on the sklearn Wine recognition dataset including summary statistics and a plot.\",\n \"task_type\": \"eda\"\n },\n {\n \"task_id\": \"2\",\n \"dependent_task_ids\": [\"1\"],\n \"instruction\": \"Preprocess the dataset by splitting it into training and validation sets with a 80-20 split.\",\n \"task_type\": \"data preprocessing\"\n },\n {\n \"task_id\": \"3\",\n \"dependent_task_ids\": [\"2\"],\n \"instruction\": \"Train a model using the training set to predict wine class.\",\n \"task_type\": \"model train\"\n },\n {\n \"task_id\": \"4\",\n \"dependent_task_ids\": [\"3\"],\n \"instruction\": \"Evaluate the model on the validation set and show the validation accuracy.\",\n \"task_type\": \"model evaluate\"\n }\n]\n```", + "As a data scientist, you need to help user to achieve their goal step by step in a continuous Jupyter notebook. Since it is a notebook environment, don't use asyncio.run. Instead, use await if you need to call an async function.#SYSTEM_MSG_END#\n# User Requirement\nRun data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.\n\n# Plan Status\n\n## Finished Tasks\n### code\n```python\n\n```\n\n### execution result\n\n\n## Current Task\nPerform exploratory data analysis on the sklearn Wine recognition dataset including summary statistics and a plot.\n\n## Task Guidance\nWrite complete code for 'Current Task'. And avoid duplicating code from 'Finished Tasks', such as repeated import of packages, reading data, etc.\nSpecifically, \nThe current task is about exploratory data analysis, please note the following:\n- Distinguish column types with `select_dtypes` for tailored analysis and visualization, such as correlation.\n- Remember to `import numpy as np` before using Numpy functions.\n\n\n\n# Tool Info\n\n\n# Constraints\n- Take on Current Task if it is in Plan Status, otherwise, tackle User Requirement directly.\n- Ensure the output new code is executable in the same Jupyter notebook as the previous executed code.\n- Always prioritize using pre-defined tools for the same functionality.\n\n# Output\nWhile some concise thoughts are helpful, code is absolutely required. Always output one and only one code block in your response. Output code in the following format:\n```python\nyour code\n```\n": "```python\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom sklearn.datasets import load_wine\n\n# Load the wine dataset\nwine = load_wine()\nwine_df = pd.DataFrame(data=np.c_[wine['data'], wine['target']],\n columns=wine['feature_names'] + ['target'])\n\n# Summary statistics for numerical features\nnumerical_summary = wine_df.describe()\n\n# Summary statistics for categorical features\ncategorical_summary = wine_df.select_dtypes(include=['object', 'category']).describe()\n\n# Correlation matrix for numerical features\ncorrelation_matrix = wine_df.corr()\n\n# Plotting a histogram for each numerical feature\nwine_df.hist(bins=15, figsize=(15, 10), layout=(4, 4))\nplt.tight_layout()\nplt.show()\n\n# Displaying the summary statistics\nprint(\"Numerical Summary:\\n\", numerical_summary)\nprint(\"\\nCategorical Summary:\\n\", categorical_summary)\nprint(\"\\nCorrelation Matrix:\\n\", correlation_matrix)\n```", + "\n# Background\nCheck latest data info to guide subsequent tasks.\n\n## Finished Tasks\n```python\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom sklearn.datasets import load_wine\nwine = load_wine()\nwine_df = pd.DataFrame(data=np.c_[wine['data'], wine['target']],\n columns=wine['feature_names'] + ['target'])\nnumerical_summary = wine_df.describe()\ncategorical_summary = wine_df.select_dtypes(include=['object', 'category']).describe()\ncorrelation_matrix = wine_df.corr()\nwine_df.hist(bins=15, figsize=(15, 10), layout=(4, 4))\nplt.tight_layout()\nplt.show()\nprint(\"Numerical Summary:\\n\", numerical_summary)\nprint(\"\\nCategorical Summary:\\n\", categorical_summary)\nprint(\"\\nCorrelation Matrix:\\n\", correlation_matrix)\n```end\n\n# Task\nCheck code in finished tasks, print key variables to guide your following actions.\nSpecifically, if it is a data analysis or machine learning task, print the the latest column information using the following code, with DataFrame variable from 'Finished Tasks' in place of df:\n```python\nfrom metagpt.tools.libs.data_preprocess import get_column_info\n\ncolumn_info = get_column_info(df)\nprint(\"column_info\")\nprint(column_info)\n```end\nOtherwise, print out any key variables you see fit. Return an empty string if you think there is no important data to check.\n\n# Constraints:\n- Your code is to be added to a new cell in jupyter.\n\n# Instruction\nOutput code following the format:\n```python\nyour code\n```\n": "```python\nfrom metagpt.tools.libs.data_preprocess import get_column_info\n\ncolumn_info = get_column_info(wine_df)\nprint(\"column_info\")\nprint(column_info)\n```", + "As a data scientist, you need to help user to achieve their goal step by step in a continuous Jupyter notebook. Since it is a notebook environment, don't use asyncio.run. Instead, use await if you need to call an async function.#SYSTEM_MSG_END#\n# User Requirement\nRun data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.\n\n# Plan Status\n\n## Finished Tasks\n### code\n```python\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom sklearn.datasets import load_wine\nwine = load_wine()\nwine_df = pd.DataFrame(data=np.c_[wine['data'], wine['target']],\n columns=wine['feature_names'] + ['target'])\nnumerical_summary = wine_df.describe()\ncategorical_summary = wine_df.select_dtypes(include=['object', 'category']).describe()\ncorrelation_matrix = wine_df.corr()\nwine_df.hist(bins=15, figsize=(15, 10), layout=(4, 4))\nplt.tight_layout()\nplt.show()\nprint(\"Numerical Summary:\\n\", numerical_summary)\nprint(\"\\nCategorical Summary:\\n\", categorical_summary)\nprint(\"\\nCorrelation Matrix:\\n\", correlation_matrix)\n```\n\n### execution result\na successful run\n\n## Current Task\nPreprocess the dataset by splitting it into training and validation sets with a 80-20 split.\n\n## Task Guidance\nWrite complete code for 'Current Task'. And avoid duplicating code from 'Finished Tasks', such as repeated import of packages, reading data, etc.\nSpecifically, \n\n\n# Tool Info\n\n\n# Constraints\n- Take on Current Task if it is in Plan Status, otherwise, tackle User Requirement directly.\n- Ensure the output new code is executable in the same Jupyter notebook as the previous executed code.\n- Always prioritize using pre-defined tools for the same functionality.\n\n# Output\nWhile some concise thoughts are helpful, code is absolutely required. Always output one and only one code block in your response. Output code in the following format:\n```python\nyour code\n```\n#MSG_SEP#\n# Latest Data Info\nLatest data info after previous tasks:\na successful run\n": "```python\nfrom sklearn.model_selection import train_test_split\n\n# Split the data into training and validation sets\nX_train, X_val, y_train, y_val = train_test_split(\n wine.data, wine.target, test_size=0.2, random_state=42\n)\n```", + "\n# Background\nCheck latest data info to guide subsequent tasks.\n\n## Finished Tasks\n```python\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom sklearn.datasets import load_wine\nwine = load_wine()\nwine_df = pd.DataFrame(data=np.c_[wine['data'], wine['target']],\n columns=wine['feature_names'] + ['target'])\nnumerical_summary = wine_df.describe()\ncategorical_summary = wine_df.select_dtypes(include=['object', 'category']).describe()\ncorrelation_matrix = wine_df.corr()\nwine_df.hist(bins=15, figsize=(15, 10), layout=(4, 4))\nplt.tight_layout()\nplt.show()\nprint(\"Numerical Summary:\\n\", numerical_summary)\nprint(\"\\nCategorical Summary:\\n\", categorical_summary)\nprint(\"\\nCorrelation Matrix:\\n\", correlation_matrix)\n\nfrom sklearn.model_selection import train_test_split\nX_train, X_val, y_train, y_val = train_test_split(\n wine.data, wine.target, test_size=0.2, random_state=42\n)\n```end\n\n# Task\nCheck code in finished tasks, print key variables to guide your following actions.\nSpecifically, if it is a data analysis or machine learning task, print the the latest column information using the following code, with DataFrame variable from 'Finished Tasks' in place of df:\n```python\nfrom metagpt.tools.libs.data_preprocess import get_column_info\n\ncolumn_info = get_column_info(df)\nprint(\"column_info\")\nprint(column_info)\n```end\nOtherwise, print out any key variables you see fit. Return an empty string if you think there is no important data to check.\n\n# Constraints:\n- Your code is to be added to a new cell in jupyter.\n\n# Instruction\nOutput code following the format:\n```python\nyour code\n```\n": "```python\nfrom metagpt.tools.libs.data_preprocess import get_column_info\n\ncolumn_info = get_column_info(wine_df)\nprint(\"column_info\")\nprint(column_info)\n```", + "As a data scientist, you need to help user to achieve their goal step by step in a continuous Jupyter notebook. Since it is a notebook environment, don't use asyncio.run. Instead, use await if you need to call an async function.#SYSTEM_MSG_END#\n# User Requirement\nRun data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.\n\n# Plan Status\n\n## Finished Tasks\n### code\n```python\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom sklearn.datasets import load_wine\nwine = load_wine()\nwine_df = pd.DataFrame(data=np.c_[wine['data'], wine['target']],\n columns=wine['feature_names'] + ['target'])\nnumerical_summary = wine_df.describe()\ncategorical_summary = wine_df.select_dtypes(include=['object', 'category']).describe()\ncorrelation_matrix = wine_df.corr()\nwine_df.hist(bins=15, figsize=(15, 10), layout=(4, 4))\nplt.tight_layout()\nplt.show()\nprint(\"Numerical Summary:\\n\", numerical_summary)\nprint(\"\\nCategorical Summary:\\n\", categorical_summary)\nprint(\"\\nCorrelation Matrix:\\n\", correlation_matrix)\n\nfrom sklearn.model_selection import train_test_split\nX_train, X_val, y_train, y_val = train_test_split(\n wine.data, wine.target, test_size=0.2, random_state=42\n)\n```\n\n### execution result\na successful run\n\na successful run\n\n## Current Task\nTrain a model using the training set to predict wine class.\n\n## Task Guidance\nWrite complete code for 'Current Task'. And avoid duplicating code from 'Finished Tasks', such as repeated import of packages, reading data, etc.\nSpecifically, \n\n\n# Tool Info\n\n\n# Constraints\n- Take on Current Task if it is in Plan Status, otherwise, tackle User Requirement directly.\n- Ensure the output new code is executable in the same Jupyter notebook as the previous executed code.\n- Always prioritize using pre-defined tools for the same functionality.\n\n# Output\nWhile some concise thoughts are helpful, code is absolutely required. Always output one and only one code block in your response. Output code in the following format:\n```python\nyour code\n```\n#MSG_SEP#\n# Latest Data Info\nLatest data info after previous tasks:\na successful run\n": "```python\nfrom sklearn.ensemble import RandomForestClassifier\nfrom sklearn.metrics import accuracy_score\n\n# Initialize the model\nmodel = RandomForestClassifier(random_state=42)\n\n# Train the model\nmodel.fit(X_train, y_train)\n\n# Predict the validation set\ny_pred = model.predict(X_val)\n\n# Calculate the accuracy\naccuracy = accuracy_score(y_val, y_pred)\nprint(f\"Validation Accuracy: {accuracy:.2f}\")\n```", + "As a data scientist, you need to help user to achieve their goal step by step in a continuous Jupyter notebook. Since it is a notebook environment, don't use asyncio.run. Instead, use await if you need to call an async function.#SYSTEM_MSG_END#\n# User Requirement\nRun data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.\n\n# Plan Status\n\n## Finished Tasks\n### code\n```python\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom sklearn.datasets import load_wine\nwine = load_wine()\nwine_df = pd.DataFrame(data=np.c_[wine['data'], wine['target']],\n columns=wine['feature_names'] + ['target'])\nnumerical_summary = wine_df.describe()\ncategorical_summary = wine_df.select_dtypes(include=['object', 'category']).describe()\ncorrelation_matrix = wine_df.corr()\nwine_df.hist(bins=15, figsize=(15, 10), layout=(4, 4))\nplt.tight_layout()\nplt.show()\nprint(\"Numerical Summary:\\n\", numerical_summary)\nprint(\"\\nCategorical Summary:\\n\", categorical_summary)\nprint(\"\\nCorrelation Matrix:\\n\", correlation_matrix)\n\nfrom sklearn.model_selection import train_test_split\nX_train, X_val, y_train, y_val = train_test_split(\n wine.data, wine.target, test_size=0.2, random_state=42\n)\n\nfrom sklearn.ensemble import RandomForestClassifier\nfrom sklearn.metrics import accuracy_score\nmodel = RandomForestClassifier(random_state=42)\nmodel.fit(X_train, y_train)\ny_pred = model.predict(X_val)\naccuracy = accuracy_score(y_val, y_pred)\nprint(f\"Validation Accuracy: {accuracy:.2f}\")\n```\n\n### execution result\na successful run\n\na successful run\n\na successful run\n\n## Current Task\nEvaluate the model on the validation set and show the validation accuracy.\n\n## Task Guidance\nWrite complete code for 'Current Task'. And avoid duplicating code from 'Finished Tasks', such as repeated import of packages, reading data, etc.\nSpecifically, \n\n\n# Tool Info\n\n\n# Constraints\n- Take on Current Task if it is in Plan Status, otherwise, tackle User Requirement directly.\n- Ensure the output new code is executable in the same Jupyter notebook as the previous executed code.\n- Always prioritize using pre-defined tools for the same functionality.\n\n# Output\nWhile some concise thoughts are helpful, code is absolutely required. Always output one and only one code block in your response. Output code in the following format:\n```python\nyour code\n```\n": "```python\n# Since the model has already been trained and predictions have been made on the validation set,\n# we only need to evaluate the model using the predictions and the true labels from the validation set.\n\n# The accuracy has already been calculated and printed in the previous task.\n# However, to follow the task guidance and to ensure the output is only related to the current task,\n# we will calculate and print the validation accuracy again.\n\nvalidation_accuracy = accuracy_score(y_val, y_pred)\nprint(f\"Validation Accuracy: {validation_accuracy:.2f}\")\n```", + "user: \n## User Requirement\nRun data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.\n## Context\n\n## Current Plan\n[]\n## Current Task\n{}\n\nassistant: [\n {\n \"task_id\": \"1\",\n \"dependent_task_ids\": [],\n \"instruction\": \"Perform exploratory data analysis on the sklearn Wine recognition dataset including summary statistics and a plot.\",\n \"task_type\": \"eda\"\n },\n {\n \"task_id\": \"2\",\n \"dependent_task_ids\": [\"1\"],\n \"instruction\": \"Preprocess the dataset by splitting it into training and validation sets with a 80-20 split.\",\n \"task_type\": \"data preprocessing\"\n },\n {\n \"task_id\": \"3\",\n \"dependent_task_ids\": [\"2\"],\n \"instruction\": \"Train a model using the training set to predict wine class.\",\n \"task_type\": \"model train\"\n },\n {\n \"task_id\": \"4\",\n \"dependent_task_ids\": [\"3\"],\n \"instruction\": \"Evaluate the model on the validation set and show the validation accuracy.\",\n \"task_type\": \"model evaluate\"\n }\n]\nThis is a review. Please review output from metagpt.actions.di.write_plan.WritePlan\nIf you want to change, add, delete a task or merge tasks in the plan, say 'change task task_id or current task, ... (things to change)' If you confirm the output from the current task and wish to continue, type: confirmIf you think user requirement has been fulfilled completedly, you can finish the process by typing: finish\nIf you want to terminate the process, type: exit\nPlease type your review below:\n": "confirm", + "user: \n## User Requirement\nRun data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.\n## Context\n\n## Current Plan\n[\n {\n \"task_id\": \"1\",\n \"dependent_task_ids\": [],\n \"instruction\": \"Perform exploratory data analysis on the sklearn Wine recognition dataset including summary statistics and a plot.\",\n \"task_type\": \"eda\",\n \"code\": \"\",\n \"result\": \"\",\n \"is_success\": false,\n \"is_finished\": false\n },\n {\n \"task_id\": \"2\",\n \"dependent_task_ids\": [\n \"1\"\n ],\n \"instruction\": \"Preprocess the dataset by splitting it into training and validation sets with a 80-20 split.\",\n \"task_type\": \"data preprocessing\",\n \"code\": \"\",\n \"result\": \"\",\n \"is_success\": false,\n \"is_finished\": false\n },\n {\n \"task_id\": \"3\",\n \"dependent_task_ids\": [\n \"2\"\n ],\n \"instruction\": \"Train a model using the training set to predict wine class.\",\n \"task_type\": \"model train\",\n \"code\": \"\",\n \"result\": \"\",\n \"is_success\": false,\n \"is_finished\": false\n },\n {\n \"task_id\": \"4\",\n \"dependent_task_ids\": [\n \"3\"\n ],\n \"instruction\": \"Evaluate the model on the validation set and show the validation accuracy.\",\n \"task_type\": \"model evaluate\",\n \"code\": \"\",\n \"result\": \"\",\n \"is_success\": false,\n \"is_finished\": false\n }\n]\n## Current Task\n{\"task_id\":\"1\",\"dependent_task_ids\":[],\"instruction\":\"Perform exploratory data analysis on the sklearn Wine recognition dataset including summary statistics and a plot.\",\"task_type\":\"eda\",\"code\":\"\",\"result\":\"\",\"is_success\":false,\"is_finished\":false}\n\nassistant: import numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom sklearn.datasets import load_wine\n\n# Load the wine dataset\nwine = load_wine()\nwine_df = pd.DataFrame(data=np.c_[wine['data'], wine['target']],\n columns=wine['feature_names'] + ['target'])\n\n# Summary statistics for numerical features\nnumerical_summary = wine_df.describe()\n\n# Summary statistics for categorical features\ncategorical_summary = wine_df.select_dtypes(include=['object', 'category']).describe()\n\n# Correlation matrix for numerical features\ncorrelation_matrix = wine_df.corr()\n\n# Plotting a histogram for each numerical feature\nwine_df.hist(bins=15, figsize=(15, 10), layout=(4, 4))\nplt.tight_layout()\nplt.show()\n\n# Displaying the summary statistics\nprint(\"Numerical Summary:\\n\", numerical_summary)\nprint(\"\\nCategorical Summary:\\n\", categorical_summary)\nprint(\"\\nCorrelation Matrix:\\n\", correlation_matrix)\n\nuser: a successful runThis is a review. Please review output from metagpt.actions.di.execute_nb_code.ExecuteNbCode\nIf you want to change, add, delete a task or merge tasks in the plan, say 'change task task_id or current task, ... (things to change)' If you confirm the output from the current task and wish to continue, type: confirmIf you think user requirement has been fulfilled completedly, you can finish the process by typing: finish\nIf you want to terminate the process, type: exit\nPlease type your review below:\n": "confirm", + "user: \n## User Requirement\nRun data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.\n## Context\n\n## Current Plan\n[\n {\n \"task_id\": \"1\",\n \"dependent_task_ids\": [],\n \"instruction\": \"Perform exploratory data analysis on the sklearn Wine recognition dataset including summary statistics and a plot.\",\n \"task_type\": \"eda\",\n \"code\": \"import numpy as np\\nimport pandas as pd\\nimport matplotlib.pyplot as plt\\nfrom sklearn.datasets import load_wine\\n\\n# Load the wine dataset\\nwine = load_wine()\\nwine_df = pd.DataFrame(data=np.c_[wine['data'], wine['target']],\\n columns=wine['feature_names'] + ['target'])\\n\\n# Summary statistics for numerical features\\nnumerical_summary = wine_df.describe()\\n\\n# Summary statistics for categorical features\\ncategorical_summary = wine_df.select_dtypes(include=['object', 'category']).describe()\\n\\n# Correlation matrix for numerical features\\ncorrelation_matrix = wine_df.corr()\\n\\n# Plotting a histogram for each numerical feature\\nwine_df.hist(bins=15, figsize=(15, 10), layout=(4, 4))\\nplt.tight_layout()\\nplt.show()\\n\\n# Displaying the summary statistics\\nprint(\\\"Numerical Summary:\\\\n\\\", numerical_summary)\\nprint(\\\"\\\\nCategorical Summary:\\\\n\\\", categorical_summary)\\nprint(\\\"\\\\nCorrelation Matrix:\\\\n\\\", correlation_matrix)\\n\",\n \"result\": \"a successful run\",\n \"is_success\": true,\n \"is_finished\": true\n },\n {\n \"task_id\": \"2\",\n \"dependent_task_ids\": [\n \"1\"\n ],\n \"instruction\": \"Preprocess the dataset by splitting it into training and validation sets with a 80-20 split.\",\n \"task_type\": \"data preprocessing\",\n \"code\": \"\",\n \"result\": \"\",\n \"is_success\": false,\n \"is_finished\": false\n },\n {\n \"task_id\": \"3\",\n \"dependent_task_ids\": [\n \"2\"\n ],\n \"instruction\": \"Train a model using the training set to predict wine class.\",\n \"task_type\": \"model train\",\n \"code\": \"\",\n \"result\": \"\",\n \"is_success\": false,\n \"is_finished\": false\n },\n {\n \"task_id\": \"4\",\n \"dependent_task_ids\": [\n \"3\"\n ],\n \"instruction\": \"Evaluate the model on the validation set and show the validation accuracy.\",\n \"task_type\": \"model evaluate\",\n \"code\": \"\",\n \"result\": \"\",\n \"is_success\": false,\n \"is_finished\": false\n }\n]\n## Current Task\n{\"task_id\":\"2\",\"dependent_task_ids\":[\"1\"],\"instruction\":\"Preprocess the dataset by splitting it into training and validation sets with a 80-20 split.\",\"task_type\":\"data preprocessing\",\"code\":\"\",\"result\":\"\",\"is_success\":false,\"is_finished\":false}\n\nuser: \n# Latest Data Info\nLatest data info after previous tasks:\na successful run\n\nassistant: from sklearn.model_selection import train_test_split\n\n# Split the data into training and validation sets\nX_train, X_val, y_train, y_val = train_test_split(\n wine.data, wine.target, test_size=0.2, random_state=42\n)\n\nuser: a successful runThis is a review. Please review output from metagpt.actions.di.execute_nb_code.ExecuteNbCode\nIf you want to change, add, delete a task or merge tasks in the plan, say 'change task task_id or current task, ... (things to change)' If you confirm the output from the current task and wish to continue, type: confirmIf you think user requirement has been fulfilled completedly, you can finish the process by typing: finish\nIf you want to terminate the process, type: exit\nPlease type your review below:\n": "confirm", + "user: \n## User Requirement\nRun data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.\n## Context\n\n## Current Plan\n[\n {\n \"task_id\": \"1\",\n \"dependent_task_ids\": [],\n \"instruction\": \"Perform exploratory data analysis on the sklearn Wine recognition dataset including summary statistics and a plot.\",\n \"task_type\": \"eda\",\n \"code\": \"import numpy as np\\nimport pandas as pd\\nimport matplotlib.pyplot as plt\\nfrom sklearn.datasets import load_wine\\n\\n# Load the wine dataset\\nwine = load_wine()\\nwine_df = pd.DataFrame(data=np.c_[wine['data'], wine['target']],\\n columns=wine['feature_names'] + ['target'])\\n\\n# Summary statistics for numerical features\\nnumerical_summary = wine_df.describe()\\n\\n# Summary statistics for categorical features\\ncategorical_summary = wine_df.select_dtypes(include=['object', 'category']).describe()\\n\\n# Correlation matrix for numerical features\\ncorrelation_matrix = wine_df.corr()\\n\\n# Plotting a histogram for each numerical feature\\nwine_df.hist(bins=15, figsize=(15, 10), layout=(4, 4))\\nplt.tight_layout()\\nplt.show()\\n\\n# Displaying the summary statistics\\nprint(\\\"Numerical Summary:\\\\n\\\", numerical_summary)\\nprint(\\\"\\\\nCategorical Summary:\\\\n\\\", categorical_summary)\\nprint(\\\"\\\\nCorrelation Matrix:\\\\n\\\", correlation_matrix)\\n\",\n \"result\": \"a successful run\",\n \"is_success\": true,\n \"is_finished\": true\n },\n {\n \"task_id\": \"2\",\n \"dependent_task_ids\": [\n \"1\"\n ],\n \"instruction\": \"Preprocess the dataset by splitting it into training and validation sets with a 80-20 split.\",\n \"task_type\": \"data preprocessing\",\n \"code\": \"from sklearn.model_selection import train_test_split\\n\\n# Split the data into training and validation sets\\nX_train, X_val, y_train, y_val = train_test_split(\\n wine.data, wine.target, test_size=0.2, random_state=42\\n)\\n\",\n \"result\": \"a successful run\",\n \"is_success\": true,\n \"is_finished\": true\n },\n {\n \"task_id\": \"3\",\n \"dependent_task_ids\": [\n \"2\"\n ],\n \"instruction\": \"Train a model using the training set to predict wine class.\",\n \"task_type\": \"model train\",\n \"code\": \"\",\n \"result\": \"\",\n \"is_success\": false,\n \"is_finished\": false\n },\n {\n \"task_id\": \"4\",\n \"dependent_task_ids\": [\n \"3\"\n ],\n \"instruction\": \"Evaluate the model on the validation set and show the validation accuracy.\",\n \"task_type\": \"model evaluate\",\n \"code\": \"\",\n \"result\": \"\",\n \"is_success\": false,\n \"is_finished\": false\n }\n]\n## Current Task\n{\"task_id\":\"3\",\"dependent_task_ids\":[\"2\"],\"instruction\":\"Train a model using the training set to predict wine class.\",\"task_type\":\"model train\",\"code\":\"\",\"result\":\"\",\"is_success\":false,\"is_finished\":false}\n\nuser: \n# Latest Data Info\nLatest data info after previous tasks:\na successful run\n\nassistant: from sklearn.ensemble import RandomForestClassifier\nfrom sklearn.metrics import accuracy_score\n\n# Initialize the model\nmodel = RandomForestClassifier(random_state=42)\n\n# Train the model\nmodel.fit(X_train, y_train)\n\n# Predict the validation set\ny_pred = model.predict(X_val)\n\n# Calculate the accuracy\naccuracy = accuracy_score(y_val, y_pred)\nprint(f\"Validation Accuracy: {accuracy:.2f}\")\n\nuser: a successful runThis is a review. Please review output from metagpt.actions.di.execute_nb_code.ExecuteNbCode\nIf you want to change, add, delete a task or merge tasks in the plan, say 'change task task_id or current task, ... (things to change)' If you confirm the output from the current task and wish to continue, type: confirmIf you think user requirement has been fulfilled completedly, you can finish the process by typing: finish\nIf you want to terminate the process, type: exit\nPlease type your review below:\n": "confirm", + "user: \n## User Requirement\nRun data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.\n## Context\n\n## Current Plan\n[\n {\n \"task_id\": \"1\",\n \"dependent_task_ids\": [],\n \"instruction\": \"Perform exploratory data analysis on the sklearn Wine recognition dataset including summary statistics and a plot.\",\n \"task_type\": \"eda\",\n \"code\": \"import numpy as np\\nimport pandas as pd\\nimport matplotlib.pyplot as plt\\nfrom sklearn.datasets import load_wine\\n\\n# Load the wine dataset\\nwine = load_wine()\\nwine_df = pd.DataFrame(data=np.c_[wine['data'], wine['target']],\\n columns=wine['feature_names'] + ['target'])\\n\\n# Summary statistics for numerical features\\nnumerical_summary = wine_df.describe()\\n\\n# Summary statistics for categorical features\\ncategorical_summary = wine_df.select_dtypes(include=['object', 'category']).describe()\\n\\n# Correlation matrix for numerical features\\ncorrelation_matrix = wine_df.corr()\\n\\n# Plotting a histogram for each numerical feature\\nwine_df.hist(bins=15, figsize=(15, 10), layout=(4, 4))\\nplt.tight_layout()\\nplt.show()\\n\\n# Displaying the summary statistics\\nprint(\\\"Numerical Summary:\\\\n\\\", numerical_summary)\\nprint(\\\"\\\\nCategorical Summary:\\\\n\\\", categorical_summary)\\nprint(\\\"\\\\nCorrelation Matrix:\\\\n\\\", correlation_matrix)\\n\",\n \"result\": \"a successful run\",\n \"is_success\": true,\n \"is_finished\": true\n },\n {\n \"task_id\": \"2\",\n \"dependent_task_ids\": [\n \"1\"\n ],\n \"instruction\": \"Preprocess the dataset by splitting it into training and validation sets with a 80-20 split.\",\n \"task_type\": \"data preprocessing\",\n \"code\": \"from sklearn.model_selection import train_test_split\\n\\n# Split the data into training and validation sets\\nX_train, X_val, y_train, y_val = train_test_split(\\n wine.data, wine.target, test_size=0.2, random_state=42\\n)\\n\",\n \"result\": \"a successful run\",\n \"is_success\": true,\n \"is_finished\": true\n },\n {\n \"task_id\": \"3\",\n \"dependent_task_ids\": [\n \"2\"\n ],\n \"instruction\": \"Train a model using the training set to predict wine class.\",\n \"task_type\": \"model train\",\n \"code\": \"from sklearn.ensemble import RandomForestClassifier\\nfrom sklearn.metrics import accuracy_score\\n\\n# Initialize the model\\nmodel = RandomForestClassifier(random_state=42)\\n\\n# Train the model\\nmodel.fit(X_train, y_train)\\n\\n# Predict the validation set\\ny_pred = model.predict(X_val)\\n\\n# Calculate the accuracy\\naccuracy = accuracy_score(y_val, y_pred)\\nprint(f\\\"Validation Accuracy: {accuracy:.2f}\\\")\\n\",\n \"result\": \"a successful run\",\n \"is_success\": true,\n \"is_finished\": true\n },\n {\n \"task_id\": \"4\",\n \"dependent_task_ids\": [\n \"3\"\n ],\n \"instruction\": \"Evaluate the model on the validation set and show the validation accuracy.\",\n \"task_type\": \"model evaluate\",\n \"code\": \"\",\n \"result\": \"\",\n \"is_success\": false,\n \"is_finished\": false\n }\n]\n## Current Task\n{\"task_id\":\"4\",\"dependent_task_ids\":[\"3\"],\"instruction\":\"Evaluate the model on the validation set and show the validation accuracy.\",\"task_type\":\"model evaluate\",\"code\":\"\",\"result\":\"\",\"is_success\":false,\"is_finished\":false}\n\nassistant: # Since the model has already been trained and predictions have been made on the validation set,\n# we only need to evaluate the model using the predictions and the true labels from the validation set.\n\n# The accuracy has already been calculated and printed in the previous task.\n# However, to follow the task guidance and to ensure the output is only related to the current task,\n# we will calculate and print the validation accuracy again.\n\nvalidation_accuracy = accuracy_score(y_val, y_pred)\nprint(f\"Validation Accuracy: {validation_accuracy:.2f}\")\n\nuser: a successful runThis is a review. Please review output from metagpt.actions.di.execute_nb_code.ExecuteNbCode\nIf you want to change, add, delete a task or merge tasks in the plan, say 'change task task_id or current task, ... (things to change)' If you confirm the output from the current task and wish to continue, type: confirmIf you think user requirement has been fulfilled completedly, you can finish the process by typing: finish\nIf you want to terminate the process, type: exit\nPlease type your review below:\n": "confirm", + "As a data scientist, you need to help user to achieve their goal step by step in a continuous Jupyter notebook. Since it is a notebook environment, don't use asyncio.run. Instead, use await if you need to call an async function.#SYSTEM_MSG_END#\n# User Requirement\nRun data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.\n\n# Plan Status\n\n\n# Tool Info\n\n\n# Constraints\n- Take on Current Task if it is in Plan Status, otherwise, tackle User Requirement directly.\n- Ensure the output new code is executable in the same Jupyter notebook as the previous executed code.\n- Always prioritize using pre-defined tools for the same functionality.\n\n# Output\nWhile some concise thoughts are helpful, code is absolutely required. Always output one and only one code block in your response. Output code in the following format:\n```python\nyour code\n```\n#MSG_SEP#Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.": "To achieve the user requirement, we will follow these steps:\n\n1. Load the Wine recognition dataset from `sklearn.datasets`.\n2. Perform a basic data analysis to understand the dataset.\n3. Visualize the data to gain insights.\n4. Split the dataset into training and validation sets.\n5. Train a classification model using the training set.\n6. Evaluate the model on the validation set and report the accuracy.\n\nLet's start by loading the dataset and performing the initial data analysis:\n\n```python\nimport pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom sklearn.datasets import load_wine\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.ensemble import RandomForestClassifier\nfrom sklearn.metrics import accuracy_score\n\n# Load the wine dataset\nwine = load_wine()\nX = wine.data\ny = wine.target\nfeature_names = wine.feature_names\nclass_names = wine.target_names\n\n# Convert to a DataFrame for better visualization and analysis\nwine_df = pd.DataFrame(X, columns=feature_names)\nwine_df['target'] = y\n\n# Basic data analysis\nprint(wine_df.describe())\n\n# Plotting the data\n# Let's plot the distribution of alcohol content for each class\nfor class_idx, class_name in enumerate(class_names):\n plt.hist(wine_df[wine_df['target'] == class_idx]['alcohol'], label=class_name, alpha=0.5)\nplt.title('Alcohol content distribution by wine class')\nplt.xlabel('Alcohol content')\nplt.ylabel('Frequency')\nplt.legend()\nplt.show()\n\n# Split the dataset into training and validation sets (80% train, 20% validation)\nX_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42)\n\n# Train a RandomForestClassifier\nmodel = RandomForestClassifier(random_state=42)\nmodel.fit(X_train, y_train)\n\n# Predict on the validation set\ny_pred = model.predict(X_val)\n\n# Calculate the accuracy on the validation set\naccuracy = accuracy_score(y_val, y_pred)\nprint(f'Validation accuracy: {accuracy:.2f}')\n```\n\nThis code will load the dataset, perform a basic analysis, plot the distribution of alcohol content for each wine class, split the data, train a RandomForestClassifier, and finally, evaluate its performance on the validation set.", + "\n# User Requirement\nRun data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.\n# Context\n[user: Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy., assistant: import pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom sklearn.datasets import load_wine\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.ensemble import RandomForestClassifier\nfrom sklearn.metrics import accuracy_score\n\n# Load the wine dataset\nwine = load_wine()\nX = wine.data\ny = wine.target\nfeature_names = wine.feature_names\nclass_names = wine.target_names\n\n# Convert to a DataFrame for better visualization and analysis\nwine_df = pd.DataFrame(X, columns=feature_names)\nwine_df['target'] = y\n\n# Basic data analysis\nprint(wine_df.describe())\n\n# Plotting the data\n# Let's plot the distribution of alcohol content for each class\nfor class_idx, class_name in enumerate(class_names):\n plt.hist(wine_df[wine_df['target'] == class_idx]['alcohol'], label=class_name, alpha=0.5)\nplt.title('Alcohol content distribution by wine class')\nplt.xlabel('Alcohol content')\nplt.ylabel('Frequency')\nplt.legend()\nplt.show()\n\n# Split the dataset into training and validation sets (80% train, 20% validation)\nX_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42)\n\n# Train a RandomForestClassifier\nmodel = RandomForestClassifier(random_state=42)\nmodel.fit(X_train, y_train)\n\n# Predict on the validation set\ny_pred = model.predict(X_val)\n\n# Calculate the accuracy on the validation set\naccuracy = accuracy_score(y_val, y_pred)\nprint(f'Validation accuracy: {accuracy:.2f}')\n, user: a successful run]\n\nOutput a json following the format:\n```json\n{\n \"thoughts\": str = \"Thoughts on current situation, reflect on how you should proceed to fulfill the user requirement\",\n \"state\": bool = \"Decide whether you need to take more actions to complete the user requirement. Return true if you think so. Return false if you think the requirement has been completely fulfilled.\"\n}\n```\n": "```json\n{\n \"thoughts\": \"The user has requested a data analysis on the sklearn Wine recognition dataset, including a plot and training a model to predict wine class with validation accuracy. The provided code snippet appears to fulfill the user's requirements. It loads the dataset, performs basic data analysis, creates a plot of alcohol content distribution by wine class, splits the data into training and validation sets, trains a RandomForestClassifier, predicts on the validation set, and prints the validation accuracy. Assuming the code runs successfully without errors, the user's requirements should be met.\",\n \"state\": false\n}\n```", + "\n## User Requirement:\nconduct feature engineering, add new features on the dataset\n\n## Task\nRecommend up to 5 tools from 'Available Tools' that can help solve the 'User Requirement'. \n\n## Available Tools:\n{'PolynomialExpansion': 'Add polynomial and interaction features from selected numeric columns to input DataFrame.', 'scrape_web_playwright': 'Asynchronously Scrape and save the HTML structure and inner text content of a web page using Playwright. ', 'FillMissingValue': 'Completing missing values with simple strategies.'}\n\n## Tool Selection and Instructions:\n- Select tools most relevant to completing the 'User Requirement'.\n- If you believe that no tools are suitable, indicate with an empty list.\n- Only list the names of the tools, not the full schema of each tool.\n- Ensure selected tools are listed in 'Available Tools'.\n- Output a json list of tool names:\n```json\n[\"tool_name1\", \"tool_name2\", ...]\n```\n": "```json\n[\"PolynomialExpansion\", \"FillMissingValue\"]\n```" } \ No newline at end of file From e77ebcadc8d768d1245f78cbab65f1b5b8dbabc6 Mon Sep 17 00:00:00 2001 From: yzlin Date: Wed, 13 Mar 2024 23:14:31 +0800 Subject: [PATCH 073/131] keep mockllm aask_code updated with openai api --- tests/mock/mock_llm.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/mock/mock_llm.py b/tests/mock/mock_llm.py index 48154840a..b4cdfa0cf 100644 --- a/tests/mock/mock_llm.py +++ b/tests/mock/mock_llm.py @@ -5,8 +5,10 @@ from metagpt.config2 import config from metagpt.configs.llm_config import LLMType from metagpt.logs import logger from metagpt.provider.azure_openai_api import AzureOpenAILLM +from metagpt.provider.constant import GENERAL_FUNCTION_SCHEMA from metagpt.provider.openai_api import OpenAILLM from metagpt.schema import Message +from metagpt.utils.common import process_message OriginalLLM = OpenAILLM if config.llm.api_type == LLMType.OPENAI else AzureOpenAILLM @@ -70,6 +72,9 @@ class MockLLM(OriginalLLM): A copy of metagpt.provider.openai_api.OpenAILLM.aask_code, we can't use super().aask because it will be mocked. Since openai_api.OpenAILLM.aask_code is different from base_llm.BaseLLM.aask_code, we use the former. """ + if "tools" not in kwargs: + configs = {"tools": [{"type": "function", "function": GENERAL_FUNCTION_SCHEMA}]} + kwargs.update(configs) rsp = await self._achat_completion_function(messages, **kwargs) return self.get_choice_function_arguments(rsp) @@ -100,8 +105,7 @@ class MockLLM(OriginalLLM): return rsp async def aask_code(self, messages: Union[str, Message, list[dict]], **kwargs) -> dict: - messages = self._process_message(messages) - msg_key = json.dumps(messages, ensure_ascii=False) + msg_key = json.dumps(process_message(messages), ensure_ascii=False) rsp = await self._mock_rsp(msg_key, self.original_aask_code, messages, **kwargs) return rsp From b6eab0c638b4d93631091c161668bd9930bbf712 Mon Sep 17 00:00:00 2001 From: yzlin Date: Wed, 13 Mar 2024 23:39:46 +0800 Subject: [PATCH 074/131] fixed azure pricing plan --- metagpt/provider/azure_openai_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/provider/azure_openai_api.py b/metagpt/provider/azure_openai_api.py index 33331ae94..9aeeda00c 100644 --- a/metagpt/provider/azure_openai_api.py +++ b/metagpt/provider/azure_openai_api.py @@ -25,7 +25,7 @@ class AzureOpenAILLM(OpenAILLM): # https://learn.microsoft.com/zh-cn/azure/ai-services/openai/how-to/migration?tabs=python-new%2Cdalle-fix self.aclient = AsyncAzureOpenAI(**kwargs) self.model = self.config.model # Used in _calc_usage & _cons_kwargs - self.pricing_plan = self.config.pricing_plan + self.pricing_plan = self.config.pricing_plan or self.model def _make_client_kwargs(self) -> dict: kwargs = dict( From 83fb239bb745b83dd3c2d1d7ec13b5c1b24f82f5 Mon Sep 17 00:00:00 2001 From: yzlin Date: Thu, 14 Mar 2024 00:42:27 +0800 Subject: [PATCH 075/131] update example --- examples/di/machine_learning_complex.py | 16 ---------------- examples/di/machine_learning_with_tools.py | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 16 deletions(-) delete mode 100644 examples/di/machine_learning_complex.py create mode 100644 examples/di/machine_learning_with_tools.py diff --git a/examples/di/machine_learning_complex.py b/examples/di/machine_learning_complex.py deleted file mode 100644 index 42059ac4f..000000000 --- a/examples/di/machine_learning_complex.py +++ /dev/null @@ -1,16 +0,0 @@ -import asyncio - -from metagpt.roles.di.data_interpreter import DataInterpreter - - -async def main(requirement: str): - role = DataInterpreter(use_reflection=True, tools=[""]) - await role.run(requirement) - - -if __name__ == "__main__": - data_path = "your_path_to_icr/icr-identify-age-related-conditions" - train_path = f"{data_path}/your_train_data.csv" - eval_path = f"{data_path}/your_eval_data.csv" - requirement = f"This is a medical dataset with over fifty anonymized health characteristics linked to three age-related conditions. Your goal is to predict whether a subject has or has not been diagnosed with one of these conditions.The target column is Class. Perform data analysis, data preprocessing, feature engineering, and modeling to predict the target. Report f1 score on the eval data. Train data path: {train_path}, eval data path:{eval_path}." - asyncio.run(main(requirement)) diff --git a/examples/di/machine_learning_with_tools.py b/examples/di/machine_learning_with_tools.py new file mode 100644 index 000000000..291e734c8 --- /dev/null +++ b/examples/di/machine_learning_with_tools.py @@ -0,0 +1,16 @@ +import asyncio + +from metagpt.roles.di.data_interpreter import DataInterpreter + + +async def main(requirement: str): + role = DataInterpreter(use_reflection=True, tools=[""]) + await role.run(requirement) + + +if __name__ == "__main__": + data_path = "your/path/to/titanic" + train_path = f"{data_path}/split_train.csv" + eval_path = f"{data_path}/split_eval.csv" + requirement = f"This is a titanic passenger survival dataset, your goal is to predict passenger survival outcome. The target column is Survived. Perform data analysis, data preprocessing, feature engineering, and modeling to predict the target. Report accuracy on the eval data. Train data path: '{train_path}', eval data path: '{eval_path}'." + asyncio.run(main(requirement)) From 54aace8d116093b69d80f12a726e84bb10ccfa38 Mon Sep 17 00:00:00 2001 From: geekan Date: Thu, 14 Mar 2024 01:50:31 +0800 Subject: [PATCH 076/131] update real publish date --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4c1d75489..f0478bcee 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ # MetaGPT: The Multi-Agent Framework

## News -🚀 Mar. 01, 2024: Our Data Interpreter paper is on [arxiv](https://arxiv.org/abs/2402.18679). Check the [example](https://docs.deepwisdom.ai/main/en/DataInterpreter/) and [code](https://github.com/geekan/MetaGPT/tree/main/examples/di)! +🚀 Mar. 14, 2024: Our Data Interpreter paper is on [arxiv](https://arxiv.org/abs/2402.18679). Check the [example](https://docs.deepwisdom.ai/main/en/DataInterpreter/) and [code](https://github.com/geekan/MetaGPT/tree/main/examples/di)! 🚀 Feb. 08, 2024: [v0.7.0](https://github.com/geekan/MetaGPT/releases/tag/v0.7.0) released, supporting assigning different LLMs to different Roles. We also introduced [Data Interpreter](https://github.com/geekan/MetaGPT/blob/main/examples/di/README.md), a powerful agent capable of solving a wide range of real-world problems. From 8cf989d30406ff279638f6a81009c7f5e0378a67 Mon Sep 17 00:00:00 2001 From: Sirui Hong <34952977+stellaHSR@users.noreply.github.com> Date: Thu, 14 Mar 2024 02:15:04 +0800 Subject: [PATCH 077/131] Update README.md --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f0478bcee..81b6a876f 100644 --- a/README.md +++ b/README.md @@ -179,7 +179,7 @@ ### Contact Information ## Citation -For now, cite the [arXiv paper](https://arxiv.org/abs/2308.00352): +If you use MetaGPT or Data Interpreter in a research paper, please cite our work as follows: ```bibtex @misc{hong2023metagpt, @@ -190,4 +190,14 @@ ## Citation archivePrefix={arXiv}, primaryClass={cs.AI} } +@misc{hong2024data, + title={Data Interpreter: An LLM Agent For Data Science}, + author={Sirui Hong and Yizhang Lin and Bang Liu and Bangbang Liu and Binhao Wu and Danyang Li and Jiaqi Chen and Jiayi Zhang and Jinlin Wang and Li Zhang and Lingyao Zhang and Min Yang and Mingchen Zhuge and Taicheng Guo and Tuo Zhou and Wei Tao and Wenyi Wang and Xiangru Tang and Xiangtao Lu and Xiawu Zheng and Xinbing Liang and Yaying Fei and Yuheng Cheng and Zongze Xu and Chenglin Wu}, + year={2024}, + eprint={2402.18679}, + archivePrefix={arXiv}, + primaryClass={cs.AI} +} + ``` + From 08c0d4bf5ee0852d12883a54a7e538e94cecdcc2 Mon Sep 17 00:00:00 2001 From: Sirui Hong <34952977+stellaHSR@users.noreply.github.com> Date: Thu, 14 Mar 2024 02:19:05 +0800 Subject: [PATCH 078/131] Update README_CN.md --- docs/README_CN.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/README_CN.md b/docs/README_CN.md index 7a0db4974..8aea5e4cb 100644 --- a/docs/README_CN.md +++ b/docs/README_CN.md @@ -116,7 +116,7 @@ ### 联系信息 ## 引用 -引用 [arXiv paper](https://arxiv.org/abs/2308.00352): +如果您在研究论文中使用 MetaGPT 或 Data Interpreter,请引用我们的工作: ```bibtex @misc{hong2023metagpt, @@ -127,4 +127,12 @@ ## 引用 archivePrefix={arXiv}, primaryClass={cs.AI} } +@misc{hong2024data, + title={Data Interpreter: An LLM Agent For Data Science}, + author={Sirui Hong and Yizhang Lin and Bang Liu and Bangbang Liu and Binhao Wu and Danyang Li and Jiaqi Chen and Jiayi Zhang and Jinlin Wang and Li Zhang and Lingyao Zhang and Min Yang and Mingchen Zhuge and Taicheng Guo and Tuo Zhou and Wei Tao and Wenyi Wang and Xiangru Tang and Xiangtao Lu and Xiawu Zheng and Xinbing Liang and Yaying Fei and Yuheng Cheng and Zongze Xu and Chenglin Wu}, + year={2024}, + eprint={2402.18679}, + archivePrefix={arXiv}, + primaryClass={cs.AI} +} ``` From 72179a0bd455baaec5b49a9621e3665d0f1e50a8 Mon Sep 17 00:00:00 2001 From: Sirui Hong <34952977+stellaHSR@users.noreply.github.com> Date: Thu, 14 Mar 2024 02:21:00 +0800 Subject: [PATCH 079/131] Update README_JA.md --- docs/README_JA.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/README_JA.md b/docs/README_JA.md index c6b99461c..91155532b 100644 --- a/docs/README_JA.md +++ b/docs/README_JA.md @@ -295,7 +295,7 @@ ## クイックスタート ## 引用 -現時点では、[arXiv 論文](https://arxiv.org/abs/2308.00352)を引用してください: +研究論文でMetaGPTやData Interpreterを使用する場合は、以下のように当社の作業を引用してください: ```bibtex @misc{hong2023metagpt, @@ -306,6 +306,14 @@ ## 引用 archivePrefix={arXiv}, primaryClass={cs.AI} } +@misc{hong2024data, + title={Data Interpreter: An LLM Agent For Data Science}, + author={Sirui Hong and Yizhang Lin and Bang Liu and Bangbang Liu and Binhao Wu and Danyang Li and Jiaqi Chen and Jiayi Zhang and Jinlin Wang and Li Zhang and Lingyao Zhang and Min Yang and Mingchen Zhuge and Taicheng Guo and Tuo Zhou and Wei Tao and Wenyi Wang and Xiangru Tang and Xiangtao Lu and Xiawu Zheng and Xinbing Liang and Yaying Fei and Yuheng Cheng and Zongze Xu and Chenglin Wu}, + year={2024}, + eprint={2402.18679}, + archivePrefix={arXiv}, + primaryClass={cs.AI} +} ``` ## お問い合わせ先 From b2cfea2c74736e93bea894918a3acc94ac28cc3e Mon Sep 17 00:00:00 2001 From: liujun Date: Thu, 14 Mar 2024 08:32:18 +0800 Subject: [PATCH 080/131] fix:repair custom llm config --- metagpt/config2.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/metagpt/config2.py b/metagpt/config2.py index 4b3d82593..c603f2938 100644 --- a/metagpt/config2.py +++ b/metagpt/config2.py @@ -99,20 +99,21 @@ class Config(CLIParams, YamlModel): dicts += [Config.read_yaml(path) for path in default_config_paths] final = merge_dict(dicts) return Config(**final) - + @classmethod - def config(cls,config:dict): + def from_llm_config(cls, llm_config: dict): """user config llm example: - llm_config = {"llm": {"api_type": "xxx", "api_key": "xxx", "model": "xxx"} - gpt4 = Config.config(llm_config) + llm_config = {"api_type": "xxx", "api_key": "xxx", "model": "xxx"} + gpt4 = Config.from_llm_config(llm_config) A = Role(name="A", profile="Democratic candidate", goal="Win the election", actions=[a1], watch=[a2], config=gpt4) """ + llm_config = LLMConfig.model_validate(llm_config) dicts = [dict(os.environ)] - dicts += [config] + dicts += [{"llm": llm_config}] final = merge_dict(dicts) return Config(**final) - + def update_via_cli(self, project_path, project_name, inc, reqa_file, max_auto_summarize_code): """update config via cli""" From a26c849b5087184cb1902f35ae74d3f5e0e280ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Thu, 14 Mar 2024 10:26:12 +0800 Subject: [PATCH 081/131] restore WalmartSalesForecast example. --- examples/di/machine_learning.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/examples/di/machine_learning.py b/examples/di/machine_learning.py index a58735831..c674e66e8 100644 --- a/examples/di/machine_learning.py +++ b/examples/di/machine_learning.py @@ -2,11 +2,21 @@ import fire from metagpt.roles.di.data_interpreter import DataInterpreter +WINE_REQ = "Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy." -async def main(auto_run: bool = True): - requirement = "Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy." - di = DataInterpreter(auto_run=auto_run) - await di.run(requirement) +DATA_DIR = "path/to/your/data" +# sales_forecast data from https://www.kaggle.com/datasets/aslanahmedov/walmart-sales-forecast/data +SALES_FORECAST_REQ = f"""Train a model to predict sales for each department in every store (split the last 40 weeks records as validation dataset, the others is train dataset), include plot total sales trends, print metric and plot scatter plots of +groud truth and predictions on validation data. Dataset is {DATA_DIR}/train.csv, the metric is weighted mean absolute error (WMAE) for test data. Notice: *print* key variables to get more information for next task step. +""" + +REQUIREMENTS = {"wine": WINE_REQ, "sales_forecast": SALES_FORECAST_REQ} + + +async def main(use_case: str = "wine"): + mi = DataInterpreter() + requirement = REQUIREMENTS[use_case] + await mi.run(requirement) if __name__ == "__main__": From 1306971d2dddaf254f3f8ae7baa4531dffc9328a Mon Sep 17 00:00:00 2001 From: better629 Date: Thu, 14 Mar 2024 21:34:23 +0800 Subject: [PATCH 082/131] add yi-34b --- metagpt/configs/llm_config.py | 1 + metagpt/provider/openai_api.py | 2 +- metagpt/utils/token_counter.py | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/metagpt/configs/llm_config.py b/metagpt/configs/llm_config.py index 77bfc8466..fa9bc0b1b 100644 --- a/metagpt/configs/llm_config.py +++ b/metagpt/configs/llm_config.py @@ -29,6 +29,7 @@ class LLMType(Enum): DASHSCOPE = "dashscope" # Aliyun LingJi DashScope MOONSHOT = "moonshot" MISTRAL = "mistral" + YI = "yi" # lingyiwanwu def __missing__(self, key): return self.OPENAI diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py index 8f3b71c42..3a53a4548 100644 --- a/metagpt/provider/openai_api.py +++ b/metagpt/provider/openai_api.py @@ -44,7 +44,7 @@ from metagpt.utils.token_counter import ( ) -@register_provider([LLMType.OPENAI, LLMType.FIREWORKS, LLMType.OPEN_LLM, LLMType.MOONSHOT, LLMType.MISTRAL]) +@register_provider([LLMType.OPENAI, LLMType.FIREWORKS, LLMType.OPEN_LLM, LLMType.MOONSHOT, LLMType.MISTRAL, LLMType.YI]) class OpenAILLM(BaseLLM): """Check https://platform.openai.com/examples for examples""" diff --git a/metagpt/utils/token_counter.py b/metagpt/utils/token_counter.py index f7c53706b..cf5f94ba5 100644 --- a/metagpt/utils/token_counter.py +++ b/metagpt/utils/token_counter.py @@ -48,6 +48,8 @@ TOKEN_COSTS = { "claude-2.1": {"prompt": 0.008, "completion": 0.024}, "claude-3-sonnet-20240229": {"prompt": 0.003, "completion": 0.015}, "claude-3-opus-20240229": {"prompt": 0.015, "completion": 0.075}, + "yi-34b-chat-0205": {"prompt": 0.0003, "completion": 0.0003}, + "yi-34b-chat-200k": {"prompt": 0.0017, "completion": 0.0017}, } @@ -176,6 +178,8 @@ TOKEN_MAX = { "claude-2.1": 200000, "claude-3-sonnet-20240229": 200000, "claude-3-opus-20240229": 200000, + "yi-34b-chat-0205": 4000, + "yi-34b-chat-200k": 200000, } From 8a8e29902f214449b6e0bb54bf010dd41db6be3f Mon Sep 17 00:00:00 2001 From: better629 Date: Thu, 14 Mar 2024 22:33:36 +0800 Subject: [PATCH 083/131] fix anthropic_api _update_costs problem --- metagpt/provider/base_llm.py | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/metagpt/provider/base_llm.py b/metagpt/provider/base_llm.py index fa14496aa..70de4e10d 100644 --- a/metagpt/provider/base_llm.py +++ b/metagpt/provider/base_llm.py @@ -10,10 +10,9 @@ from __future__ import annotations import json from abc import ABC, abstractmethod -from typing import Dict, Optional, Union +from typing import Optional, Union from openai import AsyncOpenAI -from openai.types import CompletionUsage from pydantic import BaseModel from tenacity import ( after_log, @@ -28,7 +27,6 @@ from metagpt.logs import logger from metagpt.schema import Message from metagpt.utils.common import log_and_reraise from metagpt.utils.cost_manager import CostManager, Costs -from metagpt.utils.exceptions import handle_exception class BaseLLM(ABC): @@ -88,6 +86,7 @@ class BaseLLM(ABC): local_calc_usage (bool): some models don't calculate usage, it will overwrite LLMConfig.calc_usage """ calc_usage = self.config.calc_usage and local_calc_usage + model = model or self.pricing_plan model = model or self.model usage = usage.model_dump() if isinstance(usage, BaseModel) else usage if calc_usage and self.cost_manager: @@ -225,20 +224,6 @@ class BaseLLM(ABC): """ return json.loads(self.get_choice_function(rsp)["arguments"], strict=False) - @handle_exception - def _update_costs(self, usage: CompletionUsage | Dict): - """ - Updates the costs based on the provided usage information. - """ - if self.config.calc_usage and usage and self.cost_manager: - if isinstance(usage, Dict): - prompt_tokens = int(usage.get("prompt_tokens", 0)) - completion_tokens = int(usage.get("completion_tokens", 0)) - else: - prompt_tokens = usage.prompt_tokens - completion_tokens = usage.completion_tokens - self.cost_manager.update_cost(prompt_tokens, completion_tokens, self.pricing_plan) - def messages_to_prompt(self, messages: list[dict]): """[{"role": "user", "content": msg}] to user: etc.""" return "\n".join([f"{i['role']}: {i['content']}" for i in messages]) From ba1866f7caf5b39b5a4ede1b3a139f28236675ea Mon Sep 17 00:00:00 2001 From: better629 Date: Fri, 15 Mar 2024 15:19:18 +0800 Subject: [PATCH 084/131] Update openai_api.py --- metagpt/provider/openai_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py index 3a53a4548..b4f99e69f 100644 --- a/metagpt/provider/openai_api.py +++ b/metagpt/provider/openai_api.py @@ -142,7 +142,7 @@ class OpenAILLM(BaseLLM): async def acompletion_text(self, messages: list[dict], stream=False, timeout=3) -> str: """when streaming, print each token in place.""" if stream: - await self._achat_completion_stream(messages, timeout=timeout) + return await self._achat_completion_stream(messages, timeout=timeout) rsp = await self._achat_completion(messages, timeout=timeout) return self.get_choice_text(rsp) From 28c7ffba5fc707641b366cb7b1ea15dbe1f61535 Mon Sep 17 00:00:00 2001 From: frf Date: Sun, 17 Mar 2024 20:41:31 +0800 Subject: [PATCH 085/131] update gpt-3.5-turbo-0125 in count_message_tokens --- metagpt/utils/token_counter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/metagpt/utils/token_counter.py b/metagpt/utils/token_counter.py index 5e163960d..a397f7353 100644 --- a/metagpt/utils/token_counter.py +++ b/metagpt/utils/token_counter.py @@ -196,6 +196,7 @@ def count_message_tokens(messages, model="gpt-3.5-turbo-0125"): "gpt-35-turbo-16k", "gpt-3.5-turbo-16k", "gpt-3.5-turbo-1106", + "gpt-3.5-turbo-0125", "gpt-4-0314", "gpt-4-32k-0314", "gpt-4-0613", From c9f6a52752c12adc8e6c4666abac8036c1b5d9f2 Mon Sep 17 00:00:00 2001 From: Ruifeng Fu Date: Sun, 17 Mar 2024 21:53:10 +0800 Subject: [PATCH 086/131] update gpt-3.5-turbo-0125 in TOKEN_COSTS --- metagpt/utils/token_counter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/metagpt/utils/token_counter.py b/metagpt/utils/token_counter.py index a397f7353..d639ccf32 100644 --- a/metagpt/utils/token_counter.py +++ b/metagpt/utils/token_counter.py @@ -21,6 +21,7 @@ TOKEN_COSTS = { "gpt-35-turbo": {"prompt": 0.0015, "completion": 0.002}, "gpt-35-turbo-16k": {"prompt": 0.003, "completion": 0.004}, "gpt-3.5-turbo-1106": {"prompt": 0.001, "completion": 0.002}, + "gpt-3.5-turbo-0125": {"prompt": 0.001, "completion": 0.002}, "gpt-4-0314": {"prompt": 0.03, "completion": 0.06}, "gpt-4": {"prompt": 0.03, "completion": 0.06}, "gpt-4-32k": {"prompt": 0.06, "completion": 0.12}, From e40fc66f9830f8f4341feeafa00eb227a940b20a Mon Sep 17 00:00:00 2001 From: geekan Date: Mon, 18 Mar 2024 11:39:30 +0800 Subject: [PATCH 087/131] add with_model to BaseLLM --- metagpt/provider/base_llm.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/metagpt/provider/base_llm.py b/metagpt/provider/base_llm.py index 70de4e10d..71308930a 100644 --- a/metagpt/provider/base_llm.py +++ b/metagpt/provider/base_llm.py @@ -39,7 +39,7 @@ class BaseLLM(ABC): # OpenAI / Azure / Others aclient: Optional[Union[AsyncOpenAI]] = None cost_manager: Optional[CostManager] = None - model: Optional[str] = None + model: Optional[str] = None # deprecated pricing_plan: Optional[str] = None @abstractmethod @@ -231,3 +231,8 @@ class BaseLLM(ABC): def messages_to_dict(self, messages): """objects to [{"role": "user", "content": msg}] etc.""" return [i.to_dict() for i in messages] + + def with_model(self, model: str): + """Set model and return self. For example, `with_model("gpt-3.5-turbo")`.""" + self.config.model = model + return self From d326a9b96138afa6731092d7ea57cbead94c72ba Mon Sep 17 00:00:00 2001 From: guoguangwu Date: Mon, 18 Mar 2024 13:37:17 +0800 Subject: [PATCH 088/131] fix: typos Signed-off-by: guoguangwu --- metagpt/environment/base_env.py | 2 +- metagpt/environment/mincraft_env/mincraft_env.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/metagpt/environment/base_env.py b/metagpt/environment/base_env.py index 0e583ffb3..144e9df00 100644 --- a/metagpt/environment/base_env.py +++ b/metagpt/environment/base_env.py @@ -47,7 +47,7 @@ def mark_as_writeable(func): class ExtEnv(BaseModel): - """External Env to intergate actual game environment""" + """External Env to integrate actual game environment""" def _check_api_exist(self, rw_api: Optional[str] = None): if not rw_api: diff --git a/metagpt/environment/mincraft_env/mincraft_env.py b/metagpt/environment/mincraft_env/mincraft_env.py index 6aaa34187..fdc477164 100644 --- a/metagpt/environment/mincraft_env/mincraft_env.py +++ b/metagpt/environment/mincraft_env/mincraft_env.py @@ -20,7 +20,7 @@ from metagpt.utils.common import load_mc_skills_code, read_json_file, write_json class MincraftEnv(Environment, MincraftExtEnv): - """MincraftEnv, including shared memory of cache and infomation between roles""" + """MincraftEnv, including shared memory of cache and information between roles""" model_config = ConfigDict(arbitrary_types_allowed=True) From 9a51038bd16ae2deb17ddb0658215a5be811b9b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Tue, 19 Mar 2024 11:46:30 +0800 Subject: [PATCH 089/131] fixbug: #1031 --- metagpt/environment/base_env.py | 2 +- metagpt/roles/role.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/metagpt/environment/base_env.py b/metagpt/environment/base_env.py index 0e583ffb3..9829c5b3a 100644 --- a/metagpt/environment/base_env.py +++ b/metagpt/environment/base_env.py @@ -129,8 +129,8 @@ class Environment(ExtEnv): self.roles[role.profile] = role for role in roles: # setup system message with roles - role.set_env(self) role.context = self.context + role.set_env(self) def publish_message(self, message: Message, peekable: bool = True) -> bool: """ diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index 19ed048e3..e0f8a7ea6 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -169,6 +169,7 @@ class Role(SerializationMixin, ContextMixin, BaseModel): self._check_actions() self.llm.system_prompt = self._get_prefix() + self.llm.cost_manager = self.context.cost_manager self._watch(kwargs.pop("watch", [UserRequirement])) if self.latest_observed_msg: @@ -331,6 +332,7 @@ class Role(SerializationMixin, ContextMixin, BaseModel): if env: env.set_addresses(self, self.addresses) self.llm.system_prompt = self._get_prefix() + self.llm.cost_manager = self.context.cost_manager self.set_actions(self.actions) # reset actions to update llm and prefix def _get_prefix(self): From 4a72f3980b98e01693e0c4b93522faea77c72839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Tue, 19 Mar 2024 15:08:44 +0800 Subject: [PATCH 090/131] fixbug: #1018 --- SECURITY.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 623e6e556..924ce5015 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,9 +4,9 @@ ## Supported Versions | Version | Supported | |---------|--------------------| - | 7.x | :x: | - | 6.x | :x: | -| < 6.x | :x: | + | 0.7.x | :x: | + | 0.6.x | :x: | +| < 0.6.x | :x: | ## Reporting a Vulnerability From 356ff8926e6d67770f6ff58f2eddf282025ed0e3 Mon Sep 17 00:00:00 2001 From: jinchihe Date: Tue, 19 Mar 2024 17:34:51 +0800 Subject: [PATCH 091/131] Correct bad URL in token_counter --- metagpt/utils/token_counter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/utils/token_counter.py b/metagpt/utils/token_counter.py index d639ccf32..0ba2daa89 100644 --- a/metagpt/utils/token_counter.py +++ b/metagpt/utils/token_counter.py @@ -229,7 +229,7 @@ def count_message_tokens(messages, model="gpt-3.5-turbo-0125"): else: raise NotImplementedError( f"num_tokens_from_messages() is not implemented for model {model}. " - f"See https://github.com/openai/openai-python/blob/main/chatml.md " + f"See https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken " f"for information on how messages are converted to tokens." ) num_tokens = 0 From 51ba2b393b18c6e223a76430147a1eab293f87ea Mon Sep 17 00:00:00 2001 From: yzlin Date: Tue, 19 Mar 2024 17:53:26 +0800 Subject: [PATCH 092/131] fix critical bug: human prior not injected --- metagpt/strategy/planner.py | 5 ++- metagpt/strategy/task_type.py | 7 ++++ tests/data/rsp_cache.json | 9 ++++- .../metagpt/roles/di/test_data_interpreter.py | 1 - tests/metagpt/strategy/test_planner.py | 37 +++++++++++++++++++ 5 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 tests/metagpt/strategy/test_planner.py diff --git a/metagpt/strategy/planner.py b/metagpt/strategy/planner.py index 0fc9cf77f..fbf784837 100644 --- a/metagpt/strategy/planner.py +++ b/metagpt/strategy/planner.py @@ -164,8 +164,9 @@ class Planner(BaseModel): code_written = "\n\n".join(code_written) task_results = [task.result for task in finished_tasks] task_results = "\n\n".join(task_results) - task_type_name = self.current_task.task_type.upper() - guidance = TaskType[task_type_name].value.guidance if hasattr(TaskType, task_type_name) else "" + task_type_name = self.current_task.task_type + task_type = TaskType.get_type(task_type_name) + guidance = task_type.guidance if task_type else "" # combine components in a prompt prompt = PLAN_STATUS.format( diff --git a/metagpt/strategy/task_type.py b/metagpt/strategy/task_type.py index 7c88817cc..d21705c16 100644 --- a/metagpt/strategy/task_type.py +++ b/metagpt/strategy/task_type.py @@ -71,3 +71,10 @@ class TaskType(Enum): @property def type_name(self): return self.value.name + + @classmethod + def get_type(cls, type_name): + for member in cls: + if member.type_name == type_name: + return member.value + return None diff --git a/tests/data/rsp_cache.json b/tests/data/rsp_cache.json index 8f0567c56..565241779 100644 --- a/tests/data/rsp_cache.json +++ b/tests/data/rsp_cache.json @@ -420,5 +420,12 @@ "user: \n## User Requirement\nRun data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.\n## Context\n\n## Current Plan\n[\n {\n \"task_id\": \"1\",\n \"dependent_task_ids\": [],\n \"instruction\": \"Perform exploratory data analysis on the sklearn Wine recognition dataset including summary statistics and a plot.\",\n \"task_type\": \"eda\",\n \"code\": \"import numpy as np\\nimport pandas as pd\\nimport matplotlib.pyplot as plt\\nfrom sklearn.datasets import load_wine\\n\\n# Load the wine dataset\\nwine = load_wine()\\nwine_df = pd.DataFrame(data=np.c_[wine['data'], wine['target']],\\n columns=wine['feature_names'] + ['target'])\\n\\n# Summary statistics for numerical features\\nnumerical_summary = wine_df.describe()\\n\\n# Summary statistics for categorical features\\ncategorical_summary = wine_df.select_dtypes(include=['object', 'category']).describe()\\n\\n# Correlation matrix for numerical features\\ncorrelation_matrix = wine_df.corr()\\n\\n# Plotting a histogram for each numerical feature\\nwine_df.hist(bins=15, figsize=(15, 10), layout=(4, 4))\\nplt.tight_layout()\\nplt.show()\\n\\n# Displaying the summary statistics\\nprint(\\\"Numerical Summary:\\\\n\\\", numerical_summary)\\nprint(\\\"\\\\nCategorical Summary:\\\\n\\\", categorical_summary)\\nprint(\\\"\\\\nCorrelation Matrix:\\\\n\\\", correlation_matrix)\\n\",\n \"result\": \"a successful run\",\n \"is_success\": true,\n \"is_finished\": true\n },\n {\n \"task_id\": \"2\",\n \"dependent_task_ids\": [\n \"1\"\n ],\n \"instruction\": \"Preprocess the dataset by splitting it into training and validation sets with a 80-20 split.\",\n \"task_type\": \"data preprocessing\",\n \"code\": \"from sklearn.model_selection import train_test_split\\n\\n# Split the data into training and validation sets\\nX_train, X_val, y_train, y_val = train_test_split(\\n wine.data, wine.target, test_size=0.2, random_state=42\\n)\\n\",\n \"result\": \"a successful run\",\n \"is_success\": true,\n \"is_finished\": true\n },\n {\n \"task_id\": \"3\",\n \"dependent_task_ids\": [\n \"2\"\n ],\n \"instruction\": \"Train a model using the training set to predict wine class.\",\n \"task_type\": \"model train\",\n \"code\": \"from sklearn.ensemble import RandomForestClassifier\\nfrom sklearn.metrics import accuracy_score\\n\\n# Initialize the model\\nmodel = RandomForestClassifier(random_state=42)\\n\\n# Train the model\\nmodel.fit(X_train, y_train)\\n\\n# Predict the validation set\\ny_pred = model.predict(X_val)\\n\\n# Calculate the accuracy\\naccuracy = accuracy_score(y_val, y_pred)\\nprint(f\\\"Validation Accuracy: {accuracy:.2f}\\\")\\n\",\n \"result\": \"a successful run\",\n \"is_success\": true,\n \"is_finished\": true\n },\n {\n \"task_id\": \"4\",\n \"dependent_task_ids\": [\n \"3\"\n ],\n \"instruction\": \"Evaluate the model on the validation set and show the validation accuracy.\",\n \"task_type\": \"model evaluate\",\n \"code\": \"\",\n \"result\": \"\",\n \"is_success\": false,\n \"is_finished\": false\n }\n]\n## Current Task\n{\"task_id\":\"4\",\"dependent_task_ids\":[\"3\"],\"instruction\":\"Evaluate the model on the validation set and show the validation accuracy.\",\"task_type\":\"model evaluate\",\"code\":\"\",\"result\":\"\",\"is_success\":false,\"is_finished\":false}\n\nassistant: # Since the model has already been trained and predictions have been made on the validation set,\n# we only need to evaluate the model using the predictions and the true labels from the validation set.\n\n# The accuracy has already been calculated and printed in the previous task.\n# However, to follow the task guidance and to ensure the output is only related to the current task,\n# we will calculate and print the validation accuracy again.\n\nvalidation_accuracy = accuracy_score(y_val, y_pred)\nprint(f\"Validation Accuracy: {validation_accuracy:.2f}\")\n\nuser: a successful runThis is a review. Please review output from metagpt.actions.di.execute_nb_code.ExecuteNbCode\nIf you want to change, add, delete a task or merge tasks in the plan, say 'change task task_id or current task, ... (things to change)' If you confirm the output from the current task and wish to continue, type: confirmIf you think user requirement has been fulfilled completedly, you can finish the process by typing: finish\nIf you want to terminate the process, type: exit\nPlease type your review below:\n": "confirm", "As a data scientist, you need to help user to achieve their goal step by step in a continuous Jupyter notebook. Since it is a notebook environment, don't use asyncio.run. Instead, use await if you need to call an async function.#SYSTEM_MSG_END#\n# User Requirement\nRun data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.\n\n# Plan Status\n\n\n# Tool Info\n\n\n# Constraints\n- Take on Current Task if it is in Plan Status, otherwise, tackle User Requirement directly.\n- Ensure the output new code is executable in the same Jupyter notebook as the previous executed code.\n- Always prioritize using pre-defined tools for the same functionality.\n\n# Output\nWhile some concise thoughts are helpful, code is absolutely required. Always output one and only one code block in your response. Output code in the following format:\n```python\nyour code\n```\n#MSG_SEP#Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.": "To achieve the user requirement, we will follow these steps:\n\n1. Load the Wine recognition dataset from `sklearn.datasets`.\n2. Perform a basic data analysis to understand the dataset.\n3. Visualize the data to gain insights.\n4. Split the dataset into training and validation sets.\n5. Train a classification model using the training set.\n6. Evaluate the model on the validation set and report the accuracy.\n\nLet's start by loading the dataset and performing the initial data analysis:\n\n```python\nimport pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom sklearn.datasets import load_wine\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.ensemble import RandomForestClassifier\nfrom sklearn.metrics import accuracy_score\n\n# Load the wine dataset\nwine = load_wine()\nX = wine.data\ny = wine.target\nfeature_names = wine.feature_names\nclass_names = wine.target_names\n\n# Convert to a DataFrame for better visualization and analysis\nwine_df = pd.DataFrame(X, columns=feature_names)\nwine_df['target'] = y\n\n# Basic data analysis\nprint(wine_df.describe())\n\n# Plotting the data\n# Let's plot the distribution of alcohol content for each class\nfor class_idx, class_name in enumerate(class_names):\n plt.hist(wine_df[wine_df['target'] == class_idx]['alcohol'], label=class_name, alpha=0.5)\nplt.title('Alcohol content distribution by wine class')\nplt.xlabel('Alcohol content')\nplt.ylabel('Frequency')\nplt.legend()\nplt.show()\n\n# Split the dataset into training and validation sets (80% train, 20% validation)\nX_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42)\n\n# Train a RandomForestClassifier\nmodel = RandomForestClassifier(random_state=42)\nmodel.fit(X_train, y_train)\n\n# Predict on the validation set\ny_pred = model.predict(X_val)\n\n# Calculate the accuracy on the validation set\naccuracy = accuracy_score(y_val, y_pred)\nprint(f'Validation accuracy: {accuracy:.2f}')\n```\n\nThis code will load the dataset, perform a basic analysis, plot the distribution of alcohol content for each wine class, split the data, train a RandomForestClassifier, and finally, evaluate its performance on the validation set.", "\n# User Requirement\nRun data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.\n# Context\n[user: Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy., assistant: import pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom sklearn.datasets import load_wine\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.ensemble import RandomForestClassifier\nfrom sklearn.metrics import accuracy_score\n\n# Load the wine dataset\nwine = load_wine()\nX = wine.data\ny = wine.target\nfeature_names = wine.feature_names\nclass_names = wine.target_names\n\n# Convert to a DataFrame for better visualization and analysis\nwine_df = pd.DataFrame(X, columns=feature_names)\nwine_df['target'] = y\n\n# Basic data analysis\nprint(wine_df.describe())\n\n# Plotting the data\n# Let's plot the distribution of alcohol content for each class\nfor class_idx, class_name in enumerate(class_names):\n plt.hist(wine_df[wine_df['target'] == class_idx]['alcohol'], label=class_name, alpha=0.5)\nplt.title('Alcohol content distribution by wine class')\nplt.xlabel('Alcohol content')\nplt.ylabel('Frequency')\nplt.legend()\nplt.show()\n\n# Split the dataset into training and validation sets (80% train, 20% validation)\nX_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42)\n\n# Train a RandomForestClassifier\nmodel = RandomForestClassifier(random_state=42)\nmodel.fit(X_train, y_train)\n\n# Predict on the validation set\ny_pred = model.predict(X_val)\n\n# Calculate the accuracy on the validation set\naccuracy = accuracy_score(y_val, y_pred)\nprint(f'Validation accuracy: {accuracy:.2f}')\n, user: a successful run]\n\nOutput a json following the format:\n```json\n{\n \"thoughts\": str = \"Thoughts on current situation, reflect on how you should proceed to fulfill the user requirement\",\n \"state\": bool = \"Decide whether you need to take more actions to complete the user requirement. Return true if you think so. Return false if you think the requirement has been completely fulfilled.\"\n}\n```\n": "```json\n{\n \"thoughts\": \"The user has requested a data analysis on the sklearn Wine recognition dataset, including a plot and training a model to predict wine class with validation accuracy. The provided code snippet appears to fulfill the user's requirements. It loads the dataset, performs basic data analysis, creates a plot of alcohol content distribution by wine class, splits the data into training and validation sets, trains a RandomForestClassifier, predicts on the validation set, and prints the validation accuracy. Assuming the code runs successfully without errors, the user's requirements should be met.\",\n \"state\": false\n}\n```", - "\n## User Requirement:\nconduct feature engineering, add new features on the dataset\n\n## Task\nRecommend up to 5 tools from 'Available Tools' that can help solve the 'User Requirement'. \n\n## Available Tools:\n{'PolynomialExpansion': 'Add polynomial and interaction features from selected numeric columns to input DataFrame.', 'scrape_web_playwright': 'Asynchronously Scrape and save the HTML structure and inner text content of a web page using Playwright. ', 'FillMissingValue': 'Completing missing values with simple strategies.'}\n\n## Tool Selection and Instructions:\n- Select tools most relevant to completing the 'User Requirement'.\n- If you believe that no tools are suitable, indicate with an empty list.\n- Only list the names of the tools, not the full schema of each tool.\n- Ensure selected tools are listed in 'Available Tools'.\n- Output a json list of tool names:\n```json\n[\"tool_name1\", \"tool_name2\", ...]\n```\n": "```json\n[\"PolynomialExpansion\", \"FillMissingValue\"]\n```" + "\n## User Requirement:\nconduct feature engineering, add new features on the dataset\n\n## Task\nRecommend up to 5 tools from 'Available Tools' that can help solve the 'User Requirement'. \n\n## Available Tools:\n{'PolynomialExpansion': 'Add polynomial and interaction features from selected numeric columns to input DataFrame.', 'scrape_web_playwright': 'Asynchronously Scrape and save the HTML structure and inner text content of a web page using Playwright. ', 'FillMissingValue': 'Completing missing values with simple strategies.'}\n\n## Tool Selection and Instructions:\n- Select tools most relevant to completing the 'User Requirement'.\n- If you believe that no tools are suitable, indicate with an empty list.\n- Only list the names of the tools, not the full schema of each tool.\n- Ensure selected tools are listed in 'Available Tools'.\n- Output a json list of tool names:\n```json\n[\"tool_name1\", \"tool_name2\", ...]\n```\n": "```json\n[\"PolynomialExpansion\", \"FillMissingValue\"]\n```", + "As a data scientist, you need to help user to achieve their goal step by step in a continuous Jupyter notebook. Since it is a notebook environment, don't use asyncio.run. Instead, use await if you need to call an async function.#SYSTEM_MSG_END#\n# User Requirement\nRun data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.\n\n# Plan Status\n\n## Finished Tasks\n### code\n```python\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom sklearn.datasets import load_wine\nwine = load_wine()\nwine_df = pd.DataFrame(data=np.c_[wine['data'], wine['target']],\n columns=wine['feature_names'] + ['target'])\nnumerical_summary = wine_df.describe()\ncategorical_summary = wine_df.select_dtypes(include=['object', 'category']).describe()\ncorrelation_matrix = wine_df.corr()\nwine_df.hist(bins=15, figsize=(15, 10), layout=(4, 4))\nplt.tight_layout()\nplt.show()\nprint(\"Numerical Summary:\\n\", numerical_summary)\nprint(\"\\nCategorical Summary:\\n\", categorical_summary)\nprint(\"\\nCorrelation Matrix:\\n\", correlation_matrix)\n```\n\n### execution result\na successful run\n\n## Current Task\nPreprocess the dataset by splitting it into training and validation sets with a 80-20 split.\n\n## Task Guidance\nWrite complete code for 'Current Task'. And avoid duplicating code from 'Finished Tasks', such as repeated import of packages, reading data, etc.\nSpecifically, \nThe current task is about data preprocessing, please note the following:\n- Monitor data types per column, applying appropriate methods.\n- Ensure operations are on existing dataset columns.\n- Avoid writing processed data to files.\n- Avoid any change to label column, such as standardization, etc.\n- Prefer alternatives to one-hot encoding for categorical data.\n- Only encode or scale necessary columns to allow for potential feature-specific engineering tasks (like time_extract, binning, extraction, etc.) later.\n- Each step do data preprocessing to train, must do same for test separately at the same time.\n- Always copy the DataFrame before processing it and use the copy to process.\n\n\n\n# Tool Info\n\n\n# Constraints\n- Take on Current Task if it is in Plan Status, otherwise, tackle User Requirement directly.\n- Ensure the output new code is executable in the same Jupyter notebook as the previous executed code.\n- Always prioritize using pre-defined tools for the same functionality.\n\n# Output\nWhile some concise thoughts are helpful, code is absolutely required. Always output one and only one code block in your response. Output code in the following format:\n```python\nyour code\n```\n#MSG_SEP#\n# Latest Data Info\nLatest data info after previous tasks:\na successful run\n": "```python\nfrom sklearn.model_selection import train_test_split\n\n# Split the data into training and validation sets (80-20 split)\nX_train, X_val, y_train, y_val = train_test_split(\n wine_df.drop('target', axis=1), # features\n wine_df['target'], # target variable\n test_size=0.2, # 20% for validation\n random_state=42 # seed for reproducibility\n)\n\n# Output the shapes of the resulting data splits\nprint(f\"Training set shape: {X_train.shape}\")\nprint(f\"Validation set shape: {X_val.shape}\")\nprint(f\"Training target shape: {y_train.shape}\")\nprint(f\"Validation target shape: {y_val.shape}\")\n```", + "\n# Background\nCheck latest data info to guide subsequent tasks.\n\n## Finished Tasks\n```python\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom sklearn.datasets import load_wine\nwine = load_wine()\nwine_df = pd.DataFrame(data=np.c_[wine['data'], wine['target']],\n columns=wine['feature_names'] + ['target'])\nnumerical_summary = wine_df.describe()\ncategorical_summary = wine_df.select_dtypes(include=['object', 'category']).describe()\ncorrelation_matrix = wine_df.corr()\nwine_df.hist(bins=15, figsize=(15, 10), layout=(4, 4))\nplt.tight_layout()\nplt.show()\nprint(\"Numerical Summary:\\n\", numerical_summary)\nprint(\"\\nCategorical Summary:\\n\", categorical_summary)\nprint(\"\\nCorrelation Matrix:\\n\", correlation_matrix)\n\nfrom sklearn.model_selection import train_test_split\nX_train, X_val, y_train, y_val = train_test_split(\n wine_df.drop('target', axis=1),\n wine_df['target'],\n test_size=0.2,\n random_state=42\n)\nprint(f\"Training set shape: {X_train.shape}\")\nprint(f\"Validation set shape: {X_val.shape}\")\nprint(f\"Training target shape: {y_train.shape}\")\nprint(f\"Validation target shape: {y_val.shape}\")\n```end\n\n# Task\nCheck code in finished tasks, print key variables to guide your following actions.\nSpecifically, if it is a data analysis or machine learning task, print the the latest column information using the following code, with DataFrame variable from 'Finished Tasks' in place of df:\n```python\nfrom metagpt.tools.libs.data_preprocess import get_column_info\n\ncolumn_info = get_column_info(df)\nprint(\"column_info\")\nprint(column_info)\n```end\nOtherwise, print out any key variables you see fit. Return an empty string if you think there is no important data to check.\n\n# Constraints:\n- Your code is to be added to a new cell in jupyter.\n\n# Instruction\nOutput code following the format:\n```python\nyour code\n```\n": "```python\nfrom metagpt.tools.libs.data_preprocess import get_column_info\n\ncolumn_info = get_column_info(wine_df)\nprint(\"column_info\")\nprint(column_info)\n```", + "As a data scientist, you need to help user to achieve their goal step by step in a continuous Jupyter notebook. Since it is a notebook environment, don't use asyncio.run. Instead, use await if you need to call an async function.#SYSTEM_MSG_END#\n# User Requirement\nRun data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.\n\n# Plan Status\n\n## Finished Tasks\n### code\n```python\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom sklearn.datasets import load_wine\nwine = load_wine()\nwine_df = pd.DataFrame(data=np.c_[wine['data'], wine['target']],\n columns=wine['feature_names'] + ['target'])\nnumerical_summary = wine_df.describe()\ncategorical_summary = wine_df.select_dtypes(include=['object', 'category']).describe()\ncorrelation_matrix = wine_df.corr()\nwine_df.hist(bins=15, figsize=(15, 10), layout=(4, 4))\nplt.tight_layout()\nplt.show()\nprint(\"Numerical Summary:\\n\", numerical_summary)\nprint(\"\\nCategorical Summary:\\n\", categorical_summary)\nprint(\"\\nCorrelation Matrix:\\n\", correlation_matrix)\n\nfrom sklearn.model_selection import train_test_split\nX_train, X_val, y_train, y_val = train_test_split(\n wine_df.drop('target', axis=1),\n wine_df['target'],\n test_size=0.2,\n random_state=42\n)\nprint(f\"Training set shape: {X_train.shape}\")\nprint(f\"Validation set shape: {X_val.shape}\")\nprint(f\"Training target shape: {y_train.shape}\")\nprint(f\"Validation target shape: {y_val.shape}\")\n```\n\n### execution result\na successful run\n\na successful run\n\n## Current Task\nTrain a model using the training set to predict wine class.\n\n## Task Guidance\nWrite complete code for 'Current Task'. And avoid duplicating code from 'Finished Tasks', such as repeated import of packages, reading data, etc.\nSpecifically, \nThe current task is about training a model, please ensure high performance:\n- Keep in mind that your user prioritizes results and is highly focused on model performance. So, when needed, feel free to use models of any complexity to improve effectiveness, such as XGBoost, CatBoost, etc.\n- If non-numeric columns exist, perform label encode together with all steps.\n- Use the data from previous task result directly, do not mock or reload data yourself.\n- Set suitable hyperparameters for the model, make metrics as high as possible.\n\n\n\n# Tool Info\n\n\n# Constraints\n- Take on Current Task if it is in Plan Status, otherwise, tackle User Requirement directly.\n- Ensure the output new code is executable in the same Jupyter notebook as the previous executed code.\n- Always prioritize using pre-defined tools for the same functionality.\n\n# Output\nWhile some concise thoughts are helpful, code is absolutely required. Always output one and only one code block in your response. Output code in the following format:\n```python\nyour code\n```\n#MSG_SEP#\n# Latest Data Info\nLatest data info after previous tasks:\na successful run\n": "```python\nfrom sklearn.ensemble import RandomForestClassifier\nfrom sklearn.metrics import accuracy_score\n\n# Initialize the RandomForestClassifier\nmodel = RandomForestClassifier(n_estimators=100, random_state=42)\n\n# Train the model on the training data\nmodel.fit(X_train, y_train)\n\n# Predict the wine classes on the validation set\ny_val_pred = model.predict(X_val)\n\n# Calculate the accuracy on the validation set\nval_accuracy = accuracy_score(y_val, y_val_pred)\n\nprint(f\"Validation Accuracy: {val_accuracy:.4f}\")\n```", + "As a data scientist, you need to help user to achieve their goal step by step in a continuous Jupyter notebook. Since it is a notebook environment, don't use asyncio.run. Instead, use await if you need to call an async function.#SYSTEM_MSG_END#\n# User Requirement\nRun data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.\n\n# Plan Status\n\n## Finished Tasks\n### code\n```python\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom sklearn.datasets import load_wine\nwine = load_wine()\nwine_df = pd.DataFrame(data=np.c_[wine['data'], wine['target']],\n columns=wine['feature_names'] + ['target'])\nnumerical_summary = wine_df.describe()\ncategorical_summary = wine_df.select_dtypes(include=['object', 'category']).describe()\ncorrelation_matrix = wine_df.corr()\nwine_df.hist(bins=15, figsize=(15, 10), layout=(4, 4))\nplt.tight_layout()\nplt.show()\nprint(\"Numerical Summary:\\n\", numerical_summary)\nprint(\"\\nCategorical Summary:\\n\", categorical_summary)\nprint(\"\\nCorrelation Matrix:\\n\", correlation_matrix)\n\nfrom sklearn.model_selection import train_test_split\nX_train, X_val, y_train, y_val = train_test_split(\n wine_df.drop('target', axis=1),\n wine_df['target'],\n test_size=0.2,\n random_state=42\n)\nprint(f\"Training set shape: {X_train.shape}\")\nprint(f\"Validation set shape: {X_val.shape}\")\nprint(f\"Training target shape: {y_train.shape}\")\nprint(f\"Validation target shape: {y_val.shape}\")\n\nfrom sklearn.ensemble import RandomForestClassifier\nfrom sklearn.metrics import accuracy_score\nmodel = RandomForestClassifier(n_estimators=100, random_state=42)\nmodel.fit(X_train, y_train)\ny_val_pred = model.predict(X_val)\nval_accuracy = accuracy_score(y_val, y_val_pred)\nprint(f\"Validation Accuracy: {val_accuracy:.4f}\")\n```\n\n### execution result\na successful run\n\na successful run\n\na successful run\n\n## Current Task\nEvaluate the model on the validation set and show the validation accuracy.\n\n## Task Guidance\nWrite complete code for 'Current Task'. And avoid duplicating code from 'Finished Tasks', such as repeated import of packages, reading data, etc.\nSpecifically, \nThe current task is about evaluating a model, please note the following:\n- Ensure that the evaluated data is same processed as the training data. If not, remember use object in 'Done Tasks' to transform the data.\n- Use trained model from previous task result directly, do not mock or reload model yourself.\n\n\n\n# Tool Info\n\n\n# Constraints\n- Take on Current Task if it is in Plan Status, otherwise, tackle User Requirement directly.\n- Ensure the output new code is executable in the same Jupyter notebook as the previous executed code.\n- Always prioritize using pre-defined tools for the same functionality.\n\n# Output\nWhile some concise thoughts are helpful, code is absolutely required. Always output one and only one code block in your response. Output code in the following format:\n```python\nyour code\n```\n": "```python\n# Evaluate the model on the validation set and show the validation accuracy\nval_accuracy = accuracy_score(y_val, y_val_pred)\nprint(f\"Validation Accuracy: {val_accuracy:.4f}\")\n```", + "user: \n## User Requirement\nRun data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.\n## Context\n\n## Current Plan\n[\n {\n \"task_id\": \"1\",\n \"dependent_task_ids\": [],\n \"instruction\": \"Perform exploratory data analysis on the sklearn Wine recognition dataset including summary statistics and a plot.\",\n \"task_type\": \"eda\",\n \"code\": \"import numpy as np\\nimport pandas as pd\\nimport matplotlib.pyplot as plt\\nfrom sklearn.datasets import load_wine\\n\\n# Load the wine dataset\\nwine = load_wine()\\nwine_df = pd.DataFrame(data=np.c_[wine['data'], wine['target']],\\n columns=wine['feature_names'] + ['target'])\\n\\n# Summary statistics for numerical features\\nnumerical_summary = wine_df.describe()\\n\\n# Summary statistics for categorical features\\ncategorical_summary = wine_df.select_dtypes(include=['object', 'category']).describe()\\n\\n# Correlation matrix for numerical features\\ncorrelation_matrix = wine_df.corr()\\n\\n# Plotting a histogram for each numerical feature\\nwine_df.hist(bins=15, figsize=(15, 10), layout=(4, 4))\\nplt.tight_layout()\\nplt.show()\\n\\n# Displaying the summary statistics\\nprint(\\\"Numerical Summary:\\\\n\\\", numerical_summary)\\nprint(\\\"\\\\nCategorical Summary:\\\\n\\\", categorical_summary)\\nprint(\\\"\\\\nCorrelation Matrix:\\\\n\\\", correlation_matrix)\\n\",\n \"result\": \"a successful run\",\n \"is_success\": true,\n \"is_finished\": true\n },\n {\n \"task_id\": \"2\",\n \"dependent_task_ids\": [\n \"1\"\n ],\n \"instruction\": \"Preprocess the dataset by splitting it into training and validation sets with a 80-20 split.\",\n \"task_type\": \"data preprocessing\",\n \"code\": \"\",\n \"result\": \"\",\n \"is_success\": false,\n \"is_finished\": false\n },\n {\n \"task_id\": \"3\",\n \"dependent_task_ids\": [\n \"2\"\n ],\n \"instruction\": \"Train a model using the training set to predict wine class.\",\n \"task_type\": \"model train\",\n \"code\": \"\",\n \"result\": \"\",\n \"is_success\": false,\n \"is_finished\": false\n },\n {\n \"task_id\": \"4\",\n \"dependent_task_ids\": [\n \"3\"\n ],\n \"instruction\": \"Evaluate the model on the validation set and show the validation accuracy.\",\n \"task_type\": \"model evaluate\",\n \"code\": \"\",\n \"result\": \"\",\n \"is_success\": false,\n \"is_finished\": false\n }\n]\n## Current Task\n{\"task_id\":\"2\",\"dependent_task_ids\":[\"1\"],\"instruction\":\"Preprocess the dataset by splitting it into training and validation sets with a 80-20 split.\",\"task_type\":\"data preprocessing\",\"code\":\"\",\"result\":\"\",\"is_success\":false,\"is_finished\":false}\n\nuser: \n# Latest Data Info\nLatest data info after previous tasks:\na successful run\n\nassistant: from sklearn.model_selection import train_test_split\n\n# Split the data into training and validation sets (80-20 split)\nX_train, X_val, y_train, y_val = train_test_split(\n wine_df.drop('target', axis=1), # features\n wine_df['target'], # target variable\n test_size=0.2, # 20% for validation\n random_state=42 # seed for reproducibility\n)\n\n# Output the shapes of the resulting data splits\nprint(f\"Training set shape: {X_train.shape}\")\nprint(f\"Validation set shape: {X_val.shape}\")\nprint(f\"Training target shape: {y_train.shape}\")\nprint(f\"Validation target shape: {y_val.shape}\")\n\nuser: a successful runThis is a review. Please review output from metagpt.actions.di.execute_nb_code.ExecuteNbCode\nIf you want to change, add, delete a task or merge tasks in the plan, say 'change task task_id or current task, ... (things to change)' If you confirm the output from the current task and wish to continue, type: confirmIf you think user requirement has been fulfilled completedly, you can finish the process by typing: finish\nIf you want to terminate the process, type: exit\nPlease type your review below:\n": "confirm", + "user: \n## User Requirement\nRun data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.\n## Context\n\n## Current Plan\n[\n {\n \"task_id\": \"1\",\n \"dependent_task_ids\": [],\n \"instruction\": \"Perform exploratory data analysis on the sklearn Wine recognition dataset including summary statistics and a plot.\",\n \"task_type\": \"eda\",\n \"code\": \"import numpy as np\\nimport pandas as pd\\nimport matplotlib.pyplot as plt\\nfrom sklearn.datasets import load_wine\\n\\n# Load the wine dataset\\nwine = load_wine()\\nwine_df = pd.DataFrame(data=np.c_[wine['data'], wine['target']],\\n columns=wine['feature_names'] + ['target'])\\n\\n# Summary statistics for numerical features\\nnumerical_summary = wine_df.describe()\\n\\n# Summary statistics for categorical features\\ncategorical_summary = wine_df.select_dtypes(include=['object', 'category']).describe()\\n\\n# Correlation matrix for numerical features\\ncorrelation_matrix = wine_df.corr()\\n\\n# Plotting a histogram for each numerical feature\\nwine_df.hist(bins=15, figsize=(15, 10), layout=(4, 4))\\nplt.tight_layout()\\nplt.show()\\n\\n# Displaying the summary statistics\\nprint(\\\"Numerical Summary:\\\\n\\\", numerical_summary)\\nprint(\\\"\\\\nCategorical Summary:\\\\n\\\", categorical_summary)\\nprint(\\\"\\\\nCorrelation Matrix:\\\\n\\\", correlation_matrix)\\n\",\n \"result\": \"a successful run\",\n \"is_success\": true,\n \"is_finished\": true\n },\n {\n \"task_id\": \"2\",\n \"dependent_task_ids\": [\n \"1\"\n ],\n \"instruction\": \"Preprocess the dataset by splitting it into training and validation sets with a 80-20 split.\",\n \"task_type\": \"data preprocessing\",\n \"code\": \"from sklearn.model_selection import train_test_split\\n\\n# Split the data into training and validation sets (80-20 split)\\nX_train, X_val, y_train, y_val = train_test_split(\\n wine_df.drop('target', axis=1), # features\\n wine_df['target'], # target variable\\n test_size=0.2, # 20% for validation\\n random_state=42 # seed for reproducibility\\n)\\n\\n# Output the shapes of the resulting data splits\\nprint(f\\\"Training set shape: {X_train.shape}\\\")\\nprint(f\\\"Validation set shape: {X_val.shape}\\\")\\nprint(f\\\"Training target shape: {y_train.shape}\\\")\\nprint(f\\\"Validation target shape: {y_val.shape}\\\")\\n\",\n \"result\": \"a successful run\",\n \"is_success\": true,\n \"is_finished\": true\n },\n {\n \"task_id\": \"3\",\n \"dependent_task_ids\": [\n \"2\"\n ],\n \"instruction\": \"Train a model using the training set to predict wine class.\",\n \"task_type\": \"model train\",\n \"code\": \"\",\n \"result\": \"\",\n \"is_success\": false,\n \"is_finished\": false\n },\n {\n \"task_id\": \"4\",\n \"dependent_task_ids\": [\n \"3\"\n ],\n \"instruction\": \"Evaluate the model on the validation set and show the validation accuracy.\",\n \"task_type\": \"model evaluate\",\n \"code\": \"\",\n \"result\": \"\",\n \"is_success\": false,\n \"is_finished\": false\n }\n]\n## Current Task\n{\"task_id\":\"3\",\"dependent_task_ids\":[\"2\"],\"instruction\":\"Train a model using the training set to predict wine class.\",\"task_type\":\"model train\",\"code\":\"\",\"result\":\"\",\"is_success\":false,\"is_finished\":false}\n\nuser: \n# Latest Data Info\nLatest data info after previous tasks:\na successful run\n\nassistant: from sklearn.ensemble import RandomForestClassifier\nfrom sklearn.metrics import accuracy_score\n\n# Initialize the RandomForestClassifier\nmodel = RandomForestClassifier(n_estimators=100, random_state=42)\n\n# Train the model on the training data\nmodel.fit(X_train, y_train)\n\n# Predict the wine classes on the validation set\ny_val_pred = model.predict(X_val)\n\n# Calculate the accuracy on the validation set\nval_accuracy = accuracy_score(y_val, y_val_pred)\n\nprint(f\"Validation Accuracy: {val_accuracy:.4f}\")\n\nuser: a successful runThis is a review. Please review output from metagpt.actions.di.execute_nb_code.ExecuteNbCode\nIf you want to change, add, delete a task or merge tasks in the plan, say 'change task task_id or current task, ... (things to change)' If you confirm the output from the current task and wish to continue, type: confirmIf you think user requirement has been fulfilled completedly, you can finish the process by typing: finish\nIf you want to terminate the process, type: exit\nPlease type your review below:\n": "confirm", + "user: \n## User Requirement\nRun data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.\n## Context\n\n## Current Plan\n[\n {\n \"task_id\": \"1\",\n \"dependent_task_ids\": [],\n \"instruction\": \"Perform exploratory data analysis on the sklearn Wine recognition dataset including summary statistics and a plot.\",\n \"task_type\": \"eda\",\n \"code\": \"import numpy as np\\nimport pandas as pd\\nimport matplotlib.pyplot as plt\\nfrom sklearn.datasets import load_wine\\n\\n# Load the wine dataset\\nwine = load_wine()\\nwine_df = pd.DataFrame(data=np.c_[wine['data'], wine['target']],\\n columns=wine['feature_names'] + ['target'])\\n\\n# Summary statistics for numerical features\\nnumerical_summary = wine_df.describe()\\n\\n# Summary statistics for categorical features\\ncategorical_summary = wine_df.select_dtypes(include=['object', 'category']).describe()\\n\\n# Correlation matrix for numerical features\\ncorrelation_matrix = wine_df.corr()\\n\\n# Plotting a histogram for each numerical feature\\nwine_df.hist(bins=15, figsize=(15, 10), layout=(4, 4))\\nplt.tight_layout()\\nplt.show()\\n\\n# Displaying the summary statistics\\nprint(\\\"Numerical Summary:\\\\n\\\", numerical_summary)\\nprint(\\\"\\\\nCategorical Summary:\\\\n\\\", categorical_summary)\\nprint(\\\"\\\\nCorrelation Matrix:\\\\n\\\", correlation_matrix)\\n\",\n \"result\": \"a successful run\",\n \"is_success\": true,\n \"is_finished\": true\n },\n {\n \"task_id\": \"2\",\n \"dependent_task_ids\": [\n \"1\"\n ],\n \"instruction\": \"Preprocess the dataset by splitting it into training and validation sets with a 80-20 split.\",\n \"task_type\": \"data preprocessing\",\n \"code\": \"from sklearn.model_selection import train_test_split\\n\\n# Split the data into training and validation sets (80-20 split)\\nX_train, X_val, y_train, y_val = train_test_split(\\n wine_df.drop('target', axis=1), # features\\n wine_df['target'], # target variable\\n test_size=0.2, # 20% for validation\\n random_state=42 # seed for reproducibility\\n)\\n\\n# Output the shapes of the resulting data splits\\nprint(f\\\"Training set shape: {X_train.shape}\\\")\\nprint(f\\\"Validation set shape: {X_val.shape}\\\")\\nprint(f\\\"Training target shape: {y_train.shape}\\\")\\nprint(f\\\"Validation target shape: {y_val.shape}\\\")\\n\",\n \"result\": \"a successful run\",\n \"is_success\": true,\n \"is_finished\": true\n },\n {\n \"task_id\": \"3\",\n \"dependent_task_ids\": [\n \"2\"\n ],\n \"instruction\": \"Train a model using the training set to predict wine class.\",\n \"task_type\": \"model train\",\n \"code\": \"from sklearn.ensemble import RandomForestClassifier\\nfrom sklearn.metrics import accuracy_score\\n\\n# Initialize the RandomForestClassifier\\nmodel = RandomForestClassifier(n_estimators=100, random_state=42)\\n\\n# Train the model on the training data\\nmodel.fit(X_train, y_train)\\n\\n# Predict the wine classes on the validation set\\ny_val_pred = model.predict(X_val)\\n\\n# Calculate the accuracy on the validation set\\nval_accuracy = accuracy_score(y_val, y_val_pred)\\n\\nprint(f\\\"Validation Accuracy: {val_accuracy:.4f}\\\")\\n\",\n \"result\": \"a successful run\",\n \"is_success\": true,\n \"is_finished\": true\n },\n {\n \"task_id\": \"4\",\n \"dependent_task_ids\": [\n \"3\"\n ],\n \"instruction\": \"Evaluate the model on the validation set and show the validation accuracy.\",\n \"task_type\": \"model evaluate\",\n \"code\": \"\",\n \"result\": \"\",\n \"is_success\": false,\n \"is_finished\": false\n }\n]\n## Current Task\n{\"task_id\":\"4\",\"dependent_task_ids\":[\"3\"],\"instruction\":\"Evaluate the model on the validation set and show the validation accuracy.\",\"task_type\":\"model evaluate\",\"code\":\"\",\"result\":\"\",\"is_success\":false,\"is_finished\":false}\n\nassistant: # Evaluate the model on the validation set and show the validation accuracy\nval_accuracy = accuracy_score(y_val, y_val_pred)\nprint(f\"Validation Accuracy: {val_accuracy:.4f}\")\n\nuser: a successful runThis is a review. Please review output from metagpt.actions.di.execute_nb_code.ExecuteNbCode\nIf you want to change, add, delete a task or merge tasks in the plan, say 'change task task_id or current task, ... (things to change)' If you confirm the output from the current task and wish to continue, type: confirmIf you think user requirement has been fulfilled completedly, you can finish the process by typing: finish\nIf you want to terminate the process, type: exit\nPlease type your review below:\n": "confirm" } \ No newline at end of file diff --git a/tests/metagpt/roles/di/test_data_interpreter.py b/tests/metagpt/roles/di/test_data_interpreter.py index d25e5a099..e5cc5b29b 100644 --- a/tests/metagpt/roles/di/test_data_interpreter.py +++ b/tests/metagpt/roles/di/test_data_interpreter.py @@ -25,7 +25,6 @@ async def test_interpreter(mocker, auto_run): @pytest.mark.asyncio async def test_interpreter_react_mode(mocker): mocker.patch("metagpt.actions.di.execute_nb_code.ExecuteNbCode.run", return_value=("a successful run", True)) - mocker.patch("builtins.input", return_value="confirm") requirement = "Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy." diff --git a/tests/metagpt/strategy/test_planner.py b/tests/metagpt/strategy/test_planner.py new file mode 100644 index 000000000..ff1c6da3f --- /dev/null +++ b/tests/metagpt/strategy/test_planner.py @@ -0,0 +1,37 @@ +from metagpt.schema import Plan, Task +from metagpt.strategy.planner import Planner +from metagpt.strategy.task_type import TaskType + +MOCK_TASK_MAP = { + "1": Task( + task_id="1", + instruction="test instruction for finished task", + task_type=TaskType.EDA.type_name, + dependent_task_ids=[], + code="some finished test code", + result="some finished test result", + is_finished=True, + ), + "2": Task( + task_id="2", + instruction="test instruction for current task", + task_type=TaskType.DATA_PREPROCESS.type_name, + dependent_task_ids=["1"], + ), +} +MOCK_PLAN = Plan( + goal="test goal", + tasks=list(MOCK_TASK_MAP.values()), + task_map=MOCK_TASK_MAP, + current_task_id="2", +) + + +def test_planner_get_plan_status(): + planner = Planner(plan=MOCK_PLAN) + status = planner.get_plan_status() + + assert "some finished test code" in status + assert "some finished test result" in status + assert "test instruction for current task" in status + assert TaskType.DATA_PREPROCESS.value.guidance in status # current task guidance From c9f60f344b8dabf221de48dcac531bf73814dd13 Mon Sep 17 00:00:00 2001 From: geekan Date: Tue, 19 Mar 2024 10:58:23 +0800 Subject: [PATCH 093/131] add arxiv reader --- examples/di/arxiv_reader.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 examples/di/arxiv_reader.py diff --git a/examples/di/arxiv_reader.py b/examples/di/arxiv_reader.py new file mode 100644 index 000000000..9395cf32a --- /dev/null +++ b/examples/di/arxiv_reader.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@Time : 2024/01/15 +@Author : mannaandpoem +@File : imitate_webpage.py +""" +from metagpt.roles.di.data_interpreter import DataInterpreter + + +async def main(): + template = "https://arxiv.org/list/{tag}/pastweek?skip=0&show=300" + tags = ["cs.ai", "cs.cl", "cs.lg", "cs.se"] + urls = [template.format(tag=tag) for tag in tags] + prompt = f"""This is a collection of arxiv urls: '{urls}' . +Record each article, remove duplicates by title (they may have multiple tags), filter out papers related to +large language model / agent / , and pay attention to maintaining the original order as much as possible""" + di = DataInterpreter(react_mode="react", tools=["scrape_web_playwright"]) + + await di.run(prompt) + + +if __name__ == "__main__": + import asyncio + + asyncio.run(main()) From 29a492d03ea3b0f9a789ec87defa88dabf28e8dd Mon Sep 17 00:00:00 2001 From: geekan Date: Tue, 19 Mar 2024 19:22:34 +0800 Subject: [PATCH 094/131] update Install and Config in README --- README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 81b6a876f..40a404bb6 100644 --- a/README.md +++ b/README.md @@ -63,13 +63,19 @@ ### Pip installation > You can use conda like this: `conda create -n metagpt python=3.9 && conda activate metagpt` ```bash -pip install metagpt -# https://docs.deepwisdom.ai/main/en/guide/get_started/configuration.html -metagpt --init-config # it will create ~/.metagpt/config2.yaml, just modify it to your needs +pip install --upgrade metagpt +# or `pip install --upgrade git+https://github.com/geekan/MetaGPT.git` +# or `git clone https://github.com/geekan/MetaGPT && cd MetaGPT && pip install --upgrade -e .` ``` ### Configuration +You can init the config of MetaGPT by running the following command, or manually create `~/.metagpt/config2.yaml` file: +```bash +# Check https://docs.deepwisdom.ai/main/en/guide/get_started/configuration.html for more details +metagpt --init-config # it will create ~/.metagpt/config2.yaml, just modify it to your needs +``` + You can configure `~/.metagpt/config2.yaml` according to the [example](https://github.com/geekan/MetaGPT/blob/main/config/config2.example.yaml) and [doc](https://docs.deepwisdom.ai/main/en/guide/get_started/configuration.html): ```yaml From d9f92ee9a43817035d6f5e849abf1d8985ce5b21 Mon Sep 17 00:00:00 2001 From: geekan Date: Tue, 19 Mar 2024 19:30:14 +0800 Subject: [PATCH 095/131] update arxiv reader --- examples/di/arxiv_reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/di/arxiv_reader.py b/examples/di/arxiv_reader.py index 9395cf32a..e376b0778 100644 --- a/examples/di/arxiv_reader.py +++ b/examples/di/arxiv_reader.py @@ -14,7 +14,7 @@ async def main(): urls = [template.format(tag=tag) for tag in tags] prompt = f"""This is a collection of arxiv urls: '{urls}' . Record each article, remove duplicates by title (they may have multiple tags), filter out papers related to -large language model / agent / , and pay attention to maintaining the original order as much as possible""" +large language model / agent / llm, and visualize the word count of the titles""" di = DataInterpreter(react_mode="react", tools=["scrape_web_playwright"]) await di.run(prompt) From 29386e6d2cbf7bf547cf8c2ad0556c7a711fdb37 Mon Sep 17 00:00:00 2001 From: geekan Date: Tue, 19 Mar 2024 19:30:31 +0800 Subject: [PATCH 096/131] update arxiv reader --- examples/di/arxiv_reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/di/arxiv_reader.py b/examples/di/arxiv_reader.py index e376b0778..fab0e2e48 100644 --- a/examples/di/arxiv_reader.py +++ b/examples/di/arxiv_reader.py @@ -14,7 +14,7 @@ async def main(): urls = [template.format(tag=tag) for tag in tags] prompt = f"""This is a collection of arxiv urls: '{urls}' . Record each article, remove duplicates by title (they may have multiple tags), filter out papers related to -large language model / agent / llm, and visualize the word count of the titles""" +large language model / agent / llm, print top 100 and visualize the word count of the titles""" di = DataInterpreter(react_mode="react", tools=["scrape_web_playwright"]) await di.run(prompt) From 9e8a04ffeef3c8c638432a3ee864815b8cd6f97a Mon Sep 17 00:00:00 2001 From: geekan Date: Tue, 19 Mar 2024 19:33:22 +0800 Subject: [PATCH 097/131] update README --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 40a404bb6..ced47960b 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,9 @@ ### Contact Information ## Citation -If you use MetaGPT or Data Interpreter in a research paper, please cite our work as follows: +To stay updated with the latest research and development, follow [MetaGPT_](https://twitter.com/MetaGPT_) on Twitter. + +To cite [MetaGPT](https://arxiv.org/abs/2308.00352) or [Data Interpreter](https://arxiv.org/abs/2402.18679) in publications, please use the following BibTeX entries. ```bibtex @misc{hong2023metagpt, From 416bc597734d6883dc4981d8dbf9348836bed84d Mon Sep 17 00:00:00 2001 From: geekan Date: Tue, 19 Mar 2024 19:36:13 +0800 Subject: [PATCH 098/131] update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ced47960b..0c6c80260 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,7 @@ ### Contact Information ## Citation -To stay updated with the latest research and development, follow [MetaGPT_](https://twitter.com/MetaGPT_) on Twitter. +To stay updated with the latest research and development, follow [@MetaGPT_](https://twitter.com/MetaGPT_) on Twitter. To cite [MetaGPT](https://arxiv.org/abs/2308.00352) or [Data Interpreter](https://arxiv.org/abs/2402.18679) in publications, please use the following BibTeX entries. From 32a86bdd4e1b4fda8010e261b435f899e3bf067f Mon Sep 17 00:00:00 2001 From: yzlin Date: Tue, 19 Mar 2024 20:26:36 +0800 Subject: [PATCH 099/131] update readme --- README.md | 60 +++++++++++++++++-------------------------------------- 1 file changed, 18 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 81b6a876f..027b3b243 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ # MetaGPT: The Multi-Agent Framework

## News -🚀 Mar. 14, 2024: Our Data Interpreter paper is on [arxiv](https://arxiv.org/abs/2402.18679). Check the [example](https://docs.deepwisdom.ai/main/en/DataInterpreter/) and [code](https://github.com/geekan/MetaGPT/tree/main/examples/di)! +🚀 Mar. 14, 2024: Our **Data Interpreter** paper is on [arxiv](https://arxiv.org/abs/2402.18679). Check the [example](https://docs.deepwisdom.ai/main/en/DataInterpreter/) and [code](https://github.com/geekan/MetaGPT/tree/main/examples/di)! 🚀 Feb. 08, 2024: [v0.7.0](https://github.com/geekan/MetaGPT/releases/tag/v0.7.0) released, supporting assigning different LLMs to different Roles. We also introduced [Data Interpreter](https://github.com/geekan/MetaGPT/blob/main/examples/di/README.md), a powerful agent capable of solving a wide range of real-world problems. @@ -55,9 +55,9 @@ ## Software Company as Multi-Agent System

Software Company Multi-Agent Schematic (Gradually Implementing)

-## Install +## Get Started -### Pip installation +### Installation > Ensure that Python 3.9+ is installed on your system. You can check this by using: `python --version`. > You can use conda like this: `conda create -n metagpt python=3.9 && conda activate metagpt` @@ -68,6 +68,9 @@ # https://docs.deepwisdom.ai/main/en/guide/get_started/configuration.html metagpt --init-config # it will create ~/.metagpt/config2.yaml, just modify it to your needs ``` +For detailed installation guidance, please refer to [cli_install](https://docs.deepwisdom.ai/main/en/guide/get_started/installation.html#install-stable-version) + or [docker_install](https://docs.deepwisdom.ai/main/en/guide/get_started/installation.html#install-with-docker) + ### Configuration You can configure `~/.metagpt/config2.yaml` according to the [example](https://github.com/geekan/MetaGPT/blob/main/config/config2.example.yaml) and [doc](https://docs.deepwisdom.ai/main/en/guide/get_started/configuration.html): @@ -82,13 +85,13 @@ ### Configuration ### Usage -After installation, you can use it as CLI +After installation, you can use MetaGPT at CLI ```bash metagpt "Create a 2048 game" # this will create a repo in ./workspace ``` -or you can use it as library +or use it as library ```python from metagpt.software_company import generate_repo, ProjectRepo @@ -96,47 +99,19 @@ ### Usage print(repo) # it will print the repo structure with files ``` -detail installation please refer to [cli_install](https://docs.deepwisdom.ai/main/en/guide/get_started/installation.html#install-stable-version) - or [docker_install](https://docs.deepwisdom.ai/main/en/guide/get_started/installation.html#install-with-docker) +You can also use its [Data Interpreter](https://github.com/geekan/MetaGPT/tree/main/examples/di) -### Docker installation -
⏬ Step 1: Download metagpt image and prepare config2.yaml :: click to expand :: -
+```python +import asyncio +from metagpt.roles.di.data_interpreter import DataInterpreter -```bash -docker pull metagpt/metagpt:latest -mkdir -p /opt/metagpt/{config,workspace} -docker run --rm metagpt/metagpt:latest cat /app/metagpt/config/config2.yaml > /opt/metagpt/config/config2.yaml -vim /opt/metagpt/config/config2.yaml # Change the config +async def main(): + di = DataInterpreter() + await di.run("Run data analysis on sklearn Iris dataset, include a plot") + +asyncio.run(main()) # or await main() in a jupyter notebook setting ``` -
-
- -
⏬ Step 2: Run metagpt container :: click to expand :: -
- -```bash -docker run --name metagpt -d \ - --privileged \ - -v /opt/metagpt/config/config2.yaml:/app/metagpt/config/config2.yaml \ - -v /opt/metagpt/workspace:/app/metagpt/workspace \ - metagpt/metagpt:latest -``` - -
-
- -
⏬ Step 3: Use metagpt :: click to expand :: -
- -```bash -docker exec -it metagpt /bin/bash -$ metagpt "Create a 2048 game" # this will create a repo in ./workspace -``` - -
-
### QuickStart & Demo Video - Try it on [MetaGPT Huggingface Space](https://huggingface.co/spaces/deepwisdom/MetaGPT) @@ -156,6 +131,7 @@ ## Tutorial - 🧑‍💻 Contribution - [Develop Roadmap](docs/ROADMAP.md) - 🔖 Use Cases + - [Data Interpreter](https://docs.deepwisdom.ai/main/en/guide/use_cases/agent/interpreter/intro.html) - [Debate](https://docs.deepwisdom.ai/main/en/guide/use_cases/multi_agent/debate.html) - [Researcher](https://docs.deepwisdom.ai/main/en/guide/use_cases/agent/researcher.html) - [Recepit Assistant](https://docs.deepwisdom.ai/main/en/guide/use_cases/agent/receipt_assistant.html) From f91334deafc38f33b05deb43f1a0dc6102b7c8d1 Mon Sep 17 00:00:00 2001 From: shenchucheng Date: Tue, 19 Mar 2024 23:31:05 +0800 Subject: [PATCH 100/131] fix text ut error --- tests/metagpt/utils/test_text.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/metagpt/utils/test_text.py b/tests/metagpt/utils/test_text.py index c9a9753be..319a4ebe0 100644 --- a/tests/metagpt/utils/test_text.py +++ b/tests/metagpt/utils/test_text.py @@ -22,7 +22,7 @@ def _paragraphs(n): @pytest.mark.parametrize( "msgs, model_name, system_text, reserved, expected", [ - (_msgs(), "gpt-3.5-turbo", "System", 1500, 1), + (_msgs(), "gpt-3.5-turbo-0613", "System", 1500, 1), (_msgs(), "gpt-3.5-turbo-16k", "System", 3000, 6), (_msgs(), "gpt-3.5-turbo-16k", "Hello," * 1000, 3000, 5), (_msgs(), "gpt-4", "System", 2000, 3), @@ -32,22 +32,23 @@ def _paragraphs(n): ], ) def test_reduce_message_length(msgs, model_name, system_text, reserved, expected): - assert len(reduce_message_length(msgs, model_name, system_text, reserved)) / (len("Hello,")) / 1000 == expected + length = len(reduce_message_length(msgs, model_name, system_text, reserved)) / (len("Hello,")) / 1000 + assert length == expected @pytest.mark.parametrize( "text, prompt_template, model_name, system_text, reserved, expected", [ - (" ".join("Hello World." for _ in range(1000)), "Prompt: {}", "gpt-3.5-turbo", "System", 1500, 2), + (" ".join("Hello World." for _ in range(1000)), "Prompt: {}", "gpt-3.5-turbo-0613", "System", 1500, 2), (" ".join("Hello World." for _ in range(1000)), "Prompt: {}", "gpt-3.5-turbo-16k", "System", 3000, 1), (" ".join("Hello World." for _ in range(4000)), "Prompt: {}", "gpt-4", "System", 2000, 2), (" ".join("Hello World." for _ in range(8000)), "Prompt: {}", "gpt-4-32k", "System", 4000, 1), - (" ".join("Hello World" for _ in range(8000)), "Prompt: {}", "gpt-3.5-turbo", "System", 1000, 8), + (" ".join("Hello World" for _ in range(8000)), "Prompt: {}", "gpt-3.5-turbo-0613", "System", 1000, 8), ], ) def test_generate_prompt_chunk(text, prompt_template, model_name, system_text, reserved, expected): - ret = list(generate_prompt_chunk(text, prompt_template, model_name, system_text, reserved)) - assert len(ret) == expected + chunk = len(list(generate_prompt_chunk(text, prompt_template, model_name, system_text, reserved))) + assert chunk == expected @pytest.mark.parametrize( From 21c5d1955bf552612d0184e684ceb39235972067 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 20 Mar 2024 14:27:11 +0800 Subject: [PATCH 101/131] Update process for "Fix bug" --- metagpt/roles/engineer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 329b21553..9d8f6884f 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -240,8 +240,8 @@ class Engineer(Role): async def _think(self) -> Action | None: if not self.src_workspace: self.src_workspace = self.git_repo.workdir / self.git_repo.workdir.name - write_plan_and_change_filters = any_to_str_set([WriteTasks]) - write_code_filters = any_to_str_set([WriteTasks, WriteCodePlanAndChange, SummarizeCode, FixBug]) + write_plan_and_change_filters = any_to_str_set([WriteTasks, FixBug]) + write_code_filters = any_to_str_set([WriteTasks, WriteCodePlanAndChange, SummarizeCode]) summarize_code_filters = any_to_str_set([WriteCode, WriteCodeReview]) if not self.rc.news: return None From f6a11d508904e6a56a9e35895abfeb439f5c4110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 20 Mar 2024 17:34:30 +0800 Subject: [PATCH 102/131] fixbug: #1016 --- metagpt/actions/di/write_analysis_code.py | 4 +-- metagpt/provider/base_llm.py | 22 +++++++++++++++++ metagpt/provider/google_gemini_api.py | 30 +++++++++++++++++++++++ metagpt/provider/openai_api.py | 9 ++----- metagpt/utils/common.py | 23 ----------------- tests/mock/mock_llm.py | 3 +-- 6 files changed, 57 insertions(+), 34 deletions(-) diff --git a/metagpt/actions/di/write_analysis_code.py b/metagpt/actions/di/write_analysis_code.py index 185926e31..711e56d39 100644 --- a/metagpt/actions/di/write_analysis_code.py +++ b/metagpt/actions/di/write_analysis_code.py @@ -18,7 +18,7 @@ from metagpt.prompts.di.write_analysis_code import ( STRUCTUAL_PROMPT, ) from metagpt.schema import Message, Plan -from metagpt.utils.common import CodeParser, process_message, remove_comments +from metagpt.utils.common import CodeParser, remove_comments class WriteAnalysisCode(Action): @@ -50,7 +50,7 @@ class WriteAnalysisCode(Action): ) working_memory = working_memory or [] - context = process_message([Message(content=structual_prompt, role="user")] + working_memory) + context = self.llm.format_msg([Message(content=structual_prompt, role="user")] + working_memory) # LLM call if use_reflection: diff --git a/metagpt/provider/base_llm.py b/metagpt/provider/base_llm.py index 71308930a..601980d5e 100644 --- a/metagpt/provider/base_llm.py +++ b/metagpt/provider/base_llm.py @@ -73,6 +73,28 @@ class BaseLLM(ABC): def _system_msg(self, msg: str) -> dict[str, str]: return {"role": "system", "content": msg} + def format_msg(self, messages: Union[str, Message, list[dict], list[Message], list[str]]) -> list[dict]: + """convert messages to list[dict].""" + from metagpt.schema import Message + + if not isinstance(messages, list): + messages = [messages] + + 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 + def _system_msgs(self, msgs: list[str]) -> list[dict[str, str]]: return [self._system_msg(msg) for msg in msgs] diff --git a/metagpt/provider/google_gemini_api.py b/metagpt/provider/google_gemini_api.py index 09e554205..7370747a5 100644 --- a/metagpt/provider/google_gemini_api.py +++ b/metagpt/provider/google_gemini_api.py @@ -18,6 +18,7 @@ from metagpt.configs.llm_config import LLMConfig, LLMType from metagpt.logs import log_llm_stream from metagpt.provider.base_llm import BaseLLM from metagpt.provider.llm_provider_registry import register_provider +from metagpt.schema import Message class GeminiGenerativeModel(GenerativeModel): @@ -61,6 +62,35 @@ class GeminiLLM(BaseLLM): def _assistant_msg(self, msg: str) -> dict[str, str]: return {"role": "model", "parts": [msg]} + def _system_msg(self, msg: str) -> dict[str, str]: + return {"role": "user", "parts": [msg]} + + def format_msg(self, messages: Union[str, Message, list[dict], list[Message], list[str]]) -> list[dict]: + """convert messages to list[dict].""" + from metagpt.schema import Message + + if not isinstance(messages, list): + messages = [messages] + + # REF: https://ai.google.dev/tutorials/python_quickstart + # As a dictionary, the message requires `role` and `parts` keys. + # The role in a conversation can either be the `user`, which provides the prompts, + # or `model`, which provides the responses. + processed_messages = [] + for msg in messages: + if isinstance(msg, str): + processed_messages.append({"role": "user", "parts": [msg]}) + elif isinstance(msg, dict): + assert set(msg.keys()) == set(["role", "parts"]) + processed_messages.append(msg) + elif isinstance(msg, Message): + processed_messages.append({"role": "user" if msg.role == "user" else "model", "parts": [msg.content]}) + else: + raise ValueError( + f"Only support message type are: str, Message, dict, but got {type(messages).__name__}!" + ) + return processed_messages + def _const_kwargs(self, messages: list[dict], stream: bool = False) -> dict: kwargs = {"contents": messages, "generation_config": GenerationConfig(temperature=0.3), "stream": stream} return kwargs diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py index b4f99e69f..2fb64dc85 100644 --- a/metagpt/provider/openai_api.py +++ b/metagpt/provider/openai_api.py @@ -29,12 +29,7 @@ from metagpt.logs import log_llm_stream, logger from metagpt.provider.base_llm import BaseLLM from metagpt.provider.constant import GENERAL_FUNCTION_SCHEMA from metagpt.provider.llm_provider_registry import register_provider -from metagpt.utils.common import ( - CodeParser, - decode_image, - log_and_reraise, - process_message, -) +from metagpt.utils.common import CodeParser, decode_image, log_and_reraise from metagpt.utils.cost_manager import CostManager from metagpt.utils.exceptions import handle_exception from metagpt.utils.token_counter import ( @@ -150,7 +145,7 @@ class OpenAILLM(BaseLLM): async def _achat_completion_function( self, messages: list[dict], timeout: int = 3, **chat_configs ) -> ChatCompletion: - messages = process_message(messages) + messages = self.format_msg(messages) kwargs = self._cons_kwargs(messages=messages, timeout=timeout, **chat_configs) rsp: ChatCompletion = await self.aclient.chat.completions.create(**kwargs) self._update_costs(rsp.usage) diff --git a/metagpt/utils/common.py b/metagpt/utils/common.py index e9cef69a4..7493712c2 100644 --- a/metagpt/utils/common.py +++ b/metagpt/utils/common.py @@ -802,29 +802,6 @@ def decode_image(img_url_or_b64: str) -> Image: return img -def process_message(messages: Union[str, Message, list[dict], list[Message], list[str]]) -> list[dict]: - """convert messages to list[dict].""" - from metagpt.schema import Message - - # 全部转成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 - - def log_and_reraise(retry_state: RetryCallState): logger.error(f"Retry attempts exhausted. Last exception: {retry_state.outcome.exception()}") logger.warning( diff --git a/tests/mock/mock_llm.py b/tests/mock/mock_llm.py index b4cdfa0cf..c4262e080 100644 --- a/tests/mock/mock_llm.py +++ b/tests/mock/mock_llm.py @@ -8,7 +8,6 @@ from metagpt.provider.azure_openai_api import AzureOpenAILLM from metagpt.provider.constant import GENERAL_FUNCTION_SCHEMA from metagpt.provider.openai_api import OpenAILLM from metagpt.schema import Message -from metagpt.utils.common import process_message OriginalLLM = OpenAILLM if config.llm.api_type == LLMType.OPENAI else AzureOpenAILLM @@ -105,7 +104,7 @@ class MockLLM(OriginalLLM): return rsp async def aask_code(self, messages: Union[str, Message, list[dict]], **kwargs) -> dict: - msg_key = json.dumps(process_message(messages), ensure_ascii=False) + msg_key = json.dumps(self.format_msg(messages), ensure_ascii=False) rsp = await self._mock_rsp(msg_key, self.original_aask_code, messages, **kwargs) return rsp From af3a409ac4b7b1632512384ff86d46349a746145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 20 Mar 2024 21:24:41 +0800 Subject: [PATCH 103/131] fixbug: llm.timeout not working --- metagpt/actions/action_node.py | 6 +++--- metagpt/configs/llm_config.py | 2 +- metagpt/provider/anthropic_api.py | 8 ++++---- metagpt/provider/base_llm.py | 26 +++++++++++++++----------- metagpt/provider/dashscope_api.py | 8 ++++---- metagpt/provider/general_api_base.py | 2 +- metagpt/provider/google_gemini_api.py | 8 ++++---- metagpt/provider/human_provider.py | 14 +++++++------- metagpt/provider/ollama_api.py | 13 ++++++------- metagpt/provider/openai_api.py | 26 +++++++++++++------------- metagpt/provider/qianfan_api.py | 8 ++++---- metagpt/provider/spark_api.py | 8 ++++---- metagpt/provider/zhipuai_api.py | 10 +++++----- requirements.txt | 2 +- 14 files changed, 72 insertions(+), 69 deletions(-) diff --git a/metagpt/actions/action_node.py b/metagpt/actions/action_node.py index 09da4a988..63925a052 100644 --- a/metagpt/actions/action_node.py +++ b/metagpt/actions/action_node.py @@ -416,7 +416,7 @@ class ActionNode: images: Optional[Union[str, list[str]]] = None, system_msgs: Optional[list[str]] = None, schema="markdown", # compatible to original format - timeout=3, + timeout=0, ) -> (str, BaseModel): """Use ActionOutput to wrap the output of aask""" content = await self.llm.aask(prompt, system_msgs, images=images, timeout=timeout) @@ -448,7 +448,7 @@ class ActionNode: def set_context(self, context): self.set_recursive("context", context) - async def simple_fill(self, schema, mode, images: Optional[Union[str, list[str]]] = None, timeout=3, exclude=None): + async def simple_fill(self, schema, mode, images: Optional[Union[str, list[str]]] = None, timeout=0, exclude=None): prompt = self.compile(context=self.context, schema=schema, mode=mode, exclude=exclude) if schema != "raw": @@ -473,7 +473,7 @@ class ActionNode: mode="auto", strgy="simple", images: Optional[Union[str, list[str]]] = None, - timeout=3, + timeout=0, exclude=[], ): """Fill the node(s) with mode. diff --git a/metagpt/configs/llm_config.py b/metagpt/configs/llm_config.py index fa9bc0b1b..92b8e1512 100644 --- a/metagpt/configs/llm_config.py +++ b/metagpt/configs/llm_config.py @@ -74,7 +74,7 @@ class LLMConfig(YamlModel): stream: bool = False logprobs: Optional[bool] = None # https://cookbook.openai.com/examples/using_logprobs top_logprobs: Optional[int] = None - timeout: int = 60 + timeout: int = 600 # For Network proxy: Optional[str] = None diff --git a/metagpt/provider/anthropic_api.py b/metagpt/provider/anthropic_api.py index 872f9b2c7..3125ffc22 100644 --- a/metagpt/provider/anthropic_api.py +++ b/metagpt/provider/anthropic_api.py @@ -41,15 +41,15 @@ class AnthropicLLM(BaseLLM): def get_choice_text(self, resp: Message) -> str: return resp.content[0].text - async def _achat_completion(self, messages: list[dict], timeout: int = 3) -> Message: + async def _achat_completion(self, messages: list[dict], timeout: int = 0) -> Message: resp: Message = await self.aclient.messages.create(**self._const_kwargs(messages)) self._update_costs(resp.usage, self.model) return resp - async def acompletion(self, messages: list[dict], timeout: int = 3) -> Message: - return await self._achat_completion(messages, timeout=timeout) + async def acompletion(self, messages: list[dict], timeout: int = 0) -> Message: + return await self._achat_completion(messages, timeout=self.get_timeout(timeout)) - async def _achat_completion_stream(self, messages: list[dict], timeout: int = 3) -> str: + async def _achat_completion_stream(self, messages: list[dict], timeout: int = 0) -> str: stream = await self.aclient.messages.create(**self._const_kwargs(messages, stream=True)) collected_content = [] usage = Usage(input_tokens=0, output_tokens=0) diff --git a/metagpt/provider/base_llm.py b/metagpt/provider/base_llm.py index 71308930a..fa5119c67 100644 --- a/metagpt/provider/base_llm.py +++ b/metagpt/provider/base_llm.py @@ -23,6 +23,7 @@ from tenacity import ( ) from metagpt.configs.llm_config import LLMConfig +from metagpt.const import LLM_API_TIMEOUT from metagpt.logs import logger from metagpt.schema import Message from metagpt.utils.common import log_and_reraise @@ -108,7 +109,7 @@ class BaseLLM(ABC): system_msgs: Optional[list[str]] = None, format_msgs: Optional[list[dict[str, str]]] = None, images: Optional[Union[str, list[str]]] = None, - timeout=3, + timeout=0, stream=True, ) -> str: if system_msgs: @@ -124,31 +125,31 @@ class BaseLLM(ABC): else: message.extend(msg) logger.debug(message) - rsp = await self.acompletion_text(message, stream=stream, timeout=timeout) + rsp = await self.acompletion_text(message, stream=stream, timeout=self.get_timeout(timeout)) return rsp def _extract_assistant_rsp(self, context): return "\n".join([i["content"] for i in context if i["role"] == "assistant"]) - async def aask_batch(self, msgs: list, timeout=3) -> str: + async def aask_batch(self, msgs: list, timeout=0) -> str: """Sequential questioning""" context = [] for msg in msgs: umsg = self._user_msg(msg) context.append(umsg) - rsp_text = await self.acompletion_text(context, timeout=timeout) + rsp_text = await self.acompletion_text(context, timeout=self.get_timeout(timeout)) context.append(self._assistant_msg(rsp_text)) return self._extract_assistant_rsp(context) - async def aask_code(self, messages: Union[str, Message, list[dict]], timeout=3, **kwargs) -> dict: + async def aask_code(self, messages: Union[str, Message, list[dict]], timeout=0, **kwargs) -> dict: raise NotImplementedError @abstractmethod - async def _achat_completion(self, messages: list[dict], timeout=3): + async def _achat_completion(self, messages: list[dict], timeout=0): """_achat_completion implemented by inherited class""" @abstractmethod - async def acompletion(self, messages: list[dict], timeout=3): + async def acompletion(self, messages: list[dict], timeout=0): """Asynchronous version of completion All GPTAPIs are required to provide the standard OpenAI completion interface [ @@ -159,7 +160,7 @@ class BaseLLM(ABC): """ @abstractmethod - async def _achat_completion_stream(self, messages: list[dict], timeout: int = 3) -> str: + async def _achat_completion_stream(self, messages: list[dict], timeout: int = 0) -> str: """_achat_completion_stream implemented by inherited class""" @retry( @@ -169,11 +170,11 @@ class BaseLLM(ABC): retry=retry_if_exception_type(ConnectionError), retry_error_callback=log_and_reraise, ) - async def acompletion_text(self, messages: list[dict], stream: bool = False, timeout: int = 3) -> str: + async def acompletion_text(self, messages: list[dict], stream: bool = False, timeout: int = 0) -> str: """Asynchronous version of completion. Return str. Support stream-print""" if stream: - return await self._achat_completion_stream(messages, timeout=timeout) - resp = await self._achat_completion(messages, timeout=timeout) + return await self._achat_completion_stream(messages, timeout=self.get_timeout(timeout)) + resp = await self._achat_completion(messages, timeout=self.get_timeout(timeout)) return self.get_choice_text(resp) def get_choice_text(self, rsp: dict) -> str: @@ -236,3 +237,6 @@ class BaseLLM(ABC): """Set model and return self. For example, `with_model("gpt-3.5-turbo")`.""" self.config.model = model return self + + def get_timeout(self, timeout: int) -> int: + return timeout or self.config.timeout or LLM_API_TIMEOUT diff --git a/metagpt/provider/dashscope_api.py b/metagpt/provider/dashscope_api.py index 21f3ef351..a3efd5116 100644 --- a/metagpt/provider/dashscope_api.py +++ b/metagpt/provider/dashscope_api.py @@ -202,16 +202,16 @@ class DashScopeLLM(BaseLLM): self._update_costs(dict(resp.usage)) return resp.output - async def _achat_completion(self, messages: list[dict], timeout: int = 3) -> GenerationOutput: + async def _achat_completion(self, messages: list[dict], timeout: int = 0) -> GenerationOutput: resp: GenerationResponse = await self.aclient.acall(**self._const_kwargs(messages, stream=False)) self._check_response(resp) self._update_costs(dict(resp.usage)) return resp.output - async def acompletion(self, messages: list[dict], timeout=3) -> GenerationOutput: - return await self._achat_completion(messages, timeout=timeout) + async def acompletion(self, messages: list[dict], timeout=0) -> GenerationOutput: + return await self._achat_completion(messages, timeout=self.get_timeout(timeout)) - async def _achat_completion_stream(self, messages: list[dict], timeout: int = 3) -> str: + async def _achat_completion_stream(self, messages: list[dict], timeout: int = 0) -> str: resp = await self.aclient.acall(**self._const_kwargs(messages, stream=True)) collected_content = [] usage = {} diff --git a/metagpt/provider/general_api_base.py b/metagpt/provider/general_api_base.py index 1b9149396..8e5da8f16 100644 --- a/metagpt/provider/general_api_base.py +++ b/metagpt/provider/general_api_base.py @@ -573,7 +573,7 @@ class APIRequestor: total=request_timeout[1], ) else: - timeout = aiohttp.ClientTimeout(total=request_timeout if request_timeout else TIMEOUT_SECS) + timeout = aiohttp.ClientTimeout(total=request_timeout or TIMEOUT_SECS) if files: # TODO: Use `aiohttp.MultipartWriter` to create the multipart form data here. diff --git a/metagpt/provider/google_gemini_api.py b/metagpt/provider/google_gemini_api.py index 09e554205..2a00d4b5a 100644 --- a/metagpt/provider/google_gemini_api.py +++ b/metagpt/provider/google_gemini_api.py @@ -88,16 +88,16 @@ class GeminiLLM(BaseLLM): self._update_costs(usage) return resp - async def _achat_completion(self, messages: list[dict], timeout: int = 3) -> "AsyncGenerateContentResponse": + async def _achat_completion(self, messages: list[dict], timeout: int = 0) -> "AsyncGenerateContentResponse": resp: AsyncGenerateContentResponse = await self.llm.generate_content_async(**self._const_kwargs(messages)) usage = await self.aget_usage(messages, resp.text) self._update_costs(usage) return resp - async def acompletion(self, messages: list[dict], timeout=3) -> dict: - return await self._achat_completion(messages, timeout=timeout) + async def acompletion(self, messages: list[dict], timeout=0) -> dict: + return await self._achat_completion(messages, timeout=self.get_timeout(timeout)) - async def _achat_completion_stream(self, messages: list[dict], timeout: int = 3) -> str: + async def _achat_completion_stream(self, messages: list[dict], timeout: int = 0) -> str: resp: AsyncGenerateContentResponse = await self.llm.generate_content_async( **self._const_kwargs(messages, stream=True) ) diff --git a/metagpt/provider/human_provider.py b/metagpt/provider/human_provider.py index e5f37c5b9..df63a8bc9 100644 --- a/metagpt/provider/human_provider.py +++ b/metagpt/provider/human_provider.py @@ -18,7 +18,7 @@ class HumanProvider(BaseLLM): def __init__(self, config: LLMConfig): pass - def ask(self, msg: str, timeout=3) -> str: + def ask(self, msg: str, timeout=0) -> str: logger.info("It's your turn, please type in your response. You may also refer to the context below") rsp = input(msg) if rsp in ["exit", "quit"]: @@ -31,20 +31,20 @@ class HumanProvider(BaseLLM): system_msgs: Optional[list[str]] = None, format_msgs: Optional[list[dict[str, str]]] = None, generator: bool = False, - timeout=3, + timeout=0, ) -> str: - return self.ask(msg, timeout=timeout) + return self.ask(msg, timeout=self.get_timeout(timeout)) - async def _achat_completion(self, messages: list[dict], timeout=3): + async def _achat_completion(self, messages: list[dict], timeout=0): pass - async def acompletion(self, messages: list[dict], timeout=3): + async def acompletion(self, messages: list[dict], timeout=0): """dummy implementation of abstract method in base""" return [] - async def _achat_completion_stream(self, messages: list[dict], timeout: int = 3) -> str: + async def _achat_completion_stream(self, messages: list[dict], timeout: int = 0) -> str: pass - async def acompletion_text(self, messages: list[dict], stream=False, timeout=3) -> str: + async def acompletion_text(self, messages: list[dict], stream=False, timeout=0) -> str: """dummy implementation of abstract method in base""" return "" diff --git a/metagpt/provider/ollama_api.py b/metagpt/provider/ollama_api.py index f65d7e411..723abb574 100644 --- a/metagpt/provider/ollama_api.py +++ b/metagpt/provider/ollama_api.py @@ -5,7 +5,6 @@ import json from metagpt.configs.llm_config import LLMConfig, LLMType -from metagpt.const import LLM_API_TIMEOUT from metagpt.logs import log_llm_stream from metagpt.provider.base_llm import BaseLLM from metagpt.provider.general_api_requestor import GeneralAPIRequestor @@ -50,28 +49,28 @@ class OllamaLLM(BaseLLM): chunk = chunk.decode(encoding) return json.loads(chunk) - async def _achat_completion(self, messages: list[dict], timeout: int = 3) -> dict: + async def _achat_completion(self, messages: list[dict], timeout: int = 0) -> dict: resp, _, _ = await self.client.arequest( method=self.http_method, url=self.suffix_url, params=self._const_kwargs(messages), - request_timeout=LLM_API_TIMEOUT, + request_timeout=self.get_timeout(timeout), ) resp = self._decode_and_load(resp) usage = self.get_usage(resp) self._update_costs(usage) return resp - async def acompletion(self, messages: list[dict], timeout=3) -> dict: - return await self._achat_completion(messages, timeout=timeout) + async def acompletion(self, messages: list[dict], timeout=0) -> dict: + return await self._achat_completion(messages, timeout=self.get_timeout(timeout)) - async def _achat_completion_stream(self, messages: list[dict], timeout: int = 3) -> str: + async def _achat_completion_stream(self, messages: list[dict], timeout: int = 0) -> str: stream_resp, _, _ = await self.client.arequest( method=self.http_method, url=self.suffix_url, stream=True, params=self._const_kwargs(messages, stream=True), - request_timeout=LLM_API_TIMEOUT, + request_timeout=self.get_timeout(timeout), ) collected_content = [] diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py index b4f99e69f..5b4abae61 100644 --- a/metagpt/provider/openai_api.py +++ b/metagpt/provider/openai_api.py @@ -79,9 +79,9 @@ class OpenAILLM(BaseLLM): return params - async def _achat_completion_stream(self, messages: list[dict], timeout=3) -> str: + async def _achat_completion_stream(self, messages: list[dict], timeout=0) -> str: response: AsyncStream[ChatCompletionChunk] = await self.aclient.chat.completions.create( - **self._cons_kwargs(messages, timeout=timeout), stream=True + **self._cons_kwargs(messages, timeout=self.get_timeout(timeout)), stream=True ) usage = None collected_messages = [] @@ -109,7 +109,7 @@ class OpenAILLM(BaseLLM): self._update_costs(usage) return full_reply_content - def _cons_kwargs(self, messages: list[dict], timeout=3, **extra_kwargs) -> dict: + def _cons_kwargs(self, messages: list[dict], timeout=0, **extra_kwargs) -> dict: kwargs = { "messages": messages, "max_tokens": self._get_max_tokens(messages), @@ -117,20 +117,20 @@ class OpenAILLM(BaseLLM): # "stop": None, # default it's None and gpt4-v can't have this one "temperature": self.config.temperature, "model": self.model, - "timeout": max(self.config.timeout, timeout), + "timeout": self.get_timeout(timeout), } if extra_kwargs: kwargs.update(extra_kwargs) return kwargs - async def _achat_completion(self, messages: list[dict], timeout=3) -> ChatCompletion: - kwargs = self._cons_kwargs(messages, timeout=timeout) + async def _achat_completion(self, messages: list[dict], timeout=0) -> ChatCompletion: + kwargs = self._cons_kwargs(messages, timeout=self.get_timeout(timeout)) rsp: ChatCompletion = await self.aclient.chat.completions.create(**kwargs) self._update_costs(rsp.usage) return rsp - async def acompletion(self, messages: list[dict], timeout=3) -> ChatCompletion: - return await self._achat_completion(messages, timeout=timeout) + async def acompletion(self, messages: list[dict], timeout=0) -> ChatCompletion: + return await self._achat_completion(messages, timeout=self.get_timeout(timeout)) @retry( wait=wait_random_exponential(min=1, max=60), @@ -139,24 +139,24 @@ class OpenAILLM(BaseLLM): retry=retry_if_exception_type(APIConnectionError), retry_error_callback=log_and_reraise, ) - async def acompletion_text(self, messages: list[dict], stream=False, timeout=3) -> str: + async def acompletion_text(self, messages: list[dict], stream=False, timeout=0) -> str: """when streaming, print each token in place.""" if stream: return await self._achat_completion_stream(messages, timeout=timeout) - rsp = await self._achat_completion(messages, timeout=timeout) + rsp = await self._achat_completion(messages, timeout=self.get_timeout(timeout)) return self.get_choice_text(rsp) async def _achat_completion_function( - self, messages: list[dict], timeout: int = 3, **chat_configs + self, messages: list[dict], timeout: int = 0, **chat_configs ) -> ChatCompletion: messages = process_message(messages) - kwargs = self._cons_kwargs(messages=messages, timeout=timeout, **chat_configs) + kwargs = self._cons_kwargs(messages=messages, timeout=self.get_timeout(timeout), **chat_configs) rsp: ChatCompletion = await self.aclient.chat.completions.create(**kwargs) self._update_costs(rsp.usage) return rsp - async def aask_code(self, messages: list[dict], timeout: int = 3, **kwargs) -> dict: + async def aask_code(self, messages: list[dict], timeout: int = 0, **kwargs) -> dict: """Use function of tools to ask a code. Note: Keep kwargs consistent with https://platform.openai.com/docs/api-reference/chat/create diff --git a/metagpt/provider/qianfan_api.py b/metagpt/provider/qianfan_api.py index 50916fa3e..7e0bf009e 100644 --- a/metagpt/provider/qianfan_api.py +++ b/metagpt/provider/qianfan_api.py @@ -107,15 +107,15 @@ class QianFanLLM(BaseLLM): self._update_costs(resp.body.get("usage", {})) return resp.body - async def _achat_completion(self, messages: list[dict], timeout: int = 3) -> JsonBody: + async def _achat_completion(self, messages: list[dict], timeout: int = 0) -> JsonBody: resp = await self.aclient.ado(**self._const_kwargs(messages=messages, stream=False)) self._update_costs(resp.body.get("usage", {})) return resp.body - async def acompletion(self, messages: list[dict], timeout: int = 3) -> JsonBody: - return await self._achat_completion(messages, timeout=timeout) + async def acompletion(self, messages: list[dict], timeout: int = 0) -> JsonBody: + return await self._achat_completion(messages, timeout=self.get_timeout(timeout)) - async def _achat_completion_stream(self, messages: list[dict], timeout: int = 3) -> str: + async def _achat_completion_stream(self, messages: list[dict], timeout: int = 0) -> str: resp = await self.aclient.ado(**self._const_kwargs(messages=messages, stream=True)) collected_content = [] usage = {} diff --git a/metagpt/provider/spark_api.py b/metagpt/provider/spark_api.py index 882c6ce85..0f450ccf5 100644 --- a/metagpt/provider/spark_api.py +++ b/metagpt/provider/spark_api.py @@ -31,19 +31,19 @@ class SparkLLM(BaseLLM): def get_choice_text(self, rsp: dict) -> str: return rsp["payload"]["choices"]["text"][-1]["content"] - async def _achat_completion_stream(self, messages: list[dict], timeout: int = 3) -> str: + async def _achat_completion_stream(self, messages: list[dict], timeout: int = 0) -> str: pass - async def acompletion_text(self, messages: list[dict], stream=False, timeout: int = 3) -> str: + async def acompletion_text(self, messages: list[dict], stream=False, timeout: int = 0) -> str: # 不支持 # logger.warning("当前方法无法支持异步运行。当你使用acompletion时,并不能并行访问。") w = GetMessageFromWeb(messages, self.config) return w.run() - async def _achat_completion(self, messages: list[dict], timeout=3): + async def _achat_completion(self, messages: list[dict], timeout=0): pass - async def acompletion(self, messages: list[dict], timeout=3): + async def acompletion(self, messages: list[dict], timeout=0): # 不支持异步 w = GetMessageFromWeb(messages, self.config) return w.run() diff --git a/metagpt/provider/zhipuai_api.py b/metagpt/provider/zhipuai_api.py index 14ad1a36b..8c5284770 100644 --- a/metagpt/provider/zhipuai_api.py +++ b/metagpt/provider/zhipuai_api.py @@ -45,22 +45,22 @@ class ZhiPuAILLM(BaseLLM): kwargs = {"model": self.model, "messages": messages, "stream": stream, "temperature": 0.3} return kwargs - def completion(self, messages: list[dict], timeout=3) -> dict: + def completion(self, messages: list[dict], timeout=0) -> dict: resp: Completion = self.llm.chat.completions.create(**self._const_kwargs(messages)) usage = resp.usage.model_dump() self._update_costs(usage) return resp.model_dump() - async def _achat_completion(self, messages: list[dict], timeout=3) -> dict: + async def _achat_completion(self, messages: list[dict], timeout=0) -> dict: resp = await self.llm.acreate(**self._const_kwargs(messages)) usage = resp.get("usage", {}) self._update_costs(usage) return resp - async def acompletion(self, messages: list[dict], timeout=3) -> dict: - return await self._achat_completion(messages, timeout=timeout) + async def acompletion(self, messages: list[dict], timeout=0) -> dict: + return await self._achat_completion(messages, timeout=self.get_timeout(timeout)) - async def _achat_completion_stream(self, messages: list[dict], timeout=3) -> str: + async def _achat_completion_stream(self, messages: list[dict], timeout=0) -> str: response = await self.llm.acreate_stream(**self._const_kwargs(messages, stream=True)) collected_content = [] usage = {} diff --git a/requirements.txt b/requirements.txt index 83565278b..6b23e47b6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,7 +34,7 @@ PyYAML==6.0.1 # sentence_transformers==2.2.2 setuptools==65.6.3 tenacity==8.2.3 -tiktoken==0.5.2 +tiktoken==0.6.0 tqdm==4.66.2 #unstructured[local-inference] # selenium>4 From 7c8f57e46c16aaa5a3acfeae6f3659f47b8952d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 20 Mar 2024 21:37:47 +0800 Subject: [PATCH 104/131] feat: + timeout --- config/config2.example.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/config2.example.yaml b/config/config2.example.yaml index 3a5cc3585..46059e7e1 100644 --- a/config/config2.example.yaml +++ b/config/config2.example.yaml @@ -4,6 +4,7 @@ llm: api_key: "YOUR_API_KEY" model: "gpt-4-turbo-preview" # or gpt-3.5-turbo-1106 / gpt-4-1106-preview proxy: "YOUR_PROXY" # for LLM API requests + # timeout: 600 # Optional. pricing_plan: "" # Optional. If invalid, it will be automatically filled in with the value of the `model`. # Azure-exclusive pricing plan mappings: # - gpt-3.5-turbo 4k: "gpt-3.5-turbo-1106" From 067a39e7b923e4d9394ca8547f04cc1e24cd079d Mon Sep 17 00:00:00 2001 From: geekan Date: Thu, 21 Mar 2024 10:45:53 +0800 Subject: [PATCH 105/131] remove incorrect file header --- examples/di/arxiv_reader.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/di/arxiv_reader.py b/examples/di/arxiv_reader.py index fab0e2e48..7f2c6e331 100644 --- a/examples/di/arxiv_reader.py +++ b/examples/di/arxiv_reader.py @@ -2,8 +2,6 @@ # -*- coding: utf-8 -*- """ @Time : 2024/01/15 -@Author : mannaandpoem -@File : imitate_webpage.py """ from metagpt.roles.di.data_interpreter import DataInterpreter From dd348d07071c9d59ec243058922d3d37c90eb6c3 Mon Sep 17 00:00:00 2001 From: geekan Date: Thu, 21 Mar 2024 10:45:58 +0800 Subject: [PATCH 106/131] remove incorrect file header --- examples/di/arxiv_reader.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/examples/di/arxiv_reader.py b/examples/di/arxiv_reader.py index 7f2c6e331..6e1939b81 100644 --- a/examples/di/arxiv_reader.py +++ b/examples/di/arxiv_reader.py @@ -1,8 +1,5 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -""" -@Time : 2024/01/15 -""" from metagpt.roles.di.data_interpreter import DataInterpreter From 9350e214b4280049387d50bf8af2ae6bf5205029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 21 Mar 2024 12:59:44 +0800 Subject: [PATCH 107/131] feat: + repo to markdown --- .gitignore | 2 +- metagpt/utils/common.py | 19 +++++ metagpt/utils/repo_to_markdown.py | 80 ++++++++++++++++++++ metagpt/utils/tree.py | 2 +- tests/metagpt/utils/test_repo_to_markdown.py | 25 ++++++ 5 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 metagpt/utils/repo_to_markdown.py create mode 100644 tests/metagpt/utils/test_repo_to_markdown.py diff --git a/.gitignore b/.gitignore index 922116d12..aa5edd74a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ ### Python template # Byte-compiled / optimized / DLL files -__pycache__/ +__pycache__ *.py[cod] *$py.class diff --git a/metagpt/utils/common.py b/metagpt/utils/common.py index e9cef69a4..cc40e3762 100644 --- a/metagpt/utils/common.py +++ b/metagpt/utils/common.py @@ -18,6 +18,7 @@ import csv import importlib import inspect import json +import mimetypes import os import platform import re @@ -834,3 +835,21 @@ See FAQ 5.8 """ ) raise retry_state.outcome.exception() + + +def get_markdown_codeblock_type(filename: str) -> str: + """Return the markdown code-block type corresponding to the file extension.""" + mime_type, _ = mimetypes.guess_type(filename) + mappings = { + "text/x-shellscript": "bash", + "text/x-c++src": "cpp", + "text/css": "css", + "text/html": "html", + "text/x-java": "java", + "application/javascript": "javascript", + "application/json": "json", + "text/x-python": "python", + "text/x-ruby": "ruby", + "application/sql": "sql", + } + return mappings.get(mime_type, "text") diff --git a/metagpt/utils/repo_to_markdown.py b/metagpt/utils/repo_to_markdown.py new file mode 100644 index 000000000..76dfe1b82 --- /dev/null +++ b/metagpt/utils/repo_to_markdown.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This file provides functionality to convert a local repository into a markdown representation. +""" +from __future__ import annotations + +import mimetypes +from pathlib import Path + +from gitignore_parser import parse_gitignore + +from metagpt.logs import logger +from metagpt.utils.common import aread, awrite, get_markdown_codeblock_type, list_files +from metagpt.utils.tree import tree + + +async def repo_to_markdown(repo_path: str | Path, output: str | Path = None, gitignore: str | Path = None) -> str: + """ + Convert a local repository into a markdown representation. + + This function takes a path to a local repository and generates a markdown representation of the repository structure, + including directory trees and file listings. + + Args: + repo_path (str | Path): The path to the local repository. + output (str | Path, optional): The path to save the generated markdown file. Defaults to None. + gitignore (str | Path, optional): The path to the .gitignore file. Defaults to None. + + Returns: + str: The markdown representation of the repository. + """ + repo_path = Path(repo_path) + gitignore = Path(gitignore or Path(__file__).parent / "../../.gitignore").resolve() + + markdown = await _write_dir_tree(repo_path=repo_path, gitignore=gitignore) + + gitignore_rules = parse_gitignore(full_path=str(gitignore)) + markdown += await _write_files(repo_path=repo_path, gitignore_rules=gitignore_rules) + + if output: + await awrite(filename=str(output), data=markdown, encoding="utf-8") + return markdown + + +async def _write_dir_tree(repo_path: Path, gitignore: Path) -> str: + try: + content = tree(repo_path, gitignore, run_command=True) + except Exception as e: + logger.info(f"{e}, using safe mode.") + content = tree(repo_path, gitignore, run_command=False) + + doc = f"## Directory Tree\n```text\n{content}\n```\n---\n\n" + return doc + + +async def _write_files(repo_path, gitignore_rules) -> str: + filenames = list_files(repo_path) + markdown = "" + for filename in filenames: + if gitignore_rules(str(filename)): + continue + markdown += await _write_file(filename=filename, repo_path=repo_path) + return markdown + + +async def _write_file(filename: Path, repo_path: Path) -> str: + relative_path = filename.relative_to(repo_path) + markdown = f"## {relative_path}\n" + + mime_type, _ = mimetypes.guess_type(filename.name) + if "text/" not in mime_type: + logger.info(f"Ignore content: {filename}") + markdown += "\n---\n\n" + return markdown + content = await aread(filename, encoding="utf-8") + content = content.replace("```", "\\`\\`\\`").replace("---", "\\-\\-\\-") + code_block_type = get_markdown_codeblock_type(filename.name) + markdown += f"```{code_block_type}\n{content}\n```\n---\n\n" + return markdown diff --git a/metagpt/utils/tree.py b/metagpt/utils/tree.py index fbf085e48..bd7922290 100644 --- a/metagpt/utils/tree.py +++ b/metagpt/utils/tree.py @@ -130,7 +130,7 @@ def _add_line(rows: List[str]) -> List[str]: def _execute_tree(root: Path, gitignore: str | Path) -> str: - args = ["--gitignore", str(gitignore)] if gitignore else [] + args = ["--gitfile", str(gitignore)] if gitignore else [] try: result = subprocess.run(["tree"] + args + [str(root)], capture_output=True, text=True, check=True) if result.returncode != 0: diff --git a/tests/metagpt/utils/test_repo_to_markdown.py b/tests/metagpt/utils/test_repo_to_markdown.py new file mode 100644 index 000000000..914c50dd7 --- /dev/null +++ b/tests/metagpt/utils/test_repo_to_markdown.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import uuid +from pathlib import Path + +import pytest + +from metagpt.utils.repo_to_markdown import repo_to_markdown + + +@pytest.mark.parametrize( + ["repo_path", "output"], + [(Path(__file__).parent.parent, Path(__file__).parent.parent.parent / f"workspace/unittest/{uuid.uuid4().hex}.md")], +) +@pytest.mark.asyncio +async def test_repo_to_markdown(repo_path: Path, output: Path): + markdown = await repo_to_markdown(repo_path=repo_path, output=output) + assert output.exists() + assert markdown + + output.unlink(missing_ok=True) + + +if __name__ == "__main__": + pytest.main([__file__, "-s"]) From b42cf5cbd6d1cc7b0ffb22e893efb754022da8be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 21 Mar 2024 13:21:24 +0800 Subject: [PATCH 108/131] refactor: timeout 0 --- config/config2.example.yaml | 2 +- metagpt/actions/action_node.py | 9 ++++++--- metagpt/configs/llm_config.py | 6 ++++++ metagpt/const.py | 5 ++++- metagpt/provider/anthropic_api.py | 7 ++++--- metagpt/provider/base_llm.py | 18 ++++++++++-------- metagpt/provider/dashscope_api.py | 7 ++++--- metagpt/provider/google_gemini_api.py | 9 ++++++--- metagpt/provider/human_provider.py | 13 +++++++------ metagpt/provider/ollama_api.py | 7 ++++--- metagpt/provider/openai_api.py | 15 ++++++++------- metagpt/provider/qianfan_api.py | 7 ++++--- metagpt/provider/spark_api.py | 9 +++++---- metagpt/provider/zhipuai_api.py | 9 +++++---- 14 files changed, 74 insertions(+), 49 deletions(-) diff --git a/config/config2.example.yaml b/config/config2.example.yaml index 46059e7e1..c5454ec32 100644 --- a/config/config2.example.yaml +++ b/config/config2.example.yaml @@ -4,7 +4,7 @@ llm: api_key: "YOUR_API_KEY" model: "gpt-4-turbo-preview" # or gpt-3.5-turbo-1106 / gpt-4-1106-preview proxy: "YOUR_PROXY" # for LLM API requests - # timeout: 600 # Optional. + # timeout: 600 # Optional. If set to 0, default value is 300. pricing_plan: "" # Optional. If invalid, it will be automatically filled in with the value of the `model`. # Azure-exclusive pricing plan mappings: # - gpt-3.5-turbo 4k: "gpt-3.5-turbo-1106" diff --git a/metagpt/actions/action_node.py b/metagpt/actions/action_node.py index 63925a052..3f822568e 100644 --- a/metagpt/actions/action_node.py +++ b/metagpt/actions/action_node.py @@ -17,6 +17,7 @@ from pydantic import BaseModel, Field, create_model, model_validator from tenacity import retry, stop_after_attempt, wait_random_exponential from metagpt.actions.action_outcls_registry import register_action_outcls +from metagpt.const import USE_CONFIG_TIMEOUT from metagpt.llm import BaseLLM from metagpt.logs import logger from metagpt.provider.postprocess.llm_output_postprocess import llm_output_postprocess @@ -416,7 +417,7 @@ class ActionNode: images: Optional[Union[str, list[str]]] = None, system_msgs: Optional[list[str]] = None, schema="markdown", # compatible to original format - timeout=0, + timeout=USE_CONFIG_TIMEOUT, ) -> (str, BaseModel): """Use ActionOutput to wrap the output of aask""" content = await self.llm.aask(prompt, system_msgs, images=images, timeout=timeout) @@ -448,7 +449,9 @@ class ActionNode: def set_context(self, context): self.set_recursive("context", context) - async def simple_fill(self, schema, mode, images: Optional[Union[str, list[str]]] = None, timeout=0, exclude=None): + async def simple_fill( + self, schema, mode, images: Optional[Union[str, list[str]]] = None, timeout=USE_CONFIG_TIMEOUT, exclude=None + ): prompt = self.compile(context=self.context, schema=schema, mode=mode, exclude=exclude) if schema != "raw": @@ -473,7 +476,7 @@ class ActionNode: mode="auto", strgy="simple", images: Optional[Union[str, list[str]]] = None, - timeout=0, + timeout=USE_CONFIG_TIMEOUT, exclude=[], ): """Fill the node(s) with mode. diff --git a/metagpt/configs/llm_config.py b/metagpt/configs/llm_config.py index 92b8e1512..af8f56372 100644 --- a/metagpt/configs/llm_config.py +++ b/metagpt/configs/llm_config.py @@ -10,6 +10,7 @@ from typing import Optional from pydantic import field_validator +from metagpt.const import LLM_API_TIMEOUT from metagpt.utils.yaml_model import YamlModel @@ -88,3 +89,8 @@ class LLMConfig(YamlModel): if v in ["", None, "YOUR_API_KEY"]: raise ValueError("Please set your API key in config2.yaml") return v + + @field_validator("timeout") + @classmethod + def check_timeout(cls, v): + return v or LLM_API_TIMEOUT diff --git a/metagpt/const.py b/metagpt/const.py index 6dbbfe0c1..e4cebfd96 100644 --- a/metagpt/const.py +++ b/metagpt/const.py @@ -123,7 +123,6 @@ BASE64_FORMAT = "base64" # REDIS REDIS_KEY = "REDIS_KEY" -LLM_API_TIMEOUT = 300 # Message id IGNORED_MESSAGE_ID = "0" @@ -132,3 +131,7 @@ IGNORED_MESSAGE_ID = "0" GENERALIZATION = "Generalize" COMPOSITION = "Composite" AGGREGATION = "Aggregate" + +# Timeout +USE_CONFIG_TIMEOUT = 0 # Using llm.timeout configuration. +LLM_API_TIMEOUT = 300 diff --git a/metagpt/provider/anthropic_api.py b/metagpt/provider/anthropic_api.py index 3125ffc22..1aeacbe83 100644 --- a/metagpt/provider/anthropic_api.py +++ b/metagpt/provider/anthropic_api.py @@ -5,6 +5,7 @@ from anthropic import AsyncAnthropic from anthropic.types import Message, Usage from metagpt.configs.llm_config import LLMConfig, LLMType +from metagpt.const import USE_CONFIG_TIMEOUT from metagpt.logs import log_llm_stream from metagpt.provider.base_llm import BaseLLM from metagpt.provider.llm_provider_registry import register_provider @@ -41,15 +42,15 @@ class AnthropicLLM(BaseLLM): def get_choice_text(self, resp: Message) -> str: return resp.content[0].text - async def _achat_completion(self, messages: list[dict], timeout: int = 0) -> Message: + async def _achat_completion(self, messages: list[dict], timeout: int = USE_CONFIG_TIMEOUT) -> Message: resp: Message = await self.aclient.messages.create(**self._const_kwargs(messages)) self._update_costs(resp.usage, self.model) return resp - async def acompletion(self, messages: list[dict], timeout: int = 0) -> Message: + async def acompletion(self, messages: list[dict], timeout: int = USE_CONFIG_TIMEOUT) -> Message: return await self._achat_completion(messages, timeout=self.get_timeout(timeout)) - async def _achat_completion_stream(self, messages: list[dict], timeout: int = 0) -> str: + async def _achat_completion_stream(self, messages: list[dict], timeout: int = USE_CONFIG_TIMEOUT) -> str: stream = await self.aclient.messages.create(**self._const_kwargs(messages, stream=True)) collected_content = [] usage = Usage(input_tokens=0, output_tokens=0) diff --git a/metagpt/provider/base_llm.py b/metagpt/provider/base_llm.py index fa5119c67..e085d0187 100644 --- a/metagpt/provider/base_llm.py +++ b/metagpt/provider/base_llm.py @@ -23,7 +23,7 @@ from tenacity import ( ) from metagpt.configs.llm_config import LLMConfig -from metagpt.const import LLM_API_TIMEOUT +from metagpt.const import LLM_API_TIMEOUT, USE_CONFIG_TIMEOUT from metagpt.logs import logger from metagpt.schema import Message from metagpt.utils.common import log_and_reraise @@ -109,7 +109,7 @@ class BaseLLM(ABC): system_msgs: Optional[list[str]] = None, format_msgs: Optional[list[dict[str, str]]] = None, images: Optional[Union[str, list[str]]] = None, - timeout=0, + timeout=USE_CONFIG_TIMEOUT, stream=True, ) -> str: if system_msgs: @@ -131,7 +131,7 @@ class BaseLLM(ABC): def _extract_assistant_rsp(self, context): return "\n".join([i["content"] for i in context if i["role"] == "assistant"]) - async def aask_batch(self, msgs: list, timeout=0) -> str: + async def aask_batch(self, msgs: list, timeout=USE_CONFIG_TIMEOUT) -> str: """Sequential questioning""" context = [] for msg in msgs: @@ -141,15 +141,15 @@ class BaseLLM(ABC): context.append(self._assistant_msg(rsp_text)) return self._extract_assistant_rsp(context) - async def aask_code(self, messages: Union[str, Message, list[dict]], timeout=0, **kwargs) -> dict: + async def aask_code(self, messages: Union[str, Message, list[dict]], timeout=USE_CONFIG_TIMEOUT, **kwargs) -> dict: raise NotImplementedError @abstractmethod - async def _achat_completion(self, messages: list[dict], timeout=0): + async def _achat_completion(self, messages: list[dict], timeout=USE_CONFIG_TIMEOUT): """_achat_completion implemented by inherited class""" @abstractmethod - async def acompletion(self, messages: list[dict], timeout=0): + async def acompletion(self, messages: list[dict], timeout=USE_CONFIG_TIMEOUT): """Asynchronous version of completion All GPTAPIs are required to provide the standard OpenAI completion interface [ @@ -160,7 +160,7 @@ class BaseLLM(ABC): """ @abstractmethod - async def _achat_completion_stream(self, messages: list[dict], timeout: int = 0) -> str: + async def _achat_completion_stream(self, messages: list[dict], timeout: int = USE_CONFIG_TIMEOUT) -> str: """_achat_completion_stream implemented by inherited class""" @retry( @@ -170,7 +170,9 @@ class BaseLLM(ABC): retry=retry_if_exception_type(ConnectionError), retry_error_callback=log_and_reraise, ) - async def acompletion_text(self, messages: list[dict], stream: bool = False, timeout: int = 0) -> str: + async def acompletion_text( + self, messages: list[dict], stream: bool = False, timeout: int = USE_CONFIG_TIMEOUT + ) -> str: """Asynchronous version of completion. Return str. Support stream-print""" if stream: return await self._achat_completion_stream(messages, timeout=self.get_timeout(timeout)) diff --git a/metagpt/provider/dashscope_api.py b/metagpt/provider/dashscope_api.py index a3efd5116..82224e893 100644 --- a/metagpt/provider/dashscope_api.py +++ b/metagpt/provider/dashscope_api.py @@ -25,6 +25,7 @@ from dashscope.common.error import ( UnsupportedApiProtocol, ) +from metagpt.const import USE_CONFIG_TIMEOUT from metagpt.logs import log_llm_stream from metagpt.provider.base_llm import BaseLLM, LLMConfig from metagpt.provider.llm_provider_registry import LLMType, register_provider @@ -202,16 +203,16 @@ class DashScopeLLM(BaseLLM): self._update_costs(dict(resp.usage)) return resp.output - async def _achat_completion(self, messages: list[dict], timeout: int = 0) -> GenerationOutput: + async def _achat_completion(self, messages: list[dict], timeout: int = USE_CONFIG_TIMEOUT) -> GenerationOutput: resp: GenerationResponse = await self.aclient.acall(**self._const_kwargs(messages, stream=False)) self._check_response(resp) self._update_costs(dict(resp.usage)) return resp.output - async def acompletion(self, messages: list[dict], timeout=0) -> GenerationOutput: + async def acompletion(self, messages: list[dict], timeout=USE_CONFIG_TIMEOUT) -> GenerationOutput: return await self._achat_completion(messages, timeout=self.get_timeout(timeout)) - async def _achat_completion_stream(self, messages: list[dict], timeout: int = 0) -> str: + async def _achat_completion_stream(self, messages: list[dict], timeout: int = USE_CONFIG_TIMEOUT) -> str: resp = await self.aclient.acall(**self._const_kwargs(messages, stream=True)) collected_content = [] usage = {} diff --git a/metagpt/provider/google_gemini_api.py b/metagpt/provider/google_gemini_api.py index 2a00d4b5a..e041f4c87 100644 --- a/metagpt/provider/google_gemini_api.py +++ b/metagpt/provider/google_gemini_api.py @@ -15,6 +15,7 @@ from google.generativeai.types.generation_types import ( ) from metagpt.configs.llm_config import LLMConfig, LLMType +from metagpt.const import USE_CONFIG_TIMEOUT from metagpt.logs import log_llm_stream from metagpt.provider.base_llm import BaseLLM from metagpt.provider.llm_provider_registry import register_provider @@ -88,16 +89,18 @@ class GeminiLLM(BaseLLM): self._update_costs(usage) return resp - async def _achat_completion(self, messages: list[dict], timeout: int = 0) -> "AsyncGenerateContentResponse": + async def _achat_completion( + self, messages: list[dict], timeout: int = USE_CONFIG_TIMEOUT + ) -> "AsyncGenerateContentResponse": resp: AsyncGenerateContentResponse = await self.llm.generate_content_async(**self._const_kwargs(messages)) usage = await self.aget_usage(messages, resp.text) self._update_costs(usage) return resp - async def acompletion(self, messages: list[dict], timeout=0) -> dict: + async def acompletion(self, messages: list[dict], timeout=USE_CONFIG_TIMEOUT) -> dict: return await self._achat_completion(messages, timeout=self.get_timeout(timeout)) - async def _achat_completion_stream(self, messages: list[dict], timeout: int = 0) -> str: + async def _achat_completion_stream(self, messages: list[dict], timeout: int = USE_CONFIG_TIMEOUT) -> str: resp: AsyncGenerateContentResponse = await self.llm.generate_content_async( **self._const_kwargs(messages, stream=True) ) diff --git a/metagpt/provider/human_provider.py b/metagpt/provider/human_provider.py index df63a8bc9..f205ecd1f 100644 --- a/metagpt/provider/human_provider.py +++ b/metagpt/provider/human_provider.py @@ -6,6 +6,7 @@ Author: garylin2099 from typing import Optional from metagpt.configs.llm_config import LLMConfig +from metagpt.const import USE_CONFIG_TIMEOUT from metagpt.logs import logger from metagpt.provider.base_llm import BaseLLM @@ -18,7 +19,7 @@ class HumanProvider(BaseLLM): def __init__(self, config: LLMConfig): pass - def ask(self, msg: str, timeout=0) -> str: + def ask(self, msg: str, timeout=USE_CONFIG_TIMEOUT) -> str: logger.info("It's your turn, please type in your response. You may also refer to the context below") rsp = input(msg) if rsp in ["exit", "quit"]: @@ -31,20 +32,20 @@ class HumanProvider(BaseLLM): system_msgs: Optional[list[str]] = None, format_msgs: Optional[list[dict[str, str]]] = None, generator: bool = False, - timeout=0, + timeout=USE_CONFIG_TIMEOUT, ) -> str: return self.ask(msg, timeout=self.get_timeout(timeout)) - async def _achat_completion(self, messages: list[dict], timeout=0): + async def _achat_completion(self, messages: list[dict], timeout=USE_CONFIG_TIMEOUT): pass - async def acompletion(self, messages: list[dict], timeout=0): + async def acompletion(self, messages: list[dict], timeout=USE_CONFIG_TIMEOUT): """dummy implementation of abstract method in base""" return [] - async def _achat_completion_stream(self, messages: list[dict], timeout: int = 0) -> str: + async def _achat_completion_stream(self, messages: list[dict], timeout: int = USE_CONFIG_TIMEOUT) -> str: pass - async def acompletion_text(self, messages: list[dict], stream=False, timeout=0) -> str: + async def acompletion_text(self, messages: list[dict], stream=False, timeout=USE_CONFIG_TIMEOUT) -> str: """dummy implementation of abstract method in base""" return "" diff --git a/metagpt/provider/ollama_api.py b/metagpt/provider/ollama_api.py index 723abb574..2913eb1dd 100644 --- a/metagpt/provider/ollama_api.py +++ b/metagpt/provider/ollama_api.py @@ -5,6 +5,7 @@ import json from metagpt.configs.llm_config import LLMConfig, LLMType +from metagpt.const import USE_CONFIG_TIMEOUT from metagpt.logs import log_llm_stream from metagpt.provider.base_llm import BaseLLM from metagpt.provider.general_api_requestor import GeneralAPIRequestor @@ -49,7 +50,7 @@ class OllamaLLM(BaseLLM): chunk = chunk.decode(encoding) return json.loads(chunk) - async def _achat_completion(self, messages: list[dict], timeout: int = 0) -> dict: + async def _achat_completion(self, messages: list[dict], timeout: int = USE_CONFIG_TIMEOUT) -> dict: resp, _, _ = await self.client.arequest( method=self.http_method, url=self.suffix_url, @@ -61,10 +62,10 @@ class OllamaLLM(BaseLLM): self._update_costs(usage) return resp - async def acompletion(self, messages: list[dict], timeout=0) -> dict: + async def acompletion(self, messages: list[dict], timeout=USE_CONFIG_TIMEOUT) -> dict: return await self._achat_completion(messages, timeout=self.get_timeout(timeout)) - async def _achat_completion_stream(self, messages: list[dict], timeout: int = 0) -> str: + async def _achat_completion_stream(self, messages: list[dict], timeout: int = USE_CONFIG_TIMEOUT) -> str: stream_resp, _, _ = await self.client.arequest( method=self.http_method, url=self.suffix_url, diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py index 5b4abae61..10b7749d6 100644 --- a/metagpt/provider/openai_api.py +++ b/metagpt/provider/openai_api.py @@ -25,6 +25,7 @@ from tenacity import ( ) from metagpt.configs.llm_config import LLMConfig, LLMType +from metagpt.const import USE_CONFIG_TIMEOUT from metagpt.logs import log_llm_stream, logger from metagpt.provider.base_llm import BaseLLM from metagpt.provider.constant import GENERAL_FUNCTION_SCHEMA @@ -79,7 +80,7 @@ class OpenAILLM(BaseLLM): return params - async def _achat_completion_stream(self, messages: list[dict], timeout=0) -> str: + async def _achat_completion_stream(self, messages: list[dict], timeout=USE_CONFIG_TIMEOUT) -> str: response: AsyncStream[ChatCompletionChunk] = await self.aclient.chat.completions.create( **self._cons_kwargs(messages, timeout=self.get_timeout(timeout)), stream=True ) @@ -109,7 +110,7 @@ class OpenAILLM(BaseLLM): self._update_costs(usage) return full_reply_content - def _cons_kwargs(self, messages: list[dict], timeout=0, **extra_kwargs) -> dict: + def _cons_kwargs(self, messages: list[dict], timeout=USE_CONFIG_TIMEOUT, **extra_kwargs) -> dict: kwargs = { "messages": messages, "max_tokens": self._get_max_tokens(messages), @@ -123,13 +124,13 @@ class OpenAILLM(BaseLLM): kwargs.update(extra_kwargs) return kwargs - async def _achat_completion(self, messages: list[dict], timeout=0) -> ChatCompletion: + async def _achat_completion(self, messages: list[dict], timeout=USE_CONFIG_TIMEOUT) -> ChatCompletion: kwargs = self._cons_kwargs(messages, timeout=self.get_timeout(timeout)) rsp: ChatCompletion = await self.aclient.chat.completions.create(**kwargs) self._update_costs(rsp.usage) return rsp - async def acompletion(self, messages: list[dict], timeout=0) -> ChatCompletion: + async def acompletion(self, messages: list[dict], timeout=USE_CONFIG_TIMEOUT) -> ChatCompletion: return await self._achat_completion(messages, timeout=self.get_timeout(timeout)) @retry( @@ -139,7 +140,7 @@ class OpenAILLM(BaseLLM): retry=retry_if_exception_type(APIConnectionError), retry_error_callback=log_and_reraise, ) - async def acompletion_text(self, messages: list[dict], stream=False, timeout=0) -> str: + async def acompletion_text(self, messages: list[dict], stream=False, timeout=USE_CONFIG_TIMEOUT) -> str: """when streaming, print each token in place.""" if stream: return await self._achat_completion_stream(messages, timeout=timeout) @@ -148,7 +149,7 @@ class OpenAILLM(BaseLLM): return self.get_choice_text(rsp) async def _achat_completion_function( - self, messages: list[dict], timeout: int = 0, **chat_configs + self, messages: list[dict], timeout: int = USE_CONFIG_TIMEOUT, **chat_configs ) -> ChatCompletion: messages = process_message(messages) kwargs = self._cons_kwargs(messages=messages, timeout=self.get_timeout(timeout), **chat_configs) @@ -156,7 +157,7 @@ class OpenAILLM(BaseLLM): self._update_costs(rsp.usage) return rsp - async def aask_code(self, messages: list[dict], timeout: int = 0, **kwargs) -> dict: + async def aask_code(self, messages: list[dict], timeout: int = USE_CONFIG_TIMEOUT, **kwargs) -> dict: """Use function of tools to ask a code. Note: Keep kwargs consistent with https://platform.openai.com/docs/api-reference/chat/create diff --git a/metagpt/provider/qianfan_api.py b/metagpt/provider/qianfan_api.py index 7e0bf009e..3d78c8bfc 100644 --- a/metagpt/provider/qianfan_api.py +++ b/metagpt/provider/qianfan_api.py @@ -9,6 +9,7 @@ from qianfan import ChatCompletion from qianfan.resources.typing import JsonBody from metagpt.configs.llm_config import LLMConfig, LLMType +from metagpt.const import USE_CONFIG_TIMEOUT from metagpt.logs import log_llm_stream from metagpt.provider.base_llm import BaseLLM from metagpt.provider.llm_provider_registry import register_provider @@ -107,15 +108,15 @@ class QianFanLLM(BaseLLM): self._update_costs(resp.body.get("usage", {})) return resp.body - async def _achat_completion(self, messages: list[dict], timeout: int = 0) -> JsonBody: + async def _achat_completion(self, messages: list[dict], timeout: int = USE_CONFIG_TIMEOUT) -> JsonBody: resp = await self.aclient.ado(**self._const_kwargs(messages=messages, stream=False)) self._update_costs(resp.body.get("usage", {})) return resp.body - async def acompletion(self, messages: list[dict], timeout: int = 0) -> JsonBody: + async def acompletion(self, messages: list[dict], timeout: int = USE_CONFIG_TIMEOUT) -> JsonBody: return await self._achat_completion(messages, timeout=self.get_timeout(timeout)) - async def _achat_completion_stream(self, messages: list[dict], timeout: int = 0) -> str: + async def _achat_completion_stream(self, messages: list[dict], timeout: int = USE_CONFIG_TIMEOUT) -> str: resp = await self.aclient.ado(**self._const_kwargs(messages=messages, stream=True)) collected_content = [] usage = {} diff --git a/metagpt/provider/spark_api.py b/metagpt/provider/spark_api.py index 0f450ccf5..594267259 100644 --- a/metagpt/provider/spark_api.py +++ b/metagpt/provider/spark_api.py @@ -17,6 +17,7 @@ from wsgiref.handlers import format_date_time import websocket # 使用websocket_client from metagpt.configs.llm_config import LLMConfig, LLMType +from metagpt.const import USE_CONFIG_TIMEOUT from metagpt.logs import logger from metagpt.provider.base_llm import BaseLLM from metagpt.provider.llm_provider_registry import register_provider @@ -31,19 +32,19 @@ class SparkLLM(BaseLLM): def get_choice_text(self, rsp: dict) -> str: return rsp["payload"]["choices"]["text"][-1]["content"] - async def _achat_completion_stream(self, messages: list[dict], timeout: int = 0) -> str: + async def _achat_completion_stream(self, messages: list[dict], timeout: int = USE_CONFIG_TIMEOUT) -> str: pass - async def acompletion_text(self, messages: list[dict], stream=False, timeout: int = 0) -> str: + async def acompletion_text(self, messages: list[dict], stream=False, timeout: int = USE_CONFIG_TIMEOUT) -> str: # 不支持 # logger.warning("当前方法无法支持异步运行。当你使用acompletion时,并不能并行访问。") w = GetMessageFromWeb(messages, self.config) return w.run() - async def _achat_completion(self, messages: list[dict], timeout=0): + async def _achat_completion(self, messages: list[dict], timeout=USE_CONFIG_TIMEOUT): pass - async def acompletion(self, messages: list[dict], timeout=0): + async def acompletion(self, messages: list[dict], timeout=USE_CONFIG_TIMEOUT): # 不支持异步 w = GetMessageFromWeb(messages, self.config) return w.run() diff --git a/metagpt/provider/zhipuai_api.py b/metagpt/provider/zhipuai_api.py index 8c5284770..2db441991 100644 --- a/metagpt/provider/zhipuai_api.py +++ b/metagpt/provider/zhipuai_api.py @@ -8,6 +8,7 @@ from typing import Optional from zhipuai.types.chat.chat_completion import Completion from metagpt.configs.llm_config import LLMConfig, LLMType +from metagpt.const import USE_CONFIG_TIMEOUT from metagpt.logs import log_llm_stream from metagpt.provider.base_llm import BaseLLM from metagpt.provider.llm_provider_registry import register_provider @@ -45,22 +46,22 @@ class ZhiPuAILLM(BaseLLM): kwargs = {"model": self.model, "messages": messages, "stream": stream, "temperature": 0.3} return kwargs - def completion(self, messages: list[dict], timeout=0) -> dict: + def completion(self, messages: list[dict], timeout=USE_CONFIG_TIMEOUT) -> dict: resp: Completion = self.llm.chat.completions.create(**self._const_kwargs(messages)) usage = resp.usage.model_dump() self._update_costs(usage) return resp.model_dump() - async def _achat_completion(self, messages: list[dict], timeout=0) -> dict: + async def _achat_completion(self, messages: list[dict], timeout=USE_CONFIG_TIMEOUT) -> dict: resp = await self.llm.acreate(**self._const_kwargs(messages)) usage = resp.get("usage", {}) self._update_costs(usage) return resp - async def acompletion(self, messages: list[dict], timeout=0) -> dict: + async def acompletion(self, messages: list[dict], timeout=USE_CONFIG_TIMEOUT) -> dict: return await self._achat_completion(messages, timeout=self.get_timeout(timeout)) - async def _achat_completion_stream(self, messages: list[dict], timeout=0) -> str: + async def _achat_completion_stream(self, messages: list[dict], timeout=USE_CONFIG_TIMEOUT) -> str: response = await self.llm.acreate_stream(**self._const_kwargs(messages, stream=True)) collected_content = [] usage = {} From fcb2ea48638d1890faf277b87acc2e63e59521c5 Mon Sep 17 00:00:00 2001 From: geekan Date: Thu, 21 Mar 2024 13:57:32 +0800 Subject: [PATCH 109/131] add params to search engine --- examples/search_with_specific_engine.py | 2 +- metagpt/configs/search_config.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/search_with_specific_engine.py b/examples/search_with_specific_engine.py index 1eee762d5..48bcc67b8 100644 --- a/examples/search_with_specific_engine.py +++ b/examples/search_with_specific_engine.py @@ -13,7 +13,7 @@ async def main(): question = "What are the most interesting human facts?" search = Config.default().search - kwargs = {"api_key": search.api_key, "cse_id": search.cse_id, "proxy": None} + kwargs = {"api_key": search.api_key, "cse_id": search.cse_id, "proxy": None, "params": search.params} await Searcher(search_engine=SearchEngine(engine=search.api_type, **kwargs)).run(question) diff --git a/metagpt/configs/search_config.py b/metagpt/configs/search_config.py index af928b02a..38af4d91f 100644 --- a/metagpt/configs/search_config.py +++ b/metagpt/configs/search_config.py @@ -18,3 +18,4 @@ class SearchConfig(YamlModel): api_key: str = "" cse_id: str = "" # for google search_func: Optional[Callable] = None + params: dict = {} From 8486e520485359410a7560b23ee39895fdae6de2 Mon Sep 17 00:00:00 2001 From: geekan Date: Thu, 21 Mar 2024 14:52:17 +0800 Subject: [PATCH 110/131] add params to search engine --- metagpt/actions/action_node.py | 2 +- metagpt/configs/search_config.py | 11 ++++++++++- tests/metagpt/tools/test_search_engine.py | 4 ---- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/metagpt/actions/action_node.py b/metagpt/actions/action_node.py index 09da4a988..59559ad04 100644 --- a/metagpt/actions/action_node.py +++ b/metagpt/actions/action_node.py @@ -330,7 +330,7 @@ class ActionNode: def compile_to(self, i: Dict, schema, kv_sep) -> str: if schema == "json": - return json.dumps(i, indent=4) + return json.dumps(i, indent=4, ensure_ascii=False) elif schema == "markdown": return dict_to_markdown(i, kv_sep=kv_sep) else: diff --git a/metagpt/configs/search_config.py b/metagpt/configs/search_config.py index 38af4d91f..e28b14c99 100644 --- a/metagpt/configs/search_config.py +++ b/metagpt/configs/search_config.py @@ -7,6 +7,8 @@ """ from typing import Callable, Optional +from pydantic import Field + from metagpt.tools import SearchEngineType from metagpt.utils.yaml_model import YamlModel @@ -18,4 +20,11 @@ class SearchConfig(YamlModel): api_key: str = "" cse_id: str = "" # for google search_func: Optional[Callable] = None - params: dict = {} + params: dict = Field( + default_factory=lambda: { + "engine": "google", + "google_domain": "google.com", + "gl": "us", + "hl": "en", + } + ) diff --git a/tests/metagpt/tools/test_search_engine.py b/tests/metagpt/tools/test_search_engine.py index a1f03ef7b..964ead02f 100644 --- a/tests/metagpt/tools/test_search_engine.py +++ b/tests/metagpt/tools/test_search_engine.py @@ -11,7 +11,6 @@ from typing import Callable import pytest -from metagpt.config2 import config from metagpt.configs.search_config import SearchConfig from metagpt.logs import logger from metagpt.tools import SearchEngineType @@ -53,14 +52,11 @@ async def test_search_engine( search_engine_config = {"engine": search_engine_type, "run_func": run_func} if search_engine_type is SearchEngineType.SERPAPI_GOOGLE: - assert config.search search_engine_config["api_key"] = "mock-serpapi-key" elif search_engine_type is SearchEngineType.DIRECT_GOOGLE: - assert config.search search_engine_config["api_key"] = "mock-google-key" search_engine_config["cse_id"] = "mock-google-cse" elif search_engine_type is SearchEngineType.SERPER_GOOGLE: - assert config.search search_engine_config["api_key"] = "mock-serper-key" async def test(search_engine): From 8cda8c6f1b0f35e90ecca9467a3ee346b441860b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 21 Mar 2024 14:54:14 +0800 Subject: [PATCH 111/131] fixbug: reset ocr_receipt.py --- examples/di/ocr_receipt.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/di/ocr_receipt.py b/examples/di/ocr_receipt.py index 8b48be4f1..6b969ee99 100644 --- a/examples/di/ocr_receipt.py +++ b/examples/di/ocr_receipt.py @@ -6,7 +6,9 @@ async def main(): image_path = "image.jpg" language = "English" requirement = f"""This is a {language} receipt image. - Your goal is to perform OCR on images using PaddleOCR, then extract the total amount from ocr text results, and finally save as table. Image path: {image_path}. + Your goal is to perform OCR on images using PaddleOCR, output text content from the OCR results and discard + coordinates and confidence levels, then recognize the total amount from ocr text content, and finally save as table. + Image path: {image_path}. NOTE: The environments for Paddle and PaddleOCR are all ready and has been fully installed.""" di = DataInterpreter() From 587c13489320c62c8958f57bea54842e5a993614 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 21 Mar 2024 14:55:00 +0800 Subject: [PATCH 112/131] fixbug: reset ocr_receipt.py --- examples/di/ocr_receipt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/di/ocr_receipt.py b/examples/di/ocr_receipt.py index 6b969ee99..af54d519b 100644 --- a/examples/di/ocr_receipt.py +++ b/examples/di/ocr_receipt.py @@ -8,7 +8,7 @@ async def main(): requirement = f"""This is a {language} receipt image. Your goal is to perform OCR on images using PaddleOCR, output text content from the OCR results and discard coordinates and confidence levels, then recognize the total amount from ocr text content, and finally save as table. - Image path: {image_path}. + Image path: {image_path}. NOTE: The environments for Paddle and PaddleOCR are all ready and has been fully installed.""" di = DataInterpreter() From f051104bce125539f4ea24598b4b35a9e2c0ae69 Mon Sep 17 00:00:00 2001 From: geekan Date: Thu, 21 Mar 2024 14:55:42 +0800 Subject: [PATCH 113/131] simplify code --- examples/search_with_specific_engine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/search_with_specific_engine.py b/examples/search_with_specific_engine.py index 48bcc67b8..276431ed8 100644 --- a/examples/search_with_specific_engine.py +++ b/examples/search_with_specific_engine.py @@ -13,7 +13,7 @@ async def main(): question = "What are the most interesting human facts?" search = Config.default().search - kwargs = {"api_key": search.api_key, "cse_id": search.cse_id, "proxy": None, "params": search.params} + kwargs = search.model_dump() await Searcher(search_engine=SearchEngine(engine=search.api_type, **kwargs)).run(question) From 9cfcfb1ea8bae086ed4fc5fe9d8365038c95e89e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 21 Mar 2024 15:14:14 +0800 Subject: [PATCH 114/131] feat: use --gitfile --- .gitignore | 2 +- metagpt/utils/tree.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 6bc67fa61..1542bbb98 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ ### Python template # Byte-compiled / optimized / DLL files -__pycache__/ +__pycache__ *.py[cod] *$py.class diff --git a/metagpt/utils/tree.py b/metagpt/utils/tree.py index fbf085e48..bd7922290 100644 --- a/metagpt/utils/tree.py +++ b/metagpt/utils/tree.py @@ -130,7 +130,7 @@ def _add_line(rows: List[str]) -> List[str]: def _execute_tree(root: Path, gitignore: str | Path) -> str: - args = ["--gitignore", str(gitignore)] if gitignore else [] + args = ["--gitfile", str(gitignore)] if gitignore else [] try: result = subprocess.run(["tree"] + args + [str(root)], capture_output=True, text=True, check=True) if result.returncode != 0: From 12551ab60cc9ee0e59cf31a1a29c24e13d9e7abd Mon Sep 17 00:00:00 2001 From: better629 Date: Thu, 21 Mar 2024 21:57:59 +0800 Subject: [PATCH 115/131] fix state value extract for https://github.com/geekan/MetaGPT/issues/1067 --- metagpt/utils/repair_llm_raw_output.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/metagpt/utils/repair_llm_raw_output.py b/metagpt/utils/repair_llm_raw_output.py index b8756e8c6..17e095c5f 100644 --- a/metagpt/utils/repair_llm_raw_output.py +++ b/metagpt/utils/repair_llm_raw_output.py @@ -340,7 +340,9 @@ def extract_state_value_from_output(content: str) -> str: content (str): llm's output from `Role._think` """ content = content.strip() # deal the output cases like " 0", "0\n" and so on. - pattern = r"([0-9])" # TODO find the number using a more proper method not just extract from content using pattern + pattern = ( + r"(? 0 else "-1" From e88aa63bf0f49e3e90a33bf818a91f45ba950752 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Fri, 22 Mar 2024 10:28:11 +0800 Subject: [PATCH 116/131] make the rag module optional --- requirements.txt | 8 -------- setup.py | 11 ++++++++++- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/requirements.txt b/requirements.txt index 83565278b..a447eef13 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,14 +10,6 @@ typer==0.9.0 # godot==0.1.1 # google_api_python_client==2.93.0 # Used by search_engine.py lancedb==0.4.0 -llama-index-core==0.10.15 -llama-index-embeddings-azure-openai==0.1.6 -llama-index-embeddings-openai==0.1.5 -llama-index-llms-azure-openai==0.1.4 -llama-index-readers-file==0.1.4 -llama-index-retrievers-bm25==0.1.3 -llama-index-vector-stores-faiss==0.1.1 -chromadb==0.4.23 loguru==0.6.0 meilisearch==0.21.0 numpy==1.24.3 diff --git a/setup.py b/setup.py index df9bedc9b..f834b4c44 100644 --- a/setup.py +++ b/setup.py @@ -28,6 +28,16 @@ extras_require = { "search-google": ["google-api-python-client==2.94.0"], "search-ddg": ["duckduckgo-search~=4.1.1"], "ocr": ["paddlepaddle==2.4.2", "paddleocr>=2.0.1", "tabulate==0.9.0"], + "rag": [ + "llama-index-core==0.10.15", + "llama-index-embeddings-azure-openai==0.1.6", + "llama-index-embeddings-openai==0.1.5", + "llama-index-llms-azure-openai==0.1.4", + "llama-index-readers-file==0.1.4", + "llama-index-retrievers-bm25==0.1.3", + "llama-index-vector-stores-faiss==0.1.1", + "chromadb==0.4.23", + ], } extras_require["test"] = [ @@ -42,7 +52,6 @@ extras_require["test"] = [ "connexion[uvicorn]~=3.0.5", "azure-cognitiveservices-speech~=1.31.0", "aioboto3~=11.3.0", - "chromadb==0.4.23", "gradio==3.0.0", "grpcio-status==1.48.2", "pylint==3.0.3", From bac81ee64b82feae76ef07cf8068a26d89974ca3 Mon Sep 17 00:00:00 2001 From: geekan Date: Fri, 22 Mar 2024 10:58:22 +0800 Subject: [PATCH 117/131] add custom tool example --- examples/di/custom_tool.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 examples/di/custom_tool.py diff --git a/examples/di/custom_tool.py b/examples/di/custom_tool.py new file mode 100644 index 000000000..3f955529c --- /dev/null +++ b/examples/di/custom_tool.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@Time : 2024/3/22 10:54 +@Author : alexanderwu +@File : custom_tool.py +""" + +from metagpt.roles.di.data_interpreter import DataInterpreter +from metagpt.tools.tool_registry import register_tool + + +@register_tool() +def magic_function(arg1: str, arg2: int) -> dict: + """ + The magic function that does something. + + Args: + arg1 (str): ... + arg2 (int): ... + + Returns: + dict: ... + """ + return {"arg1": arg1 * 3, "arg2": arg2 * 5} + + +async def main(): + di = DataInterpreter(tools=["magic_function"]) + await di.run("Just call the magic function with arg1 'A' and arg2 'B'. Tell me the result.") + + +if __name__ == "__main__": + import asyncio + + asyncio.run(main()) From aea06852bad093aa0bb1f68dacba7d5384d9c429 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Fri, 22 Mar 2024 11:15:31 +0800 Subject: [PATCH 118/131] make the rag module optional --- metagpt/environment/__init__.py | 3 +-- metagpt/roles/__init__.py | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/metagpt/environment/__init__.py b/metagpt/environment/__init__.py index 692672fa7..28981f2f8 100644 --- a/metagpt/environment/__init__.py +++ b/metagpt/environment/__init__.py @@ -4,10 +4,9 @@ from metagpt.environment.base_env import Environment from metagpt.environment.android_env.android_env import AndroidEnv -from metagpt.environment.mincraft_env.mincraft_env import MincraftExtEnv from metagpt.environment.werewolf_env.werewolf_env import WerewolfEnv from metagpt.environment.stanford_town_env.stanford_town_env import StanfordTownEnv from metagpt.environment.software_env.software_env import SoftwareEnv -__all__ = ["AndroidEnv", "MincraftExtEnv", "WerewolfEnv", "StanfordTownEnv", "SoftwareEnv", "Environment"] +__all__ = ["AndroidEnv", "WerewolfEnv", "StanfordTownEnv", "SoftwareEnv", "Environment"] diff --git a/metagpt/roles/__init__.py b/metagpt/roles/__init__.py index f033a5dfa..08a0406b3 100644 --- a/metagpt/roles/__init__.py +++ b/metagpt/roles/__init__.py @@ -14,7 +14,6 @@ from metagpt.roles.engineer import Engineer from metagpt.roles.qa_engineer import QaEngineer from metagpt.roles.searcher import Searcher from metagpt.roles.sales import Sales -from metagpt.roles.customer_service import CustomerService __all__ = [ @@ -26,5 +25,4 @@ __all__ = [ "QaEngineer", "Searcher", "Sales", - "CustomerService", ] From 0c27be629b1f9b38a414b055b77ded284ba33a01 Mon Sep 17 00:00:00 2001 From: geekan Date: Fri, 22 Mar 2024 11:19:20 +0800 Subject: [PATCH 119/131] add custom tool example --- examples/di/custom_tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/di/custom_tool.py b/examples/di/custom_tool.py index 3f955529c..cbe7380c7 100644 --- a/examples/di/custom_tool.py +++ b/examples/di/custom_tool.py @@ -27,7 +27,7 @@ def magic_function(arg1: str, arg2: int) -> dict: async def main(): di = DataInterpreter(tools=["magic_function"]) - await di.run("Just call the magic function with arg1 'A' and arg2 'B'. Tell me the result.") + await di.run("Just call the magic function with arg1 'A' and arg2 2. Tell me the result.") if __name__ == "__main__": From bf44a699585a3bddfd8665f565c8f1c6b853f109 Mon Sep 17 00:00:00 2001 From: geekan Date: Fri, 22 Mar 2024 11:25:22 +0800 Subject: [PATCH 120/131] fix minecraft typo --- metagpt/environment/README.md | 2 +- metagpt/environment/__init__.py | 4 ++-- metagpt/environment/base_env.py | 2 +- .../{mincraft_env => minecraft_env}/__init__.py | 0 .../{mincraft_env => minecraft_env}/const.py | 4 ++-- .../mincraft_env.py => minecraft_env/minecraft_env.py} | 10 +++++----- .../minecraft_ext_env.py} | 10 +++++----- .../mineflayer/.gitignore | 0 .../mineflayer/.prettierignore | 0 .../mineflayer/.prettierrc.json | 0 .../mineflayer/index.js | 0 .../mineflayer/lib/observation/base.js | 0 .../mineflayer/lib/observation/chests.js | 0 .../mineflayer/lib/observation/inventory.js | 0 .../mineflayer/lib/observation/onChat.js | 0 .../mineflayer/lib/observation/onError.js | 0 .../mineflayer/lib/observation/onSave.js | 0 .../mineflayer/lib/observation/status.js | 0 .../mineflayer/lib/observation/voxels.js | 0 .../mineflayer/lib/skillLoader.js | 0 .../mineflayer/lib/utils.js | 0 .../mineflayer/mineflayer-collectblock/.gitignore | 0 .../mineflayer/mineflayer-collectblock/LICENSE | 0 .../mineflayer/mineflayer-collectblock/README.md | 0 .../mineflayer/mineflayer-collectblock/_config.yml | 0 .../mineflayer/mineflayer-collectblock/docs/api.md | 0 .../mineflayer-collectblock/examples/collector.js | 0 .../mineflayer-collectblock/examples/oreMiner.js | 0 .../mineflayer-collectblock/examples/storageBot.js | 0 .../mineflayer/mineflayer-collectblock/package.json | 0 .../mineflayer-collectblock/src/BlockVeins.ts | 0 .../mineflayer-collectblock/src/CollectBlock.ts | 0 .../mineflayer-collectblock/src/Inventory.ts | 0 .../mineflayer/mineflayer-collectblock/src/Targets.ts | 0 .../mineflayer-collectblock/src/TaskQueue.ts | 0 .../mineflayer-collectblock/src/TemporarySubscriber.ts | 0 .../mineflayer/mineflayer-collectblock/src/Util.ts | 0 .../mineflayer/mineflayer-collectblock/src/index.ts | 0 .../mineflayer/mineflayer-collectblock/tsconfig.json | 0 .../mineflayer/package.json | 0 .../{mincraft_env => minecraft_env}/process_monitor.py | 0 metagpt/utils/common.py | 2 +- .../environment/mincraft_env/test_mincraft_ext_env.py | 10 +++++----- 43 files changed, 22 insertions(+), 22 deletions(-) rename metagpt/environment/{mincraft_env => minecraft_env}/__init__.py (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/const.py (92%) rename metagpt/environment/{mincraft_env/mincraft_env.py => minecraft_env/minecraft_env.py} (98%) rename metagpt/environment/{mincraft_env/mincraft_ext_env.py => minecraft_env/minecraft_ext_env.py} (95%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/.gitignore (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/.prettierignore (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/.prettierrc.json (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/index.js (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/lib/observation/base.js (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/lib/observation/chests.js (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/lib/observation/inventory.js (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/lib/observation/onChat.js (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/lib/observation/onError.js (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/lib/observation/onSave.js (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/lib/observation/status.js (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/lib/observation/voxels.js (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/lib/skillLoader.js (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/lib/utils.js (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/mineflayer-collectblock/.gitignore (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/mineflayer-collectblock/LICENSE (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/mineflayer-collectblock/README.md (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/mineflayer-collectblock/_config.yml (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/mineflayer-collectblock/docs/api.md (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/mineflayer-collectblock/examples/collector.js (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/mineflayer-collectblock/examples/oreMiner.js (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/mineflayer-collectblock/examples/storageBot.js (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/mineflayer-collectblock/package.json (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/mineflayer-collectblock/src/BlockVeins.ts (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/mineflayer-collectblock/src/CollectBlock.ts (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/mineflayer-collectblock/src/Inventory.ts (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/mineflayer-collectblock/src/Targets.ts (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/mineflayer-collectblock/src/TaskQueue.ts (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/mineflayer-collectblock/src/TemporarySubscriber.ts (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/mineflayer-collectblock/src/Util.ts (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/mineflayer-collectblock/src/index.ts (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/mineflayer-collectblock/tsconfig.json (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/mineflayer/package.json (100%) rename metagpt/environment/{mincraft_env => minecraft_env}/process_monitor.py (100%) diff --git a/metagpt/environment/README.md b/metagpt/environment/README.md index 9476ac75a..ff56b73f1 100644 --- a/metagpt/environment/README.md +++ b/metagpt/environment/README.md @@ -34,5 +34,5 @@ # do a `tap` action on the screen ## TODO - add android app operation assistant under `examples/android_assistant` - migrate roles/actions of werewolf game from old version into current version -- migrate roles/actions of mincraft game from old version into current version +- migrate roles/actions of minecraft game from old version into current version - migrate roles/actions of stanford_town game from old version into current version diff --git a/metagpt/environment/__init__.py b/metagpt/environment/__init__.py index 692672fa7..a844827d8 100644 --- a/metagpt/environment/__init__.py +++ b/metagpt/environment/__init__.py @@ -4,10 +4,10 @@ from metagpt.environment.base_env import Environment from metagpt.environment.android_env.android_env import AndroidEnv -from metagpt.environment.mincraft_env.mincraft_env import MincraftExtEnv +from metagpt.environment.minecraft_env.minecraft_env import MinecraftExtEnv from metagpt.environment.werewolf_env.werewolf_env import WerewolfEnv from metagpt.environment.stanford_town_env.stanford_town_env import StanfordTownEnv from metagpt.environment.software_env.software_env import SoftwareEnv -__all__ = ["AndroidEnv", "MincraftExtEnv", "WerewolfEnv", "StanfordTownEnv", "SoftwareEnv", "Environment"] +__all__ = ["AndroidEnv", "MinecraftExtEnv", "WerewolfEnv", "StanfordTownEnv", "SoftwareEnv", "Environment"] diff --git a/metagpt/environment/base_env.py b/metagpt/environment/base_env.py index 14023e3b7..942bf2409 100644 --- a/metagpt/environment/base_env.py +++ b/metagpt/environment/base_env.py @@ -26,7 +26,7 @@ class EnvType(Enum): ANDROID = "Android" GYM = "Gym" WEREWOLF = "Werewolf" - MINCRAFT = "Mincraft" + MINECRAFT = "Minecraft" STANFORDTOWN = "StanfordTown" diff --git a/metagpt/environment/mincraft_env/__init__.py b/metagpt/environment/minecraft_env/__init__.py similarity index 100% rename from metagpt/environment/mincraft_env/__init__.py rename to metagpt/environment/minecraft_env/__init__.py diff --git a/metagpt/environment/mincraft_env/const.py b/metagpt/environment/minecraft_env/const.py similarity index 92% rename from metagpt/environment/mincraft_env/const.py rename to metagpt/environment/minecraft_env/const.py index a7222f9cd..8ac15decc 100644 --- a/metagpt/environment/mincraft_env/const.py +++ b/metagpt/environment/minecraft_env/const.py @@ -4,8 +4,8 @@ from metagpt.const import METAGPT_ROOT -# For Mincraft Game Agent -MC_CKPT_DIR = METAGPT_ROOT / "data/mincraft/ckpt" +# For Minecraft Game Agent +MC_CKPT_DIR = METAGPT_ROOT / "data/minecraft/ckpt" MC_LOG_DIR = METAGPT_ROOT / "logs" MC_DEFAULT_WARMUP = { "context": 15, diff --git a/metagpt/environment/mincraft_env/mincraft_env.py b/metagpt/environment/minecraft_env/minecraft_env.py similarity index 98% rename from metagpt/environment/mincraft_env/mincraft_env.py rename to metagpt/environment/minecraft_env/minecraft_env.py index fdc477164..26d4d03a8 100644 --- a/metagpt/environment/mincraft_env/mincraft_env.py +++ b/metagpt/environment/minecraft_env/minecraft_env.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# @Desc : MG Mincraft Env +# @Desc : MG Minecraft Env # refs to `voyager voyager.py` import json @@ -12,15 +12,15 @@ from pydantic import ConfigDict, Field from metagpt.config2 import config as CONFIG from metagpt.environment.base_env import Environment -from metagpt.environment.mincraft_env.const import MC_CKPT_DIR -from metagpt.environment.mincraft_env.mincraft_ext_env import MincraftExtEnv +from metagpt.environment.minecraft_env.const import MC_CKPT_DIR +from metagpt.environment.minecraft_env.minecraft_ext_env import MinecraftExtEnv from metagpt.logs import logger from metagpt.rag.vector_stores.chroma import ChromaVectorStore from metagpt.utils.common import load_mc_skills_code, read_json_file, write_json_file -class MincraftEnv(Environment, MincraftExtEnv): - """MincraftEnv, including shared memory of cache and information between roles""" +class MinecraftEnv(Environment, MinecraftExtEnv): + """MinecraftEnv, including shared memory of cache and information between roles""" model_config = ConfigDict(arbitrary_types_allowed=True) diff --git a/metagpt/environment/mincraft_env/mincraft_ext_env.py b/metagpt/environment/minecraft_env/minecraft_ext_env.py similarity index 95% rename from metagpt/environment/mincraft_env/mincraft_ext_env.py rename to metagpt/environment/minecraft_env/minecraft_ext_env.py index b86250d8c..3b793079f 100644 --- a/metagpt/environment/mincraft_env/mincraft_ext_env.py +++ b/metagpt/environment/minecraft_env/minecraft_ext_env.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# @Desc : The Mincraft external environment to integrate with Mincraft game +# @Desc : The Minecraft external environment to integrate with Minecraft game # refs to `voyager bridge.py` import json @@ -11,18 +11,18 @@ import requests from pydantic import ConfigDict, Field, model_validator from metagpt.environment.base_env import ExtEnv, mark_as_writeable -from metagpt.environment.mincraft_env.const import ( +from metagpt.environment.minecraft_env.const import ( MC_CKPT_DIR, MC_CORE_INVENTORY_ITEMS, MC_CURRICULUM_OB, MC_DEFAULT_WARMUP, METAGPT_ROOT, ) -from metagpt.environment.mincraft_env.process_monitor import SubprocessMonitor +from metagpt.environment.minecraft_env.process_monitor import SubprocessMonitor from metagpt.logs import logger -class MincraftExtEnv(ExtEnv): +class MinecraftExtEnv(ExtEnv): model_config = ConfigDict(arbitrary_types_allowed=True) mc_port: Optional[int] = Field(default=None) @@ -48,7 +48,7 @@ class MincraftExtEnv(ExtEnv): self.mineflayer = SubprocessMonitor( commands=[ "node", - METAGPT_ROOT.joinpath("metagpt", "environment", "mincraft_env", "mineflayer", "index.js"), + METAGPT_ROOT.joinpath("metagpt", "environment", "minecraft_env", "mineflayer", "index.js"), str(self.server_port), ], name="mineflayer", diff --git a/metagpt/environment/mincraft_env/mineflayer/.gitignore b/metagpt/environment/minecraft_env/mineflayer/.gitignore similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/.gitignore rename to metagpt/environment/minecraft_env/mineflayer/.gitignore diff --git a/metagpt/environment/mincraft_env/mineflayer/.prettierignore b/metagpt/environment/minecraft_env/mineflayer/.prettierignore similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/.prettierignore rename to metagpt/environment/minecraft_env/mineflayer/.prettierignore diff --git a/metagpt/environment/mincraft_env/mineflayer/.prettierrc.json b/metagpt/environment/minecraft_env/mineflayer/.prettierrc.json similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/.prettierrc.json rename to metagpt/environment/minecraft_env/mineflayer/.prettierrc.json diff --git a/metagpt/environment/mincraft_env/mineflayer/index.js b/metagpt/environment/minecraft_env/mineflayer/index.js similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/index.js rename to metagpt/environment/minecraft_env/mineflayer/index.js diff --git a/metagpt/environment/mincraft_env/mineflayer/lib/observation/base.js b/metagpt/environment/minecraft_env/mineflayer/lib/observation/base.js similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/lib/observation/base.js rename to metagpt/environment/minecraft_env/mineflayer/lib/observation/base.js diff --git a/metagpt/environment/mincraft_env/mineflayer/lib/observation/chests.js b/metagpt/environment/minecraft_env/mineflayer/lib/observation/chests.js similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/lib/observation/chests.js rename to metagpt/environment/minecraft_env/mineflayer/lib/observation/chests.js diff --git a/metagpt/environment/mincraft_env/mineflayer/lib/observation/inventory.js b/metagpt/environment/minecraft_env/mineflayer/lib/observation/inventory.js similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/lib/observation/inventory.js rename to metagpt/environment/minecraft_env/mineflayer/lib/observation/inventory.js diff --git a/metagpt/environment/mincraft_env/mineflayer/lib/observation/onChat.js b/metagpt/environment/minecraft_env/mineflayer/lib/observation/onChat.js similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/lib/observation/onChat.js rename to metagpt/environment/minecraft_env/mineflayer/lib/observation/onChat.js diff --git a/metagpt/environment/mincraft_env/mineflayer/lib/observation/onError.js b/metagpt/environment/minecraft_env/mineflayer/lib/observation/onError.js similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/lib/observation/onError.js rename to metagpt/environment/minecraft_env/mineflayer/lib/observation/onError.js diff --git a/metagpt/environment/mincraft_env/mineflayer/lib/observation/onSave.js b/metagpt/environment/minecraft_env/mineflayer/lib/observation/onSave.js similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/lib/observation/onSave.js rename to metagpt/environment/minecraft_env/mineflayer/lib/observation/onSave.js diff --git a/metagpt/environment/mincraft_env/mineflayer/lib/observation/status.js b/metagpt/environment/minecraft_env/mineflayer/lib/observation/status.js similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/lib/observation/status.js rename to metagpt/environment/minecraft_env/mineflayer/lib/observation/status.js diff --git a/metagpt/environment/mincraft_env/mineflayer/lib/observation/voxels.js b/metagpt/environment/minecraft_env/mineflayer/lib/observation/voxels.js similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/lib/observation/voxels.js rename to metagpt/environment/minecraft_env/mineflayer/lib/observation/voxels.js diff --git a/metagpt/environment/mincraft_env/mineflayer/lib/skillLoader.js b/metagpt/environment/minecraft_env/mineflayer/lib/skillLoader.js similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/lib/skillLoader.js rename to metagpt/environment/minecraft_env/mineflayer/lib/skillLoader.js diff --git a/metagpt/environment/mincraft_env/mineflayer/lib/utils.js b/metagpt/environment/minecraft_env/mineflayer/lib/utils.js similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/lib/utils.js rename to metagpt/environment/minecraft_env/mineflayer/lib/utils.js diff --git a/metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/.gitignore b/metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/.gitignore similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/.gitignore rename to metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/.gitignore diff --git a/metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/LICENSE b/metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/LICENSE similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/LICENSE rename to metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/LICENSE diff --git a/metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/README.md b/metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/README.md similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/README.md rename to metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/README.md diff --git a/metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/_config.yml b/metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/_config.yml similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/_config.yml rename to metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/_config.yml diff --git a/metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/docs/api.md b/metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/docs/api.md similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/docs/api.md rename to metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/docs/api.md diff --git a/metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/examples/collector.js b/metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/examples/collector.js similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/examples/collector.js rename to metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/examples/collector.js diff --git a/metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/examples/oreMiner.js b/metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/examples/oreMiner.js similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/examples/oreMiner.js rename to metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/examples/oreMiner.js diff --git a/metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/examples/storageBot.js b/metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/examples/storageBot.js similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/examples/storageBot.js rename to metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/examples/storageBot.js diff --git a/metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/package.json b/metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/package.json similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/package.json rename to metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/package.json diff --git a/metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/src/BlockVeins.ts b/metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/src/BlockVeins.ts similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/src/BlockVeins.ts rename to metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/src/BlockVeins.ts diff --git a/metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/src/CollectBlock.ts b/metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/src/CollectBlock.ts similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/src/CollectBlock.ts rename to metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/src/CollectBlock.ts diff --git a/metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/src/Inventory.ts b/metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/src/Inventory.ts similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/src/Inventory.ts rename to metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/src/Inventory.ts diff --git a/metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/src/Targets.ts b/metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/src/Targets.ts similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/src/Targets.ts rename to metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/src/Targets.ts diff --git a/metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/src/TaskQueue.ts b/metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/src/TaskQueue.ts similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/src/TaskQueue.ts rename to metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/src/TaskQueue.ts diff --git a/metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/src/TemporarySubscriber.ts b/metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/src/TemporarySubscriber.ts similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/src/TemporarySubscriber.ts rename to metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/src/TemporarySubscriber.ts diff --git a/metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/src/Util.ts b/metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/src/Util.ts similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/src/Util.ts rename to metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/src/Util.ts diff --git a/metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/src/index.ts b/metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/src/index.ts similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/src/index.ts rename to metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/src/index.ts diff --git a/metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/tsconfig.json b/metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/tsconfig.json similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/mineflayer-collectblock/tsconfig.json rename to metagpt/environment/minecraft_env/mineflayer/mineflayer-collectblock/tsconfig.json diff --git a/metagpt/environment/mincraft_env/mineflayer/package.json b/metagpt/environment/minecraft_env/mineflayer/package.json similarity index 100% rename from metagpt/environment/mincraft_env/mineflayer/package.json rename to metagpt/environment/minecraft_env/mineflayer/package.json diff --git a/metagpt/environment/mincraft_env/process_monitor.py b/metagpt/environment/minecraft_env/process_monitor.py similarity index 100% rename from metagpt/environment/mincraft_env/process_monitor.py rename to metagpt/environment/minecraft_env/process_monitor.py diff --git a/metagpt/utils/common.py b/metagpt/utils/common.py index 7493712c2..ee0f74989 100644 --- a/metagpt/utils/common.py +++ b/metagpt/utils/common.py @@ -765,7 +765,7 @@ def is_coroutine_func(func: Callable) -> bool: def load_mc_skills_code(skill_names: list[str] = None, skills_dir: Path = None) -> list[str]: - """load mincraft skill from js files""" + """load minecraft skill from js files""" if not skills_dir: skills_dir = Path(__file__).parent.absolute() if skill_names is None: diff --git a/tests/metagpt/environment/mincraft_env/test_mincraft_ext_env.py b/tests/metagpt/environment/mincraft_env/test_mincraft_ext_env.py index ad3376141..ed08b719f 100644 --- a/tests/metagpt/environment/mincraft_env/test_mincraft_ext_env.py +++ b/tests/metagpt/environment/mincraft_env/test_mincraft_ext_env.py @@ -1,14 +1,14 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# @Desc : the unittest of MincraftExtEnv +# @Desc : the unittest of MinecraftExtEnv -from metagpt.environment.mincraft_env.const import MC_CKPT_DIR -from metagpt.environment.mincraft_env.mincraft_ext_env import MincraftExtEnv +from metagpt.environment.minecraft_env.const import MC_CKPT_DIR +from metagpt.environment.minecraft_env.minecraft_ext_env import MinecraftExtEnv -def test_mincraft_ext_env(): - ext_env = MincraftExtEnv() +def test_minecraft_ext_env(): + ext_env = MinecraftExtEnv() assert ext_env.server, f"{ext_env.server_host}:{ext_env.server_port}" assert MC_CKPT_DIR.joinpath("skill/code").exists() assert ext_env.warm_up.get("optional_inventory_items") == 7 From 82fa7f487c444802ce3cc050add7b360158faaa7 Mon Sep 17 00:00:00 2001 From: geekan Date: Fri, 22 Mar 2024 11:55:14 +0800 Subject: [PATCH 121/131] fix minecraft typo --- .../environment/{mincraft_env => minecraft_env}/__init__.py | 0 .../test_minecraft_ext_env.py} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename tests/metagpt/environment/{mincraft_env => minecraft_env}/__init__.py (100%) rename tests/metagpt/environment/{mincraft_env/test_mincraft_ext_env.py => minecraft_env/test_minecraft_ext_env.py} (100%) diff --git a/tests/metagpt/environment/mincraft_env/__init__.py b/tests/metagpt/environment/minecraft_env/__init__.py similarity index 100% rename from tests/metagpt/environment/mincraft_env/__init__.py rename to tests/metagpt/environment/minecraft_env/__init__.py diff --git a/tests/metagpt/environment/mincraft_env/test_mincraft_ext_env.py b/tests/metagpt/environment/minecraft_env/test_minecraft_ext_env.py similarity index 100% rename from tests/metagpt/environment/mincraft_env/test_mincraft_ext_env.py rename to tests/metagpt/environment/minecraft_env/test_minecraft_ext_env.py From 89b263a2fb47ccd37a5d287e74f91e70899ce430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Sat, 23 Mar 2024 13:24:23 +0800 Subject: [PATCH 122/131] fixbug: #1076 --- metagpt/provider/human_provider.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/metagpt/provider/human_provider.py b/metagpt/provider/human_provider.py index f205ecd1f..824acd345 100644 --- a/metagpt/provider/human_provider.py +++ b/metagpt/provider/human_provider.py @@ -6,7 +6,7 @@ Author: garylin2099 from typing import Optional from metagpt.configs.llm_config import LLMConfig -from metagpt.const import USE_CONFIG_TIMEOUT +from metagpt.const import LLM_API_TIMEOUT, USE_CONFIG_TIMEOUT from metagpt.logs import logger from metagpt.provider.base_llm import BaseLLM @@ -49,3 +49,6 @@ class HumanProvider(BaseLLM): async def acompletion_text(self, messages: list[dict], stream=False, timeout=USE_CONFIG_TIMEOUT) -> str: """dummy implementation of abstract method in base""" return "" + + def get_timeout(self, timeout: int) -> int: + return timeout or LLM_API_TIMEOUT From d53cfd39f7711824d14fec63e8d9b5f29097464b Mon Sep 17 00:00:00 2001 From: shenchucheng Date: Wed, 21 Feb 2024 15:40:23 +0800 Subject: [PATCH 123/131] replace *.deepwisdom.ai for scraping by local server --- metagpt/tools/web_browser_engine_selenium.py | 2 +- tests/conftest.py | 9 +++--- tests/metagpt/tools/libs/test_web_scraping.py | 5 +-- .../metagpt/tools/test_web_browser_engine.py | 11 ++++--- .../test_web_browser_engine_playwright.py | 26 +++++++++++---- .../tools/test_web_browser_engine_selenium.py | 32 +++++++++---------- 6 files changed, 50 insertions(+), 35 deletions(-) diff --git a/metagpt/tools/web_browser_engine_selenium.py b/metagpt/tools/web_browser_engine_selenium.py index 3b1682291..786715461 100644 --- a/metagpt/tools/web_browser_engine_selenium.py +++ b/metagpt/tools/web_browser_engine_selenium.py @@ -103,7 +103,7 @@ class WDMHttpProxyClient(WDMHttpClient): def get(self, url, **kwargs): if "proxies" not in kwargs and self.proxy: - kwargs["proxies"] = {"all_proxy": self.proxy} + kwargs["proxies"] = {"all": self.proxy} return super().get(url, **kwargs) diff --git a/tests/conftest.py b/tests/conftest.py index efd782417..8603c752a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -113,12 +113,13 @@ def proxy(): while not reader.at_eof(): writer.write(await reader.read(2048)) writer.close() + await writer.wait_closed() async def handle_client(reader, writer): data = await reader.readuntil(b"\r\n\r\n") - print(f"Proxy: {data}") # checking with capfd fixture infos = pattern.match(data) host, port = infos.group("host"), infos.group("port") + print(f"Proxy: {host}") # checking with capfd fixture port = int(port) if port else 80 remote_reader, remote_writer = await asyncio.open_connection(host, port) if data.startswith(b"CONNECT"): @@ -257,10 +258,10 @@ def http_server(): server = aiohttp.web.Server(handler) runner = aiohttp.web.ServerRunner(server) await runner.setup() - site = aiohttp.web.TCPSite(runner, "localhost", 0) + site = aiohttp.web.TCPSite(runner, "127.0.0.1", 0) await site.start() - host, port = site._server.sockets[0].getsockname() - return site, f"http://{host}:{port}" + _, port, *_ = site._server.sockets[0].getsockname() + return site, f"http://127.0.0.1:{port}" return start diff --git a/tests/metagpt/tools/libs/test_web_scraping.py b/tests/metagpt/tools/libs/test_web_scraping.py index c11960e68..3d8877b8d 100644 --- a/tests/metagpt/tools/libs/test_web_scraping.py +++ b/tests/metagpt/tools/libs/test_web_scraping.py @@ -4,8 +4,8 @@ from metagpt.tools.libs.web_scraping import scrape_web_playwright @pytest.mark.asyncio -async def test_scrape_web_playwright(): - test_url = "https://www.deepwisdom.ai" +async def test_scrape_web_playwright(http_server): + server, test_url = await http_server() result = await scrape_web_playwright(test_url) @@ -21,3 +21,4 @@ async def test_scrape_web_playwright(): assert not result["inner_text"].endswith(" ") assert not result["html"].startswith(" ") assert not result["html"].endswith(" ") + await server.stop() diff --git a/tests/metagpt/tools/test_web_browser_engine.py b/tests/metagpt/tools/test_web_browser_engine.py index ceebd67fc..7a344e0ad 100644 --- a/tests/metagpt/tools/test_web_browser_engine.py +++ b/tests/metagpt/tools/test_web_browser_engine.py @@ -9,14 +9,16 @@ from metagpt.utils.parse_html import WebPage @pytest.mark.asyncio @pytest.mark.parametrize( - "browser_type, url, urls", + "browser_type", [ - (WebBrowserEngineType.PLAYWRIGHT, "https://deepwisdom.ai", ("https://deepwisdom.ai",)), - (WebBrowserEngineType.SELENIUM, "https://deepwisdom.ai", ("https://deepwisdom.ai",)), + WebBrowserEngineType.PLAYWRIGHT, + WebBrowserEngineType.SELENIUM, ], ids=["playwright", "selenium"], ) -async def test_scrape_web_page(browser_type, url, urls): +async def test_scrape_web_page(browser_type, http_server): + server, url = await http_server() + urls = [url, url, url] browser = web_browser_engine.WebBrowserEngine(engine=browser_type) result = await browser.run(url) assert isinstance(result, WebPage) @@ -27,6 +29,7 @@ async def test_scrape_web_page(browser_type, url, urls): assert isinstance(results, list) assert len(results) == len(urls) + 1 assert all(("MetaGPT" in i.inner_text) for i in results) + await server.stop() if __name__ == "__main__": diff --git a/tests/metagpt/tools/test_web_browser_engine_playwright.py b/tests/metagpt/tools/test_web_browser_engine_playwright.py index f35848cf4..12ea96d7b 100644 --- a/tests/metagpt/tools/test_web_browser_engine_playwright.py +++ b/tests/metagpt/tools/test_web_browser_engine_playwright.py @@ -9,18 +9,28 @@ from metagpt.utils.parse_html import WebPage @pytest.mark.asyncio @pytest.mark.parametrize( - "browser_type, use_proxy, kwagrs, url, urls", + "browser_type, use_proxy, kwagrs,", [ - ("chromium", {"proxy": True}, {}, "https://www.deepwisdom.ai", ("https://www.deepwisdom.ai",)), - ("firefox", {}, {"ignore_https_errors": True}, "https://www.deepwisdom.ai", ("https://www.deepwisdom.ai",)), - ("webkit", {}, {"ignore_https_errors": True}, "https://www.deepwisdom.ai", ("https://www.deepwisdom.ai",)), + ("chromium", {"proxy": True}, {}), + ( + "firefox", + {}, + {"ignore_https_errors": True}, + ), + ( + "webkit", + {}, + {"ignore_https_errors": True}, + ), ], ids=["chromium-normal", "firefox-normal", "webkit-normal"], ) -async def test_scrape_web_page(browser_type, use_proxy, kwagrs, url, urls, proxy, capfd): +async def test_scrape_web_page(browser_type, use_proxy, kwagrs, proxy, capfd, http_server): + server, url = await http_server() + urls = [url, url, url] proxy_url = None if use_proxy: - server, proxy_url = await proxy() + proxy_server, proxy_url = await proxy() browser = web_browser_engine_playwright.PlaywrightWrapper(browser_type=browser_type, proxy=proxy_url, **kwagrs) result = await browser.run(url) assert isinstance(result, WebPage) @@ -32,8 +42,10 @@ async def test_scrape_web_page(browser_type, use_proxy, kwagrs, url, urls, proxy assert len(results) == len(urls) + 1 assert all(("MetaGPT" in i.inner_text) for i in results) if use_proxy: - server.close() + proxy_server.close() + await proxy_server.wait_closed() assert "Proxy:" in capfd.readouterr().out + await server.stop() if __name__ == "__main__": diff --git a/tests/metagpt/tools/test_web_browser_engine_selenium.py b/tests/metagpt/tools/test_web_browser_engine_selenium.py index a88a5d0f4..a214748bd 100644 --- a/tests/metagpt/tools/test_web_browser_engine_selenium.py +++ b/tests/metagpt/tools/test_web_browser_engine_selenium.py @@ -1,6 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- + import browsers import pytest @@ -10,51 +11,48 @@ from metagpt.utils.parse_html import WebPage @pytest.mark.asyncio @pytest.mark.parametrize( - "browser_type, use_proxy, url, urls", + "browser_type, use_proxy,", [ pytest.param( "chrome", - True, - "https://deepwisdom.ai", - ("https://deepwisdom.ai",), + False, marks=pytest.mark.skipif(not browsers.get("chrome"), reason="chrome browser not found"), ), pytest.param( "firefox", False, - "https://deepwisdom.ai", - ("https://deepwisdom.ai",), marks=pytest.mark.skipif(not browsers.get("firefox"), reason="firefox browser not found"), ), pytest.param( "edge", False, - "https://deepwisdom.ai", - ("https://deepwisdom.ai",), marks=pytest.mark.skipif(not browsers.get("msedge"), reason="edge browser not found"), ), ], ids=["chrome-normal", "firefox-normal", "edge-normal"], ) -async def test_scrape_web_page(browser_type, use_proxy, url, urls, proxy, capfd): +async def test_scrape_web_page(browser_type, use_proxy, proxy, capfd, http_server): # Prerequisites # firefox, chrome, Microsoft Edge + server, url = await http_server() + urls = [url, url, url] proxy_url = None if use_proxy: - server, proxy_url = await proxy() + proxy_server, proxy_url = await proxy() browser = web_browser_engine_selenium.SeleniumWrapper(browser_type=browser_type, proxy=proxy_url) result = await browser.run(url) assert isinstance(result, WebPage) assert "MetaGPT" in result.inner_text - if urls: - results = await browser.run(url, *urls) - assert isinstance(results, list) - assert len(results) == len(urls) + 1 - assert all(("MetaGPT" in i.inner_text) for i in results) + results = await browser.run(url, *urls) + assert isinstance(results, list) + assert len(results) == len(urls) + 1 + assert all(("MetaGPT" in i.inner_text) for i in results) if use_proxy: - server.close() - assert "Proxy:" in capfd.readouterr().out + proxy_server.close() + await proxy_server.wait_closed() + assert "Proxy: localhost" in capfd.readouterr().out + await server.stop() if __name__ == "__main__": From a988d9c4f3c8fad9802a092cb088659e3fe1fbcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Sat, 23 Mar 2024 17:57:10 +0800 Subject: [PATCH 124/131] fixbug: proxy --- metagpt/provider/google_gemini_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/metagpt/provider/google_gemini_api.py b/metagpt/provider/google_gemini_api.py index bdbf7acd6..d604313a5 100644 --- a/metagpt/provider/google_gemini_api.py +++ b/metagpt/provider/google_gemini_api.py @@ -60,8 +60,8 @@ class GeminiLLM(BaseLLM): def __init_gemini(self, config: LLMConfig): if config.proxy: logger.info(f"Use proxy: {config.proxy}") - os.environ["HTTP_PROXY"] = config.proxy - os.environ["HTTP_PROXYS"] = config.proxy + os.environ["http_proxy"] = config.proxy + os.environ["https_proxy"] = config.proxy genai.configure(api_key=config.api_key) def _user_msg(self, msg: str, images: Optional[Union[str, list[str]]] = None) -> dict[str, str]: From 83e5eb133a8d48a84c67b46d237d5c95e5caca2f Mon Sep 17 00:00:00 2001 From: luxiangtao Date: Sun, 24 Mar 2024 15:01:45 +0800 Subject: [PATCH 125/131] updata di example and Change the location of the .ipynb locations --- examples/di/data_visualization.py | 15 +++++++++++---- metagpt/utils/recovery_util.py | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/examples/di/data_visualization.py b/examples/di/data_visualization.py index 1a21ab7cb..62e7e7c2f 100644 --- a/examples/di/data_visualization.py +++ b/examples/di/data_visualization.py @@ -1,14 +1,21 @@ import asyncio - +import os +import sys +import metagpt.utils +from metagpt.logs import logger +from metagpt.utils.recovery_util import save_history from metagpt.roles.di.data_interpreter import DataInterpreter - async def main(requirement: str = ""): + di = DataInterpreter() - await di.run(requirement) + rsp = await di.run(requirement) + logger.info(rsp) + save_history(role=di) if __name__ == "__main__": - requirement = "Run data analysis on sklearn Iris dataset, include a plot" + requirement = "Run data analysis on sklearn Iris dataset, include a plot" asyncio.run(main(requirement)) + diff --git a/metagpt/utils/recovery_util.py b/metagpt/utils/recovery_util.py index d0b197e69..2089ae018 100644 --- a/metagpt/utils/recovery_util.py +++ b/metagpt/utils/recovery_util.py @@ -54,5 +54,5 @@ def save_history(role: Role, save_dir: str = ""): with open(save_path / "plan.json", "w", encoding="utf-8") as plan_file: json.dump(plan, plan_file, indent=4, ensure_ascii=False) - save_code_file(name=Path(record_time) / "history_nb", code_context=role.execute_code.nb, file_format="ipynb") + save_code_file(name=Path(record_time), code_context=role.execute_code.nb, file_format="ipynb") return save_path From 37fcabfc3ca312a68a62349744ce5993b4316df6 Mon Sep 17 00:00:00 2001 From: luxiangtao Date: Sun, 24 Mar 2024 15:04:42 +0800 Subject: [PATCH 126/131] remove di example's uselsee codes --- examples/di/data_visualization.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/examples/di/data_visualization.py b/examples/di/data_visualization.py index 62e7e7c2f..4c88ffe00 100644 --- a/examples/di/data_visualization.py +++ b/examples/di/data_visualization.py @@ -1,7 +1,4 @@ import asyncio -import os -import sys -import metagpt.utils from metagpt.logs import logger from metagpt.utils.recovery_util import save_history from metagpt.roles.di.data_interpreter import DataInterpreter From 82ca12f4da251ae2b63d9723eea26c98f5b87b4d Mon Sep 17 00:00:00 2001 From: luxiangtao Date: Mon, 25 Mar 2024 11:24:02 +0800 Subject: [PATCH 127/131] pre-commit --- examples/di/data_visualization.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/di/data_visualization.py b/examples/di/data_visualization.py index 4c88ffe00..184e04f26 100644 --- a/examples/di/data_visualization.py +++ b/examples/di/data_visualization.py @@ -1,10 +1,11 @@ import asyncio + from metagpt.logs import logger -from metagpt.utils.recovery_util import save_history from metagpt.roles.di.data_interpreter import DataInterpreter +from metagpt.utils.recovery_util import save_history + async def main(requirement: str = ""): - di = DataInterpreter() rsp = await di.run(requirement) logger.info(rsp) @@ -12,7 +13,5 @@ async def main(requirement: str = ""): if __name__ == "__main__": - requirement = "Run data analysis on sklearn Iris dataset, include a plot" asyncio.run(main(requirement)) - From d0dc2674cab060da71f6e2289cc0a7ad3d1388e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=A3=92=E6=A3=92?= Date: Mon, 25 Mar 2024 16:30:53 +0800 Subject: [PATCH 128/131] fix #868 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6b23e47b6..aca31ef00 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,7 +24,7 @@ numpy==1.24.3 openai==1.6.1 openpyxl beautifulsoup4==4.12.3 -pandas==2.0.3 +pandas==2.1.1 pydantic==2.5.3 #pygame==2.1.3 #pymilvus==2.2.8 From b403f2b2098dbd7e9ea86b5171d5820315483a2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Mon, 25 Mar 2024 22:56:18 +0800 Subject: [PATCH 129/131] fixbug: #1100 --- metagpt/provider/google_gemini_api.py | 19 ++++++++++++++++--- requirements.txt | 2 +- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/metagpt/provider/google_gemini_api.py b/metagpt/provider/google_gemini_api.py index 4ff49befe..e4b3a3f17 100644 --- a/metagpt/provider/google_gemini_api.py +++ b/metagpt/provider/google_gemini_api.py @@ -1,9 +1,10 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Desc : Google Gemini LLM from https://ai.google.dev/tutorials/python_quickstart - +import json import os -from typing import Optional, Union +from dataclasses import asdict +from typing import List, Optional, Union import google.generativeai as genai from google.ai import generativelanguage as glm @@ -11,6 +12,7 @@ from google.generativeai.generative_models import GenerativeModel from google.generativeai.types import content_types from google.generativeai.types.generation_types import ( AsyncGenerateContentResponse, + BlockedPromptException, GenerateContentResponse, GenerationConfig, ) @@ -141,7 +143,11 @@ class GeminiLLM(BaseLLM): ) collected_content = [] async for chunk in resp: - content = chunk.text + try: + content = chunk.text + except Exception as e: + logger.warning(f"messages: {messages}\nerrors: {e}\n{BlockedPromptException(str(chunk))}") + raise BlockedPromptException(str(chunk)) log_llm_stream(content) collected_content.append(content) log_llm_stream("\n") @@ -150,3 +156,10 @@ class GeminiLLM(BaseLLM): usage = await self.aget_usage(messages, full_content) self._update_costs(usage) return full_content + + def list_models(self) -> List: + models = [] + for model in genai.list_models(page_size=100): + models.append(asdict(model)) + logger.info(json.dumps(models)) + return models diff --git a/requirements.txt b/requirements.txt index a0ce1d1ac..da8aa26b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -60,7 +60,7 @@ gitignore-parser==0.1.9 # connexion[uvicorn]~=3.0.5 # Used by metagpt/tools/openapi_v3_hello.py websockets~=11.0 networkx~=3.2.1 -google-generativeai==0.3.2 +google-generativeai==0.4.1 playwright>=1.26 # used at metagpt/tools/libs/web_scraping.py anytree ipywidgets==8.1.1 From 4bd80c5e7dbf5cd4d52444951a2c6bb44e46a5a1 Mon Sep 17 00:00:00 2001 From: better629 Date: Tue, 26 Mar 2024 10:22:00 +0800 Subject: [PATCH 130/131] fix provider failed ut --- metagpt/provider/human_provider.py | 2 +- tests/metagpt/provider/test_base_llm.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/metagpt/provider/human_provider.py b/metagpt/provider/human_provider.py index 824acd345..87dbd105f 100644 --- a/metagpt/provider/human_provider.py +++ b/metagpt/provider/human_provider.py @@ -17,7 +17,7 @@ class HumanProvider(BaseLLM): """ def __init__(self, config: LLMConfig): - pass + self.config = config def ask(self, msg: str, timeout=USE_CONFIG_TIMEOUT) -> str: logger.info("It's your turn, please type in your response. You may also refer to the context below") diff --git a/tests/metagpt/provider/test_base_llm.py b/tests/metagpt/provider/test_base_llm.py index bff8dbde4..40a9fda92 100644 --- a/tests/metagpt/provider/test_base_llm.py +++ b/tests/metagpt/provider/test_base_llm.py @@ -11,6 +11,7 @@ import pytest from metagpt.configs.llm_config import LLMConfig from metagpt.provider.base_llm import BaseLLM from metagpt.schema import Message +from tests.metagpt.provider.mock_llm_config import mock_llm_config from tests.metagpt.provider.req_resp_const import ( default_resp_cont, get_part_chat_completion, @@ -22,7 +23,7 @@ name = "GPT" class MockBaseLLM(BaseLLM): def __init__(self, config: LLMConfig = None): - pass + self.config = config or mock_llm_config def completion(self, messages: list[dict], timeout=3): return get_part_chat_completion(name) From b2fd9608299a17cb805449a8491a7862d2950add Mon Sep 17 00:00:00 2001 From: geekan Date: Tue, 26 Mar 2024 10:22:32 +0800 Subject: [PATCH 131/131] skip rebuild seq view --- tests/metagpt/actions/test_rebuild_sequence_view.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/metagpt/actions/test_rebuild_sequence_view.py b/tests/metagpt/actions/test_rebuild_sequence_view.py index 0e10e3776..9be3e8a99 100644 --- a/tests/metagpt/actions/test_rebuild_sequence_view.py +++ b/tests/metagpt/actions/test_rebuild_sequence_view.py @@ -18,6 +18,7 @@ from metagpt.utils.git_repository import ChangeType from metagpt.utils.graph_repository import SPO +@pytest.mark.skip @pytest.mark.asyncio async def test_rebuild(context, mocker): # Mock