refactor: Refactor Message transmission & filtering

This commit is contained in:
莘权 马 2023-11-01 20:08:58 +08:00
parent 5e8ada5cff
commit 545d77ce0d
30 changed files with 658 additions and 296 deletions

View file

@ -4,6 +4,10 @@
@Time : 2023/5/11 14:43
@Author : alexanderwu
@File : engineer.py
@Modified By: mashenquan, 2023-11-1. Optimization:
1. Consolidate message reception and processing logic within `_observe`.
2. Fix bug: Add logic for handling asynchronous message processing when messages are not ready.
3. Supplemented the external transmission of internal messages.
"""
import asyncio
import shutil
@ -15,7 +19,7 @@ from metagpt.const import WORKSPACE_ROOT
from metagpt.logs import logger
from metagpt.roles import Role
from metagpt.schema import Message
from metagpt.utils.common import CodeParser
from metagpt.utils.common import CodeParser, get_object_name
from metagpt.utils.special_tokens import FILENAME_CODE_SEP, MSG_SEP
@ -75,7 +79,7 @@ class Engineer(Role):
self.use_code_review = use_code_review
if self.use_code_review:
self._init_actions([WriteCode, WriteCodeReview])
self._watch([WriteTasks])
self._watch([WriteTasks, WriteDesign])
self.todos = []
self.n_borg = n_borg
@ -96,7 +100,7 @@ class Engineer(Role):
return CodeParser.parse_str(block="Python package name", text=system_design_msg.content)
def get_workspace(self) -> Path:
msg = self._rc.memory.get_by_action(WriteDesign)[-1]
msg = self._rc.memory.get_by_action(WriteDesign.get_class_name())[-1]
if not msg:
return WORKSPACE_ROOT / "src"
workspace = self.parse_workspace(msg)
@ -119,17 +123,13 @@ class Engineer(Role):
file.write_text(code)
return file
def recv(self, message: Message) -> None:
self._rc.memory.add(message)
if message in self._rc.important_memory:
self.todos = self.parse_tasks(message)
async def _act_mp(self) -> Message:
# self.recreate_workspace()
todo_coros = []
for todo in self.todos:
todo_coro = WriteCode().run(
context=self._rc.memory.get_by_actions([WriteTasks, WriteDesign]), filename=todo
context=self._rc.memory.get_by_actions([WriteTasks.get_class_name(), WriteDesign.get_class_name()]),
filename=todo,
)
todo_coros.append(todo_coro)
@ -139,12 +139,13 @@ class Engineer(Role):
logger.info(todo)
logger.info(code_rsp)
# self.write_file(todo, code)
msg = Message(content=code_rsp, role=self.profile, cause_by=type(self._rc.todo))
msg = Message(content=code_rsp, role=self.profile, cause_by=get_object_name(self._rc.todo))
self._rc.memory.add(msg)
self.publish_message(msg)
del self.todos[0]
logger.info(f"Done {self.get_workspace()} generating.")
msg = Message(content="all done.", role=self.profile, cause_by=type(self._rc.todo))
msg = Message(content="all done.", role=self.profile, cause_by=get_object_name(self._rc.todo))
return msg
async def _act_sp(self) -> Message:
@ -155,15 +156,19 @@ class Engineer(Role):
# logger.info(code_rsp)
# code = self.parse_code(code_rsp)
file_path = self.write_file(todo, code)
msg = Message(content=code, role=self.profile, cause_by=type(self._rc.todo))
msg = Message(content=code, role=self.profile, cause_by=get_object_name(self._rc.todo))
self._rc.memory.add(msg)
self.publish_message(msg)
code_msg = todo + FILENAME_CODE_SEP + str(file_path)
code_msg_all.append(code_msg)
logger.info(f"Done {self.get_workspace()} generating.")
msg = Message(
content=MSG_SEP.join(code_msg_all), role=self.profile, cause_by=type(self._rc.todo), send_to="QaEngineer"
content=MSG_SEP.join(code_msg_all),
role=self.profile,
cause_by=get_object_name(self._rc.todo),
tx_to="QaEngineer",
)
return msg
@ -178,7 +183,8 @@ class Engineer(Role):
TODO: The goal is not to need it. After clear task decomposition, based on the design idea, you should be able to write a single file without needing other codes. If you can't, it means you need a clearer definition. This is the key to writing longer code.
"""
context = []
msg = self._rc.memory.get_by_actions([WriteDesign, WriteTasks, WriteCode])
msg_filters = [WriteDesign.get_class_name(), WriteTasks.get_class_name(), WriteCode.get_class_name()]
msg = self._rc.memory.get_by_actions(msg_filters)
for m in msg:
context.append(m.content)
context_str = "\n".join(context)
@ -193,20 +199,50 @@ class Engineer(Role):
logger.error("code review failed!", e)
pass
file_path = self.write_file(todo, code)
msg = Message(content=code, role=self.profile, cause_by=WriteCode)
msg = Message(content=code, role=self.profile, cause_by=WriteCode.get_class_name())
self._rc.memory.add(msg)
self.publish_message(msg)
code_msg = todo + FILENAME_CODE_SEP + str(file_path)
code_msg_all.append(code_msg)
logger.info(f"Done {self.get_workspace()} generating.")
msg = Message(
content=MSG_SEP.join(code_msg_all), role=self.profile, cause_by=type(self._rc.todo), send_to="QaEngineer"
content=MSG_SEP.join(code_msg_all),
role=self.profile,
cause_by=get_object_name(self._rc.todo),
tx_to="QaEngineer",
)
return msg
async def _act(self) -> Message:
"""Determines the mode of action based on whether code review is used."""
if not self._rc.todo:
return None
if self.use_code_review:
return await self._act_sp_precision()
return await self._act_sp()
async def _observe(self) -> int:
ret = await super(Engineer, self)._observe()
if ret == 0:
return ret
# Parse task lists
message_filter = {WriteTasks.get_class_name()}
for message in self._rc.news:
if not message.is_recipient(message_filter):
continue
self.todos = self.parse_tasks(message)
return ret
async def _think(self) -> None:
# In asynchronous scenarios, first check if the required messages are ready.
filters = {WriteTasks.get_class_name()}
msgs = self._rc.memory.get_by_actions(filters)
if not msgs:
self._rc.todo = None
return
await super(Engineer, self)._think()

