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 01/52] 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 02/52] 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 03/52] 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 04/52] 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 05/52] 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 06/52] 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 07/52] 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 08/52] 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 09/52] 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 10/52] 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 11/52] 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 12/52] 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 13/52] 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 14/52] 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 219d361ca6d377f749631cca9794133984e98c19 Mon Sep 17 00:00:00 2001 From: yzlin Date: Mon, 26 Feb 2024 21:10:16 +0800 Subject: [PATCH 15/52] 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 16/52] 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 17/52] 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 0a2273c7a071490f8bb6cbab3d8e0e20b9f86a63 Mon Sep 17 00:00:00 2001 From: yzlin Date: Mon, 4 Mar 2024 10:45:00 +0800 Subject: [PATCH 18/52] 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 0116de01b9201af7affe1ab46c79cfb76cc39df4 Mon Sep 17 00:00:00 2001 From: yzlin Date: Thu, 7 Mar 2024 21:22:44 +0800 Subject: [PATCH 19/52] 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 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 20/52] 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 21/52] 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 22/52] 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 23/52] 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 24/52] 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 25/52] 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 26/52] 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 27/52] 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 28/52] 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 29/52] 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 30/52] 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 31/52] 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 32/52] 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 33/52] 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 9ad2f22c16247a159186a90c3335125acacd41da Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Mon, 11 Mar 2024 19:59:12 +0800 Subject: [PATCH 34/52] 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 a680a1a82fd16501c47f89c6efda0b756705a415 Mon Sep 17 00:00:00 2001 From: yzlin Date: Tue, 12 Mar 2024 16:11:57 +0800 Subject: [PATCH 35/52] 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 36/52] 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 a585ef52a60b3e2f4dfff19d512d9835cddf377e Mon Sep 17 00:00:00 2001 From: yzlin Date: Tue, 12 Mar 2024 17:50:10 +0800 Subject: [PATCH 37/52] 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 38/52] 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 39/52] 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 40/52] 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 77eacec1b85fdf6e33684dd3c4c5a3d3555dec17 Mon Sep 17 00:00:00 2001 From: yzlin Date: Wed, 13 Mar 2024 16:29:28 +0800 Subject: [PATCH 41/52] 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 42/52] 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 43/52] 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 44/52] 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 45/52] 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 46/52] 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 47/52] 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 48/52] 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 49/52] 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 50/52] 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 51/52] 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 52/52] 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))