Skip to content

Commit 1dfce07

Browse files
authored
Merge pull request FoundationAgents#1786 from garylin2099/simplify_rz
Simplify RoleZero code
2 parents 5aae56e + b19dee3 commit 1dfce07

File tree

3 files changed

+205
-166
lines changed

3 files changed

+205
-166
lines changed

metagpt/roles/di/engineer2.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from metagpt.tools.tool_registry import register_tool
2626
from metagpt.utils.common import CodeParser, awrite
2727
from metagpt.utils.report import EditorReporter
28+
from metagpt.utils.role_zero_utils import get_plan_status
2829

2930

3031
@register_tool(include_functions=["write_new_code"])
@@ -117,7 +118,7 @@ async def write_new_code(self, path: str, file_description: str = "") -> str:
117118
"""
118119
# If the path is not absolute, try to fix it with the editor's working directory.
119120
path = self.editor._try_fix_path(path)
120-
plan_status, _ = self._get_plan_status()
121+
plan_status, _ = get_plan_status(planner=self.planner)
121122
prompt = WRITE_CODE_PROMPT.format(
122123
user_requirement=self.planner.plan.goal,
123124
plan_status=plan_status,

metagpt/roles/di/role_zero.py

Lines changed: 29 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -5,35 +5,28 @@
55
import re
66
import traceback
77
from datetime import datetime
8-
from typing import Annotated, Callable, Dict, List, Literal, Optional, Tuple
8+
from typing import Annotated, Callable, Literal, Optional, Tuple
99

1010
from pydantic import Field, model_validator
1111

1212
from metagpt.actions import Action, UserRequirement
1313
from metagpt.actions.di.run_command import RunCommand
1414
from metagpt.actions.search_enhanced_qa import SearchEnhancedQA
15-
from metagpt.const import IMAGES
1615
from metagpt.exp_pool import exp_cache
1716
from metagpt.exp_pool.context_builders import RoleZeroContextBuilder
1817
from metagpt.exp_pool.serializers import RoleZeroSerializer
1918
from metagpt.logs import logger
2019
from metagpt.memory.role_zero_memory import RoleZeroLongTermMemory
2120
from metagpt.prompts.di.role_zero import (
22-
ASK_HUMAN_COMMAND,
23-
ASK_HUMAN_GUIDANCE_FORMAT,
2421
CMD_PROMPT,
2522
DETECT_LANGUAGE_PROMPT,
26-
END_COMMAND,
27-
JSON_REPAIR_PROMPT,
2823
QUICK_RESPONSE_SYSTEM_PROMPT,
2924
QUICK_THINK_EXAMPLES,
3025
QUICK_THINK_PROMPT,
3126
QUICK_THINK_SYSTEM_PROMPT,
3227
QUICK_THINK_TAG,
33-
REGENERATE_PROMPT,
3428
REPORT_TO_HUMAN_PROMPT,
3529
ROLE_INSTRUCTION,
36-
SUMMARY_PROBLEM_WHEN_DUPLICATE,
3730
SUMMARY_PROMPT,
3831
SYSTEM_PROMPT,
3932
)
@@ -45,13 +38,17 @@
4538
from metagpt.tools.libs.editor import Editor
4639
from metagpt.tools.tool_recommend import BM25ToolRecommender, ToolRecommender
4740
from metagpt.tools.tool_registry import register_tool
48-
from metagpt.utils.common import CodeParser, any_to_str, extract_and_encode_images
49-
from metagpt.utils.repair_llm_raw_output import (
50-
RepairType,
51-
repair_escape_error,
52-
repair_llm_raw_output,
53-
)
41+
from metagpt.utils.common import any_to_str
5442
from metagpt.utils.report import ThoughtReporter
43+
from metagpt.utils.role_zero_utils import (
44+
check_duplicates,
45+
format_terminal_output,
46+
get_plan_status,
47+
parse_browser_actions,
48+
parse_commands,
49+
parse_editor_result,
50+
parse_images,
51+
)
5552

5653

5754
@register_tool(include_functions=["ask_human", "reply_to_human"])
@@ -216,7 +213,7 @@ async def _think(self) -> bool:
216213
example = self._retrieve_experience()
217214

218215
### 2. Plan Status ###
219-
plan_status, current_task = self._get_plan_status()
216+
plan_status, current_task = get_plan_status(planner=self.planner)
220217

221218
### 3. Tool/Command Info ###
222219
tools = await self.tool_recommender.recommend_tools()
@@ -242,9 +239,9 @@ async def _think(self) -> bool:
242239

243240
### Recent Observation ###
244241
memory = self.rc.memory.get(self.memory_k)
245-
memory = await self.parse_browser_actions(memory)
246-
memory = await self.parse_editor_result(memory)
247-
memory = self.parse_images(memory)
242+
memory = await parse_browser_actions(memory, browser=self.browser)
243+
memory = await parse_editor_result(memory)
244+
memory = await parse_images(memory, llm=self.llm)
248245

249246
req = self.llm.format_msg(memory + [UserMessage(content=prompt)])
250247
state_data = dict(
@@ -255,7 +252,16 @@ async def _think(self) -> bool:
255252
async with ThoughtReporter(enable_llm_stream=True) as reporter:
256253
await reporter.async_report({"type": "react"})
257254
self.command_rsp = await self.llm_cached_aask(req=req, system_msgs=[system_prompt], state_data=state_data)
258-
self.command_rsp = await self._check_duplicates(req, self.command_rsp)
255+
256+
rsp_hist = [mem.content for mem in self.rc.memory.get()]
257+
self.command_rsp = await check_duplicates(
258+
req=req,
259+
command_rsp=self.command_rsp,
260+
rsp_hist=rsp_hist,
261+
llm=self.llm,
262+
respond_language=self.respond_language,
263+
)
264+
259265
return True
260266

261267
@exp_cache(context_builder=RoleZeroContextBuilder(), serializer=RoleZeroSerializer())
@@ -267,44 +273,6 @@ async def llm_cached_aask(self, *, req: list[dict], system_msgs: list[str], **kw
267273
"""
268274
return await self.llm.aask(req, system_msgs=system_msgs)
269275

