diff --git a/README.md b/README.md index edb2066a3..44fcfab18 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ # MetaGPT: The Multi-Agent Framework

## News +🚀 Mar. 29, 2024: [v0.8.0](https://github.com/geekan/MetaGPT/releases/tag/v0.8.0) released. Now you can use Data Interpreter via pypi package import. Meanwhile, we integrated RAG module and supported multiple new LLMs. + 🚀 Mar. 14, 2024: Our **Data Interpreter** paper is on [arxiv](https://arxiv.org/abs/2402.18679). Check the [example](https://docs.deepwisdom.ai/main/en/DataInterpreter/) and [code](https://github.com/geekan/MetaGPT/tree/main/examples/di)! 🚀 Feb. 08, 2024: [v0.7.0](https://github.com/geekan/MetaGPT/releases/tag/v0.7.0) released, supporting assigning different LLMs to different Roles. We also introduced [Data Interpreter](https://github.com/geekan/MetaGPT/blob/main/examples/di/README.md), a powerful agent capable of solving a wide range of real-world problems. @@ -145,10 +147,13 @@ ## Tutorial ## Support -### Discard Join US -📢 Join Our [Discord Channel](https://discord.gg/ZRHeExS6xv)! +### Discord Join US -Looking forward to seeing you there! 🎉 +📢 Join Our [Discord Channel](https://discord.gg/ZRHeExS6xv)! Looking forward to seeing you there! 🎉 + +### Contributor form + +📝 [Fill out the form](https://airtable.com/appInfdG0eJ9J4NNL/pagK3Fh1sGclBvVkV/form) to become a contributor. We are looking forward to your participation! ### Contact Information diff --git a/examples/build_customized_agent.py b/examples/build_customized_agent.py index cfe264b47..7dab4833d 100644 --- a/examples/build_customized_agent.py +++ b/examples/build_customized_agent.py @@ -17,7 +17,7 @@ from metagpt.schema import Message class SimpleWriteCode(Action): PROMPT_TEMPLATE: str = """ - Write a python function that can {instruction} and provide two runnnable test cases. + Write a python function that can {instruction} and provide two runnable test cases. Return ```python your_code_here ``` with NO other texts, your code: """ diff --git a/metagpt/actions/write_code_plan_and_change_an.py b/metagpt/actions/write_code_plan_and_change_an.py index f99bffd84..a90946981 100644 --- a/metagpt/actions/write_code_plan_and_change_an.py +++ b/metagpt/actions/write_code_plan_and_change_an.py @@ -128,6 +128,9 @@ CODE_PLAN_AND_CHANGE_CONTEXT = """ ## User New Requirements {requirement} +## Issue +{issue} + ## PRD {prd} @@ -211,7 +214,8 @@ class WriteCodePlanAndChange(Action): design_doc = await self.repo.docs.system_design.get(filename=self.i_context.design_filename) task_doc = await self.repo.docs.task.get(filename=self.i_context.task_filename) context = CODE_PLAN_AND_CHANGE_CONTEXT.format( - requirement=self.i_context.requirement, + requirement=f"```text\n{self.i_context.requirement}\n```", + issue=f"```text\n{self.i_context.issue}\n```", prd=prd_doc.content, design=design_doc.content, task=task_doc.content, diff --git a/metagpt/actions/write_prd_an.py b/metagpt/actions/write_prd_an.py index 5733b29da..6a995e184 100644 --- a/metagpt/actions/write_prd_an.py +++ b/metagpt/actions/write_prd_an.py @@ -133,10 +133,10 @@ REQUIREMENT_ANALYSIS = ActionNode( REFINED_REQUIREMENT_ANALYSIS = ActionNode( key="Refined Requirement Analysis", expected_type=List[str], - instruction="Review and refine the existing requirement analysis to align with the evolving needs of the project " + instruction="Review and refine the existing requirement analysis into a string list 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.", - example=["Require add/update/modify ..."], + example=["Require add ...", "Require modify ..."], ) REQUIREMENT_POOL = ActionNode( diff --git a/metagpt/context.py b/metagpt/context.py index 0add4c71a..2bd541202 100644 --- a/metagpt/context.py +++ b/metagpt/context.py @@ -7,7 +7,7 @@ """ import os from pathlib import Path -from typing import Any, Optional +from typing import Any, Dict, Optional from pydantic import BaseModel, ConfigDict @@ -78,12 +78,6 @@ class Context(BaseModel): # env.update({k: v for k, v in i.items() if isinstance(v, str)}) return env - # def use_llm(self, name: Optional[str] = None, provider: LLMType = LLMType.OPENAI) -> BaseLLM: - # """Use a LLM instance""" - # self._llm_config = self.config.get_llm_config(name, provider) - # self._llm = None - # return self._llm - def _select_costmanager(self, llm_config: LLMConfig) -> CostManager: """Return a CostManager instance""" if llm_config.api_type == LLMType.FIREWORKS: @@ -108,3 +102,38 @@ class Context(BaseModel): if llm.cost_manager is None: llm.cost_manager = self._select_costmanager(llm_config) return llm + + def serialize(self) -> Dict[str, Any]: + """Serialize the object's attributes into a dictionary. + + Returns: + Dict[str, Any]: A dictionary containing serialized data. + """ + return { + "workdir": str(self.repo.workdir) if self.repo else "", + "kwargs": {k: v for k, v in self.kwargs.__dict__.items()}, + "cost_manager": self.cost_manager.model_dump_json(), + } + + def deserialize(self, serialized_data: Dict[str, Any]): + """Deserialize the given serialized data and update the object's attributes accordingly. + + Args: + serialized_data (Dict[str, Any]): A dictionary containing serialized data. + """ + if not serialized_data: + return + workdir = serialized_data.get("workdir") + if workdir: + self.git_repo = GitRepository(local_path=workdir, auto_init=True) + self.repo = ProjectRepo(self.git_repo) + src_workspace = self.git_repo.workdir / self.git_repo.workdir.name + if src_workspace.exists(): + self.src_workspace = src_workspace + kwargs = serialized_data.get("kwargs") + if kwargs: + for k, v in kwargs.items(): + self.kwargs.set(k, v) + cost_manager = serialized_data.get("cost_manager") + if cost_manager: + self.cost_manager.model_validate_json(cost_manager) diff --git a/metagpt/roles/di/data_interpreter.py b/metagpt/roles/di/data_interpreter.py index 1943b4234..547f4b90b 100644 --- a/metagpt/roles/di/data_interpreter.py +++ b/metagpt/roles/di/data_interpreter.py @@ -86,9 +86,13 @@ class DataInterpreter(Role): return Message(content=code, role="assistant", cause_by=WriteAnalysisCode) async def _plan_and_act(self) -> Message: - rsp = await super()._plan_and_act() - await self.execute_code.terminate() - return rsp + try: + rsp = await super()._plan_and_act() + await self.execute_code.terminate() + return rsp + except Exception as e: + await self.execute_code.terminate() + raise e 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.""" diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index 9d8f6884f..6962b1bb5 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -22,7 +22,7 @@ from __future__ import annotations import json from collections import defaultdict from pathlib import Path -from typing import Set +from typing import Optional, Set from metagpt.actions import Action, WriteCode, WriteCodeReview, WriteTasks from metagpt.actions.fix_bug import FixBug @@ -30,6 +30,7 @@ from metagpt.actions.project_management_an import REFINED_TASK_LIST, TASK_LIST from metagpt.actions.summarize_code import SummarizeCode from metagpt.actions.write_code_plan_and_change_an import WriteCodePlanAndChange from metagpt.const import ( + BUGFIX_FILENAME, CODE_PLAN_AND_CHANGE_FILE_REPO, REQUIREMENT_FILENAME, SYSTEM_DESIGN_FILE_REPO, @@ -208,9 +209,9 @@ class Engineer(Role): code_plan_and_change = node.instruct_content.model_dump_json() dependencies = { REQUIREMENT_FILENAME, - self.rc.todo.i_context.prd_filename, - self.rc.todo.i_context.design_filename, - self.rc.todo.i_context.task_filename, + str(self.project_repo.docs.prd.root_path / self.rc.todo.i_context.prd_filename), + str(self.project_repo.docs.system_design.root_path / self.rc.todo.i_context.design_filename), + str(self.project_repo.docs.task.root_path / self.rc.todo.i_context.task_filename), } code_plan_and_change_filepath = Path(self.rc.todo.i_context.design_filename) await self.project_repo.docs.code_plan_and_change.save( @@ -248,11 +249,11 @@ class Engineer(Role): msg = self.rc.news[0] if self.config.inc and msg.cause_by in write_plan_and_change_filters: logger.debug(f"TODO WriteCodePlanAndChange:{msg.model_dump_json()}") - await self._new_code_plan_and_change_action() + await self._new_code_plan_and_change_action(cause_by=msg.cause_by) return self.rc.todo if msg.cause_by in write_code_filters: logger.debug(f"TODO WriteCode:{msg.model_dump_json()}") - await self._new_code_actions(bug_fix=msg.cause_by == any_to_str(FixBug)) + await self._new_code_actions() return self.rc.todo if msg.cause_by in summarize_code_filters and msg.sent_from == any_to_str(self): logger.debug(f"TODO SummarizeCode:{msg.model_dump_json()}") @@ -267,7 +268,7 @@ class Engineer(Role): dependencies = {Path(i) for i in await dependency.get(old_code_doc.root_relative_path)} task_doc = None design_doc = None - code_plan_and_change_doc = None + code_plan_and_change_doc = await self._get_any_code_plan_and_change() if await self._is_fixbug() else None for i in dependencies: if str(i.parent) == TASK_FILE_REPO: task_doc = await self.project_repo.docs.task.get(i.name) @@ -294,7 +295,8 @@ class Engineer(Role): ) return coding_doc - async def _new_code_actions(self, bug_fix=False): + async def _new_code_actions(self): + bug_fix = await self._is_fixbug() # Prepare file repos changed_src_files = self.project_repo.srcs.all_files if bug_fix else self.project_repo.srcs.changed_files changed_task_files = self.project_repo.docs.task.changed_files @@ -371,15 +373,32 @@ class Engineer(Role): self.set_todo(self.summarize_todos[0]) self.summarize_todos.pop(0) - async def _new_code_plan_and_change_action(self): + async def _new_code_plan_and_change_action(self, cause_by: str): """Create a WriteCodePlanAndChange action for subsequent to-do actions.""" files = self.project_repo.all_files - requirement_doc = await self.project_repo.docs.get(REQUIREMENT_FILENAME) - requirement = requirement_doc.content if requirement_doc else "" - code_plan_and_change_ctx = CodePlanAndChangeContext.loads(files, requirement=requirement) + options = {} + if cause_by != any_to_str(FixBug): + requirement_doc = await self.project_repo.docs.get(REQUIREMENT_FILENAME) + options["requirement"] = requirement_doc.content + else: + fixbug_doc = await self.project_repo.docs.get(BUGFIX_FILENAME) + options["issue"] = fixbug_doc.content + code_plan_and_change_ctx = CodePlanAndChangeContext.loads(files, **options) self.rc.todo = WriteCodePlanAndChange(i_context=code_plan_and_change_ctx, context=self.context, llm=self.llm) @property def action_description(self) -> str: """AgentStore uses this attribute to display to the user what actions the current role should take.""" return self.next_todo_action + + async def _is_fixbug(self) -> bool: + fixbug_doc = await self.project_repo.docs.get(BUGFIX_FILENAME) + return bool(fixbug_doc and fixbug_doc.content) + + async def _get_any_code_plan_and_change(self) -> Optional[Document]: + changed_files = self.project_repo.docs.code_plan_and_change.changed_files + for filename in changed_files.keys(): + doc = await self.project_repo.docs.code_plan_and_change.get(filename) + if doc and doc.content: + return doc + return None diff --git a/metagpt/schema.py b/metagpt/schema.py index ebd0e5be3..ca514f027 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -684,13 +684,14 @@ class BugFixContext(BaseContext): class CodePlanAndChangeContext(BaseModel): requirement: str = "" + issue: str = "" prd_filename: str = "" design_filename: str = "" task_filename: str = "" @staticmethod def loads(filenames: List, **kwargs) -> CodePlanAndChangeContext: - ctx = CodePlanAndChangeContext(requirement=kwargs.get("requirement", "")) + ctx = CodePlanAndChangeContext(requirement=kwargs.get("requirement", ""), issue=kwargs.get("issue", "")) for filename in filenames: filename = Path(filename) if filename.is_relative_to(PRDS_FILE_REPO): diff --git a/metagpt/team.py b/metagpt/team.py index 35f987b57..79c4c36aa 100644 --- a/metagpt/team.py +++ b/metagpt/team.py @@ -56,8 +56,10 @@ class Team(BaseModel): def serialize(self, stg_path: Path = None): stg_path = SERDESER_PATH.joinpath("team") if stg_path is None else stg_path team_info_path = stg_path.joinpath("team.json") + serialized_data = self.model_dump() + serialized_data["context"] = self.env.context.serialize() - write_json_file(team_info_path, self.model_dump()) + write_json_file(team_info_path, serialized_data) @classmethod def deserialize(cls, stg_path: Path, context: Context = None) -> "Team": @@ -71,6 +73,7 @@ class Team(BaseModel): team_info: dict = read_json_file(team_info_path) ctx = context or Context() + ctx.deserialize(team_info.pop("context", None)) team = Team(**team_info, context=ctx) return team diff --git a/setup.py b/setup.py index 8e1ad71c7..382e13a47 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ extras_require = { "selenium": ["selenium>4", "webdriver_manager", "beautifulsoup4"], "search-google": ["google-api-python-client==2.94.0"], "search-ddg": ["duckduckgo-search~=4.1.1"], - "ocr": ["paddlepaddle==2.4.2", "paddleocr>=2.0.1", "tabulate==0.9.0"], + "ocr": ["paddlepaddle==2.4.2", "paddleocr~=2.7.3", "tabulate==0.9.0"], "rag": [ "llama-index-core==0.10.15", "llama-index-embeddings-azure-openai==0.1.6", @@ -67,7 +67,7 @@ extras_require["dev"] = (["pylint~=3.0.3", "black~=23.3.0", "isort~=5.12.0", "pr setup( name="metagpt", - version="0.7.6", + version="0.8.0", description="The Multi-Agent Framework", long_description=long_description, long_description_content_type="text/markdown", diff --git a/tests/metagpt/serialize_deserialize/test_team.py b/tests/metagpt/serialize_deserialize/test_team.py index dbd38422d..6312e1fde 100644 --- a/tests/metagpt/serialize_deserialize/test_team.py +++ b/tests/metagpt/serialize_deserialize/test_team.py @@ -8,6 +8,7 @@ from pathlib import Path import pytest +from metagpt.context import Context from metagpt.logs import logger from metagpt.roles import Architect, ProductManager, ProjectManager from metagpt.team import Team @@ -146,5 +147,21 @@ async def test_team_recover_multi_roles_save(mocker, context): await new_company.run(n_round=4) +@pytest.mark.asyncio +async def test_context(context): + context.kwargs.set("a", "a") + context.cost_manager.max_budget = 9 + company = Team(context=context) + + save_to = context.repo.workdir / "serial" + company.serialize(save_to) + + company.deserialize(save_to, Context()) + assert company.env.context.repo + assert company.env.context.repo.workdir == context.repo.workdir + assert company.env.context.kwargs.a == "a" + assert company.env.context.cost_manager.max_budget == context.cost_manager.max_budget + + if __name__ == "__main__": pytest.main([__file__, "-s"])