refactor: @cause_by.setter

This commit is contained in:
莘权 马 2023-11-04 16:46:32 +08:00
parent 1febf168e7
commit d9775037b6
21 changed files with 73 additions and 123 deletions

View file

@ -66,7 +66,7 @@ class Trump(Role):
async def _act(self) -> Message:
logger.info(f"{self._setting}: ready to {self._rc.todo}")
msg_history = self._rc.memory.get_by_actions([ShoutOut.get_class_name()])
msg_history = self._rc.memory.get_by_actions([ShoutOut])
context = []
for m in msg_history:
context.append(str(m))
@ -77,7 +77,7 @@ class Trump(Role):
msg = Message(
content=rsp,
role=self.profile,
cause_by=ShoutOut.get_class_name(),
cause_by=ShoutOut,
tx_from=self.name,
tx_to=self.opponent_name,
)
@ -102,14 +102,14 @@ class Biden(Role):
await super()._observe()
# accept the very first human instruction (the debate topic) or messages sent (from opponent) to self,
# disregard own messages from the last round
message_filter = {BossRequirement.get_class_name(), self.name}
message_filter = {BossRequirement, self.name}
self._rc.news = [msg for msg in self._rc.news if msg.is_recipient(message_filter)]
return len(self._rc.news)
async def _act(self) -> Message:
logger.info(f"{self._setting}: ready to {self._rc.todo}")
msg_history = self._rc.memory.get_by_actions([BossRequirement.get_class_name(), ShoutOut.get_class_name()])
msg_history = self._rc.memory.get_by_actions([BossRequirement, ShoutOut])
context = []
for m in msg_history:
context.append(str(m))
@ -120,7 +120,7 @@ class Biden(Role):
msg = Message(
content=rsp,
role=self.profile,
cause_by=ShoutOut.get_class_name(),
cause_by=ShoutOut,
tx_from=self.name,
tx_to=self.opponent_name,
)

View file

@ -41,7 +41,7 @@ async def basic_planner_example():
role.import_semantic_skill_from_directory(SKILL_DIRECTORY, "WriterSkill")
role.import_skill(TextSkill(), "TextSkill")
# using BasicPlanner
await role.run(Message(content=task, cause_by=BossRequirement.get_class_name()))
await role.run(Message(content=task, cause_by=BossRequirement))
async def sequential_planner_example():
@ -55,7 +55,7 @@ async def sequential_planner_example():
role.import_semantic_skill_from_directory(SKILL_DIRECTORY, "WriterSkill")
role.import_skill(TextSkill(), "TextSkill")
# using BasicPlanner
await role.run(Message(content=task, cause_by=BossRequirement.get_class_name()))
await role.run(Message(content=task, cause_by=BossRequirement))
async def basic_planner_web_search_example():
@ -66,7 +66,7 @@ async def basic_planner_web_search_example():
role.import_skill(SkSearchEngine(), "WebSearchSkill")
# role.import_semantic_skill_from_directory(skills_directory, "QASkill")
await role.run(Message(content=task, cause_by=BossRequirement.get_class_name()))
await role.run(Message(content=task, cause_by=BossRequirement))
async def action_planner_example():
@ -77,7 +77,7 @@ async def action_planner_example():
role.import_skill(TimeSkill(), "time")
role.import_skill(TextSkill(), "text")
task = "What is the sum of 110 and 990?"
await role.run(Message(content=task, cause_by=BossRequirement.get_class_name())) # it will choose mathskill.Add
await role.run(Message(content=task, cause_by=BossRequirement)) # it will choose mathskill.Add
if __name__ == "__main__":

View file

@ -17,10 +17,9 @@ from metagpt.llm import LLM
from metagpt.logs import logger
from metagpt.utils.common import OutputParser
from metagpt.utils.custom_decoder import CustomDecoder
from metagpt.utils.named import Named
class Action(ABC, Named):
class Action(ABC):
def __init__(self, name: str = "", context=None, llm: LLM = None):
self.name: str = name
if llm is None:

View file

