From d0486f8e11b9e1ff544444ea237b199b81e95874 Mon Sep 17 00:00:00 2001 From: shenchucheng Date: Sat, 11 May 2024 19:47:33 +0800 Subject: [PATCH 1/6] add cr reporter --- metagpt/actions/write_code.py | 2 +- metagpt/actions/write_code_review.py | 16 +++++++++++----- metagpt/utils/report.py | 8 ++++++-- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index 67b859d23..dc8a6dee5 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -153,7 +153,7 @@ class WriteCode(Action): root_path = self.context.src_workspace if self.context.src_workspace else "" coding_context.code_doc = Document(filename=coding_context.filename, root_path=str(root_path)) coding_context.code_doc.content = code - await reporter.async_report(self.repo.workdir / coding_context.code_doc.root_relative_path, "path") + await reporter.async_report(coding_context.code_doc, "document") return coding_context @staticmethod diff --git a/metagpt/actions/write_code_review.py b/metagpt/actions/write_code_review.py index f0faea701..1b9f9554b 100644 --- a/metagpt/actions/write_code_review.py +++ b/metagpt/actions/write_code_review.py @@ -17,6 +17,7 @@ from metagpt.const import REQUIREMENT_FILENAME from metagpt.logs import logger from metagpt.schema import CodingContext from metagpt.utils.common import CodeParser +from metagpt.utils.report import EditorReporter PROMPT_TEMPLATE = """ # System @@ -128,16 +129,21 @@ class WriteCodeReview(Action): i_context: CodingContext = Field(default_factory=CodingContext) @retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6)) - async def write_code_review_and_rewrite(self, context_prompt, cr_prompt, filename): + async def write_code_review_and_rewrite(self, context_prompt, cr_prompt, doc): + filename = doc.filename cr_rsp = await self._aask(context_prompt + cr_prompt) result = CodeParser.parse_block("Code Review Result", cr_rsp) if "LGTM" in result: return result, None # if LBTM, rewrite code - rewrite_prompt = f"{context_prompt}\n{cr_rsp}\n{REWRITE_CODE_TEMPLATE.format(filename=filename)}" - code_rsp = await self._aask(rewrite_prompt) - code = CodeParser.parse_code(text=code_rsp) + async with EditorReporter(enable_llm_stream=True) as reporter: + await reporter.async_report({"type": "code", "filename": filename, "src_path": doc.root_relative_path}, "meta") + rewrite_prompt = f"{context_prompt}\n{cr_rsp}\n{REWRITE_CODE_TEMPLATE.format(filename=filename)}" + code_rsp = await self._aask(rewrite_prompt) + code = CodeParser.parse_code(text=code_rsp) + doc.content = code + await reporter.async_report(doc, "document") return result, code async def run(self, *args, **kwargs) -> CodingContext: @@ -182,7 +188,7 @@ class WriteCodeReview(Action): f"len(self.i_context.code_doc.content)={len2}" ) result, rewrited_code = await self.write_code_review_and_rewrite( - context_prompt, cr_prompt, self.i_context.code_doc.filename + context_prompt, cr_prompt, self.i_context.code_doc ) if "LBTM" in result: iterative_code = rewrited_code diff --git a/metagpt/utils/report.py b/metagpt/utils/report.py index a61c77381..616a52f30 100644 --- a/metagpt/utils/report.py +++ b/metagpt/utils/report.py @@ -131,7 +131,11 @@ class ResourceReporter(BaseModel): def _format_data(self, value, name): data = self.model_dump(mode="json", exclude=("callback_url", "llm_stream")) - data["value"] = str(value) if isinstance(value, Path) else value + if isinstance(value, BaseModel): + value = value.model_dump(mode="json") + elif isinstance(value, Path): + value = str(value) + data["value"] = value data["name"] = name role = CURRENT_ROLE.get(None) if role: @@ -263,7 +267,7 @@ class FileReporter(ResourceReporter): """Report file resource synchronously.""" return super().report(value, name) - async def async_report(self, value: Path, name: Literal["path", "meta", "content"] = "path"): + async def async_report(self, value: Path, name: Literal["path", "meta", "content", "document"] = "path"): """Report file resource asynchronously.""" return await super().async_report(value, name) From 2079cbf3ae69ff2ae9d67d6d76fb9d963bf0c239 Mon Sep 17 00:00:00 2001 From: shenchucheng Date: Wed, 15 May 2024 14:34:15 +0800 Subject: [PATCH 2/6] report abs path --- metagpt/utils/report.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/metagpt/utils/report.py b/metagpt/utils/report.py index 616a52f30..85f9bfa22 100644 --- a/metagpt/utils/report.py +++ b/metagpt/utils/report.py @@ -135,6 +135,9 @@ class ResourceReporter(BaseModel): value = value.model_dump(mode="json") elif isinstance(value, Path): value = str(value) + + if name == "path": + value = os.path.abspath(value) data["value"] = value data["name"] = name role = CURRENT_ROLE.get(None) From 7745e09c397b78846723a4f1bfc5fdf17cd80649 Mon Sep 17 00:00:00 2001 From: shenchucheng Date: Fri, 17 May 2024 10:29:25 +0800 Subject: [PATCH 3/6] add thought reporter --- metagpt/roles/di/data_analyst.py | 5 +++-- metagpt/roles/di/data_interpreter.py | 4 +++- metagpt/roles/di/team_leader.py | 4 +++- metagpt/roles/role.py | 5 +++-- metagpt/utils/common.py | 2 +- metagpt/utils/report.py | 11 +++++++++++ 6 files changed, 24 insertions(+), 7 deletions(-) diff --git a/metagpt/roles/di/data_analyst.py b/metagpt/roles/di/data_analyst.py index 0fc95b9d6..fc298ea4c 100644 --- a/metagpt/roles/di/data_analyst.py +++ b/metagpt/roles/di/data_analyst.py @@ -20,6 +20,7 @@ from metagpt.strategy.thinking_command import ( ) from metagpt.tools.tool_recommend import BM25ToolRecommender from metagpt.utils.common import CodeParser +from metagpt.utils.report import ThoughtReporter class DataAnalyst(DataInterpreter): @@ -82,8 +83,8 @@ class DataAnalyst(DataInterpreter): available_commands=prepare_command_prompt(self.available_commands), ) context = self.llm.format_msg(self.working_memory.get() + [Message(content=prompt, role="user")]) - - rsp = await self.llm.aask(context) + async with ThoughtReporter(): + rsp = await self.llm.aask(context) self.commands = json.loads(CodeParser.parse_code(block=None, text=rsp)) self.rc.memory.add(Message(content=rsp, role="assistant")) diff --git a/metagpt/roles/di/data_interpreter.py b/metagpt/roles/di/data_interpreter.py index e147cbbe3..bdfc0e294 100644 --- a/metagpt/roles/di/data_interpreter.py +++ b/metagpt/roles/di/data_interpreter.py @@ -15,6 +15,7 @@ from metagpt.schema import Message, Task, TaskResult from metagpt.strategy.task_type import TaskType from metagpt.tools.tool_recommend import BM25ToolRecommender, ToolRecommender from metagpt.utils.common import CodeParser +from metagpt.utils.report import ThoughtReporter REACT_THINK_PROMPT = """ # User Requirement @@ -73,7 +74,8 @@ class DataInterpreter(Role): return True prompt = REACT_THINK_PROMPT.format(user_requirement=self.user_requirement, context=context) - rsp = await self.llm.aask(prompt) + async with ThoughtReporter(): + rsp = await self.llm.aask(prompt) rsp_dict = json.loads(CodeParser.parse_code(text=rsp)) self.working_memory.add(Message(content=rsp_dict["thoughts"], role="assistant")) need_action = rsp_dict["state"] diff --git a/metagpt/roles/di/team_leader.py b/metagpt/roles/di/team_leader.py index a1ef11fa6..2fa782ade 100644 --- a/metagpt/roles/di/team_leader.py +++ b/metagpt/roles/di/team_leader.py @@ -20,6 +20,7 @@ from metagpt.strategy.thinking_command import ( run_commands, ) from metagpt.utils.common import CodeParser +from metagpt.utils.report import ThoughtReporter class TeamLeader(Role): @@ -69,7 +70,8 @@ class TeamLeader(Role): ) context = self.llm.format_msg(self.get_memories(k=10) + [Message(content=prompt, role="user")]) - rsp = await self.llm.aask(context, system_msgs=[SYSTEM_PROMPT]) + async with ThoughtReporter(): + rsp = await self.llm.aask(context, system_msgs=[SYSTEM_PROMPT]) self.commands = json.loads(CodeParser.parse_code(text=rsp)) self.rc.memory.add(Message(content=rsp, role="assistant")) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index 1eaa77fa3..f6d26eeb1 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -47,6 +47,7 @@ from metagpt.strategy.planner import Planner from metagpt.utils.common import any_to_name, any_to_str, role_raise_decorator from metagpt.utils.project_repo import ProjectRepo from metagpt.utils.repair_llm_raw_output import extract_state_value_from_output +from metagpt.utils.report import ThoughtReporter if TYPE_CHECKING: from metagpt.environment import Environment # noqa: F401 @@ -381,8 +382,8 @@ class Role(SerializationMixin, ContextMixin, BaseModel): n_states=len(self.states) - 1, previous_state=self.rc.state, ) - - next_state = await self.llm.aask(prompt) + async with ThoughtReporter(): + next_state = await self.llm.aask(prompt) next_state = extract_state_value_from_output(next_state) logger.debug(f"{prompt=}") diff --git a/metagpt/utils/common.py b/metagpt/utils/common.py index e2520ef13..fd7fdcb7a 100644 --- a/metagpt/utils/common.py +++ b/metagpt/utils/common.py @@ -646,7 +646,7 @@ def role_raise_decorator(func): raise Exception(format_trackback_info(limit=None)) except Exception as e: if self.latest_observed_msg: - logger.warning( + logger.exception( "There is a exception in role's execution, in order to resume, " "we delete the newest role communication message in the role's memory." ) diff --git a/metagpt/utils/report.py b/metagpt/utils/report.py index 85f9bfa22..491688f3a 100644 --- a/metagpt/utils/report.py +++ b/metagpt/utils/report.py @@ -39,6 +39,7 @@ class BlockType(str, Enum): GALLERY = "Gallery" NOTEBOOK = "Notebook" DOCS = "Docs" + THOUGHT = "Thought" END_MARKER_NAME = "end_marker" @@ -259,6 +260,16 @@ class TaskReporter(ObjectReporter): block: Literal[BlockType.TASK] = BlockType.TASK +class ThoughtReporter(ObjectReporter): + """Reporter for object resources to Task Block.""" + + block: Literal[BlockType.THOUGHT] = BlockType.THOUGHT + + async def __aenter__(self): + await self.async_report({}) + return await super().__aenter__() + + class FileReporter(ResourceReporter): """File resource callback for reporting complete file paths. From 76e3a14d38caa1a6cbd138598e6cfd5a3b3b0d9d Mon Sep 17 00:00:00 2001 From: shenchucheng Date: Tue, 21 May 2024 10:28:24 +0800 Subject: [PATCH 4/6] add extra field for report --- metagpt/tools/libs/editor.py | 3 ++- metagpt/utils/report.py | 38 ++++++++++++++++++++++++------------ 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/metagpt/tools/libs/editor.py b/metagpt/tools/libs/editor.py index e032dcef5..a2670a2bd 100644 --- a/metagpt/tools/libs/editor.py +++ b/metagpt/tools/libs/editor.py @@ -100,7 +100,8 @@ class Editor: file_path=file_path, block_content=block_content, ) - self.resource.report(result.file_path, "path") + self.resource.report(result.file_path, "path", + extra={"type": "search", "line_range": {"start": start, "end": end}}) return result return None diff --git a/metagpt/utils/report.py b/metagpt/utils/report.py index 491688f3a..2d72af111 100644 --- a/metagpt/utils/report.py +++ b/metagpt/utils/report.py @@ -56,23 +56,23 @@ class ResourceReporter(BaseModel): callback_url: str = Field(METAGPT_REPORTER_DEFAULT_URL, description="The URL to which the report should be sent") _llm_task: Optional[asyncio.Task] = PrivateAttr(None) - def report(self, value: Any, name: str): + def report(self, value: Any, name: str, extra: Optional[dict] = None): """Synchronously report resource observation data. Args: value: The data to report. name: The type name of the data. """ - return self._report(value, name) + return self._report(value, name, extra) - async def async_report(self, value: Any, name: str): + async def async_report(self, value: Any, name: str, extra: Optional[dict] = None): """Asynchronously report resource observation data. Args: value: The data to report. name: The type name of the data. """ - return await self._async_report(value, name) + return await self._async_report(value, name, extra) @classmethod def set_report_fn(cls, fn: Callable): @@ -101,20 +101,20 @@ class ResourceReporter(BaseModel): """ cls._async_report = fn - def _report(self, value: Any, name: str): + def _report(self, value: Any, name: str, extra: Optional[dict] = None): if not self.callback_url: return - data = self._format_data(value, name) + data = self._format_data(value, name, extra) resp = requests.post(self.callback_url, json=data) resp.raise_for_status() return resp.text - async def _async_report(self, value: Any, name: str): + async def _async_report(self, value: Any, name: str, extra: Optional[dict] = None): if not self.callback_url: return - data = self._format_data(value, name) + data = self._format_data(value, name, extra) url = self.callback_url _result = urlparse(url) sessiion_kwargs = {} @@ -130,7 +130,7 @@ class ResourceReporter(BaseModel): resp.raise_for_status() return await resp.text() - def _format_data(self, value, name): + def _format_data(self, value, name, extra): data = self.model_dump(mode="json", exclude=("callback_url", "llm_stream")) if isinstance(value, BaseModel): value = value.model_dump(mode="json") @@ -147,6 +147,8 @@ class ResourceReporter(BaseModel): else: role_name = os.environ.get("METAGPT_ROLE") data["role"] = role_name + if extra: + data["extra"] = extra return data def __enter__(self): @@ -277,13 +279,23 @@ class FileReporter(ResourceReporter): if the file can be partially output for display first, use streaming callback. """ - def report(self, value: Union[Path, dict, Any], name: Literal["path", "meta", "content"] = "path"): + def report( + self, + value: Union[Path, dict, Any], + name: Literal["path", "meta", "content"] = "path", + extra: Optional[dict] = None, + ): """Report file resource synchronously.""" - return super().report(value, name) + return super().report(value, name, extra) - async def async_report(self, value: Path, name: Literal["path", "meta", "content", "document"] = "path"): + async def async_report( + self, + value: Union[Path, dict, Any], + name: Literal["path", "meta", "content"] = "path", + extra: Optional[dict] = None, + ): """Report file resource asynchronously.""" - return await super().async_report(value, name) + return await super().async_report(value, name, extra) class NotebookReporter(FileReporter): From 6a38d5173362b2fd34de6e8b50963a5a05499ea7 Mon Sep 17 00:00:00 2001 From: shenchucheng Date: Thu, 23 May 2024 21:16:17 +0800 Subject: [PATCH 5/6] report the search content result --- metagpt/tools/libs/editor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/metagpt/tools/libs/editor.py b/metagpt/tools/libs/editor.py index a2670a2bd..78560e375 100644 --- a/metagpt/tools/libs/editor.py +++ b/metagpt/tools/libs/editor.py @@ -100,8 +100,7 @@ class Editor: file_path=file_path, block_content=block_content, ) - self.resource.report(result.file_path, "path", - extra={"type": "search", "line_range": {"start": start, "end": end}}) + self.resource.report(result.file_path, "path", extra={"type": "search", "line": i, "symbol": symbol}) return result return None From 3afb8a87f3ab6525eebf6b414b7fd3e7363aea6c Mon Sep 17 00:00:00 2001 From: shenchucheng Date: Thu, 30 May 2024 11:00:05 +0800 Subject: [PATCH 6/6] undo thought reporter in the base role --- metagpt/roles/role.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index f6d26eeb1..1eaa77fa3 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -47,7 +47,6 @@ from metagpt.strategy.planner import Planner from metagpt.utils.common import any_to_name, any_to_str, role_raise_decorator from metagpt.utils.project_repo import ProjectRepo from metagpt.utils.repair_llm_raw_output import extract_state_value_from_output -from metagpt.utils.report import ThoughtReporter if TYPE_CHECKING: from metagpt.environment import Environment # noqa: F401 @@ -382,8 +381,8 @@ class Role(SerializationMixin, ContextMixin, BaseModel): n_states=len(self.states) - 1, previous_state=self.rc.state, ) - async with ThoughtReporter(): - next_state = await self.llm.aask(prompt) + + next_state = await self.llm.aask(prompt) next_state = extract_state_value_from_output(next_state) logger.debug(f"{prompt=}")