determine the content to be saved to the experience pool using cmd_prompt_exp_part.

This commit is contained in:
seehi 2024-07-12 19:30:33 +08:00
parent 39360b41c5
commit bf21bbf12e
13 changed files with 113 additions and 139 deletions

View file

@ -1,5 +1,6 @@
"""Action Node context builder."""
from typing import Any
from metagpt.exp_pool.context_builders.base import BaseContextBuilder
@ -17,17 +18,12 @@ Consider **Experiences** to generate a better answer.
class ActionNodeContextBuilder(BaseContextBuilder):
async def build(self, **kwargs) -> str:
async def build(self, req: Any) -> str:
"""Builds the action node context string.
Args:
**kwargs: Arbitrary keyword arguments, expecting 'req' as a key.
Returns:
str: The formatted context string using the request and formatted experiences.
If no experiences are available, returns the request as is.
If there are no experiences, returns the original `req`;
otherwise returns context with `req` and formatted experiences.
"""
req = kwargs.get("req", "")
exps = self.format_exps()
return ACTION_NODE_CONTEXT_TEMPLATE.format(req=req, exps=exps) if exps else req

View file

@ -16,8 +16,11 @@ class BaseContextBuilder(BaseModel, ABC):
exps: list[Experience] = []
@abstractmethod
async def build(self, **kwargs) -> Any:
"""Build context from parameters."""
async def build(self, req: Any) -> Any:
"""Build context from req.
Do not modify `req`. If modification is necessary, use copy.deepcopy to create a copy first.
"""
def format_exps(self) -> str:
"""Format experiences into a numbered list of strings.

View file

@ -1,34 +1,36 @@
"""RoleZero context builder."""
import copy
import re
from typing import Any
from metagpt.exp_pool.context_builders.base import BaseContextBuilder
class RoleZeroContextBuilder(BaseContextBuilder):
async def build(self, **kwargs) -> list[dict]:
"""Builds the context by updating the req with formatted experiences.
async def build(self, req: Any) -> list[dict]:
"""Builds the role zero context string.
Args:
**kwargs: Arbitrary keyword arguments, expecting 'req' as a key.
Returns:
list[dict]: The updated request with formatted experiences or the original request if no experiences are available.
Note:
1. The expected format for `req`, e.g., [{...}, {"role": "user", "content": "context"}, {"role": "user", "content": "context exp part"}].
2. Returns the original `req` if it is empty, incorrectly formatted or there are no experiences.
3. Creates a copy of req and replaces the example content in the copied req with actual experiences.
"""
req = kwargs.get("req", [])
if not req:
if not req or len(req) < 2:
return req
exps = self.format_exps()
if not exps:
return req
req[-1]["content"] = self.replace_example_content(req[-1].get("content", ""), exps)
req_copy = copy.deepcopy(req)
return req
req_copy[-2]["content"] = self.replace_example_content(req_copy[-2].get("content", ""), exps)
return req_copy
def replace_example_content(self, text: str, new_example_content: str) -> str:
return self.replace_content_between_markers(text, "# Example", "# Instruction", new_example_content)
return self.replace_content_between_markers(text, "# Example", "# Available Commands", new_example_content)
@staticmethod
def replace_content_between_markers(text: str, start_marker: str, end_marker: str, new_content: str) -> str:

View file

@ -1,6 +1,8 @@
"""Simple context builder."""
from typing import Any
from metagpt.exp_pool.context_builders.base import BaseContextBuilder
SIMPLE_CONTEXT_TEMPLATE = """
@ -20,5 +22,5 @@ Consider **Experiences** to generate a better answer.
class SimpleContextBuilder(BaseContextBuilder):
async def build(self, **kwargs) -> str:
return SIMPLE_CONTEXT_TEMPLATE.format(req=kwargs.get("req", ""), exps=self.format_exps())
async def build(self, req: Any) -> str:
return SIMPLE_CONTEXT_TEMPLATE.format(req=req, exps=self.format_exps())

View file

@ -200,7 +200,7 @@ class ExpCacheHandler(BaseModel):
async def _build_context(self) -> str:
self.context_builder.exps = self._exps
return await self.context_builder.build(**self.kwargs)
return await self.context_builder.build(self.kwargs["req"])
async def _execute_function(self):
self.kwargs["req"] = await self._build_context()

View file

@ -3,7 +3,6 @@
import copy
import json
from metagpt.exp_pool.context_builders import RoleZeroContextBuilder
from metagpt.exp_pool.serializers.simple import SimpleSerializer
@ -11,14 +10,15 @@ class RoleZeroSerializer(SimpleSerializer):
def serialize_req(self, req: list[dict]) -> str:
"""Serialize the request for database storage, ensuring it is a string.
This function does not modify `req`; it only extracts the necessary content from `req` because `req` may be very lengthy and could cause embedding errors.
Only extracts the necessary content from `req` because `req` may be very lengthy and could cause embedding errors.
Args:
req (list[dict]): The request to be serialized. Example:
[
{"role": "user", "content": "..."},
{"role": "assistant", "content": "..."},
{"role": "user", "content": "..."},
{"role": "user", "content": "context"},
{"role": "user", "content": "context exp part"},
]
Returns:
@ -28,12 +28,12 @@ class RoleZeroSerializer(SimpleSerializer):
return ""
filtered_req = self._filter_req(req)
self._clean_last_entry_content(filtered_req)
filtered_req.append(req[-1])
return json.dumps(filtered_req)
def _filter_req(self, req: list[dict]) -> list[dict]:
"""Filter the request to include only necessary items and the last entry.
"""Filter the `req` to include only necessary items.
Args:
req (list[dict]): The original request.
@ -45,20 +45,5 @@ class RoleZeroSerializer(SimpleSerializer):
filtered_req = [
copy.deepcopy(item) for item in req if "Command Editor.read executed: file_path" in item["content"]
]
filtered_req.append(copy.deepcopy(req[-1]))
return filtered_req
def _clean_last_entry_content(self, req: list[dict]):
"""Modifies the content of the last element in the request to remove unnecessary sections, making the request more concise."""
last_content = req[-1]["content"]
last_content = RoleZeroContextBuilder.replace_content_between_markers(
last_content, "# Data Structure", "# Current Plan", ""
)
last_content = RoleZeroContextBuilder.replace_content_between_markers(
last_content, "# Example", "# Instruction", ""
)
req[-1]["content"] = last_content