View file

@ -4,6 +4,7 @@
@Time : 2023/5/11 14:43
@Author : alexanderwu
@File : qa_engineer.py
@Modified By: mashenquan, 2023-11-1. Standardize the usage of message filtering-related features.
"""
import os
from pathlib import Path
@ -48,7 +49,7 @@ class QaEngineer(Role):
return CodeParser.parse_str(block="Python package name", text=system_design_msg.content)
def get_workspace(self, return_proj_dir=True) -> Path:
msg = self._rc.memory.get_by_action(WriteDesign)[-1]
msg = self._rc.memory.get_by_action(WriteDesign.get_class_name())[-1]
if not msg:
return WORKSPACE_ROOT / "src"
workspace = self.parse_workspace(msg)
@ -97,11 +98,11 @@ class QaEngineer(Role):
msg = Message(
content=str(file_info),
role=self.profile,
cause_by=WriteTest,
sent_from=self.profile,
send_to=self.profile,
cause_by=WriteTest.get_class_name(),
tx_from=self.profile,
tx_to=self.profile,
)
self._publish_message(msg)
self.publish_message(msg)
logger.info(f"Done {self.get_workspace()}/tests generating.")
@ -131,8 +132,10 @@ class QaEngineer(Role):
recipient = parse_recipient(result_msg) # the recipient might be Engineer or myself
content = str(file_info) + FILENAME_CODE_SEP + result_msg
msg = Message(content=content, role=self.profile, cause_by=RunCode, sent_from=self.profile, send_to=recipient)
self._publish_message(msg)
msg = Message(
content=content, role=self.profile, cause_by=RunCode.get_class_name(), tx_from=self.profile, tx_to=recipient
)
self.publish_message(msg)
async def _debug_error(self, msg):
file_info, context = msg.content.split(FILENAME_CODE_SEP)
@ -141,14 +144,18 @@ class QaEngineer(Role):
self.write_file(file_name, code)
recipient = msg.sent_from # send back to the one who ran the code for another run, might be one's self
msg = Message(
content=file_info, role=self.profile, cause_by=DebugError, sent_from=self.profile, send_to=recipient
content=file_info,
role=self.profile,
cause_by=DebugError.get_class_name(),
tx_from=self.profile,
tx_to=recipient,
)
self._publish_message(msg)
self.publish_message(msg)
async def _observe(self) -> int:
await super()._observe()
self._rc.news = [
msg for msg in self._rc.news if msg.send_to == self.profile
msg for msg in self._rc.news if msg.is_recipient({self.profile})
] # only relevant msgs count as observed news
return len(self._rc.news)
@ -157,30 +164,31 @@ class QaEngineer(Role):
result_msg = Message(
content=f"Exceeding {self.test_round_allowed} rounds of tests, skip (writing code counts as a round, too)",
role=self.profile,
cause_by=WriteTest,
sent_from=self.profile,
send_to="",
cause_by=WriteTest.get_class_name(),
tx_from=self.profile,
)
return result_msg
code_filters = {WriteCode.get_class_name(), WriteCodeReview.get_class_name()}
test_filters = {WriteTest.get_class_name(), DebugError.get_class_name()}
run_filters = {RunCode.get_class_name()}
for msg in self._rc.news:
# Decide what to do based on observed msg type, currently defined by human,
# might potentially be moved to _think, that is, let the agent decides for itself
if msg.cause_by in [WriteCode, WriteCodeReview]:
if msg.is_recipient(code_filters):
# engineer wrote a code, time to write a test for it
await self._write_test(msg)
elif msg.cause_by in [WriteTest, DebugError]:
elif msg.is_recipient(test_filters):
# I wrote or debugged my test code, time to run it
await self._run_code(msg)
elif msg.cause_by == RunCode:
elif msg.is_recipient(run_filters):
# I ran my test code, time to fix bugs, if any
await self._debug_error(msg)
self.test_round += 1
result_msg = Message(
content=f"Round {self.test_round} of tests done",
role=self.profile,
cause_by=WriteTest,
sent_from=self.profile,
send_to="",
cause_by=WriteTest.get_class_name(),
tx_from=self.profile,
)
return result_msg

View file

@ -1,4 +1,8 @@
#!/usr/bin/env python
"""
@Modified By: mashenquan, 2023-11-1. Standardize the usage of message filtering-related features.
"""
import asyncio
@ -10,6 +14,7 @@ from metagpt.const import RESEARCH_PATH
from metagpt.logs import logger
from metagpt.roles import Role
from metagpt.schema import Message
from metagpt.utils.common import get_object_name
class Report(BaseModel):
@ -58,18 +63,22 @@ class Researcher(Role):
research_system_text = get_research_system_text(topic, self.language)
if isinstance(todo, CollectLinks):
links = await todo.run(topic, 4, 4)
ret = Message("", Report(topic=topic, links=links), role=self.profile, cause_by=type(todo))
ret = Message("", Report(topic=topic, links=links), role=self.profile, cause_by=get_object_name(todo))
elif isinstance(todo, WebBrowseAndSummarize):
links = instruct_content.links
todos = (todo.run(*url, query=query, system_text=research_system_text) for (query, url) in links.items())
summaries = await asyncio.gather(*todos)
summaries = list((url, summary) for i in summaries for (url, summary) in i.items() if summary)
ret = Message("", Report(topic=topic, summaries=summaries), role=self.profile, cause_by=type(todo))
ret = Message(
"", Report(topic=topic, summaries=summaries), role=self.profile, cause_by=get_object_name(todo)
)
else:
summaries = instruct_content.summaries
summary_text = "\n---\n".join(f"url: {url}\nsummary: {summary}" for (url, summary) in summaries)
content = await self._rc.todo.run(topic, summary_text, system_text=research_system_text)
ret = Message("", Report(topic=topic, content=content), role=self.profile, cause_by=type(self._rc.todo))
ret = Message(
"", Report(topic=topic, content=content), role=self.profile, get_object_name=type(self._rc.todo)
)
self._rc.memory.add(ret)
return ret

View file

@ -4,20 +4,32 @@
@Time : 2023/5/11 14:42
@Author : alexanderwu
@File : role.py
@Modified By: mashenquan, 2023-11-1. Optimization:
1. Merge the `recv` functionality into the `_observe` function. Future message reading operations will be
consolidated within the `_observe` function.
2. Standardize the message filtering for string label matching. Role objects can access the message labels
they've subscribed to through the `subscribed_tags` property.
3. Move the message receive buffer from the global variable `self._rc.env.memory` to the role's private variable
`self._rc.msg_buffer` for easier message identification and asynchronous appending of messages.
4. Standardize the way messages are passed: `publish_message` sends messages out, while `async_put_message` places
messages into the Role object's private message receive buffer. There are no other message transmit methods.
5. Standardize the parameters for the `run` function: the `test_message` parameter is used for testing purposes
only. In the normal workflow, you should use `publish_message` or `async_put_message` to transmit messages.
"""
from __future__ import annotations
from typing import Iterable, Type
from typing import Iterable, Set, Type
from pydantic import BaseModel, Field
# from metagpt.environment import Environment
from metagpt.config import CONFIG
from metagpt.actions import Action, ActionOutput
from metagpt.config import CONFIG
from metagpt.llm import LLM
from metagpt.logs import logger
from metagpt.memory import Memory, LongTermMemory
from metagpt.schema import Message
from metagpt.memory import LongTermMemory, Memory
from metagpt.schema import Message, MessageQueue
from metagpt.utils.common import get_class_name, get_object_name
from metagpt.utils.named import Named
PREFIX_TEMPLATE = """You are a {profile}, named {name}, your goal is {goal}, and the constraint is {constraints}. """
@ -49,6 +61,7 @@ ROLE_TEMPLATE = """Your response should be based on the previous conversation hi
class RoleSetting(BaseModel):
"""Role Settings"""
name: str
profile: str
goal: str
@ -64,12 +77,14 @@ class RoleSetting(BaseModel):
class RoleContext(BaseModel):
"""Role Runtime Context"""
env: 'Environment' = Field(default=None)
env: "Environment" = Field(default=None)
msg_buffer: MessageQueue = Field(default_factory=MessageQueue) # Message Buffer with Asynchronous Updates
memory: Memory = Field(default_factory=Memory)
long_term_memory: LongTermMemory = Field(default_factory=LongTermMemory)
state: int = Field(default=0)
todo: Action = Field(default=None)
watch: set[Type[Action]] = Field(default_factory=set)
watch: set[str] = Field(default_factory=set)
news: list[Type[Message]] = Field(default=[])
class Config:
@ -90,7 +105,7 @@ class RoleContext(BaseModel):
return self.memory.get()
class Role:
class Role(Named):
"""Role/Agent"""
def __init__(self, name="", profile="", goal="", constraints="", desc=""):
@ -118,7 +133,8 @@ class Role:
def _watch(self, actions: Iterable[Type[Action]]):
"""Listen to the corresponding behaviors"""
self._rc.watch.update(actions)
tags = [get_class_name(t) for t in actions]
self._rc.watch.update(tags)
# check RoleContext after adding watch actions
self._rc.check(self._role_id)
@ -128,7 +144,7 @@ class Role:
logger.debug(self._actions)
self._rc.todo = self._actions[self._rc.state]
def set_env(self, env: 'Environment'):
def set_env(self, env: "Environment"):
"""Set the environment in which the role works. The role can talk to the environment and can also receive messages by observing."""
self._rc.env = env
@ -137,6 +153,24 @@ class Role:
"""Get the role description (position)"""
return self._setting.profile
@property
def name(self):
"""Get virtual user name"""
return self._setting.name
@property
def subscribed_tags(self) -> Set:
"""The labels for messages to be consumed by the Role object."""
if self._rc.watch:
return self._rc.watch
return {
self.name,
self.get_object_name(),
self.profile,
f"{self.name}({self.profile})",
f"{self.name}({self.get_object_name()})",
}
def _get_prefix(self):
"""Get the role prefix"""
if self._setting.desc:
@ -150,94 +184,99 @@ class Role:
self._set_state(0)
return
prompt = self._get_prefix()
prompt += STATE_TEMPLATE.format(history=self._rc.history, states="\n".join(self._states),
n_states=len(self._states) - 1)
prompt += STATE_TEMPLATE.format(
history=self._rc.history, states="\n".join(self._states), n_states=len(self._states) - 1
)
next_state = await self._llm.aask(prompt)
logger.debug(f"{prompt=}")
if not next_state.isdigit() or int(next_state) not in range(len(self._states)):
logger.warning(f'Invalid answer of state, {next_state=}')
logger.warning(f"Invalid answer of state, {next_state=}")
next_state = "0"
self._set_state(int(next_state))
async def _act(self) -> Message:
# prompt = self.get_prefix()
# prompt += ROLE_TEMPLATE.format(name=self.profile, state=self.states[self.state], result=response,
# history=self.history)
logger.info(f"{self._setting}: ready to {self._rc.todo}")
response = await self._rc.todo.run(self._rc.important_memory)
# logger.info(response)
if isinstance(response, ActionOutput):
msg = Message(content=response.content, instruct_content=response.instruct_content,
role=self.profile, cause_by=type(self._rc.todo))
msg = Message(
content=response.content,
instruct_content=response.instruct_content,
role=self.profile,
cause_by=get_object_name(self._rc.todo),
tx_from=get_object_name(self),
)
else:
msg = Message(content=response, role=self.profile, cause_by=type(self._rc.todo))
self._rc.memory.add(msg)
# logger.debug(f"{response}")
msg = Message(
content=response,
role=self.profile,
cause_by=get_object_name(self._rc.todo),
tx_from=get_object_name(self),
)
return msg
async def _observe(self) -> int:
"""Observe from the environment, obtain important information, and add it to memory"""
if not self._rc.env:
return 0
env_msgs = self._rc.env.memory.get()
observed = self._rc.env.memory.get_by_actions(self._rc.watch)
self._rc.news = self._rc.memory.find_news(observed) # find news (previously unseen messages) from observed messages
for i in env_msgs:
self.recv(i)
"""Prepare new messages for processing from the message buffer and other sources."""
# Read unprocessed messages from the msg buffer.
self._rc.news = self._rc.msg_buffer.pop_all()
# Store the read messages in your own memory to prevent duplicate processing.
self._rc.memory.add_batch(self._rc.news)
# Design Rules:
# If you need to further categorize Message objects, you can do so using the Message.set_meta function.
# msg_buffer is a receiving buffer, avoid adding message data and operations to msg_buffer.
news_text = [f"{i.role}: {i.content[:20]}..." for i in self._rc.news]
if news_text:
logger.debug(f'{self._setting} observed: {news_text}')
logger.debug(f"{self._setting} observed: {news_text}")
return len(self._rc.news)
def _publish_message(self, msg):
def publish_message(self, msg):
"""If the role belongs to env, then the role's messages will be broadcast to env"""
if not msg:
return
if not self._rc.env:
# If env does not exist, do not publish the message
return
self._rc.env.publish_message(msg)
def async_put_message(self, message):
"""Place the message into the Role object's private message buffer."""
if not message:
return
self._rc.msg_buffer.push(message)
async def _react(self) -> Message:
"""Think first, then act"""
await self._think()
logger.debug(f"{self._setting}: {self._rc.state=}, will do {self._rc.todo}")
return await self._act()
def recv(self, message: Message) -> None:
"""add message to history."""
# self._history += f"\n{message}"
# self._context = self._history
if message in self._rc.memory.get():
return
self._rc.memory.add(message)
async def handle(self, message: Message) -> Message:
"""Receive information and reply with actions"""
# logger.debug(f"{self.name=}, {self.profile=}, {message.role=}")
self.recv(message)
return await self._react()
async def run(self, message=None):
async def run(self, test_message=None):
"""Observe, and think and act based on the results of the observation"""
if message:
if isinstance(message, str):
message = Message(message)
if isinstance(message, Message):
self.recv(message)
if isinstance(message, list):
self.recv(Message("\n".join(message)))
elif not await self._observe():
if test_message: # For test
seed = None
if isinstance(test_message, str):
seed = Message(test_message)
elif isinstance(test_message, Message):
seed = test_message
elif isinstance(test_message, list):
seed = Message("\n".join(test_message))
self.async_put_message(seed)
if not await self._observe():
# If there is no new information, suspend and wait
logger.debug(f"{self._setting}: no news. waiting.")
return
rsp = await self._react()
# Publish the reply to the environment, waiting for the next subscriber to process
self._publish_message(rsp)
# Reset the next action to be taken.
self._rc.todo = None
# Send the response message to the Environment object to have it relay the message to the subscribers.
self.publish_message(rsp)
return rsp
@property
def is_idle(self) -> bool:
"""If true, all actions have been executed."""
return not self._rc.news and not self._rc.todo and self._rc.msg_buffer.empty()

View file

@ -4,18 +4,20 @@
@Time : 2023/5/23 17:25
@Author : alexanderwu
@File : seacher.py
@Modified By: mashenquan, 2023-11-1. Standardize the usage of message filtering-related features.
"""
from metagpt.actions import ActionOutput, SearchAndSummarize
from metagpt.logs import logger
from metagpt.roles import Role
from metagpt.schema import Message
from metagpt.tools import SearchEngineType
from metagpt.utils.common import get_object_name
class Searcher(Role):
"""
Represents a Searcher role responsible for providing search services to users.
Attributes:
name (str): Name of the searcher.
profile (str): Role profile.
@ -23,17 +25,19 @@ class Searcher(Role):
constraints (str): Constraints or limitations for the searcher.
engine (SearchEngineType): The type of search engine to use.
"""
def __init__(self,
name: str = 'Alice',
profile: str = 'Smart Assistant',
goal: str = 'Provide search services for users',
constraints: str = 'Answer is rich and complete',
engine=SearchEngineType.SERPAPI_GOOGLE,
**kwargs) -> None:
def __init__(
self,
name: str = "Alice",
profile: str = "Smart Assistant",
goal: str = "Provide search services for users",
constraints: str = "Answer is rich and complete",
engine=SearchEngineType.SERPAPI_GOOGLE,
**kwargs,
) -> None:
"""
Initializes the Searcher role with given attributes.
Args:
name (str): Name of the searcher.
profile (str): Role profile.
@ -53,12 +57,16 @@ class Searcher(Role):
"""Performs the search action in a single process."""
logger.info(f"{self._setting}: ready to {self._rc.todo}")
response = await self._rc.todo.run(self._rc.memory.get(k=0))
if isinstance(response, ActionOutput):
msg = Message(content=response.content, instruct_content=response.instruct_content,
role=self.profile, cause_by=type(self._rc.todo))
msg = Message(
content=response.content,
instruct_content=response.instruct_content,
role=self.profile,
cause_by=get_object_name(self._rc.todo),
)
else:
msg = Message(content=response, role=self.profile, cause_by=type(self._rc.todo))
msg = Message(content=response, role=self.profile, cause_by=get_object_name(self._rc.todo))
self._rc.memory.add(msg)
return msg

View file

@ -4,6 +4,7 @@
@Time : 2023/9/13 12:23
@Author : femto Zheng
@File : sk_agent.py
@Modified By: mashenquan, 2023-11-1. Standardize the usage of message filtering-related features.
"""
from semantic_kernel.planning import SequentialPlanner
from semantic_kernel.planning.action_planner.action_planner import ActionPlanner
@ -14,6 +15,7 @@ from metagpt.actions.execute_task import ExecuteTask
from metagpt.logs import logger
from metagpt.roles import Role
from metagpt.schema import Message
from metagpt.utils.common import get_object_name
from metagpt.utils.make_sk_kernel import make_sk_kernel
@ -70,7 +72,7 @@ class SkAgent(Role):
result = (await self.plan.invoke_async()).result
logger.info(result)
msg = Message(content=result, role=self.profile, cause_by=type(self._rc.todo))
msg = Message(content=result, role=self.profile, cause_by=get_object_name(self._rc.todo))
self._rc.memory.add(msg)
# logger.debug(f"{response}")
self.publish_message(msg)
return msg