diff --git a/metagpt/prompts/di/data_analyst.py b/metagpt/prompts/di/data_analyst.py index a0624f047..8450b2fe1 100644 --- a/metagpt/prompts/di/data_analyst.py +++ b/metagpt/prompts/di/data_analyst.py @@ -22,7 +22,8 @@ If plan is created, you should track the progress and update the plan accordingl Pay close attention to new user message, review the conversation history, use reply_to_human to respond to new user requirement. Note: 1. If you keeping encountering errors, unexpected situation, or you are not sure of proceeding, use ask_human to ask for help. -2. Each time you finish a task, use reply_to_human to report your progress. +2. Carefully review your progress at the current task, if your actions so far has not fulfilled the task instruction, you should continue with current task. Otherwise, finish current task. +3. Each time you finish a task, use reply_to_human to report your progress. Pay close attention to the Example provided, you can reuse the example for your current situation if it fits. You may use any of the available commands to create a plan or update the plan. You may output mutiple commands, they will be executed sequentially. diff --git a/metagpt/roles/di/data_analyst.py b/metagpt/roles/di/data_analyst.py index df0b8d288..0fc95b9d6 100644 --- a/metagpt/roles/di/data_analyst.py +++ b/metagpt/roles/di/data_analyst.py @@ -73,9 +73,9 @@ class DataAnalyst(DataInterpreter): # TODO: implement experience retrieval in multi-round setting plan_status = self.planner.plan.model_dump(include=["goal", "tasks"]) - for task in plan_status["tasks"]: - task.pop("code") - task.pop("result") + # for task in plan_status["tasks"]: + # task.pop("code") + # task.pop("result") prompt = CMD_PROMPT.format( plan_status=plan_status, example=example, diff --git a/metagpt/tools/libs/editor.py b/metagpt/tools/libs/editor.py index f973bad91..fcf892fdb 100644 --- a/metagpt/tools/libs/editor.py +++ b/metagpt/tools/libs/editor.py @@ -2,7 +2,7 @@ import os import shutil import subprocess -from pydantic import BaseModel, Field +from pydantic import BaseModel from metagpt.tools.tool_registry import register_tool from metagpt.utils.report import EditorReporter @@ -13,12 +13,6 @@ class FileBlock(BaseModel): file_path: str block_content: str - block_start_line: int - block_end_line: int - symbol: str = Field(default="", description="The symbol of interest in the block, empty if not applicable.") - symbol_start_line: int = Field( - default=-1, description="The line number of the symbol in the file, -1 if not applicable" - ) @register_tool() @@ -34,13 +28,19 @@ class Editor: f.write(content) self.resource.report(path, "path") - def read(self, path: str) -> str: + def read(self, path: str) -> FileBlock: """Read the whole content of a file.""" with open(path, "r") as f: self.resource.report(path, "path") - return f.read() + lines = f.readlines() + lines_with_num = [f"{i + 1:03}|{line}" for i, line in enumerate(lines)] + result = FileBlock( + file_path=path, + block_content="".join(lines_with_num), + ) + return result - def search_content(self, symbol: str, root_path: str = ".", window: int = 20) -> FileBlock: + def search_content(self, symbol: str, root_path: str = ".", window: int = 50) -> FileBlock: """ Search symbol in all files under root_path, return the context of symbol with window size Useful for locating class or function in a large codebase. Example symbol can be "def some_function", "class SomeClass", etc. @@ -48,7 +48,7 @@ class Editor: Args: symbol (str): The symbol to search. - root_path (str, optional): The root path to search in. If not provided, search in the current directory. Defaults to ".". + root_path (str, optional): The root path to search in, the path can be a folder or a file. If not provided, search in the current directory. Defaults to ".". window (int, optional): The window size to return. Defaults to 20. Returns: @@ -56,42 +56,50 @@ class Editor: class FileBlock(BaseModel): file_path: str block_content: str - block_start_line: int - block_end_line: int - symbol: str = Field(default="", description="The symbol of interest in the block, empty if not applicable.") - symbol_start_line: int = Field(default=-1, description="The line number of the symbol in the file, -1 if not applicable") """ if not os.path.exists(root_path): print(f"Currently at {os.getcwd()}. Path {root_path} does not exist.") return None + not_found_msg = ( + "symbol not found, you may try searching another one, or break down your search term to search a part of it" + ) + if os.path.isfile(root_path): + result = self._search_content_in_file(symbol, root_path, window) + if not result: + print(not_found_msg) + return result for root, _, files in os.walk(root_path or "."): for file in files: file_path = os.path.join(root, file) - if not file.endswith(".py"): - continue - with open(file_path, "r", encoding="utf-8") as f: - try: - lines = f.readlines() - except Exception: - continue - for i, line in enumerate(lines): - if symbol in line: - start = max(i - window, 0) - end = min(i + window, len(lines) - 1) - block_content = "".join(lines[start : end + 1]) - result = FileBlock( - file_path=file_path, - block_content=block_content, - block_start_line=start + 1, - block_end_line=end + 1, - symbol=symbol, - symbol_start_line=i + 1, - ) - self.resource.report(result.file_path, "path") - return result - print( - "symbol not found, you may try searching another one, or break down your search term to search a part of it" - ) + result = self._search_content_in_file(symbol, file_path, window) + if result: + # FIXME: This returns the first found result, not all results. + return result + print(not_found_msg) + return None + + def _search_content_in_file(self, symbol: str, file_path: str, window: int = 50) -> FileBlock: + print("search in", file_path) + if not file_path.endswith(".py"): + return None + with open(file_path, "r", encoding="utf-8") as f: + try: + lines = f.readlines() + except Exception: + return None + for i, line in enumerate(lines): + if symbol in line: + start = max(i - window, 0) + end = min(i + window, len(lines) - 1) + for row_num in range(start, end + 1): + lines[row_num] = f"{(row_num + 1):03}|{lines[row_num]}" + block_content = "".join(lines[start : end + 1]) + result = FileBlock( + file_path=file_path, + block_content=block_content, + ) + self.resource.report(result.file_path, "path") + return result return None def write_content(self, file_path: str, start_line: int, end_line: int, new_block_content: str = "") -> str: @@ -100,12 +108,13 @@ class Editor: 1. If the new block content is empty, the original block will be deleted. 2. If the new block content is not empty and end_line < start_line (e.g. set end_line = -1) the new block content will be inserted at start_line. 3. If the new block content is not empty and end_line >= start_line, the original block from start_line to end_line (both inclusively) will be replaced by the new block content. - This function can sometimes be used given a FileBlock upstream. Think carefully if you want to use block_start_line or symbol_start_line in the FileBlock as your start_line input. Your new_block_content will be placed at the start_line. + This function can sometimes be used given a FileBlock upstream. You should carefully review its row number. Determine the start_line and end_line based on the row number of the FileBlock. + The file content from start_line to end_line will be replaced by your new_block_content. DON'T replace more than you intend to. Args: file_path (str): The file path to write the new block content. - start_line (int): start line of the original block to be updated. - end_line (int): end line of the original block to be updated. + start_line (int): start line of the original block to be updated (inclusive). + end_line (int): end line of the original block to be updated (inclusive). new_block_content (str): The new block content to write. Returns: @@ -130,8 +139,6 @@ class Editor: new_file_block = FileBlock( file_path=file_path, block_content=new_block_content, - block_start_line=start_line, - block_end_line=-1 if end_line < start_line else start_line + new_block_content.count("\n"), ) self.resource.report(new_file_block.file_path, "path") @@ -143,6 +150,7 @@ class Editor: os.remove(temp_file_path) def _write_content(self, file_path: str, start_line: int, end_line: int, new_block_content: str = ""): + """start_line and end_line are both 1-based indices and inclusive.""" with open(file_path, "r") as file: lines = file.readlines()