@ -58,7 +58,7 @@ class WriteCode(Action):
if self._is_invalid(filename):
return
message_filter = {WriteDesign.get_class_name()}
message_filter = {WriteDesign}
design = [i for i in context if i.is_recipient(message_filter)][0]
ws_name = CodeParser.parse_str(block="Python package name", text=design.content)

View file

@ -102,7 +102,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.get_class_name())[-1]
msg = self._rc.memory.get_by_action(WriteDesign)[-1]
if not msg:
return WORKSPACE_ROOT / "src"
workspace = self.parse_workspace(msg)
@ -130,7 +130,7 @@ class Engineer(Role):
todo_coros = []
for todo in self.todos:
todo_coro = WriteCode().run(
context=self._rc.memory.get_by_actions([WriteTasks.get_class_name(), WriteDesign.get_class_name()]),
context=self._rc.memory.get_by_actions([WriteTasks, WriteDesign]),
filename=todo,
)
todo_coros.append(todo_coro)
@ -185,7 +185,7 @@ 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_filters = [WriteDesign.get_class_name(), WriteTasks.get_class_name(), WriteCode.get_class_name()]
msg_filters = [WriteDesign, WriteTasks, WriteCode]
msg = self._rc.memory.get_by_actions(msg_filters)
for m in msg:
context.append(m.content)
@ -201,7 +201,7 @@ 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.get_class_name())
msg = Message(content=code, role=self.profile, cause_by=WriteCode)
self._rc.memory.add(msg)
self.publish_message(msg)
@ -231,7 +231,7 @@ class Engineer(Role):
return ret
# Parse task lists
message_filter = {WriteTasks.get_class_name()}
message_filter = {WriteTasks}
for message in self._rc.news:
if not message.is_recipient(message_filter):
continue
@ -241,7 +241,7 @@ class Engineer(Role):
async def _think(self) -> None:
# In asynchronous scenarios, first check if the required messages are ready.
filters = {WriteTasks.get_class_name()}
filters = {WriteTasks}
msgs = self._rc.memory.get_by_actions(filters)
if not msgs:
self._rc.todo = None

View file

@ -50,7 +50,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.get_class_name())[-1]
msg = self._rc.memory.get_by_action(WriteDesign)[-1]
if not msg:
return WORKSPACE_ROOT / "src"
workspace = self.parse_workspace(msg)
@ -99,7 +99,7 @@ class QaEngineer(Role):
msg = Message(
content=str(file_info),
role=self.profile,
cause_by=WriteTest.get_class_name(),
cause_by=WriteTest,
tx_from=self.profile,
tx_to=self.profile,
)
@ -133,9 +133,7 @@ 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.get_class_name(), tx_from=self.profile, tx_to=recipient
)
msg = Message(content=content, role=self.profile, cause_by=RunCode, tx_from=self.profile, tx_to=recipient)
self.publish_message(msg)
async def _debug_error(self, msg):
@ -147,7 +145,7 @@ class QaEngineer(Role):
msg = Message(
content=file_info,
role=self.profile,
cause_by=DebugError.get_class_name(),
cause_by=DebugError,
tx_from=self.profile,
tx_to=recipient,
)
@ -165,14 +163,14 @@ 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.get_class_name(),
cause_by=WriteTest,
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()}
code_filters = {WriteCode, WriteCodeReview}
test_filters = {WriteTest, DebugError}
run_filters = {RunCode}
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
@ -189,7 +187,7 @@ class QaEngineer(Role):
result_msg = Message(
content=f"Round {self.test_round} of tests done",
role=self.profile,
cause_by=WriteTest.get_class_name(),
cause_by=WriteTest,
tx_from=self.profile,
)
return result_msg

View file

