mirror of
https://github.com/FoundationAgents/MetaGPT.git
synced 2026-07-14 16:32:16 +02:00
refactor: Refactor Message transmission & filtering
This commit is contained in:
parent
5e8ada5cff
commit
545d77ce0d
30 changed files with 658 additions and 296 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue