55import re
66import traceback
77from datetime import datetime
8- from typing import Annotated , Callable , Dict , List , Literal , Optional , Tuple
8+ from typing import Annotated , Callable , Literal , Optional , Tuple
99
1010from pydantic import Field , model_validator
1111
1212from metagpt .actions import Action , UserRequirement
1313from metagpt .actions .di .run_command import RunCommand
1414from metagpt .actions .search_enhanced_qa import SearchEnhancedQA
15- from metagpt .const import IMAGES
1615from metagpt .exp_pool import exp_cache
1716from metagpt .exp_pool .context_builders import RoleZeroContextBuilder
1817from metagpt .exp_pool .serializers import RoleZeroSerializer
1918from metagpt .logs import logger
2019from metagpt .memory .role_zero_memory import RoleZeroLongTermMemory
2120from 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)
4538from metagpt .tools .libs .editor import Editor
4639from metagpt .tools .tool_recommend import BM25ToolRecommender , ToolRecommender
4740from 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
5442from 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