diff --git a/config/config.yaml b/config/config.yaml index 9acdbe8a1..b841ee477 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -94,4 +94,8 @@ MODEL_FOR_RESEARCHER_REPORT: gpt-3.5-turbo-16k ### browser path for pyppeteer engine, support Chrome, Chromium,MS Edge #PYPPETEER_EXECUTABLE_PATH: "/usr/bin/google-chrome-stable" -PROMPT_FORMAT: json #json or markdown \ No newline at end of file +PROMPT_FORMAT: json #json or markdown + +### Agent configurations +# RAISE_NOT_CONFIG_ERROR: true # "true" if the LLM key is not configured, throw a NotConfiguredException, else "false". +# WORKSPACE_PATH_WITH_UID: false # "true" if using `{workspace}/{uid}` as the workspace path; "false" use `{workspace}`. \ No newline at end of file diff --git a/metagpt/actions/prepare_documents.py b/metagpt/actions/prepare_documents.py index 05255dcc5..8d3445ae4 100644 --- a/metagpt/actions/prepare_documents.py +++ b/metagpt/actions/prepare_documents.py @@ -27,8 +27,8 @@ class PrepareDocuments(Action): # Create and initialize the workspace folder, initialize the Git environment. project_name = CONFIG.project_name or FileRepository.new_filename() workdir = CONFIG.project_path - if not workdir and CONFIG.workspace: - workdir = Path(CONFIG.workspace) / project_name + if not workdir and CONFIG.workspace_path: + workdir = Path(CONFIG.workspace_path) / project_name workdir = Path(workdir or DEFAULT_WORKSPACE_ROOT / project_name) if not CONFIG.inc and workdir.exists(): shutil.rmtree(workdir) diff --git a/metagpt/config.py b/metagpt/config.py index d04ae7291..aabd54c4b 100644 --- a/metagpt/config.py +++ b/metagpt/config.py @@ -6,10 +6,12 @@ Provide configuration, singleton 1. According to Section 2.2.3.11 of RFC 135, add git repository support. 2. Add the parameter `src_workspace` for the old version project path. """ +import datetime import os from copy import deepcopy from pathlib import Path from typing import Any +from uuid import uuid4 import yaml @@ -60,7 +62,11 @@ class Config(metaclass=Singleton): and (not self.anthropic_api_key or "YOUR_API_KEY" == self.anthropic_api_key) and (not self.zhipuai_api_key or "YOUR_API_KEY" == self.zhipuai_api_key) ): - raise NotConfiguredException("Set OPENAI_API_KEY or Anthropic_API_KEY or ZHIPUAI_API_KEY first") + val = self._get("RAISE_NOT_CONFIG_ERROR") + if val is None or val.lower() == "true": + raise NotConfiguredException("Set OPENAI_API_KEY or Anthropic_API_KEY or ZHIPUAI_API_KEY first") + else: # for agent + logger.warning("Set OPENAI_API_KEY or Anthropic_API_KEY or ZHIPUAI_API_KEY first") self.openai_api_base = self._get("OPENAI_API_BASE") self.openai_proxy = self._get("OPENAI_PROXY") or self.global_proxy self.openai_api_type = self._get("OPENAI_API_TYPE") @@ -103,8 +109,15 @@ class Config(metaclass=Singleton): self.pyppeteer_executable_path = self._get("PYPPETEER_EXECUTABLE_PATH", "") self.prompt_format = self._get("PROMPT_FORMAT", "markdown") + workspace_uid = ( + self._get("WORKSPACE_UID") or f"{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}-{uuid4().hex[-8:]}" + ) self.workspace_path = Path(self._get("WORKSPACE_PATH", DEFAULT_WORKSPACE_ROOT)) + val = self._get("WORKSPACE_PATH_WITH_UID") + if val and val.lower() == "true": # for agent + self.workspace_path = self.workspace_path / workspace_uid self._ensure_workspace_exists() + self.max_auto_summarize_code = self.max_auto_summarize_code or self._get("MAX_AUTO_SUMMARIZE_CODE", 1) def _ensure_workspace_exists(self): self.workspace_path.mkdir(parents=True, exist_ok=True) diff --git a/metagpt/environment.py b/metagpt/environment.py index 02eb3d340..88beb5f25 100644 --- a/metagpt/environment.py +++ b/metagpt/environment.py @@ -49,7 +49,7 @@ class Environment(BaseModel): for role in roles: self.add_role(role) - def publish_message(self, message: Message) -> bool: + def publish_message(self, message: Message, peekable: bool = True) -> bool: """ Distribute the message to the recipients. In accordance with the Message routing structure design in Chapter 2.2.1 of RFC 116, as already planned diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index cedd2101f..4f7f0b796 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -42,7 +42,7 @@ from metagpt.schema import ( Documents, Message, ) -from metagpt.utils.common import any_to_str, any_to_str_set +from metagpt.utils.common import any_to_name, any_to_str, any_to_str_set IS_PASS_PROMPT = """ {context} @@ -83,6 +83,7 @@ class Engineer(Role): self.code_todos = [] self.summarize_todos = [] self.n_borg = n_borg + self._next_todo = any_to_name(WriteCode) @staticmethod def _parse_tasks(task_msg: Document) -> list[str]: @@ -124,8 +125,10 @@ class Engineer(Role): if self._rc.todo is None: return None if isinstance(self._rc.todo, WriteCode): + self._next_todo = any_to_name(SummarizeCode) return await self._act_write_code() if isinstance(self._rc.todo, SummarizeCode): + self._next_todo = any_to_name(WriteCode) return await self._act_summarize() return None @@ -296,3 +299,7 @@ class Engineer(Role): self.summarize_todos.append(SummarizeCode(context=ctx, llm=self._llm)) if self.summarize_todos: self._rc.todo = self.summarize_todos[0] + + @property + def todo(self) -> str: + return self._next_todo diff --git a/metagpt/roles/product_manager.py b/metagpt/roles/product_manager.py index 017feade7..284fcca96 100644 --- a/metagpt/roles/product_manager.py +++ b/metagpt/roles/product_manager.py @@ -11,6 +11,7 @@ from metagpt.actions import UserRequirement, WritePRD from metagpt.actions.prepare_documents import PrepareDocuments from metagpt.config import CONFIG from metagpt.roles import Role +from metagpt.utils.common import any_to_name class ProductManager(Role): @@ -55,3 +56,10 @@ class ProductManager(Role): async def _observe(self, ignore_memory=False) -> int: return await super(ProductManager, self)._observe(ignore_memory=True) + + @property + def todo(self) -> str: + if self._rc.state == 0: + return any_to_name(WritePRD) + else: + return any_to_name(PrepareDocuments) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index 52ac3cf28..e34daa307 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -30,10 +30,8 @@ from metagpt.config import CONFIG from metagpt.llm import LLM, HumanProvider from metagpt.logs import logger from metagpt.memory import Memory - -# from metagpt.memory import LongTermMemory from metagpt.schema import Message, MessageQueue -from metagpt.utils.common import any_to_str +from metagpt.utils.common import any_to_name, any_to_str PREFIX_TEMPLATE = """You are a {profile}, named {name}, your goal is {goal}, and the constraint is {constraints}. """ @@ -191,6 +189,9 @@ class Role: # check RoleContext after adding watch actions self._rc.check(self._role_id) + def is_watch(self, caused_by: str): + return caused_by in self._rc.watch + def subscribe(self, tags: Set[str]): """Used to receive Messages with certain tags from the environment. Message will be put into personal message buffer to be further processed in _observe. By default, a Role subscribes Messages with a tag of its own name @@ -213,22 +214,6 @@ class Role: if env: env.set_subscription(self, self._subscription) - # # Replaced by FileRepository.set_file - # def set_doc(self, content: str, filename: str): - # return self._rc.env.set_doc(content, filename) - # - # # Replaced by FileRepository.get_file - # def get_doc(self, filename: str): - # return self._rc.env.get_doc(filename) - # - # # Replaced by CONFIG.xx - # def set(self, k, v): - # return self._rc.env.set(k, v) - # - # # Replaced by CONFIG.xx - # def get(self, k): - # return self._rc.env.get(k) - @property def profile(self): """Get the role description (position)""" @@ -368,23 +353,6 @@ class Role: self._set_state(state=-1) # current reaction is complete, reset state to -1 and todo back to None return rsp - # # Replaced by run() - # 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) - - # # Replaced by run() - # 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() - def get_memories(self, k=0) -> list[Message]: """A wrapper to return the most recent k memories of this role, return all when k=0""" return self._rc.memory.get(k=k) @@ -418,3 +386,19 @@ class Role: 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() + + async def think(self) -> Action: + """The exported `think` function""" + await self._think() + return self._rc.todo + + async def act(self) -> ActionOutput: + """The exported `act` function""" + msg = await self._act() + return ActionOutput(content=msg.content, instruct_content=msg.instruct_content) + + @property + def todo(self) -> str: + if self._actions: + return any_to_name(self._actions[0]) + return "" diff --git a/metagpt/team.py b/metagpt/team.py index 92f379c97..152ad24f0 100644 --- a/metagpt/team.py +++ b/metagpt/team.py @@ -52,13 +52,14 @@ class Team(BaseModel): # Human requirement. self.env.publish_message( - Message(role="Human", content=idea, cause_by=UserRequirement, send_to=send_to or MESSAGE_ROUTE_TO_ALL) + Message(role="Human", content=idea, cause_by=UserRequirement, send_to=send_to or MESSAGE_ROUTE_TO_ALL), + peekable=False, ) def _save(self): logger.info(self.json(ensure_ascii=False)) - async def run(self, n_round=3): + async def run(self, n_round=3, auto_archive=True): """Run company until target round or no money""" while n_round > 0: # self._save() @@ -66,6 +67,6 @@ class Team(BaseModel): logger.debug(f"{n_round=}") self._check_balance() await self.env.run() - if CONFIG.git_repo: + if auto_archive and CONFIG.git_repo: CONFIG.git_repo.archive() return self.env.history diff --git a/metagpt/utils/common.py b/metagpt/utils/common.py index f08519f8e..8d4d8eaf9 100644 --- a/metagpt/utils/common.py +++ b/metagpt/utils/common.py @@ -358,3 +358,14 @@ def is_subscribed(message, tags): if t in message.send_to: return True return False + + +def any_to_name(val): + """ + Convert a value to its name by extracting the last part of the dotted path. + + :param val: The value to convert. + + :return: The name of the value. + """ + return any_to_str(val).split(".")[-1]