270-
async def parse_browser_actions(self, memory: list[Message]) -> list[Message]:
271-
if not self.browser.is_empty_page:
272-
pattern = re.compile(r"Command Browser\.(\w+) executed")
273-
for index, msg in zip(range(len(memory), 0, -1), memory[::-1]):
274-
if pattern.search(msg.content):
275-
memory.insert(index, UserMessage(cause_by="browser", content=await self.browser.view()))
276-
break
277-
return memory
278-
279-
async def parse_editor_result(self, memory: list[Message], keep_latest_count=5) -> list[Message]:
280-
"""Retain the latest result and remove outdated editor results."""
281-
pattern = re.compile(r"Command Editor\.(\w+?) executed")
282-
new_memory = []
283-
i = 0
284-
for msg in reversed(memory):
285-
matches = pattern.findall(msg.content)
286-
if matches:
287-
i += 1
288-
if i > keep_latest_count:
289-
new_content = msg.content[: msg.content.find("Command Editor")]
290-
new_content += "\n".join([f"Command Editor.{match} executed." for match in matches])
291-
msg = UserMessage(content=new_content)
292-
new_memory.append(msg)
293-
# Reverse the new memory list so the latest message is at the end
294-
new_memory.reverse()
295-
return new_memory
296-
297-
def parse_images(self, memory: list[Message]) -> list[Message]:
298-
if not self.llm.support_image_input():
299-
return memory
300-
for msg in memory:
301-
if IMAGES in msg.metadata or msg.role != "user":
302-
continue
303-
images = extract_and_encode_images(msg.content)
304-
if images:
305-
msg.add_metadata(IMAGES, images)
306-
return memory
307-
308276
def _get_prefix(self) -> str:
309277
time_info = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
310278
return super()._get_prefix() + f" The current time is {time_info}."
@@ -313,7 +281,9 @@ async def _act(self) -> Message:
313281
if self.use_fixed_sop:
314282
return await super()._act()
315283