@ -31,7 +31,6 @@ from metagpt.logs import logger
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}. """
@ -107,7 +106,7 @@ class RoleContext(BaseModel):
return self.memory.get()
class Role(Named):
class Role:
"""Role/Agent"""
def __init__(self, name="", profile="", goal="", constraints="", desc=""):
@ -174,10 +173,10 @@ class Role(Named):
return self._rc.watch
return {
self.name,
self.get_object_name(),
get_object_name(self),
self.profile,
f"{self.name}({self.profile})",
f"{self.name}({self.get_object_name()})",
f"{self.name}({get_object_name(self)})",
}
def _get_prefix(self):

View file

@ -24,7 +24,7 @@ from metagpt.const import (
MESSAGE_ROUTE_TO,
)
from metagpt.logs import logger
from metagpt.utils.common import get_class_name, get_object_name
from metagpt.utils.common import any_to_str
class RawMessage(TypedDict):
@ -129,11 +129,12 @@ class Message(BaseModel):
if k in attribute_names:
continue
if k == MESSAGE_ROUTE_FROM:
self.set_from(v)
self.set_from(any_to_str(v))
continue
if k == MESSAGE_ROUTE_CAUSE_BY:
self.meta_info[k] = v
if k == MESSAGE_ROUTE_TO or k == MESSAGE_ROUTE_CAUSE_BY:
self.set_cause_by(v)
continue
if k == MESSAGE_ROUTE_TO:
self.add_to(v)
continue
self.meta_info[k] = v
@ -161,18 +162,14 @@ class Message(BaseModel):
if key == MESSAGE_ROUTE_CAUSE_BY:
self.set_cause_by(val)
return
if key == MESSAGE_ROUTE_FROM:
self.set_from(any_to_str(val))
super().__setattr__(key, val)
def set_cause_by(self, val):
"""Update the value of `cause_by` in the `meta_info` and `routes` attributes."""
old_value = self.get_meta(MESSAGE_ROUTE_CAUSE_BY)
new_value = None
if isinstance(val, str):
new_value = val
elif not callable(val):
new_value = get_object_name(val)
else:
new_value = get_class_name(val)
new_value = any_to_str(val)
self.set_meta(MESSAGE_ROUTE_CAUSE_BY, new_value)
self.route.replace(old_value, new_value)

View file

@ -18,10 +18,9 @@ from metagpt.logs import logger
from metagpt.roles import Role
from metagpt.schema import Message
from metagpt.utils.common import NoMoneyException
from metagpt.utils.named import Named
class SoftwareCompany(BaseModel, Named):
class SoftwareCompany(BaseModel):
"""
Software Company: Possesses a team, SOP (Standard Operating Procedures), and a platform for instant messaging,
dedicated to writing executable code.
@ -55,8 +54,8 @@ class SoftwareCompany(BaseModel, Named):
Message(
role="BOSS",
content=idea,
cause_by=BossRequirement.get_class_name(),
tx_from=SoftwareCompany.get_class_name(),
cause_by=BossRequirement,
tx_from=SoftwareCompany,
)
)

View file

@ -315,3 +315,13 @@ def get_object_name(obj) -> str:
"""Return class name of the object"""
cls = type(obj)
return f"{cls.__module__}.{cls.__name__}"
def any_to_str(val) -> str:
"""Return the class name or the class name of the object, or 'val' if it's a string type."""
if isinstance(val, str):
return val
if not callable(val):
return get_object_name(val)
return get_class_name(val)

View file

