Merge branch 'dev' of https://github.com/geekan/MetaGPT into geekan/dev

This commit is contained in:
莘权 马 2024-01-11 23:16:39 +08:00
commit 2ed7c50822
26 changed files with 282 additions and 117 deletions

View file

@ -25,7 +25,7 @@ from metagpt.utils.project_repo import ProjectRepo
class Action(SerializationMixin, ContextMixin, BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True, exclude=["llm"])
model_config = ConfigDict(arbitrary_types_allowed=True)
name: str = ""
i_context: Union[dict, CodingContext, CodeSummarizeContext, TestingContext, RunCodeContext, str, None] = ""

View file

@ -18,7 +18,7 @@ from metagpt.configs.redis_config import RedisConfig
from metagpt.configs.s3_config import S3Config
from metagpt.configs.search_config import SearchConfig
from metagpt.configs.workspace_config import WorkspaceConfig
from metagpt.const import METAGPT_ROOT
from metagpt.const import CONFIG_ROOT, METAGPT_ROOT
from metagpt.utils.yaml_model import YamlModel
@ -81,6 +81,11 @@ class Config(CLIParams, YamlModel):
AZURE_TTS_REGION: str = ""
mermaid_engine: str = "nodejs"
@classmethod
def from_home(cls, path):
"""Load config from ~/.metagpt/config.yaml"""
return Config.model_validate_yaml(CONFIG_ROOT / path)
@classmethod
def default(cls):
"""Load default config

View file

@ -47,7 +47,7 @@ def get_metagpt_root():
# METAGPT PROJECT ROOT AND VARS
CONFIG_ROOT = Path.home() / ".metagpt"
METAGPT_ROOT = get_metagpt_root() # Dependent on METAGPT_PROJECT_ROOT
DEFAULT_WORKSPACE_ROOT = METAGPT_ROOT / "workspace"

View file

@ -19,6 +19,11 @@ class ContextMixin(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
# Pydantic has bug on _private_attr when using inheritance, so we use private_* instead
# - https://github.com/pydantic/pydantic/issues/7142
# - https://github.com/pydantic/pydantic/issues/7083
# - https://github.com/pydantic/pydantic/issues/7091
# Env/Role/Action will use this context as private context, or use self.context as public context
private_context: Optional[Context] = Field(default=None, exclude=True)
# Env/Role/Action will use this config as private config, or use self.context.config as public config
@ -52,6 +57,8 @@ class ContextMixin(BaseModel):
def set_config(self, config: Config, override=False):
"""Set config"""
self.set("private_config", config, override)
if config is not None:
_ = self.llm # init llm
def set_llm(self, llm: BaseLLM, override=False):
"""Set llm"""

View file

@ -5,13 +5,15 @@
@Author : alexanderwu
@File : llm.py
"""
from typing import Optional
from metagpt.configs.llm_config import LLMConfig
from metagpt.context import CONTEXT
from metagpt.provider.base_llm import BaseLLM
def LLM() -> BaseLLM:
def LLM(llm_config: Optional[LLMConfig] = None) -> BaseLLM:
"""get the default llm provider if name is None"""
# context.use_llm(name=name, provider=provider)
if llm_config is not None:
CONTEXT.llm_with_cost_manager_from_llm_config(llm_config)
return CONTEXT.llm()

View file

