From b262e5df7b27177bcad7c366ceb5ebe8f107ae32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E4=BC=9F=E9=9F=AC?= Date: Wed, 11 Sep 2024 15:56:16 +0800 Subject: [PATCH 01/11] automated get image --- metagpt/prompts/di/engineer2.py | 2 + metagpt/prompts/di/team_leader.py | 6 +- metagpt/roles/di/engineer2.py | 4 ++ metagpt/strategy/experience_retriever.py | 15 ++++ metagpt/tools/libs/image_getter.py | 87 ++++++++++++++++++++++++ 5 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 metagpt/tools/libs/image_getter.py diff --git a/metagpt/prompts/di/engineer2.py b/metagpt/prompts/di/engineer2.py index 70e45acb5..140c1a929 100644 --- a/metagpt/prompts/di/engineer2.py +++ b/metagpt/prompts/di/engineer2.py @@ -76,6 +76,8 @@ Note: 19. When the requirement is simple, you don't need to create a plan, just do it right away. 20. If the code exists, use the Editor tool's open and edit commands to modify it. Since it is not a new code, do not use write_new_code. 21. When using the editor, pay attention to the editor's current directory. When you use editor tools, the paths must be either absolute or relative to the editor's current directory. +22. The default programming languages are HTML (.html), CSS (.css), and Pure JavaScript (.js). +23. When planning, consider whether images are needed. If you are developing a showcase website, start by using ImageGetter.get_image to obtain the necessary images. """ CURRENT_STATE = """ The current editor state is: diff --git a/metagpt/prompts/di/team_leader.py b/metagpt/prompts/di/team_leader.py index 8d85a8cf3..d5bf6cb72 100644 --- a/metagpt/prompts/di/team_leader.py +++ b/metagpt/prompts/di/team_leader.py @@ -32,8 +32,10 @@ Note: 9. Do not use the 'end' command when the current task remains unfinished; instead, use the 'finish_current_task' command to indicate completion before switching to the next task. 10. Do not use escape characters in json data, particularly within file paths. 11. Analyze the capabilities of team members and assign tasks to them based on user Requirements. If the requirements ask to ignore certain tasks, follow the requirements. -12. Add default web technologies: HTML (*.html), CSS (*.css), and JavaScript (*.js) to your requirements.If no specific programming language is required, include these technologies in the project requirements. Using instruction to forward this information to your team members. -13. If the the user message is a question. use 'reply to human' to respond to the question, and then end. +12. If the the user message is a question. use 'reply to human' to respond to the question, and then end. +13. Instructions and reply must be in the same language. +14. Default technology stack is HTML (.html), CSS (.css), and Pure JavaScript (.js). Web app is the default option when developing software. +15. You are the only one who decides the programming language for the software, so the instruction must contain the programming language. """ TL_THOUGHT_GUIDANCE = ( THOUGHT_GUIDANCE diff --git a/metagpt/roles/di/engineer2.py b/metagpt/roles/di/engineer2.py index 1a224623b..e235c5a43 100644 --- a/metagpt/roles/di/engineer2.py +++ b/metagpt/roles/di/engineer2.py @@ -18,6 +18,7 @@ from metagpt.schema import UserMessage from metagpt.strategy.experience_retriever import ENGINEER_EXAMPLE from metagpt.tools.libs.cr import CodeReview from metagpt.tools.libs.git import git_create_pull +from metagpt.tools.libs.image_getter import ImageGetter from metagpt.tools.libs.terminal import Terminal from metagpt.tools.tool_registry import register_tool from metagpt.utils.common import CodeParser, awrite @@ -42,6 +43,7 @@ class Engineer2(RoleZero): "SearchEnhancedQA", "Engineer2", "CodeReview", + "ImageGetter", ] # SWE Agent parameter run_eval: bool = False @@ -84,10 +86,12 @@ class Engineer2(RoleZero): ) else: # Default tool map + image_getter = ImageGetter() self.tool_execution_map.update( { "git_create_pull": git_create_pull, "Engineer2.write_new_code": self.write_new_code, + "ImageGetter.get_image": image_getter.get_image, "CodeReview.review": cr.review, "CodeReview.fix": cr.fix, "Terminal.run_command": self.terminal.run_command, diff --git a/metagpt/strategy/experience_retriever.py b/metagpt/strategy/experience_retriever.py index 416e16279..d5601a6a1 100644 --- a/metagpt/strategy/experience_retriever.py +++ b/metagpt/strategy/experience_retriever.py @@ -1026,6 +1026,21 @@ Thought: Now that the changes have been pushed to the remote repository, due to } ] ``` + +## example 11 +The requirements is a product website contain some goods including cap, dress and tshit. +I think the website should conatin the picture of the goods,but user did not provide, so i will get the image first. +```json +[ + { + "command_name": "ImageGetter.get_image", + "args": { + "search_term": "cap", + "save_file_path": "/tmp/workspace/images/cap.png", + } + } +] +``` """ WEB_SCRAPING_EXAMPLE = """ diff --git a/metagpt/tools/libs/image_getter.py b/metagpt/tools/libs/image_getter.py new file mode 100644 index 000000000..bda939ea9 --- /dev/null +++ b/metagpt/tools/libs/image_getter.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +import base64 +import os +import re +from pathlib import Path +from typing import Optional + +from playwright.async_api import Browser as Browser_ +from playwright.async_api import BrowserContext, Page, Playwright, async_playwright +from pydantic import BaseModel, ConfigDict, Field + +from metagpt.tools.tool_registry import register_tool +from metagpt.utils.proxy_env import get_proxy_from_env +from metagpt.utils.report import BrowserReporter + + +@register_tool(include_functions=["get_image"]) +class ImageGetter(BaseModel): + """ + A tool to get images. + """ + + model_config = ConfigDict(arbitrary_types_allowed=True) + + playwright: Optional[Playwright] = Field(default=None, exclude=True) + browser_instance: Optional[Browser_] = Field(default=None, exclude=True) + browser_ctx: Optional[BrowserContext] = Field(default=None, exclude=True) + page: Optional[Page] = Field(default=None, exclude=True) + headless: bool = Field(default=True) + proxy: Optional[dict] = Field(default_factory=get_proxy_from_env) + reporter: BrowserReporter = Field(default_factory=BrowserReporter) + + async def start(self) -> None: + """Starts Playwright and launches a browser""" + if self.playwright is None: + self.playwright = playwright = await async_playwright().start() + browser = self.browser_instance = await playwright.chromium.launch(headless=self.headless, proxy=self.proxy) + browser_ctx = self.browser_ctx = await browser.new_context() + self.page = await browser_ctx.new_page() + + async def get_image(self, search_term, save_file_path): + """ + Get an image related to the search term. + + Args: + search_term (str): The term to search for the image. + save_file_path (str): The file path where the image will + """ + # Seach image + url = f"https://unsplash.com/s/photos/{search_term}/" + if self.page is None: + await self.start() + await self.page.goto(url, wait_until="domcontentloaded") + # Wait for the element + try: + await self.page.wait_for_selector(".zNNw1 > div > img:nth-of-type(2)") + except TimeoutError: + return f"{search_term} not found. Please broaden the search term." + + image_base64 = await self.page.evaluate( + """async () => { + var img = document.querySelector('.zNNw1 > div > img:nth-of-type(2)'); + if (img && img.src) { + const response = await fetch(img.src); + if (response.ok) { + const blob = await response.blob(); + return await new Promise(resolve => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result); + reader.readAsDataURL(blob); + }); + } + } + return null; + }""" + ) + if image_base64: + file_path = Path(save_file_path) + os.makedirs(file_path.parent, exist_ok=True) + with open(save_file_path, "wb") as f: + imgstr = re.sub("data:image/.*?;base64,", "", image_base64) + image_data = base64.b64decode(imgstr) + f.write(image_data) + return f"{search_term} found. The image is saved in {save_file_path}." + else: + return f"{search_term} not found. Please broaden the search term." From 2857a990ff61ff2bc9410207a76da5bfd2908791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E4=BC=9F=E9=9F=AC?= Date: Wed, 11 Sep 2024 16:15:51 +0800 Subject: [PATCH 02/11] fix prompt issues. --- metagpt/prompts/di/team_leader.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/metagpt/prompts/di/team_leader.py b/metagpt/prompts/di/team_leader.py index d5bf6cb72..8d85a8cf3 100644 --- a/metagpt/prompts/di/team_leader.py +++ b/metagpt/prompts/di/team_leader.py @@ -32,10 +32,8 @@ Note: 9. Do not use the 'end' command when the current task remains unfinished; instead, use the 'finish_current_task' command to indicate completion before switching to the next task. 10. Do not use escape characters in json data, particularly within file paths. 11. Analyze the capabilities of team members and assign tasks to them based on user Requirements. If the requirements ask to ignore certain tasks, follow the requirements. -12. If the the user message is a question. use 'reply to human' to respond to the question, and then end. -13. Instructions and reply must be in the same language. -14. Default technology stack is HTML (.html), CSS (.css), and Pure JavaScript (.js). Web app is the default option when developing software. -15. You are the only one who decides the programming language for the software, so the instruction must contain the programming language. +12. Add default web technologies: HTML (*.html), CSS (*.css), and JavaScript (*.js) to your requirements.If no specific programming language is required, include these technologies in the project requirements. Using instruction to forward this information to your team members. +13. If the the user message is a question. use 'reply to human' to respond to the question, and then end. """ TL_THOUGHT_GUIDANCE = ( THOUGHT_GUIDANCE From d9dacb630541de34e7bcfeeac53938bdc8021048 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E4=BC=9F=E9=9F=AC?= Date: Wed, 11 Sep 2024 17:24:00 +0800 Subject: [PATCH 03/11] update prompt --- metagpt/strategy/experience_retriever.py | 6 +++--- metagpt/tools/libs/image_getter.py | 17 +++++++++-------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/metagpt/strategy/experience_retriever.py b/metagpt/strategy/experience_retriever.py index d5601a6a1..5cb0573b2 100644 --- a/metagpt/strategy/experience_retriever.py +++ b/metagpt/strategy/experience_retriever.py @@ -1027,9 +1027,9 @@ Thought: Now that the changes have been pushed to the remote repository, due to ] ``` -## example 11 -The requirements is a product website contain some goods including cap, dress and tshit. -I think the website should conatin the picture of the goods,but user did not provide, so i will get the image first. +## example 12 +The requirement is to create a product website featuring goods such as caps, dresses, and T-shirts. +I believe pictures would improve the site, so I will get the images first. ```json [ { diff --git a/metagpt/tools/libs/image_getter.py b/metagpt/tools/libs/image_getter.py index bda939ea9..1420f9d7f 100644 --- a/metagpt/tools/libs/image_getter.py +++ b/metagpt/tools/libs/image_getter.py @@ -39,25 +39,25 @@ class ImageGetter(BaseModel): browser_ctx = self.browser_ctx = await browser.new_context() self.page = await browser_ctx.new_page() - async def get_image(self, search_term, save_file_path): + async def get_image(self, search_term, image_save_path): """ Get an image related to the search term. Args: search_term (str): The term to search for the image. - save_file_path (str): The file path where the image will + image_save_path (str): The file path where the image will be saved. """ - # Seach image + # Search for images from https://unsplash.com/s/photos/ url = f"https://unsplash.com/s/photos/{search_term}/" if self.page is None: await self.start() await self.page.goto(url, wait_until="domcontentloaded") - # Wait for the element + # Wait until the image element is loaded try: await self.page.wait_for_selector(".zNNw1 > div > img:nth-of-type(2)") except TimeoutError: return f"{search_term} not found. Please broaden the search term." - + # Get the base64 code of the first retrieved image image_base64 = await self.page.evaluate( """async () => { var img = document.querySelector('.zNNw1 > div > img:nth-of-type(2)'); @@ -76,12 +76,13 @@ class ImageGetter(BaseModel): }""" ) if image_base64: - file_path = Path(save_file_path) + # Save image + file_path = Path(image_save_path) os.makedirs(file_path.parent, exist_ok=True) - with open(save_file_path, "wb") as f: + with open(image_save_path, "wb") as f: imgstr = re.sub("data:image/.*?;base64,", "", image_base64) image_data = base64.b64decode(imgstr) f.write(image_data) - return f"{search_term} found. The image is saved in {save_file_path}." + return f"{search_term} found. The image is saved in {image_save_path}." else: return f"{search_term} not found. Please broaden the search term." From 65dc47c90d105b5ef93b59ff9db94ca92492211e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E4=BC=9F=E9=9F=AC?= Date: Thu, 12 Sep 2024 13:26:35 +0800 Subject: [PATCH 04/11] fix code issues --- metagpt/strategy/experience_retriever.py | 2 +- metagpt/tools/libs/image_getter.py | 61 +++++++++++------------- 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/metagpt/strategy/experience_retriever.py b/metagpt/strategy/experience_retriever.py index 5cb0573b2..e974e41a0 100644 --- a/metagpt/strategy/experience_retriever.py +++ b/metagpt/strategy/experience_retriever.py @@ -845,7 +845,7 @@ Consider this example only after you have obtained the content of system design Suppose the system design and project schedule prescribes three files index.html, style.css, script.js, to follow the design and schedule, I will create a plan consisting of three tasks, each corresponding to the creation of one of the required files: `index.html`, `style.css`, and `script.js`. Here's the plan: - +[Optional] 0. **Task 0**: Obtain images before coding. 1. **Task 1**: Create `index.html` - This file will contain the HTML structure necessary for the game's UI. 2. **Task 2**: Create `style.css` - This file will define the CSS styles to make the game visually appealing and responsive. 3. **Task 3**: Create `script.js` - This file will contain the JavaScript code for the game logic and UI interactions. diff --git a/metagpt/tools/libs/image_getter.py b/metagpt/tools/libs/image_getter.py index 1420f9d7f..a8f982fa5 100644 --- a/metagpt/tools/libs/image_getter.py +++ b/metagpt/tools/libs/image_getter.py @@ -1,9 +1,5 @@ from __future__ import annotations -import base64 -import os -import re -from pathlib import Path from typing import Optional from playwright.async_api import Browser as Browser_ @@ -11,9 +7,28 @@ from playwright.async_api import BrowserContext, Page, Playwright, async_playwri from pydantic import BaseModel, ConfigDict, Field from metagpt.tools.tool_registry import register_tool +from metagpt.utils.common import decode_image from metagpt.utils.proxy_env import get_proxy_from_env from metagpt.utils.report import BrowserReporter +DOWNLOAD_PICTURE_JAVASCRIPT = """ +async () => {{ + var img = document.querySelector('{img_element_selector}'); + if (img && img.src) {{ + const response = await fetch(img.src); + if (response.ok) {{ + const blob = await response.blob(); + return await new Promise(resolve => {{ + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result); + reader.readAsDataURL(blob); + }}); + }} + }} + return null; +}} +""" + @register_tool(include_functions=["get_image"]) class ImageGetter(BaseModel): @@ -30,6 +45,8 @@ class ImageGetter(BaseModel): headless: bool = Field(default=True) proxy: Optional[dict] = Field(default_factory=get_proxy_from_env) reporter: BrowserReporter = Field(default_factory=BrowserReporter) + url: str = "https://unsplash.com/s/photos/{search_term}/" + img_element_selector: str = ".zNNw1 > div > img:nth-of-type(2)" async def start(self) -> None: """Starts Playwright and launches a browser""" @@ -44,45 +61,25 @@ class ImageGetter(BaseModel): Get an image related to the search term. Args: - search_term (str): The term to search for the image. + search_term (str): The term to search for the image. The search term must be in English; using any other language may lead to a mismatch. image_save_path (str): The file path where the image will be saved. """ # Search for images from https://unsplash.com/s/photos/ - url = f"https://unsplash.com/s/photos/{search_term}/" + if self.page is None: await self.start() - await self.page.goto(url, wait_until="domcontentloaded") + await self.page.goto(self.url.format(search_term=search_term), wait_until="domcontentloaded") # Wait until the image element is loaded try: - await self.page.wait_for_selector(".zNNw1 > div > img:nth-of-type(2)") + await self.page.wait_for_selector(self.img_element_selector) except TimeoutError: return f"{search_term} not found. Please broaden the search term." # Get the base64 code of the first retrieved image image_base64 = await self.page.evaluate( - """async () => { - var img = document.querySelector('.zNNw1 > div > img:nth-of-type(2)'); - if (img && img.src) { - const response = await fetch(img.src); - if (response.ok) { - const blob = await response.blob(); - return await new Promise(resolve => { - const reader = new FileReader(); - reader.onloadend = () => resolve(reader.result); - reader.readAsDataURL(blob); - }); - } - } - return null; - }""" + DOWNLOAD_PICTURE_JAVASCRIPT.format(img_element_selector=self.img_element_selector) ) if image_base64: - # Save image - file_path = Path(image_save_path) - os.makedirs(file_path.parent, exist_ok=True) - with open(image_save_path, "wb") as f: - imgstr = re.sub("data:image/.*?;base64,", "", image_base64) - image_data = base64.b64decode(imgstr) - f.write(image_data) + image = decode_image(image_base64) + image.save(image_save_path) return f"{search_term} found. The image is saved in {image_save_path}." - else: - return f"{search_term} not found. Please broaden the search term." + return f"{search_term} not found. Please broaden the search term." From d7abceb67f0df74ff5ba723cb5ffd8b0e7f3522d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E4=BC=9F=E9=9F=AC?= Date: Thu, 12 Sep 2024 13:46:01 +0800 Subject: [PATCH 05/11] update comment --- metagpt/tools/libs/image_getter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/tools/libs/image_getter.py b/metagpt/tools/libs/image_getter.py index a8f982fa5..ecbaaf510 100644 --- a/metagpt/tools/libs/image_getter.py +++ b/metagpt/tools/libs/image_getter.py @@ -61,7 +61,7 @@ class ImageGetter(BaseModel): Get an image related to the search term. Args: - search_term (str): The term to search for the image. The search term must be in English; using any other language may lead to a mismatch. + search_term (str): The term to search for the image. The search term must be in English. Using any other language may lead to a mismatch. image_save_path (str): The file path where the image will be saved. """ # Search for images from https://unsplash.com/s/photos/ From e0a104dd05b0cfc13859848176ec598c1b40eeff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E4=BC=9F=E9=9F=AC?= Date: Thu, 12 Sep 2024 15:52:14 +0800 Subject: [PATCH 06/11] write_new_code_fail --- metagpt/prompts/di/engineer2.py | 3 +++ metagpt/roles/di/engineer2.py | 11 +++++++++-- metagpt/roles/di/role_zero.py | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/metagpt/prompts/di/engineer2.py b/metagpt/prompts/di/engineer2.py index 73127a2be..1345f6f6b 100644 --- a/metagpt/prompts/di/engineer2.py +++ b/metagpt/prompts/di/engineer2.py @@ -103,6 +103,9 @@ WRITE_CODE_PROMPT = """ # Plan Status {plan_status} +# Current Coding File +{file_path} + # Further Instruction {instruction} diff --git a/metagpt/roles/di/engineer2.py b/metagpt/roles/di/engineer2.py index 1a224623b..6d5abfe2d 100644 --- a/metagpt/roles/di/engineer2.py +++ b/metagpt/roles/di/engineer2.py @@ -1,5 +1,6 @@ from __future__ import annotations +import re from pathlib import Path from pydantic import Field @@ -14,7 +15,7 @@ from metagpt.prompts.di.engineer2 import ( WRITE_CODE_SYSTEM_PROMPT, ) from metagpt.roles.di.role_zero import RoleZero -from metagpt.schema import UserMessage +from metagpt.schema import AIMessage, UserMessage from metagpt.strategy.experience_retriever import ENGINEER_EXAMPLE from metagpt.tools.libs.cr import CodeReview from metagpt.tools.libs.git import git_create_pull @@ -117,10 +118,16 @@ class Engineer2(RoleZero): plan_status, _ = self._get_plan_status() prompt = WRITE_CODE_PROMPT.format( user_requirement=self.planner.plan.goal, + file_path=path, plan_status=plan_status, instruction=instruction, ) - context = self.llm.format_msg(self.rc.memory.get(self.memory_k) + [UserMessage(content=prompt)]) + # Remove the json command in last message. + memory = self.rc.memory.get(self.memory_k) + pattern = r"```json.*?\s+(.*)\n```" + last_memory_content = re.sub(pattern, "", memory[-1].content, flags=re.DOTALL) + memory = memory[:-1] + [AIMessage(content=last_memory_content)] + context = self.llm.format_msg(memory + [UserMessage(content=prompt)]) async with EditorReporter(enable_llm_stream=True) as reporter: await reporter.async_report({"type": "code", "filename": Path(path).name, "src_path": path}, "meta") diff --git a/metagpt/roles/di/role_zero.py b/metagpt/roles/di/role_zero.py index ab8179618..2d1fb556a 100644 --- a/metagpt/roles/di/role_zero.py +++ b/metagpt/roles/di/role_zero.py @@ -430,7 +430,7 @@ class RoleZero(Role): for index, cmd in enumerate(commands) if index == index_of_first_exclusive or cmd["command_name"] not in self.exclusive_tool_commands ] - command_rsp = "```json\n" + json.dumps(commands, indent=4, ensure_ascii=False) + "\n```json" + command_rsp = "```json\n" + json.dumps(commands, indent=4, ensure_ascii=False) + "\n```" logger.info( "exclusive command more than one in current command list. change the command list.\n" + command_rsp ) From fdeb43d81ad87c896ebca4b1f9f526c0f78b045c Mon Sep 17 00:00:00 2001 From: zhanglei Date: Thu, 12 Sep 2024 16:00:03 +0800 Subject: [PATCH 07/11] update: create a pr to get git access token --- metagpt/tools/libs/git.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/metagpt/tools/libs/git.py b/metagpt/tools/libs/git.py index 9a33ee4c1..dab2f820f 100644 --- a/metagpt/tools/libs/git.py +++ b/metagpt/tools/libs/git.py @@ -2,6 +2,8 @@ # -*- coding: utf-8 -*- from __future__ import annotations +import urllib +from pathlib import Path from typing import Optional from github.Issue import Issue @@ -67,6 +69,19 @@ async def git_create_pull( from metagpt.utils.git_repository import GitRepository access_token = await get_env(key="access_token", app_name=app_name) + git_credentials_path = Path.home() / ".git-credentials" + with open(git_credentials_path, "r", encoding="utf-8") as f: + lines = f.readlines() + + for line in lines: + line = line.strip() + if not line: + continue + parsed_url = urllib.parse.urlparse(line) + if parsed_url.hostname == app_name: + colon_index = parsed_url.netloc.find(":") + at_index = parsed_url.netloc.find("@") + access_token = parsed_url.netloc[colon_index + 1 : at_index] return await GitRepository.create_pull( base=base, head=head, From 40b54b42c3275cf07629e1966ed2d61ac5e297e7 Mon Sep 17 00:00:00 2001 From: zhanglei Date: Thu, 12 Sep 2024 16:22:42 +0800 Subject: [PATCH 08/11] update: git.py --- metagpt/tools/libs/git.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/metagpt/tools/libs/git.py b/metagpt/tools/libs/git.py index dab2f820f..0f68418eb 100644 --- a/metagpt/tools/libs/git.py +++ b/metagpt/tools/libs/git.py @@ -64,11 +64,8 @@ async def git_create_pull( Returns: PullRequest: The created pull request. """ - - from metagpt.tools.libs import get_env from metagpt.utils.git_repository import GitRepository - access_token = await get_env(key="access_token", app_name=app_name) git_credentials_path = Path.home() / ".git-credentials" with open(git_credentials_path, "r", encoding="utf-8") as f: lines = f.readlines() @@ -78,7 +75,7 @@ async def git_create_pull( if not line: continue parsed_url = urllib.parse.urlparse(line) - if parsed_url.hostname == app_name: + if app_name in parsed_url.hostname: colon_index = parsed_url.netloc.find(":") at_index = parsed_url.netloc.find("@") access_token = parsed_url.netloc[colon_index + 1 : at_index] From d630d722dfa6a8546bda93bd3e640d6d851fc4fb Mon Sep 17 00:00:00 2001 From: zhanglei Date: Thu, 12 Sep 2024 16:34:31 +0800 Subject: [PATCH 09/11] update:git.py --- metagpt/tools/libs/git.py | 1 + 1 file changed, 1 insertion(+) diff --git a/metagpt/tools/libs/git.py b/metagpt/tools/libs/git.py index 0f68418eb..8a3e464f0 100644 --- a/metagpt/tools/libs/git.py +++ b/metagpt/tools/libs/git.py @@ -79,6 +79,7 @@ async def git_create_pull( colon_index = parsed_url.netloc.find(":") at_index = parsed_url.netloc.find("@") access_token = parsed_url.netloc[colon_index + 1 : at_index] + break return await GitRepository.create_pull( base=base, head=head, From 09f25460be440512a8b61810010692c328d8f9bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E4=BC=9F=E9=9F=AC?= Date: Thu, 12 Sep 2024 16:49:32 +0800 Subject: [PATCH 10/11] ignore the lastest command in write new code. --- metagpt/roles/di/engineer2.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/metagpt/roles/di/engineer2.py b/metagpt/roles/di/engineer2.py index 6d5abfe2d..1110b61c9 100644 --- a/metagpt/roles/di/engineer2.py +++ b/metagpt/roles/di/engineer2.py @@ -1,6 +1,5 @@ from __future__ import annotations -import re from pathlib import Path from pydantic import Field @@ -15,7 +14,7 @@ from metagpt.prompts.di.engineer2 import ( WRITE_CODE_SYSTEM_PROMPT, ) from metagpt.roles.di.role_zero import RoleZero -from metagpt.schema import AIMessage, UserMessage +from metagpt.schema import UserMessage from metagpt.strategy.experience_retriever import ENGINEER_EXAMPLE from metagpt.tools.libs.cr import CodeReview from metagpt.tools.libs.git import git_create_pull @@ -122,11 +121,9 @@ class Engineer2(RoleZero): plan_status=plan_status, instruction=instruction, ) - # Remove the json command in last message. - memory = self.rc.memory.get(self.memory_k) - pattern = r"```json.*?\s+(.*)\n```" - last_memory_content = re.sub(pattern, "", memory[-1].content, flags=re.DOTALL) - memory = memory[:-1] + [AIMessage(content=last_memory_content)] + # Sometimes the Engineer repeats the last command to respond. + # Replace the last command with a manual prompt to guide the Engineer to write new code. + memory = self.rc.memory.get(self.memory_k)[:-1] context = self.llm.format_msg(memory + [UserMessage(content=prompt)]) async with EditorReporter(enable_llm_stream=True) as reporter: From 190405ac3cc68ffc74f721ee81afe7e113f54dcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E4=BC=9F=E9=9F=AC?= Date: Thu, 12 Sep 2024 20:40:34 +0800 Subject: [PATCH 11/11] editor use tmpfile to backup --- metagpt/prompts/di/engineer2.py | 6 ++---- metagpt/roles/di/engineer2.py | 7 +++++-- metagpt/tools/libs/editor.py | 24 ++++++++++-------------- metagpt/tools/libs/linter.py | 1 + tests/metagpt/tools/libs/test_editor.py | 23 ++++++++++++++++++++++- 5 files changed, 40 insertions(+), 21 deletions(-) diff --git a/metagpt/prompts/di/engineer2.py b/metagpt/prompts/di/engineer2.py index 1345f6f6b..f9b814a43 100644 --- a/metagpt/prompts/di/engineer2.py +++ b/metagpt/prompts/di/engineer2.py @@ -80,10 +80,8 @@ Note: """ CURRENT_STATE = """ The current editor state is: -(Editor current directory: {editor_current_directory}) -(Editor open file: {editor_open_file}) -The current terminal state is: -(Terminal current directory: {terminal_current_directory}) +(Current directory: {current_directory}) +(Open file: {editor_open_file}) """ ENGINEER2_INSTRUCTION = ROLE_INSTRUCTION + EXTRA_INSTRUCTION.strip() diff --git a/metagpt/roles/di/engineer2.py b/metagpt/roles/di/engineer2.py index 1110b61c9..2fb1b2184 100644 --- a/metagpt/roles/di/engineer2.py +++ b/metagpt/roles/di/engineer2.py @@ -58,10 +58,13 @@ class Engineer2(RoleZero): Display the current terminal and editor state. This information will be dynamically added to the command prompt. """ + current_directory = (await self.terminal.run_command("pwd")).strip() + # Synchronize Terminal and Editor Working Directories + if str(self.editor.working_dir.absolute()).strip() != current_directory: + self.editor._set_workdir(current_directory) state = { "editor_open_file": self.editor.current_file, - "editor_current_directory": self.editor.working_dir.absolute(), - "terminal_current_directory": (await self.terminal.run_command("pwd")).strip(), + "current_directory": current_directory, } self.cmd_prompt_current_state = CURRENT_STATE.format(**state).strip() diff --git a/metagpt/tools/libs/editor.py b/metagpt/tools/libs/editor.py index 63d7f9fd9..2463c563a 100644 --- a/metagpt/tools/libs/editor.py +++ b/metagpt/tools/libs/editor.py @@ -514,6 +514,8 @@ class Editor(BaseModel): temp_file_path = "" src_abs_path = file_name.resolve() first_error_line = None + # The file to store previous content and will be removed automatically. + temp_backup_file = tempfile.NamedTemporaryFile("w", delete=True) try: # lint the original file @@ -556,10 +558,8 @@ class Editor(BaseModel): # because the env var will be set AFTER the agentskills is imported if self.enable_auto_lint: # BACKUP the original file - original_file_backup_path = file_name.parent / f".backup.{file_name.name}" - with original_file_backup_path.open("w") as f: - f.writelines(lines) - + temp_backup_file.writelines(lines) + temp_backup_file.flush() lint_error, first_error_line = self._lint_file(file_name) # Select the errors caused by the modification @@ -609,15 +609,13 @@ class Editor(BaseModel): linter_error_msg=LINTER_ERROR_MSG + lint_error, window_after_applied=self._print_window(file_name, show_line, n_added_lines + 20), window_before_applied=self._print_window( - original_file_backup_path, show_line, n_added_lines + 20 + Path(temp_backup_file.name), show_line, n_added_lines + 20 ), guidance_message=guidance_message, ).strip() # recover the original file - with original_file_backup_path.open() as fin, file_name.open("w") as fout: - fout.write(fin.read()) - original_file_backup_path.unlink() + shutil.move(temp_backup_file.name, src_abs_path) return lint_error_info except FileNotFoundError as e: @@ -635,18 +633,16 @@ class Editor(BaseModel): error_info = ERROR_GUIDANCE.format( linter_error_msg=LINTER_ERROR_MSG + str(e), window_after_applied=self._print_window(file_name, start or len(lines), 40), - window_before_applied=self._print_window(original_file_backup_path, start or len(lines), 40), + window_before_applied=self._print_window(Path(temp_backup_file.name), start or len(lines), 40), guidance_message=guidance_message, ).strip() # Clean up the temporary file if an error occurs - with original_file_backup_path.open() as fin, file_name.open("w") as fout: - fout.write(fin.read()) + shutil.move(temp_backup_file.name, src_abs_path) if temp_file_path and Path(temp_file_path).exists(): Path(temp_file_path).unlink() # logger.warning(f"An unexpected error occurred: {e}") raise Exception(f"{error_info}") from e - # Update the file information and print the updated content with file_name.open("r", encoding="utf-8") as file: n_total_lines = max(1, len(file.readlines())) @@ -798,7 +794,6 @@ class Editor(BaseModel): If you need to use it multiple times, wait for the next turn. """ file_name = self._try_fix_path(file_name) - ret_str = self._edit_file_impl( file_name, start=line_number, @@ -807,6 +802,7 @@ class Editor(BaseModel): is_insert=True, is_append=False, ) + self.resource.report(file_name, "path") return ret_str def append_file(self, file_name: str, content: str) -> str: @@ -821,7 +817,6 @@ class Editor(BaseModel): If you need to use it multiple times, wait for the next turn. """ file_name = self._try_fix_path(file_name) - ret_str = self._edit_file_impl( file_name, start=None, @@ -830,6 +825,7 @@ class Editor(BaseModel): is_insert=False, is_append=True, ) + self.resource.report(file_name, "path") return ret_str def search_dir(self, search_term: str, dir_path: str = "./") -> str: diff --git a/metagpt/tools/libs/linter.py b/metagpt/tools/libs/linter.py index 7720d6962..61de7acce 100644 --- a/metagpt/tools/libs/linter.py +++ b/metagpt/tools/libs/linter.py @@ -33,6 +33,7 @@ class Linter: self.languages = dict( python=self.py_lint, sql=self.fake_lint, # base_lint lacks support for full SQL syntax. Use fake_lint to bypass the validation. + css=self.fake_lint, # base_lint lacks support for css syntax. Use fake_lint to bypass the validation. ) self.all_lint_cmd = None diff --git a/tests/metagpt/tools/libs/test_editor.py b/tests/metagpt/tools/libs/test_editor.py index 0c5d64a9a..a716b3de4 100644 --- a/tests/metagpt/tools/libs/test_editor.py +++ b/tests/metagpt/tools/libs/test_editor.py @@ -545,7 +545,7 @@ def test_edit_file_by_replace_to_palce_empty(empty_file): editor.edit_file_by_replace( file_name=str(empty_file), to_replace="", new_content=EXPECTED_CONTENT_AFTER_REPLACE_TEXT.strip() ) - assert "is empty. Use the append method to add content." in str(exc_info.value) + assert "is empty. Use the append method to add content." in str(exc_info.value) def test_append_file(temp_file_path): @@ -604,6 +604,27 @@ def test_search_dir(tmp_path): assert "Another file with different content." not in result +def test_search_dir_in_default_dir(tmp_path): + editor = Editor() + dir_path = editor.working_dir / "test_dir" + dir_path.mkdir(exist_ok=True) + + # Create some files with specific content + (dir_path / "file1.txt").write_text("This is a test file with some content.") + (dir_path / "file2.txt").write_text("Another file with different content.") + sub_dir = dir_path / "sub_dir" + sub_dir.mkdir(exist_ok=True) + (sub_dir / "file3.txt").write_text("This file is inside a sub directory with some content.") + + search_term = "some content" + + result = editor.search_dir(search_term) + + assert "file1.txt" in result + assert "file3.txt" in result + assert "Another file with different content." not in result + + def test_search_file(temp_file_path): editor = Editor() file_path = temp_file_path