316-
commands, ok, self.command_rsp = await self._parse_commands(self.command_rsp)
284+
commands, ok, self.command_rsp = await parse_commands(
285+
command_rsp=self.command_rsp, llm=self.llm, exclusive_tool_commands=self.exclusive_tool_commands
286+
)
317287
self.rc.memory.add(AIMessage(content=self.command_rsp))
318288
if not ok:
319289
error_msg = commands
@@ -412,85 +382,6 @@ async def _quick_think(self) -> Tuple[Message, str]:
412382

413383
return rsp_msg, intent_result
414384

415-
async def _check_duplicates(self, req: list[dict], command_rsp: str, check_window: int = 10):
416-
past_rsp = [mem.content for mem in self.rc.memory.get(check_window)]
417-
if command_rsp in past_rsp and '"command_name": "end"' not in command_rsp:
418-
# Normal response with thought contents are highly unlikely to reproduce
419-
# If an identical response is detected, it is a bad response, mostly due to LLM repeating generated content
420-
# In this case, ask human for help and regenerate
421-
# TODO: switch to llm_cached_aask
422-
423-
# Hard rule to ask human for help
424-
if past_rsp.count(command_rsp) >= 3:
425-
if '"command_name": "Plan.finish_current_task",' in command_rsp:
426-
# Detect the duplicate of the 'Plan.finish_current_task' command, and use the 'end' command to finish the task.
427-
logger.warning(f"Duplicate response detected: {command_rsp}")
428-
return END_COMMAND
429-
problem = await self.llm.aask(
430-
req + [UserMessage(content=SUMMARY_PROBLEM_WHEN_DUPLICATE.format(language=self.respond_language))]
431-
)
432-
ASK_HUMAN_COMMAND[0]["args"]["question"] = ASK_HUMAN_GUIDANCE_FORMAT.format(problem=problem).strip()
433-
ask_human_command = "```json\n" + json.dumps(ASK_HUMAN_COMMAND, indent=4, ensure_ascii=False) + "\n```"
434-
return ask_human_command
435-
# Try correction by self
436-
logger.warning(f"Duplicate response detected: {command_rsp}")
437-
regenerate_req = req + [UserMessage(content=REGENERATE_PROMPT)]
438-
regenerate_req = self.llm.format_msg(regenerate_req)
439-
command_rsp = await self.llm.aask(regenerate_req)
440-
return command_rsp
441-
442-
async def _parse_commands(self, command_rsp) -> Tuple[List[Dict], bool]:
443-
"""Retrieves commands from the Large Language Model (LLM).
444-
445-
This function attempts to retrieve a list of commands from the LLM by
446-
processing the response (`self.command_rsp`). It handles potential errors
447-
during parsing and LLM response formats.
448-
449-
Returns:
450-
A tuple containing:
451-
- A boolean flag indicating success (True) or failure (False).
452-
"""
453-
try:
454-
commands = CodeParser.parse_code(block=None, lang="json", text=command_rsp)
455-
if commands.endswith("]") and not commands.startswith("["):
456-
commands = "[" + commands
457-
commands = json.loads(repair_llm_raw_output(output=commands, req_keys=[None], repair_type=RepairType.JSON))
458-
except json.JSONDecodeError as e:
459-
logger.warning(f"Failed to parse JSON for: {command_rsp}. Trying to repair...")
460-
commands = await self.llm.aask(
461-
msg=JSON_REPAIR_PROMPT.format(json_data=command_rsp, json_decode_error=str(e))
462-
)
463-
try:
464-
commands = json.loads(CodeParser.parse_code(block=None, lang="json", text=commands))
465-
except json.JSONDecodeError:
466-
# repair escape error of code and math
467-
commands = CodeParser.parse_code(block=None, lang="json", text=command_rsp)
468-
new_command = repair_escape_error(commands)
469-
commands = json.loads(
470-
repair_llm_raw_output(output=new_command, req_keys=[None], repair_type=RepairType.JSON)
471-
)
472-
except Exception as e:
473-
tb = traceback.format_exc()
474-
print(tb)
475-
error_msg = str(e)
476-
return error_msg, False, command_rsp
477-
478-
# 为了对LLM不按格式生成进行容错
479-
if isinstance(commands, dict):
480-
commands = commands["commands"] if "commands" in commands else [commands]
481-
482-
# Set the exclusive command flag to False.
483-
command_flag = [command["command_name"] not in self.exclusive_tool_commands for command in commands]
484-
if command_flag.count(False) > 1:
485-
# Keep only the first exclusive command
486-
index_of_first_exclusive = command_flag.index(False)
487-
commands = commands[: index_of_first_exclusive + 1]
488-
command_rsp = "```json\n" + json.dumps(commands, indent=4, ensure_ascii=False) + "\n```"
489-
logger.info(
490-
"exclusive command more than one in current command list. change the command list.\n" + command_rsp
491-
)
492-
return commands, True, command_rsp
493-
494385
async def _run_commands(self, commands) -> str:
495386
outputs = []
496387
for cmd in commands:
@@ -552,36 +443,9 @@ async def _run_special_command(self, cmd) -> str:
552443
elif cmd["command_name"] == "Terminal.run_command":
553444
tool_obj = self.tool_execution_map[cmd["command_name"]]
554445
tool_output = await tool_obj(**cmd["args"])
555-
if len(tool_output) <= 10:
556-
command_output += (
557-
f"\n[command]: {cmd['args']['cmd']} \n[command output] : {tool_output} (pay attention to this.)"
558-
)
559-
else:
560-
command_output += f"\n[command]: {cmd['args']['cmd']} \n[command output] : {tool_output}"
561-
446+
command_output = format_terminal_output(cmd=cmd, raw_output=tool_output)
562447
return command_output
563448

