diff --git a/metagpt/roles/kaggle_manager.py b/metagpt/roles/kaggle_manager.py index 18ac6733a..cad12a16a 100644 --- a/metagpt/roles/kaggle_manager.py +++ b/metagpt/roles/kaggle_manager.py @@ -10,7 +10,8 @@ from metagpt.config import CONFIG from metagpt.const import WORKSPACE_ROOT from metagpt.roles import Role from metagpt.actions import Action, BossRequirement -from metagpt.actions.ml_da_action import AskReview, SummarizeAnalysis +from metagpt.actions.ask_review import AskReview +from metagpt.actions.ml_da_action import SummarizeAnalysis from metagpt.schema import Message, Task, Plan from metagpt.logs import logger from metagpt.utils.common import CodeParser diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index b96c361c0..8c68a7ab4 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -18,7 +18,8 @@ from metagpt.actions import Action, ActionOutput from metagpt.llm import LLM, HumanProvider from metagpt.logs import logger from metagpt.memory import Memory, LongTermMemory -from metagpt.schema import Message +from metagpt.schema import Message, Task +from metagpt.plan.planner import Planner PREFIX_TEMPLATE = """You are a {profile}, named {name}, your goal is {goal}, and the constraint is {constraints}. """ @@ -79,6 +80,7 @@ class RoleContext(BaseModel): env: 'Environment' = Field(default=None) memory: Memory = Field(default_factory=Memory) long_term_memory: LongTermMemory = Field(default_factory=LongTermMemory) + working_memory: Memory = Field(default_factory=Memory) state: int = Field(default=-1) # -1 indicates initial or termination state where todo is None todo: Action = Field(default=None) watch: set[Type[Action]] = Field(default_factory=set) @@ -115,6 +117,7 @@ class Role: self._actions = [] self._role_id = str(self._setting) self._rc = RoleContext() + self.planner = None def _reset(self): self._states = [] @@ -134,7 +137,7 @@ class Role: self._actions.append(i) self._states.append(f"{idx}. {action}") - def _set_react_mode(self, react_mode: str, max_react_loop: int = 1): + 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. @@ -154,6 +157,8 @@ class Role: self._rc.react_mode = react_mode 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._setting.goal, working_memory=self._rc.working_memory, auto_run=auto_run) def _watch(self, actions: Iterable[Type[Action]]): """Listen to the corresponding behaviors""" @@ -274,8 +279,55 @@ class Role: async def _plan_and_act(self) -> Message: """first plan, then execute an action sequence, i.e. _think (of a plan) -> _act -> _act -> ... Use llm to come up with the plan dynamically.""" - # TODO: to be implemented - return Message("") + + ### Common Procedure in both single- and multi-agent setting ### + # create initial plan and update until confirmation + await self.planner.update_plan() + + while self.planner.current_task: + task = self.planner.current_task + logger.info(f"ready to take on task {task}") + + # take on current task + task_copy_with_result = await self._act_on_task(task) + + # ask for acceptance, users can other refuse and change tasks in the plan + review, task_result_confirmed = await self.planner.ask_review(task_copy_with_result) + + if task_result_confirmed: + # tick off this task and record progress + await self.planner.confirm_task(task, task_copy_with_result, review) + + elif "redo" in review: + # Ask the Role to redo this task with help of review feedback, + # useful when the code run is successful but the procedure or result is not what we want + continue + + else: + # update plan according to user's feedback and to take on changed tasks + await self.planner.update_plan(review) + + completed_plan_memory = self.planner.get_useful_memories() # completed plan as a outcome + + rsp = completed_plan_memory[0] + + self._rc.memory.add(rsp) # add to persistent memory + + return rsp + + async def _act_on_task(self, current_task: Task) -> Task: + """Taking specific action to handle one task in plan + + Args: + current_task (Task): current task to take on + + Raises: + NotImplementedError: Specific Role must implement this method if expected to use planner + + Returns: + Task: A copy of the current task with result from actions + """ + raise NotImplementedError async def react(self) -> Message: """Entry to one of three strategies by which Role reacts to the observed Message""" diff --git a/tests/metagpt/roles/run_code_interpreter.py b/tests/metagpt/roles/run_code_interpreter.py new file mode 100644 index 000000000..daa6bbe05 --- /dev/null +++ b/tests/metagpt/roles/run_code_interpreter.py @@ -0,0 +1,85 @@ +import fire + +from metagpt.actions.execute_code import ExecutePyCode +from metagpt.const import DATA_PATH +from metagpt.logs import logger +from metagpt.roles.code_interpreter import CodeInterpreter +from metagpt.roles.ml_engineer import MLEngineer +from metagpt.schema import Plan +from metagpt.utils.recovery_util import save_history, load_history + + +async def run_code_interpreter(role_class, requirement, auto_run, use_tools, use_code_steps, make_udfs, use_udfs, save_dir): + """ + The main function to run the MLEngineer with optional history loading. + + Args: + requirement (str): The requirement for the MLEngineer. + auto_run (bool): Whether to auto-run the MLEngineer. + save_dir (str): The directory from which to load the history or to save the new history. + + Raises: + Exception: If an error occurs during execution, log the error and save the history. + """ + + if role_class == "ci": + role = CodeInterpreter(goal=requirement, auto_run=auto_run) + else: + role = MLEngineer( + goal=requirement, auto_run=auto_run, use_tools=use_tools, use_code_steps=use_code_steps, + make_udfs=make_udfs, use_udfs=use_udfs + ) + + if save_dir: + logger.info("Resuming from history trajectory") + plan, nb = load_history(save_dir) + role.planner.plan = Plan(**plan) + role.execute_code = ExecutePyCode(nb) + + else: + logger.info("Run from scratch") + + + try: + await role.run(requirement) + except Exception as e: + + save_path = save_history(role, save_dir) + + logger.exception(f"An error occurred: {e}, save trajectory here: {save_path}") + + +if __name__ == "__main__": + requirement = "Run data analysis on sklearn Iris dataset, include a plot" + # 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_path = f"{DATA_PATH}/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'." + # data_path = f"{DATA_PATH}/icr-identify-age-related-conditions" + # 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: {data_path}/split_train.csv, eval data path: {data_path}/split_eval.csv." + # data_path = f"{DATA_PATH}/santander-customer-transaction-prediction" + # requirement = f"This is a customers financial dataset. Your goal is to predict which customers will make a specific transaction in the future. The target column is target. Perform data analysis, data preprocessing, feature engineering, and modeling to predict the target. Report AUC Score on the eval data. Train data path: '{data_path}/split_train.csv', eval data path: '{data_path}/split_eval.csv' ." + # data_path = f"{DATA_PATH}/house-prices-advanced-regression-techniques" + # requirement = f"This is a house price dataset, your goal is to predict the sale price of a property based on its features. The target column is SalePrice. Perform data analysis, data preprocessing, feature engineering, and modeling to predict the target. Report RMSE between the logarithm of the predicted value and the logarithm of the observed sales price on the eval data. Train data path: '{data_path}/split_train.csv', eval data path: '{data_path}/split_eval.csv'." + + save_dir = "" + # save_dir = DATA_PATH / "output" / "2023-12-14_20-40-34" + + role_class = "ci" + # role_class = "mle" + auto_run = True + # auto_run = False + # use_tools = True + use_tools = False + # make_udfs = True + make_udfs = False + # use_udfs = True + use_udfs = False + + async def main( + role_class: str = role_class, requirement: str = requirement, auto_run: bool = auto_run, + use_tools: bool = use_tools, use_code_steps: bool = False, make_udfs: bool = make_udfs, use_udfs: bool = use_udfs, + save_dir: str = save_dir + ): + await run_code_interpreter(role_class, requirement, auto_run, use_tools, use_code_steps, make_udfs, use_udfs, save_dir) + + fire.Fire(main)