MetaGPT/metagpt/config.py

271 lines
11 KiB
Python
Raw Normal View History

2023-06-30 17:10:48 +08:00
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
2023-07-27 08:40:32 -05:00
Provide configuration, singleton
2023-11-27 15:46:25 +08:00
@Modified By: mashenquan, 2023/11/27.
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.
2023-06-30 17:10:48 +08:00
"""
2023-12-13 19:18:38 +08:00
import datetime
2023-08-28 22:04:06 +08:00
import json
2023-06-30 17:10:48 +08:00
import os
import warnings
from copy import deepcopy
2023-11-28 18:16:50 +08:00
from pathlib import Path
from typing import Any
2023-12-13 19:18:38 +08:00
from uuid import uuid4
2023-12-14 22:59:41 +08:00
2023-06-30 17:10:48 +08:00
import yaml
2023-12-14 22:59:41 +08:00
2024-01-04 21:16:23 +08:00
from metagpt.configs.llm_config import LLMType
2023-11-28 18:16:50 +08:00
from metagpt.const import DEFAULT_WORKSPACE_ROOT, METAGPT_ROOT, OPTIONS
2023-07-22 11:28:22 +08:00
from metagpt.logs import logger
from metagpt.tools import SearchEngineType, WebBrowserEngineType
from metagpt.utils.common import require_python_version
2023-08-28 22:04:06 +08:00
from metagpt.utils.cost_manager import CostManager
2023-08-15 06:51:39 -05:00
from metagpt.utils.singleton import Singleton
2023-06-30 17:10:48 +08:00
class NotConfiguredException(Exception):
"""Exception raised for errors in the configuration.
Attributes:
message -- explanation of the error
"""
def __init__(self, message="The required configuration is not set"):
self.message = message
super().__init__(self.message)
class Config(metaclass=Singleton):
"""
2023-08-02 15:57:10 -05:00
Regular usage method:
2023-06-30 17:10:48 +08:00
config = Config("config.yaml")
2023-08-02 16:22:35 -05:00
secret_key = config.get_key("MY_SECRET_KEY")
2023-06-30 17:10:48 +08:00
print("Secret key:", secret_key)
"""
2023-06-30 17:10:48 +08:00
_instance = None
home_yaml_file = Path.home() / ".metagpt/config.yaml"
key_yaml_file = METAGPT_ROOT / "config/key.yaml"
default_yaml_file = METAGPT_ROOT / "config/config.yaml"
2023-06-30 17:10:48 +08:00
2023-12-14 15:06:04 +08:00
def __init__(self, yaml_file=default_yaml_file, cost_data=""):
2023-12-19 18:54:04 +08:00
global_options = OPTIONS.get()
2023-12-19 17:55:34 +08:00
# cli paras
self.project_path = ""
self.project_name = ""
self.inc = False
self.reqa_file = ""
self.max_auto_summarize_code = 0
2023-12-28 15:46:17 +08:00
self.git_reinit = False
2023-12-19 17:55:34 +08:00
self._init_with_config_files_and_env(yaml_file)
2023-12-14 15:06:04 +08:00
# The agent needs to be billed per user, so billing information cannot be destroyed when the session ends.
self.cost_manager = CostManager(**json.loads(cost_data)) if cost_data else CostManager()
self._update()
2023-12-19 18:54:04 +08:00
global_options.update(OPTIONS.get())
2023-12-19 16:54:06 +08:00
logger.debug("Config loading done.")
2023-12-19 19:25:01 +08:00
2024-01-04 21:16:23 +08:00
def get_default_llm_provider_enum(self) -> LLMType:
2023-12-24 20:51:50 +08:00
"""Get first valid LLM provider enum"""
mappings = {
2024-01-04 21:16:23 +08:00
LLMType.OPENAI: bool(
self._is_valid_llm_key(self.OPENAI_API_KEY) and not self.OPENAI_API_TYPE and self.OPENAI_API_MODEL
),
2024-01-04 21:16:23 +08:00
LLMType.ANTHROPIC: self._is_valid_llm_key(self.ANTHROPIC_API_KEY),
LLMType.ZHIPUAI: self._is_valid_llm_key(self.ZHIPUAI_API_KEY),
LLMType.FIREWORKS: self._is_valid_llm_key(self.FIREWORKS_API_KEY),
LLMType.OPEN_LLM: self._is_valid_llm_key(self.OPEN_LLM_API_BASE),
LLMType.GEMINI: self._is_valid_llm_key(self.GEMINI_API_KEY),
LLMType.METAGPT: bool(self._is_valid_llm_key(self.OPENAI_API_KEY) and self.OPENAI_API_TYPE == "metagpt"),
LLMType.AZURE: bool(
self._is_valid_llm_key(self.OPENAI_API_KEY)
and self.OPENAI_API_TYPE == "azure"
and self.DEPLOYMENT_NAME
and self.OPENAI_API_VERSION
),
2024-01-04 21:16:23 +08:00
LLMType.OLLAMA: self._is_valid_llm_key(self.OLLAMA_API_BASE),
}
provider = None
for k, v in mappings.items():
if v:
provider = k
break
2024-01-04 12:46:41 +08:00
if provider is None:
if self.DEFAULT_PROVIDER:
2024-01-08 15:19:38 +08:00
provider = LLMType(self.DEFAULT_PROVIDER)
2024-01-04 12:46:41 +08:00
else:
raise NotConfiguredException("You should config a LLM configuration first")
2024-01-04 21:16:23 +08:00
if provider is LLMType.GEMINI and not require_python_version(req_version=(3, 10)):
warnings.warn("Use Gemini requires Python >= 3.10")
model_name = self.get_model_name(provider=provider)
2023-12-24 12:30:08 +08:00
if model_name:
logger.info(f"{provider} Model: {model_name}")
if provider:
logger.info(f"API: {provider}")
return provider
2023-12-19 17:55:34 +08:00
def get_model_name(self, provider=None) -> str:
provider = provider or self.get_default_llm_provider_enum()
model_mappings = {
2024-01-04 21:16:23 +08:00
LLMType.OPENAI: self.OPENAI_API_MODEL,
LLMType.AZURE: self.DEPLOYMENT_NAME,
}
return model_mappings.get(provider, "")
2023-12-19 16:54:06 +08:00
@staticmethod
2023-12-19 19:25:01 +08:00
def _is_valid_llm_key(k: str) -> bool:
return bool(k and k != "YOUR_API_KEY")
def _update(self):
self.global_proxy = self._get("GLOBAL_PROXY")
2023-12-19 16:54:06 +08:00
self.openai_api_key = self._get("OPENAI_API_KEY")
2023-12-19 16:54:06 +08:00
self.anthropic_api_key = self._get("ANTHROPIC_API_KEY")
self.zhipuai_api_key = self._get("ZHIPUAI_API_KEY")
self.open_llm_api_base = self._get("OPEN_LLM_API_BASE")
self.open_llm_api_model = self._get("OPEN_LLM_API_MODEL")
self.fireworks_api_key = self._get("FIREWORKS_API_KEY")
2023-12-14 16:45:40 +08:00
self.gemini_api_key = self._get("GEMINI_API_KEY")
2023-12-23 19:48:01 +08:00
self.ollama_api_base = self._get("OLLAMA_API_BASE")
self.ollama_api_model = self._get("OLLAMA_API_MODEL")
2024-01-09 22:04:49 +08:00
# if not self._get("DISABLE_LLM_PROVIDER_CHECK"):
# _ = self.get_default_llm_provider_enum()
2023-12-19 16:54:06 +08:00
2023-12-28 17:42:28 +08:00
self.openai_base_url = self._get("OPENAI_BASE_URL")
1. 动作优化 1. SummarizeCode动作:用于基于代码进行总结,思考bug、逻辑、todo 2. CodeReview动作优化:目前强制要求回答问题,有更高的成功率了 1. 增加了LGTM/LBTM的回答,在LGTM时会及时停止,不重写代码 2. 目前增加了设置中的参数code_review_k_times,与reflexion类似,设置为2 3. 仍然有概率发生指令不遵循,尤其是会有比较高的概率发生同时review多个代码文件,还没想好怎么解决 #FIXME 3. 增加了env到Action结构中,现在可以直接调用环境接口了 4. WriteDesign:去除了对project_name的纠正代码,现在引导下可以一次生成对 1. 修改了提示词中的##格式,改为了JSON格式 2. 数据结构 1. Document的标准化:Env->Repo->Document,其中Document/Asset/Code都是Document 1. 原用于检索的Document改为IndexableDocument 2. Repo结构引入:用于Document装载与元数据装载 3. RepoParser引入:写了一个简单的AST parser(后续可能要换tree-sitter),给出了整库symbol 4. Env中增加了set/get/set_doc/get_doc接口,用于set/get单个变量或者一个Document。这个逻辑后续或许会进一步简化 3. 配置优化 1. 默认更换为gpt-4-1106-preview,以获得最好的效果与成本 2. 提供~/.metagpt作为配置最高优先级目录,从中读取config.yaml 3. workspace可以灵活指定了,在config中配置 4. project_name可以由命令行指定,并且改为由ProductManager生成 4. metagpt作为默认命令行,而非python startup.py metagpt --help metagpt --project-name game_2048 "make a 2048 game" metagpt "make a 2048 game" metagpt --project-name game_2048 --inc "将2048改为4096" metagpt --project-name game_2048 --auto-inc "make a 2048 game" 1. 使用新的METAGPT_ROOT生成方式,而非寻找git,以便cli安装 2. 命令行由fire换为了typer,它会带来相对更好的体验 3. project_name可以灵活指定了,在metagpt命令行输入中配置 5. 其他 1. 现在支持多国语言了,中文已测试 2. BossRequirement -> UserRequirement 3. 大量错误文本的修正,增加了可读性 4. 中量提示词优化,稍微提升了一些准确率 5. 暂时屏蔽了LongtermMemory相关逻辑,这个逻辑底层调用了langchain的FAISS,会带来~5秒加载耗时 6. 修复了安装包中的部分描述错误 7. 去除了config中在openai_proxy设定时对base的重复修改,这个修改应该在openai初始化时发生 8. 修复了JSON在中文存储时的特定问题,ensure_ascii=False
2023-11-27 15:36:50 +08:00
self.openai_proxy = self._get("OPENAI_PROXY") or self.global_proxy
self.openai_api_type = self._get("OPENAI_API_TYPE")
self.openai_api_version = self._get("OPENAI_API_VERSION")
self.openai_api_rpm = self._get("RPM", 3)
2023-12-19 16:22:29 +08:00
self.openai_api_model = self._get("OPENAI_API_MODEL", "gpt-4-1106-preview")
self.max_tokens_rsp = self._get("MAX_TOKENS", 2048)
2023-12-06 16:06:17 +08:00
self.deployment_name = self._get("DEPLOYMENT_NAME", "gpt-4")
2023-10-18 22:46:33 +08:00
self.spark_appid = self._get("SPARK_APPID")
self.spark_api_secret = self._get("SPARK_API_SECRET")
self.spark_api_key = self._get("SPARK_API_KEY")
2023-10-16 22:53:28 +08:00
self.domain = self._get("DOMAIN")
self.spark_url = self._get("SPARK_URL")
self.fireworks_api_base = self._get("FIREWORKS_API_BASE")
self.fireworks_api_model = self._get("FIREWORKS_API_MODEL")
2023-12-19 16:54:06 +08:00
self.claude_api_key = self._get("ANTHROPIC_API_KEY")
2024-01-04 21:16:23 +08:00
self.serpapi_api_key = self._get("SERPAPI_API_KEY")
self.serper_api_key = self._get("SERPER_API_KEY")
self.google_api_key = self._get("GOOGLE_API_KEY")
self.google_cse_id = self._get("GOOGLE_CSE_ID")
self.search_engine = SearchEngineType(self._get("SEARCH_ENGINE", SearchEngineType.SERPAPI_GOOGLE))
self.web_browser_engine = WebBrowserEngineType(self._get("WEB_BROWSER_ENGINE", WebBrowserEngineType.PLAYWRIGHT))
self.playwright_browser_type = self._get("PLAYWRIGHT_BROWSER_TYPE", "chromium")
self.selenium_browser_type = self._get("SELENIUM_BROWSER_TYPE", "chrome")
2023-08-15 06:51:39 -05:00
self.long_term_memory = self._get("LONG_TERM_MEMORY", False)
if self.long_term_memory:
logger.warning("LONG_TERM_MEMORY is True")
2023-08-28 22:04:06 +08:00
self.cost_manager.max_budget = self._get("MAX_BUDGET", 10.0)
1. 动作优化 1. SummarizeCode动作:用于基于代码进行总结,思考bug、逻辑、todo 2. CodeReview动作优化:目前强制要求回答问题,有更高的成功率了 1. 增加了LGTM/LBTM的回答,在LGTM时会及时停止,不重写代码 2. 目前增加了设置中的参数code_review_k_times,与reflexion类似,设置为2 3. 仍然有概率发生指令不遵循,尤其是会有比较高的概率发生同时review多个代码文件,还没想好怎么解决 #FIXME 3. 增加了env到Action结构中,现在可以直接调用环境接口了 4. WriteDesign:去除了对project_name的纠正代码,现在引导下可以一次生成对 1. 修改了提示词中的##格式,改为了JSON格式 2. 数据结构 1. Document的标准化:Env->Repo->Document,其中Document/Asset/Code都是Document 1. 原用于检索的Document改为IndexableDocument 2. Repo结构引入:用于Document装载与元数据装载 3. RepoParser引入:写了一个简单的AST parser(后续可能要换tree-sitter),给出了整库symbol 4. Env中增加了set/get/set_doc/get_doc接口,用于set/get单个变量或者一个Document。这个逻辑后续或许会进一步简化 3. 配置优化 1. 默认更换为gpt-4-1106-preview,以获得最好的效果与成本 2. 提供~/.metagpt作为配置最高优先级目录,从中读取config.yaml 3. workspace可以灵活指定了,在config中配置 4. project_name可以由命令行指定,并且改为由ProductManager生成 4. metagpt作为默认命令行,而非python startup.py metagpt --help metagpt --project-name game_2048 "make a 2048 game" metagpt "make a 2048 game" metagpt --project-name game_2048 --inc "将2048改为4096" metagpt --project-name game_2048 --auto-inc "make a 2048 game" 1. 使用新的METAGPT_ROOT生成方式,而非寻找git,以便cli安装 2. 命令行由fire换为了typer,它会带来相对更好的体验 3. project_name可以灵活指定了,在metagpt命令行输入中配置 5. 其他 1. 现在支持多国语言了,中文已测试 2. BossRequirement -> UserRequirement 3. 大量错误文本的修正,增加了可读性 4. 中量提示词优化,稍微提升了一些准确率 5. 暂时屏蔽了LongtermMemory相关逻辑,这个逻辑底层调用了langchain的FAISS,会带来~5秒加载耗时 6. 修复了安装包中的部分描述错误 7. 去除了config中在openai_proxy设定时对base的重复修改,这个修改应该在openai初始化时发生 8. 修复了JSON在中文存储时的特定问题,ensure_ascii=False
2023-11-27 15:36:50 +08:00
self.code_review_k_times = 2
2023-08-15 07:46:02 -05:00
self.puppeteer_config = self._get("PUPPETEER_CONFIG", "")
self.mmdc = self._get("MMDC", "mmdc")
self.calc_usage = self._get("CALC_USAGE", True)
2023-08-15 06:51:39 -05:00
self.model_for_researcher_summary = self._get("MODEL_FOR_RESEARCHER_SUMMARY")
self.model_for_researcher_report = self._get("MODEL_FOR_RESEARCHER_REPORT")
2023-09-19 21:31:27 +08:00
self.mermaid_engine = self._get("MERMAID_ENGINE", "nodejs")
self.pyppeteer_executable_path = self._get("PYPPETEER_EXECUTABLE_PATH", "")
2023-12-13 19:18:38 +08:00
workspace_uid = (
self._get("WORKSPACE_UID") or f"{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}-{uuid4().hex[-8:]}"
)
2023-12-15 17:05:09 +08:00
self.repair_llm_output = self._get("REPAIR_LLM_OUTPUT", False)
2023-12-24 15:41:35 +08:00
self.prompt_schema = self._get("PROMPT_FORMAT", "json")
self.workspace_path = Path(self._get("WORKSPACE_PATH", DEFAULT_WORKSPACE_ROOT))
2023-12-13 19:18:38 +08:00
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()
2023-12-13 19:18:38 +08:00
self.max_auto_summarize_code = self.max_auto_summarize_code or self._get("MAX_AUTO_SUMMARIZE_CODE", 1)
2024-01-04 21:16:23 +08:00
self.timeout = int(self._get("TIMEOUT", 60))
2023-12-19 17:06:07 +08:00
def update_via_cli(self, project_path, project_name, inc, reqa_file, max_auto_summarize_code):
"""update config via cli"""
2023-12-19 17:11:02 +08:00
# Use in the PrepareDocuments action according to Section 2.2.3.5.1 of RFC 135.
if project_path:
inc = True
project_name = project_name or Path(project_path).name
2023-12-19 17:06:07 +08:00
self.project_path = project_path
self.project_name = project_name
self.inc = inc
self.reqa_file = reqa_file
self.max_auto_summarize_code = max_auto_summarize_code
def _ensure_workspace_exists(self):
self.workspace_path.mkdir(parents=True, exist_ok=True)
2023-12-13 17:47:09 +08:00
logger.debug(f"WORKSPACE_PATH set to {self.workspace_path}")
2023-06-30 17:10:48 +08:00
def _init_with_config_files_and_env(self, yaml_file):
2023-08-02 15:57:10 -05:00
"""Load from config/key.yaml, config/config.yaml, and env in decreasing order of priority"""
configs = dict(os.environ)
2023-06-30 17:10:48 +08:00
for _yaml_file in [yaml_file, self.key_yaml_file, self.home_yaml_file]:
2023-06-30 17:10:48 +08:00
if not _yaml_file.exists():
continue
2023-07-27 08:40:32 -05:00
# Load local YAML file
with open(_yaml_file, "r", encoding="utf-8") as file:
2023-06-30 17:10:48 +08:00
yaml_data = yaml.safe_load(file)
if not yaml_data:
continue
configs.update(yaml_data)
OPTIONS.set(configs)
2023-06-30 17:10:48 +08:00
@staticmethod
def _get(*args, **kwargs):
2023-12-19 16:54:06 +08:00
i = OPTIONS.get()
return i.get(*args, **kwargs)
2023-06-30 17:10:48 +08:00
def get(self, key, *args, **kwargs):
2023-08-28 22:04:06 +08:00
"""Retrieve values from config/key.yaml, config/config.yaml, and environment variables.
Throw an error if not found."""
2023-06-30 17:10:48 +08:00
value = self._get(key, *args, **kwargs)
if value is None:
raise ValueError(f"Key '{key}' not found in environment variables or in the YAML file")
return value
2023-07-07 10:30:53 +08:00
def __setattr__(self, name: str, value: Any) -> None:
OPTIONS.get()[name] = value
def __getattr__(self, name: str) -> Any:
2023-12-19 16:54:06 +08:00
i = OPTIONS.get()
return i.get(name)
def set_context(self, options: dict):
"""Update current config"""
if not options:
return
opts = deepcopy(OPTIONS.get())
opts.update(options)
OPTIONS.set(opts)
self._update()
@property
def options(self):
"""Return all key-values"""
return OPTIONS.get()
def new_environ(self):
"""Return a new os.environ object"""
env = os.environ.copy()
2023-12-19 16:54:06 +08:00
i = self.options
env.update({k: v for k, v in i.items() if isinstance(v, str)})
return env
2023-07-07 10:30:53 +08:00
2023-09-19 21:31:27 +08:00
CONFIG = Config()