@ -15,6 +15,7 @@ from metagpt.provider.zhipuai_api import ZhiPuAILLM
from metagpt.provider.azure_openai_api import AzureOpenAILLM
from metagpt.provider.metagpt_api import MetaGPTLLM
from metagpt.provider.human_provider import HumanProvider
from metagpt.provider.spark_api import SparkLLM
__all__ = [
"FireworksLLM",
@ -26,4 +27,5 @@ __all__ = [
"MetaGPTLLM",
"OllamaLLM",
"HumanProvider",
"SparkLLM",
]

View file

@ -59,7 +59,9 @@ class BaseLLM(ABC):
if system_msgs:
message = self._system_msgs(system_msgs)
else:
message = [self._default_system_msg()] if self.use_system_prompt else []
message = [self._default_system_msg()]
if not self.use_system_prompt:
message = []
if format_msgs:
message.extend(format_msgs)
message.append(self._user_msg(msg))

View file

@ -220,10 +220,12 @@ class OpenAILLM(BaseLLM):
@handle_exception
def _update_costs(self, usage: CompletionUsage):
if self.config.calc_usage and usage:
if self.config.calc_usage and usage and self.cost_manager:
self.cost_manager.update_cost(usage.prompt_tokens, usage.completion_tokens, self.model)
def get_costs(self) -> Costs:
if not self.cost_manager:
return Costs()
return self.cost_manager.get_costs()
def _get_max_tokens(self, messages: list[dict]):

View file

@ -26,14 +26,14 @@ from metagpt.provider.llm_provider_registry import register_provider
class SparkLLM(BaseLLM):
def __init__(self, config: LLMConfig):
self.config = config
logger.warning("当前方法无法支持异步运行。当你使用acompletion时并不能并行访问。")
logger.warning("SparkLLM当前方法无法支持异步运行。当你使用acompletion时并不能并行访问。")
def get_choice_text(self, rsp: dict) -> str:
return rsp["payload"]["choices"]["text"][-1]["content"]
async def acompletion_text(self, messages: list[dict], stream=False, timeout: int = 3) -> str:
# 不支持
logger.warning("当前方法无法支持异步运行。当你使用acompletion时并不能并行访问。")
# logger.warning("当前方法无法支持异步运行。当你使用acompletion时并不能并行访问。")
w = GetMessageFromWeb(messages, self.config)
return w.run()

View file

@ -104,7 +104,7 @@ class Engineer(Role):
# Code review
if review:
action = WriteCodeReview(i_context=coding_context, context=self.context, llm=self.llm)
self._init_action_system_message(action)
self._init_action(action)
coding_context = await action.run()
await self.project_repo.srcs.save(
filename=coding_context.filename,

View file

@ -132,6 +132,13 @@ class Role(SerializationMixin, ContextMixin, BaseModel):
role_id: str = ""
states: list[str] = []
# scenarios to set action system_prompt:
# 1. `__init__` while using Role(actions=[...])
# 2. add action to role while using `role.set_action(action)`
# 3. set_todo while using `role.set_todo(action)`
# 4. when role.system_prompt is being updated (e.g. by `role.system_prompt = "..."`)
# Additional, if llm is not set, we will use role's llm
actions: list[SerializeAsAny[Action]] = Field(default=[], validate_default=True)
rc: RoleContext = Field(default_factory=RoleContext)
addresses: set[str] = set()
@ -146,6 +153,10 @@ class Role(SerializationMixin, ContextMixin, BaseModel):
self.pydantic_rebuild_model()
super().__init__(**data)
if self.is_human:
self.llm = HumanProvider(None)
self._check_actions()
self.llm.system_prompt = self._get_prefix()
self._watch(data.get("watch") or [UserRequirement])
@ -225,7 +236,16 @@ class Role(SerializationMixin, ContextMixin, BaseModel):
def _setting(self):
return f"{self.name}({self.profile})"
def _init_action_system_message(self, action: Action):
def _check_actions(self):
"""Check actions and set llm and prefix for each action."""
self.set_actions(self.actions)
return self
def _init_action(self, action: Action):
if not action.private_config:
action.set_llm(self.llm, override=True)
else:
action.set_llm(self.llm, override=False)
action.set_prefix(self._get_prefix())
def set_action(self, action: Action):
@ -241,7 +261,7 @@ class Role(SerializationMixin, ContextMixin, BaseModel):
self._reset()
for action in actions:
if not isinstance(action, Action):
i = action(name="", llm=self.llm)
i = action()
else:
if self.is_human and not isinstance(action.llm, HumanProvider):
logger.warning(
@ -250,7 +270,7 @@ class Role(SerializationMixin, ContextMixin, BaseModel):
f"try passing in Action classes instead of initialized instances"
)
i = action
self._init_action_system_message(i)
self._init_action(i)
self.actions.append(i)
self.states.append(f"{len(self.actions)}. {action}")
@ -308,6 +328,7 @@ class Role(SerializationMixin, ContextMixin, BaseModel):
if env:
env.set_addresses(self, self.addresses)
self.llm.system_prompt = self._get_prefix()
self.set_actions(self.actions) # reset actions to update llm and prefix
def _get_prefix(self):
"""Get the role prefix"""
@ -320,7 +341,8 @@ class Role(SerializationMixin, ContextMixin, BaseModel):
prefix += CONSTRAINT_TEMPLATE.format(**{"constraints": self.constraints})
if self.rc.env and self.rc.env.desc:
other_role_names = ", ".join(self.rc.env.role_names())
all_roles = self.rc.env.role_names()
other_role_names = ", ".join([r for r in all_roles if r != self.name])
env_desc = f"You are in {self.rc.env.desc} with roles({other_role_names})."
prefix += env_desc
return prefix
@ -480,7 +502,6 @@ class Role(SerializationMixin, ContextMixin, BaseModel):
if not msg.cause_by:
msg.cause_by = UserRequirement
self.put_message(msg)
if not await self._observe():
# If there is no new information, suspend and wait
logger.debug(f"{self._setting}: no news. waiting.")

View file

@ -7,7 +7,7 @@ from pathlib import Path
import typer
from metagpt.config2 import config
from metagpt.const import METAGPT_ROOT
from metagpt.const import CONFIG_ROOT, METAGPT_ROOT
app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
@ -118,7 +118,7 @@ def startup(
def copy_config_to(config_path=METAGPT_ROOT / "config" / "config2.yaml"):
"""Initialize the configuration file for MetaGPT."""
target_path = Path.home() / ".metagpt" / "config2.yaml"
target_path = CONFIG_ROOT / "config2.yaml"
# 创建目标目录(如果不存在)
target_path.parent.mkdir(parents=True, exist_ok=True)