View file

@ -8,7 +8,16 @@ Note:
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 RoleZero.reply_to_human to report your progress.
"""
CMD_PROMPT_EXP_PART = """
# Current Plan
{plan_status}
# Current Task
{current_task}
# Instruction
{instruction}
"""
CMD_PROMPT = """
# Data Structure
class Task(BaseModel):
@ -18,21 +27,14 @@ class Task(BaseModel):
task_type: str = ""
assignee: str = ""
# Example
{example}
# Available Commands
{available_commands}
Special Command: Use {{"command_name": "end"}} to do nothing or indicate completion of all requirements and the end of actions.
# Current Plan
{plan_status}
# Current Task
{current_task}
# Example
{example}
# Instruction
{instruction}
{cmd_prompt_exp_part}
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.

View file

@ -16,6 +16,7 @@ from metagpt.exp_pool.serializers import RoleZeroSerializer
from metagpt.logs import logger
from metagpt.prompts.di.role_zero import (
CMD_PROMPT,
CMD_PROMPT_EXP_PART,
JSON_REPAIR_PROMPT,
ROLE_INSTRUCTION,
)
@ -144,13 +145,14 @@ class RoleZero(Role):
tool_info = json.dumps({tool.name: tool.schemas for tool in tools})
### Make Decision Dynamically ###
prompt = self.cmd_prompt.format(
cmd_prompt_exp_part = CMD_PROMPT_EXP_PART.format(
plan_status=plan_status,
current_task=current_task,
example=example,
available_commands=tool_info,
instruction=self.instruction.strip(),
)
prompt = self.cmd_prompt.format(
example=example, available_commands=tool_info, cmd_prompt_exp_part=cmd_prompt_exp_part
)
memory = self.rc.memory.get(self.memory_k)
if not self.browser.is_empty_page:
pattern = re.compile(r"Command Browser\.(\w+) executed")
@ -158,16 +160,24 @@ class RoleZero(Role):
if pattern.match(msg.content):
memory.insert(index, UserMessage(cause_by="browser", content=await self.browser.view()))
break
context = self.llm.format_msg(memory + [UserMessage(content=prompt)])
# print(*context, sep="\n" + "*" * 5 + "\n")
req = self.llm.format_msg(memory + [UserMessage(content=prompt), UserMessage(content=cmd_prompt_exp_part)])
async with ThoughtReporter(enable_llm_stream=True):
self.command_rsp = await self.llm_cached_aask(req=context, system_msgs=self.system_msg)
self.command_rsp = await self.llm_cached_aask(req=req, system_msgs=self.system_msg)
self.rc.memory.add(AIMessage(content=self.command_rsp))
return True
@exp_cache(context_builder=RoleZeroContextBuilder(), serializer=RoleZeroSerializer())
async def llm_cached_aask(self, *, req: list[dict], system_msgs: list[str]) -> str:
"""Use `exp_cache` to automatically manage experiences.
The `RoleZeroContextBuilder` attempts to add experiences to `req`.
The `RoleZeroSerializer` extracts essential parts of `req` for the experience pool, trimming lengthy entries to retain only necessary parts.
"""
# Remove the "cmd_prompt_exp_part", it is only used within the exp_cache decorator.
if req:
req.pop()
return await self.llm.aask(req, system_msgs=system_msgs)
async def _act(self) -> Message: