feat: Remove global configuration , enable configuration support for business isolation.

This commit is contained in:
莘权 马 2023-08-20 17:33:13 +08:00
parent d764b8e6fa
commit f45a8e5284
50 changed files with 437 additions and 278 deletions

View file

@ -4,6 +4,7 @@
@Time : 2023/5/11 14:43
@Author : alexanderwu
@File : action.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
from abc import ABC
from typing import Optional
@ -11,15 +12,14 @@ from typing import Optional
from tenacity import retry, stop_after_attempt, wait_fixed
from metagpt.actions.action_output import ActionOutput
from metagpt.llm import LLM
from metagpt.utils.common import OutputParser
from metagpt.logs import logger
class Action(ABC):
def __init__(self, name: str = '', context=None, llm: LLM = None):
def __init__(self, options, name: str = '', context=None, llm=None):
self.options = options
self.name: str = name
if llm is None:
llm = LLM()
self.llm = llm
self.context = context
self.prefix = ""

View file

@ -4,6 +4,7 @@
@Time : 2023/5/19 12:01
@Author : alexanderwu
@File : analyze_dep_libs.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
from metagpt.actions import Action
@ -26,8 +27,8 @@ Focus only on the names of shared dependencies, do not add any other explanation
class AnalyzeDepLibs(Action):
def __init__(self, name, context=None, llm=None):
super().__init__(name, context, llm)
def __init__(self, options, name, context=None, llm=None):
super().__init__(options=options, name=name, context=context, llm=llm)
self.desc = "根据上下文,分析程序运行依赖库"
async def run(self, requirement, filepaths_string):

View file

@ -4,6 +4,7 @@
@Time : 2023/5/11 17:46
@Author : alexanderwu
@File : debug_error.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
import re
@ -25,8 +26,8 @@ Now you should start rewriting the code:
## file name of the code to rewrite: Write code with triple quoto. Do your best to implement THIS IN ONLY ONE FILE.
"""
class DebugError(Action):
def __init__(self, name="DebugError", context=None, llm=None):
super().__init__(name, context, llm)
def __init__(self, options, name="DebugError", context=None, llm=None):
super().__init__(options=options, name=name, context=context, llm=llm)
# async def run(self, code, error):
# prompt = f"Here is a piece of Python code:\n\n{code}\n\nThe following error occurred during execution:" \

View file

@ -4,6 +4,7 @@
@Time : 2023/5/11 19:26
@Author : alexanderwu
@File : design_api.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
import shutil
from pathlib import Path
@ -90,8 +91,8 @@ OUTPUT_MAPPING = {
class WriteDesign(Action):
def __init__(self, name, context=None, llm=None):
super().__init__(name, context, llm)
def __init__(self, options, name, context=None, llm=None):
super().__init__(options=options, name=name, context=context, llm=llm)
self.desc = "Based on the PRD, think about the system design, and design the corresponding APIs, " \
"data structures, library tables, processes, and paths. Please provide your design, feedback " \
"clearly and in detail."
@ -106,15 +107,15 @@ class WriteDesign(Action):
def _save_prd(self, docs_path, resources_path, prd):
prd_file = docs_path / 'prd.md'
quadrant_chart = CodeParser.parse_code(block="Competitive Quadrant Chart", text=prd)
mermaid_to_file(quadrant_chart, resources_path / 'competitive_analysis')
mermaid_to_file(options=self.options, mermaid_code=quadrant_chart, output_file_without_suffix=resources_path / 'competitive_analysis')
logger.info(f"Saving PRD to {prd_file}")
prd_file.write_text(prd)
def _save_system_design(self, docs_path, resources_path, content):
data_api_design = CodeParser.parse_code(block="Data structures and interface definitions", text=content)
seq_flow = CodeParser.parse_code(block="Program call flow", text=content)
mermaid_to_file(data_api_design, resources_path / 'data_api_design')
mermaid_to_file(seq_flow, resources_path / 'seq_flow')
mermaid_to_file(options=self.options, mermaid_code=data_api_design, output_file_without_suffix=resources_path / 'data_api_design')
mermaid_to_file(options=self.options, mermaid_code=seq_flow, output_file_without_suffix=resources_path / 'seq_flow')
system_design_file = docs_path / 'system_design.md'
logger.info(f"Saving System Designs to {system_design_file}")
system_design_file.write_text(content)

View file

@ -4,13 +4,14 @@
@Time : 2023/5/11 19:31
@Author : alexanderwu
@File : design_api_review.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
from metagpt.actions.action import Action
class DesignReview(Action):
def __init__(self, name, context=None, llm=None):
super().__init__(name, context, llm)
def __init__(self, options, name, context=None, llm=None):
super().__init__(options=options, name=name, context=context, llm=llm)
async def run(self, prd, api_design):
prompt = f"Here is the Product Requirement Document (PRD):\n\n{prd}\n\nHere is the list of APIs designed " \

View file

@ -4,6 +4,7 @@
@Time : 2023/5/19 11:50
@Author : alexanderwu
@File : design_filenames.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
from metagpt.actions import Action
from metagpt.logs import logger
@ -15,8 +16,8 @@ Do not add any other explanations, just return a Python string list."""
class DesignFilenames(Action):
def __init__(self, name, context=None, llm=None):
super().__init__(name, context, llm)
def __init__(self, options, name, context=None, llm=None):
super().__init__(options=options, name=name, context=context, llm=llm)
self.desc = "Based on the PRD, consider system design, and carry out the basic design of the corresponding " \
"APIs, data structures, and database tables. Please give your design, feedback clearly and in detail."

View file