564-
def _get_plan_status(self) -> Tuple[str, str]:
565-
plan_status = self.planner.plan.model_dump(include=["goal", "tasks"])
566-
current_task = (
567-
self.planner.plan.current_task.model_dump(exclude=["code", "result", "is_success"])
568-
if self.planner.plan.current_task
569-
else ""
570-
)
571-
# format plan status
572-
# Example:
573-
# [GOAL] create a 2048 game
574-
# [TASK_ID 1] (finished) Create a Product Requirement Document (PRD) for the 2048 game. This task depends on tasks[]. [Assign to Alice]
575-
# [TASK_ID 2] ( ) Design the system architecture for the 2048 game. This task depends on tasks[1]. [Assign to Bob]
576-
formatted_plan_status = f"[GOAL] {plan_status['goal']}\n"
577-
if len(plan_status["tasks"]) > 0:
578-
formatted_plan_status += "[Plan]\n"
579-
for task in plan_status["tasks"]:
580-
formatted_plan_status += f"[TASK_ID {task['task_id']}] ({'finished' if task['is_finished'] else ' '}){task['instruction']} This task depends on tasks{task['dependent_task_ids']}. [Assign to {task['assignee']}]\n"
581-
else:
582-
formatted_plan_status += "No Plan \n"
583-
return formatted_plan_status, current_task
584-
585449
def _retrieve_experience(self) -> str:
586450
"""Default implementation of experience retrieval. Can be overwritten in subclasses."""
587451
context = [str(msg) for msg in self.rc.memory.get(self.memory_k)]

0 commit comments

Comments
 (0)