diff --git a/metagpt/prompts/di/data_analyst.py b/metagpt/prompts/di/data_analyst.py index 8450b2fe1..a0624f047 100644 --- a/metagpt/prompts/di/data_analyst.py +++ b/metagpt/prompts/di/data_analyst.py @@ -22,8 +22,7 @@ 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. 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. +2. 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 89caf9688..359094c8e 100644 --- a/metagpt/roles/di/data_analyst.py +++ b/metagpt/roles/di/data_analyst.py @@ -25,7 +25,6 @@ from metagpt.utils.common import CodeParser class DataAnalyst(DataInterpreter): name: str = "David" profile: str = "DataAnalyst" - goal: str = "Take on any data-related tasks, such as data analysis, machine learning, deep learning, web browsing, web scraping, web searching, web deployment, terminal operation, git and github operation, etc." react_mode: Literal["react"] = "react" max_react_loop: int = 20 # used for react mode task_result: TaskResult = None @@ -34,7 +33,6 @@ class DataAnalyst(DataInterpreter): Command.RESET_TASK, Command.REPLACE_TASK, Command.FINISH_CURRENT_TASK, - Command.CONTINUE_WITH_CURRENT_TASK, # Command.PUBLISH_MESSAGE, Command.ASK_HUMAN, Command.REPLY_TO_HUMAN, @@ -57,7 +55,7 @@ class DataAnalyst(DataInterpreter): self._set_state(0) # HACK: Init Planner, control it through dynamic thinking; Consider formalizing as a react mode - self.planner = Planner(goal="", working_memory=self.rc.working_memory, auto_run=True) + self.planner = Planner(goal=self.goal, working_memory=self.rc.working_memory, auto_run=True) return self @@ -71,14 +69,11 @@ class DataAnalyst(DataInterpreter): example = KeywordExpRetriever().retrieve(self.user_requirement) else: self.working_memory.add_batch(self.rc.news) - context = "\n\n".join([str(mem) for mem in self.working_memory.get()]) - example = KeywordExpRetriever().retrieve(context) plan_status = self.planner.plan.model_dump(include=["goal", "tasks"]) for task in plan_status["tasks"]: task.pop("code") task.pop("result") - task.pop("is_success") prompt = CMD_PROMPT.format( plan_status=plan_status, example=example, @@ -101,8 +96,6 @@ class DataAnalyst(DataInterpreter): self.planner.plan.current_task.is_success = ( is_success # mark is_success, determine is_finished later in thinking ) - - # FIXME: task result is always overwritten by the last act, whereas it can be made of of multiple acts self.task_result = TaskResult(code=code, result=result, is_success=is_success) return Message(content="Task completed", role="assistant", sent_from=self._setting, cause_by=WriteAnalysisCode) diff --git a/metagpt/strategy/experience_retriever.py b/metagpt/strategy/experience_retriever.py index 7beb1b404..edc49a1a0 100644 --- a/metagpt/strategy/experience_retriever.py +++ b/metagpt/strategy/experience_retriever.py @@ -159,101 +159,44 @@ class SimpleExpRetriever(ExpRetriever): class KeywordExpRetriever(ExpRetriever): """An experience retriever that returns examples based on keywords in the context.""" + EXAMPLE: dict = { + "deploy": """ + ## example 1 + User Requirement: launch a service from workspace/web_snake_game/web_snake_game, and deploy it to public + Explanation: Launching a service requires Terminal tool with daemon mode, write this into task instruction. + ```json + [ + { + "command_name": "append_task", + "args": { + "task_id": "1", + "dependent_task_ids": [], + "instruction": "Use the Terminal tool to launch the service in daemon mode", + "assignee": "David" + } + }, + { + "command_name": "append_task", + "args": { + "task_id": "2", + "dependent_task_ids": ["1"], + "instruction": "Test the service with a simple request", + "assignee": "David" + } + }, + { + "command_name": "append_task", + "args": { + "task_id": "3", + "dependent_task_ids": ["2"], + "instruction": "Deploy the service to public", + "assignee": "David" + } + }, + """ + } + def retrieve(self, context: str) -> str: if "deploy" in context.lower(): - return DEPLOY_EXAMPLE - elif "issue" in context.lower(): - return FIX_ISSUE_EXAMPLE + return self.EXAMPLE["deploy"] return "" - - -DEPLOY_EXAMPLE = """ -## example 1 -User Requirement: launch a service from workspace/web_snake_game/web_snake_game, and deploy it to public -Explanation: Launching a service requires Terminal tool with daemon mode, write this into task instruction. -```json -[ - { - "command_name": "append_task", - "args": { - "task_id": "1", - "dependent_task_ids": [], - "instruction": "Use the Terminal tool to launch the service in daemon mode", - "assignee": "David" - } - }, - { - "command_name": "append_task", - "args": { - "task_id": "2", - "dependent_task_ids": ["1"], - "instruction": "Test the service with a simple request", - "assignee": "David" - } - }, - { - "command_name": "append_task", - "args": { - "task_id": "3", - "dependent_task_ids": ["2"], - "instruction": "Deploy the service to public", - "assignee": "David" - } - }, -""" - - -FIX_ISSUE_EXAMPLE = """ -## example 1 -User Requirement: Write a fix for this issue: https://github.com/xxx/xxx/issues/xxx, and commit and push your changes. -Explanation: The requirement is for software development, focusing on fixing an issue in an existing repository. The process is broken down into several steps, each demanding specific actions and tools. -```json -[ - { - "command_name": "append_task", - "args": { - "task_id": "1", - "dependent_task_ids": [], - "instruction": "Read the issue description to understand the problem using the Browser tool.", - "assignee": "David" - } - }, - { - "command_name": "append_task", - "args": { - "task_id": "2", - "dependent_task_ids": ["1"], - "instruction": "Clone the repository using the Terminal tool.", - "assignee": "David" - } - }, - { - "command_name": "append_task", - "args": { - "task_id": "3", - "dependent_task_ids": ["2"], - "instruction": "Use Editor to search the relevant function(s), then diagnose and identify the source of the problem.", - "assignee": "David" - } - }, - { - "command_name": "append_task", - "args": { - "task_id": "4", - "dependent_task_ids": ["3"], - "instruction": "Use Editor tool to fix the problem in the corresponding file(s).", - "assignee": "David" - } - }, - { - "command_name": "append_task", - "args": { - "task_id": "5", - "dependent_task_ids": ["4"], - "instruction": "Commit, push the changes to the repository.", - "assignee": "David" - } - }, -] -``` -""" diff --git a/metagpt/strategy/thinking_command.py b/metagpt/strategy/thinking_command.py index 4a666e926..cde16e714 100644 --- a/metagpt/strategy/thinking_command.py +++ b/metagpt/strategy/thinking_command.py @@ -36,12 +36,7 @@ class Command(Enum): FINISH_CURRENT_TASK = CommandDef( name="finish_current_task", signature="finish_current_task()", - desc="Finishes current task, set Task.is_finished=True, set current task to next task. You should not finish current task if task instruction has not been fulfilled.", - ) - CONTINUE_WITH_CURRENT_TASK = CommandDef( - name="continue_with_current_task", - signature="continue_with_current_task()", - desc="Continue with the current task, use this if you think you need more actions to achieve what is prescribed by the task instruction.", + desc="Finishes current task, set Task.is_finished=True, set current task to next task", ) # commands for env interaction @@ -101,13 +96,7 @@ def run_plan_command(role: Role, cmd: list[dict]): elif cmd["command_name"] == Command.RESET_TASK.cmd_name: role.planner.plan.reset_task(**cmd["args"]) elif cmd["command_name"] == Command.REPLACE_TASK.cmd_name: - new_task = Task( - task_id=cmd["args"]["task_id"], - dependent_task_ids=cmd["args"]["new_dependent_task_ids"], - instruction=cmd["args"]["new_instruction"], - assignee=cmd["args"]["new_assignee"], - ) - role.planner.plan.replace_task(new_task) + role.planner.plan.replace_task(Task(**cmd["args"])) elif cmd["command_name"] == Command.FINISH_CURRENT_TASK.cmd_name: if role.planner.plan.is_plan_finished(): return diff --git a/metagpt/tools/libs/__init__.py b/metagpt/tools/libs/__init__.py index 725ab73c9..f2ae30f03 100644 --- a/metagpt/tools/libs/__init__.py +++ b/metagpt/tools/libs/__init__.py @@ -12,7 +12,7 @@ from metagpt.tools.libs import ( # web_scraping, # email_login, terminal, - editor, + file_manager, browser, deployer, git, @@ -27,7 +27,7 @@ _ = ( # web_scraping, # email_login, terminal, - editor, + file_manager, browser, deployer, git, diff --git a/metagpt/tools/libs/browser.py b/metagpt/tools/libs/browser.py index 7fde804fe..6a7fd8e3b 100644 --- a/metagpt/tools/libs/browser.py +++ b/metagpt/tools/libs/browser.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from playwright.async_api import async_playwright from metagpt.const import DEFAULT_WORKSPACE_ROOT @@ -7,10 +5,10 @@ from metagpt.tools.tool_registry import register_tool from metagpt.utils.report import BrowserReporter -@register_tool(tags=["web", "browse", "scrape"]) +@register_tool() class Browser: """ - A tool for browsing the web and scraping. Don't initialize a new instance of this class if one already exists. + A tool for browsing the web. Don't initialize a new instance of this class if one already exists. Note: Combine searching and scrolling together to achieve most effective browsing. DON'T stick to one method. """ @@ -33,7 +31,7 @@ class Browser: self.current_page = page self.current_page_url = url print("Now on page ", url) - await self._view() + print(await self._view()) async def open_new_page(self, url: str): """open a new page in the browser and view the page""" @@ -53,12 +51,6 @@ class Browser: else: print(f"Page not found: {url}") - async def _view_page_html(self, keep_len: int = 5000) -> str: - """view the HTML content of current page, return the HTML content as a string. When executed, the content will be printed out""" - html = await self.current_page.content() - html_content = html.strip()[:keep_len] - return html_content - async def search_content_all(self, search_term: str) -> list[dict]: """search all occurences of search term in the current page and return the search results with their position. Useful if you have a keyword or sentence in mind and want to quickly narrow down the content relevant to it. @@ -150,12 +142,10 @@ class Browser: await self.current_page.screenshot(path=path) print(f"Screenshot saved to: {path}") - async def _view(self, keep_len: int = 5000) -> str: + async def _view(self) -> str: """simulate human viewing the current page, return the visible text with links""" visible_text_with_links = await self.current_page.evaluate(VIEW_CONTENT_JS) - print("The visible text and their links (if any): ", visible_text_with_links[:keep_len]) - # html_content = await self._view_page_html(keep_len=keep_len) - # print("The html content: ", html_content) + return visible_text_with_links async def scroll_current_page(self, offset: int = 500): """scroll the current page by offset pixels, negative value means scrolling up, will print out observed content after scrolling""" diff --git a/metagpt/tools/libs/editor.py b/metagpt/tools/libs/file_manager.py similarity index 91% rename from metagpt/tools/libs/editor.py rename to metagpt/tools/libs/file_manager.py index 132626701..588059dc5 100644 --- a/metagpt/tools/libs/editor.py +++ b/metagpt/tools/libs/file_manager.py @@ -20,7 +20,7 @@ class FileBlock(BaseModel): @register_tool() -class Editor: +class FileManager: """A tool for reading, understanding, writing, and editing files""" def __init__(self) -> None: @@ -42,8 +42,6 @@ class Editor: """ 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. - When both search_content and find_file are feasible, search_content is used in priority. - In searching, attempt different symbols of different granualities, e.g. "def some_function", "class SomeClass", a certain line of code, etc. Args: symbol (str): The symbol to search. @@ -60,9 +58,6 @@ class Editor: symbol: str = Field(default="", description="The symbol of interest in the block, empty if not applicable.") symbol_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 for root, _, files in os.walk(root_path or "."): for file in files: file_path = os.path.join(root, file) @@ -88,9 +83,6 @@ class Editor: ) 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" - ) return None def write_content(self, file_path: str, start_line: int, end_line: int, new_block_content: str = "") -> str: @@ -120,8 +112,8 @@ class Editor: # Lint the modified temporary file lint_passed, lint_message = self._lint_file(temp_file_path) - # if not lint_passed: - # return f"Linting the content at a temp file, failed with:\n{lint_message}" + if not lint_passed: + return f"Linting the content at a temp file, failed with:\n{lint_message}" # If linting passes, overwrite the original file with the temporary file shutil.move(temp_file_path, file_path) diff --git a/tests/metagpt/tools/libs/test_editor.py b/tests/metagpt/tools/libs/test_file_manager.py similarity index 85% rename from tests/metagpt/tools/libs/test_editor.py rename to tests/metagpt/tools/libs/test_file_manager.py index cc1783e53..8fad587b8 100644 --- a/tests/metagpt/tools/libs/test_editor.py +++ b/tests/metagpt/tools/libs/test_file_manager.py @@ -1,7 +1,7 @@ import pytest from metagpt.const import TEST_DATA_PATH -from metagpt.tools.libs.editor import Editor, FileBlock +from metagpt.tools.libs.file_manager import FileBlock, FileManager TEST_FILE_CONTENT = """ # this is line one @@ -13,7 +13,7 @@ def test_function_for_fm(): # this is the 7th line """.strip() -TEST_FILE_PATH = TEST_DATA_PATH / "tools/test_script_for_editor.py" +TEST_FILE_PATH = TEST_DATA_PATH / "tools/test_script_for_file_manager.py" @pytest.fixture @@ -36,7 +36,7 @@ EXPECTED_SEARCHED_BLOCK = FileBlock( def test_search_content(test_file): - block = Editor().search_content("def test_function_for_fm", root_path=TEST_DATA_PATH, window=3) + block = FileManager().search_content("def test_function_for_fm", root_path=TEST_DATA_PATH, window=3) assert block == EXPECTED_SEARCHED_BLOCK @@ -51,7 +51,7 @@ def test_function_for_fm(): def test_replace_content(test_file): - Editor().write_content( + FileManager().write_content( file_path=str(TEST_FILE_PATH), start_line=3, end_line=5, @@ -71,7 +71,7 @@ def test_function_for_fm(): def test_delete_content(test_file): - Editor().write_content(file_path=str(TEST_FILE_PATH), start_line=3, end_line=5) + FileManager().write_content(file_path=str(TEST_FILE_PATH), start_line=3, end_line=5) with open(TEST_FILE_PATH, "r") as f: new_content = f.read() assert new_content == EXPECTED_CONTENT_AFTER_DELETE @@ -90,7 +90,7 @@ def test_function_for_fm(): def test_insert_content(test_file): - Editor().write_content( + FileManager().write_content( file_path=str(TEST_FILE_PATH), start_line=3, end_line=-1, @@ -102,7 +102,7 @@ def test_insert_content(test_file): def test_new_content_wrong_indentation(test_file): - msg = Editor().write_content( + msg = FileManager().write_content( file_path=str(TEST_FILE_PATH), start_line=3, end_line=-1, @@ -112,7 +112,7 @@ def test_new_content_wrong_indentation(test_file): def test_new_content_format_issue(test_file): - msg = Editor().write_content( + msg = FileManager().write_content( file_path=str(TEST_FILE_PATH), start_line=3, end_line=-1,