@ -4,6 +4,7 @@
@Time : 2023/5/11 19:12
@Author : alexanderwu
@File : project_management.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
from typing import List, Tuple
@ -103,8 +104,8 @@ OUTPUT_MAPPING = {
class WriteTasks(Action):
def __init__(self, name="CreateTasks", context=None, llm=None):
super().__init__(name, context, llm)
def __init__(self, options, name="CreateTasks", context=None, llm=None):
super().__init__(options=options, name=name, context=context, llm=llm)
def _save(self, context, rsp):
ws_name = CodeParser.parse_str(block="Python package name", text=context[-1].content)

View file

@ -1,5 +1,9 @@
#!/usr/bin/env python
"""
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
from __future__ import annotations
import asyncio
@ -9,7 +13,6 @@ from typing import Callable
from pydantic import parse_obj_as
from metagpt.actions import Action
from metagpt.config import CONFIG
from metagpt.logs import logger
from metagpt.tools.search_engine import SearchEngine
from metagpt.tools.web_browser_engine import WebBrowserEngine, WebBrowserEngineType
@ -79,14 +82,15 @@ class CollectLinks(Action):
"""Action class to collect links from a search engine."""
def __init__(
self,
options,
name: str = "",
*args,
rank_func: Callable[[list[str]], None] | None = None,
**kwargs,
):
super().__init__(name, *args, **kwargs)
super().__init__(options=options, name=name, *args, **kwargs)
self.desc = "Collect links from a search engine."
self.search_engine = SearchEngine()
self.search_engine = SearchEngine(options=options)
self.rank_func = rank_func
async def run(
@ -126,7 +130,7 @@ class CollectLinks(Action):
remove.pop()
if len(remove) == 0:
break
prompt = reduce_message_length(gen_msg(), self.llm.model, system_text, CONFIG.max_tokens_rsp)
prompt = reduce_message_length(gen_msg(), self.llm.model, system_text, self.options.get("max_tokens_rsp"))
logger.debug(prompt)
queries = await self._aask(prompt, [system_text])
try:
@ -178,9 +182,10 @@ class WebBrowseAndSummarize(Action):
**kwargs,
):
super().__init__(*args, **kwargs)
if CONFIG.model_for_researcher_summary:
self.llm.model = CONFIG.model_for_researcher_summary
if self.options.get("model_for_researcher_summary"):
self.llm.model = self.options.get("model_for_researcher_summary")
self.web_browser_engine = WebBrowserEngine(
options=self.options,
engine=WebBrowserEngineType.CUSTOM if browse_func else None,
run_func=browse_func,
)
@ -213,7 +218,8 @@ class WebBrowseAndSummarize(Action):
for u, content in zip([url, *urls], contents):
content = content.inner_text
chunk_summaries = []
for prompt in generate_prompt_chunk(content, prompt_template, self.llm.model, system_text, CONFIG.max_tokens_rsp):
for prompt in generate_prompt_chunk(content, prompt_template, self.llm.model, system_text,
self.options.get("max_tokens_rsp")):
logger.debug(prompt)
summary = await self._aask(prompt, [system_text])
if summary == "Not relevant.":
@ -239,8 +245,8 @@ class ConductResearch(Action):
"""Action class to conduct research and generate a research report."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if CONFIG.model_for_researcher_report:
self.llm.model = CONFIG.model_for_researcher_report
if self.options.get("model_for_researcher_report"):
self.llm.model = self.options.get("model_for_researcher_report")
async def run(
self,

View file

@ -4,6 +4,7 @@
@Time : 2023/5/11 17:46
@Author : alexanderwu
@File : run_code.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
import os
import subprocess
@ -57,8 +58,8 @@ standard errors: {errs};
class RunCode(Action):
def __init__(self, name="RunCode", context=None, llm=None):
super().__init__(name, context, llm)
def __init__(self, options, name="RunCode", context=None, llm=None):
super().__init__(options=options, name=name, context=context, llm=llm)
@classmethod
async def run_text(cls, code) -> Tuple[str, str]:

View file

@ -4,11 +4,11 @@
@Time : 2023/5/23 17:26
@Author : alexanderwu
@File : search_google.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
import pydantic
from metagpt.actions import Action
from metagpt.config import Config
from metagpt.logs import logger
from metagpt.schema import Message
from metagpt.tools.search_engine import SearchEngine
@ -101,17 +101,16 @@ You are a member of a professional butler team and will provide helpful suggesti
class SearchAndSummarize(Action):
def __init__(self, name="", context=None, llm=None, engine=None, search_func=None):
self.config = Config()
self.engine = engine or self.config.search_engine
def __init__(self, options, name="", context=None, llm=None, engine=None, search_func=None):
self.engine = engine or options.get("search_engine")
try:
self.search_engine = SearchEngine(self.engine, run_func=search_func)
self.search_engine = SearchEngine(options=options, engine=self.engine, run_func=search_func)
except pydantic.ValidationError:
self.search_engine = None
self.result = ""
super().__init__(name, context, llm)
super().__init__(options=options, name=name, context=context, llm=llm)
async def run(self, context: list[Message], system_text=SEARCH_AND_SUMMARIZE_SYSTEM) -> str:
if self.search_engine is None:

View file

@ -4,6 +4,7 @@
@Time : 2023/5/11 17:45
@Author : alexanderwu
@File : write_code.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
from metagpt.actions import WriteDesign
from metagpt.actions.action import Action
@ -43,8 +44,8 @@ ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenc
class WriteCode(Action):
def __init__(self, name="WriteCode", context: list[Message] = None, llm=None):
super().__init__(name, context, llm)
def __init__(self, options, name="WriteCode", context: list[Message] = None, llm=None):
super().__init__(options=options, name=name, context=context, llm=llm)
def _is_invalid(self, filename):
return any(i in filename for i in ["mp3", "wav"])

View file

@ -4,6 +4,7 @@
@Time : 2023/5/11 17:45
@Author : alexanderwu
@File : write_code_review.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
from metagpt.actions.action import Action
@ -62,8 +63,8 @@ FORMAT_EXAMPLE = """
class WriteCodeReview(Action):
def __init__(self, name="WriteCodeReview", context: list[Message] = None, llm=None):
super().__init__(name, context, llm)
def __init__(self, options, name="WriteCodeReview", context: list[Message] = None, llm=None):
super().__init__(options=options, name=name, context=context, llm=llm)
@retry(stop=stop_after_attempt(2), wait=wait_fixed(1))
async def write_code(self, prompt):

View file

@ -4,6 +4,7 @@
@Time : 2023/5/11 17:45
@Author : alexanderwu
@File : write_prd.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
from typing import List, Tuple
@ -127,11 +128,11 @@ OUTPUT_MAPPING = {
class WritePRD(Action):
def __init__(self, name="", context=None, llm=None):
super().__init__(name, context, llm)
def __init__(self, options, name="", context=None, llm=None):
super().__init__(options=options, name=name, context=context, llm=llm)
async def run(self, requirements, *args, **kwargs) -> ActionOutput:
sas = SearchAndSummarize()
sas = SearchAndSummarize(options=self.options, llm=self.llm)
# rsp = await sas.run(context=requirements, system_text=SEARCH_AND_SUMMARIZE_SYSTEM_EN_US)
rsp = ""
info = f"### Search Results\n{sas.result}\n\n### Search Summary\n{rsp}"

View file

@ -4,13 +4,14 @@
@Time : 2023/5/11 17:45
@Author : alexanderwu
@File : write_prd_review.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
from metagpt.actions.action import Action
class WritePRDReview(Action):
def __init__(self, name, context=None, llm=None):
super().__init__(name, context, llm)
def __init__(self, options, name, context=None, llm=None):
super().__init__(options=options, name=name, context=context, llm=llm)
self.prd = None
self.desc = "Based on the PRD, conduct a PRD Review, providing clear and detailed feedback"
self.prd_review_prompt_template = """

View file

@ -4,6 +4,7 @@
@Time : 2023/5/11 17:45
@Author : alexanderwu
@File : write_test.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
from metagpt.actions.action import Action
from metagpt.utils.common import CodeParser
@ -30,8 +31,8 @@ you should correctly import the necessary classes based on these file locations!
class WriteTest(Action):
def __init__(self, name="WriteTest", context=None, llm=None):
super().__init__(name, context, llm)
def __init__(self, options, name="WriteTest", context=None, llm=None):
super().__init__(options=options, name=name, context=context, llm=llm)
async def write_code(self, prompt):
code_rsp = await self._aask(prompt)

View file

@ -118,8 +118,8 @@ class Config:
return value
@property
def options(self):
"""Return key-value configuration parameters."""
def runtime_options(self):
"""Runtime key-value configuration parameters."""
opts = {}
for k, v in self._configs.items():
opts[k] = v

View file

@ -4,6 +4,7 @@
@Time : 2023/5/25 10:20
@Author : alexanderwu
@File : faiss_store.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
import pickle
from pathlib import Path
@ -36,8 +37,11 @@ class FaissStore(LocalStore):
store.index = index
return store
def _write(self, docs, metadatas):
store = FAISS.from_texts(docs, OpenAIEmbeddings(openai_api_version="2020-11-07"), metadatas=metadatas)
def _write(self, docs, metadatas, **kwargs):
store = FAISS.from_texts(docs,
OpenAIEmbeddings(openai_api_version="2020-11-07",
openai_api_key=kwargs.get("OPENAI_API_KEY")),
metadatas=metadatas)
return store
def persist(self):

View file

@ -1,20 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Time : 2023/5/11 14:45
@Author : alexanderwu
@File : llm.py
"""
from metagpt.provider.anthropic_api import Claude2 as Claude
from metagpt.provider.openai_api import OpenAIGPTAPI as LLM
DEFAULT_LLM = LLM()
CLAUDE_LLM = Claude()
async def ai_func(prompt):
"""使用LLM进行QA
QA with LLMs
"""
return await DEFAULT_LLM.aask(prompt)

View file

@ -4,11 +4,11 @@
@Time : 2023/6/5 01:44
@Author : alexanderwu
@File : skill_manager.py
@Modified By: mashenquan, 2023/8/20. Remove useless `_llm`
"""
from metagpt.actions import Action
from metagpt.const import PROMPT_PATH
from metagpt.document_store.chromadb_store import ChromaStore
from metagpt.llm import LLM
from metagpt.logs import logger
Skill = Action
@ -18,7 +18,6 @@ class SkillManager:
"""用来管理所有技能"""
def __init__(self):
self._llm = LLM()
self._store = ChromaStore('skill_manager')
self._skills: dict[str: Skill] = {}

View file

@ -4,14 +4,15 @@
@Time : 2023/5/11 14:42
@Author : alexanderwu
@File : manager.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
from metagpt.llm import LLM
from metagpt.logs import logger
from metagpt.schema import Message
class Manager:
def __init__(self, llm: LLM = LLM()):
def __init__(self, llm):
self.llm = llm # Large Language Model
self.role_directions = {
"BOSS": "Product Manager",

View file

@ -1,6 +1,9 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Desc : the implement of Long-term memory
"""
@Desc : the implement of Long-term memory
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
from metagpt.logs import logger
from metagpt.memory import Memory
@ -34,13 +37,13 @@ class LongTermMemory(Memory):
self.add_batch(messages)
self.msg_from_recover = False
def add(self, message: Message):
def add(self, message: Message, **kwargs):
super(LongTermMemory, self).add(message)
for action in self.rc.watch:
if message.cause_by == action and not self.msg_from_recover:
# currently, only add role's watching messages to its memory_storage
# and ignore adding messages from recover repeatedly
self.memory_storage.add(message)
self.memory_storage.add(message, **kwargs)
def remember(self, observed: list[Message], k=0) -> list[Message]:
"""

View file

@ -1,6 +1,9 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Desc : the implement of memory storage
"""
@Desc : the implement of memory storage
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
from typing import List
from pathlib import Path
@ -61,13 +64,13 @@ class MemoryStorage(FaissStore):
super(MemoryStorage, self).persist()
logger.debug(f'Agent {self.role_id} persist memory into local')
def add(self, message: Message) -> bool:
def add(self, message: Message, **kwargs) -> bool:
""" add message into memory storage"""
docs = [message.content]
metadatas = [{"message_ser": serialize_message(message)}]
if not self.store:
# init Faiss
self.store = self._write(docs, metadatas)
self.store = self._write(docs, metadatas, **kwargs)
self._initialized = True
else:
self.store.add_texts(texts=docs, metadatas=metadatas)

View file

@ -4,17 +4,22 @@
@Time : 2023/7/21 11:15
@Author : Leo Xiao
@File : anthropic_api.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation;
Change cost control from global to company level.
"""
import anthropic
from anthropic import Anthropic
from metagpt.config import CONFIG
from metagpt.config import Config
class Claude2:
def __init__(self, options=None):
self.options = options or Config().runtime_options
def ask(self, prompt):
client = Anthropic(api_key=CONFIG.claude_api_key)
client = Anthropic(api_key=self.claude_api_key)
res = client.completions.create(
model="claude-2",
@ -24,7 +29,7 @@ class Claude2:
return res.completion
async def aask(self, prompt):
client = Anthropic(api_key=CONFIG.claude_api_key)
client = Anthropic(api_key=self.claude_api_key)
res = client.completions.create(
model="claude-2",
@ -32,3 +37,7 @@ class Claude2:
max_tokens_to_sample=1000,
)
return res.completion
@property
def claude_api_key(self):
return self.options.get("claude_api_key")

View file

@ -3,6 +3,8 @@
@Time : 2023/5/5 23:08
@Author : alexanderwu
@File : openai.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation;
Change cost control from global to company level.
"""
import asyncio
import time
@ -12,10 +14,8 @@ import openai
from openai.error import APIConnectionError
from tenacity import retry, stop_after_attempt, after_log, wait_fixed, retry_if_exception_type
from metagpt.config import CONFIG
from metagpt.logs import logger
from metagpt.provider.base_gpt_api import BaseGPTAPI
from metagpt.utils.singleton import Singleton
from metagpt.utils.token_counter import (
TOKEN_COSTS,
count_message_tokens,
@ -56,13 +56,13 @@ class Costs(NamedTuple):
total_budget: float
class CostManager(metaclass=Singleton):
class CostManager:
"""计算使用接口的开销"""
def __init__(self):
def __init__(self, options):
self.total_prompt_tokens = 0
self.total_completion_tokens = 0
self.total_cost = 0
self.options = options
self.total_budget = 0
def update_cost(self, prompt_tokens, completion_tokens, model):
@ -79,10 +79,9 @@ class CostManager(metaclass=Singleton):
cost = (prompt_tokens * TOKEN_COSTS[model]["prompt"] + completion_tokens * TOKEN_COSTS[model]["completion"]) / 1000
self.total_cost += cost
logger.info(
f"Total running cost: ${self.total_cost:.3f} | Max budget: ${CONFIG.max_budget:.3f} | "
f"Total running cost: ${self.total_cost:.3f} | Max budget: ${self.max_budget:.3f} | "
f"Current cost: ${cost:.3f}, prompt_tokens: {prompt_tokens}, completion_tokens: {completion_tokens}"
)
CONFIG.total_cost = self.total_cost
def get_total_prompt_tokens(self):
"""
@ -115,6 +114,18 @@ class CostManager(metaclass=Singleton):
"""获得所有开销"""
return Costs(self.total_prompt_tokens, self.total_completion_tokens, self.total_cost, self.total_budget)
@property
def total_cost(self):
return self.options.get("total_cost", 0)
@total_cost.setter
def total_cost(self, v):
self.options["total_cost"] = v
@property
def max_budget(self):
return self.options.get("max_budget", 0)
def log_and_reraise(retry_state):
logger.error(f"Retry attempts exhausted. Last exception: {retry_state.outcome.exception()}")
@ -130,22 +141,23 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter):
Check https://platform.openai.com/examples for examples
"""
def __init__(self):
self.__init_openai(CONFIG)
def __init__(self, options, cost_manager):
self._options = options
self.__init_openai()
self.llm = openai
self.model = CONFIG.openai_api_model
self.model = self.openai_api_model
self.auto_max_tokens = False
self._cost_manager = CostManager()
self._cost_manager = cost_manager
RateLimiter.__init__(self, rpm=self.rpm)
def __init_openai(self, config):
openai.api_key = config.openai_api_key
if config.openai_api_base:
openai.api_base = config.openai_api_base
if config.openai_api_type:
openai.api_type = config.openai_api_type
openai.api_version = config.openai_api_version
self.rpm = int(config.get("RPM", 10))
def __init_openai(self):
openai.api_key = self.openai_api_key
if self.openai_api_base:
openai.api_base = self.openai_api_base
if self.openai_api_type:
openai.api_type = self.openai_api_type
openai.api_version = self.openai_api_version
self.rpm = int(self._options.get("RPM", 10))
async def _achat_completion_stream(self, messages: list[dict]) -> str:
response = await openai.ChatCompletion.acreate(**self._cons_kwargs(messages), stream=True)
@ -168,9 +180,9 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter):
return full_reply_content
def _cons_kwargs(self, messages: list[dict]) -> dict:
if CONFIG.openai_api_type == "azure":
if self._options.get("openai_api_type") == "azure":
kwargs = {
"deployment_id": CONFIG.deployment_id,
"deployment_id": self._options.get("deployment_id"),
"messages": messages,
"max_tokens": self.get_max_tokens(messages),
"n": 1,
@ -225,7 +237,7 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter):
def _calc_usage(self, messages: list[dict], rsp: str) -> dict:
usage = {}
if CONFIG.calc_usage:
if self._options.get("calc_usage"):
try:
prompt_tokens = count_message_tokens(messages, self.model)
completion_tokens = count_string_tokens(rsp, self.model)
@ -264,7 +276,7 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter):
return results
def _update_costs(self, usage: dict):
if CONFIG.calc_usage:
if self._options.get("calc_usage"):
try:
prompt_tokens = int(usage['prompt_tokens'])
completion_tokens = int(usage['completion_tokens'])
@ -277,5 +289,25 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter):
def get_max_tokens(self, messages: list[dict]):
if not self.auto_max_tokens:
return CONFIG.max_tokens_rsp
return get_max_completion_tokens(messages, self.model, CONFIG.max_tokens_rsp)
return self._options.get("max_tokens_rsp")
return get_max_completion_tokens(messages, self.model, self._options.get("max_tokens_rsp"))
@property
def openai_api_model(self):
return self._options.get("openai_api_model")
@property
def openai_api_key(self):
return self._options.get("openai_api_key")
@property
def openai_api_base(self):
return self._options.get("openai_api_base")
@property
def openai_api_type(self):
return self._options.get("openai_api_type")
@property
def openai_api_version(self):
return self._options.get("openai_api_version")

View file

@ -4,6 +4,8 @@
@Time : 2023/5/11 14:43
@Author : alexanderwu
@File : architect.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation;
Change cost control from global to company level.
"""
from metagpt.actions import WriteDesign, WritePRD
@ -12,8 +14,8 @@ from metagpt.roles import Role
class Architect(Role):
"""Architect: Listen to PRD, responsible for designing API, designing code files"""
def __init__(self, name="Bob", profile="Architect", goal="Design a concise, usable, complete python system",
def __init__(self, options, cost_manager, name="Bob", profile="Architect", goal="Design a concise, usable, complete python system",
constraints="Try to specify good open source tools as much as possible"):
super().__init__(name, profile, goal, constraints)
super().__init__(name=name, profile=profile, goal=goal, constraints=constraints, options=options, cost_manager=cost_manager)
self._init_actions([WriteDesign])
self._watch({WritePRD})

View file

@ -47,10 +47,10 @@ async def gather_ordered_k(coros, k) -> list:
class Engineer(Role):
def __init__(self, name="Alex", profile="Engineer", goal="Write elegant, readable, extensible, efficient code",
def __init__(self, options, cost_manager, name="Alex", profile="Engineer", goal="Write elegant, readable, extensible, efficient code",
constraints="The code you write should conform to code standard like PEP8, be modular, easy to read and maintain",
n_borg=1, use_code_review=False):
super().__init__(name, profile, goal, constraints)
super().__init__(name=name, profile=profile, goal=goal, constraints=constraints, options=options, cost_manager=cost_manager)
self._init_actions([WriteCode])
self.use_code_review = use_code_review
if self.use_code_review:
@ -131,7 +131,7 @@ class Engineer(Role):
async def _act_sp(self) -> Message:
code_msg_all = [] # gather all code info, will pass to qa_engineer for tests later
for todo in self.todos:
code = await WriteCode().run(
code = await WriteCode(options=self.options, llm=self._llm).run(
context=self._rc.history,
filename=todo
)

View file

@ -4,14 +4,16 @@
@Time : 2023/5/11 14:43
@Author : alexanderwu
@File : product_manager.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation;
Change cost control from global to company level.
"""
from metagpt.actions import BossRequirement, WritePRD
from metagpt.roles import Role
class ProductManager(Role):
def __init__(self, name="Alice", profile="Product Manager", goal="Efficiently create a successful product",
def __init__(self, options, cost_manager, name="Alice", profile="Product Manager", goal="Efficiently create a successful product",
constraints=""):
super().__init__(name, profile, goal, constraints)
super().__init__(name=name, profile=profile, goal=goal, constraints=constraints, options=options, cost_manager=cost_manager)
self._init_actions([WritePRD])
self._watch([BossRequirement])

View file

@ -4,14 +4,16 @@
@Time : 2023/5/11 15:04
@Author : alexanderwu
@File : project_manager.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation;
Change cost control from global to company level.
"""
from metagpt.actions import WriteDesign, WriteTasks
from metagpt.roles import Role
class ProjectManager(Role):
def __init__(self, name="Eve", profile="Project Manager",
def __init__(self, options, cost_manager, name="Eve", profile="Project Manager",
goal="Improve team efficiency and deliver with quality and quantity", constraints=""):
super().__init__(name, profile, goal, constraints)
super().__init__(name=name, profile=profile, goal=goal, constraints=constraints, options=options, cost_manager=cost_manager)
self._init_actions([WriteTasks])
self._watch([WriteDesign])

View file

@ -20,13 +20,15 @@ from metagpt.utils.special_tokens import FILENAME_CODE_SEP, MSG_SEP
class QaEngineer(Role):
def __init__(
self,
options,
cost_manager,
name="Edward",
profile="QaEngineer",
goal="Write comprehensive and robust tests to ensure codes will work as expected without bugs",
constraints="The test code you write should conform to code standard like PEP8, be modular, easy to read and maintain",
test_round_allowed=5,
):
super().__init__(name, profile, goal, constraints)
super().__init__(name=name, profile=profile, goal=goal, constraints=constraints, options=options, cost_manager=cost_manager)
self._init_actions(
[WriteTest]
) # FIXME: a bit hack here, only init one action to circumvent _think() logic, will overwrite _think() in future updates

View file

@ -4,17 +4,16 @@
@Time : 2023/5/11 14:42
@Author : alexanderwu
@File : role.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation;
Change cost control from global to company level.
"""
from __future__ import annotations
from typing import Iterable, Type
from typing import Iterable, Type, Dict
from pydantic import BaseModel, Field
# from metagpt.environment import Environment
from metagpt.config import CONFIG
from metagpt.provider.openai_api import OpenAIGPTAPI as LLM
from metagpt.actions import Action, ActionOutput
from metagpt.llm import LLM
from metagpt.logs import logger
from metagpt.memory import Memory, LongTermMemory
from metagpt.schema import Message
@ -71,12 +70,13 @@ class RoleContext(BaseModel):
todo: Action = Field(default=None)
watch: set[Type[Action]] = Field(default_factory=set)
news: list[Type[Message]] = Field(default=[])
options: Dict
class Config:
arbitrary_types_allowed = True
def check(self, role_id: str):
if hasattr(CONFIG, "long_term_memory") and CONFIG.long_term_memory:
if self.options.get("long_term_memory"):
self.long_term_memory.recover_memory(role_id, self)
self.memory = self.long_term_memory # use memory to act as long_term_memory for unify operation
@ -93,13 +93,15 @@ class RoleContext(BaseModel):
class Role:
"""角色/代理"""
def __init__(self, name="", profile="", goal="", constraints="", desc=""):
self._llm = LLM()
def __init__(self, options, cost_manager, name="", profile="", goal="", constraints="", desc=""):
self._options = options if options else {}
self._cost_manager = cost_manager
self._llm = LLM(options=self._options, cost_manager=cost_manager)
self._setting = RoleSetting(name=name, profile=profile, goal=goal, constraints=constraints, desc=desc)
self._states = []
self._actions = []
self._role_id = str(self._setting)
self._rc = RoleContext()
self._rc = RoleContext(options=options)
def _reset(self):
self._states = []
@ -109,7 +111,7 @@ class Role:
self._reset()
for idx, action in enumerate(actions):
if not isinstance(action, Action):
i = action("")
i = action(options=self._options, name="", llm=self._llm)
else:
i = action
i.set_prefix(self._get_prefix(), self.profile)
@ -137,6 +139,14 @@ class Role:
"""获取角色描述(职位)"""
return self._setting.profile
@property
def options(self):
return self._options
@options.setter
def options(self, opts):
self._options.update(opts)
def _get_prefix(self):
"""获取角色前缀"""
if self._setting.desc:

View file

@ -4,16 +4,21 @@
@Time : 2023/5/12 00:30
@Author : alexanderwu
@File : software_company.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation;
Change cost control from global to company level.
"""
from typing import Dict
from pydantic import BaseModel, Field
from metagpt.actions import BossRequirement
from metagpt.config import CONFIG
from metagpt.environment import Environment
from metagpt.logs import logger
from metagpt.provider.openai_api import CostManager
from metagpt.roles import Role
from metagpt.schema import Message
from metagpt.utils.common import NoMoneyException
from metagpt.config import Config
class SoftwareCompany(BaseModel):
@ -24,6 +29,8 @@ class SoftwareCompany(BaseModel):
environment: Environment = Field(default_factory=Environment)
investment: float = Field(default=10.0)
idea: str = Field(default="")
options: Dict = Field(default=Config().runtime_options)
cost_manager: CostManager = Field(default=CostManager(Config().runtime_options))
class Config:
arbitrary_types_allowed = True
@ -35,12 +42,12 @@ class SoftwareCompany(BaseModel):
def invest(self, investment: float):
"""Invest company. raise NoMoneyException when exceed max_budget."""
self.investment = investment
CONFIG.max_budget = investment
self.options["max_budget"] = investment
logger.info(f'Investment: ${investment}.')
def _check_balance(self):
if CONFIG.total_cost > CONFIG.max_budget:
raise NoMoneyException(CONFIG.total_cost, f'Insufficient funds: {CONFIG.max_budget}')
if self.total_cost > self.max_budget:
raise NoMoneyException(self.total_cost, f'Insufficient funds: {self.max_budget}')
def start_project(self, idea):
"""Start a project from publishing boss requirement."""
@ -59,3 +66,13 @@ class SoftwareCompany(BaseModel):
self._check_balance()
await self.environment.run()
return self.environment.history
@property
def max_budget(self):
return self.options.get("max_budget", 0)
@property
def total_cost(self):
return self.options.get("total_cost", 0)

View file

@ -4,13 +4,13 @@
@Time : 2023/5/6 20:15
@Author : alexanderwu
@File : search_engine.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
from __future__ import annotations
import importlib
from typing import Callable, Coroutine, Literal, overload
from typing import Callable, Coroutine, Literal, overload, Dict
from metagpt.config import CONFIG
from metagpt.tools import SearchEngineType
@ -25,24 +25,26 @@ class SearchEngine:
run_func: The function to run the search.
engine: The search engine type.
"""
def __init__(
self,
engine: SearchEngineType | None = None,
run_func: Callable[[str, int, bool], Coroutine[None, None, str | list[str]]] = None,
self,
options: Dict,
engine: SearchEngineType | None = None,
run_func: Callable[[str, int, bool], Coroutine[None, None, str | list[str]]] = None
):
engine = engine or CONFIG.search_engine
engine = engine or options.get("search_engine")
if engine == SearchEngineType.SERPAPI_GOOGLE:
module = "metagpt.tools.search_engine_serpapi"
run_func = importlib.import_module(module).SerpAPIWrapper().run
run_func = importlib.import_module(module).SerpAPIWrapper(**options).run
elif engine == SearchEngineType.SERPER_GOOGLE:
module = "metagpt.tools.search_engine_serper"
run_func = importlib.import_module(module).SerperWrapper().run
run_func = importlib.import_module(module).SerperWrapper(**options).run
elif engine == SearchEngineType.DIRECT_GOOGLE:
module = "metagpt.tools.search_engine_googleapi"
run_func = importlib.import_module(module).GoogleAPIWrapper().run
run_func = importlib.import_module(module).GoogleAPIWrapper(**options).run
elif engine == SearchEngineType.DUCK_DUCK_GO:
module = "metagpt.tools.search_engine_ddg"
run_func = importlib.import_module(module).DDGAPIWrapper().run
run_func = importlib.import_module(module).DDGAPIWrapper(**options).run
elif engine == SearchEngineType.CUSTOM_ENGINE:
pass # run_func = run_func
else:
@ -52,19 +54,19 @@ class SearchEngine:
@overload
def run(
self,
query: str,
max_results: int = 8,
as_string: Literal[True] = True,
self,
query: str,
max_results: int = 8,
as_string: Literal[True] = True,
) -> str:
...
@overload
def run(
self,
query: str,
max_results: int = 8,
as_string: Literal[False] = False,
self,
query: str,
max_results: int = 8,
as_string: Literal[False] = False,
) -> list[dict[str, str]]:
...

View file

@ -1,11 +1,14 @@
#!/usr/bin/env python
"""
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
from __future__ import annotations
import asyncio
import json
from concurrent import futures
from typing import Literal, overload
from typing import Literal, overload, Optional
try:
from duckduckgo_search import DDGS
@ -15,8 +18,6 @@ except ImportError:
"You can install it by running the command: `pip install -e.[search-ddg]`"
)
from metagpt.config import CONFIG
class DDGAPIWrapper:
"""Wrapper around duckduckgo_search API.
@ -25,43 +26,44 @@ class DDGAPIWrapper:
"""
def __init__(
self,
*,
loop: asyncio.AbstractEventLoop | None = None,
executor: futures.Executor | None = None,
self,
*,
global_proxy: Optional[str] = None,
loop: asyncio.AbstractEventLoop | None = None,
executor: futures.Executor | None = None,
):
kwargs = {}
if CONFIG.global_proxy:
kwargs["proxies"] = CONFIG.global_proxy
if global_proxy:
kwargs["proxies"] = global_proxy
self.loop = loop
self.executor = executor
self.ddgs = DDGS(**kwargs)
@overload
def run(
self,
query: str,
max_results: int = 8,
as_string: Literal[True] = True,
focus: list[str] | None = None,
self,
query: str,
max_results: int = 8,
as_string: Literal[True] = True,
focus: list[str] | None = None,
) -> str:
...
@overload
def run(
self,
query: str,
max_results: int = 8,
as_string: Literal[False] = False,
focus: list[str] | None = None,
self,
query: str,
max_results: int = 8,
as_string: Literal[False] = False,
focus: list[str] | None = None,
) -> list[dict[str, str]]:
...
async def run(
self,
query: str,
max_results: int = 8,
as_string: bool = True,
self,
query: str,
max_results: int = 8,
as_string: bool = True,
) -> str | list[dict]:
"""Return the results of a Google search using the official Google API

View file

@ -1,5 +1,8 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
from __future__ import annotations
import asyncio
@ -11,7 +14,6 @@ from urllib.parse import urlparse
import httplib2
from pydantic import BaseModel, validator
from metagpt.config import CONFIG
from metagpt.logs import logger
try:
@ -27,6 +29,7 @@ except ImportError:
class GoogleAPIWrapper(BaseModel):
google_api_key: Optional[str] = None
google_cse_id: Optional[str] = None
global_proxy: Optional[str] = None
loop: Optional[asyncio.AbstractEventLoop] = None
executor: Optional[futures.Executor] = None
@ -36,7 +39,6 @@ class GoogleAPIWrapper(BaseModel):
@validator("google_api_key", always=True)
@classmethod
def check_google_api_key(cls, val: str):
val = val or CONFIG.google_api_key
if not val:
raise ValueError(
"To use, make sure you provide the google_api_key when constructing an object. Alternatively, "
@ -47,8 +49,7 @@ class GoogleAPIWrapper(BaseModel):
@validator("google_cse_id", always=True)
@classmethod
def check_google_cse_id(cls, val: str):
val = val or CONFIG.google_cse_id
def check_google_cse_id(cls, val):
if not val:
raise ValueError(
"To use, make sure you provide the google_cse_id when constructing an object. Alternatively, "
@ -60,8 +61,8 @@ class GoogleAPIWrapper(BaseModel):
@property
def google_api_client(self):
build_kwargs = {"developerKey": self.google_api_key}
if CONFIG.global_proxy:
parse_result = urlparse(CONFIG.global_proxy)
if self.global_proxy:
parse_result = urlparse(self.global_proxy)
proxy_type = parse_result.scheme
if proxy_type == "https":
proxy_type = "http"

View file

@ -4,13 +4,14 @@
@Time : 2023/5/23 18:27
@Author : alexanderwu
@File : search_engine_serpapi.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
from typing import Any, Dict, Optional, Tuple
import aiohttp
from pydantic import BaseModel, Field, validator
from metagpt.config import CONFIG
from metagpt.config import Config
class SerpAPIWrapper(BaseModel):
@ -32,7 +33,6 @@ class SerpAPIWrapper(BaseModel):
@validator("serpapi_api_key", always=True)
@classmethod
def check_serpapi_api_key(cls, val: str):
val = val or CONFIG.serpapi_api_key
if not val:
raise ValueError(
"To use, make sure you provide the serpapi_api_key when constructing an object. Alternatively, "
@ -112,4 +112,4 @@ class SerpAPIWrapper(BaseModel):
if __name__ == "__main__":
import fire
fire.Fire(SerpAPIWrapper().run)
fire.Fire(SerpAPIWrapper(Config().runtime_options).run)

View file

@ -4,6 +4,7 @@
@Time : 2023/5/23 18:27
@Author : alexanderwu
@File : search_engine_serpapi.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
import json
from typing import Any, Dict, Optional, Tuple
@ -11,8 +12,6 @@ from typing import Any, Dict, Optional, Tuple
import aiohttp
from pydantic import BaseModel, Field, validator
from metagpt.config import CONFIG
class SerperWrapper(BaseModel):
search_engine: Any #: :meta private:
@ -26,7 +25,6 @@ class SerperWrapper(BaseModel):
@validator("serper_api_key", always=True)
@classmethod
def check_serper_api_key(cls, val: str):
val = val or CONFIG.serper_api_key
if not val:
raise ValueError(
"To use, make sure you provide the serper_api_key when constructing an object. Alternatively, "

View file

@ -1,29 +1,33 @@
#!/usr/bin/env python
"""
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
from __future__ import annotations
import importlib
from typing import Any, Callable, Coroutine, Literal, overload
from typing import Any, Callable, Coroutine, Literal, overload, Dict
from metagpt.config import CONFIG
from metagpt.config import Config
from metagpt.tools import WebBrowserEngineType
from metagpt.utils.parse_html import WebPage
class WebBrowserEngine:
def __init__(
self,
engine: WebBrowserEngineType | None = None,
run_func: Callable[..., Coroutine[Any, Any, WebPage | list[WebPage]]] | None = None,
self,
options: Dict,
engine: WebBrowserEngineType | None = None,
run_func: Callable[..., Coroutine[Any, Any, WebPage | list[WebPage]]] | None = None,
):
engine = engine or CONFIG.web_browser_engine
engine = engine or options.get("web_browser_engine")
if engine == WebBrowserEngineType.PLAYWRIGHT:
module = "metagpt.tools.web_browser_engine_playwright"
run_func = importlib.import_module(module).PlaywrightWrapper().run
run_func = importlib.import_module(module).PlaywrightWrapper(options=options).run
elif engine == WebBrowserEngineType.SELENIUM:
module = "metagpt.tools.web_browser_engine_selenium"
run_func = importlib.import_module(module).SeleniumWrapper().run
run_func = importlib.import_module(module).SeleniumWrapper(options=options).run
elif engine == WebBrowserEngineType.CUSTOM:
run_func = run_func
else:
@ -47,6 +51,10 @@ if __name__ == "__main__":
import fire
async def main(url: str, *urls: str, engine_type: Literal["playwright", "selenium"] = "playwright", **kwargs):
return await WebBrowserEngine(WebBrowserEngineType(engine_type), **kwargs).run(url, *urls)
conf = Config()
return await WebBrowserEngine(options=conf.runtime_options,
engine=WebBrowserEngineType(engine_type),
**kwargs).run(url, *urls)
fire.Fire(main)

View file

@ -1,14 +1,18 @@
#!/usr/bin/env python
"""
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
from __future__ import annotations
import asyncio
import sys
from pathlib import Path
from typing import Literal
from typing import Literal, Dict
from playwright.async_api import async_playwright
from metagpt.config import CONFIG
from metagpt.config import Config
from metagpt.logs import logger
from metagpt.utils.parse_html import WebPage
@ -24,18 +28,20 @@ class PlaywrightWrapper:
def __init__(
self,
options: Dict,
browser_type: Literal["chromium", "firefox", "webkit"] | None = None,
launch_kwargs: dict | None = None,
**kwargs,
) -> None:
self.options = options
if browser_type is None:
browser_type = CONFIG.playwright_browser_type
browser_type = options.get("playwright_browser_type")
self.browser_type = browser_type
launch_kwargs = launch_kwargs or {}
if CONFIG.global_proxy and "proxy" not in launch_kwargs:
if options.get("global_proxy") and "proxy" not in launch_kwargs:
args = launch_kwargs.get("args", [])
if not any(str.startswith(i, "--proxy-server=") for i in args):
launch_kwargs["proxy"] = {"server": CONFIG.global_proxy}
launch_kwargs["proxy"] = {"server": options.get("global_proxy")}
self.launch_kwargs = launch_kwargs
context_kwargs = {}
if "ignore_https_errors" in kwargs:
@ -75,8 +81,8 @@ class PlaywrightWrapper:
executable_path = Path(browser_type.executable_path)
if not executable_path.exists() and "executable_path" not in self.launch_kwargs:
kwargs = {}
if CONFIG.global_proxy:
kwargs["env"] = {"ALL_PROXY": CONFIG.global_proxy}
if self.options.get("global_proxy"):
kwargs["env"] = {"ALL_PROXY": self.options.get("global_proxy")}
await _install_browsers(self.browser_type, **kwargs)
if self._has_run_precheck:
@ -144,6 +150,8 @@ if __name__ == "__main__":
import fire
async def main(url: str, *urls: str, browser_type: str = "chromium", **kwargs):
return await PlaywrightWrapper(browser_type, **kwargs).run(url, *urls)
return await PlaywrightWrapper(options=Config().runtime_options,
browser_type=browser_type,
**kwargs).run(url, *urls)
fire.Fire(main)

View file

@ -1,17 +1,21 @@
#!/usr/bin/env python
"""
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
from __future__ import annotations
import asyncio
import importlib
from concurrent import futures
from copy import deepcopy
from typing import Literal
from typing import Literal, Dict
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
from metagpt.config import CONFIG
from metagpt.config import Config
from metagpt.utils.parse_html import WebPage
@ -29,6 +33,7 @@ class SeleniumWrapper:
def __init__(
self,
options: Dict,
browser_type: Literal["chrome", "firefox", "edge", "ie"] | None = None,
launch_kwargs: dict | None = None,
*,
@ -36,11 +41,11 @@ class SeleniumWrapper:
executor: futures.Executor | None = None,
) -> None:
if browser_type is None:
browser_type = CONFIG.selenium_browser_type
browser_type = options.get("selenium_browser_type")
self.browser_type = browser_type
launch_kwargs = launch_kwargs or {}
if CONFIG.global_proxy and "proxy-server" not in launch_kwargs:
launch_kwargs["proxy-server"] = CONFIG.global_proxy
if options.get("global_proxy") and "proxy-server" not in launch_kwargs:
launch_kwargs["proxy-server"] = options.get("global_proxy")
self.executable_path = launch_kwargs.pop("executable_path", None)
self.launch_args = [f"--{k}={v}" for k, v in launch_kwargs.items()]
@ -118,6 +123,8 @@ if __name__ == "__main__":
import fire
async def main(url: str, *urls: str, browser_type: str = "chrome", **kwargs):
return await SeleniumWrapper(browser_type, **kwargs).run(url, *urls)
return await SeleniumWrapper(options=Config().runtime_options,
browser_type=browser_type,
**kwargs).run(url, *urls)
fire.Fire(main)

View file

@ -4,19 +4,21 @@
@Time : 2023/7/4 10:53
@Author : alexanderwu
@File : mermaid.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
import subprocess
from pathlib import Path
from metagpt.config import CONFIG
from metagpt.config import Config
from metagpt.const import PROJECT_ROOT
from metagpt.logs import logger
from metagpt.utils.common import check_cmd_exists
def mermaid_to_file(mermaid_code, output_file_without_suffix, width=2048, height=2048) -> int:
def mermaid_to_file(options, mermaid_code, output_file_without_suffix, width=2048, height=2048) -> int:
"""suffix: png/svg/pdf
:param options: runtime context options, created by `Config` class object and changed in flow pipeline
:param mermaid_code: mermaid code
:param output_file_without_suffix: output filename
:param width:
@ -36,12 +38,12 @@ def mermaid_to_file(mermaid_code, output_file_without_suffix, width=2048, height
# Call the `mmdc` command to convert the Mermaid code to a PNG
logger.info(f"Generating {output_file}..")
if CONFIG.puppeteer_config:
if options.get("puppeteer_config"):
subprocess.run(
[
CONFIG.mmdc,
options.get("mmdc"),
"-p",
CONFIG.puppeteer_config,
options.get("puppeteer_config"),
"-i",
str(tmp),
"-o",
@ -53,7 +55,7 @@ def mermaid_to_file(mermaid_code, output_file_without_suffix, width=2048, height
]
)
else:
subprocess.run([CONFIG.mmdc, "-i", str(tmp), "-o", output_file, "-w", str(width), "-H", str(height)])
subprocess.run([options.get("mmdc"), "-i", str(tmp), "-o", output_file, "-w", str(width), "-H", str(height)])
return 0
@ -109,6 +111,8 @@ MMC2 = """sequenceDiagram
if __name__ == "__main__":
# logger.info(print_members(print_members))
mermaid_to_file(MMC1, PROJECT_ROOT / "tmp/1.png")
mermaid_to_file(MMC2, PROJECT_ROOT / "tmp/2.png")
conf = Config()
mermaid_to_file(options=conf.runtime_options, mermaid_code=MMC1,
output_file_without_suffix=PROJECT_ROOT / "tmp/1.png")
mermaid_to_file(options=conf.runtime_options, mermaid_code=MMC2,
output_file_without_suffix=PROJECT_ROOT / "tmp/2.png")

View file

@ -1,5 +1,10 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation;
Change cost control from global to company level.
"""
import asyncio
import fire
@ -11,14 +16,15 @@ from metagpt.software_company import SoftwareCompany
async def startup(idea: str, investment: float = 3.0, n_round: int = 5,
code_review: bool = False, run_tests: bool = False):
"""Run a startup. Be a boss."""
company = SoftwareCompany()
company.hire([ProductManager(),
Architect(),
ProjectManager(),
Engineer(n_borg=5, use_code_review=code_review)])
company.hire([ProductManager(options=company.options, cost_manager=company.cost_manager),
Architect(options=company.options, cost_manager=company.cost_manager),
ProjectManager(options=company.options, cost_manager=company.cost_manager),
Engineer(n_borg=5, use_code_review=code_review, options=company.options, cost_manager=company.cost_manager)])
if run_tests:
# developing features: run tests on the spot and identify bugs (bug fixing capability comes soon!)
company.hire([QaEngineer()])
company.hire([QaEngineer(options=company.options, cost_manager=company.cost_manager)])
company.invest(investment)
company.start_project(idea)
await company.run(n_round=n_round)

View file

@ -4,11 +4,13 @@
@Time : 2023/5/11 17:45
@Author : alexanderwu
@File : test_write_code.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
import pytest
from metagpt.config import Config
from metagpt.provider.openai_api import OpenAIGPTAPI as LLM, CostManager
from metagpt.actions.write_code import WriteCode
from metagpt.llm import LLM
from metagpt.logs import logger
from tests.metagpt.actions.mock import TASKS_2, WRITE_CODE_PROMPT_SAMPLE
@ -16,9 +18,12 @@ from tests.metagpt.actions.mock import TASKS_2, WRITE_CODE_PROMPT_SAMPLE
@pytest.mark.asyncio
async def test_write_code():
api_design = "设计一个名为'add'的函数,该函数接受两个整数作为输入,并返回它们的和。"
write_code = WriteCode("write_code")
conf = Config()
cost_manager = CostManager(conf.runtime_options)
llm = LLM(options=conf.runtime_options, cost_manager=cost_manager)
write_code = WriteCode(options=conf.runtime_options, name="write_code", llm=llm)
code = await write_code.run(api_design)
code = await write_code.run(api_design, "filename")
logger.info(code)
# 我们不能精确地预测生成的代码,但我们可以检查某些关键字
@ -29,6 +34,7 @@ async def test_write_code():
@pytest.mark.asyncio
async def test_write_code_directly():
prompt = WRITE_CODE_PROMPT_SAMPLE + '\n' + TASKS_2[0]
llm = LLM()
options = Config().runtime_options
llm = LLM(options=options, cost_manager=CostManager(options=options))
rsp = await llm.aask(prompt)
logger.info(rsp)

View file

@ -1,8 +1,10 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Desc : unittest of `metagpt/memory/longterm_memory.py`
from metagpt.config import CONFIG
"""
@Desc : unittest of `metagpt/memory/longterm_memory.py`
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
from metagpt.config import Config
from metagpt.schema import Message
from metagpt.actions import BossRequirement
from metagpt.roles.role import RoleContext
@ -10,12 +12,13 @@ from metagpt.memory import LongTermMemory
def test_ltm_search():
assert hasattr(CONFIG, "long_term_memory") is True
openai_api_key = CONFIG.openai_api_key
conf = Config()
assert hasattr(conf, "long_term_memory") is True
openai_api_key = conf.openai_api_key
assert len(openai_api_key) > 20
role_id = 'UTUserLtm(Product Manager)'
rc = RoleContext(watch=[BossRequirement])
rc = RoleContext(options=conf.runtime_options, watch=[BossRequirement])
ltm = LongTermMemory()
ltm.recover_memory(role_id, rc)
@ -23,19 +26,19 @@ def test_ltm_search():
message = Message(role='BOSS', content=idea, cause_by=BossRequirement)
news = ltm.remember([message])
assert len(news) == 1
ltm.add(message)
ltm.add(message, **conf.runtime_options)
sim_idea = 'Write a game of cli snake'
sim_message = Message(role='BOSS', content=sim_idea, cause_by=BossRequirement)
news = ltm.remember([sim_message])
assert len(news) == 0
ltm.add(sim_message)
ltm.add(sim_message, **conf.runtime_options)
new_idea = 'Write a 2048 web game'
new_message = Message(role='BOSS', content=new_idea, cause_by=BossRequirement)
news = ltm.remember([new_message])
assert len(news) == 1
ltm.add(new_message)
ltm.add(new_message, **conf.runtime_options)
# restore from local index
ltm_new = LongTermMemory()

View file

@ -4,14 +4,17 @@
@Time : 2023/5/12 00:47
@Author : alexanderwu
@File : test_environment.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
import pytest
from metagpt.actions import BossRequirement
from metagpt.config import Config
from metagpt.environment import Environment
from metagpt.logs import logger
from metagpt.manager import Manager
from metagpt.provider.openai_api import CostManager
from metagpt.roles import Architect, ProductManager, Role
from metagpt.schema import Message
@ -22,33 +25,45 @@ def env():
def test_add_role(env: Environment):
role = ProductManager("Alice", "product manager", "create a new product", "limited resources")
conf = Config()
cost_manager = CostManager(options=conf.runtime_options)
role = ProductManager(options=conf.runtime_options,
cost_manager=cost_manager,
name="Alice",
profile="product manager",
goal="create a new product",
constraints="limited resources")
env.add_role(role)
assert env.get_role(role.profile) == role
def test_get_roles(env: Environment):
role1 = Role("Alice", "product manager", "create a new product", "limited resources")
role2 = Role("Bob", "engineer", "develop the new product", "short deadline")
conf = Config()
cost_manager = CostManager(options=conf.runtime_options)
role1 = Role(options=conf.runtime_options, cost_manager=cost_manager, name="Alice", profile="product manager",
goal="create a new product", constraints="limited resources")
role2 = Role(options=conf.runtime_options, cost_manager=cost_manager, name="Bob", profile="engineer",
goal="develop the new product", constraints="short deadline")
env.add_role(role1)
env.add_role(role2)
roles = env.get_roles()
assert roles == {role1.profile: role1, role2.profile: role2}
def test_set_manager(env: Environment):
manager = Manager()
env.set_manager(manager)
assert env.manager == manager
@pytest.mark.asyncio
async def test_publish_and_process_message(env: Environment):
product_manager = ProductManager("Alice", "Product Manager", "做AI Native产品", "资源有限")
architect = Architect("Bob", "Architect", "设计一个可用、高效、较低成本的系统,包括数据结构与接口", "资源有限,需要节省成本")
conf = Config()
cost_manager = CostManager(options=conf.runtime_options)
product_manager = ProductManager(options=conf.runtime_options,
cost_manager=cost_manager,
name="Alice", profile="Product Manager",
goal="做AI Native产品", constraints="资源有限")
architect = Architect(options=conf.runtime_options,
cost_manager=cost_manager,
name="Bob", profile="Architect", goal="设计一个可用、高效、较低成本的系统,包括数据结构与接口",
constraints="资源有限,需要节省成本")
env.add_roles([product_manager, architect])
env.set_manager(Manager())
env.publish_message(Message(role="BOSS", content="需要一个基于LLM做总结的搜索引擎", cause_by=BossRequirement))
await env.run(k=2)

View file

@ -4,16 +4,19 @@
@Time : 2023/5/11 14:45
@Author : alexanderwu
@File : test_llm.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
import pytest
from metagpt.llm import LLM
from metagpt.config import Config
from metagpt.provider.openai_api import OpenAIGPTAPI as LLM, CostManager
@pytest.fixture()
def llm():
return LLM()
options = Config().runtime_options
return LLM(options=options, cost_manager=CostManager(options))
@pytest.mark.asyncio

View file

@ -4,11 +4,13 @@
@Time : 2023/5/2 17:46
@Author : alexanderwu
@File : test_search_engine.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
from __future__ import annotations
import pytest
from metagpt.config import Config
from metagpt.logs import logger
from metagpt.tools import SearchEngineType
from metagpt.tools.search_engine import SearchEngine
@ -37,9 +39,10 @@ class MockSearchEnine:
],
)
async def test_search_engine(search_engine_typpe, run_func, max_results, as_string, ):
search_engine = SearchEngine(search_engine_typpe, run_func)
rsp = await search_engine.run("metagpt", max_results=max_results, as_string=as_string)
async def test_search_engine(search_engine_typpe, run_func, max_results, as_string):
conf = Config()
search_engine = SearchEngine(options=conf.runtime_options, engine=search_engine_typpe, run_func=run_func)
rsp = await search_engine.run(query="metagpt", max_results=max_results, as_string=as_string)
logger.info(rsp)
if as_string:
assert isinstance(rsp, str)

View file

@ -1,5 +1,10 @@
"""
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
import pytest
from metagpt.config import Config
from metagpt.tools import WebBrowserEngineType, web_browser_engine
@ -13,7 +18,8 @@ from metagpt.tools import WebBrowserEngineType, web_browser_engine
ids=["playwright", "selenium"],
)
async def test_scrape_web_page(browser_type, url, urls):
browser = web_browser_engine.WebBrowserEngine(browser_type)
conf = Config()
browser = web_browser_engine.WebBrowserEngine(options=conf.runtime_options, engine=browser_type)
result = await browser.run(url)
assert isinstance(result, str)
assert "深度赋智" in result

View file

@ -1,6 +1,10 @@
"""
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
import pytest
from metagpt.config import CONFIG
from metagpt.config import Config
from metagpt.tools import web_browser_engine_playwright
@ -15,22 +19,24 @@ from metagpt.tools import web_browser_engine_playwright
ids=["chromium-normal", "firefox-normal", "webkit-normal"],
)
async def test_scrape_web_page(browser_type, use_proxy, kwagrs, url, urls, proxy, capfd):
conf = Config()
global_proxy = conf.global_proxy
try:
global_proxy = CONFIG.global_proxy
if use_proxy:
CONFIG.global_proxy = proxy
browser = web_browser_engine_playwright.PlaywrightWrapper(browser_type, **kwagrs)
conf.global_proxy = proxy
browser = web_browser_engine_playwright.PlaywrightWrapper(options=conf.runtime_options,
browser_type=browser_type, **kwagrs)
result = await browser.run(url)
result = result.inner_text
assert isinstance(result, str)
assert "Deepwisdom" in result
assert "DeepWisdom" in result
if urls:
results = await browser.run(url, *urls)
assert isinstance(results, list)
assert len(results) == len(urls) + 1
assert all(("Deepwisdom" in i) for i in results)
assert all(("DeepWisdom" in i) for i in results)
if use_proxy:
assert "Proxy:" in capfd.readouterr().out
finally:
CONFIG.global_proxy = global_proxy
conf.global_proxy = global_proxy

View file

@ -1,6 +1,10 @@
"""
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
import pytest
from metagpt.config import CONFIG
from metagpt.config import Config
from metagpt.tools import web_browser_engine_selenium
@ -15,11 +19,12 @@ from metagpt.tools import web_browser_engine_selenium
ids=["chrome-normal", "firefox-normal", "edge-normal"],
)
async def test_scrape_web_page(browser_type, use_proxy, url, urls, proxy, capfd):
conf = Config()
global_proxy = conf.global_proxy
try:
global_proxy = CONFIG.global_proxy
if use_proxy:
CONFIG.global_proxy = proxy
browser = web_browser_engine_selenium.SeleniumWrapper(browser_type)
conf.global_proxy = proxy
browser = web_browser_engine_selenium.SeleniumWrapper(options=conf.runtime_options, browser_type=browser_type)
result = await browser.run(url)
result = result.inner_text
assert isinstance(result, str)
@ -33,4 +38,4 @@ async def test_scrape_web_page(browser_type, use_proxy, url, urls, proxy, capfd)
if use_proxy:
assert "Proxy:" in capfd.readouterr().out
finally:
CONFIG.global_proxy = global_proxy
conf.global_proxy = global_proxy

View file

@ -4,7 +4,7 @@
@Time : 2023/5/1 11:19
@Author : alexanderwu
@File : test_config.py
@Modified By: mashenquan, 2013/8/20, add `test_options`
@Modified By: mashenquan, 2013/8/20, Add `test_options`; remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
from pathlib import Path
@ -13,12 +13,6 @@ import pytest
from metagpt.config import Config
def test_config_class_is_singleton():
config_1 = Config()
config_2 = Config()
assert config_1 == config_2
def test_config_class_get_key_exception():
with pytest.raises(Exception) as exc_info:
config = Config()
@ -27,16 +21,15 @@ def test_config_class_get_key_exception():
def test_config_yaml_file_not_exists():
config = Config('wtf.yaml')
with pytest.raises(Exception) as exc_info:
config.get('OPENAI_BASE_URL')
assert str(exc_info.value) == "Key 'OPENAI_BASE_URL' not found in environment variables or in the YAML file"
Config(Path('wtf.yaml'))
assert str(exc_info.value) == "Set OPENAI_API_KEY or Anthropic_API_KEY first"
def test_options():
filename = Path(__file__).resolve().parent.parent.parent.parent / "config/config.yaml"
config = Config(filename)
opts = config.options
opts = config.runtime_options
assert opts