@ -1,21 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Time : 2023/11/1
@Author : mashenquan
@File : named.py
"""
class Named:
"""A base class with functions for converting classes to names and objects to class names."""
@classmethod
def get_class_name(cls):
"""Return class name"""
return f"{cls.__module__}.{cls.__name__}"
def get_object_name(self):
"""Return class name of the object"""
cls = type(self)
return f"{cls.__module__}.{cls.__name__}"

View file

@ -19,7 +19,7 @@ from metagpt.schema import Message
async def test_write_prd():
product_manager = ProductManager()
requirements = "开发一个基于大语言模型与私有知识库的搜索引擎,希望可以基于大语言模型进行搜索总结"
prd = await product_manager.run(Message(content=requirements, cause_by=BossRequirement.get_class_name()))
prd = await product_manager.run(Message(content=requirements, cause_by=BossRequirement))
logger.info(requirements)
logger.info(prd)

View file

@ -19,24 +19,24 @@ def test_ltm_search():
assert len(openai_api_key) > 20
role_id = "UTUserLtm(Product Manager)"
rc = RoleContext(watch=[BossRequirement.get_class_name()])
rc = RoleContext(watch=[BossRequirement])
ltm = LongTermMemory()
ltm.recover_memory(role_id, rc)
idea = "Write a cli snake game"
message = Message(role="BOSS", content=idea, cause_by=BossRequirement.get_class_name())
message = Message(role="BOSS", content=idea, cause_by=BossRequirement)
news = ltm.find_news([message])
assert len(news) == 1
ltm.add(message)
sim_idea = "Write a game of cli snake"
sim_message = Message(role="BOSS", content=sim_idea, cause_by=BossRequirement.get_class_name())
sim_message = Message(role="BOSS", content=sim_idea, cause_by=BossRequirement)
news = ltm.find_news([sim_message])
assert len(news) == 0
ltm.add(sim_message)
new_idea = "Write a 2048 web game"
new_message = Message(role="BOSS", content=new_idea, cause_by=BossRequirement.get_class_name())
new_message = Message(role="BOSS", content=new_idea, cause_by=BossRequirement)
news = ltm.find_news([new_message])
assert len(news) == 1
ltm.add(new_message)
@ -52,7 +52,7 @@ def test_ltm_search():
assert len(news) == 0
new_idea = "Write a Battle City"
new_message = Message(role="BOSS", content=new_idea, cause_by=BossRequirement.get_class_name())
new_message = Message(role="BOSS", content=new_idea, cause_by=BossRequirement)
news = ltm_new.find_news([new_message])
assert len(news) == 1

View file

@ -18,7 +18,7 @@ from metagpt.schema import Message
def test_idea_message():
idea = "Write a cli snake game"
role_id = "UTUser1(Product Manager)"
message = Message(role="BOSS", content=idea, cause_by=BossRequirement.get_class_name())
message = Message(role="BOSS", content=idea, cause_by=BossRequirement)
memory_storage: MemoryStorage = MemoryStorage()
messages = memory_storage.recover_memory(role_id)
@ -28,12 +28,12 @@ def test_idea_message():
assert memory_storage.is_initialized is True
sim_idea = "Write a game of cli snake"
sim_message = Message(role="BOSS", content=sim_idea, cause_by=BossRequirement.get_class_name())
sim_message = Message(role="BOSS", content=sim_idea, cause_by=BossRequirement)
new_messages = memory_storage.search(sim_message)
assert len(new_messages) == 0 # similar, return []
new_idea = "Write a 2048 web game"
new_message = Message(role="BOSS", content=new_idea, cause_by=BossRequirement.get_class_name())
new_message = Message(role="BOSS", content=new_idea, cause_by=BossRequirement)
new_messages = memory_storage.search(new_message)
assert new_messages[0].content == message.content
@ -49,7 +49,7 @@ def test_actionout_message():
role_id = "UTUser2(Architect)"
content = "The boss has requested the creation of a command-line interface (CLI) snake game"
message = Message(
content=content, instruct_content=ic_obj(**out_data), role="user", cause_by=WritePRD.get_class_name()
content=content, instruct_content=ic_obj(**out_data), role="user", cause_by=WritePRD
) # WritePRD as test action
memory_storage: MemoryStorage = MemoryStorage()
@ -60,16 +60,12 @@ def test_actionout_message():
assert memory_storage.is_initialized is True
sim_conent = "The request is command-line interface (CLI) snake game"
sim_message = Message(
content=sim_conent, instruct_content=ic_obj(**out_data), role="user", cause_by=WritePRD.get_class_name()
)
sim_message = Message(content=sim_conent, instruct_content=ic_obj(**out_data), role="user", cause_by=WritePRD)
new_messages = memory_storage.search(sim_message)
assert len(new_messages) == 0 # similar, return []
new_conent = "Incorporate basic features of a snake game such as scoring and increasing difficulty"
new_message = Message(
content=new_conent, instruct_content=ic_obj(**out_data), role="user", cause_by=WritePRD.get_class_name()
)
new_message = Message(content=new_conent, instruct_content=ic_obj(**out_data), role="user", cause_by=WritePRD)
new_messages = memory_storage.search(new_message)
assert new_messages[0].content == message.content

View file

@ -26,7 +26,7 @@ async def test_action_planner():
role.import_skill(TimeSkill(), "time")
role.import_skill(TextSkill(), "text")
task = "What is the sum of 110 and 990?"
role.put_message(Message(content=task, cause_by=BossRequirement.get_class_name()))
role.put_message(Message(content=task, cause_by=BossRequirement))
await role._observe()
await role._think() # it will choose mathskill.Add
assert "1100" == (await role._act()).content

View file

@ -29,7 +29,7 @@ async def test_basic_planner():
role.import_semantic_skill_from_directory(SKILL_DIRECTORY, "WriterSkill")
role.import_skill(TextSkill(), "TextSkill")
# using BasicPlanner
role.put_message(Message(content=task, cause_by=BossRequirement.get_class_name()))
role.put_message(Message(content=task, cause_by=BossRequirement))
await role._observe()
await role._think()
# assuming sk_agent will think he needs WriterSkill.Brainstorm and WriterSkill.Translate

View file

@ -254,7 +254,7 @@ a = 'a'
class MockMessages:
req = Message(role="Boss", content=BOSS_REQUIREMENT, cause_by=BossRequirement.get_class_name())
prd = Message(role="Product Manager", content=PRD, cause_by=WritePRD.get_class_name())
system_design = Message(role="Architect", content=SYSTEM_DESIGN, cause_by=WriteDesign.get_class_name())
tasks = Message(role="Project Manager", content=TASKS, cause_by=WriteTasks.get_class_name())
req = Message(role="Boss", content=BOSS_REQUIREMENT, cause_by=BossRequirement)
prd = Message(role="Product Manager", content=PRD, cause_by=WritePRD)
system_design = Message(role="Architect", content=SYSTEM_DESIGN, cause_by=WriteDesign)
tasks = Message(role="Project Manager", content=TASKS, cause_by=WriteTasks)

View file

@ -51,7 +51,7 @@ async def test_publish_and_process_message(env: Environment):
env.add_roles([product_manager, architect])
env.set_manager(Manager())
env.publish_message(Message(role="BOSS", content="需要一个基于LLM做总结的搜索引擎", cause_by=BossRequirement.get_class_name()))
env.publish_message(Message(role="BOSS", content="需要一个基于LLM做总结的搜索引擎", cause_by=BossRequirement))
await env.run(k=2)
logger.info(f"{env.history=}")

View file

@ -13,6 +13,7 @@ import pytest
from metagpt.actions import Action
from metagpt.schema import AIMessage, Message, Routes, SystemMessage, UserMessage
from metagpt.utils.common import get_class_name
@pytest.mark.asyncio
@ -54,9 +55,9 @@ def test_message():
m.cause_by = "Message"
assert m.cause_by == "Message"
m.cause_by = Action
assert m.cause_by == Action.get_class_name()
assert m.cause_by == get_class_name(Action)
m.cause_by = Action()
assert m.cause_by == Action.get_class_name()
assert m.cause_by == get_class_name(Action)
m.content = "b"
assert m.content == "b"

View file

@ -1,28 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Time : 2023-11-1
@Author : mashenquan
@File : test_named.py
"""
import pytest
from metagpt.utils.named import Named
@pytest.mark.asyncio
async def test_suite():
class A(Named):
pass
class B(A):
pass
assert A.get_class_name() == "tests.metagpt.utils.test_named.A"
assert A().get_object_name() == "tests.metagpt.utils.test_named.A"
assert B.get_class_name() == "tests.metagpt.utils.test_named.B"
assert B().get_object_name() == "tests.metagpt.utils.test_named.B"
if __name__ == "__main__":
pytest.main([__file__, "-s"])

View file

@ -59,7 +59,7 @@ def test_serialize_and_deserialize_message():
ic_obj = ActionOutput.create_model_class("prd", out_mapping)
message = Message(
content="prd demand", instruct_content=ic_obj(**out_data), role="user", cause_by=WritePRD.get_class_name()
content="prd demand", instruct_content=ic_obj(**out_data), role="user", cause_by=WritePRD
) # WritePRD as test action
message_ser = serialize_message(message)