From 15b86e85334eaa98910a15ea84580dff7da1e9c5 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Mon, 3 Jun 2024 10:14:57 +0800 Subject: [PATCH 001/119] experience pool --- config/config2.example.yaml | 3 +++ examples/exp_pool/manager.py | 21 ++++++++++++++++++++ metagpt/config2.py | 4 ++++ metagpt/configs/exp_pool_config.py | 6 ++++++ metagpt/exp_pool/__init__.py | 0 metagpt/exp_pool/decorator.py | 4 ++++ metagpt/exp_pool/manager.py | 32 ++++++++++++++++++++++++++++++ metagpt/exp_pool/schema.py | 25 +++++++++++++++++++++++ metagpt/utils/reflection.py | 9 +++++++++ metagpt/utils/token_counter.py | 2 ++ 10 files changed, 106 insertions(+) create mode 100644 examples/exp_pool/manager.py create mode 100644 metagpt/configs/exp_pool_config.py create mode 100644 metagpt/exp_pool/__init__.py create mode 100644 metagpt/exp_pool/decorator.py create mode 100644 metagpt/exp_pool/manager.py create mode 100644 metagpt/exp_pool/schema.py diff --git a/config/config2.example.yaml b/config/config2.example.yaml index f1158775b..c5ca6e767 100644 --- a/config/config2.example.yaml +++ b/config/config2.example.yaml @@ -74,6 +74,9 @@ s3: secure: false bucket: "test" +experience_pool: + enable_read: false + enable_write: false azure_tts_subscription_key: "YOUR_SUBSCRIPTION_KEY" azure_tts_region: "eastus" diff --git a/examples/exp_pool/manager.py b/examples/exp_pool/manager.py new file mode 100644 index 000000000..f5766f9a5 --- /dev/null +++ b/examples/exp_pool/manager.py @@ -0,0 +1,21 @@ +from metagpt.exp_pool.manager import ExperiencePoolManager +from metagpt.exp_pool.schema import Experience +from pprint import pprint +import asyncio +# import logging +# logging.basicConfig(level=logging.DEBUG) + +async def main(): + req = "2048 game" + exp = Experience(req=req, resp="python code") + + manager = ExperiencePoolManager() + + # pprint(manager.storage.get()) + # manager.create_exp(exp) + result = await manager.query_exp(req) + print(result) + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/metagpt/config2.py b/metagpt/config2.py index 8c61fdbf2..3f8930401 100644 --- a/metagpt/config2.py +++ b/metagpt/config2.py @@ -21,6 +21,7 @@ from metagpt.configs.search_config import SearchConfig from metagpt.configs.workspace_config import WorkspaceConfig from metagpt.const import CONFIG_ROOT, METAGPT_ROOT from metagpt.utils.yaml_model import YamlModel +from metagpt.configs.exp_pool_config import ExperiencePoolConfig class CLIParams(BaseModel): @@ -67,6 +68,9 @@ class Config(CLIParams, YamlModel): enable_longterm_memory: bool = False code_review_k_times: int = 2 + # Experience Pool Parameters + experience_pool: Optional[ExperiencePoolConfig] = None + # Will be removed in the future metagpt_tti_url: str = "" language: str = "English" diff --git a/metagpt/configs/exp_pool_config.py b/metagpt/configs/exp_pool_config.py new file mode 100644 index 000000000..f7312d2de --- /dev/null +++ b/metagpt/configs/exp_pool_config.py @@ -0,0 +1,6 @@ +from metagpt.utils.yaml_model import YamlModel + + +class ExperiencePoolConfig(YamlModel): + enable_read: bool = False + enable_write: bool = False diff --git a/metagpt/exp_pool/__init__.py b/metagpt/exp_pool/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/metagpt/exp_pool/decorator.py b/metagpt/exp_pool/decorator.py new file mode 100644 index 000000000..6629e8377 --- /dev/null +++ b/metagpt/exp_pool/decorator.py @@ -0,0 +1,4 @@ + + +def exp_cache(func): + pass diff --git a/metagpt/exp_pool/manager.py b/metagpt/exp_pool/manager.py new file mode 100644 index 000000000..c32073a9f --- /dev/null +++ b/metagpt/exp_pool/manager.py @@ -0,0 +1,32 @@ +from pydantic import BaseModel, ConfigDict +from metagpt.exp_pool.schema import Experience +import uuid +import chromadb +from chromadb import Collection, QueryResult +from typing import Optional +from metagpt.rag.engines import SimpleEngine +from metagpt.rag.schema import ChromaRetrieverConfig + + +class ExperiencePoolManager(BaseModel): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._storage = None + + @property + def storage(self) -> SimpleEngine: + if self._storage is None: + self._storage = SimpleEngine.from_objs(retriever_configs=[ChromaRetrieverConfig(collection_name="experience_pool", persist_path="./chroma_data")]) + return self._storage + + def create_exp(self, exp: Experience): + self.storage.add_objs([exp]) + + async def query_exp(self, req: str) -> list[Experience]: + nodes = await self.storage.aretrieve(req) + exps = [node.metadata["obj"] for node in nodes] + + return exps + + + diff --git a/metagpt/exp_pool/schema.py b/metagpt/exp_pool/schema.py new file mode 100644 index 000000000..359268612 --- /dev/null +++ b/metagpt/exp_pool/schema.py @@ -0,0 +1,25 @@ +from pydantic import BaseModel, Field +from llama_index.core.schema import TextNode + + +class Experience(BaseModel): + req: str = Field(..., description="") + resp: str = Field(..., description="") + + def rag_key(self): + return self.req + + +class ExperienceNodeMetadata(BaseModel): + """Metadata of ExperienceNode.""" + + resp: str = Field(..., description="") + + +class ExperienceNode(TextNode): + """ExperienceNode for RAG.""" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.excluded_llm_metadata_keys = list(ExperienceNodeMetadata.model_fields.keys()) + self.excluded_embed_metadata_keys = self.excluded_llm_metadata_keys diff --git a/metagpt/utils/reflection.py b/metagpt/utils/reflection.py index 8b8237ae7..688831f06 100644 --- a/metagpt/utils/reflection.py +++ b/metagpt/utils/reflection.py @@ -1,4 +1,5 @@ """class tools, including method inspection, class attributes, inheritance relationships, etc.""" +import inspect def check_methods(C, *methods): @@ -16,3 +17,11 @@ def check_methods(C, *methods): else: return NotImplemented return True + + +def get_func_full_name(func, *args) -> str: + if inspect.ismethod(func) or (inspect.isfunction(func) and "self" in inspect.signature(func).parameters): + cls_name = args[0].__class__.__name__ + return f"{func.__module__}.{cls_name}.{func.__name__}" + + return f"{func.__module__}.{func.__name__}" diff --git a/metagpt/utils/token_counter.py b/metagpt/utils/token_counter.py index 0ba2daa89..496842a2d 100644 --- a/metagpt/utils/token_counter.py +++ b/metagpt/utils/token_counter.py @@ -150,6 +150,8 @@ TOKEN_MAX = { "gpt-4-1106-preview": 128000, "gpt-4-vision-preview": 128000, "gpt-4-1106-vision-preview": 128000, + "gpt-4-turbo": 128000, + "gpt-4o": 128000, "gpt-4": 8192, "gpt-4-0613": 8192, "gpt-4-32k": 32768, From 471310f3b3e879ea269d2941f525c83e04b55938 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Tue, 4 Jun 2024 10:28:39 +0800 Subject: [PATCH 002/119] experiment pool init --- .gitignore | 1 + config/config2.example.yaml | 6 +- examples/exp_pool/decorator.py | 26 ++++++ examples/exp_pool/manager.py | 21 ----- examples/exp_pool/simple.py | 29 ++++++ metagpt/config2.py | 4 +- metagpt/configs/exp_pool_config.py | 6 +- metagpt/exp_pool/__init__.py | 6 ++ metagpt/exp_pool/decorator.py | 56 ++++++++++- metagpt/exp_pool/manager.py | 113 +++++++++++++++++++---- metagpt/exp_pool/schema.py | 40 +++++++- metagpt/rag/retrievers/bm25_retriever.py | 2 +- metagpt/utils/file.py | 1 - metagpt/utils/reflection.py | 2 +- 14 files changed, 258 insertions(+), 55 deletions(-) create mode 100644 examples/exp_pool/decorator.py delete mode 100644 examples/exp_pool/manager.py create mode 100644 examples/exp_pool/simple.py diff --git a/.gitignore b/.gitignore index aa5edd74a..7c64829ad 100644 --- a/.gitignore +++ b/.gitignore @@ -162,6 +162,7 @@ examples/graph_store.json examples/image__vector_store.json examples/index_store.json .chroma +.chroma_exp_data *~$* workspace/* tmp diff --git a/config/config2.example.yaml b/config/config2.example.yaml index c5ca6e767..c7b2cae2c 100644 --- a/config/config2.example.yaml +++ b/config/config2.example.yaml @@ -74,9 +74,9 @@ s3: secure: false bucket: "test" -experience_pool: - enable_read: false - enable_write: false +exp_pool: + enable_read: true + enable_write: true azure_tts_subscription_key: "YOUR_SUBSCRIPTION_KEY" azure_tts_region: "eastus" diff --git a/examples/exp_pool/decorator.py b/examples/exp_pool/decorator.py new file mode 100644 index 000000000..2f6397f80 --- /dev/null +++ b/examples/exp_pool/decorator.py @@ -0,0 +1,26 @@ +"""Decorator example of experience pool.""" + +import asyncio +import uuid + +from metagpt.exp_pool import exp_cache, exp_manager +from metagpt.logs import logger + + +@exp_cache +async def produce(req): + return f"{req} {uuid.uuid4().hex}" + + +async def main(): + req = "Water" + + resp = await produce(req) + logger.info(f"The resp of `produce{req}` is: {resp}") + + exps = await exp_manager.query_exps(req) + logger.info(f"Find experiences: {exps}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/exp_pool/manager.py b/examples/exp_pool/manager.py deleted file mode 100644 index f5766f9a5..000000000 --- a/examples/exp_pool/manager.py +++ /dev/null @@ -1,21 +0,0 @@ -from metagpt.exp_pool.manager import ExperiencePoolManager -from metagpt.exp_pool.schema import Experience -from pprint import pprint -import asyncio -# import logging -# logging.basicConfig(level=logging.DEBUG) - -async def main(): - req = "2048 game" - exp = Experience(req=req, resp="python code") - - manager = ExperiencePoolManager() - - # pprint(manager.storage.get()) - # manager.create_exp(exp) - result = await manager.query_exp(req) - print(result) - - -if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file diff --git a/examples/exp_pool/simple.py b/examples/exp_pool/simple.py new file mode 100644 index 000000000..bc20fbcdd --- /dev/null +++ b/examples/exp_pool/simple.py @@ -0,0 +1,29 @@ +"""Simple example of experience pool.""" + +import asyncio + +from metagpt.exp_pool import exp_manager +from metagpt.exp_pool.schema import EntryType, Experience +from metagpt.logs import logger + + +async def main(): + req = "Simple task." + + # 1. Find experiences. + exps = await exp_manager.query_exps(req) + if exps: + logger.info(f"Experiences already exist for the request `{req}`: {exps}") + return + + # 2. Create a new experience if none exist + exp_manager.create_exp(Experience(req=req, resp="Simple echo.", entry_type=EntryType.MANUAL)) + logger.info(f"New experience created for the request `{req}`.") + + # 3. Find again + exps = await exp_manager.query_exps(req) + logger.info(f"Updated experiences: {exps}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/metagpt/config2.py b/metagpt/config2.py index 6f5a1add6..6588a6036 100644 --- a/metagpt/config2.py +++ b/metagpt/config2.py @@ -13,6 +13,7 @@ from pydantic import BaseModel, model_validator from metagpt.configs.browser_config import BrowserConfig from metagpt.configs.embedding_config import EmbeddingConfig +from metagpt.configs.exp_pool_config import ExperiencePoolConfig from metagpt.configs.llm_config import LLMConfig, LLMType from metagpt.configs.mermaid_config import MermaidConfig from metagpt.configs.redis_config import RedisConfig @@ -22,7 +23,6 @@ from metagpt.configs.search_config import SearchConfig from metagpt.configs.workspace_config import WorkspaceConfig from metagpt.const import CONFIG_ROOT, METAGPT_ROOT from metagpt.utils.yaml_model import YamlModel -from metagpt.configs.exp_pool_config import ExperiencePoolConfig class CLIParams(BaseModel): @@ -73,7 +73,7 @@ class Config(CLIParams, YamlModel): code_review_k_times: int = 2 # Experience Pool Parameters - experience_pool: Optional[ExperiencePoolConfig] = None + exp_pool: ExperiencePoolConfig = ExperiencePoolConfig() # Will be removed in the future metagpt_tti_url: str = "" diff --git a/metagpt/configs/exp_pool_config.py b/metagpt/configs/exp_pool_config.py index f7312d2de..3f86173c1 100644 --- a/metagpt/configs/exp_pool_config.py +++ b/metagpt/configs/exp_pool_config.py @@ -1,6 +1,8 @@ +from pydantic import Field + from metagpt.utils.yaml_model import YamlModel class ExperiencePoolConfig(YamlModel): - enable_read: bool = False - enable_write: bool = False + enable_read: bool = Field(default=True, description="Enable to read from experience pool.") + enable_write: bool = Field(default=True, description="Enable to write to experience pool.") diff --git a/metagpt/exp_pool/__init__.py b/metagpt/exp_pool/__init__.py index e69de29bb..aeeb94b38 100644 --- a/metagpt/exp_pool/__init__.py +++ b/metagpt/exp_pool/__init__.py @@ -0,0 +1,6 @@ +"""Experience pool init.""" + +from metagpt.exp_pool.manager import exp_manager +from metagpt.exp_pool.decorator import exp_cache + +__all__ = ["exp_manager", "exp_cache"] diff --git a/metagpt/exp_pool/decorator.py b/metagpt/exp_pool/decorator.py index 6629e8377..1d691b8f3 100644 --- a/metagpt/exp_pool/decorator.py +++ b/metagpt/exp_pool/decorator.py @@ -1,4 +1,56 @@ +"""Experience Decorator.""" + +import asyncio +import functools +from typing import Any, Callable, Optional, TypeVar + +from metagpt.exp_pool.manager import exp_manager +from metagpt.exp_pool.schema import Experience +from metagpt.utils.async_helper import NestAsyncio + +ReturnType = TypeVar("ReturnType") -def exp_cache(func): - pass +def exp_cache(_func: Optional[Callable[..., ReturnType]] = None): + """Decorator to check for a perfect experience and returns it if exists. + + Otherwise, it executes the function, save the result as a new experience, and returns the result. + + This can be applied to both synchronous and asynchronous functions. + """ + + def decorator(func: Callable[..., ReturnType]) -> Callable[..., ReturnType]: + @functools.wraps(func) + async def get_or_create(args: Any, kwargs: Any, is_async: bool) -> ReturnType: + """Attempts to retrieve a cached experience or creates one if not found.""" + + req = f"{func.__name__}_{args}_{kwargs}" + exps = await exp_manager.query_exps(req) + if perfect_exp := exp_manager.extract_one_perfect_exp(exps): + return perfect_exp + + if is_async: + result = await func(*args, **kwargs) + else: + result = func(*args, **kwargs) + + exp_manager.create_exp(Experience(req=req, resp=result)) + + return result + + def sync_wrapper(*args: Any, **kwargs: Any) -> ReturnType: + NestAsyncio.apply_once() + return asyncio.get_event_loop().run_until_complete(get_or_create(args, kwargs, is_async=False)) + + async def async_wrapper(*args: Any, **kwargs: Any) -> ReturnType: + return await get_or_create(args, kwargs, is_async=True) + + if asyncio.iscoroutinefunction(func): + return async_wrapper + else: + return sync_wrapper + + if _func is None: + return decorator + else: + return decorator(_func) diff --git a/metagpt/exp_pool/manager.py b/metagpt/exp_pool/manager.py index c32073a9f..4bc566104 100644 --- a/metagpt/exp_pool/manager.py +++ b/metagpt/exp_pool/manager.py @@ -1,32 +1,105 @@ -from pydantic import BaseModel, ConfigDict -from metagpt.exp_pool.schema import Experience -import uuid -import chromadb -from chromadb import Collection, QueryResult +"""Experience Manager.""" + from typing import Optional + +from pydantic import BaseModel, ConfigDict, model_validator + +from metagpt.config2 import Config, config +from metagpt.exp_pool.schema import MAX_SCORE, Experience from metagpt.rag.engines import SimpleEngine -from metagpt.rag.schema import ChromaRetrieverConfig +from metagpt.rag.schema import ChromaRetrieverConfig, LLMRankerConfig -class ExperiencePoolManager(BaseModel): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self._storage = None +class ExperienceManager(BaseModel): + """ExperienceManager manages the lifecycle of experiences, including CRUD and optimization. + + Attributes: + config (Config): Configuration for managing experiences. + storage (SimpleEngine): Engine to handle the storage and retrieval of experiences. + """ + + model_config = ConfigDict(arbitrary_types_allowed=True) + + config: Config = config + storage: SimpleEngine = None + + @model_validator(mode="after") + def initialize(self): + if self.storage is None: + self.storage = SimpleEngine.from_objs( + retriever_configs=[ + ChromaRetrieverConfig(collection_name="experience_pool", persist_path=".chroma_exp_data") + ], + ranker_configs=[LLMRankerConfig()], + ) + return self - @property - def storage(self) -> SimpleEngine: - if self._storage is None: - self._storage = SimpleEngine.from_objs(retriever_configs=[ChromaRetrieverConfig(collection_name="experience_pool", persist_path="./chroma_data")]) - return self._storage - def create_exp(self, exp: Experience): + """Adds an experience to the storage if writing is enabled. + + Args: + exp (Experience): The experience to add. + """ + if not self.config.exp_pool.enable_write: + return + self.storage.add_objs([exp]) - - async def query_exp(self, req: str) -> list[Experience]: + + async def query_exps(self, req: str, tag: str = "") -> list[Experience]: + """Retrieves and filters experiences. + + Args: + req (str): The query string to retrieve experiences. + tag (str): Optional tag to filter the experiences by. + + Returns: + list[Experience]: A list of experiences that match the args. + """ + if not self.config.exp_pool.enable_read: + return [] + nodes = await self.storage.aretrieve(req) - exps = [node.metadata["obj"] for node in nodes] + exps: list[Experience] = [node.metadata["obj"] for node in nodes] + + # TODO: filter by metadata + if tag: + exps = [exp for exp in exps if exp.tag == tag] return exps - + def extract_one_perfect_exp(self, exps: list[Experience]) -> Optional[Experience]: + """Extracts the first 'perfect' experience from a list of experiences. + Args: + exps (list[Experience]): The experiences to evaluate. + + Returns: + Optional[Experience]: The first perfect experience if found, otherwise None. + """ + for exp in exps: + if self.is_perfect_exp(exp): + return exp + + return None + + @staticmethod + def is_perfect_exp(exp: Experience) -> bool: + """Determines if an experience is considered 'perfect'. + + Args: + exp (Experience): The experience to evaluate. + + Returns: + bool: True if the experience is manually entered, otherwise False. + """ + if not exp: + return False + + # TODO: need more metrics + if exp.metric and exp.metric.score == MAX_SCORE: + return True + + return False + + +exp_manager = ExperienceManager() diff --git a/metagpt/exp_pool/schema.py b/metagpt/exp_pool/schema.py index 359268612..b51bc3c17 100644 --- a/metagpt/exp_pool/schema.py +++ b/metagpt/exp_pool/schema.py @@ -1,10 +1,46 @@ -from pydantic import BaseModel, Field +"""Experience schema.""" + +from enum import Enum +from typing import Optional + from llama_index.core.schema import TextNode +from pydantic import BaseModel, Field + +MAX_SCORE = 10 + + +class ExperienceType(str, Enum): + """Experience Type.""" + + SUCCESS = "success" + FAILURE = "failure" + INSIGHT = "insight" + + +class EntryType(Enum): + """Experience Entry Type.""" + + AUTOMATIC = "Automatic" + MANUAL = "Manual" + + +class Metric(BaseModel): + """Experience Metric.""" + + time_cost: float = Field(default=0.000, description="Time cost, the unit is milliseconds.") + money_cost: float = Field(default=0.000, description="Money cost, the unit is US dollars.") + score: int = Field(default=1, description="Score, a value between 1 and 10.") class Experience(BaseModel): + """Experience.""" + req: str = Field(..., description="") - resp: str = Field(..., description="") + resp: str = Field(..., description="The type is string/json/code.") + metric: Optional[Metric] = Field(default=None, description="Metric.") + exp_type: ExperienceType = Field(default=ExperienceType.SUCCESS, description="The type of experience.") + entry_type: EntryType = Field(default=EntryType.AUTOMATIC, description="Type of entry: Manual or Automatic.") + tag: str = Field(default="", description="Tagging experience.") def rag_key(self): return self.req diff --git a/metagpt/rag/retrievers/bm25_retriever.py b/metagpt/rag/retrievers/bm25_retriever.py index 3b085cb73..dc75d87b0 100644 --- a/metagpt/rag/retrievers/bm25_retriever.py +++ b/metagpt/rag/retrievers/bm25_retriever.py @@ -46,4 +46,4 @@ class DynamicBM25Retriever(BM25Retriever): def persist(self, persist_dir: str, **kwargs) -> None: """Support persist.""" if self._index: - self._index.storage_context.persist(persist_dir) \ No newline at end of file + self._index.storage_context.persist(persist_dir) diff --git a/metagpt/utils/file.py b/metagpt/utils/file.py index a8ed482d9..8861f65dc 100644 --- a/metagpt/utils/file.py +++ b/metagpt/utils/file.py @@ -72,7 +72,6 @@ class File: class MemoryFileSystem(_MemoryFileSystem): - @classmethod def _strip_protocol(cls, path): return super()._strip_protocol(str(path)) diff --git a/metagpt/utils/reflection.py b/metagpt/utils/reflection.py index 688831f06..2683e5657 100644 --- a/metagpt/utils/reflection.py +++ b/metagpt/utils/reflection.py @@ -23,5 +23,5 @@ def get_func_full_name(func, *args) -> str: if inspect.ismethod(func) or (inspect.isfunction(func) and "self" in inspect.signature(func).parameters): cls_name = args[0].__class__.__name__ return f"{func.__module__}.{cls_name}.{func.__name__}" - + return f"{func.__module__}.{func.__name__}" From 6d983908314622125033459cce16c401d47bc87d Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Tue, 4 Jun 2024 11:33:34 +0800 Subject: [PATCH 003/119] experiment pool init --- examples/exp_pool/simple.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/examples/exp_pool/simple.py b/examples/exp_pool/simple.py index bc20fbcdd..608578519 100644 --- a/examples/exp_pool/simple.py +++ b/examples/exp_pool/simple.py @@ -9,20 +9,13 @@ from metagpt.logs import logger async def main(): req = "Simple task." + resp = "Simple echo." - # 1. Find experiences. - exps = await exp_manager.query_exps(req) - if exps: - logger.info(f"Experiences already exist for the request `{req}`: {exps}") - return - - # 2. Create a new experience if none exist - exp_manager.create_exp(Experience(req=req, resp="Simple echo.", entry_type=EntryType.MANUAL)) + exp_manager.create_exp(Experience(req=req, resp=resp, entry_type=EntryType.MANUAL)) logger.info(f"New experience created for the request `{req}`.") - # 3. Find again exps = await exp_manager.query_exps(req) - logger.info(f"Updated experiences: {exps}") + logger.info(f"Got experiences: {exps}") if __name__ == "__main__": From 9f817bd59c254ae0b86b1b1ef9d5d57b6f63a44a Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Tue, 4 Jun 2024 11:46:57 +0800 Subject: [PATCH 004/119] experiment pool init --- examples/exp_pool/simple.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/exp_pool/simple.py b/examples/exp_pool/simple.py index 608578519..f270824bf 100644 --- a/examples/exp_pool/simple.py +++ b/examples/exp_pool/simple.py @@ -10,8 +10,9 @@ from metagpt.logs import logger async def main(): req = "Simple task." resp = "Simple echo." + exp = Experience(req=req, resp=resp, entry_type=EntryType.MANUAL) - exp_manager.create_exp(Experience(req=req, resp=resp, entry_type=EntryType.MANUAL)) + exp_manager.create_exp(exp) logger.info(f"New experience created for the request `{req}`.") exps = await exp_manager.query_exps(req) From d10881c0e441e0f37b1645c4ca890c30f2868da3 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Tue, 4 Jun 2024 15:16:58 +0800 Subject: [PATCH 005/119] add trajectory schema --- metagpt/exp_pool/schema.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/metagpt/exp_pool/schema.py b/metagpt/exp_pool/schema.py index b51bc3c17..e6ae4ee1d 100644 --- a/metagpt/exp_pool/schema.py +++ b/metagpt/exp_pool/schema.py @@ -32,6 +32,14 @@ class Metric(BaseModel): score: int = Field(default=1, description="Score, a value between 1 and 10.") +class Trajectory(BaseModel): + """Experience Trajectory.""" + + plan: str = Field(default="", description="The plan.") + action: str = Field(default="", description="Action for the plan.") + observation: str = Field(default="", description="Output of the action.") + + class Experience(BaseModel): """Experience.""" @@ -41,6 +49,7 @@ class Experience(BaseModel): exp_type: ExperienceType = Field(default=ExperienceType.SUCCESS, description="The type of experience.") entry_type: EntryType = Field(default=EntryType.AUTOMATIC, description="Type of entry: Manual or Automatic.") tag: str = Field(default="", description="Tagging experience.") + traj: Optional[Trajectory] = Field(default=None, description="Trajectory.") def rag_key(self): return self.req From 2eedc23a827acc892c4928bf14c7e1b99f081c59 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Tue, 4 Jun 2024 22:08:40 +0800 Subject: [PATCH 006/119] add exp_pool test --- metagpt/exp_pool/schema.py | 5 +- tests/metagpt/exp_pool/test_manager.py | 77 ++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 tests/metagpt/exp_pool/test_manager.py diff --git a/metagpt/exp_pool/schema.py b/metagpt/exp_pool/schema.py index e6ae4ee1d..1afcc1508 100644 --- a/metagpt/exp_pool/schema.py +++ b/metagpt/exp_pool/schema.py @@ -1,7 +1,7 @@ """Experience schema.""" from enum import Enum -from typing import Optional +from typing import Any, Optional from llama_index.core.schema import TextNode from pydantic import BaseModel, Field @@ -38,13 +38,14 @@ class Trajectory(BaseModel): plan: str = Field(default="", description="The plan.") action: str = Field(default="", description="Action for the plan.") observation: str = Field(default="", description="Output of the action.") + reward: int = Field(default=0, description="Measure the action.") class Experience(BaseModel): """Experience.""" req: str = Field(..., description="") - resp: str = Field(..., description="The type is string/json/code.") + resp: Any = Field(..., description="The type is string/json/code.") metric: Optional[Metric] = Field(default=None, description="Metric.") exp_type: ExperienceType = Field(default=ExperienceType.SUCCESS, description="The type of experience.") entry_type: EntryType = Field(default=EntryType.AUTOMATIC, description="Type of entry: Manual or Automatic.") diff --git a/tests/metagpt/exp_pool/test_manager.py b/tests/metagpt/exp_pool/test_manager.py new file mode 100644 index 000000000..a0d7005f5 --- /dev/null +++ b/tests/metagpt/exp_pool/test_manager.py @@ -0,0 +1,77 @@ +import pytest + +from metagpt.config2 import Config +from metagpt.configs.exp_pool_config import ExperiencePoolConfig +from metagpt.configs.llm_config import LLMConfig +from metagpt.exp_pool.manager import ExperienceManager +from metagpt.exp_pool.schema import MAX_SCORE, Experience, Metric +from metagpt.rag.engines import SimpleEngine + + +class TestExperienceManager: + @pytest.fixture + def mock_config(self): + return Config(llm=LLMConfig(), exp_pool=ExperiencePoolConfig(enable_write=True, enable_read=True)) + + @pytest.fixture + def mock_storage(self, mocker): + engine = mocker.MagicMock(spec=SimpleEngine) + engine.add_objs = mocker.MagicMock() + engine.aretrieve = mocker.AsyncMock(return_value=[]) + return engine + + @pytest.fixture + def mock_experience_manager(self, mock_config, mock_storage): + return ExperienceManager(config=mock_config, storage=mock_storage) + + @pytest.fixture + def mock_experience(self): + return Experience(req="req", resp="resp") + + def test_initialize_storage(self, mock_experience_manager, mock_storage): + assert mock_experience_manager.storage is mock_storage + + def test_create_exp(self, mock_experience_manager, mock_experience): + mock_experience_manager.create_exp(mock_experience) + mock_experience_manager.storage.add_objs.assert_called_once_with([mock_experience]) + + def test_create_exp_write_disabled(self, mock_experience_manager, mock_experience, mock_config): + mock_config.exp_pool.enable_write = False + mock_experience_manager.create_exp(mock_experience) + mock_experience_manager.storage.add_objs.assert_not_called() + + @pytest.mark.asyncio + async def test_query_exps(self, mock_experience_manager, mocker): + req = "req" + resp = "resp" + tag = "test" + experiences = [Experience(req=req, resp=resp, tag="test"), Experience(req=req, resp=resp, tag="other")] + mock_experience_manager.storage.aretrieve.return_value = [ + mocker.MagicMock(metadata={"obj": exp}) for exp in experiences + ] + + result = await mock_experience_manager.query_exps(req, tag) + assert len(result) == 1 + assert result[0].tag == "test" + + @pytest.mark.asyncio + async def test_query_exps_no_read_permission(self, mock_experience_manager, mock_config): + mock_config.exp_pool.enable_read = False + result = await mock_experience_manager.query_exps("query") + assert result == [] + + def test_extract_one_perfect_exp(self, mock_experience_manager): + experiences = [ + Experience(req="req", resp="resp", metric=Metric(score=MAX_SCORE)), + Experience(req="req", resp="resp"), + ] + perfect_exp: Experience = mock_experience_manager.extract_one_perfect_exp(experiences) + assert perfect_exp is not None + assert perfect_exp.metric.score == MAX_SCORE + + def test_is_perfect_exp(self): + exp = Experience(req="req", resp="resp", metric=Metric(score=MAX_SCORE)) + assert ExperienceManager.is_perfect_exp(exp) == True + + exp = Experience(req="req", resp="resp") + assert ExperienceManager.is_perfect_exp(exp) == False From 1d8d85e9a50f02ddf4b394ff1589c77322ac0c19 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Wed, 5 Jun 2024 10:32:48 +0800 Subject: [PATCH 007/119] update comment --- metagpt/exp_pool/decorator.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/metagpt/exp_pool/decorator.py b/metagpt/exp_pool/decorator.py index 1d691b8f3..e073ee494 100644 --- a/metagpt/exp_pool/decorator.py +++ b/metagpt/exp_pool/decorator.py @@ -22,18 +22,21 @@ def exp_cache(_func: Optional[Callable[..., ReturnType]] = None): def decorator(func: Callable[..., ReturnType]) -> Callable[..., ReturnType]: @functools.wraps(func) async def get_or_create(args: Any, kwargs: Any, is_async: bool) -> ReturnType: - """Attempts to retrieve a cached experience or creates one if not found.""" + """Attempts to retrieve a perfect experience or creates an experience if not found.""" + # 1. Get exps. req = f"{func.__name__}_{args}_{kwargs}" exps = await exp_manager.query_exps(req) if perfect_exp := exp_manager.extract_one_perfect_exp(exps): return perfect_exp + # 2. Exec func. TODO: pass exps to func if is_async: result = await func(*args, **kwargs) else: result = func(*args, **kwargs) + # 3. Create an exp. exp_manager.create_exp(Experience(req=req, resp=result)) return result From c78cddd1021c5073e7f4e17d22149053fc8e3276 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Wed, 5 Jun 2024 22:15:09 +0800 Subject: [PATCH 008/119] add exp_pool tests --- examples/exp_pool/decorator.py | 5 +- metagpt/exp_pool/decorator.py | 144 ++++++++++++++++------ metagpt/exp_pool/manager.py | 10 +- metagpt/exp_pool/schema.py | 16 ++- metagpt/exp_pool/scorers/__init__.py | 6 + metagpt/exp_pool/scorers/base.py | 27 +++++ metagpt/exp_pool/scorers/simple.py | 73 ++++++++++++ tests/metagpt/exp_pool/test_decorator.py | 145 +++++++++++++++++++++++ tests/metagpt/exp_pool/test_manager.py | 8 +- 9 files changed, 391 insertions(+), 43 deletions(-) create mode 100644 metagpt/exp_pool/scorers/__init__.py create mode 100644 metagpt/exp_pool/scorers/base.py create mode 100644 metagpt/exp_pool/scorers/simple.py create mode 100644 tests/metagpt/exp_pool/test_decorator.py diff --git a/examples/exp_pool/decorator.py b/examples/exp_pool/decorator.py index 2f6397f80..3f6093e01 100644 --- a/examples/exp_pool/decorator.py +++ b/examples/exp_pool/decorator.py @@ -7,8 +7,9 @@ from metagpt.exp_pool import exp_cache, exp_manager from metagpt.logs import logger -@exp_cache -async def produce(req): +@exp_cache(pass_exps_to_func=True) +async def produce(req, exps=None): + logger.info(f"Previous experiences: {exps}") return f"{req} {uuid.uuid4().hex}" diff --git a/metagpt/exp_pool/decorator.py b/metagpt/exp_pool/decorator.py index e073ee494..9eb4d9e61 100644 --- a/metagpt/exp_pool/decorator.py +++ b/metagpt/exp_pool/decorator.py @@ -4,56 +4,134 @@ import asyncio import functools from typing import Any, Callable, Optional, TypeVar -from metagpt.exp_pool.manager import exp_manager -from metagpt.exp_pool.schema import Experience +from pydantic import BaseModel, ConfigDict + +from metagpt.exp_pool.manager import ExperienceManager, exp_manager +from metagpt.exp_pool.schema import Experience, Metric, QueryType, Score +from metagpt.exp_pool.scorers import ExperienceScorer, SimpleScorer from metagpt.utils.async_helper import NestAsyncio ReturnType = TypeVar("ReturnType") -def exp_cache(_func: Optional[Callable[..., ReturnType]] = None): - """Decorator to check for a perfect experience and returns it if exists. - - Otherwise, it executes the function, save the result as a new experience, and returns the result. +def exp_cache( + _func: Optional[Callable[..., ReturnType]] = None, + query_type: QueryType = QueryType.SEMANTIC, + scorer: Optional[ExperienceScorer] = None, + manager: Optional[ExperienceManager] = None, + pass_exps_to_func: bool = False, +): + """Decorator to get a perfect experience, otherwise, it executes the function, and create a new experience. This can be applied to both synchronous and asynchronous functions. + + Args: + _func: Just to make the decorator more flexible, for example, it can be used directly with @exp_cache by default, without the need for @exp_cache(). + query_type: The type of query to be used when fetching experiences. + scorer: Evaluate experience. Default SimpleScorer. + manager: How to fetch, evaluate and save experience, etc. Default exp_manager. + pass_exps_to_func: To control whether imperfect experiences are passed to the function, if True, the func must have a parameter named 'exps'. """ def decorator(func: Callable[..., ReturnType]) -> Callable[..., ReturnType]: @functools.wraps(func) - async def get_or_create(args: Any, kwargs: Any, is_async: bool) -> ReturnType: - """Attempts to retrieve a perfect experience or creates an experience if not found.""" + async def get_or_create(args: Any, kwargs: Any) -> ReturnType: + handler = ExpCacheHandler( + func=func, + args=args, + kwargs=kwargs, + exp_manager=manager or exp_manager, + exp_scorer=scorer or SimpleScorer(), + pass_exps=pass_exps_to_func, + ) - # 1. Get exps. - req = f"{func.__name__}_{args}_{kwargs}" - exps = await exp_manager.query_exps(req) - if perfect_exp := exp_manager.extract_one_perfect_exp(exps): - return perfect_exp + await handler.fetch_experiences(query_type) + if exp := handler.get_one_perfect_experience(): + return exp - # 2. Exec func. TODO: pass exps to func - if is_async: - result = await func(*args, **kwargs) - else: - result = func(*args, **kwargs) + await handler.execute_function() + await handler.evaluate_experience() + handler.save_experience() - # 3. Create an exp. - exp_manager.create_exp(Experience(req=req, resp=result)) + return handler._result - return result + return ExpCacheHandler.choose_wrapper(func, get_or_create) - def sync_wrapper(*args: Any, **kwargs: Any) -> ReturnType: + return decorator(_func) if _func else decorator + + +class ExpCacheHandler(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + + func: Callable + args: Any + kwargs: Any + exp_manager: ExperienceManager + exp_scorer: ExperienceScorer + pass_exps: bool + + _exps: list[Experience] = None + _result: Any = None + _score: Score = None + + async def fetch_experiences(self, query_type: QueryType): + """Fetch a potentially perfect existing experience.""" + + req = self.generate_req_identifier() + self._exps = await self.exp_manager.query_exps(req, query_type=query_type) + + def get_one_perfect_experience(self) -> Optional[Experience]: + return self.exp_manager.extract_one_perfect_exp(self._exps) + + async def execute_function(self): + """Execute the function, and save the result.""" + self._result = await self._execute_function() + + async def evaluate_experience(self): + """Evaluate the experience, and save the score.""" + + self._score = await self.exp_scorer.evaluate(self.func, self._result, self.args, self.kwargs) + + def save_experience(self): + """Save the new experience.""" + + req = self.generate_req_identifier() + exp = Experience(req=req, resp=self._result, metric=Metric(score=self._score)) + + self.exp_manager.create_exp(exp) + + def generate_req_identifier(self): + """Generate a unique request identifier based on the function and its arguments.""" + + return f"{self.func.__name__}_{self.args}_{self.kwargs}" + + @staticmethod + def choose_wrapper(func, wrapped_func): + """Choose how to run wrapped_func based on whether the function is asynchronous.""" + + async def async_wrapper(*args, **kwargs): + return await wrapped_func(args, kwargs) + + def sync_wrapper(*args, **kwargs): NestAsyncio.apply_once() - return asyncio.get_event_loop().run_until_complete(get_or_create(args, kwargs, is_async=False)) + return asyncio.get_event_loop().run_until_complete(wrapped_func(args, kwargs)) - async def async_wrapper(*args: Any, **kwargs: Any) -> ReturnType: - return await get_or_create(args, kwargs, is_async=True) + return async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper - if asyncio.iscoroutinefunction(func): - return async_wrapper - else: - return sync_wrapper + async def _execute_function(self): + if self.pass_exps: + return await self._execute_function_with_exps() - if _func is None: - return decorator - else: - return decorator(_func) + return await self._execute_function_without_exps() + + async def _execute_function_without_exps(self): + if asyncio.iscoroutinefunction(self.func): + return await self.func(*self.args, **self.kwargs) + + return self.func(*self.args, **self.kwargs) + + async def _execute_function_with_exps(self): + if asyncio.iscoroutinefunction(self.func): + return await self.func(*self.args, **self.kwargs, exps=self._exps) + + return self.func(*self.args, **self.kwargs, exps=self._exps) diff --git a/metagpt/exp_pool/manager.py b/metagpt/exp_pool/manager.py index 4bc566104..58499104d 100644 --- a/metagpt/exp_pool/manager.py +++ b/metagpt/exp_pool/manager.py @@ -5,7 +5,7 @@ from typing import Optional from pydantic import BaseModel, ConfigDict, model_validator from metagpt.config2 import Config, config -from metagpt.exp_pool.schema import MAX_SCORE, Experience +from metagpt.exp_pool.schema import MAX_SCORE, Experience, QueryType from metagpt.rag.engines import SimpleEngine from metagpt.rag.schema import ChromaRetrieverConfig, LLMRankerConfig @@ -45,12 +45,13 @@ class ExperienceManager(BaseModel): self.storage.add_objs([exp]) - async def query_exps(self, req: str, tag: str = "") -> list[Experience]: + async def query_exps(self, req: str, tag: str = "", query_type: QueryType = QueryType.SEMANTIC) -> list[Experience]: """Retrieves and filters experiences. Args: req (str): The query string to retrieve experiences. tag (str): Optional tag to filter the experiences by. + query_type (QueryType): Default semantic to vector matching. exact to same matching. Returns: list[Experience]: A list of experiences that match the args. @@ -65,6 +66,9 @@ class ExperienceManager(BaseModel): if tag: exps = [exp for exp in exps if exp.tag == tag] + if query_type == QueryType.EXACT: + exps = [exp for exp in exps if exp.req == req] + return exps def extract_one_perfect_exp(self, exps: list[Experience]) -> Optional[Experience]: @@ -96,7 +100,7 @@ class ExperienceManager(BaseModel): return False # TODO: need more metrics - if exp.metric and exp.metric.score == MAX_SCORE: + if exp.metric and exp.metric.score.val == MAX_SCORE: return True return False diff --git a/metagpt/exp_pool/schema.py b/metagpt/exp_pool/schema.py index 1afcc1508..9fc665cca 100644 --- a/metagpt/exp_pool/schema.py +++ b/metagpt/exp_pool/schema.py @@ -9,6 +9,13 @@ from pydantic import BaseModel, Field MAX_SCORE = 10 +class QueryType(str, Enum): + """Type of query experiences.""" + + EXACT = "exact" + SEMANTIC = "semantic" + + class ExperienceType(str, Enum): """Experience Type.""" @@ -24,12 +31,19 @@ class EntryType(Enum): MANUAL = "Manual" +class Score(BaseModel): + """Score in Metric.""" + + val: int = Field(default=1, description="Value of the score, Between 1 and 10, higher is better.") + reason: str = Field(default="", description="Reason for the value.") + + class Metric(BaseModel): """Experience Metric.""" time_cost: float = Field(default=0.000, description="Time cost, the unit is milliseconds.") money_cost: float = Field(default=0.000, description="Money cost, the unit is US dollars.") - score: int = Field(default=1, description="Score, a value between 1 and 10.") + score: Score = Field(default=None, description="Score, with value and reason.") class Trajectory(BaseModel): diff --git a/metagpt/exp_pool/scorers/__init__.py b/metagpt/exp_pool/scorers/__init__.py new file mode 100644 index 000000000..85bea88ff --- /dev/null +++ b/metagpt/exp_pool/scorers/__init__.py @@ -0,0 +1,6 @@ +"""Experience scorers init.""" + +from metagpt.exp_pool.scorers.base import ExperienceScorer +from metagpt.exp_pool.scorers.simple import SimpleScorer + +__all__ = ["ExperienceScorer", "SimpleScorer"] diff --git a/metagpt/exp_pool/scorers/base.py b/metagpt/exp_pool/scorers/base.py new file mode 100644 index 000000000..a9d30cffe --- /dev/null +++ b/metagpt/exp_pool/scorers/base.py @@ -0,0 +1,27 @@ +"""Experience Scorers.""" + +from abc import abstractmethod +from typing import Any, Callable + +from pydantic import BaseModel, ConfigDict + +from metagpt.exp_pool.schema import Score + + +class ExperienceScorer(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + + @abstractmethod + async def evaluate(self, func: Callable, result: Any, args: tuple = None, kwargs: dict = None) -> Score: + """Evaluate the quality of the result produced by the function and parameters. + + Args: + func (Callable): The function whose result is to be evaluated. + result (Any): The result produced by the function. + args (Tuple[Any, ...]): The tuple of arguments that were passed to the function. + kwargs (Dict[str, Any]): The dictionary of keyword arguments that were passed to the function. + + Example: + result = await sample(5, name="foo") + score = await scorer.evaluate(sample, result, args=(5), kwargs={"name": "foo"}) + """ diff --git a/metagpt/exp_pool/scorers/simple.py b/metagpt/exp_pool/scorers/simple.py new file mode 100644 index 000000000..d0301cbc2 --- /dev/null +++ b/metagpt/exp_pool/scorers/simple.py @@ -0,0 +1,73 @@ +"""Evalate by llm.""" +import inspect +import json +from typing import Any, Callable + +from pydantic import Field + +from metagpt.exp_pool.schema import Score +from metagpt.exp_pool.scorers.base import ExperienceScorer +from metagpt.llm import LLM +from metagpt.provider.base_llm import BaseLLM +from metagpt.utils.common import parse_json_code_block + +SIMPLE_SCORER_TEMPLATE = """ +Role: You're an expert score evaluator. You specialize in assessing the output of the given function, based on its intended requirement and produced result. + +## Context +### Function Name +{func_name} + +### Function Document +{func_doc} + +### Function Signature +{func_signature} + +### Function Parameters +args: {func_args} +kwargs: {func_kwargs} + +### Produced Result By Function and Parameters +{func_result} + +## Format Example +```json +{{ + "val": "the value of the score, int from 1 to 10, higher is better.", + "reason": "an explanation supporting the score." +}} +``` + +## Instructions +- Understand the function and requirements given by the user. +- Analyze the results produced by the function. +- Grade the results based on level of alignment with the requirements. +- Provide a score on a scale defined by user or a default scale (1 to 10). + +## Constraint +Format: Just print the result in json format like **Format Example**. + +## Action +Follow instructions, generate output and make sure it follows the **Constraint**. +""" + + +class SimpleScorer(ExperienceScorer): + llm: BaseLLM = Field(default_factory=LLM) + + async def evaluate(self, func: Callable, result: Any, args: tuple = None, kwargs: dict = None) -> Score: + """Evaluate the quality of content.""" + + prompt = SIMPLE_SCORER_TEMPLATE.format( + func_name=func.__name__, + func_doc=func.__doc__, + func_signature=inspect.signature(func), + func_args=args, + func_kwargs=kwargs, + func_result=result, + ) + resp = await self.llm.aask(prompt) + resp_json = json.loads(parse_json_code_block(resp)[0]) + + return Score(**resp_json) diff --git a/tests/metagpt/exp_pool/test_decorator.py b/tests/metagpt/exp_pool/test_decorator.py new file mode 100644 index 000000000..508229d18 --- /dev/null +++ b/tests/metagpt/exp_pool/test_decorator.py @@ -0,0 +1,145 @@ +import asyncio + +import pytest + +from metagpt.exp_pool.decorator import ExpCacheHandler +from metagpt.exp_pool.manager import ExperienceManager +from metagpt.exp_pool.schema import Experience, QueryType, Score +from metagpt.exp_pool.scorers import SimpleScorer +from metagpt.rag.engines import SimpleEngine + + +class TestExpCache: + @pytest.fixture + def mock_func(self, mocker): + return mocker.AsyncMock() + + @pytest.fixture + def mock_exp_manager(self, mocker): + manager = mocker.MagicMock(spec=ExperienceManager) + manager.storage = mocker.MagicMock(spec=SimpleEngine) + manager.query_exps = mocker.AsyncMock() + manager.create_exp = mocker.MagicMock() + manager.extract_one_perfect_exp = mocker.MagicMock() + return manager + + @pytest.fixture + def mock_scorer(self, mocker): + scorer = mocker.MagicMock(spec=SimpleScorer) + scorer.evaluate = mocker.AsyncMock() + return scorer + + @pytest.fixture + def exp_cache_handler(self, mock_func, mock_exp_manager, mock_scorer): + return ExpCacheHandler( + func=mock_func, args=(), kwargs={}, exp_manager=mock_exp_manager, exp_scorer=mock_scorer, pass_exps=False + ) + + @pytest.mark.asyncio + async def test_fetch_experiences(self, exp_cache_handler, mock_exp_manager): + await exp_cache_handler.fetch_experiences(QueryType.SEMANTIC) + mock_exp_manager.query_exps.assert_called_once() + + @pytest.mark.asyncio + async def test_perfect_experience_found(self, exp_cache_handler, mock_exp_manager, mock_func): + # Setup: Assume perfect experience is found + perfect_exp = Experience(req="req", resp="resp") + mock_exp_manager.extract_one_perfect_exp.return_value = perfect_exp + + # Execute + exp_cache_handler._exps = [perfect_exp] # Simulate fetched experiences + result = exp_cache_handler.get_one_perfect_experience() + + # Assert + assert result.resp == "resp" + mock_func.assert_not_called() # Function should not be called + + @pytest.mark.asyncio + async def test_execute_function_when_no_perfect_exp(self, exp_cache_handler, mock_exp_manager, mock_func): + # Setup: No perfect experience + mock_exp_manager.extract_one_perfect_exp.return_value = None + mock_func.return_value = "Computed result" + + # Execute + await exp_cache_handler.execute_function() + + # Assert + assert exp_cache_handler._result == "Computed result" + mock_func.assert_called_once() + + @pytest.mark.asyncio + async def test_evaluate_and_save_experience(self, exp_cache_handler, mock_scorer, mock_exp_manager): + # Setup + mock_scorer.evaluate.return_value = Score(value=100) + exp_cache_handler._result = "Computed result" + + # Execute + await exp_cache_handler.evaluate_experience() + exp_cache_handler.save_experience() + + # Assert + mock_scorer.evaluate.assert_called_once() + mock_exp_manager.create_exp.assert_called_once() + + @pytest.mark.asyncio + async def test_async_function_execution_with_exps(self, exp_cache_handler, mock_exp_manager, mock_func): + # Setup + exp_cache_handler.pass_exps = True + mock_func.return_value = "Async result with exps" + mock_exp_manager.extract_one_perfect_exp.return_value = None + exp_cache_handler._exps = [Experience(req="req", resp="resp")] + + # Execute + await exp_cache_handler.execute_function() + + # Assert + mock_func.assert_called_once_with(exps=exp_cache_handler._exps) + assert exp_cache_handler._result == "Async result with exps" + + def test_sync_function_execution_with_exps(self, mocker, exp_cache_handler, mock_exp_manager, mock_func): + # Setup + exp_cache_handler.func = mocker.Mock(return_value="Sync result with exps") + exp_cache_handler.pass_exps = True + mock_exp_manager.extract_one_perfect_exp.return_value = None + exp_cache_handler._exps = [Experience(req="req", resp="resp")] + + # Execute + asyncio.get_event_loop().run_until_complete(exp_cache_handler.execute_function()) + + # Assert + exp_cache_handler.func.assert_called_once_with(exps=exp_cache_handler._exps) + assert exp_cache_handler._result == "Sync result with exps" + + def test_wrapper_selection_async(self, mocker, exp_cache_handler, mock_func): + # Setup + mock_func = mocker.AsyncMock() + + # Execute + wrapper = ExpCacheHandler.choose_wrapper(mock_func, exp_cache_handler.execute_function) + + # Assert + assert asyncio.iscoroutinefunction(wrapper), "Wrapper should be asynchronous" + + def test_wrapper_selection_sync(self, exp_cache_handler, mocker): + # Setup + sync_func = mocker.Mock() + + # Execute + wrapper = ExpCacheHandler.choose_wrapper(sync_func, exp_cache_handler.execute_function) + + # Assert + assert not asyncio.iscoroutinefunction(wrapper), "Wrapper should be synchronous" + + @pytest.mark.asyncio + async def test_generate_req_identifier(self, exp_cache_handler): + # Setup + exp_cache_handler.func = lambda x: x + exp_cache_handler.args = (42,) + exp_cache_handler.kwargs = {"y": 3.14} + + # Execute + req_id = exp_cache_handler.generate_req_identifier() + + # Assert + expected_id = "_(42,)_{'y': 3.14}" + assert req_id == expected_id, "Request identifier should match the expected format" diff --git a/tests/metagpt/exp_pool/test_manager.py b/tests/metagpt/exp_pool/test_manager.py index a0d7005f5..3e8f47417 100644 --- a/tests/metagpt/exp_pool/test_manager.py +++ b/tests/metagpt/exp_pool/test_manager.py @@ -4,7 +4,7 @@ from metagpt.config2 import Config from metagpt.configs.exp_pool_config import ExperiencePoolConfig from metagpt.configs.llm_config import LLMConfig from metagpt.exp_pool.manager import ExperienceManager -from metagpt.exp_pool.schema import MAX_SCORE, Experience, Metric +from metagpt.exp_pool.schema import MAX_SCORE, Experience, Metric, Score from metagpt.rag.engines import SimpleEngine @@ -62,15 +62,15 @@ class TestExperienceManager: def test_extract_one_perfect_exp(self, mock_experience_manager): experiences = [ - Experience(req="req", resp="resp", metric=Metric(score=MAX_SCORE)), + Experience(req="req", resp="resp", metric=Metric(score=Score(val=MAX_SCORE))), Experience(req="req", resp="resp"), ] perfect_exp: Experience = mock_experience_manager.extract_one_perfect_exp(experiences) assert perfect_exp is not None - assert perfect_exp.metric.score == MAX_SCORE + assert perfect_exp.metric.score.val == MAX_SCORE def test_is_perfect_exp(self): - exp = Experience(req="req", resp="resp", metric=Metric(score=MAX_SCORE)) + exp = Experience(req="req", resp="resp", metric=Metric(score=Score(val=MAX_SCORE))) assert ExperienceManager.is_perfect_exp(exp) == True exp = Experience(req="req", resp="resp") From d148a3217bbe6eb2aa80bdd0132801da98fd1a14 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Wed, 5 Jun 2024 23:26:09 +0800 Subject: [PATCH 009/119] add handle_exception to ensure robustness --- metagpt/exp_pool/decorator.py | 14 ++++++++++++-- metagpt/exp_pool/manager.py | 3 +++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/metagpt/exp_pool/decorator.py b/metagpt/exp_pool/decorator.py index 9eb4d9e61..9cf924779 100644 --- a/metagpt/exp_pool/decorator.py +++ b/metagpt/exp_pool/decorator.py @@ -10,6 +10,7 @@ from metagpt.exp_pool.manager import ExperienceManager, exp_manager from metagpt.exp_pool.schema import Experience, Metric, QueryType, Score from metagpt.exp_pool.scorers import ExperienceScorer, SimpleScorer from metagpt.utils.async_helper import NestAsyncio +from metagpt.utils.exceptions import handle_exception ReturnType = TypeVar("ReturnType") @@ -50,8 +51,7 @@ def exp_cache( return exp await handler.execute_function() - await handler.evaluate_experience() - handler.save_experience() + await handler.process_experience() return handler._result @@ -87,6 +87,16 @@ class ExpCacheHandler(BaseModel): """Execute the function, and save the result.""" self._result = await self._execute_function() + @handle_exception + async def process_experience(self): + """Process experience. + + Evaluates and saves experience. + Use `handle_exception` to ensure robustness, do not stop subsequent operations. + """ + await self.evaluate_experience() + self.save_experience() + async def evaluate_experience(self): """Evaluate the experience, and save the score.""" diff --git a/metagpt/exp_pool/manager.py b/metagpt/exp_pool/manager.py index 58499104d..546086b37 100644 --- a/metagpt/exp_pool/manager.py +++ b/metagpt/exp_pool/manager.py @@ -8,6 +8,7 @@ from metagpt.config2 import Config, config from metagpt.exp_pool.schema import MAX_SCORE, Experience, QueryType from metagpt.rag.engines import SimpleEngine from metagpt.rag.schema import ChromaRetrieverConfig, LLMRankerConfig +from metagpt.utils.exceptions import handle_exception class ExperienceManager(BaseModel): @@ -34,6 +35,7 @@ class ExperienceManager(BaseModel): ) return self + @handle_exception def create_exp(self, exp: Experience): """Adds an experience to the storage if writing is enabled. @@ -45,6 +47,7 @@ class ExperienceManager(BaseModel): self.storage.add_objs([exp]) + @handle_exception(default_return=[]) async def query_exps(self, req: str, tag: str = "", query_type: QueryType = QueryType.SEMANTIC) -> list[Experience]: """Retrieves and filters experiences. From 1679757d9f3cc086ab251d0c6be4e542f1f14830 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Thu, 6 Jun 2024 20:18:40 +0800 Subject: [PATCH 010/119] update exp_pool example --- examples/exp_pool/simple.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/exp_pool/simple.py b/examples/exp_pool/simple.py index f270824bf..3216e78b8 100644 --- a/examples/exp_pool/simple.py +++ b/examples/exp_pool/simple.py @@ -9,8 +9,7 @@ from metagpt.logs import logger async def main(): req = "Simple task." - resp = "Simple echo." - exp = Experience(req=req, resp=resp, entry_type=EntryType.MANUAL) + exp = Experience(req=req, resp="echo", entry_type=EntryType.MANUAL) exp_manager.create_exp(exp) logger.info(f"New experience created for the request `{req}`.") From 16fd197e068cd10947340382d246ab505d5f0860 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Fri, 7 Jun 2024 10:30:37 +0800 Subject: [PATCH 011/119] update comment --- metagpt/exp_pool/scorers/simple.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/metagpt/exp_pool/scorers/simple.py b/metagpt/exp_pool/scorers/simple.py index d0301cbc2..5779f7fb1 100644 --- a/metagpt/exp_pool/scorers/simple.py +++ b/metagpt/exp_pool/scorers/simple.py @@ -1,4 +1,5 @@ -"""Evalate by llm.""" +"""Simple Scorer.""" + import inspect import json from typing import Any, Callable @@ -57,8 +58,17 @@ class SimpleScorer(ExperienceScorer): llm: BaseLLM = Field(default_factory=LLM) async def evaluate(self, func: Callable, result: Any, args: tuple = None, kwargs: dict = None) -> Score: - """Evaluate the quality of content.""" + """Evaluates the quality of content by LLM. + Args: + func: The function to evaluate. + result: The result produced by the function. + args: The positional arguments used when calling the function, if any. + kwargs: The keyword arguments used when calling the function, if any. + + Returns: + A Score object containing the evaluation results. + """ prompt = SIMPLE_SCORER_TEMPLATE.format( func_name=func.__name__, func_doc=func.__doc__, From 547bbfcffc2086ea3958fbf829aab55ec9e3e17d Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Fri, 7 Jun 2024 14:35:47 +0800 Subject: [PATCH 012/119] update comment --- metagpt/exp_pool/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/exp_pool/manager.py b/metagpt/exp_pool/manager.py index 546086b37..35ee5fdac 100644 --- a/metagpt/exp_pool/manager.py +++ b/metagpt/exp_pool/manager.py @@ -14,7 +14,7 @@ from metagpt.utils.exceptions import handle_exception class ExperienceManager(BaseModel): """ExperienceManager manages the lifecycle of experiences, including CRUD and optimization. - Attributes: + Args: config (Config): Configuration for managing experiences. storage (SimpleEngine): Engine to handle the storage and retrieval of experiences. """ From 7ac8397cc97ce7ad361f9420715ae55b5c1b5d88 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Fri, 7 Jun 2024 18:15:23 +0800 Subject: [PATCH 013/119] add scorer example --- examples/exp_pool/{simple.py => manager.py} | 0 examples/exp_pool/scorer.py | 25 +++++++++++++++++++++ metagpt/exp_pool/scorers/simple.py | 4 ++-- 3 files changed, 27 insertions(+), 2 deletions(-) rename examples/exp_pool/{simple.py => manager.py} (100%) create mode 100644 examples/exp_pool/scorer.py diff --git a/examples/exp_pool/simple.py b/examples/exp_pool/manager.py similarity index 100% rename from examples/exp_pool/simple.py rename to examples/exp_pool/manager.py diff --git a/examples/exp_pool/scorer.py b/examples/exp_pool/scorer.py new file mode 100644 index 000000000..1efe07bdf --- /dev/null +++ b/examples/exp_pool/scorer.py @@ -0,0 +1,25 @@ +import asyncio + +from metagpt.exp_pool.scorers import SimpleScorer +from metagpt.logs import logger + + +def echo(req: str): + """Echo from req.""" + + return req + + +async def simple(): + scorer = SimpleScorer() + + score = await scorer.evaluate(echo, "data", ("data",)) + logger.info(f"The score is: {score}") + + +async def main(): + await simple() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/metagpt/exp_pool/scorers/simple.py b/metagpt/exp_pool/scorers/simple.py index 5779f7fb1..84995b60f 100644 --- a/metagpt/exp_pool/scorers/simple.py +++ b/metagpt/exp_pool/scorers/simple.py @@ -10,7 +10,7 @@ from metagpt.exp_pool.schema import Score from metagpt.exp_pool.scorers.base import ExperienceScorer from metagpt.llm import LLM from metagpt.provider.base_llm import BaseLLM -from metagpt.utils.common import parse_json_code_block +from metagpt.utils.common import CodeParser SIMPLE_SCORER_TEMPLATE = """ Role: You're an expert score evaluator. You specialize in assessing the output of the given function, based on its intended requirement and produced result. @@ -78,6 +78,6 @@ class SimpleScorer(ExperienceScorer): func_result=result, ) resp = await self.llm.aask(prompt) - resp_json = json.loads(parse_json_code_block(resp)[0]) + resp_json = json.loads(CodeParser.parse_code(resp, lang="json")) return Score(**resp_json) From 797a8c5326c13feebaa1d0676b7eebf571eb980e Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Tue, 11 Jun 2024 15:40:01 +0800 Subject: [PATCH 014/119] change req in exp --- metagpt/exp_pool/decorator.py | 29 +++++++++++++++++++------- metagpt/exp_pool/manager.py | 3 +-- metagpt/utils/reflection.py | 25 +++++++++++++++++----- tests/metagpt/utils/test_reflection.py | 29 ++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 15 deletions(-) create mode 100644 tests/metagpt/utils/test_reflection.py diff --git a/metagpt/exp_pool/decorator.py b/metagpt/exp_pool/decorator.py index 9cf924779..e559797a3 100644 --- a/metagpt/exp_pool/decorator.py +++ b/metagpt/exp_pool/decorator.py @@ -11,6 +11,7 @@ from metagpt.exp_pool.schema import Experience, Metric, QueryType, Score from metagpt.exp_pool.scorers import ExperienceScorer, SimpleScorer from metagpt.utils.async_helper import NestAsyncio from metagpt.utils.exceptions import handle_exception +from metagpt.utils.reflection import get_class_name ReturnType = TypeVar("ReturnType") @@ -43,7 +44,7 @@ def exp_cache( kwargs=kwargs, exp_manager=manager or exp_manager, exp_scorer=scorer or SimpleScorer(), - pass_exps=pass_exps_to_func, + pass_exps_to_func=pass_exps_to_func, ) await handler.fetch_experiences(query_type) @@ -68,16 +69,17 @@ class ExpCacheHandler(BaseModel): kwargs: Any exp_manager: ExperienceManager exp_scorer: ExperienceScorer - pass_exps: bool + pass_exps_to_func: bool = False _exps: list[Experience] = None _result: Any = None _score: Score = None + _req: str = None async def fetch_experiences(self, query_type: QueryType): """Fetch a potentially perfect existing experience.""" - req = self.generate_req_identifier() + req = self._get_req_identifier() self._exps = await self.exp_manager.query_exps(req, query_type=query_type) def get_one_perfect_experience(self) -> Optional[Experience]: @@ -105,15 +107,26 @@ class ExpCacheHandler(BaseModel): def save_experience(self): """Save the new experience.""" - req = self.generate_req_identifier() + req = self._get_req_identifier() exp = Experience(req=req, resp=self._result, metric=Metric(score=self._score)) self.exp_manager.create_exp(exp) - def generate_req_identifier(self): - """Generate a unique request identifier based on the function and its arguments.""" + def _get_req_identifier(self): + """Generate a unique request identifier based on the function and its arguments. - return f"{self.func.__name__}_{self.args}_{self.kwargs}" + Result Example: + - "write_prd-('2048',)-{}" + - "WritePRD.run-('2048',)-{}" + """ + if not self._req: + cls_name = get_class_name(self.func, *self.args) + func_name = f"{cls_name}.{self.func.__name__}" if cls_name else self.func.__name__ + args = self.args[1:] if cls_name and len(self.args) >= 1 else self.args + + self._req = f"{func_name}-{args}-{self.kwargs}" + + return self._req @staticmethod def choose_wrapper(func, wrapped_func): @@ -129,7 +142,7 @@ class ExpCacheHandler(BaseModel): return async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper async def _execute_function(self): - if self.pass_exps: + if self.pass_exps_to_func: return await self._execute_function_with_exps() return await self._execute_function_without_exps() diff --git a/metagpt/exp_pool/manager.py b/metagpt/exp_pool/manager.py index 35ee5fdac..7382fe8f1 100644 --- a/metagpt/exp_pool/manager.py +++ b/metagpt/exp_pool/manager.py @@ -7,7 +7,7 @@ from pydantic import BaseModel, ConfigDict, model_validator from metagpt.config2 import Config, config from metagpt.exp_pool.schema import MAX_SCORE, Experience, QueryType from metagpt.rag.engines import SimpleEngine -from metagpt.rag.schema import ChromaRetrieverConfig, LLMRankerConfig +from metagpt.rag.schema import ChromaRetrieverConfig from metagpt.utils.exceptions import handle_exception @@ -31,7 +31,6 @@ class ExperienceManager(BaseModel): retriever_configs=[ ChromaRetrieverConfig(collection_name="experience_pool", persist_path=".chroma_exp_data") ], - ranker_configs=[LLMRankerConfig()], ) return self diff --git a/metagpt/utils/reflection.py b/metagpt/utils/reflection.py index 2683e5657..9b10a4b3e 100644 --- a/metagpt/utils/reflection.py +++ b/metagpt/utils/reflection.py @@ -19,9 +19,24 @@ def check_methods(C, *methods): return True -def get_func_full_name(func, *args) -> str: - if inspect.ismethod(func) or (inspect.isfunction(func) and "self" in inspect.signature(func).parameters): - cls_name = args[0].__class__.__name__ - return f"{func.__module__}.{cls_name}.{func.__name__}" +def get_class_name(func, *args) -> str: + """Returns the class name of the object that a method belongs to. - return f"{func.__module__}.{func.__name__}" + - If `func` is a bound method, extracts the class name directly from the method. + - If `func` is an unbound method and `args` are provided, assumes the first argument is `self` and extracts the class name. + - Returns an empty string if neither condition is met. + """ + if inspect.ismethod(func): + return func.__self__.__class__.__name__ + + if inspect.isfunction(func) and "self" in inspect.signature(func).parameters and args: + return args[0].__class__.__name__ + + return "" + + +def get_func_or_method_name(func, *args) -> str: + """Function name, or method name with class name.""" + cls_name = get_class_name(func, *args) + + return f"{cls_name}.{func.__name__}" if cls_name else f"{func.__name__}" diff --git a/tests/metagpt/utils/test_reflection.py b/tests/metagpt/utils/test_reflection.py new file mode 100644 index 000000000..e78e1b400 --- /dev/null +++ b/tests/metagpt/utils/test_reflection.py @@ -0,0 +1,29 @@ +from metagpt.utils.reflection import get_func_or_method_name + + +def simple_function(): + pass + + +class SampleClass: + def method(self): + pass + + +class TestFunctionOrMethodName: + def test_simple_function(self): + assert get_func_or_method_name(simple_function) == "simple_function" + + def test_class_method_without_args(self): + sample_instance = SampleClass() + assert get_func_or_method_name(sample_instance.method) == "SampleClass.method" + + def test_class_method_with_args(self): + sample_instance = SampleClass() + assert get_func_or_method_name(SampleClass.method, sample_instance) == "SampleClass.method" + + def test_function_with_no_args(self): + assert get_func_or_method_name(simple_function) == "simple_function" + + def test_method_without_instance(self): + assert get_func_or_method_name(SampleClass.method) == "method" From 29c61a7fa407ccad69e871a147a5b9ad22e958b8 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Mon, 3 Jun 2024 10:14:57 +0800 Subject: [PATCH 015/119] experience pool --- config/config2.example.yaml | 3 +++ examples/exp_pool/manager.py | 21 ++++++++++++++++++++ metagpt/config2.py | 4 ++++ metagpt/configs/exp_pool_config.py | 6 ++++++ metagpt/exp_pool/__init__.py | 0 metagpt/exp_pool/decorator.py | 4 ++++ metagpt/exp_pool/manager.py | 32 ++++++++++++++++++++++++++++++ metagpt/exp_pool/schema.py | 25 +++++++++++++++++++++++ metagpt/utils/reflection.py | 9 +++++++++ metagpt/utils/token_counter.py | 2 ++ 10 files changed, 106 insertions(+) create mode 100644 examples/exp_pool/manager.py create mode 100644 metagpt/configs/exp_pool_config.py create mode 100644 metagpt/exp_pool/__init__.py create mode 100644 metagpt/exp_pool/decorator.py create mode 100644 metagpt/exp_pool/manager.py create mode 100644 metagpt/exp_pool/schema.py diff --git a/config/config2.example.yaml b/config/config2.example.yaml index f1158775b..c5ca6e767 100644 --- a/config/config2.example.yaml +++ b/config/config2.example.yaml @@ -74,6 +74,9 @@ s3: secure: false bucket: "test" +experience_pool: + enable_read: false + enable_write: false azure_tts_subscription_key: "YOUR_SUBSCRIPTION_KEY" azure_tts_region: "eastus" diff --git a/examples/exp_pool/manager.py b/examples/exp_pool/manager.py new file mode 100644 index 000000000..f5766f9a5 --- /dev/null +++ b/examples/exp_pool/manager.py @@ -0,0 +1,21 @@ +from metagpt.exp_pool.manager import ExperiencePoolManager +from metagpt.exp_pool.schema import Experience +from pprint import pprint +import asyncio +# import logging +# logging.basicConfig(level=logging.DEBUG) + +async def main(): + req = "2048 game" + exp = Experience(req=req, resp="python code") + + manager = ExperiencePoolManager() + + # pprint(manager.storage.get()) + # manager.create_exp(exp) + result = await manager.query_exp(req) + print(result) + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/metagpt/config2.py b/metagpt/config2.py index 717fe63a9..6f5a1add6 100644 --- a/metagpt/config2.py +++ b/metagpt/config2.py @@ -22,6 +22,7 @@ from metagpt.configs.search_config import SearchConfig from metagpt.configs.workspace_config import WorkspaceConfig from metagpt.const import CONFIG_ROOT, METAGPT_ROOT from metagpt.utils.yaml_model import YamlModel +from metagpt.configs.exp_pool_config import ExperiencePoolConfig class CLIParams(BaseModel): @@ -71,6 +72,9 @@ class Config(CLIParams, YamlModel): enable_longterm_memory: bool = False code_review_k_times: int = 2 + # Experience Pool Parameters + experience_pool: Optional[ExperiencePoolConfig] = None + # Will be removed in the future metagpt_tti_url: str = "" language: str = "English" diff --git a/metagpt/configs/exp_pool_config.py b/metagpt/configs/exp_pool_config.py new file mode 100644 index 000000000..f7312d2de --- /dev/null +++ b/metagpt/configs/exp_pool_config.py @@ -0,0 +1,6 @@ +from metagpt.utils.yaml_model import YamlModel + + +class ExperiencePoolConfig(YamlModel): + enable_read: bool = False + enable_write: bool = False diff --git a/metagpt/exp_pool/__init__.py b/metagpt/exp_pool/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/metagpt/exp_pool/decorator.py b/metagpt/exp_pool/decorator.py new file mode 100644 index 000000000..6629e8377 --- /dev/null +++ b/metagpt/exp_pool/decorator.py @@ -0,0 +1,4 @@ + + +def exp_cache(func): + pass diff --git a/metagpt/exp_pool/manager.py b/metagpt/exp_pool/manager.py new file mode 100644 index 000000000..c32073a9f --- /dev/null +++ b/metagpt/exp_pool/manager.py @@ -0,0 +1,32 @@ +from pydantic import BaseModel, ConfigDict +from metagpt.exp_pool.schema import Experience +import uuid +import chromadb +from chromadb import Collection, QueryResult +from typing import Optional +from metagpt.rag.engines import SimpleEngine +from metagpt.rag.schema import ChromaRetrieverConfig + + +class ExperiencePoolManager(BaseModel): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._storage = None + + @property + def storage(self) -> SimpleEngine: + if self._storage is None: + self._storage = SimpleEngine.from_objs(retriever_configs=[ChromaRetrieverConfig(collection_name="experience_pool", persist_path="./chroma_data")]) + return self._storage + + def create_exp(self, exp: Experience): + self.storage.add_objs([exp]) + + async def query_exp(self, req: str) -> list[Experience]: + nodes = await self.storage.aretrieve(req) + exps = [node.metadata["obj"] for node in nodes] + + return exps + + + diff --git a/metagpt/exp_pool/schema.py b/metagpt/exp_pool/schema.py new file mode 100644 index 000000000..359268612 --- /dev/null +++ b/metagpt/exp_pool/schema.py @@ -0,0 +1,25 @@ +from pydantic import BaseModel, Field +from llama_index.core.schema import TextNode + + +class Experience(BaseModel): + req: str = Field(..., description="") + resp: str = Field(..., description="") + + def rag_key(self): + return self.req + + +class ExperienceNodeMetadata(BaseModel): + """Metadata of ExperienceNode.""" + + resp: str = Field(..., description="") + + +class ExperienceNode(TextNode): + """ExperienceNode for RAG.""" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.excluded_llm_metadata_keys = list(ExperienceNodeMetadata.model_fields.keys()) + self.excluded_embed_metadata_keys = self.excluded_llm_metadata_keys diff --git a/metagpt/utils/reflection.py b/metagpt/utils/reflection.py index 8b8237ae7..688831f06 100644 --- a/metagpt/utils/reflection.py +++ b/metagpt/utils/reflection.py @@ -1,4 +1,5 @@ """class tools, including method inspection, class attributes, inheritance relationships, etc.""" +import inspect def check_methods(C, *methods): @@ -16,3 +17,11 @@ def check_methods(C, *methods): else: return NotImplemented return True + + +def get_func_full_name(func, *args) -> str: + if inspect.ismethod(func) or (inspect.isfunction(func) and "self" in inspect.signature(func).parameters): + cls_name = args[0].__class__.__name__ + return f"{func.__module__}.{cls_name}.{func.__name__}" + + return f"{func.__module__}.{func.__name__}" diff --git a/metagpt/utils/token_counter.py b/metagpt/utils/token_counter.py index 0ba2daa89..496842a2d 100644 --- a/metagpt/utils/token_counter.py +++ b/metagpt/utils/token_counter.py @@ -150,6 +150,8 @@ TOKEN_MAX = { "gpt-4-1106-preview": 128000, "gpt-4-vision-preview": 128000, "gpt-4-1106-vision-preview": 128000, + "gpt-4-turbo": 128000, + "gpt-4o": 128000, "gpt-4": 8192, "gpt-4-0613": 8192, "gpt-4-32k": 32768, From 808d65b4c3ec5198ced79f487422109468494406 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Tue, 4 Jun 2024 10:28:39 +0800 Subject: [PATCH 016/119] experiment pool init --- .gitignore | 1 + config/config2.example.yaml | 6 +- examples/exp_pool/decorator.py | 26 +++++++ examples/exp_pool/manager.py | 21 ------ examples/exp_pool/simple.py | 29 ++++++++ metagpt/config2.py | 4 +- metagpt/configs/exp_pool_config.py | 6 +- metagpt/exp_pool/__init__.py | 6 ++ metagpt/exp_pool/decorator.py | 56 +++++++++++++- metagpt/exp_pool/manager.py | 113 ++++++++++++++++++++++++----- metagpt/exp_pool/schema.py | 40 +++++++++- metagpt/utils/reflection.py | 2 +- 12 files changed, 257 insertions(+), 53 deletions(-) create mode 100644 examples/exp_pool/decorator.py delete mode 100644 examples/exp_pool/manager.py create mode 100644 examples/exp_pool/simple.py diff --git a/.gitignore b/.gitignore index aa5edd74a..7c64829ad 100644 --- a/.gitignore +++ b/.gitignore @@ -162,6 +162,7 @@ examples/graph_store.json examples/image__vector_store.json examples/index_store.json .chroma +.chroma_exp_data *~$* workspace/* tmp diff --git a/config/config2.example.yaml b/config/config2.example.yaml index c5ca6e767..c7b2cae2c 100644 --- a/config/config2.example.yaml +++ b/config/config2.example.yaml @@ -74,9 +74,9 @@ s3: secure: false bucket: "test" -experience_pool: - enable_read: false - enable_write: false +exp_pool: + enable_read: true + enable_write: true azure_tts_subscription_key: "YOUR_SUBSCRIPTION_KEY" azure_tts_region: "eastus" diff --git a/examples/exp_pool/decorator.py b/examples/exp_pool/decorator.py new file mode 100644 index 000000000..2f6397f80 --- /dev/null +++ b/examples/exp_pool/decorator.py @@ -0,0 +1,26 @@ +"""Decorator example of experience pool.""" + +import asyncio +import uuid + +from metagpt.exp_pool import exp_cache, exp_manager +from metagpt.logs import logger + + +@exp_cache +async def produce(req): + return f"{req} {uuid.uuid4().hex}" + + +async def main(): + req = "Water" + + resp = await produce(req) + logger.info(f"The resp of `produce{req}` is: {resp}") + + exps = await exp_manager.query_exps(req) + logger.info(f"Find experiences: {exps}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/exp_pool/manager.py b/examples/exp_pool/manager.py deleted file mode 100644 index f5766f9a5..000000000 --- a/examples/exp_pool/manager.py +++ /dev/null @@ -1,21 +0,0 @@ -from metagpt.exp_pool.manager import ExperiencePoolManager -from metagpt.exp_pool.schema import Experience -from pprint import pprint -import asyncio -# import logging -# logging.basicConfig(level=logging.DEBUG) - -async def main(): - req = "2048 game" - exp = Experience(req=req, resp="python code") - - manager = ExperiencePoolManager() - - # pprint(manager.storage.get()) - # manager.create_exp(exp) - result = await manager.query_exp(req) - print(result) - - -if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file diff --git a/examples/exp_pool/simple.py b/examples/exp_pool/simple.py new file mode 100644 index 000000000..bc20fbcdd --- /dev/null +++ b/examples/exp_pool/simple.py @@ -0,0 +1,29 @@ +"""Simple example of experience pool.""" + +import asyncio + +from metagpt.exp_pool import exp_manager +from metagpt.exp_pool.schema import EntryType, Experience +from metagpt.logs import logger + + +async def main(): + req = "Simple task." + + # 1. Find experiences. + exps = await exp_manager.query_exps(req) + if exps: + logger.info(f"Experiences already exist for the request `{req}`: {exps}") + return + + # 2. Create a new experience if none exist + exp_manager.create_exp(Experience(req=req, resp="Simple echo.", entry_type=EntryType.MANUAL)) + logger.info(f"New experience created for the request `{req}`.") + + # 3. Find again + exps = await exp_manager.query_exps(req) + logger.info(f"Updated experiences: {exps}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/metagpt/config2.py b/metagpt/config2.py index 6f5a1add6..6588a6036 100644 --- a/metagpt/config2.py +++ b/metagpt/config2.py @@ -13,6 +13,7 @@ from pydantic import BaseModel, model_validator from metagpt.configs.browser_config import BrowserConfig from metagpt.configs.embedding_config import EmbeddingConfig +from metagpt.configs.exp_pool_config import ExperiencePoolConfig from metagpt.configs.llm_config import LLMConfig, LLMType from metagpt.configs.mermaid_config import MermaidConfig from metagpt.configs.redis_config import RedisConfig @@ -22,7 +23,6 @@ from metagpt.configs.search_config import SearchConfig from metagpt.configs.workspace_config import WorkspaceConfig from metagpt.const import CONFIG_ROOT, METAGPT_ROOT from metagpt.utils.yaml_model import YamlModel -from metagpt.configs.exp_pool_config import ExperiencePoolConfig class CLIParams(BaseModel): @@ -73,7 +73,7 @@ class Config(CLIParams, YamlModel): code_review_k_times: int = 2 # Experience Pool Parameters - experience_pool: Optional[ExperiencePoolConfig] = None + exp_pool: ExperiencePoolConfig = ExperiencePoolConfig() # Will be removed in the future metagpt_tti_url: str = "" diff --git a/metagpt/configs/exp_pool_config.py b/metagpt/configs/exp_pool_config.py index f7312d2de..3f86173c1 100644 --- a/metagpt/configs/exp_pool_config.py +++ b/metagpt/configs/exp_pool_config.py @@ -1,6 +1,8 @@ +from pydantic import Field + from metagpt.utils.yaml_model import YamlModel class ExperiencePoolConfig(YamlModel): - enable_read: bool = False - enable_write: bool = False + enable_read: bool = Field(default=True, description="Enable to read from experience pool.") + enable_write: bool = Field(default=True, description="Enable to write to experience pool.") diff --git a/metagpt/exp_pool/__init__.py b/metagpt/exp_pool/__init__.py index e69de29bb..aeeb94b38 100644 --- a/metagpt/exp_pool/__init__.py +++ b/metagpt/exp_pool/__init__.py @@ -0,0 +1,6 @@ +"""Experience pool init.""" + +from metagpt.exp_pool.manager import exp_manager +from metagpt.exp_pool.decorator import exp_cache + +__all__ = ["exp_manager", "exp_cache"] diff --git a/metagpt/exp_pool/decorator.py b/metagpt/exp_pool/decorator.py index 6629e8377..1d691b8f3 100644 --- a/metagpt/exp_pool/decorator.py +++ b/metagpt/exp_pool/decorator.py @@ -1,4 +1,56 @@ +"""Experience Decorator.""" + +import asyncio +import functools +from typing import Any, Callable, Optional, TypeVar + +from metagpt.exp_pool.manager import exp_manager +from metagpt.exp_pool.schema import Experience +from metagpt.utils.async_helper import NestAsyncio + +ReturnType = TypeVar("ReturnType") -def exp_cache(func): - pass +def exp_cache(_func: Optional[Callable[..., ReturnType]] = None): + """Decorator to check for a perfect experience and returns it if exists. + + Otherwise, it executes the function, save the result as a new experience, and returns the result. + + This can be applied to both synchronous and asynchronous functions. + """ + + def decorator(func: Callable[..., ReturnType]) -> Callable[..., ReturnType]: + @functools.wraps(func) + async def get_or_create(args: Any, kwargs: Any, is_async: bool) -> ReturnType: + """Attempts to retrieve a cached experience or creates one if not found.""" + + req = f"{func.__name__}_{args}_{kwargs}" + exps = await exp_manager.query_exps(req) + if perfect_exp := exp_manager.extract_one_perfect_exp(exps): + return perfect_exp + + if is_async: + result = await func(*args, **kwargs) + else: + result = func(*args, **kwargs) + + exp_manager.create_exp(Experience(req=req, resp=result)) + + return result + + def sync_wrapper(*args: Any, **kwargs: Any) -> ReturnType: + NestAsyncio.apply_once() + return asyncio.get_event_loop().run_until_complete(get_or_create(args, kwargs, is_async=False)) + + async def async_wrapper(*args: Any, **kwargs: Any) -> ReturnType: + return await get_or_create(args, kwargs, is_async=True) + + if asyncio.iscoroutinefunction(func): + return async_wrapper + else: + return sync_wrapper + + if _func is None: + return decorator + else: + return decorator(_func) diff --git a/metagpt/exp_pool/manager.py b/metagpt/exp_pool/manager.py index c32073a9f..4bc566104 100644 --- a/metagpt/exp_pool/manager.py +++ b/metagpt/exp_pool/manager.py @@ -1,32 +1,105 @@ -from pydantic import BaseModel, ConfigDict -from metagpt.exp_pool.schema import Experience -import uuid -import chromadb -from chromadb import Collection, QueryResult +"""Experience Manager.""" + from typing import Optional + +from pydantic import BaseModel, ConfigDict, model_validator + +from metagpt.config2 import Config, config +from metagpt.exp_pool.schema import MAX_SCORE, Experience from metagpt.rag.engines import SimpleEngine -from metagpt.rag.schema import ChromaRetrieverConfig +from metagpt.rag.schema import ChromaRetrieverConfig, LLMRankerConfig -class ExperiencePoolManager(BaseModel): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self._storage = None +class ExperienceManager(BaseModel): + """ExperienceManager manages the lifecycle of experiences, including CRUD and optimization. + + Attributes: + config (Config): Configuration for managing experiences. + storage (SimpleEngine): Engine to handle the storage and retrieval of experiences. + """ + + model_config = ConfigDict(arbitrary_types_allowed=True) + + config: Config = config + storage: SimpleEngine = None + + @model_validator(mode="after") + def initialize(self): + if self.storage is None: + self.storage = SimpleEngine.from_objs( + retriever_configs=[ + ChromaRetrieverConfig(collection_name="experience_pool", persist_path=".chroma_exp_data") + ], + ranker_configs=[LLMRankerConfig()], + ) + return self - @property - def storage(self) -> SimpleEngine: - if self._storage is None: - self._storage = SimpleEngine.from_objs(retriever_configs=[ChromaRetrieverConfig(collection_name="experience_pool", persist_path="./chroma_data")]) - return self._storage - def create_exp(self, exp: Experience): + """Adds an experience to the storage if writing is enabled. + + Args: + exp (Experience): The experience to add. + """ + if not self.config.exp_pool.enable_write: + return + self.storage.add_objs([exp]) - - async def query_exp(self, req: str) -> list[Experience]: + + async def query_exps(self, req: str, tag: str = "") -> list[Experience]: + """Retrieves and filters experiences. + + Args: + req (str): The query string to retrieve experiences. + tag (str): Optional tag to filter the experiences by. + + Returns: + list[Experience]: A list of experiences that match the args. + """ + if not self.config.exp_pool.enable_read: + return [] + nodes = await self.storage.aretrieve(req) - exps = [node.metadata["obj"] for node in nodes] + exps: list[Experience] = [node.metadata["obj"] for node in nodes] + + # TODO: filter by metadata + if tag: + exps = [exp for exp in exps if exp.tag == tag] return exps - + def extract_one_perfect_exp(self, exps: list[Experience]) -> Optional[Experience]: + """Extracts the first 'perfect' experience from a list of experiences. + Args: + exps (list[Experience]): The experiences to evaluate. + + Returns: + Optional[Experience]: The first perfect experience if found, otherwise None. + """ + for exp in exps: + if self.is_perfect_exp(exp): + return exp + + return None + + @staticmethod + def is_perfect_exp(exp: Experience) -> bool: + """Determines if an experience is considered 'perfect'. + + Args: + exp (Experience): The experience to evaluate. + + Returns: + bool: True if the experience is manually entered, otherwise False. + """ + if not exp: + return False + + # TODO: need more metrics + if exp.metric and exp.metric.score == MAX_SCORE: + return True + + return False + + +exp_manager = ExperienceManager() diff --git a/metagpt/exp_pool/schema.py b/metagpt/exp_pool/schema.py index 359268612..b51bc3c17 100644 --- a/metagpt/exp_pool/schema.py +++ b/metagpt/exp_pool/schema.py @@ -1,10 +1,46 @@ -from pydantic import BaseModel, Field +"""Experience schema.""" + +from enum import Enum +from typing import Optional + from llama_index.core.schema import TextNode +from pydantic import BaseModel, Field + +MAX_SCORE = 10 + + +class ExperienceType(str, Enum): + """Experience Type.""" + + SUCCESS = "success" + FAILURE = "failure" + INSIGHT = "insight" + + +class EntryType(Enum): + """Experience Entry Type.""" + + AUTOMATIC = "Automatic" + MANUAL = "Manual" + + +class Metric(BaseModel): + """Experience Metric.""" + + time_cost: float = Field(default=0.000, description="Time cost, the unit is milliseconds.") + money_cost: float = Field(default=0.000, description="Money cost, the unit is US dollars.") + score: int = Field(default=1, description="Score, a value between 1 and 10.") class Experience(BaseModel): + """Experience.""" + req: str = Field(..., description="") - resp: str = Field(..., description="") + resp: str = Field(..., description="The type is string/json/code.") + metric: Optional[Metric] = Field(default=None, description="Metric.") + exp_type: ExperienceType = Field(default=ExperienceType.SUCCESS, description="The type of experience.") + entry_type: EntryType = Field(default=EntryType.AUTOMATIC, description="Type of entry: Manual or Automatic.") + tag: str = Field(default="", description="Tagging experience.") def rag_key(self): return self.req diff --git a/metagpt/utils/reflection.py b/metagpt/utils/reflection.py index 688831f06..2683e5657 100644 --- a/metagpt/utils/reflection.py +++ b/metagpt/utils/reflection.py @@ -23,5 +23,5 @@ def get_func_full_name(func, *args) -> str: if inspect.ismethod(func) or (inspect.isfunction(func) and "self" in inspect.signature(func).parameters): cls_name = args[0].__class__.__name__ return f"{func.__module__}.{cls_name}.{func.__name__}" - + return f"{func.__module__}.{func.__name__}" From d1198dc58de9e5503e1bdbc42297e25957c83ea0 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Tue, 4 Jun 2024 11:33:34 +0800 Subject: [PATCH 017/119] experiment pool init --- examples/exp_pool/simple.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/examples/exp_pool/simple.py b/examples/exp_pool/simple.py index bc20fbcdd..608578519 100644 --- a/examples/exp_pool/simple.py +++ b/examples/exp_pool/simple.py @@ -9,20 +9,13 @@ from metagpt.logs import logger async def main(): req = "Simple task." + resp = "Simple echo." - # 1. Find experiences. - exps = await exp_manager.query_exps(req) - if exps: - logger.info(f"Experiences already exist for the request `{req}`: {exps}") - return - - # 2. Create a new experience if none exist - exp_manager.create_exp(Experience(req=req, resp="Simple echo.", entry_type=EntryType.MANUAL)) + exp_manager.create_exp(Experience(req=req, resp=resp, entry_type=EntryType.MANUAL)) logger.info(f"New experience created for the request `{req}`.") - # 3. Find again exps = await exp_manager.query_exps(req) - logger.info(f"Updated experiences: {exps}") + logger.info(f"Got experiences: {exps}") if __name__ == "__main__": From 8edfa02533a41002723d3cf5e871204e9e980f16 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Tue, 4 Jun 2024 11:46:57 +0800 Subject: [PATCH 018/119] experiment pool init --- examples/exp_pool/simple.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/exp_pool/simple.py b/examples/exp_pool/simple.py index 608578519..f270824bf 100644 --- a/examples/exp_pool/simple.py +++ b/examples/exp_pool/simple.py @@ -10,8 +10,9 @@ from metagpt.logs import logger async def main(): req = "Simple task." resp = "Simple echo." + exp = Experience(req=req, resp=resp, entry_type=EntryType.MANUAL) - exp_manager.create_exp(Experience(req=req, resp=resp, entry_type=EntryType.MANUAL)) + exp_manager.create_exp(exp) logger.info(f"New experience created for the request `{req}`.") exps = await exp_manager.query_exps(req) From 96cd6b5f64f0e3f4cbc4bd37fb97f08c3e4d197b Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Tue, 4 Jun 2024 15:16:58 +0800 Subject: [PATCH 019/119] add trajectory schema --- metagpt/exp_pool/schema.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/metagpt/exp_pool/schema.py b/metagpt/exp_pool/schema.py index b51bc3c17..e6ae4ee1d 100644 --- a/metagpt/exp_pool/schema.py +++ b/metagpt/exp_pool/schema.py @@ -32,6 +32,14 @@ class Metric(BaseModel): score: int = Field(default=1, description="Score, a value between 1 and 10.") +class Trajectory(BaseModel): + """Experience Trajectory.""" + + plan: str = Field(default="", description="The plan.") + action: str = Field(default="", description="Action for the plan.") + observation: str = Field(default="", description="Output of the action.") + + class Experience(BaseModel): """Experience.""" @@ -41,6 +49,7 @@ class Experience(BaseModel): exp_type: ExperienceType = Field(default=ExperienceType.SUCCESS, description="The type of experience.") entry_type: EntryType = Field(default=EntryType.AUTOMATIC, description="Type of entry: Manual or Automatic.") tag: str = Field(default="", description="Tagging experience.") + traj: Optional[Trajectory] = Field(default=None, description="Trajectory.") def rag_key(self): return self.req From d600cc47f45f07d144c65669959530bbba4069c7 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Tue, 4 Jun 2024 22:08:40 +0800 Subject: [PATCH 020/119] add exp_pool test --- metagpt/exp_pool/schema.py | 5 +- tests/metagpt/exp_pool/test_manager.py | 77 ++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 tests/metagpt/exp_pool/test_manager.py diff --git a/metagpt/exp_pool/schema.py b/metagpt/exp_pool/schema.py index e6ae4ee1d..1afcc1508 100644 --- a/metagpt/exp_pool/schema.py +++ b/metagpt/exp_pool/schema.py @@ -1,7 +1,7 @@ """Experience schema.""" from enum import Enum -from typing import Optional +from typing import Any, Optional from llama_index.core.schema import TextNode from pydantic import BaseModel, Field @@ -38,13 +38,14 @@ class Trajectory(BaseModel): plan: str = Field(default="", description="The plan.") action: str = Field(default="", description="Action for the plan.") observation: str = Field(default="", description="Output of the action.") + reward: int = Field(default=0, description="Measure the action.") class Experience(BaseModel): """Experience.""" req: str = Field(..., description="") - resp: str = Field(..., description="The type is string/json/code.") + resp: Any = Field(..., description="The type is string/json/code.") metric: Optional[Metric] = Field(default=None, description="Metric.") exp_type: ExperienceType = Field(default=ExperienceType.SUCCESS, description="The type of experience.") entry_type: EntryType = Field(default=EntryType.AUTOMATIC, description="Type of entry: Manual or Automatic.") diff --git a/tests/metagpt/exp_pool/test_manager.py b/tests/metagpt/exp_pool/test_manager.py new file mode 100644 index 000000000..a0d7005f5 --- /dev/null +++ b/tests/metagpt/exp_pool/test_manager.py @@ -0,0 +1,77 @@ +import pytest + +from metagpt.config2 import Config +from metagpt.configs.exp_pool_config import ExperiencePoolConfig +from metagpt.configs.llm_config import LLMConfig +from metagpt.exp_pool.manager import ExperienceManager +from metagpt.exp_pool.schema import MAX_SCORE, Experience, Metric +from metagpt.rag.engines import SimpleEngine + + +class TestExperienceManager: + @pytest.fixture + def mock_config(self): + return Config(llm=LLMConfig(), exp_pool=ExperiencePoolConfig(enable_write=True, enable_read=True)) + + @pytest.fixture + def mock_storage(self, mocker): + engine = mocker.MagicMock(spec=SimpleEngine) + engine.add_objs = mocker.MagicMock() + engine.aretrieve = mocker.AsyncMock(return_value=[]) + return engine + + @pytest.fixture + def mock_experience_manager(self, mock_config, mock_storage): + return ExperienceManager(config=mock_config, storage=mock_storage) + + @pytest.fixture + def mock_experience(self): + return Experience(req="req", resp="resp") + + def test_initialize_storage(self, mock_experience_manager, mock_storage): + assert mock_experience_manager.storage is mock_storage + + def test_create_exp(self, mock_experience_manager, mock_experience): + mock_experience_manager.create_exp(mock_experience) + mock_experience_manager.storage.add_objs.assert_called_once_with([mock_experience]) + + def test_create_exp_write_disabled(self, mock_experience_manager, mock_experience, mock_config): + mock_config.exp_pool.enable_write = False + mock_experience_manager.create_exp(mock_experience) + mock_experience_manager.storage.add_objs.assert_not_called() + + @pytest.mark.asyncio + async def test_query_exps(self, mock_experience_manager, mocker): + req = "req" + resp = "resp" + tag = "test" + experiences = [Experience(req=req, resp=resp, tag="test"), Experience(req=req, resp=resp, tag="other")] + mock_experience_manager.storage.aretrieve.return_value = [ + mocker.MagicMock(metadata={"obj": exp}) for exp in experiences + ] + + result = await mock_experience_manager.query_exps(req, tag) + assert len(result) == 1 + assert result[0].tag == "test" + + @pytest.mark.asyncio + async def test_query_exps_no_read_permission(self, mock_experience_manager, mock_config): + mock_config.exp_pool.enable_read = False + result = await mock_experience_manager.query_exps("query") + assert result == [] + + def test_extract_one_perfect_exp(self, mock_experience_manager): + experiences = [ + Experience(req="req", resp="resp", metric=Metric(score=MAX_SCORE)), + Experience(req="req", resp="resp"), + ] + perfect_exp: Experience = mock_experience_manager.extract_one_perfect_exp(experiences) + assert perfect_exp is not None + assert perfect_exp.metric.score == MAX_SCORE + + def test_is_perfect_exp(self): + exp = Experience(req="req", resp="resp", metric=Metric(score=MAX_SCORE)) + assert ExperienceManager.is_perfect_exp(exp) == True + + exp = Experience(req="req", resp="resp") + assert ExperienceManager.is_perfect_exp(exp) == False From 9b65dea952d788fa9632379e6806d66a1b4f3a5b Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Wed, 5 Jun 2024 10:32:48 +0800 Subject: [PATCH 021/119] update comment --- metagpt/exp_pool/decorator.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/metagpt/exp_pool/decorator.py b/metagpt/exp_pool/decorator.py index 1d691b8f3..e073ee494 100644 --- a/metagpt/exp_pool/decorator.py +++ b/metagpt/exp_pool/decorator.py @@ -22,18 +22,21 @@ def exp_cache(_func: Optional[Callable[..., ReturnType]] = None): def decorator(func: Callable[..., ReturnType]) -> Callable[..., ReturnType]: @functools.wraps(func) async def get_or_create(args: Any, kwargs: Any, is_async: bool) -> ReturnType: - """Attempts to retrieve a cached experience or creates one if not found.""" + """Attempts to retrieve a perfect experience or creates an experience if not found.""" + # 1. Get exps. req = f"{func.__name__}_{args}_{kwargs}" exps = await exp_manager.query_exps(req) if perfect_exp := exp_manager.extract_one_perfect_exp(exps): return perfect_exp + # 2. Exec func. TODO: pass exps to func if is_async: result = await func(*args, **kwargs) else: result = func(*args, **kwargs) + # 3. Create an exp. exp_manager.create_exp(Experience(req=req, resp=result)) return result From 07bf103fb08c7c0075079380dc2692accc03e1a5 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Wed, 5 Jun 2024 22:15:09 +0800 Subject: [PATCH 022/119] add exp_pool tests --- examples/exp_pool/decorator.py | 5 +- metagpt/exp_pool/decorator.py | 144 ++++++++++++++++------ metagpt/exp_pool/manager.py | 10 +- metagpt/exp_pool/schema.py | 16 ++- metagpt/exp_pool/scorers/__init__.py | 6 + metagpt/exp_pool/scorers/base.py | 27 +++++ metagpt/exp_pool/scorers/simple.py | 73 ++++++++++++ tests/metagpt/exp_pool/test_decorator.py | 145 +++++++++++++++++++++++ tests/metagpt/exp_pool/test_manager.py | 8 +- 9 files changed, 391 insertions(+), 43 deletions(-) create mode 100644 metagpt/exp_pool/scorers/__init__.py create mode 100644 metagpt/exp_pool/scorers/base.py create mode 100644 metagpt/exp_pool/scorers/simple.py create mode 100644 tests/metagpt/exp_pool/test_decorator.py diff --git a/examples/exp_pool/decorator.py b/examples/exp_pool/decorator.py index 2f6397f80..3f6093e01 100644 --- a/examples/exp_pool/decorator.py +++ b/examples/exp_pool/decorator.py @@ -7,8 +7,9 @@ from metagpt.exp_pool import exp_cache, exp_manager from metagpt.logs import logger -@exp_cache -async def produce(req): +@exp_cache(pass_exps_to_func=True) +async def produce(req, exps=None): + logger.info(f"Previous experiences: {exps}") return f"{req} {uuid.uuid4().hex}" diff --git a/metagpt/exp_pool/decorator.py b/metagpt/exp_pool/decorator.py index e073ee494..9eb4d9e61 100644 --- a/metagpt/exp_pool/decorator.py +++ b/metagpt/exp_pool/decorator.py @@ -4,56 +4,134 @@ import asyncio import functools from typing import Any, Callable, Optional, TypeVar -from metagpt.exp_pool.manager import exp_manager -from metagpt.exp_pool.schema import Experience +from pydantic import BaseModel, ConfigDict + +from metagpt.exp_pool.manager import ExperienceManager, exp_manager +from metagpt.exp_pool.schema import Experience, Metric, QueryType, Score +from metagpt.exp_pool.scorers import ExperienceScorer, SimpleScorer from metagpt.utils.async_helper import NestAsyncio ReturnType = TypeVar("ReturnType") -def exp_cache(_func: Optional[Callable[..., ReturnType]] = None): - """Decorator to check for a perfect experience and returns it if exists. - - Otherwise, it executes the function, save the result as a new experience, and returns the result. +def exp_cache( + _func: Optional[Callable[..., ReturnType]] = None, + query_type: QueryType = QueryType.SEMANTIC, + scorer: Optional[ExperienceScorer] = None, + manager: Optional[ExperienceManager] = None, + pass_exps_to_func: bool = False, +): + """Decorator to get a perfect experience, otherwise, it executes the function, and create a new experience. This can be applied to both synchronous and asynchronous functions. + + Args: + _func: Just to make the decorator more flexible, for example, it can be used directly with @exp_cache by default, without the need for @exp_cache(). + query_type: The type of query to be used when fetching experiences. + scorer: Evaluate experience. Default SimpleScorer. + manager: How to fetch, evaluate and save experience, etc. Default exp_manager. + pass_exps_to_func: To control whether imperfect experiences are passed to the function, if True, the func must have a parameter named 'exps'. """ def decorator(func: Callable[..., ReturnType]) -> Callable[..., ReturnType]: @functools.wraps(func) - async def get_or_create(args: Any, kwargs: Any, is_async: bool) -> ReturnType: - """Attempts to retrieve a perfect experience or creates an experience if not found.""" + async def get_or_create(args: Any, kwargs: Any) -> ReturnType: + handler = ExpCacheHandler( + func=func, + args=args, + kwargs=kwargs, + exp_manager=manager or exp_manager, + exp_scorer=scorer or SimpleScorer(), + pass_exps=pass_exps_to_func, + ) - # 1. Get exps. - req = f"{func.__name__}_{args}_{kwargs}" - exps = await exp_manager.query_exps(req) - if perfect_exp := exp_manager.extract_one_perfect_exp(exps): - return perfect_exp + await handler.fetch_experiences(query_type) + if exp := handler.get_one_perfect_experience(): + return exp - # 2. Exec func. TODO: pass exps to func - if is_async: - result = await func(*args, **kwargs) - else: - result = func(*args, **kwargs) + await handler.execute_function() + await handler.evaluate_experience() + handler.save_experience() - # 3. Create an exp. - exp_manager.create_exp(Experience(req=req, resp=result)) + return handler._result - return result + return ExpCacheHandler.choose_wrapper(func, get_or_create) - def sync_wrapper(*args: Any, **kwargs: Any) -> ReturnType: + return decorator(_func) if _func else decorator + + +class ExpCacheHandler(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + + func: Callable + args: Any + kwargs: Any + exp_manager: ExperienceManager + exp_scorer: ExperienceScorer + pass_exps: bool + + _exps: list[Experience] = None + _result: Any = None + _score: Score = None + + async def fetch_experiences(self, query_type: QueryType): + """Fetch a potentially perfect existing experience.""" + + req = self.generate_req_identifier() + self._exps = await self.exp_manager.query_exps(req, query_type=query_type) + + def get_one_perfect_experience(self) -> Optional[Experience]: + return self.exp_manager.extract_one_perfect_exp(self._exps) + + async def execute_function(self): + """Execute the function, and save the result.""" + self._result = await self._execute_function() + + async def evaluate_experience(self): + """Evaluate the experience, and save the score.""" + + self._score = await self.exp_scorer.evaluate(self.func, self._result, self.args, self.kwargs) + + def save_experience(self): + """Save the new experience.""" + + req = self.generate_req_identifier() + exp = Experience(req=req, resp=self._result, metric=Metric(score=self._score)) + + self.exp_manager.create_exp(exp) + + def generate_req_identifier(self): + """Generate a unique request identifier based on the function and its arguments.""" + + return f"{self.func.__name__}_{self.args}_{self.kwargs}" + + @staticmethod + def choose_wrapper(func, wrapped_func): + """Choose how to run wrapped_func based on whether the function is asynchronous.""" + + async def async_wrapper(*args, **kwargs): + return await wrapped_func(args, kwargs) + + def sync_wrapper(*args, **kwargs): NestAsyncio.apply_once() - return asyncio.get_event_loop().run_until_complete(get_or_create(args, kwargs, is_async=False)) + return asyncio.get_event_loop().run_until_complete(wrapped_func(args, kwargs)) - async def async_wrapper(*args: Any, **kwargs: Any) -> ReturnType: - return await get_or_create(args, kwargs, is_async=True) + return async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper - if asyncio.iscoroutinefunction(func): - return async_wrapper - else: - return sync_wrapper + async def _execute_function(self): + if self.pass_exps: + return await self._execute_function_with_exps() - if _func is None: - return decorator - else: - return decorator(_func) + return await self._execute_function_without_exps() + + async def _execute_function_without_exps(self): + if asyncio.iscoroutinefunction(self.func): + return await self.func(*self.args, **self.kwargs) + + return self.func(*self.args, **self.kwargs) + + async def _execute_function_with_exps(self): + if asyncio.iscoroutinefunction(self.func): + return await self.func(*self.args, **self.kwargs, exps=self._exps) + + return self.func(*self.args, **self.kwargs, exps=self._exps) diff --git a/metagpt/exp_pool/manager.py b/metagpt/exp_pool/manager.py index 4bc566104..58499104d 100644 --- a/metagpt/exp_pool/manager.py +++ b/metagpt/exp_pool/manager.py @@ -5,7 +5,7 @@ from typing import Optional from pydantic import BaseModel, ConfigDict, model_validator from metagpt.config2 import Config, config -from metagpt.exp_pool.schema import MAX_SCORE, Experience +from metagpt.exp_pool.schema import MAX_SCORE, Experience, QueryType from metagpt.rag.engines import SimpleEngine from metagpt.rag.schema import ChromaRetrieverConfig, LLMRankerConfig @@ -45,12 +45,13 @@ class ExperienceManager(BaseModel): self.storage.add_objs([exp]) - async def query_exps(self, req: str, tag: str = "") -> list[Experience]: + async def query_exps(self, req: str, tag: str = "", query_type: QueryType = QueryType.SEMANTIC) -> list[Experience]: """Retrieves and filters experiences. Args: req (str): The query string to retrieve experiences. tag (str): Optional tag to filter the experiences by. + query_type (QueryType): Default semantic to vector matching. exact to same matching. Returns: list[Experience]: A list of experiences that match the args. @@ -65,6 +66,9 @@ class ExperienceManager(BaseModel): if tag: exps = [exp for exp in exps if exp.tag == tag] + if query_type == QueryType.EXACT: + exps = [exp for exp in exps if exp.req == req] + return exps def extract_one_perfect_exp(self, exps: list[Experience]) -> Optional[Experience]: @@ -96,7 +100,7 @@ class ExperienceManager(BaseModel): return False # TODO: need more metrics - if exp.metric and exp.metric.score == MAX_SCORE: + if exp.metric and exp.metric.score.val == MAX_SCORE: return True return False diff --git a/metagpt/exp_pool/schema.py b/metagpt/exp_pool/schema.py index 1afcc1508..9fc665cca 100644 --- a/metagpt/exp_pool/schema.py +++ b/metagpt/exp_pool/schema.py @@ -9,6 +9,13 @@ from pydantic import BaseModel, Field MAX_SCORE = 10 +class QueryType(str, Enum): + """Type of query experiences.""" + + EXACT = "exact" + SEMANTIC = "semantic" + + class ExperienceType(str, Enum): """Experience Type.""" @@ -24,12 +31,19 @@ class EntryType(Enum): MANUAL = "Manual" +class Score(BaseModel): + """Score in Metric.""" + + val: int = Field(default=1, description="Value of the score, Between 1 and 10, higher is better.") + reason: str = Field(default="", description="Reason for the value.") + + class Metric(BaseModel): """Experience Metric.""" time_cost: float = Field(default=0.000, description="Time cost, the unit is milliseconds.") money_cost: float = Field(default=0.000, description="Money cost, the unit is US dollars.") - score: int = Field(default=1, description="Score, a value between 1 and 10.") + score: Score = Field(default=None, description="Score, with value and reason.") class Trajectory(BaseModel): diff --git a/metagpt/exp_pool/scorers/__init__.py b/metagpt/exp_pool/scorers/__init__.py new file mode 100644 index 000000000..85bea88ff --- /dev/null +++ b/metagpt/exp_pool/scorers/__init__.py @@ -0,0 +1,6 @@ +"""Experience scorers init.""" + +from metagpt.exp_pool.scorers.base import ExperienceScorer +from metagpt.exp_pool.scorers.simple import SimpleScorer + +__all__ = ["ExperienceScorer", "SimpleScorer"] diff --git a/metagpt/exp_pool/scorers/base.py b/metagpt/exp_pool/scorers/base.py new file mode 100644 index 000000000..a9d30cffe --- /dev/null +++ b/metagpt/exp_pool/scorers/base.py @@ -0,0 +1,27 @@ +"""Experience Scorers.""" + +from abc import abstractmethod +from typing import Any, Callable + +from pydantic import BaseModel, ConfigDict + +from metagpt.exp_pool.schema import Score + + +class ExperienceScorer(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + + @abstractmethod + async def evaluate(self, func: Callable, result: Any, args: tuple = None, kwargs: dict = None) -> Score: + """Evaluate the quality of the result produced by the function and parameters. + + Args: + func (Callable): The function whose result is to be evaluated. + result (Any): The result produced by the function. + args (Tuple[Any, ...]): The tuple of arguments that were passed to the function. + kwargs (Dict[str, Any]): The dictionary of keyword arguments that were passed to the function. + + Example: + result = await sample(5, name="foo") + score = await scorer.evaluate(sample, result, args=(5), kwargs={"name": "foo"}) + """ diff --git a/metagpt/exp_pool/scorers/simple.py b/metagpt/exp_pool/scorers/simple.py new file mode 100644 index 000000000..d0301cbc2 --- /dev/null +++ b/metagpt/exp_pool/scorers/simple.py @@ -0,0 +1,73 @@ +"""Evalate by llm.""" +import inspect +import json +from typing import Any, Callable + +from pydantic import Field + +from metagpt.exp_pool.schema import Score +from metagpt.exp_pool.scorers.base import ExperienceScorer +from metagpt.llm import LLM +from metagpt.provider.base_llm import BaseLLM +from metagpt.utils.common import parse_json_code_block + +SIMPLE_SCORER_TEMPLATE = """ +Role: You're an expert score evaluator. You specialize in assessing the output of the given function, based on its intended requirement and produced result. + +## Context +### Function Name +{func_name} + +### Function Document +{func_doc} + +### Function Signature +{func_signature} + +### Function Parameters +args: {func_args} +kwargs: {func_kwargs} + +### Produced Result By Function and Parameters +{func_result} + +## Format Example +```json +{{ + "val": "the value of the score, int from 1 to 10, higher is better.", + "reason": "an explanation supporting the score." +}} +``` + +## Instructions +- Understand the function and requirements given by the user. +- Analyze the results produced by the function. +- Grade the results based on level of alignment with the requirements. +- Provide a score on a scale defined by user or a default scale (1 to 10). + +## Constraint +Format: Just print the result in json format like **Format Example**. + +## Action +Follow instructions, generate output and make sure it follows the **Constraint**. +""" + + +class SimpleScorer(ExperienceScorer): + llm: BaseLLM = Field(default_factory=LLM) + + async def evaluate(self, func: Callable, result: Any, args: tuple = None, kwargs: dict = None) -> Score: + """Evaluate the quality of content.""" + + prompt = SIMPLE_SCORER_TEMPLATE.format( + func_name=func.__name__, + func_doc=func.__doc__, + func_signature=inspect.signature(func), + func_args=args, + func_kwargs=kwargs, + func_result=result, + ) + resp = await self.llm.aask(prompt) + resp_json = json.loads(parse_json_code_block(resp)[0]) + + return Score(**resp_json) diff --git a/tests/metagpt/exp_pool/test_decorator.py b/tests/metagpt/exp_pool/test_decorator.py new file mode 100644 index 000000000..508229d18 --- /dev/null +++ b/tests/metagpt/exp_pool/test_decorator.py @@ -0,0 +1,145 @@ +import asyncio + +import pytest + +from metagpt.exp_pool.decorator import ExpCacheHandler +from metagpt.exp_pool.manager import ExperienceManager +from metagpt.exp_pool.schema import Experience, QueryType, Score +from metagpt.exp_pool.scorers import SimpleScorer +from metagpt.rag.engines import SimpleEngine + + +class TestExpCache: + @pytest.fixture + def mock_func(self, mocker): + return mocker.AsyncMock() + + @pytest.fixture + def mock_exp_manager(self, mocker): + manager = mocker.MagicMock(spec=ExperienceManager) + manager.storage = mocker.MagicMock(spec=SimpleEngine) + manager.query_exps = mocker.AsyncMock() + manager.create_exp = mocker.MagicMock() + manager.extract_one_perfect_exp = mocker.MagicMock() + return manager + + @pytest.fixture + def mock_scorer(self, mocker): + scorer = mocker.MagicMock(spec=SimpleScorer) + scorer.evaluate = mocker.AsyncMock() + return scorer + + @pytest.fixture + def exp_cache_handler(self, mock_func, mock_exp_manager, mock_scorer): + return ExpCacheHandler( + func=mock_func, args=(), kwargs={}, exp_manager=mock_exp_manager, exp_scorer=mock_scorer, pass_exps=False + ) + + @pytest.mark.asyncio + async def test_fetch_experiences(self, exp_cache_handler, mock_exp_manager): + await exp_cache_handler.fetch_experiences(QueryType.SEMANTIC) + mock_exp_manager.query_exps.assert_called_once() + + @pytest.mark.asyncio + async def test_perfect_experience_found(self, exp_cache_handler, mock_exp_manager, mock_func): + # Setup: Assume perfect experience is found + perfect_exp = Experience(req="req", resp="resp") + mock_exp_manager.extract_one_perfect_exp.return_value = perfect_exp + + # Execute + exp_cache_handler._exps = [perfect_exp] # Simulate fetched experiences + result = exp_cache_handler.get_one_perfect_experience() + + # Assert + assert result.resp == "resp" + mock_func.assert_not_called() # Function should not be called + + @pytest.mark.asyncio + async def test_execute_function_when_no_perfect_exp(self, exp_cache_handler, mock_exp_manager, mock_func): + # Setup: No perfect experience + mock_exp_manager.extract_one_perfect_exp.return_value = None + mock_func.return_value = "Computed result" + + # Execute + await exp_cache_handler.execute_function() + + # Assert + assert exp_cache_handler._result == "Computed result" + mock_func.assert_called_once() + + @pytest.mark.asyncio + async def test_evaluate_and_save_experience(self, exp_cache_handler, mock_scorer, mock_exp_manager): + # Setup + mock_scorer.evaluate.return_value = Score(value=100) + exp_cache_handler._result = "Computed result" + + # Execute + await exp_cache_handler.evaluate_experience() + exp_cache_handler.save_experience() + + # Assert + mock_scorer.evaluate.assert_called_once() + mock_exp_manager.create_exp.assert_called_once() + + @pytest.mark.asyncio + async def test_async_function_execution_with_exps(self, exp_cache_handler, mock_exp_manager, mock_func): + # Setup + exp_cache_handler.pass_exps = True + mock_func.return_value = "Async result with exps" + mock_exp_manager.extract_one_perfect_exp.return_value = None + exp_cache_handler._exps = [Experience(req="req", resp="resp")] + + # Execute + await exp_cache_handler.execute_function() + + # Assert + mock_func.assert_called_once_with(exps=exp_cache_handler._exps) + assert exp_cache_handler._result == "Async result with exps" + + def test_sync_function_execution_with_exps(self, mocker, exp_cache_handler, mock_exp_manager, mock_func): + # Setup + exp_cache_handler.func = mocker.Mock(return_value="Sync result with exps") + exp_cache_handler.pass_exps = True + mock_exp_manager.extract_one_perfect_exp.return_value = None + exp_cache_handler._exps = [Experience(req="req", resp="resp")] + + # Execute + asyncio.get_event_loop().run_until_complete(exp_cache_handler.execute_function()) + + # Assert + exp_cache_handler.func.assert_called_once_with(exps=exp_cache_handler._exps) + assert exp_cache_handler._result == "Sync result with exps" + + def test_wrapper_selection_async(self, mocker, exp_cache_handler, mock_func): + # Setup + mock_func = mocker.AsyncMock() + + # Execute + wrapper = ExpCacheHandler.choose_wrapper(mock_func, exp_cache_handler.execute_function) + + # Assert + assert asyncio.iscoroutinefunction(wrapper), "Wrapper should be asynchronous" + + def test_wrapper_selection_sync(self, exp_cache_handler, mocker): + # Setup + sync_func = mocker.Mock() + + # Execute + wrapper = ExpCacheHandler.choose_wrapper(sync_func, exp_cache_handler.execute_function) + + # Assert + assert not asyncio.iscoroutinefunction(wrapper), "Wrapper should be synchronous" + + @pytest.mark.asyncio + async def test_generate_req_identifier(self, exp_cache_handler): + # Setup + exp_cache_handler.func = lambda x: x + exp_cache_handler.args = (42,) + exp_cache_handler.kwargs = {"y": 3.14} + + # Execute + req_id = exp_cache_handler.generate_req_identifier() + + # Assert + expected_id = "_(42,)_{'y': 3.14}" + assert req_id == expected_id, "Request identifier should match the expected format" diff --git a/tests/metagpt/exp_pool/test_manager.py b/tests/metagpt/exp_pool/test_manager.py index a0d7005f5..3e8f47417 100644 --- a/tests/metagpt/exp_pool/test_manager.py +++ b/tests/metagpt/exp_pool/test_manager.py @@ -4,7 +4,7 @@ from metagpt.config2 import Config from metagpt.configs.exp_pool_config import ExperiencePoolConfig from metagpt.configs.llm_config import LLMConfig from metagpt.exp_pool.manager import ExperienceManager -from metagpt.exp_pool.schema import MAX_SCORE, Experience, Metric +from metagpt.exp_pool.schema import MAX_SCORE, Experience, Metric, Score from metagpt.rag.engines import SimpleEngine @@ -62,15 +62,15 @@ class TestExperienceManager: def test_extract_one_perfect_exp(self, mock_experience_manager): experiences = [ - Experience(req="req", resp="resp", metric=Metric(score=MAX_SCORE)), + Experience(req="req", resp="resp", metric=Metric(score=Score(val=MAX_SCORE))), Experience(req="req", resp="resp"), ] perfect_exp: Experience = mock_experience_manager.extract_one_perfect_exp(experiences) assert perfect_exp is not None - assert perfect_exp.metric.score == MAX_SCORE + assert perfect_exp.metric.score.val == MAX_SCORE def test_is_perfect_exp(self): - exp = Experience(req="req", resp="resp", metric=Metric(score=MAX_SCORE)) + exp = Experience(req="req", resp="resp", metric=Metric(score=Score(val=MAX_SCORE))) assert ExperienceManager.is_perfect_exp(exp) == True exp = Experience(req="req", resp="resp") From 0510da5295b3e1183c5c7672b15095cb5c213325 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Wed, 5 Jun 2024 23:26:09 +0800 Subject: [PATCH 023/119] add handle_exception to ensure robustness --- metagpt/exp_pool/decorator.py | 14 ++++++++++++-- metagpt/exp_pool/manager.py | 3 +++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/metagpt/exp_pool/decorator.py b/metagpt/exp_pool/decorator.py index 9eb4d9e61..9cf924779 100644 --- a/metagpt/exp_pool/decorator.py +++ b/metagpt/exp_pool/decorator.py @@ -10,6 +10,7 @@ from metagpt.exp_pool.manager import ExperienceManager, exp_manager from metagpt.exp_pool.schema import Experience, Metric, QueryType, Score from metagpt.exp_pool.scorers import ExperienceScorer, SimpleScorer from metagpt.utils.async_helper import NestAsyncio +from metagpt.utils.exceptions import handle_exception ReturnType = TypeVar("ReturnType") @@ -50,8 +51,7 @@ def exp_cache( return exp await handler.execute_function() - await handler.evaluate_experience() - handler.save_experience() + await handler.process_experience() return handler._result @@ -87,6 +87,16 @@ class ExpCacheHandler(BaseModel): """Execute the function, and save the result.""" self._result = await self._execute_function() + @handle_exception + async def process_experience(self): + """Process experience. + + Evaluates and saves experience. + Use `handle_exception` to ensure robustness, do not stop subsequent operations. + """ + await self.evaluate_experience() + self.save_experience() + async def evaluate_experience(self): """Evaluate the experience, and save the score.""" diff --git a/metagpt/exp_pool/manager.py b/metagpt/exp_pool/manager.py index 58499104d..546086b37 100644 --- a/metagpt/exp_pool/manager.py +++ b/metagpt/exp_pool/manager.py @@ -8,6 +8,7 @@ from metagpt.config2 import Config, config from metagpt.exp_pool.schema import MAX_SCORE, Experience, QueryType from metagpt.rag.engines import SimpleEngine from metagpt.rag.schema import ChromaRetrieverConfig, LLMRankerConfig +from metagpt.utils.exceptions import handle_exception class ExperienceManager(BaseModel): @@ -34,6 +35,7 @@ class ExperienceManager(BaseModel): ) return self + @handle_exception def create_exp(self, exp: Experience): """Adds an experience to the storage if writing is enabled. @@ -45,6 +47,7 @@ class ExperienceManager(BaseModel): self.storage.add_objs([exp]) + @handle_exception(default_return=[]) async def query_exps(self, req: str, tag: str = "", query_type: QueryType = QueryType.SEMANTIC) -> list[Experience]: """Retrieves and filters experiences. From 6f84110e0f0aa7635e400bdf2e31346e1411b42e Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Thu, 6 Jun 2024 20:18:40 +0800 Subject: [PATCH 024/119] update exp_pool example --- examples/exp_pool/simple.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/exp_pool/simple.py b/examples/exp_pool/simple.py index f270824bf..3216e78b8 100644 --- a/examples/exp_pool/simple.py +++ b/examples/exp_pool/simple.py @@ -9,8 +9,7 @@ from metagpt.logs import logger async def main(): req = "Simple task." - resp = "Simple echo." - exp = Experience(req=req, resp=resp, entry_type=EntryType.MANUAL) + exp = Experience(req=req, resp="echo", entry_type=EntryType.MANUAL) exp_manager.create_exp(exp) logger.info(f"New experience created for the request `{req}`.") From faeee22dcb445ba485149d055756324a0950e5fc Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Fri, 7 Jun 2024 10:30:37 +0800 Subject: [PATCH 025/119] update comment --- metagpt/exp_pool/scorers/simple.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/metagpt/exp_pool/scorers/simple.py b/metagpt/exp_pool/scorers/simple.py index d0301cbc2..5779f7fb1 100644 --- a/metagpt/exp_pool/scorers/simple.py +++ b/metagpt/exp_pool/scorers/simple.py @@ -1,4 +1,5 @@ -"""Evalate by llm.""" +"""Simple Scorer.""" + import inspect import json from typing import Any, Callable @@ -57,8 +58,17 @@ class SimpleScorer(ExperienceScorer): llm: BaseLLM = Field(default_factory=LLM) async def evaluate(self, func: Callable, result: Any, args: tuple = None, kwargs: dict = None) -> Score: - """Evaluate the quality of content.""" + """Evaluates the quality of content by LLM. + Args: + func: The function to evaluate. + result: The result produced by the function. + args: The positional arguments used when calling the function, if any. + kwargs: The keyword arguments used when calling the function, if any. + + Returns: + A Score object containing the evaluation results. + """ prompt = SIMPLE_SCORER_TEMPLATE.format( func_name=func.__name__, func_doc=func.__doc__, From f7514632d9304798cda6d469973d53e2d00489cd Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Fri, 7 Jun 2024 14:35:47 +0800 Subject: [PATCH 026/119] update comment --- metagpt/exp_pool/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/exp_pool/manager.py b/metagpt/exp_pool/manager.py index 546086b37..35ee5fdac 100644 --- a/metagpt/exp_pool/manager.py +++ b/metagpt/exp_pool/manager.py @@ -14,7 +14,7 @@ from metagpt.utils.exceptions import handle_exception class ExperienceManager(BaseModel): """ExperienceManager manages the lifecycle of experiences, including CRUD and optimization. - Attributes: + Args: config (Config): Configuration for managing experiences. storage (SimpleEngine): Engine to handle the storage and retrieval of experiences. """ From 790ff5598192005e57c1c0da0dcc6ca887381f10 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Fri, 7 Jun 2024 18:15:23 +0800 Subject: [PATCH 027/119] add scorer example --- examples/exp_pool/{simple.py => manager.py} | 0 examples/exp_pool/scorer.py | 25 +++++++++++++++++++++ metagpt/exp_pool/scorers/simple.py | 4 ++-- 3 files changed, 27 insertions(+), 2 deletions(-) rename examples/exp_pool/{simple.py => manager.py} (100%) create mode 100644 examples/exp_pool/scorer.py diff --git a/examples/exp_pool/simple.py b/examples/exp_pool/manager.py similarity index 100% rename from examples/exp_pool/simple.py rename to examples/exp_pool/manager.py diff --git a/examples/exp_pool/scorer.py b/examples/exp_pool/scorer.py new file mode 100644 index 000000000..1efe07bdf --- /dev/null +++ b/examples/exp_pool/scorer.py @@ -0,0 +1,25 @@ +import asyncio + +from metagpt.exp_pool.scorers import SimpleScorer +from metagpt.logs import logger + + +def echo(req: str): + """Echo from req.""" + + return req + + +async def simple(): + scorer = SimpleScorer() + + score = await scorer.evaluate(echo, "data", ("data",)) + logger.info(f"The score is: {score}") + + +async def main(): + await simple() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/metagpt/exp_pool/scorers/simple.py b/metagpt/exp_pool/scorers/simple.py index 5779f7fb1..84995b60f 100644 --- a/metagpt/exp_pool/scorers/simple.py +++ b/metagpt/exp_pool/scorers/simple.py @@ -10,7 +10,7 @@ from metagpt.exp_pool.schema import Score from metagpt.exp_pool.scorers.base import ExperienceScorer from metagpt.llm import LLM from metagpt.provider.base_llm import BaseLLM -from metagpt.utils.common import parse_json_code_block +from metagpt.utils.common import CodeParser SIMPLE_SCORER_TEMPLATE = """ Role: You're an expert score evaluator. You specialize in assessing the output of the given function, based on its intended requirement and produced result. @@ -78,6 +78,6 @@ class SimpleScorer(ExperienceScorer): func_result=result, ) resp = await self.llm.aask(prompt) - resp_json = json.loads(parse_json_code_block(resp)[0]) + resp_json = json.loads(CodeParser.parse_code(resp, lang="json")) return Score(**resp_json) From 4650b7bdf1a8eff0140ce0e6cd16245548fbeb43 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Tue, 11 Jun 2024 15:40:01 +0800 Subject: [PATCH 028/119] change req in exp --- metagpt/exp_pool/decorator.py | 29 +++++++++++++++++++------- metagpt/exp_pool/manager.py | 3 +-- metagpt/utils/reflection.py | 25 +++++++++++++++++----- tests/metagpt/utils/test_reflection.py | 29 ++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 15 deletions(-) create mode 100644 tests/metagpt/utils/test_reflection.py diff --git a/metagpt/exp_pool/decorator.py b/metagpt/exp_pool/decorator.py index 9cf924779..e559797a3 100644 --- a/metagpt/exp_pool/decorator.py +++ b/metagpt/exp_pool/decorator.py @@ -11,6 +11,7 @@ from metagpt.exp_pool.schema import Experience, Metric, QueryType, Score from metagpt.exp_pool.scorers import ExperienceScorer, SimpleScorer from metagpt.utils.async_helper import NestAsyncio from metagpt.utils.exceptions import handle_exception +from metagpt.utils.reflection import get_class_name ReturnType = TypeVar("ReturnType") @@ -43,7 +44,7 @@ def exp_cache( kwargs=kwargs, exp_manager=manager or exp_manager, exp_scorer=scorer or SimpleScorer(), - pass_exps=pass_exps_to_func, + pass_exps_to_func=pass_exps_to_func, ) await handler.fetch_experiences(query_type) @@ -68,16 +69,17 @@ class ExpCacheHandler(BaseModel): kwargs: Any exp_manager: ExperienceManager exp_scorer: ExperienceScorer - pass_exps: bool + pass_exps_to_func: bool = False _exps: list[Experience] = None _result: Any = None _score: Score = None + _req: str = None async def fetch_experiences(self, query_type: QueryType): """Fetch a potentially perfect existing experience.""" - req = self.generate_req_identifier() + req = self._get_req_identifier() self._exps = await self.exp_manager.query_exps(req, query_type=query_type) def get_one_perfect_experience(self) -> Optional[Experience]: @@ -105,15 +107,26 @@ class ExpCacheHandler(BaseModel): def save_experience(self): """Save the new experience.""" - req = self.generate_req_identifier() + req = self._get_req_identifier() exp = Experience(req=req, resp=self._result, metric=Metric(score=self._score)) self.exp_manager.create_exp(exp) - def generate_req_identifier(self): - """Generate a unique request identifier based on the function and its arguments.""" + def _get_req_identifier(self): + """Generate a unique request identifier based on the function and its arguments. - return f"{self.func.__name__}_{self.args}_{self.kwargs}" + Result Example: + - "write_prd-('2048',)-{}" + - "WritePRD.run-('2048',)-{}" + """ + if not self._req: + cls_name = get_class_name(self.func, *self.args) + func_name = f"{cls_name}.{self.func.__name__}" if cls_name else self.func.__name__ + args = self.args[1:] if cls_name and len(self.args) >= 1 else self.args + + self._req = f"{func_name}-{args}-{self.kwargs}" + + return self._req @staticmethod def choose_wrapper(func, wrapped_func): @@ -129,7 +142,7 @@ class ExpCacheHandler(BaseModel): return async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper async def _execute_function(self): - if self.pass_exps: + if self.pass_exps_to_func: return await self._execute_function_with_exps() return await self._execute_function_without_exps() diff --git a/metagpt/exp_pool/manager.py b/metagpt/exp_pool/manager.py index 35ee5fdac..7382fe8f1 100644 --- a/metagpt/exp_pool/manager.py +++ b/metagpt/exp_pool/manager.py @@ -7,7 +7,7 @@ from pydantic import BaseModel, ConfigDict, model_validator from metagpt.config2 import Config, config from metagpt.exp_pool.schema import MAX_SCORE, Experience, QueryType from metagpt.rag.engines import SimpleEngine -from metagpt.rag.schema import ChromaRetrieverConfig, LLMRankerConfig +from metagpt.rag.schema import ChromaRetrieverConfig from metagpt.utils.exceptions import handle_exception @@ -31,7 +31,6 @@ class ExperienceManager(BaseModel): retriever_configs=[ ChromaRetrieverConfig(collection_name="experience_pool", persist_path=".chroma_exp_data") ], - ranker_configs=[LLMRankerConfig()], ) return self diff --git a/metagpt/utils/reflection.py b/metagpt/utils/reflection.py index 2683e5657..9b10a4b3e 100644 --- a/metagpt/utils/reflection.py +++ b/metagpt/utils/reflection.py @@ -19,9 +19,24 @@ def check_methods(C, *methods): return True -def get_func_full_name(func, *args) -> str: - if inspect.ismethod(func) or (inspect.isfunction(func) and "self" in inspect.signature(func).parameters): - cls_name = args[0].__class__.__name__ - return f"{func.__module__}.{cls_name}.{func.__name__}" +def get_class_name(func, *args) -> str: + """Returns the class name of the object that a method belongs to. - return f"{func.__module__}.{func.__name__}" + - If `func` is a bound method, extracts the class name directly from the method. + - If `func` is an unbound method and `args` are provided, assumes the first argument is `self` and extracts the class name. + - Returns an empty string if neither condition is met. + """ + if inspect.ismethod(func): + return func.__self__.__class__.__name__ + + if inspect.isfunction(func) and "self" in inspect.signature(func).parameters and args: + return args[0].__class__.__name__ + + return "" + + +def get_func_or_method_name(func, *args) -> str: + """Function name, or method name with class name.""" + cls_name = get_class_name(func, *args) + + return f"{cls_name}.{func.__name__}" if cls_name else f"{func.__name__}" diff --git a/tests/metagpt/utils/test_reflection.py b/tests/metagpt/utils/test_reflection.py new file mode 100644 index 000000000..e78e1b400 --- /dev/null +++ b/tests/metagpt/utils/test_reflection.py @@ -0,0 +1,29 @@ +from metagpt.utils.reflection import get_func_or_method_name + + +def simple_function(): + pass + + +class SampleClass: + def method(self): + pass + + +class TestFunctionOrMethodName: + def test_simple_function(self): + assert get_func_or_method_name(simple_function) == "simple_function" + + def test_class_method_without_args(self): + sample_instance = SampleClass() + assert get_func_or_method_name(sample_instance.method) == "SampleClass.method" + + def test_class_method_with_args(self): + sample_instance = SampleClass() + assert get_func_or_method_name(SampleClass.method, sample_instance) == "SampleClass.method" + + def test_function_with_no_args(self): + assert get_func_or_method_name(simple_function) == "simple_function" + + def test_method_without_instance(self): + assert get_func_or_method_name(SampleClass.method) == "method" From 6052d8b9ac8095514e07dd58b4c64f46f238f693 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Tue, 11 Jun 2024 21:40:51 +0800 Subject: [PATCH 029/119] update exp_pool decorator --- metagpt/exp_pool/decorator.py | 61 ++++++++---- metagpt/exp_pool/manager.py | 3 +- metagpt/utils/reflection.py | 25 +++-- tests/metagpt/exp_pool/test_decorator.py | 112 +++++++++++++++++++---- tests/metagpt/utils/test_reflection.py | 46 ++++++---- 5 files changed, 173 insertions(+), 74 deletions(-) diff --git a/metagpt/exp_pool/decorator.py b/metagpt/exp_pool/decorator.py index e559797a3..446220a47 100644 --- a/metagpt/exp_pool/decorator.py +++ b/metagpt/exp_pool/decorator.py @@ -2,9 +2,11 @@ import asyncio import functools +import inspect +import json from typing import Any, Callable, Optional, TypeVar -from pydantic import BaseModel, ConfigDict +from pydantic import BaseModel, ConfigDict, model_validator from metagpt.exp_pool.manager import ExperienceManager, exp_manager from metagpt.exp_pool.schema import Experience, Metric, QueryType, Score @@ -42,8 +44,8 @@ def exp_cache( func=func, args=args, kwargs=kwargs, - exp_manager=manager or exp_manager, - exp_scorer=scorer or SimpleScorer(), + exp_manager=manager, + exp_scorer=scorer, pass_exps_to_func=pass_exps_to_func, ) @@ -67,8 +69,8 @@ class ExpCacheHandler(BaseModel): func: Callable args: Any kwargs: Any - exp_manager: ExperienceManager - exp_scorer: ExperienceScorer + exp_manager: Optional[ExperienceManager] = None + exp_scorer: Optional[ExperienceScorer] = None pass_exps_to_func: bool = False _exps: list[Experience] = None @@ -76,11 +78,22 @@ class ExpCacheHandler(BaseModel): _score: Score = None _req: str = None + @model_validator(mode="after") + def initialize(self): + if self.exp_manager is None: + self.exp_manager = exp_manager + + if self.exp_scorer is None: + self.exp_scorer = SimpleScorer() + + self._req = self.generate_req_identifier(self.func, *self.args, **self.kwargs) + + return self + async def fetch_experiences(self, query_type: QueryType): """Fetch a potentially perfect existing experience.""" - req = self._get_req_identifier() - self._exps = await self.exp_manager.query_exps(req, query_type=query_type) + self._exps = await self.exp_manager.query_exps(self._req, query_type=query_type) def get_one_perfect_experience(self) -> Optional[Experience]: return self.exp_manager.extract_one_perfect_exp(self._exps) @@ -107,26 +120,29 @@ class ExpCacheHandler(BaseModel): def save_experience(self): """Save the new experience.""" - req = self._get_req_identifier() - exp = Experience(req=req, resp=self._result, metric=Metric(score=self._score)) + exp = Experience(req=self._req, resp=self._result, metric=Metric(score=self._score)) self.exp_manager.create_exp(exp) - def _get_req_identifier(self): - """Generate a unique request identifier based on the function and its arguments. + @classmethod + def generate_req_identifier(cls, func, *args, **kwargs) -> str: + """Generate a unique request identifier for any given function and its arguments. - Result Example: - - "write_prd-('2048',)-{}" - - "WritePRD.run-('2048',)-{}" + Serializing args and kwargs into JSON strings and replacing ',' with '~' and ':' with '!'. + + Return Example: + SimpleClass.test_method@[1~2]@{"c"!3} """ - if not self._req: - cls_name = get_class_name(self.func, *self.args) - func_name = f"{cls_name}.{self.func.__name__}" if cls_name else self.func.__name__ - args = self.args[1:] if cls_name and len(self.args) >= 1 else self.args + cls_name = get_class_name(func) + func_name = f"{cls_name}.{func.__name__}" if cls_name else func.__name__ - self._req = f"{func_name}-{args}-{self.kwargs}" + if cls_name and args and inspect.isfunction(func): + args = args[1:] - return self._req + args = cls._serialize_and_replace(args) + kwargs = cls._serialize_and_replace(kwargs) + + return f"{func_name}@{args}@{kwargs}" @staticmethod def choose_wrapper(func, wrapped_func): @@ -141,6 +157,11 @@ class ExpCacheHandler(BaseModel): return async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper + @classmethod + def _serialize_and_replace(cls, data): + json_str = json.dumps(data) + return json_str.replace(", ", "~").replace(": ", "!") + async def _execute_function(self): if self.pass_exps_to_func: return await self._execute_function_with_exps() diff --git a/metagpt/exp_pool/manager.py b/metagpt/exp_pool/manager.py index 7382fe8f1..35ee5fdac 100644 --- a/metagpt/exp_pool/manager.py +++ b/metagpt/exp_pool/manager.py @@ -7,7 +7,7 @@ from pydantic import BaseModel, ConfigDict, model_validator from metagpt.config2 import Config, config from metagpt.exp_pool.schema import MAX_SCORE, Experience, QueryType from metagpt.rag.engines import SimpleEngine -from metagpt.rag.schema import ChromaRetrieverConfig +from metagpt.rag.schema import ChromaRetrieverConfig, LLMRankerConfig from metagpt.utils.exceptions import handle_exception @@ -31,6 +31,7 @@ class ExperienceManager(BaseModel): retriever_configs=[ ChromaRetrieverConfig(collection_name="experience_pool", persist_path=".chroma_exp_data") ], + ranker_configs=[LLMRankerConfig()], ) return self diff --git a/metagpt/utils/reflection.py b/metagpt/utils/reflection.py index 9b10a4b3e..fe852635f 100644 --- a/metagpt/utils/reflection.py +++ b/metagpt/utils/reflection.py @@ -19,24 +19,23 @@ def check_methods(C, *methods): return True -def get_class_name(func, *args) -> str: +def get_class_name(func) -> str: """Returns the class name of the object that a method belongs to. - - If `func` is a bound method, extracts the class name directly from the method. - - If `func` is an unbound method and `args` are provided, assumes the first argument is `self` and extracts the class name. - - Returns an empty string if neither condition is met. + - If `func` is a bound method or a class method, extracts the class name directly from the method. + - Returns an empty string if it's a regular function or cannot determine the class. """ if inspect.ismethod(func): + if inspect.isclass(func.__self__): + return func.__self__.__name__ + return func.__self__.__class__.__name__ - if inspect.isfunction(func) and "self" in inspect.signature(func).parameters and args: - return args[0].__class__.__name__ + if inspect.isfunction(func): + qualname_parts = func.__qualname__.split(".") + if len(qualname_parts) > 1: + class_name = qualname_parts[-2] + if class_name.isidentifier(): + return class_name return "" - - -def get_func_or_method_name(func, *args) -> str: - """Function name, or method name with class name.""" - cls_name = get_class_name(func, *args) - - return f"{cls_name}.{func.__name__}" if cls_name else f"{func.__name__}" diff --git a/tests/metagpt/exp_pool/test_decorator.py b/tests/metagpt/exp_pool/test_decorator.py index 508229d18..bedc4e391 100644 --- a/tests/metagpt/exp_pool/test_decorator.py +++ b/tests/metagpt/exp_pool/test_decorator.py @@ -1,14 +1,28 @@ import asyncio +import inspect import pytest -from metagpt.exp_pool.decorator import ExpCacheHandler +from metagpt.exp_pool.decorator import ExpCacheHandler, exp_cache from metagpt.exp_pool.manager import ExperienceManager from metagpt.exp_pool.schema import Experience, QueryType, Score from metagpt.exp_pool.scorers import SimpleScorer from metagpt.rag.engines import SimpleEngine +def for_test_function(a, b, c=None): + return a + b if c is None else a + b + c + + +class ForTestClass: + def for_test_method(self, x, y): + return x * y + + @classmethod + def for_test_class_method(cls, x, y): + return x**y + + class TestExpCache: @pytest.fixture def mock_func(self, mocker): @@ -46,7 +60,7 @@ class TestExpCache: perfect_exp = Experience(req="req", resp="resp") mock_exp_manager.extract_one_perfect_exp.return_value = perfect_exp - # Execute + # Exec exp_cache_handler._exps = [perfect_exp] # Simulate fetched experiences result = exp_cache_handler.get_one_perfect_experience() @@ -60,7 +74,7 @@ class TestExpCache: mock_exp_manager.extract_one_perfect_exp.return_value = None mock_func.return_value = "Computed result" - # Execute + # Exec await exp_cache_handler.execute_function() # Assert @@ -73,7 +87,7 @@ class TestExpCache: mock_scorer.evaluate.return_value = Score(value=100) exp_cache_handler._result = "Computed result" - # Execute + # Exec await exp_cache_handler.evaluate_experience() exp_cache_handler.save_experience() @@ -84,12 +98,12 @@ class TestExpCache: @pytest.mark.asyncio async def test_async_function_execution_with_exps(self, exp_cache_handler, mock_exp_manager, mock_func): # Setup - exp_cache_handler.pass_exps = True + exp_cache_handler.pass_exps_to_func = True mock_func.return_value = "Async result with exps" mock_exp_manager.extract_one_perfect_exp.return_value = None exp_cache_handler._exps = [Experience(req="req", resp="resp")] - # Execute + # Exec await exp_cache_handler.execute_function() # Assert @@ -99,11 +113,11 @@ class TestExpCache: def test_sync_function_execution_with_exps(self, mocker, exp_cache_handler, mock_exp_manager, mock_func): # Setup exp_cache_handler.func = mocker.Mock(return_value="Sync result with exps") - exp_cache_handler.pass_exps = True + exp_cache_handler.pass_exps_to_func = True mock_exp_manager.extract_one_perfect_exp.return_value = None exp_cache_handler._exps = [Experience(req="req", resp="resp")] - # Execute + # Exec asyncio.get_event_loop().run_until_complete(exp_cache_handler.execute_function()) # Assert @@ -114,7 +128,7 @@ class TestExpCache: # Setup mock_func = mocker.AsyncMock() - # Execute + # Exec wrapper = ExpCacheHandler.choose_wrapper(mock_func, exp_cache_handler.execute_function) # Assert @@ -124,22 +138,80 @@ class TestExpCache: # Setup sync_func = mocker.Mock() - # Execute + # Exec wrapper = ExpCacheHandler.choose_wrapper(sync_func, exp_cache_handler.execute_function) # Assert assert not asyncio.iscoroutinefunction(wrapper), "Wrapper should be synchronous" - @pytest.mark.asyncio - async def test_generate_req_identifier(self, exp_cache_handler): - # Setup - exp_cache_handler.func = lambda x: x - exp_cache_handler.args = (42,) - exp_cache_handler.kwargs = {"y": 3.14} + @pytest.mark.parametrize( + "func, args, kwargs, expected", + [ + (for_test_function, (1, 2), {"c": 3}, 'for_test_function@[1~2]@{"c"!3}'), + (ForTestClass().for_test_method, (4, 5), {}, "ForTestClass.for_test_method@[4~5]@{}"), + (ForTestClass.for_test_class_method, (6, 7), {}, "ForTestClass.for_test_class_method@[6~7]@{}"), + (for_test_function, (), {}, "for_test_function@[]@{}"), + ( + for_test_function, + ("hello", [1, 2]), + {"key": "value"}, + 'for_test_function@["hello"~[1~2]]@{"key"!"value"}', + ), + ], + ) + def test_generate_req_identifier(self, func, args, kwargs, expected): + req_identifier = ExpCacheHandler.generate_req_identifier(func, *args, **kwargs) + assert req_identifier == expected - # Execute - req_id = exp_cache_handler.generate_req_identifier() + @pytest.mark.asyncio + async def test_exp_cache_with_perfect_experience(self, mocker, mock_exp_manager): + # Mock perfect experience + perfect_exp = Experience(req="test_req", resp="perfect_response") + mock_exp_manager.query_exps = mocker.AsyncMock(return_value=[perfect_exp]) + mock_exp_manager.extract_one_perfect_exp = mocker.MagicMock(return_value=perfect_exp) + async_mock_func = mocker.AsyncMock() + + # Setup + decorated_func = exp_cache(async_mock_func, manager=mock_exp_manager) + + # Exec + result: Experience = await decorated_func() # Assert - expected_id = "_(42,)_{'y': 3.14}" - assert req_id == expected_id, "Request identifier should match the expected format" + assert result.resp == "perfect_response", "Should return the perfect experience response" + async_mock_func.assert_not_called() + + @pytest.mark.asyncio + async def test_exp_cache_without_perfect_experience(self, mocker, mock_exp_manager): + # Mock + mock_exp_manager.query_exps = mocker.AsyncMock(return_value=[]) + mock_exp_manager.extract_one_perfect_exp = mocker.MagicMock(return_value=None) + async_mock_func = mocker.AsyncMock(return_value="computed_response") + async_mock_func.__signature__ = inspect.signature(for_test_function) + + # Setup + decorated_func = exp_cache(async_mock_func, manager=mock_exp_manager) + + # Exec + result = await decorated_func() + + # Assert + assert result == "computed_response", "Should execute and return the function's response" + async_mock_func.assert_called_once() + + @pytest.mark.asyncio + async def test_exp_cache_saves_new_experience(self, mocker, mock_exp_manager, mock_scorer): + # Mock + mock_exp_manager.query_exps = mocker.AsyncMock(return_value=[]) + mock_exp_manager.extract_one_perfect_exp = mocker.MagicMock(return_value=None) + async_mock_func = mocker.AsyncMock(return_value="computed_response") + mock_scorer.evaluate = mocker.AsyncMock(return_value=Score(value=100)) + + # Setup + decorated_func = exp_cache(async_mock_func, manager=mock_exp_manager, scorer=mock_scorer) + + # Exec + await decorated_func() + + # Assert + mock_exp_manager.create_exp.assert_called_once() diff --git a/tests/metagpt/utils/test_reflection.py b/tests/metagpt/utils/test_reflection.py index e78e1b400..58fd81619 100644 --- a/tests/metagpt/utils/test_reflection.py +++ b/tests/metagpt/utils/test_reflection.py @@ -1,29 +1,35 @@ -from metagpt.utils.reflection import get_func_or_method_name +from metagpt.utils.reflection import get_class_name -def simple_function(): - pass - - -class SampleClass: - def method(self): +class SimpleFunction: + def function(self): pass -class TestFunctionOrMethodName: - def test_simple_function(self): - assert get_func_or_method_name(simple_function) == "simple_function" +class SampleClass: + @classmethod + def class_method(cls): + pass - def test_class_method_without_args(self): - sample_instance = SampleClass() - assert get_func_or_method_name(sample_instance.method) == "SampleClass.method" + def instance_method(self): + pass - def test_class_method_with_args(self): - sample_instance = SampleClass() - assert get_func_or_method_name(SampleClass.method, sample_instance) == "SampleClass.method" - def test_function_with_no_args(self): - assert get_func_or_method_name(simple_function) == "simple_function" +def standalone_function(): + pass - def test_method_without_instance(self): - assert get_func_or_method_name(SampleClass.method) == "method" + +class TestGetClassName: + def test_instance_method(self): + instance = SampleClass() + assert get_class_name(instance.instance_method) == "SampleClass" + + def test_class_method(self): + assert get_class_name(SampleClass.class_method) == "SampleClass" + + def test_standalone_function(self): + assert get_class_name(standalone_function) == "" + + def test_function_within_simple_class(self): + instance = SimpleFunction() + assert get_class_name(instance.function) == "SimpleFunction" From 0c4927f7246399bcb100fc91a63dbfae2793e430 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Wed, 12 Jun 2024 11:01:46 +0800 Subject: [PATCH 030/119] update comment --- metagpt/exp_pool/decorator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/metagpt/exp_pool/decorator.py b/metagpt/exp_pool/decorator.py index 446220a47..2a3bf2fba 100644 --- a/metagpt/exp_pool/decorator.py +++ b/metagpt/exp_pool/decorator.py @@ -91,11 +91,12 @@ class ExpCacheHandler(BaseModel): return self async def fetch_experiences(self, query_type: QueryType): - """Fetch a potentially perfect existing experience.""" + """Fetch experiences by query_type.""" self._exps = await self.exp_manager.query_exps(self._req, query_type=query_type) def get_one_perfect_experience(self) -> Optional[Experience]: + """Get a potentially perfect experience.""" return self.exp_manager.extract_one_perfect_exp(self._exps) async def execute_function(self): From a2f809263a4232367ce03397788bf0dc89108953 Mon Sep 17 00:00:00 2001 From: lidanyang Date: Thu, 27 Jun 2024 11:22:12 +0800 Subject: [PATCH 031/119] ignore warning info --- metagpt/actions/di/execute_nb_code.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/metagpt/actions/di/execute_nb_code.py b/metagpt/actions/di/execute_nb_code.py index 64620d9cc..9a9f0483a 100644 --- a/metagpt/actions/di/execute_nb_code.py +++ b/metagpt/actions/di/execute_nb_code.py @@ -30,6 +30,12 @@ from metagpt.logs import logger from metagpt.utils.report import NotebookReporter INSTALL_KEEPLEN = 500 +INI_CODE = """import warnings +import logging + +root_logger = logging.getLogger() +root_logger.setLevel(logging.ERROR) +warnings.filterwarnings('ignore')""" class RealtimeOutputNotebookClient(NotebookClient): @@ -79,6 +85,10 @@ class ExecuteNbCode(Action): ) self.reporter = NotebookReporter() self.set_nb_client() + asyncio.run(self._init_code()) + + async def _init_code(self): + await self.run(INI_CODE) def set_nb_client(self): self.nb_client = RealtimeOutputNotebookClient( @@ -175,6 +185,8 @@ class ExecuteNbCode(Action): is_success = False output_text = remove_escape_and_color_codes(output_text) + if is_success: + output_text = remove_log_and_warning_lines(output_text) # The useful information of the exception is at the end, # the useful information of normal output is at the begining. output_text = output_text[:keep_len] if is_success else output_text[-keep_len:] @@ -268,6 +280,18 @@ class ExecuteNbCode(Action): return outputs, success +def remove_log_and_warning_lines(input_str: str) -> str: + delete_lines = ["[warning]", "warning:", "[cv]", "[info]"] + result = "\n".join( + [ + line + for line in input_str.split("\n") + if not any(dl in line.lower() for dl in delete_lines) + ] + ).strip() + return result + + def remove_escape_and_color_codes(input_str: str): # 使用正则表达式去除jupyter notebook输出结果中的转义字符和颜色代码 # Use regular expressions to get rid of escape characters and color codes in jupyter notebook output. From ddaecf12eb86574d93f49f28c5f4be89ecaee55d Mon Sep 17 00:00:00 2001 From: lidanyang Date: Thu, 27 Jun 2024 11:23:00 +0800 Subject: [PATCH 032/119] apply data_analyst to role_zero --- metagpt/prompts/di/role_zero.py | 2 +- metagpt/prompts/di/write_analysis_code.py | 5 +- metagpt/roles/di/data_analyst.py | 178 ++++++++-------------- metagpt/roles/di/role_zero.py | 11 +- metagpt/tools/tool_recommend.py | 4 +- 5 files changed, 83 insertions(+), 117 deletions(-) diff --git a/metagpt/prompts/di/role_zero.py b/metagpt/prompts/di/role_zero.py index 4d52476aa..04344fa1e 100644 --- a/metagpt/prompts/di/role_zero.py +++ b/metagpt/prompts/di/role_zero.py @@ -5,7 +5,7 @@ When presented a current task, tackle the task using the available commands. Pay close attention to new user message, review the conversation history, use RoleZero.reply_to_human to respond to new user requirement. Note: 1. If you keeping encountering errors, unexpected situation, or you are not sure of proceeding, use RoleZero.ask_human to ask for help. -2. Carefully review your progress at the current task, if your actions so far has not fulfilled the task instruction, you should continue with current task. Otherwise, finish current task. +2. Carefully review your progress at the current task, if your actions so far has not fulfilled the task instruction, you should continue with current task. Otherwise, finish current task by Plan.finish_current_task. 3. Each time you finish a task, use RoleZero.reply_to_human to report your progress. """ diff --git a/metagpt/prompts/di/write_analysis_code.py b/metagpt/prompts/di/write_analysis_code.py index af941808d..1d743a719 100644 --- a/metagpt/prompts/di/write_analysis_code.py +++ b/metagpt/prompts/di/write_analysis_code.py @@ -28,7 +28,10 @@ your code ``` """ -REFLECTION_SYSTEM_MSG = """You are an AI Python assistant. You will be given your previous implementation code of a task, runtime error results, and a hint to change the implementation appropriately. Write your full implementation.""" +REFLECTION_SYSTEM_MSG = """ +You are an AI Python assistant. You will be given your previous implementation code of a task, runtime error results, and a hint to change the implementation appropriately. Write your full implementation. +When occuring ModuleNotFoundError, always install the required package. And use Terminal tool if available. +""" DEBUG_REFLECTION_EXAMPLE = ''' [previous impl]: diff --git a/metagpt/roles/di/data_analyst.py b/metagpt/roles/di/data_analyst.py index d4d67742b..f13fc78fb 100644 --- a/metagpt/roles/di/data_analyst.py +++ b/metagpt/roles/di/data_analyst.py @@ -1,134 +1,88 @@ from __future__ import annotations -import json -from typing import Literal +from pydantic import Field -from pydantic import model_validator - -from metagpt.actions import Action +from metagpt.actions.di.execute_nb_code import ExecuteNbCode from metagpt.actions.di.write_analysis_code import WriteAnalysisCode from metagpt.logs import logger -from metagpt.prompts.di.data_analyst import CMD_PROMPT -from metagpt.roles.di.data_interpreter import DataInterpreter -from metagpt.schema import Message, TaskResult -from metagpt.strategy.experience_retriever import KeywordExpRetriever -from metagpt.strategy.planner import Planner -from metagpt.strategy.thinking_command import ( - Command, - prepare_command_prompt, - run_commands, -) -from metagpt.tools.tool_recommend import BM25ToolRecommender -from metagpt.utils.common import CodeParser -from metagpt.utils.report import ThoughtReporter +from metagpt.roles.di.role_zero import RoleZero +from metagpt.schema import TaskResult, Message +from metagpt.tools.tool_registry import register_tool -class DataAnalyst(DataInterpreter): +@register_tool(include_functions=["write_and_exec_code"]) +class DataAnalyst(RoleZero): name: str = "David" profile: str = "DataAnalyst" goal: str = "Take on any data-related tasks, such as data analysis, machine learning, deep learning, web browsing, web scraping, web searching, web deployment, terminal operation, git and github operation, etc." - react_mode: Literal["react"] = "react" - max_react_loop: int = 20 # used for react mode + + tools: list[str] = ["Plan", "DataAnalyst", "RoleZero"] + custom_tools: list[str] = ["machine learning", "web scraping", "Terminal"] + + use_reflection: bool = True + write_code: WriteAnalysisCode = Field(default_factory=WriteAnalysisCode, exclude=True) + execute_code: ExecuteNbCode = Field(default_factory=ExecuteNbCode, exclude=True) task_result: TaskResult = None - available_commands: list[Command] = [ - Command.APPEND_TASK, - Command.RESET_TASK, - Command.REPLACE_TASK, - Command.FINISH_CURRENT_TASK, - # Command.PUBLISH_MESSAGE, - Command.ASK_HUMAN, - Command.REPLY_TO_HUMAN, - # Command.PASS, - ] - commands: list[dict] = [] # issued commands to be executed - user_requirement: str = "" - @model_validator(mode="after") - def set_plan_and_tool(self) -> "DataInterpreter": - # We force using this parameter for DataAnalyst - assert self.react_mode == "react" - assert self.auto_run - assert self.use_plan + def _update_tool_execution(self): + self.tool_execution_map = { + "Plan.append_task": self.planner.plan.append_task, + "Plan.reset_task": self.planner.plan.reset_task, + "Plan.replace_task": self.planner.plan.replace_task, + "DataAnalyst.write_and_exec_code": self.write_and_exec_code, + "RoleZero.ask_human": self.ask_human, + "RoleZero.reply_to_human": self.reply_to_human, + } - # Roughly the same part as DataInterpreter.set_plan_and_tool - self._set_react_mode(react_mode=self.react_mode, max_react_loop=self.max_react_loop, auto_run=self.auto_run) - if self.tools and not self.tool_recommender: - self.tool_recommender = BM25ToolRecommender(tools=self.tools) - self.set_actions([WriteAnalysisCode]) + async def write_and_exec_code(self): + """Write a code block for current task and execute it in an interactive notebook environment.""" + counter = 0 + success = False - # HACK: Init Planner, control it through dynamic thinking; Consider formalizing as a react mode - self.planner = Planner(goal="", working_memory=self.rc.working_memory, auto_run=True) + # plan info + plan_status = self.planner.get_plan_status() - return self + # tool info + if self.custom_tool_recommender: + plan = self.planner.plan + fix = ["Terminal"] if "Terminal" in self.custom_tools else None + tool_info = await self.custom_tool_recommender.get_recommended_tool_info(fix=fix, plan=plan) + else: + tool_info = "" - async def _think(self) -> bool: - """Useful in 'react' mode. Use LLM to decide whether and what to do next.""" - self._set_state(0) - example = "" - if not self.planner.plan.goal: - self.user_requirement = self.get_memories()[-1].content - self.planner.plan.goal = self.user_requirement - example = KeywordExpRetriever().retrieve(self.user_requirement) + while not success and counter < 3: + ### write code ### + logger.info(f"ready to WriteAnalysisCode") + use_reflection = (counter > 0 and self.use_reflection) # only use reflection after the first trial - plan_status = self.planner.plan.model_dump(include=["goal", "tasks"]) - # for task in plan_status["tasks"]: - # task.pop("code") - # task.pop("result") - prompt = CMD_PROMPT.format( - plan_status=plan_status, - example=example, - available_commands=prepare_command_prompt(self.available_commands), - ) - context = self.llm.format_msg(self.working_memory.get() + [Message(content=prompt, role="user")]) - # print(*context, sep="\n" + "*" * 5 + "\n") - async with ThoughtReporter(enable_llm_stream=True): - rsp = await self.llm.aask(context) - self.commands = json.loads(CodeParser.parse_code(block=None, lang='json', text=rsp)) - self.rc.working_memory.add(Message(content=rsp, role="assistant")) + code = await self.write_code.run( + user_requirement=self.planner.plan.goal, + plan_status=plan_status, + tool_info=tool_info, + working_memory=self.rc.working_memory.get() if use_reflection else None, + use_reflection=use_reflection, + ) + self.rc.working_memory.add(Message(content=code, role="assistant", cause_by=WriteAnalysisCode)) - await run_commands(self, self.commands, self.rc.working_memory) + ### execute code ### + result, success = await self.execute_code.run(code) + print(result) - return bool(self.rc.todo) + self.rc.working_memory.add(Message(content=result, role="user", cause_by=ExecuteNbCode)) - async def _act(self) -> Message: - """Useful in 'react' mode. Return a Message conforming to Role._act interface.""" - logger.info(f"ready to take on task {self.planner.plan.current_task}") + ### process execution result ### + counter += 1 + self.task_result = TaskResult(code=code, result=result, is_success=success) - # TODO: Consider an appropriate location to insert task experience formally - experience = KeywordExpRetriever().retrieve(self.planner.plan.current_task.instruction, exp_type="task") - if experience and experience not in [msg.content for msg in self.rc.working_memory.get()]: - exp_msg = Message(content=experience, role="assistant") - self.rc.working_memory.add(exp_msg) + output = f""" + Code written: + {code} + Execution status:{'Success' if success else 'Failed'} + Execution result: {result} + """ + self.rc.working_memory.clear() + return output - code, result, is_success = await self._write_and_exec_code() - self.planner.plan.current_task.is_success = ( - is_success # mark is_success, determine is_finished later in thinking - ) - - # FIXME: task result is always overwritten by the last act, whereas it can be made of of multiple acts - self.task_result = TaskResult(code=code, result=result, is_success=is_success) - return Message(content="Task completed", role="assistant", sent_from=self._setting, cause_by=WriteAnalysisCode) - - async def _react(self) -> Message: - # NOTE: Diff 1: Each time landing here means observing news, set todo to allow news processing in _think - self._set_state(0) - - actions_taken = 0 - rsp = Message(content="No actions taken yet", cause_by=Action) # will be overwritten after Role _act - while actions_taken < self.rc.max_react_loop: - # NOTE: Diff 2: Keep observing within _react, news will go into memory, allowing adapting to new info - # add news from self._observe, the one called in self.run, consider removing when switching from working_memory to memory - self.working_memory.add_batch(self.rc.news) - await self._observe() - # add news from this self._observe, we need twice because _observe rewrites rc.news - self.working_memory.add_batch(self.rc.news) - - # think - has_todo = await self._think() - if not has_todo: - break - # act - logger.debug(f"{self._setting}: {self.rc.state=}, will do {self.rc.todo}") - rsp = await self._act() - actions_taken += 1 - return rsp # return output from the last action + def _finish_current_task(self): + self.planner.current_task.update_task_result(self.task_result) + super()._finish_current_task() diff --git a/metagpt/roles/di/role_zero.py b/metagpt/roles/di/role_zero.py index 39338471a..4225d1f76 100644 --- a/metagpt/roles/di/role_zero.py +++ b/metagpt/roles/di/role_zero.py @@ -41,8 +41,10 @@ class RoleZero(Role): max_react_loop: int = 20 # used for react mode # Tools - tools: list[str] = [] # Use special symbol [""] to indicate use of all registered tools + tools: list[str] = [] tool_recommender: ToolRecommender = None + custom_tools: list[str] = [] + custom_tool_recommender: ToolRecommender = None tool_execution_map: dict[str, Callable] = {} special_tool_commands: list[str] = ["Plan.finish_current_task", "end"] # Equipped with three basic tools by default for optional use @@ -68,6 +70,8 @@ class RoleZero(Role): self._set_react_mode(react_mode=self.react_mode, max_react_loop=self.max_react_loop) if self.tools and not self.tool_recommender: self.tool_recommender = BM25ToolRecommender(tools=self.tools, force=True) + if self.custom_tools and not self.custom_tool_recommender: + self.custom_tool_recommender = BM25ToolRecommender(tools=self.custom_tools) self.set_actions([RunCommand]) # HACK: Init Planner, control it through dynamic thinking; Consider formalizing as a react mode @@ -235,13 +239,16 @@ class RoleZero(Role): if cmd["command_name"] == "Plan.finish_current_task" and not self.planner.plan.is_plan_finished(): # task_result = TaskResult(code=str(commands), result=outputs, is_success=is_success) # self.planner.plan.current_task.update_task_result(task_result=task_result) - self.planner.plan.finish_current_task() + self._finish_current_task() elif cmd["command_name"] == "end": self._set_state(-1) return is_special_cmd + def _finish_current_task(self): + self.planner.plan.finish_current_task() + def _get_plan_status(self) -> Tuple[str, str]: plan_status = self.planner.plan.model_dump(include=["goal", "tasks"]) for task in plan_status["tasks"]: diff --git a/metagpt/tools/tool_recommend.py b/metagpt/tools/tool_recommend.py index 05e8e1400..ab847d10e 100644 --- a/metagpt/tools/tool_recommend.py +++ b/metagpt/tools/tool_recommend.py @@ -101,7 +101,7 @@ class ToolRecommender(BaseModel): return ranked_tools - async def get_recommended_tool_info(self, **kwargs) -> str: + async def get_recommended_tool_info(self, fix: list[str] = None, **kwargs) -> str: """ Wrap recommended tools with their info in a string, which can be used directly in a prompt. """ @@ -109,6 +109,8 @@ class ToolRecommender(BaseModel): if not recommended_tools: return "" tool_schemas = {tool.name: tool.schemas for tool in recommended_tools} + if fix: + tool_schemas.update({tool.name: tool.schemas for tool in self.tools.values() if tool.name in fix}) return TOOL_INFO_PROMPT.format(tool_schemas=tool_schemas) async def recall_tools(self, context: str = "", plan: Plan = None, topk: int = 20) -> list[Tool]: From a5b94af82fcd5e63172578451bfb7f72cf7cc95f Mon Sep 17 00:00:00 2001 From: lidanyang Date: Thu, 27 Jun 2024 13:41:10 +0800 Subject: [PATCH 033/119] remove custom tools to data analyst --- metagpt/roles/di/data_analyst.py | 18 ++++++++++-------- metagpt/roles/di/role_zero.py | 6 +----- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/metagpt/roles/di/data_analyst.py b/metagpt/roles/di/data_analyst.py index f13fc78fb..b3144a100 100644 --- a/metagpt/roles/di/data_analyst.py +++ b/metagpt/roles/di/data_analyst.py @@ -1,12 +1,13 @@ from __future__ import annotations -from pydantic import Field +from pydantic import Field, model_validator from metagpt.actions.di.execute_nb_code import ExecuteNbCode from metagpt.actions.di.write_analysis_code import WriteAnalysisCode from metagpt.logs import logger from metagpt.roles.di.role_zero import RoleZero from metagpt.schema import TaskResult, Message +from metagpt.tools.tool_recommend import BM25ToolRecommender, ToolRecommender from metagpt.tools.tool_registry import register_tool @@ -18,21 +19,22 @@ class DataAnalyst(RoleZero): tools: list[str] = ["Plan", "DataAnalyst", "RoleZero"] custom_tools: list[str] = ["machine learning", "web scraping", "Terminal"] + custom_tool_recommender: ToolRecommender = None use_reflection: bool = True write_code: WriteAnalysisCode = Field(default_factory=WriteAnalysisCode, exclude=True) execute_code: ExecuteNbCode = Field(default_factory=ExecuteNbCode, exclude=True) task_result: TaskResult = None + @model_validator(mode="after") + def set_custom_tool(self): + if self.custom_tools and not self.custom_tool_recommender: + self.custom_tool_recommender = BM25ToolRecommender(tools=self.custom_tools) + def _update_tool_execution(self): - self.tool_execution_map = { - "Plan.append_task": self.planner.plan.append_task, - "Plan.reset_task": self.planner.plan.reset_task, - "Plan.replace_task": self.planner.plan.replace_task, + self.tool_execution_map.update({ "DataAnalyst.write_and_exec_code": self.write_and_exec_code, - "RoleZero.ask_human": self.ask_human, - "RoleZero.reply_to_human": self.reply_to_human, - } + }) async def write_and_exec_code(self): """Write a code block for current task and execute it in an interactive notebook environment.""" diff --git a/metagpt/roles/di/role_zero.py b/metagpt/roles/di/role_zero.py index 4225d1f76..93abe8c02 100644 --- a/metagpt/roles/di/role_zero.py +++ b/metagpt/roles/di/role_zero.py @@ -41,10 +41,8 @@ class RoleZero(Role): max_react_loop: int = 20 # used for react mode # Tools - tools: list[str] = [] + tools: list[str] = [] # Use special symbol [""] to indicate use of all registered tools tool_recommender: ToolRecommender = None - custom_tools: list[str] = [] - custom_tool_recommender: ToolRecommender = None tool_execution_map: dict[str, Callable] = {} special_tool_commands: list[str] = ["Plan.finish_current_task", "end"] # Equipped with three basic tools by default for optional use @@ -70,8 +68,6 @@ class RoleZero(Role): self._set_react_mode(react_mode=self.react_mode, max_react_loop=self.max_react_loop) if self.tools and not self.tool_recommender: self.tool_recommender = BM25ToolRecommender(tools=self.tools, force=True) - if self.custom_tools and not self.custom_tool_recommender: - self.custom_tool_recommender = BM25ToolRecommender(tools=self.custom_tools) self.set_actions([RunCommand]) # HACK: Init Planner, control it through dynamic thinking; Consider formalizing as a react mode From f2cf8dd74ba54fc2e37ddcf59b9fe7bcab1bd94b Mon Sep 17 00:00:00 2001 From: lidanyang Date: Thu, 27 Jun 2024 16:22:00 +0800 Subject: [PATCH 034/119] fix bug of ExecuteNbCode._init_code was never awaited --- metagpt/actions/di/execute_nb_code.py | 6 ++++-- metagpt/roles/di/data_analyst.py | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/metagpt/actions/di/execute_nb_code.py b/metagpt/actions/di/execute_nb_code.py index 9a9f0483a..cc2c05341 100644 --- a/metagpt/actions/di/execute_nb_code.py +++ b/metagpt/actions/di/execute_nb_code.py @@ -85,10 +85,12 @@ class ExecuteNbCode(Action): ) self.reporter = NotebookReporter() self.set_nb_client() - asyncio.run(self._init_code()) + self._init_called = False async def _init_code(self): - await self.run(INI_CODE) + if not self._init_called: + await self.run(INI_CODE) + self._init_called = True def set_nb_client(self): self.nb_client = RealtimeOutputNotebookClient( diff --git a/metagpt/roles/di/data_analyst.py b/metagpt/roles/di/data_analyst.py index b3144a100..491968d14 100644 --- a/metagpt/roles/di/data_analyst.py +++ b/metagpt/roles/di/data_analyst.py @@ -40,6 +40,7 @@ class DataAnalyst(RoleZero): """Write a code block for current task and execute it in an interactive notebook environment.""" counter = 0 success = False + await self.execute_code._init_code() # plan info plan_status = self.planner.get_plan_status() From 898ee44bec5ff8e737a913cadd92ae2059435818 Mon Sep 17 00:00:00 2001 From: lidanyang Date: Mon, 1 Jul 2024 10:52:50 +0800 Subject: [PATCH 035/119] add supprot of task_type and add output of special command --- metagpt/prompts/di/role_zero.py | 5 ++++- metagpt/roles/di/data_analyst.py | 16 ++++++---------- metagpt/roles/di/role_zero.py | 11 ++++++----- metagpt/schema.py | 10 +++++++--- metagpt/tools/libs/__init__.py | 12 ++++++------ 5 files changed, 29 insertions(+), 25 deletions(-) diff --git a/metagpt/prompts/di/role_zero.py b/metagpt/prompts/di/role_zero.py index 04344fa1e..2b4fe105a 100644 --- a/metagpt/prompts/di/role_zero.py +++ b/metagpt/prompts/di/role_zero.py @@ -22,6 +22,9 @@ class Task(BaseModel): {available_commands} Special Command: Use {{"command_name": "end"}} to do nothing or indicate completion of all requirements and the end of actions. +# Available Task Types +{task_type_desc} + # Current Plan {plan_status} @@ -38,7 +41,7 @@ Pay close attention to the Example provided, you can reuse the example for your You may use any of the available commands to create a plan or update the plan. You may output mutiple commands, they will be executed sequentially. If you finish current task, you will automatically take the next task in the existing plan, use Plan.finish_task, DON'T append a new task. -# Your commands in a json array, in the following output format. If there is nothing to do, use the pass or end command: +# Your commands in a json array, in the following output format with command_name and args. If there is nothing to do, use the pass or end command: Some text indicating your thoughts, such as how you should update the plan status, respond to inquiry, or seek for help. Then a json array of commands. You must output ONE and ONLY ONE json array. DON'T output multiple json arrays with thoughts between them. ```json [ diff --git a/metagpt/roles/di/data_analyst.py b/metagpt/roles/di/data_analyst.py index 491968d14..f3586a6f5 100644 --- a/metagpt/roles/di/data_analyst.py +++ b/metagpt/roles/di/data_analyst.py @@ -24,7 +24,6 @@ class DataAnalyst(RoleZero): use_reflection: bool = True write_code: WriteAnalysisCode = Field(default_factory=WriteAnalysisCode, exclude=True) execute_code: ExecuteNbCode = Field(default_factory=ExecuteNbCode, exclude=True) - task_result: TaskResult = None @model_validator(mode="after") def set_custom_tool(self): @@ -75,17 +74,14 @@ class DataAnalyst(RoleZero): ### process execution result ### counter += 1 - self.task_result = TaskResult(code=code, result=result, is_success=success) - + if success: + task_result = TaskResult(code=code, result=result, is_success=success) + self.planner.current_task.update_task_result(task_result) output = f""" - Code written: + **Code written**: {code} - Execution status:{'Success' if success else 'Failed'} - Execution result: {result} + **Execution status**:{'Success' if success else 'Failed'} + **Execution result**: {result} """ self.rc.working_memory.clear() return output - - def _finish_current_task(self): - self.planner.current_task.update_task_result(self.task_result) - super()._finish_current_task() diff --git a/metagpt/roles/di/role_zero.py b/metagpt/roles/di/role_zero.py index 93abe8c02..b179f5a00 100644 --- a/metagpt/roles/di/role_zero.py +++ b/metagpt/roles/di/role_zero.py @@ -6,6 +6,7 @@ import re import traceback from typing import Callable, Literal, Tuple +from metagpt.strategy.task_type import TaskType from pydantic import model_validator from metagpt.actions import Action @@ -130,6 +131,7 @@ class RoleZero(Role): ### 2. Plan Status ### plan_status, current_task = self._get_plan_status() + task_type_desc = "\n".join([f"- **{tt.type_name}**: {tt.value.desc}" for tt in TaskType]) ### 3. Tool/Command Info ### tools = await self.tool_recommender.recommend_tools() @@ -142,6 +144,7 @@ class RoleZero(Role): example=example, available_commands=tool_info, instruction=self.instruction.strip(), + task_type_desc=task_type_desc, ) memory = self.rc.memory.get(self.memory_k) if not self.browser.is_empty_page: @@ -201,13 +204,14 @@ class RoleZero(Role): async def _run_commands(self, commands) -> str: outputs = [] for cmd in commands: + output = f"Command {cmd['command_name']} executed" # handle special command first if await self._run_special_command(cmd): + outputs.append(output) continue # run command as specified by tool_execute_map if cmd["command_name"] in self.tool_execution_map: tool_obj = self.tool_execution_map[cmd["command_name"]] - output = f"Command {cmd['command_name']} executed" try: if inspect.iscoroutinefunction(tool_obj): tool_output = await tool_obj(**cmd["args"]) @@ -235,16 +239,13 @@ class RoleZero(Role): if cmd["command_name"] == "Plan.finish_current_task" and not self.planner.plan.is_plan_finished(): # task_result = TaskResult(code=str(commands), result=outputs, is_success=is_success) # self.planner.plan.current_task.update_task_result(task_result=task_result) - self._finish_current_task() + self.planner.plan.finish_current_task() elif cmd["command_name"] == "end": self._set_state(-1) return is_special_cmd - def _finish_current_task(self): - self.planner.plan.finish_current_task() - def _get_plan_status(self) -> Tuple[str, str]: plan_status = self.planner.plan.model_dump(include=["goal", "tasks"]) for task in plan_status["tasks"]: diff --git a/metagpt/schema.py b/metagpt/schema.py index 69c7a519b..11610b6c3 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -464,7 +464,7 @@ class Task(BaseModel): self.is_finished = False def update_task_result(self, task_result: TaskResult): - self.code = task_result.code + self.code = task_result.code + "\n" + task_result.code self.result = task_result.result self.is_success = task_result.is_success @@ -669,10 +669,14 @@ class Plan(BaseModel): """ return [task for task in self.tasks if task.is_finished] - def append_task(self, task_id: str, dependent_task_ids: list[str], instruction: str, assignee: str): + def append_task(self, task_id: str, dependent_task_ids: list[str], instruction: str, assignee: str, task_type: str): """Append a new task with task_id (number) to the end of existing task sequences. If dependent_task_ids is not empty, the task will depend on the tasks with the ids in the list.""" new_task = Task( - task_id=task_id, dependent_task_ids=dependent_task_ids, instruction=instruction, assignee=assignee + task_id=task_id, + dependent_task_ids=dependent_task_ids, + instruction=instruction, + assignee=assignee, + task_type=task_type ) return self._append_task(new_task) diff --git a/metagpt/tools/libs/__init__.py b/metagpt/tools/libs/__init__.py index 725ab73c9..c9de6bd21 100644 --- a/metagpt/tools/libs/__init__.py +++ b/metagpt/tools/libs/__init__.py @@ -5,11 +5,11 @@ # @File : __init__.py # @Desc : from metagpt.tools.libs import ( - # data_preprocess, - # feature_engineering, + data_preprocess, + feature_engineering, sd_engine, gpt_v_generator, - # web_scraping, + web_scraping, # email_login, terminal, editor, @@ -20,11 +20,11 @@ from metagpt.tools.libs import ( from metagpt.tools.libs.env import get_env, set_get_env_entry, default_get_env, get_env_description _ = ( - # data_preprocess, - # feature_engineering, + data_preprocess, + feature_engineering, sd_engine, gpt_v_generator, - # web_scraping, + web_scraping, # email_login, terminal, editor, From b73024510ba916566634bea4417b394730359ff0 Mon Sep 17 00:00:00 2001 From: shenchucheng Date: Tue, 11 Jun 2024 10:44:52 +0800 Subject: [PATCH 036/119] action reporter --- metagpt/actions/design_api.py | 44 +++++++++++++++------------ metagpt/actions/project_management.py | 24 +++++++++------ metagpt/actions/write_prd.py | 40 +++++++++++++----------- 3 files changed, 60 insertions(+), 48 deletions(-) diff --git a/metagpt/actions/design_api.py b/metagpt/actions/design_api.py index cc88171ff..7fac6710b 100644 --- a/metagpt/actions/design_api.py +++ b/metagpt/actions/design_api.py @@ -254,25 +254,29 @@ class WriteDesign(Action): extra_info=to_markdown_code_block(extra_info), prd=to_markdown_code_block(prd_content), ) - if not legacy_design_filename: - node = await self._new_system_design(context=context) - design = Document(content=node.instruct_content.model_dump_json()) - else: - old_design_content = await aread(filename=legacy_design_filename) - design = await self._merge( - prd_doc=Document(content=context), system_design_doc=Document(content=old_design_content) - ) + async with DocsReporter(enable_llm_stream=True) as reporter: + await reporter.async_report({"type": "design"}, "meta") + if not legacy_design_filename: + node = await self._new_system_design(context=context) + design = Document(content=node.instruct_content.model_dump_json()) + else: + old_design_content = await aread(filename=legacy_design_filename) + design = await self._merge( + prd_doc=Document(content=context), system_design_doc=Document(content=old_design_content) + ) - if not output_pathname: - output_pathname = Path(output_pathname) / "docs" / "sytem_design.json" - output_pathname.mkdir(parents=True, exist_ok=True) - elif not Path(output_pathname).is_absolute(): - output_pathname = DEFAULT_WORKSPACE_ROOT / output_pathname - output_pathname = Path(output_pathname) - await awrite(filename=output_pathname, data=design.content) - output_filename = output_pathname.parent / f"{output_pathname.stem}-class-diagram" - await self._save_data_api_design(design_doc=design, output_filename=output_filename) - output_filename = output_pathname.parent / f"{output_pathname.stem}-sequence-diagram" - await self._save_seq_flow(design_doc=design, output_filename=output_filename) - await save_json_to_markdown(content=design.content, output_filename=output_pathname.with_suffix(".md")) + if not output_pathname: + output_pathname = Path(output_pathname) / "docs" / "sytem_design.json" + output_pathname.mkdir(parents=True, exist_ok=True) + elif not Path(output_pathname).is_absolute(): + output_pathname = DEFAULT_WORKSPACE_ROOT / output_pathname + output_pathname = Path(output_pathname) + await awrite(filename=output_pathname, data=design.content) + output_filename = output_pathname.parent / f"{output_pathname.stem}-class-diagram" + await self._save_data_api_design(design_doc=design, output_filename=output_filename) + output_filename = output_pathname.parent / f"{output_pathname.stem}-sequence-diagram" + await self._save_seq_flow(design_doc=design, output_filename=output_filename) + md_output_filename = output_pathname.with_suffix(".md") + await save_json_to_markdown(content=design.content, output_filename=md_output_filename) + await reporter.async_report(md_output_filename, "path") return f'System Design filename: "{str(output_pathname)}"' diff --git a/metagpt/actions/project_management.py b/metagpt/actions/project_management.py index a39840bf1..e810a5dbe 100644 --- a/metagpt/actions/project_management.py +++ b/metagpt/actions/project_management.py @@ -180,16 +180,20 @@ class WriteTasks(Action): if design_filename: content = await aread(filename=design_filename) context += to_markdown_code_block(content) - node = await self._run_new_tasks(context) - file_content = node.instruct_content.model_dump_json() - if not output_pathname: - output_pathname = Path(output_pathname) / "docs" / "project_schedule.json" - output_pathname.mkdir(parents=True, exist_ok=True) - elif not Path(output_pathname).is_absolute(): - output_pathname = DEFAULT_WORKSPACE_ROOT / output_pathname - output_pathname = Path(output_pathname) - await awrite(filename=output_pathname, data=file_content) - await save_json_to_markdown(content=file_content, output_filename=output_pathname.with_suffix(".md")) + async with DocsReporter(enable_llm_stream=True) as reporter: + await reporter.async_report({"type": "task"}, "meta") + node = await self._run_new_tasks(context) + file_content = node.instruct_content.model_dump_json() + if not output_pathname: + output_pathname = Path(output_pathname) / "docs" / "project_schedule.json" + output_pathname.mkdir(parents=True, exist_ok=True) + elif not Path(output_pathname).is_absolute(): + output_pathname = DEFAULT_WORKSPACE_ROOT / output_pathname + output_pathname = Path(output_pathname) + await awrite(filename=output_pathname, data=file_content) + md_output_filename = output_pathname.with_suffix(".md") + await save_json_to_markdown(content=file_content, output_filename=md_output_filename) + await reporter.async_report(md_output_filename, "path") return f'Project Schedule filename: "{str(output_pathname)}"' diff --git a/metagpt/actions/write_prd.py b/metagpt/actions/write_prd.py index 7199ec415..4d29d8c6f 100644 --- a/metagpt/actions/write_prd.py +++ b/metagpt/actions/write_prd.py @@ -300,23 +300,27 @@ class WritePRD(Action): user_requirement=to_markdown_code_block(val=user_requirement), extra_info=to_markdown_code_block(val=extra_info), ) - req = Document(content=content) - if not legacy_prd_filename: - node = await self._new_prd(requirement=req.content) - new_prd = Document(content=node.instruct_content.model_dump_json()) - else: - content = await aread(filename=legacy_prd_filename) - old_prd = Document(content=content) - new_prd = await self._merge(req=req, related_doc=old_prd) + async with DocsReporter(enable_llm_stream=True) as reporter: + await reporter.async_report({"type": "prd"}, "meta") + req = Document(content=content) + if not legacy_prd_filename: + node = await self._new_prd(requirement=req.content) + new_prd = Document(content=node.instruct_content.model_dump_json()) + else: + content = await aread(filename=legacy_prd_filename) + old_prd = Document(content=content) + new_prd = await self._merge(req=req, related_doc=old_prd) - if not output_pathname: - output_pathname = DEFAULT_WORKSPACE_ROOT / "docs" / "prd.json" - output_pathname.mkdir(parents=True, exist_ok=True) - elif not Path(output_pathname).is_absolute(): - output_pathname = DEFAULT_WORKSPACE_ROOT / output_pathname - output_pathname = Path(output_pathname) - await awrite(filename=output_pathname, data=new_prd.content) - competitive_analysis_filename = output_pathname.parent / f"{output_pathname.stem}-competitive-analysis" - await self._save_competitive_analysis(prd_doc=new_prd, output_filename=Path(competitive_analysis_filename)) - await save_json_to_markdown(content=new_prd.content, output_filename=output_pathname.with_suffix(".md")) + if not output_pathname: + output_pathname = DEFAULT_WORKSPACE_ROOT / "docs" / "prd.json" + output_pathname.mkdir(parents=True, exist_ok=True) + elif not Path(output_pathname).is_absolute(): + output_pathname = DEFAULT_WORKSPACE_ROOT / output_pathname + output_pathname = Path(output_pathname) + await awrite(filename=output_pathname, data=new_prd.content) + competitive_analysis_filename = output_pathname.parent / f"{output_pathname.stem}-competitive-analysis" + await self._save_competitive_analysis(prd_doc=new_prd, output_filename=Path(competitive_analysis_filename)) + md_output_filename = output_pathname.with_suffix(".md") + await save_json_to_markdown(content=new_prd.content, output_filename=md_output_filename) + await reporter.async_report(md_output_filename, "path") return f'PRD filename: "{str(output_pathname)}"' From fafe2ce68feae2c6855bc3704bb64486bc00b308 Mon Sep 17 00:00:00 2001 From: seeker Date: Wed, 3 Jul 2024 20:11:32 +0800 Subject: [PATCH 037/119] update: SWE Agent --- metagpt/const.py | 2 +- metagpt/prompts/di/swe.py | 87 +++++++++++++------ metagpt/roles/di/swe.py | 8 +- metagpt/tools/libs/browser.py | 2 + metagpt/tools/libs/terminal.py | 7 +- metagpt/tools/swe_agent_commands/defaults.sh | 9 +- .../swe_agent_commands/swe_agent_utils.py | 8 +- tests/metagpt/roles/di/run_swe.py | 71 +++++---------- 8 files changed, 103 insertions(+), 91 deletions(-) diff --git a/metagpt/const.py b/metagpt/const.py index c78a22641..94d22bc70 100644 --- a/metagpt/const.py +++ b/metagpt/const.py @@ -149,6 +149,6 @@ METAGPT_REPORTER_DEFAULT_URL = os.environ.get("METAGPT_REPORTER_URL", "") # Metadata defines AGENT = "agent" - +SWE_WORKSPACE_ROOT = Path("/tmp/swe_workspace") # SWE agent SWE_SETUP_PATH = METAGPT_ROOT / "metagpt/tools/swe_agent_commands/setup_default.sh" diff --git a/metagpt/prompts/di/swe.py b/metagpt/prompts/di/swe.py index 64c67b09b..ed1f8a011 100644 --- a/metagpt/prompts/di/swe.py +++ b/metagpt/prompts/di/swe.py @@ -4,19 +4,17 @@ You can find the original examples from the SWE-agent project here: https://github.com/princeton-nlp/SWE-agent/tree/main/config/configs """ - SWE_AGENT_SYSTEM_TEMPLATE = """ -SETTING: You are an autonomous programmer, and you're working directly in the command line with a special interface. +SETTING: You are an autonomous programmer, and you're working directly in the environment line with a special interface. The special interface consists of a file editor that shows you {WINDOW} lines of a file at a time. Please note that THE EDIT COMMAND REQUIRES PROPER INDENTATION. Pay attention to the original indentation when replacing the function. If you'd like to add the line ' print(x)' you must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run. - Always review your changes post-edit to ensure they accurately reflect your intentions. If the changes are not as desired, don't hesitate to issue another command to correct them. Your output should always contain a section of reasoning and a command described in JSON format. -The command must always contain command_name and args fields. The command_name field should always be Bash.run, and the args field should always include a cmd field containing the bash command. + Use \\n to represent line breaks, ensuring the command conforms to the JSON format and is displayed on a single line. Except for the `edit` command, each parameter of the command needs to be enclosed in single quotes. As shown in the example below: @@ -31,42 +29,75 @@ First I'll start by using ls to see what files are in the current directory. The }} ``` - You should only include a *SINGLE* command in the command section and then wait for a response from the shell before continuing with more discussion and commands. Everything you include in the DISCUSSION section will be saved for future reference. If you'd like to issue two commands at once, PLEASE DO NOT DO THAT! Please instead first submit just the first command, and then after receiving a response you'll be able to issue the second command. -You're free to use any other bash commands you want (e.g. find, grep, cat, ls, cd) in addition to the special commands listed above. +Remember, YOU CAN ONLY ENTER ONE COMMAND AT A TIME. You should always wait for feedback after every command. + +You can use any bash commands you want (e.g., find, grep, cat, ls, cd) or any custom special tools (including `edit`) by calling Bash.run. Edit all the files you need. You should carefully observe the behavior and results of the previous action, and avoid triggering repeated errors. -However, the environment does NOT support interactive session commands (e.g. python, vim), so please do not invoke them. +However, the Bash.run does NOT support interactive session commands (e.g. python, vim), so please do not invoke them. + +In addition to the terminal, I also provide additional tools. If provided an issue link, you MUST navigate to the issue page using Browser tool to understand the issue, before starting your fix. + +# INSTRUCTIONS: +Your first action must be to check if the repository exists at the current path. If it exists, navigate to the repository path. If the repository doesn't exist, please download it and then navigate to it. +All subsequent actions must be performed within this repository path. Do not leave this directory to execute any actions at any time. +Your terminal session has started, and you can use any bash commands or the special interface to help you. Edit all the files you need. """ MINIMAL_EXAMPLE = """ ## Example of a actions trajectory User Requirement and Issue: Fix the bug in the repo. Because the environment is not available, you DO NOT need to run and modify any existing test case files or add new test case files to ensure that the bug is fixed. -### Locate issue(Require): Locate the issue in the code by searching for the relevant file, function, or class and open the file to view the code. -cd /workspace/django__django_3.0 +### Read and understand issue(Require): +{{ + "command_name": "Browser.goto", + "args": {{ + "url": "https://github.com/geekan/MetaGPT/issues/1275" + }} +}} -> -search_dir_and_preview ASCIIUsernameValidator --> -open /workspace/django__django_3.0/django/contrib/auth/validators.py --> -### Fix the Bug(Require): Fix the bug in the code by editing the relevant function, class or code snippet. -edit 10:20 < + +Bash.run(cmd='search_dir_and_preview ASCIIUsernameValidator') +{{ + "command_name": "Bash.run", + "args": {{ + "cmd": "open /workspace/django__django_3.0/django/contrib/auth/validators.py" + }} +}} +-> + +### Fix the Bug(Require): Fix the bug in the code by editing the relevant function, class or code snippet. +{{ + "command_name": "Bash.run", + "args": {{ + "cmd": "edit 10:20 < + ### Submit the Changes(Require): Submit the changes to the repository. -submit +{{ + "command_name": "Bash.run", + "args": {{ + "cmd": "submit" + }} +}} +Bash.run(cmd='submit') +-> +{{ + "command_name": "end", +}} """ @@ -132,6 +163,10 @@ IMPORTANT_TIPS = """ - Based on feedback of observation or bash command in trajectory to guide adjustments in your search strategy. 13. If the task results in succeed, fail, or NO PROGRESS, output `submit`. + +14. If provided an issue link, you MUST go to the issue page using Browser tool to understand the issue before starting your fix. + +15. When the edit fails, try to enlarge the starting line. """ NEXT_STEP_TEMPLATE = f""" diff --git a/metagpt/roles/di/swe.py b/metagpt/roles/di/swe.py index 6d357c02b..915d186b4 100644 --- a/metagpt/roles/di/swe.py +++ b/metagpt/roles/di/swe.py @@ -22,8 +22,7 @@ class SWE(RoleZero): _system_msg: str = SWE_AGENT_SYSTEM_TEMPLATE system_msg: list[str] = [_system_msg.format(WINDOW=_bash_window_size)] _instruction: str = NEXT_STEP_TEMPLATE - # tools: list[str] = ["Bash", "Browser"] - tools: list[str] = ["Bash"] + tools: list[str] = ["Bash", "Browser:goto,scroll"] terminal: Bash = Field(default_factory=Bash, exclude=True) output_diff: str = "" max_react_loop: int = 30 @@ -75,11 +74,10 @@ class SWE(RoleZero): if not ok: return for cmd in commands: - if "submit" not in cmd.get("args", {}).get("cmd", ""): + if "end" != cmd.get("command_name", ""): return try: - # Generate patch by git diff - diff_output = self.terminal.run("git diff") + diff_output = self.terminal.run("git diff --cached") clear_diff = extract_patch(diff_output) logger.info(f"Diff output: \n{clear_diff}") if clear_diff: diff --git a/metagpt/tools/libs/browser.py b/metagpt/tools/libs/browser.py index c6ea71bd5..864996e8c 100644 --- a/metagpt/tools/libs/browser.py +++ b/metagpt/tools/libs/browser.py @@ -122,6 +122,8 @@ class Browser: async def goto(self, url: str, timeout: float = 30000): """Navigate to a specific URL.""" + if self.page is None: + await self.start() async with self.reporter as reporter: await reporter.async_report(url, "url") await self.page.goto(url, timeout=timeout) diff --git a/metagpt/tools/libs/terminal.py b/metagpt/tools/libs/terminal.py index 938eadff4..a04acb8e9 100644 --- a/metagpt/tools/libs/terminal.py +++ b/metagpt/tools/libs/terminal.py @@ -2,7 +2,7 @@ import subprocess import threading from queue import Queue -from metagpt.const import SWE_SETUP_PATH +from metagpt.const import SWE_SETUP_PATH, SWE_WORKSPACE_ROOT from metagpt.tools.tool_registry import register_tool from metagpt.utils.report import END_MARKER_VALUE, TerminalReporter @@ -136,13 +136,14 @@ class Terminal: class Bash(Terminal): """ A class to run bash commands directly and provides custom shell functions. + All custom functions in this class can ONLY be called via the `Bash.run` method. """ def __init__(self): """init""" super().__init__() setup_cmd = f"source {SWE_SETUP_PATH}" - self.run_command(setup_cmd) + self.run_command(f"cd {SWE_WORKSPACE_ROOT} && {setup_cmd}") def run(self, cmd) -> str: """ @@ -184,7 +185,7 @@ class Bash(Terminal): filename (str): The name of the file to create. - submit - Submits your current code and terminates the session. + Submits your current code. it can only be executed once, the last action before the `end`. - search_dir_and_preview [] Searches for search_term in all files in dir and gives their code preview diff --git a/metagpt/tools/swe_agent_commands/defaults.sh b/metagpt/tools/swe_agent_commands/defaults.sh index f0898aabc..d416dcbf5 100644 --- a/metagpt/tools/swe_agent_commands/defaults.sh +++ b/metagpt/tools/swe_agent_commands/defaults.sh @@ -177,7 +177,7 @@ create() { # @yaml # signature: submit -# docstring: submits your current code and terminates the session. this is the only submit action needed; no need to run git add or git commit before this. +# docstring: submits your current code. the last action before the `end`, it can only be executed once. submit() { # Check if the patch file exists and is non-empty if [ -s "$SWE_CMD_WORK_DIR/test.patch" ]; then @@ -186,8 +186,7 @@ submit() { fi git add -A - git diff --cached > model.patch - echo "<>" + echo "<>" } diff --git a/metagpt/tools/swe_agent_commands/swe_agent_utils.py b/metagpt/tools/swe_agent_commands/swe_agent_utils.py index 8c01dc9c9..9e293f4d2 100644 --- a/metagpt/tools/swe_agent_commands/swe_agent_utils.py +++ b/metagpt/tools/swe_agent_commands/swe_agent_utils.py @@ -16,10 +16,12 @@ def extract_patch(command_output): def load_hf_dataset(dataset_name_or_path: str, cache_dir, split: str = "test", existing_ids: list = []): - if Path(dataset_name_or_path).exists(): - dataset = load_from_disk(dataset_name_or_path) + data_dir = cache_dir / dataset_name_or_path + if Path(data_dir).exists(): + dataset = load_from_disk(data_dir) else: - dataset = load_dataset(dataset_name_or_path, cache_dir=cache_dir) + dataset = load_dataset(dataset_name_or_path) + dataset.save_to_disk(data_dir) print(dataset) if split not in dataset: raise ValueError(f"Invalid split {split} for dataset {dataset_name_or_path}") diff --git a/tests/metagpt/roles/di/run_swe.py b/tests/metagpt/roles/di/run_swe.py index c6cc56fd1..f9d19be74 100644 --- a/tests/metagpt/roles/di/run_swe.py +++ b/tests/metagpt/roles/di/run_swe.py @@ -12,7 +12,7 @@ from metagpt.tools.swe_agent_commands.swe_agent_utils import load_hf_dataset # Specify by yourself TEST_REPO_DIR = Path("/Users/seeker/Projects/sdfz/mg/mg-swe-agent") / "benchmark" / "swe_bench" / "data" / "test_repo" -DATA_DIR = METAGPT_ROOT / "benchmark" / "swe_bench" / "data" +DATA_DIR = METAGPT_ROOT / "data/hugging_face" INSTANCE_TEMPLATE = """ ## User Requirement @@ -27,8 +27,9 @@ hints text is the comment under issue: {hints_text} The repository may already exist at the path `{repo_path}`. If it doesn't, please download the repository to this path. -All your subsequent actions should use the project path as your root directory, and you should never leave that directory to execute any actions. +Your first action must be to navigate to the repository path `{repo_path}`. This issue occurred in version {version}, with the corresponding base commit being {base_commit}. You need to switch to the code version associated with this commit. +All subsequent actions must be performed within this repository path. Do not leave this directory to execute any actions at any time. # INSTRUCTIONS: Now, you're going to solve this issue on your own from the perspective of a programmer. Your terminal session has started and you're in the repository's root directory. You can use any bash commands or the special interface to help you. Edit all the files you need. @@ -36,38 +37,6 @@ Remember, YOU CAN ONLY ENTER ONE COMMAND AT A TIME. You should always wait for f """ -def split_dataset_equally(dataset): - # 计算索引 - # fixme: 设置django - - part1 = dataset.filter( - lambda x: x["repo"] - not in [ - "django/django", - "sympy/sympy", - "pytest-dev/pytest", - ], - desc="Filtering out existing ids", - load_from_cache_file=True, - ) - - part2 = dataset.filter( - lambda x: x["repo"] in ["sympy/sympy", "pytest-dev/pytest"], - desc="Filtering out existing ids", - load_from_cache_file=True, - ) - - part3 = dataset.filter( - lambda x: x["repo"] in ["django/django"], - desc="Filtering out existing ids", - load_from_cache_file=False, - ) - - print(len(part1), len(part2), len(part3)) - - return [part1, part2, part3] - - def check_instance_status(instance, swe_result_dir): output_file = swe_result_dir / "all_preds.jsonl" res = True @@ -87,12 +56,20 @@ async def run(instance, swe_result_dir): logger.info(f"Instance {instance['instance_id']} already exists, skipping execution.") return - repo_path = TEST_REPO_DIR / (instance["repo"].replace("-", "_").replace("/", "__") + "_" + instance["version"]) - """ - All your subsequent actions should use the project path as your root directory, and you should never leave that directory to execute any actions. - """ + repo_path = Path("/Users/seeker/Projects/other/test_repo") / ( + instance["repo"].replace("-", "_").replace("/", "__") + "_" + instance["version"] + ) + # repo_path = Path("/Users/seeker/Projects/other/test_repo") / instance["repo"].split("/")[-1] + + # 前处理 terminal = Terminal() - terminal.run_command(f"cd {repo_path} && git checkout . && git clean -n -d && git clean -f -d") + terminal.run_command(f"cd {repo_path} && git reset --hard && git clean -n -d && git clean -f -d") + terminal.run_command("BRANCH=$(git remote show origin | awk '/HEAD branch/ {print $NF}')") + logger.info(terminal.run_command("echo $BRANCH")) + # logger.info(terminal.run_command(f'Branch name: $BRANCH')) + logger.info(terminal.run_command('git checkout "$BRANCH"')) + logger.info(terminal.run_command("git branch")) + user_requirement_and_issue = INSTANCE_TEMPLATE.format( issue=instance["problem_statement"], hints_text=instance["hints_text"], @@ -126,16 +103,14 @@ async def async_main(): dataset_path = "manna-ai/SWE-bench_Nano" # "princeton-nlp/SWE-bench_Lite" #"manna-ai/SWE-bench_Nano" dataset = load_hf_dataset(dataset_name_or_path=dataset_path, cache_dir=DATA_DIR, split="test") - sample_datasets = split_dataset_equally(dataset) date_time = datetime.now().strftime("%m-%d") - round_ = "third" - - for idx, sub_dataset in enumerate(sample_datasets): - exp_name = f"nano_mgx_{date_time}_{round_}_part_{idx}" - swe_result_dir = DEFAULT_WORKSPACE_ROOT / f"result_{config.llm.model}" / exp_name - swe_result_dir.mkdir(parents=True, exist_ok=True) - for instance in sub_dataset: - await run(instance, swe_result_dir) + # _round = "first" + _round = "second" + exp_name = f"nano_mgx_{date_time}_{_round}" + swe_result_dir = DEFAULT_WORKSPACE_ROOT / f"result_{config.llm.model.replace('/', '_')}" / exp_name + swe_result_dir.mkdir(parents=True, exist_ok=True) + for instance in dataset: + await run(instance, swe_result_dir) if __name__ == "__main__": From d571fe0f90a00877921141264ea95a815fdafe19 Mon Sep 17 00:00:00 2001 From: seeker Date: Wed, 3 Jul 2024 20:26:55 +0800 Subject: [PATCH 038/119] update: SWE Agent --- tests/metagpt/roles/di/run_swe_new.py | 38 +++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/metagpt/roles/di/run_swe_new.py diff --git a/tests/metagpt/roles/di/run_swe_new.py b/tests/metagpt/roles/di/run_swe_new.py new file mode 100644 index 000000000..94ef1e644 --- /dev/null +++ b/tests/metagpt/roles/di/run_swe_new.py @@ -0,0 +1,38 @@ +import asyncio + +from metagpt.logs import logger +from metagpt.roles.di.swe import SWE + +FIX_ISSUE1 = """ +Write a fix for this issue: https://github.com/langchain-ai/langchain/issues/20453, +you can fix it on this repo https://github.com/garylin2099/langchain, +""" + +"checkout a branch named test-fix, commit your changes, push, and create a PR to the master branch of https://github.com/iorisa/langchain" + +FIX_ISSUE2 = """ +Write a fix for this issue https://github.com/geekan/MetaGPT/issues/1275. +You can fix it on the v0.8-release branch of this repo https://github.com/garylin2099/MetaGPT, +during fixing, checkout a branch named test-fix-1275, commit your changes, push, and create a PR to the v0.8-release branch of https://github.com/garylin2099/MetaGPT +""" +FIX_ISSUE3 = """ +Write a fix for this issue https://github.com/geekan/MetaGPT/issues/1262. +You can fix it on this repo https://github.com/garylin2099/MetaGPT, +during fixing, checkout a branch named test-fix-1262, commit your changes, push, and create a PR to https://github.com/garylin2099/MetaGPT +""" +FIX_ISSUE_SIMPLE = """ +Write a fix for this issue: https://github.com/mannaandpoem/simple_calculator/issues/1, +you can fix it on this repo https://github.com/garylin2099/simple_calculator, +checkout a branch named test, commit your changes, push, and create a PR to the master branch of original repo. +""" + +if __name__ == "__main__": + swe_agent = SWE() + logger.info("**** Starting run ****") + user_requirement_and_issue = ( + FIX_ISSUE1 + + """Because the environment is not available, you DO NOT need to run and modify any existing test case files or add new test case files to ensure that the bug is fixed.""" + ) + asyncio.run(swe_agent.run(user_requirement_and_issue)) + logger.info("**** Finished running ****") + logger.info(f"Patch: {swe_agent.output_diff}") From ca0cb7639f7d764044bd3df380f68fe90b03eaca Mon Sep 17 00:00:00 2001 From: seeker Date: Wed, 3 Jul 2024 22:37:17 +0800 Subject: [PATCH 039/119] update: SWE Agent --- metagpt/const.py | 2 ++ metagpt/roles/di/swe.py | 2 +- tests/metagpt/roles/di/run_swe_new.py | 11 +++++++---- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/metagpt/const.py b/metagpt/const.py index 94d22bc70..6a0e2c4bb 100644 --- a/metagpt/const.py +++ b/metagpt/const.py @@ -150,5 +150,7 @@ METAGPT_REPORTER_DEFAULT_URL = os.environ.get("METAGPT_REPORTER_URL", "") # Metadata defines AGENT = "agent" SWE_WORKSPACE_ROOT = Path("/tmp/swe_workspace") +if not SWE_WORKSPACE_ROOT.exists(): + SWE_WORKSPACE_ROOT.mkdir(parents=True) # SWE agent SWE_SETUP_PATH = METAGPT_ROOT / "metagpt/tools/swe_agent_commands/setup_default.sh" diff --git a/metagpt/roles/di/swe.py b/metagpt/roles/di/swe.py index 915d186b4..dde686e6a 100644 --- a/metagpt/roles/di/swe.py +++ b/metagpt/roles/di/swe.py @@ -25,7 +25,7 @@ class SWE(RoleZero): tools: list[str] = ["Bash", "Browser:goto,scroll"] terminal: Bash = Field(default_factory=Bash, exclude=True) output_diff: str = "" - max_react_loop: int = 30 + max_react_loop: int = 40 async def _think(self) -> bool: self._set_system_msg() diff --git a/tests/metagpt/roles/di/run_swe_new.py b/tests/metagpt/roles/di/run_swe_new.py index 94ef1e644..8ed1e9107 100644 --- a/tests/metagpt/roles/di/run_swe_new.py +++ b/tests/metagpt/roles/di/run_swe_new.py @@ -7,14 +7,14 @@ FIX_ISSUE1 = """ Write a fix for this issue: https://github.com/langchain-ai/langchain/issues/20453, you can fix it on this repo https://github.com/garylin2099/langchain, """ - -"checkout a branch named test-fix, commit your changes, push, and create a PR to the master branch of https://github.com/iorisa/langchain" +# + "checkout a branch named test-fix, commit your changes, push, and create a PR to the master branch of https://github.com/iorisa/langchain" FIX_ISSUE2 = """ Write a fix for this issue https://github.com/geekan/MetaGPT/issues/1275. You can fix it on the v0.8-release branch of this repo https://github.com/garylin2099/MetaGPT, -during fixing, checkout a branch named test-fix-1275, commit your changes, push, and create a PR to the v0.8-release branch of https://github.com/garylin2099/MetaGPT """ +# + "during fixing, checkout a branch named test-fix-1275, commit your changes, push, and create a PR to the v0.8-release branch of https://github.com/garylin2099/MetaGPT" + FIX_ISSUE3 = """ Write a fix for this issue https://github.com/geekan/MetaGPT/issues/1262. You can fix it on this repo https://github.com/garylin2099/MetaGPT, @@ -30,7 +30,10 @@ if __name__ == "__main__": swe_agent = SWE() logger.info("**** Starting run ****") user_requirement_and_issue = ( - FIX_ISSUE1 + # FIX_ISSUE1 + # FIX_ISSUE2 + # FIX_ISSUE3 + FIX_ISSUE_SIMPLE + """Because the environment is not available, you DO NOT need to run and modify any existing test case files or add new test case files to ensure that the bug is fixed.""" ) asyncio.run(swe_agent.run(user_requirement_and_issue)) From 5c0f490c049387dc334ea623f9efc2afb505ee7d Mon Sep 17 00:00:00 2001 From: seeker Date: Wed, 3 Jul 2024 22:46:21 +0800 Subject: [PATCH 040/119] update: SWE Agent --- metagpt/tools/libs/terminal.py | 4 ++-- tests/metagpt/roles/di/run_swe.py | 15 +++++---------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/metagpt/tools/libs/terminal.py b/metagpt/tools/libs/terminal.py index a04acb8e9..3270b0623 100644 --- a/metagpt/tools/libs/terminal.py +++ b/metagpt/tools/libs/terminal.py @@ -142,8 +142,8 @@ class Bash(Terminal): def __init__(self): """init""" super().__init__() - setup_cmd = f"source {SWE_SETUP_PATH}" - self.run_command(f"cd {SWE_WORKSPACE_ROOT} && {setup_cmd}") + self.run_command(f"cd {SWE_WORKSPACE_ROOT}") + self.run_command(f"source {SWE_SETUP_PATH}") def run(self, cmd) -> str: """ diff --git a/tests/metagpt/roles/di/run_swe.py b/tests/metagpt/roles/di/run_swe.py index f9d19be74..9965107db 100644 --- a/tests/metagpt/roles/di/run_swe.py +++ b/tests/metagpt/roles/di/run_swe.py @@ -1,7 +1,6 @@ import asyncio import json from datetime import datetime -from pathlib import Path from metagpt.config2 import config from metagpt.const import DEFAULT_WORKSPACE_ROOT, METAGPT_ROOT @@ -11,7 +10,7 @@ from metagpt.tools.libs.terminal import Terminal from metagpt.tools.swe_agent_commands.swe_agent_utils import load_hf_dataset # Specify by yourself -TEST_REPO_DIR = Path("/Users/seeker/Projects/sdfz/mg/mg-swe-agent") / "benchmark" / "swe_bench" / "data" / "test_repo" +TEST_REPO_DIR = METAGPT_ROOT / "data" / "test_repo" DATA_DIR = METAGPT_ROOT / "data/hugging_face" INSTANCE_TEMPLATE = """ @@ -56,17 +55,13 @@ async def run(instance, swe_result_dir): logger.info(f"Instance {instance['instance_id']} already exists, skipping execution.") return - repo_path = Path("/Users/seeker/Projects/other/test_repo") / ( - instance["repo"].replace("-", "_").replace("/", "__") + "_" + instance["version"] - ) - # repo_path = Path("/Users/seeker/Projects/other/test_repo") / instance["repo"].split("/")[-1] + repo_path = TEST_REPO_DIR / instance["repo"].replace("-", "_").replace("/", "__") + "_" + instance["version"] # 前处理 terminal = Terminal() terminal.run_command(f"cd {repo_path} && git reset --hard && git clean -n -d && git clean -f -d") terminal.run_command("BRANCH=$(git remote show origin | awk '/HEAD branch/ {print $NF}')") logger.info(terminal.run_command("echo $BRANCH")) - # logger.info(terminal.run_command(f'Branch name: $BRANCH')) logger.info(terminal.run_command('git checkout "$BRANCH"')) logger.info(terminal.run_command("git branch")) @@ -103,9 +98,9 @@ async def async_main(): dataset_path = "manna-ai/SWE-bench_Nano" # "princeton-nlp/SWE-bench_Lite" #"manna-ai/SWE-bench_Nano" dataset = load_hf_dataset(dataset_name_or_path=dataset_path, cache_dir=DATA_DIR, split="test") - date_time = datetime.now().strftime("%m-%d") - # _round = "first" - _round = "second" + date_time = datetime.now().strftime("%m%d") + _round = "first" + # _round = "second" exp_name = f"nano_mgx_{date_time}_{_round}" swe_result_dir = DEFAULT_WORKSPACE_ROOT / f"result_{config.llm.model.replace('/', '_')}" / exp_name swe_result_dir.mkdir(parents=True, exist_ok=True) From 816a6eadde46c0ea72ffcbae060a5bda20f3c29d Mon Sep 17 00:00:00 2001 From: seeker Date: Thu, 4 Jul 2024 16:07:22 +0800 Subject: [PATCH 041/119] update: SWE Agent --- metagpt/roles/di/{swe.py => swe_agent.py} | 12 +++--- metagpt/tools/libs/terminal.py | 4 +- metagpt/tools/swe_agent_commands/__init__.py | 2 +- ...n_swe.py => run_sweagent_for_benchmark.py} | 7 ++-- ...w.py => run_sweagent_open_source_issue.py} | 41 ++++++++++--------- 5 files changed, 36 insertions(+), 30 deletions(-) rename metagpt/roles/di/{swe.py => swe_agent.py} (92%) rename tests/metagpt/roles/di/{run_swe.py => run_sweagent_for_benchmark.py} (96%) rename tests/metagpt/roles/di/{run_swe_new.py => run_sweagent_open_source_issue.py} (53%) diff --git a/metagpt/roles/di/swe.py b/metagpt/roles/di/swe_agent.py similarity index 92% rename from metagpt/roles/di/swe.py rename to metagpt/roles/di/swe_agent.py index dde686e6a..e86b50a9d 100644 --- a/metagpt/roles/di/swe.py +++ b/metagpt/roles/di/swe_agent.py @@ -14,9 +14,9 @@ from metagpt.tools.libs.terminal import Bash from metagpt.tools.swe_agent_commands.swe_agent_utils import extract_patch -class SWE(RoleZero): - name: str = "SweAgent" - profile: str = "Software Engineer" +class SWEAgent(RoleZero): + name: str = "Swen" + profile: str = "Issue Solver" goal: str = "Resolve GitHub issue" _bash_window_size: int = 100 _system_msg: str = SWE_AGENT_SYSTEM_TEMPLATE @@ -26,12 +26,14 @@ class SWE(RoleZero): terminal: Bash = Field(default_factory=Bash, exclude=True) output_diff: str = "" max_react_loop: int = 40 + run_eval: bool = False async def _think(self) -> bool: self._set_system_msg() self._format_instruction() res = await super()._think() - await self._handle_action() + if self.run_eval: + await self._parse_commands_for_eval() return res def _set_system_msg(self): @@ -61,7 +63,7 @@ class SWE(RoleZero): return self.instruction - async def _handle_action(self): + async def _parse_commands_for_eval(self): """ Handles actions based on parsed commands. diff --git a/metagpt/tools/libs/terminal.py b/metagpt/tools/libs/terminal.py index 3270b0623..bcf039a5e 100644 --- a/metagpt/tools/libs/terminal.py +++ b/metagpt/tools/libs/terminal.py @@ -2,7 +2,7 @@ import subprocess import threading from queue import Queue -from metagpt.const import SWE_SETUP_PATH, SWE_WORKSPACE_ROOT +from metagpt.const import DEFAULT_WORKSPACE_ROOT, SWE_SETUP_PATH from metagpt.tools.tool_registry import register_tool from metagpt.utils.report import END_MARKER_VALUE, TerminalReporter @@ -142,7 +142,7 @@ class Bash(Terminal): def __init__(self): """init""" super().__init__() - self.run_command(f"cd {SWE_WORKSPACE_ROOT}") + self.run_command(f"cd {DEFAULT_WORKSPACE_ROOT}") self.run_command(f"source {SWE_SETUP_PATH}") def run(self, cmd) -> str: diff --git a/metagpt/tools/swe_agent_commands/__init__.py b/metagpt/tools/swe_agent_commands/__init__.py index 42e92a12d..c0d3e2a60 100644 --- a/metagpt/tools/swe_agent_commands/__init__.py +++ b/metagpt/tools/swe_agent_commands/__init__.py @@ -1,5 +1,5 @@ """ -This tool is originally developed by the team behind the princeton-nlp/SWE-agent repository. +This folder is borrowed from princeton-nlp/SWE-agent You can find the original repository here: https://github.com/princeton-nlp/SWE-agent/tree/main/config/commands We are using a modified version from OpenDevin: diff --git a/tests/metagpt/roles/di/run_swe.py b/tests/metagpt/roles/di/run_sweagent_for_benchmark.py similarity index 96% rename from tests/metagpt/roles/di/run_swe.py rename to tests/metagpt/roles/di/run_sweagent_for_benchmark.py index 9965107db..cdd8df5d7 100644 --- a/tests/metagpt/roles/di/run_swe.py +++ b/tests/metagpt/roles/di/run_sweagent_for_benchmark.py @@ -5,7 +5,7 @@ from datetime import datetime from metagpt.config2 import config from metagpt.const import DEFAULT_WORKSPACE_ROOT, METAGPT_ROOT from metagpt.logs import logger -from metagpt.roles.di.swe import SWE +from metagpt.roles.di.swe_agent import SWEAgent from metagpt.tools.libs.terminal import Terminal from metagpt.tools.swe_agent_commands.swe_agent_utils import load_hf_dataset @@ -74,13 +74,14 @@ async def run(instance, swe_result_dir): ) logger.info(f"**** Starting to run {instance['instance_id']}****") - swe_agent = SWE() + swe_agent = SWEAgent() + swe_agent.run_eval = True await swe_agent.run(user_requirement_and_issue) save_predictions(swe_agent, instance, swe_result_dir) logger.info(f"**** Finished running {instance['instance_id']}****") -def save_predictions(swe_agent: SWE, instance, swe_result_dir): +def save_predictions(swe_agent: SWEAgent, instance, swe_result_dir): output_file = swe_result_dir / "all_preds.jsonl" instance["model_name_or_path"] = swe_agent.config.llm.model instance["model_patch"] = swe_agent.output_diff diff --git a/tests/metagpt/roles/di/run_swe_new.py b/tests/metagpt/roles/di/run_sweagent_open_source_issue.py similarity index 53% rename from tests/metagpt/roles/di/run_swe_new.py rename to tests/metagpt/roles/di/run_sweagent_open_source_issue.py index 8ed1e9107..ec87dd7e2 100644 --- a/tests/metagpt/roles/di/run_swe_new.py +++ b/tests/metagpt/roles/di/run_sweagent_open_source_issue.py @@ -1,41 +1,44 @@ import asyncio from metagpt.logs import logger -from metagpt.roles.di.swe import SWE +from metagpt.roles.di.swe_agent import SWEAgent FIX_ISSUE1 = """ Write a fix for this issue: https://github.com/langchain-ai/langchain/issues/20453, -you can fix it on this repo https://github.com/garylin2099/langchain, +you can fix it on this repo https://github.com/garylin2099/langchain """ -# + "checkout a branch named test-fix, commit your changes, push, and create a PR to the master branch of https://github.com/iorisa/langchain" - +# + "checkout a branch named test-fix, commit your changes, push, +# and create a PR to the master branch of https://github.com/iorisa/langchain" +# """ FIX_ISSUE2 = """ Write a fix for this issue https://github.com/geekan/MetaGPT/issues/1275. -You can fix it on the v0.8-release branch of this repo https://github.com/garylin2099/MetaGPT, +You can fix it on the v0.8-release branch of this repo https://github.com/garylin2099/MetaGPT """ -# + "during fixing, checkout a branch named test-fix-1275, commit your changes, push, and create a PR to the v0.8-release branch of https://github.com/garylin2099/MetaGPT" +# + "during fixing, checkout a branch named test-fix-1275, commit your changes, push, +# and create a PR to the v0.8-release branch of https://github.com/garylin2099/MetaGPT" FIX_ISSUE3 = """ Write a fix for this issue https://github.com/geekan/MetaGPT/issues/1262. -You can fix it on this repo https://github.com/garylin2099/MetaGPT, -during fixing, checkout a branch named test-fix-1262, commit your changes, push, and create a PR to https://github.com/garylin2099/MetaGPT +You can fix it on this repo https://github.com/garylin2099/MetaGPT """ +# during fixing, checkout a branch named test-fix-1262, commit your changes, push, +# and create a PR to https://github.com/garylin2099/MetaGPT +# """ FIX_ISSUE_SIMPLE = """ Write a fix for this issue: https://github.com/mannaandpoem/simple_calculator/issues/1, -you can fix it on this repo https://github.com/garylin2099/simple_calculator, -checkout a branch named test, commit your changes, push, and create a PR to the master branch of original repo. +you can fix it on this repo https://github.com/garylin2099/simple_calculator """ +# checkout a branch named test, commit your changes, push, and create a PR to the master branch of original repo. +# """ + +NO_ENV_TIP = """ +Because the environment is not available, you DO NOT need to run and modify any existing test case files or +add new test case files to ensure that the bug is fixed. +""" if __name__ == "__main__": - swe_agent = SWE() + swe_agent = SWEAgent() logger.info("**** Starting run ****") - user_requirement_and_issue = ( - # FIX_ISSUE1 - # FIX_ISSUE2 - # FIX_ISSUE3 - FIX_ISSUE_SIMPLE - + """Because the environment is not available, you DO NOT need to run and modify any existing test case files or add new test case files to ensure that the bug is fixed.""" - ) + user_requirement_and_issue = FIX_ISSUE1 + NO_ENV_TIP asyncio.run(swe_agent.run(user_requirement_and_issue)) logger.info("**** Finished running ****") - logger.info(f"Patch: {swe_agent.output_diff}") From 51bba81415a3096a4ebb974b24a4fee10bbccd84 Mon Sep 17 00:00:00 2001 From: seeker Date: Thu, 4 Jul 2024 16:12:49 +0800 Subject: [PATCH 042/119] update: SWE Agent --- metagpt/roles/di/swe_agent.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/metagpt/roles/di/swe_agent.py b/metagpt/roles/di/swe_agent.py index e86b50a9d..166de3918 100644 --- a/metagpt/roles/di/swe_agent.py +++ b/metagpt/roles/di/swe_agent.py @@ -29,14 +29,14 @@ class SWEAgent(RoleZero): run_eval: bool = False async def _think(self) -> bool: - self._set_system_msg() + self._update_system_msg() self._format_instruction() res = await super()._think() if self.run_eval: await self._parse_commands_for_eval() return res - def _set_system_msg(self): + def _update_system_msg(self): """ Sets the system message for the SWE agent. From 729c3914635751869da9fd94d1253e47c43af802 Mon Sep 17 00:00:00 2001 From: seeker Date: Thu, 4 Jul 2024 16:18:36 +0800 Subject: [PATCH 043/119] update: SWE Agent --- metagpt/prompts/di/{swe.py => swe_agent.py} | 0 ...n_sweagent_for_benchmark.py => run_swe_agent_for_benchmark.py} | 0 ...nt_open_source_issue.py => run_swe_agent_open_source_issue.py} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename metagpt/prompts/di/{swe.py => swe_agent.py} (100%) rename tests/metagpt/roles/di/{run_sweagent_for_benchmark.py => run_swe_agent_for_benchmark.py} (100%) rename tests/metagpt/roles/di/{run_sweagent_open_source_issue.py => run_swe_agent_open_source_issue.py} (100%) diff --git a/metagpt/prompts/di/swe.py b/metagpt/prompts/di/swe_agent.py similarity index 100% rename from metagpt/prompts/di/swe.py rename to metagpt/prompts/di/swe_agent.py diff --git a/tests/metagpt/roles/di/run_sweagent_for_benchmark.py b/tests/metagpt/roles/di/run_swe_agent_for_benchmark.py similarity index 100% rename from tests/metagpt/roles/di/run_sweagent_for_benchmark.py rename to tests/metagpt/roles/di/run_swe_agent_for_benchmark.py diff --git a/tests/metagpt/roles/di/run_sweagent_open_source_issue.py b/tests/metagpt/roles/di/run_swe_agent_open_source_issue.py similarity index 100% rename from tests/metagpt/roles/di/run_sweagent_open_source_issue.py rename to tests/metagpt/roles/di/run_swe_agent_open_source_issue.py From cdd222c4c50e6f6f1169c84cc4193e249023e7a8 Mon Sep 17 00:00:00 2001 From: seeker Date: Thu, 4 Jul 2024 16:25:13 +0800 Subject: [PATCH 044/119] update: SWE Agent --- metagpt/const.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/metagpt/const.py b/metagpt/const.py index 6a0e2c4bb..c78a22641 100644 --- a/metagpt/const.py +++ b/metagpt/const.py @@ -149,8 +149,6 @@ METAGPT_REPORTER_DEFAULT_URL = os.environ.get("METAGPT_REPORTER_URL", "") # Metadata defines AGENT = "agent" -SWE_WORKSPACE_ROOT = Path("/tmp/swe_workspace") -if not SWE_WORKSPACE_ROOT.exists(): - SWE_WORKSPACE_ROOT.mkdir(parents=True) + # SWE agent SWE_SETUP_PATH = METAGPT_ROOT / "metagpt/tools/swe_agent_commands/setup_default.sh" From c5a39fcf408eb4ed2efd977d396319207f905e7e Mon Sep 17 00:00:00 2001 From: seeker Date: Thu, 4 Jul 2024 17:40:36 +0800 Subject: [PATCH 045/119] fix: bug --- metagpt/roles/di/swe_agent.py | 2 +- tests/metagpt/roles/di/run_swe_agent_for_benchmark.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/metagpt/roles/di/swe_agent.py b/metagpt/roles/di/swe_agent.py index 166de3918..5acce5f9d 100644 --- a/metagpt/roles/di/swe_agent.py +++ b/metagpt/roles/di/swe_agent.py @@ -4,7 +4,7 @@ import os from pydantic import Field from metagpt.logs import logger -from metagpt.prompts.di.swe import ( +from metagpt.prompts.di.swe_agent import ( MINIMAL_EXAMPLE, NEXT_STEP_TEMPLATE, SWE_AGENT_SYSTEM_TEMPLATE, diff --git a/tests/metagpt/roles/di/run_swe_agent_for_benchmark.py b/tests/metagpt/roles/di/run_swe_agent_for_benchmark.py index cdd8df5d7..54b3623a4 100644 --- a/tests/metagpt/roles/di/run_swe_agent_for_benchmark.py +++ b/tests/metagpt/roles/di/run_swe_agent_for_benchmark.py @@ -55,7 +55,7 @@ async def run(instance, swe_result_dir): logger.info(f"Instance {instance['instance_id']} already exists, skipping execution.") return - repo_path = TEST_REPO_DIR / instance["repo"].replace("-", "_").replace("/", "__") + "_" + instance["version"] + repo_path = TEST_REPO_DIR / (instance["repo"].replace("-", "_").replace("/", "__") + "_" + instance["version"]) # 前处理 terminal = Terminal() From d7fa3059c7dfb8b6dc84d3779efbed420b785f7d Mon Sep 17 00:00:00 2001 From: zhanglei Date: Fri, 5 Jul 2024 13:39:19 +0800 Subject: [PATCH 046/119] update: update TL name --- metagpt/roles/di/team_leader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/roles/di/team_leader.py b/metagpt/roles/di/team_leader.py index 2932dd7f0..7915edcf6 100644 --- a/metagpt/roles/di/team_leader.py +++ b/metagpt/roles/di/team_leader.py @@ -14,7 +14,7 @@ from metagpt.tools.tool_registry import register_tool @register_tool(include_functions=["publish_team_message"]) class TeamLeader(RoleZero): - name: str = "Tim" + name: str = "Mike" profile: str = "Team Leader" system_msg: list[str] = [SYSTEM_PROMPT] From 338a25424aa109d6fa6edfa37246f82306685322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E5=B0=91=E6=9D=B0?= Date: Fri, 5 Jul 2024 07:52:26 +0000 Subject: [PATCH 047/119] fix: swe setup_default.sh path error --- metagpt/const.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/metagpt/const.py b/metagpt/const.py index c78a22641..17ce9210a 100644 --- a/metagpt/const.py +++ b/metagpt/const.py @@ -20,12 +20,6 @@ import metagpt def get_metagpt_package_root(): """Get the root directory of the installed package.""" package_root = Path(metagpt.__file__).parent.parent - for i in (".git", ".project_root", ".gitignore"): - if (package_root / i).exists(): - break - else: - package_root = Path.cwd() - logger.info(f"Package root set to {str(package_root)}") return package_root @@ -40,6 +34,12 @@ def get_metagpt_root(): else: # Fallback to package root if no environment variable is set project_root = get_metagpt_package_root() + for i in (".git", ".project_root", ".gitignore"): + if (project_root / i).exists(): + break + else: + project_root = Path.cwd() + return project_root @@ -151,4 +151,4 @@ METAGPT_REPORTER_DEFAULT_URL = os.environ.get("METAGPT_REPORTER_URL", "") AGENT = "agent" # SWE agent -SWE_SETUP_PATH = METAGPT_ROOT / "metagpt/tools/swe_agent_commands/setup_default.sh" +SWE_SETUP_PATH = get_metagpt_package_root() / "metagpt/tools/swe_agent_commands/setup_default.sh" From 306dc278d28f7f48b08fad85703d5783fb01940e Mon Sep 17 00:00:00 2001 From: seeker Date: Fri, 5 Jul 2024 20:36:21 +0800 Subject: [PATCH 048/119] =?UTF-8?q?update:=20=E5=A2=9E=E5=8A=A0=20swe=20ag?= =?UTF-8?q?ent=20=E4=BD=BF=E7=94=A8=20git=20push=20=E5=92=8C=20create=20pr?= =?UTF-8?q?=20=E7=9A=84=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- metagpt/roles/di/swe_agent.py | 21 +++++++++++--- metagpt/tools/libs/git.py | 54 +++++++++-------------------------- 2 files changed, 31 insertions(+), 44 deletions(-) diff --git a/metagpt/roles/di/swe_agent.py b/metagpt/roles/di/swe_agent.py index 2e1fb6412..d0458a22f 100644 --- a/metagpt/roles/di/swe_agent.py +++ b/metagpt/roles/di/swe_agent.py @@ -10,6 +10,7 @@ from metagpt.prompts.di.swe_agent import ( SWE_AGENT_SYSTEM_TEMPLATE, ) from metagpt.roles.di.role_zero import RoleZero +from metagpt.tools.libs.git import git_create_pull, git_push from metagpt.tools.libs.terminal import Bash @@ -21,7 +22,13 @@ class SWEAgent(RoleZero): _system_msg: str = SWE_AGENT_SYSTEM_TEMPLATE system_msg: list[str] = [_system_msg.format(WINDOW=_bash_window_size)] _instruction: str = NEXT_STEP_TEMPLATE - tools: list[str] = ["Bash", "Browser:goto,scroll"] + tools: list[str] = [ + "Bash", + "Browser:goto,scroll", + "RoleZero", + "git_push", + "git_create_pull", + ] terminal: Bash = Field(default_factory=Bash, exclude=True) output_diff: str = "" max_react_loop: int = 40 @@ -35,6 +42,15 @@ class SWEAgent(RoleZero): await self._parse_commands_for_eval() return res + def _update_tool_execution(self): + self.tool_execution_map.update( + { + "Bash.run": self.terminal.run, + "git_push": git_push, + "git_create_pull": git_create_pull, + } + ) + def _update_system_msg(self): """ Sets the system message for the SWE agent. @@ -90,8 +106,5 @@ class SWEAgent(RoleZero): except Exception as e: logger.error(f"Error during submission: {e}") - def _update_tool_execution(self): - self.tool_execution_map.update({"Bash.run": self.terminal.run}) - def _retrieve_experience(self) -> str: return MINIMAL_EXAMPLE diff --git a/metagpt/tools/libs/git.py b/metagpt/tools/libs/git.py index 740cb81f9..4606b5815 100644 --- a/metagpt/tools/libs/git.py +++ b/metagpt/tools/libs/git.py @@ -14,7 +14,7 @@ from metagpt.tools.tool_registry import register_tool @register_tool(tags=["software development", "git", "Commit the changes and push to remote git repository."]) async def git_push( local_path: Union[str, Path], - access_token: str, + app_name: str, comments: str = "Commit", new_branch: str = "", ) -> "GitBranch": @@ -23,7 +23,7 @@ async def git_push( Args: local_path (Union[str, Path]): The path to the local Git repository. - access_token (str): The access token for authentication. Use `get_env` to get access token. + app_name (str): The name of the application where the repository is hosted. For example, "github", "gitlab", "bitbucket", etc. comments (str, optional): The commit message to use. Defaults to "Commit". new_branch (str, optional): The name of the new branch to create and push changes to. If not provided, changes will be pushed to the current branch. Defaults to "". @@ -36,11 +36,10 @@ async def git_push( Example: >>> url = "https://github.com/iorisa/snake-game.git" >>> local_path = await git_clone(url=url) - >>> from metagpt.tools.libs import get_env - >>> access_token = await get_env(key="access_token", app_name="github") # Read access token from enviroment variables. + >>> app_name="github" >>> comments = "Archive" >>> new_branch = "feature/new" - >>> branch = await git_push(local_path=local_path, access_token=access_token, comments=comments, new_branch=new_branch) + >>> branch = await git_push(local_path=local_path, app_name=app_name, comments=comments, new_branch=new_branch) >>> base = branch.base >>> head = branch.head >>> repo_name = branch.repo_name @@ -48,12 +47,15 @@ async def git_push( base branch:'master', head branch:'feature/new', repo_name:'iorisa/snake-game' """ + from metagpt.tools.libs import get_env from metagpt.utils.git_repository import GitRepository if not GitRepository.is_git_dir(local_path): raise ValueError("Invalid local git repository") repo = GitRepository(local_path=local_path, auto_init=False) + # Read access token from environment variables. + access_token = await get_env(key="access_token", app_name=app_name) branch = await repo.push(new_branch=new_branch, comments=comments, access_token=access_token) return branch @@ -63,7 +65,7 @@ async def git_create_pull( base: str, head: str, base_repo_name: str, - access_token: str, + app_name: str, head_repo_name: Optional[str] = None, title: Optional[str] = None, body: Optional[str] = None, @@ -76,43 +78,13 @@ async def git_create_pull( base (str): The base branch of the pull request. head (str): The head branch of the pull request. base_repo_name (str): The full repository name (user/repo) where the pull request will be created. - access_token (str): The access token for authentication. Use `get_env` to get access token. + app_name (str): The name of the application where the repository is hosted. For example, "github", "gitlab", "bitbucket", etc. head_repo_name (Optional[str], optional): The full repository name (user/repo) where the pull request will merge from. Defaults to None. title (Optional[str], optional): The title of the pull request. Defaults to None. body (Optional[str], optional): The body of the pull request. Defaults to None. issue (Optional[Issue], optional): The related issue of the pull request. Defaults to None. Example: - >>> # push and create pull - >>> url = "https://github.com/iorisa/snake-game.git" - >>> local_path = await git_clone(url=url) - >>> from metagpt.tools.libs import get_env - >>> access_token = await get_env(key="access_token", app_name="github") - >>> comments = "Archive" - >>> new_branch = "feature/new" - >>> branch = await git_push(local_path=local_path, access_token=access_token, comments=comments, new_branch=new_branch) - >>> base = branch.base - >>> head = branch.head - >>> repo_name = branch.repo_name - >>> print(f"base branch:'{base}', head branch:'{head}', repo_name:'{repo_name}'") - base branch:'master', head branch:'feature/new', repo_name:'iorisa/snake-game' - >>> title = "feat: modify http lib", - >>> body = "Change HTTP library used to send requests" - >>> pr = await git_create_pull( - >>> base_repo_name=repo_name, - >>> base=base, - >>> head=head, - >>> title=title, - >>> body=body, - >>> access_token=access_token, - >>> ) - >>> if isinstance(pr, PullRequest): - >>> print(pr) - PullRequest("feat: modify http lib") - >>> if isinstance(pr, str): - >>> print(f"Visit this url to create a new pull request: '{pr}'") - Visit this url to create a new pull request: 'https://github.com/iorisa/snake-game/compare/master...feature/new' - >>> # create pull request >>> base_repo_name = "geekan/MetaGPT" >>> head_repo_name = "ioris/MetaGPT" @@ -120,8 +92,7 @@ async def git_create_pull( >>> head = "feature/http" >>> title = "feat: modify http lib", >>> body = "Change HTTP library used to send requests" - >>> from metagpt.tools.libs import get_env - >>> access_token = await get_env(key="access_token", app_name="github") + >>> app_name = "github" >>> pr = await git_create_pull( >>> base_repo_name=base_repo_name, >>> head_repo_name=head_repo_name, @@ -129,7 +100,7 @@ async def git_create_pull( >>> head=head, >>> title=title, >>> body=body, - >>> access_token=access_token, + >>> app_name=app_name, >>> ) >>> if isinstance(pr, PullRequest): >>> print(pr) @@ -141,8 +112,11 @@ async def git_create_pull( Returns: PullRequest: The created pull request. """ + + from metagpt.tools.libs import get_env from metagpt.utils.git_repository import GitRepository + access_token = await get_env(key="access_token", app_name=app_name) return await GitRepository.create_pull( base=base, head=head, From c624c0ffc74735e5467d6da3fa99adc0678f648c Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Mon, 8 Jul 2024 10:09:36 +0800 Subject: [PATCH 049/119] use llm cache to make exp_pool --- config/config2.example.yaml | 6 +- examples/exp_pool/decorator.py | 7 +- examples/write_novel.py | 4 +- metagpt/actions/action.py | 2 +- metagpt/actions/action_node.py | 31 +- metagpt/actions/design_api.py | 4 +- metagpt/actions/generate_questions.py | 2 +- metagpt/actions/prepare_interview.py | 2 +- metagpt/actions/project_management.py | 4 +- metagpt/actions/write_code_an_draft.py | 2 +- .../actions/write_code_plan_and_change_an.py | 2 +- metagpt/actions/write_prd.py | 8 +- metagpt/actions/write_review.py | 2 +- metagpt/configs/exp_pool_config.py | 8 +- metagpt/exp_pool/context_builders/__init__.py | 7 + metagpt/exp_pool/context_builders/base.py | 52 +++ .../exp_pool/context_builders/role_zero.py | 26 ++ metagpt/exp_pool/context_builders/simple.py | 24 ++ metagpt/exp_pool/decorator.py | 165 +++++---- metagpt/exp_pool/manager.py | 91 ++--- metagpt/exp_pool/perfect_judges/__init__.py | 6 + metagpt/exp_pool/perfect_judges/base.py | 20 ++ metagpt/exp_pool/perfect_judges/simple.py | 27 ++ metagpt/exp_pool/schema.py | 7 +- metagpt/exp_pool/scorers/__init__.py | 6 +- metagpt/exp_pool/scorers/base.py | 6 +- metagpt/exp_pool/scorers/simple.py | 6 +- metagpt/roles/di/role_zero.py | 41 ++- metagpt/strategy/solver.py | 2 +- metagpt/utils/reflection.py | 23 -- tests/metagpt/actions/test_action_node.py | 14 +- tests/metagpt/actions/test_design_api_an.py | 2 +- .../actions/test_project_management_an.py | 4 +- tests/metagpt/actions/test_write_prd_an.py | 2 +- .../test_base_context_builder.py | 45 +++ .../test_rolezero_context_builder.py | 38 +++ .../test_simple_context_builder.py | 46 +++ tests/metagpt/exp_pool/test_decorator.py | 316 ++++++++---------- tests/metagpt/exp_pool/test_manager.py | 63 +++- .../test_simple_perfect_judge.py | 40 +++ .../test_scorers/test_simple_scorer.py | 49 +++ 41 files changed, 844 insertions(+), 368 deletions(-) create mode 100644 metagpt/exp_pool/context_builders/__init__.py create mode 100644 metagpt/exp_pool/context_builders/base.py create mode 100644 metagpt/exp_pool/context_builders/role_zero.py create mode 100644 metagpt/exp_pool/context_builders/simple.py create mode 100644 metagpt/exp_pool/perfect_judges/__init__.py create mode 100644 metagpt/exp_pool/perfect_judges/base.py create mode 100644 metagpt/exp_pool/perfect_judges/simple.py create mode 100644 tests/metagpt/exp_pool/test_context_builders/test_base_context_builder.py create mode 100644 tests/metagpt/exp_pool/test_context_builders/test_rolezero_context_builder.py create mode 100644 tests/metagpt/exp_pool/test_context_builders/test_simple_context_builder.py create mode 100644 tests/metagpt/exp_pool/test_perfect_judges/test_simple_perfect_judge.py create mode 100644 tests/metagpt/exp_pool/test_scorers/test_simple_scorer.py diff --git a/config/config2.example.yaml b/config/config2.example.yaml index c7b2cae2c..a3bd5c367 100644 --- a/config/config2.example.yaml +++ b/config/config2.example.yaml @@ -75,8 +75,10 @@ s3: bucket: "test" exp_pool: - enable_read: true - enable_write: true + enable_read: false + enable_write: false + persist_path: .chroma_exp_data # The directory. + init_exp: false # If set to true, basic experiences associated with the roles will be added to the experience pool. azure_tts_subscription_key: "YOUR_SUBSCRIPTION_KEY" azure_tts_region: "eastus" diff --git a/examples/exp_pool/decorator.py b/examples/exp_pool/decorator.py index 3f6093e01..00726a0a8 100644 --- a/examples/exp_pool/decorator.py +++ b/examples/exp_pool/decorator.py @@ -7,16 +7,15 @@ from metagpt.exp_pool import exp_cache, exp_manager from metagpt.logs import logger -@exp_cache(pass_exps_to_func=True) -async def produce(req, exps=None): - logger.info(f"Previous experiences: {exps}") +@exp_cache() +async def produce(req=""): return f"{req} {uuid.uuid4().hex}" async def main(): req = "Water" - resp = await produce(req) + resp = await produce(req=req) logger.info(f"The resp of `produce{req}` is: {resp}") exps = await exp_manager.query_exps(req) diff --git a/examples/write_novel.py b/examples/write_novel.py index a6e9ce05d..f49918fbb 100644 --- a/examples/write_novel.py +++ b/examples/write_novel.py @@ -50,9 +50,9 @@ async def generate_novel(): "Fill the empty nodes with your own ideas. Be creative! Use your own words!" "I will tip you $100,000 if you write a good novel." ) - novel_node = await ActionNode.from_pydantic(Novel).fill(context=instruction, llm=LLM()) + novel_node = await ActionNode.from_pydantic(Novel).fill(req=instruction, llm=LLM()) chap_node = await ActionNode.from_pydantic(Chapters).fill( - context=f"### instruction\n{instruction}\n### novel\n{novel_node.content}", llm=LLM() + req=f"### instruction\n{instruction}\n### novel\n{novel_node.content}", llm=LLM() ) print(chap_node.instruct_content) diff --git a/metagpt/actions/action.py b/metagpt/actions/action.py index b760c96d8..8733947f5 100644 --- a/metagpt/actions/action.py +++ b/metagpt/actions/action.py @@ -90,7 +90,7 @@ class Action(SerializationMixin, ContextMixin, BaseModel): msgs = args[0] context = "## History Messages\n" context += "\n".join([f"{idx}: {i}" for idx, i in enumerate(reversed(msgs))]) - return await self.node.fill(context=context, llm=self.llm) + return await self.node.fill(req=context, llm=self.llm) async def run(self, *args, **kwargs): """Run action""" diff --git a/metagpt/actions/action_node.py b/metagpt/actions/action_node.py index 48372f790..e1e0bddbb 100644 --- a/metagpt/actions/action_node.py +++ b/metagpt/actions/action_node.py @@ -18,6 +18,7 @@ from tenacity import retry, stop_after_attempt, wait_random_exponential from metagpt.actions.action_outcls_registry import register_action_outcls from metagpt.const import MARKDOWN_TITLE_PREFIX, USE_CONFIG_TIMEOUT +from metagpt.exp_pool import exp_cache from metagpt.llm import BaseLLM from metagpt.logs import logger from metagpt.provider.postprocess.llm_output_postprocess import llm_output_postprocess @@ -465,9 +466,33 @@ class ActionNode: return self + @classmethod + def deserialize_to_action_node(cls, serialized_data) -> "ActionNode": + """Customized deserialization, it will be triggered when a perfect experience is found. + + ActionNode cannot be serialized, it throws an error 'cannot pickle 'SSLContext' object'. + """ + + class InstructContent: + def __init__(self, json_data): + self.json_data = json_data + + def model_dump_json(self): + return self.json_data + + action_node = cls(key="", expected_type=Type[str], instruction="", example="") + action_node.instruct_content = InstructContent(serialized_data) + + return action_node + + @exp_cache( + resp_serialize=lambda action_node: action_node.instruct_content.model_dump_json(), + resp_deserialize=lambda resp: ActionNode.deserialize_to_action_node(resp), + ) async def fill( self, - context, + *, + req, llm, schema="json", mode="auto", @@ -478,7 +503,7 @@ class ActionNode: ): """Fill the node(s) with mode. - :param context: Everything we should know when filling node. + :param req: Everything we should know when filling node. :param llm: Large Language Model with pre-defined system message. :param schema: json/markdown, determine example and output format. - raw: free form text @@ -497,7 +522,7 @@ class ActionNode: :return: self """ self.set_llm(llm) - self.set_context(context) + self.set_context(req) if self.schema: schema = self.schema diff --git a/metagpt/actions/design_api.py b/metagpt/actions/design_api.py index cc88171ff..1bfad20a2 100644 --- a/metagpt/actions/design_api.py +++ b/metagpt/actions/design_api.py @@ -178,12 +178,12 @@ class WriteDesign(Action): ) async def _new_system_design(self, context): - node = await DESIGN_API_NODE.fill(context=context, llm=self.llm, schema=self.prompt_schema) + node = await DESIGN_API_NODE.fill(req=context, llm=self.llm, schema=self.prompt_schema) return node async def _merge(self, prd_doc, system_design_doc): context = NEW_REQ_TEMPLATE.format(old_design=system_design_doc.content, context=prd_doc.content) - node = await REFINED_DESIGN_NODE.fill(context=context, llm=self.llm, schema=self.prompt_schema) + node = await REFINED_DESIGN_NODE.fill(req=context, llm=self.llm, schema=self.prompt_schema) system_design_doc.content = node.instruct_content.model_dump_json() return system_design_doc diff --git a/metagpt/actions/generate_questions.py b/metagpt/actions/generate_questions.py index c96a37649..bf0ba6277 100644 --- a/metagpt/actions/generate_questions.py +++ b/metagpt/actions/generate_questions.py @@ -22,4 +22,4 @@ class GenerateQuestions(Action): name: str = "GenerateQuestions" async def run(self, context) -> ActionNode: - return await QUESTIONS.fill(context=context, llm=self.llm) + return await QUESTIONS.fill(req=context, llm=self.llm) diff --git a/metagpt/actions/prepare_interview.py b/metagpt/actions/prepare_interview.py index 04cc954d2..0a7eb6581 100644 --- a/metagpt/actions/prepare_interview.py +++ b/metagpt/actions/prepare_interview.py @@ -22,4 +22,4 @@ class PrepareInterview(Action): name: str = "PrepareInterview" async def run(self, context): - return await QUESTIONS.fill(context=context, llm=self.llm) + return await QUESTIONS.fill(req=context, llm=self.llm) diff --git a/metagpt/actions/project_management.py b/metagpt/actions/project_management.py index a39840bf1..ca2df2da9 100644 --- a/metagpt/actions/project_management.py +++ b/metagpt/actions/project_management.py @@ -151,12 +151,12 @@ class WriteTasks(Action): return task_doc async def _run_new_tasks(self, context: str): - node = await PM_NODE.fill(context, self.llm, schema=self.prompt_schema) + node = await PM_NODE.fill(req=context, llm=self.llm, schema=self.prompt_schema) return node async def _merge(self, system_design_doc, task_doc) -> Document: context = NEW_REQ_TEMPLATE.format(context=system_design_doc.content, old_task=task_doc.content) - node = await REFINED_PM_NODE.fill(context, self.llm, schema=self.prompt_schema) + node = await REFINED_PM_NODE.fill(req=context, llm=self.llm, schema=self.prompt_schema) task_doc.content = node.instruct_content.model_dump_json() return task_doc diff --git a/metagpt/actions/write_code_an_draft.py b/metagpt/actions/write_code_an_draft.py index ce030b0e9..4c3fd4c19 100644 --- a/metagpt/actions/write_code_an_draft.py +++ b/metagpt/actions/write_code_an_draft.py @@ -578,7 +578,7 @@ class WriteCodeAN(Action): async def run(self, context): self.llm.system_prompt = "You are an outstanding engineer and can implement any code" - return await WRITE_MOVE_NODE.fill(context=context, llm=self.llm, schema="json") + return await WRITE_MOVE_NODE.fill(req=context, llm=self.llm, schema="json") async def main(): diff --git a/metagpt/actions/write_code_plan_and_change_an.py b/metagpt/actions/write_code_plan_and_change_an.py index 31482a94d..989df52f2 100644 --- a/metagpt/actions/write_code_plan_and_change_an.py +++ b/metagpt/actions/write_code_plan_and_change_an.py @@ -229,7 +229,7 @@ class WriteCodePlanAndChange(Action): code=await self.get_old_codes(), ) logger.info("Writing code plan and change..") - return await WRITE_CODE_PLAN_AND_CHANGE_NODE.fill(context=context, llm=self.llm, schema="json") + return await WRITE_CODE_PLAN_AND_CHANGE_NODE.fill(req=context, llm=self.llm, schema="json") async def get_old_codes(self) -> str: old_codes = await self.repo.srcs.get_all() diff --git a/metagpt/actions/write_prd.py b/metagpt/actions/write_prd.py index 7199ec415..810823a24 100644 --- a/metagpt/actions/write_prd.py +++ b/metagpt/actions/write_prd.py @@ -211,7 +211,7 @@ class WritePRD(Action): context = CONTEXT_TEMPLATE.format(requirements=requirement, project_name=project_name) exclude = [PROJECT_NAME.key] if project_name else [] node = await WRITE_PRD_NODE.fill( - context=context, llm=self.llm, exclude=exclude, schema=self.prompt_schema + req=context, llm=self.llm, exclude=exclude, schema=self.prompt_schema ) # schema=schema return node @@ -238,7 +238,7 @@ class WritePRD(Action): async def _is_bugfix(self, context: str) -> bool: if not self.repo.code_files_exists(): return False - node = await WP_ISSUE_TYPE_NODE.fill(context, self.llm) + node = await WP_ISSUE_TYPE_NODE.fill(req=context, llm=self.llm) return node.get("issue_type") == "BUG" async def get_related_docs(self, req: Document, docs: list[Document]) -> list[Document]: @@ -248,14 +248,14 @@ class WritePRD(Action): async def _is_related(self, req: Document, old_prd: Document) -> bool: context = NEW_REQ_TEMPLATE.format(old_prd=old_prd.content, requirements=req.content) - node = await WP_IS_RELATIVE_NODE.fill(context, self.llm) + node = await WP_IS_RELATIVE_NODE.fill(req=context, llm=self.llm) return node.get("is_relative") == "YES" async def _merge(self, req: Document, related_doc: Document) -> Document: if not self.project_name: self.project_name = Path(self.project_path).name prompt = NEW_REQ_TEMPLATE.format(requirements=req.content, old_prd=related_doc.content) - node = await REFINED_PRD_NODE.fill(context=prompt, llm=self.llm, schema=self.prompt_schema) + node = await REFINED_PRD_NODE.fill(req=prompt, llm=self.llm, schema=self.prompt_schema) related_doc.content = node.instruct_content.model_dump_json() await self._rename_workspace(node) return related_doc diff --git a/metagpt/actions/write_review.py b/metagpt/actions/write_review.py index db8512946..907a1e990 100644 --- a/metagpt/actions/write_review.py +++ b/metagpt/actions/write_review.py @@ -36,4 +36,4 @@ class WriteReview(Action): name: str = "WriteReview" async def run(self, context): - return await WRITE_REVIEW_NODE.fill(context=context, llm=self.llm, schema="json") + return await WRITE_REVIEW_NODE.fill(req=context, llm=self.llm, schema="json") diff --git a/metagpt/configs/exp_pool_config.py b/metagpt/configs/exp_pool_config.py index 3f86173c1..0c92312da 100644 --- a/metagpt/configs/exp_pool_config.py +++ b/metagpt/configs/exp_pool_config.py @@ -4,5 +4,9 @@ from metagpt.utils.yaml_model import YamlModel class ExperiencePoolConfig(YamlModel): - enable_read: bool = Field(default=True, description="Enable to read from experience pool.") - enable_write: bool = Field(default=True, description="Enable to write to experience pool.") + enable_read: bool = Field(default=False, description="Enable to read from experience pool.") + enable_write: bool = Field(default=False, description="Enable to write to experience pool.") + persist_path: str = Field(default=".chroma_exp_data", description="The persist path for experience pool.") + init_exp: bool = Field( + default=False, description="Put some basic experiences associated with the roles into the experience pool." + ) diff --git a/metagpt/exp_pool/context_builders/__init__.py b/metagpt/exp_pool/context_builders/__init__.py new file mode 100644 index 000000000..047558be0 --- /dev/null +++ b/metagpt/exp_pool/context_builders/__init__.py @@ -0,0 +1,7 @@ +"""Context builders init.""" + +from metagpt.exp_pool.context_builders.base import BaseContextBuilder +from metagpt.exp_pool.context_builders.simple import SimpleContextBuilder +from metagpt.exp_pool.context_builders.role_zero import RoleZeroContextBuilder + +__all__ = ["BaseContextBuilder", "SimpleContextBuilder", "RoleZeroContextBuilder"] diff --git a/metagpt/exp_pool/context_builders/base.py b/metagpt/exp_pool/context_builders/base.py new file mode 100644 index 000000000..e3fe320a6 --- /dev/null +++ b/metagpt/exp_pool/context_builders/base.py @@ -0,0 +1,52 @@ +"""Base context builder.""" + +import re +from abc import ABC, abstractmethod +from typing import Any + +from pydantic import BaseModel, ConfigDict + +from metagpt.exp_pool.schema import Experience + +EXP_TEMPLATE = """Given the request: {req}, We can get the response: {resp}, Which scored: {score}.""" + + +class BaseContextBuilder(BaseModel, ABC): + model_config = ConfigDict(arbitrary_types_allowed=True) + + exps: list[Experience] = [] + + @abstractmethod + async def build(self, *args, **kwargs) -> Any: + """Build context from parameters.""" + + def format_exps(self) -> str: + """Format experiences into a numbered list of strings.""" + + result = [] + for i, exp in enumerate(self.exps, start=1): + result.append(f"{i}. " + EXP_TEMPLATE.format(req=exp.req, resp=exp.resp, score=exp.metric.score.val)) + + return "\n".join(result) + + @staticmethod + def replace_content_between_markers(text: str, start_marker: str, end_marker: str, new_content: str) -> str: + """Replace the content between `start_marker` and `end_marker` in the text with `new_content`. + + Args: + text (str): The original text. + new_content (str): The new content to replace the old content. + start_marker (str): The marker indicating the start of the content to be replaced, such as '# Example'. + end_marker (str): The marker indicating the end of the content to be replaced, such as '# Instruction'. + + Returns: + str: The text with the content replaced. + """ + + pattern = re.compile(f"({start_marker}\n)(.*?)(\n{end_marker})", re.DOTALL) + + def replacement(match): + return f"{match.group(1)}{new_content}\n{match.group(3)}" + + replaced_text = pattern.sub(replacement, text) + return replaced_text diff --git a/metagpt/exp_pool/context_builders/role_zero.py b/metagpt/exp_pool/context_builders/role_zero.py new file mode 100644 index 000000000..60f71ef59 --- /dev/null +++ b/metagpt/exp_pool/context_builders/role_zero.py @@ -0,0 +1,26 @@ +"""RoleZero context builder.""" + +from metagpt.exp_pool.context_builders.base import BaseContextBuilder + + +class RoleZeroContextBuilder(BaseContextBuilder): + async def build(self, *args, **kwargs) -> list[dict]: + """Builds the context by updating the req with formatted experiences. + + If there are no experiences, retains the original examples in req, otherwise replaces the examples with the formatted experiences. + """ + + req = kwargs.get("req", []) + if not req: + return req + + exps_str = self.format_exps() + if not exps_str: + return req + + req[-1]["content"] = self.replace_example_content(req[-1].get("content", ""), exps_str) + + return req + + def replace_example_content(self, text: str, new_example_content: str) -> str: + return self.replace_content_between_markers(text, "# Example", "# Instruction", new_example_content) diff --git a/metagpt/exp_pool/context_builders/simple.py b/metagpt/exp_pool/context_builders/simple.py new file mode 100644 index 000000000..35e2e1c8a --- /dev/null +++ b/metagpt/exp_pool/context_builders/simple.py @@ -0,0 +1,24 @@ +"""Simple context builder.""" + + +from metagpt.exp_pool.context_builders.base import BaseContextBuilder + +SIMPLE_CONTEXT_TEMPLATE = """ +{req} + +### Experiences +----- +{exps} +----- + +## Instruction +Consider **Experiences** to generate a better answer. +""" + + +class SimpleContextBuilder(BaseContextBuilder): + async def build(self, *args, **kwargs) -> str: + req = kwargs.get("req", "") + exps = self.format_exps() + + return SIMPLE_CONTEXT_TEMPLATE.format(req=req, exps=exps) if exps else req diff --git a/metagpt/exp_pool/decorator.py b/metagpt/exp_pool/decorator.py index 2a3bf2fba..c518bb7ea 100644 --- a/metagpt/exp_pool/decorator.py +++ b/metagpt/exp_pool/decorator.py @@ -2,18 +2,19 @@ import asyncio import functools -import inspect -import json from typing import Any, Callable, Optional, TypeVar from pydantic import BaseModel, ConfigDict, model_validator +from metagpt.config2 import config +from metagpt.exp_pool.context_builders import BaseContextBuilder, SimpleContextBuilder from metagpt.exp_pool.manager import ExperienceManager, exp_manager +from metagpt.exp_pool.perfect_judges import BasePerfectJudge, SimplePerfectJudge from metagpt.exp_pool.schema import Experience, Metric, QueryType, Score -from metagpt.exp_pool.scorers import ExperienceScorer, SimpleScorer +from metagpt.exp_pool.scorers import BaseScorer, SimpleScorer +from metagpt.logs import logger from metagpt.utils.async_helper import NestAsyncio from metagpt.utils.exceptions import handle_exception -from metagpt.utils.reflection import get_class_name ReturnType = TypeVar("ReturnType") @@ -21,42 +22,64 @@ ReturnType = TypeVar("ReturnType") def exp_cache( _func: Optional[Callable[..., ReturnType]] = None, query_type: QueryType = QueryType.SEMANTIC, - scorer: Optional[ExperienceScorer] = None, manager: Optional[ExperienceManager] = None, - pass_exps_to_func: bool = False, + scorer: Optional[BaseScorer] = None, + perfect_judge: Optional[BasePerfectJudge] = None, + context_builder: Optional[BaseContextBuilder] = None, + req_serialize: Optional[Callable[..., str]] = None, + resp_serialize: Optional[Callable[..., str]] = None, + resp_deserialize: Optional[Callable[[str], Any]] = None, + tag: Optional[str] = None, ): """Decorator to get a perfect experience, otherwise, it executes the function, and create a new experience. - This can be applied to both synchronous and asynchronous functions. + 1. This can be applied to both synchronous and asynchronous functions. + 2. The function must have a `req` parameter, and it must be provided as a keyword argument. + 3. If `config.exp_pool.enable_read` is False, the decorator will just directly execute the function. Args: _func: Just to make the decorator more flexible, for example, it can be used directly with @exp_cache by default, without the need for @exp_cache(). query_type: The type of query to be used when fetching experiences. - scorer: Evaluate experience. Default SimpleScorer. - manager: How to fetch, evaluate and save experience, etc. Default exp_manager. - pass_exps_to_func: To control whether imperfect experiences are passed to the function, if True, the func must have a parameter named 'exps'. + manager: How to fetch, evaluate and save experience, etc. Default to `exp_manager`. + scorer: Evaluate experience. Default to `SimpleScorer()`. + perfect_judge: Determines if an experience is perfect. Defaults to `SimplePerfectJudge()`. + context_builder: Build the context from exps and the function parameters. Default to `SimpleContextBuilder()`. + req_serialize: Serializes the request for storage. Defaults to `lambda req: str(req)`. + resp_serialize: Serializes the function's return value for storage. Defaults to `lambda resp: str(resp)`. + resp_deserialize: Deserializes the stored response back to the function's return value. Defaults to `lambda resp: resp`. + tag: An optional tag for the experience. Default to `ClassName.method_name` or `function_name`. """ def decorator(func: Callable[..., ReturnType]) -> Callable[..., ReturnType]: + if not config.exp_pool.enable_read: + return func + @functools.wraps(func) async def get_or_create(args: Any, kwargs: Any) -> ReturnType: + logger.info("exp_cache is enabled.") handler = ExpCacheHandler( func=func, args=args, kwargs=kwargs, + query_type=query_type, exp_manager=manager, exp_scorer=scorer, - pass_exps_to_func=pass_exps_to_func, + exp_perfect_judge=perfect_judge, + context_builder=context_builder, + req_serialize=req_serialize, + resp_serialize=resp_serialize, + resp_deserialize=resp_deserialize, + tag=tag, ) - await handler.fetch_experiences(query_type) - if exp := handler.get_one_perfect_experience(): + await handler.fetch_experiences() + if exp := await handler.get_one_perfect_exp(): return exp await handler.execute_function() await handler.process_experience() - return handler._result + return handler._raw_resp return ExpCacheHandler.choose_wrapper(func, get_or_create) @@ -69,39 +92,59 @@ class ExpCacheHandler(BaseModel): func: Callable args: Any kwargs: Any + query_type: QueryType = QueryType.SEMANTIC exp_manager: Optional[ExperienceManager] = None - exp_scorer: Optional[ExperienceScorer] = None - pass_exps_to_func: bool = False + exp_scorer: Optional[BaseScorer] = None + exp_perfect_judge: Optional[BasePerfectJudge] = None + context_builder: Optional[BaseContextBuilder] = None + req_serialize: Optional[Callable[..., str]] = None + resp_serialize: Optional[Callable[..., str]] = None + resp_deserialize: Optional[Callable[[str], Any]] = None + tag: Optional[str] = None _exps: list[Experience] = None - _result: Any = None + _req: str = "" + _resp: str = "" + _raw_resp: Any = None _score: Score = None - _req: str = None @model_validator(mode="after") def initialize(self): - if self.exp_manager is None: - self.exp_manager = exp_manager + self._validate_params() - if self.exp_scorer is None: - self.exp_scorer = SimpleScorer() + self.exp_manager = self.exp_manager or exp_manager + self.exp_scorer = self.exp_scorer or SimpleScorer() + self.exp_perfect_judge = self.exp_perfect_judge or SimplePerfectJudge() + self.context_builder = self.context_builder or SimpleContextBuilder() + self.req_serialize = self.req_serialize or (lambda resp: str(resp)) + self.resp_serialize = self.resp_serialize or (lambda resp: str(resp)) + self.resp_deserialize = self.resp_deserialize or (lambda resp: resp) + self.tag = self.tag or self._generate_tag() - self._req = self.generate_req_identifier(self.func, *self.args, **self.kwargs) + self._req = self.req_serialize(self.kwargs["req"]) return self - async def fetch_experiences(self, query_type: QueryType): + async def fetch_experiences(self): """Fetch experiences by query_type.""" - self._exps = await self.exp_manager.query_exps(self._req, query_type=query_type) + self._exps = await self.exp_manager.query_exps(self._req, query_type=self.query_type, tag=self.tag) - def get_one_perfect_experience(self) -> Optional[Experience]: - """Get a potentially perfect experience.""" - return self.exp_manager.extract_one_perfect_exp(self._exps) + async def get_one_perfect_exp(self) -> Optional[Any]: + """Get a potentially perfect experience, and resolve resp.""" + + for exp in self._exps: + if await self.exp_perfect_judge.is_perfect_exp(exp, self._req, *self.args, **self.kwargs): + logger.info(f"Get one perfect experience: {exp.req[:20]}...") + return self.resp_deserialize(exp.resp) + + return None async def execute_function(self): - """Execute the function, and save the result.""" - self._result = await self._execute_function() + """Execute the function, and save resp.""" + + self._raw_resp = await self._execute_function() + self._resp = self.resp_serialize(self._raw_resp) @handle_exception async def process_experience(self): @@ -110,41 +153,21 @@ class ExpCacheHandler(BaseModel): Evaluates and saves experience. Use `handle_exception` to ensure robustness, do not stop subsequent operations. """ + await self.evaluate_experience() self.save_experience() async def evaluate_experience(self): """Evaluate the experience, and save the score.""" - self._score = await self.exp_scorer.evaluate(self.func, self._result, self.args, self.kwargs) + self._score = await self.exp_scorer.evaluate(self.func, self._resp, self.args, self.kwargs) def save_experience(self): """Save the new experience.""" - exp = Experience(req=self._req, resp=self._result, metric=Metric(score=self._score)) - + exp = Experience(req=self._req, resp=self._resp, tag=self.tag, metric=Metric(score=self._score)) self.exp_manager.create_exp(exp) - @classmethod - def generate_req_identifier(cls, func, *args, **kwargs) -> str: - """Generate a unique request identifier for any given function and its arguments. - - Serializing args and kwargs into JSON strings and replacing ',' with '~' and ':' with '!'. - - Return Example: - SimpleClass.test_method@[1~2]@{"c"!3} - """ - cls_name = get_class_name(func) - func_name = f"{cls_name}.{func.__name__}" if cls_name else func.__name__ - - if cls_name and args and inspect.isfunction(func): - args = args[1:] - - args = cls._serialize_and_replace(args) - kwargs = cls._serialize_and_replace(kwargs) - - return f"{func_name}@{args}@{kwargs}" - @staticmethod def choose_wrapper(func, wrapped_func): """Choose how to run wrapped_func based on whether the function is asynchronous.""" @@ -158,25 +181,31 @@ class ExpCacheHandler(BaseModel): return async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper - @classmethod - def _serialize_and_replace(cls, data): - json_str = json.dumps(data) - return json_str.replace(", ", "~").replace(": ", "!") + def _validate_params(self): + if "req" not in self.kwargs: + raise ValueError("`req` must be provided as a keyword argument.") + + def _generate_tag(self) -> str: + """Generates a tag for the self.func. + + "ClassName.method_name" if the first argument is a class instance, otherwise just "function_name". + """ + + if self.args and hasattr(self.args[0], "__class__"): + cls_name = type(self.args[0]).__name__ + return f"{cls_name}.{self.func.__name__}" + + return self.func.__name__ + + async def _build_context(self) -> str: + self.context_builder.exps = self._exps + + return await self.context_builder.build(*self.args, **self.kwargs) async def _execute_function(self): - if self.pass_exps_to_func: - return await self._execute_function_with_exps() + self.kwargs["req"] = await self._build_context() - return await self._execute_function_without_exps() - - async def _execute_function_without_exps(self): if asyncio.iscoroutinefunction(self.func): return await self.func(*self.args, **self.kwargs) return self.func(*self.args, **self.kwargs) - - async def _execute_function_with_exps(self): - if asyncio.iscoroutinefunction(self.func): - return await self.func(*self.args, **self.kwargs, exps=self._exps) - - return self.func(*self.args, **self.kwargs, exps=self._exps) diff --git a/metagpt/exp_pool/manager.py b/metagpt/exp_pool/manager.py index 35ee5fdac..276b1e8e3 100644 --- a/metagpt/exp_pool/manager.py +++ b/metagpt/exp_pool/manager.py @@ -1,13 +1,22 @@ """Experience Manager.""" -from typing import Optional - +from llama_index.vector_stores.chroma import ChromaVectorStore from pydantic import BaseModel, ConfigDict, model_validator from metagpt.config2 import Config, config -from metagpt.exp_pool.schema import MAX_SCORE, Experience, QueryType +from metagpt.exp_pool.schema import ( + DEFAULT_COLLECTION_NAME, + DEFAULT_SIMILARITY_TOP_K, + EntryType, + Experience, + Metric, + QueryType, + Score, +) +from metagpt.logs import logger from metagpt.rag.engines import SimpleEngine from metagpt.rag.schema import ChromaRetrieverConfig, LLMRankerConfig +from metagpt.strategy.experience_retriever import ENGINEER_EXAMPLE, TL_EXAMPLE from metagpt.utils.exceptions import handle_exception @@ -27,14 +36,33 @@ class ExperienceManager(BaseModel): @model_validator(mode="after") def initialize(self): if self.storage is None: - self.storage = SimpleEngine.from_objs( - retriever_configs=[ - ChromaRetrieverConfig(collection_name="experience_pool", persist_path=".chroma_exp_data") - ], - ranker_configs=[LLMRankerConfig()], - ) + retriever_configs = [ + ChromaRetrieverConfig( + persist_path=self.config.exp_pool.persist_path, + collection_name=DEFAULT_COLLECTION_NAME, + similarity_top_k=DEFAULT_SIMILARITY_TOP_K, + ) + ] + ranker_configs = [LLMRankerConfig()] + + self.storage = SimpleEngine.from_objs(retriever_configs=retriever_configs, ranker_configs=ranker_configs) + + self.init_exp_pool() + return self + @handle_exception + def init_exp_pool(self): + if not self.config.exp_pool.init_exp: + return + + if self._has_exps(): + return + + self._init_teamleader_exps() + self._init_engineer2_exps() + logger.info("`init_exp_pool` done.") + @handle_exception def create_exp(self, exp: Experience): """Adds an experience to the storage if writing is enabled. @@ -74,39 +102,26 @@ class ExperienceManager(BaseModel): return exps - def extract_one_perfect_exp(self, exps: list[Experience]) -> Optional[Experience]: - """Extracts the first 'perfect' experience from a list of experiences. + def _has_exps(self) -> bool: + vector_store: ChromaVectorStore = self.storage._retriever._vector_store - Args: - exps (list[Experience]): The experiences to evaluate. + return bool(vector_store._get(limit=1, where={}).ids) - Returns: - Optional[Experience]: The first perfect experience if found, otherwise None. - """ - for exp in exps: - if self.is_perfect_exp(exp): - return exp + def _init_exp(self, req: str, resp: str, tag: str, metric: Metric = None): + exp = Experience( + req=req, + resp=resp, + entry_type=EntryType.MANUAL, + tag=tag, + metric=metric or Metric(score=Score(val=9, reason="Manual")), + ) + self.create_exp(exp) - return None + def _init_teamleader_exps(self): + self._init_exp(req=TL_EXAMPLE, resp=TL_EXAMPLE, tag="TeamLeader.llm_cached_aask") - @staticmethod - def is_perfect_exp(exp: Experience) -> bool: - """Determines if an experience is considered 'perfect'. - - Args: - exp (Experience): The experience to evaluate. - - Returns: - bool: True if the experience is manually entered, otherwise False. - """ - if not exp: - return False - - # TODO: need more metrics - if exp.metric and exp.metric.score.val == MAX_SCORE: - return True - - return False + def _init_engineer2_exps(self): + self._init_exp(req=ENGINEER_EXAMPLE, resp=ENGINEER_EXAMPLE, tag="Engineer2.llm_cached_aask") exp_manager = ExperienceManager() diff --git a/metagpt/exp_pool/perfect_judges/__init__.py b/metagpt/exp_pool/perfect_judges/__init__.py new file mode 100644 index 000000000..d8796c7c8 --- /dev/null +++ b/metagpt/exp_pool/perfect_judges/__init__.py @@ -0,0 +1,6 @@ +"""Perfect judges init.""" + +from metagpt.exp_pool.perfect_judges.base import BasePerfectJudge +from metagpt.exp_pool.perfect_judges.simple import SimplePerfectJudge + +__all__ = ["BasePerfectJudge", "SimplePerfectJudge"] diff --git a/metagpt/exp_pool/perfect_judges/base.py b/metagpt/exp_pool/perfect_judges/base.py new file mode 100644 index 000000000..293522993 --- /dev/null +++ b/metagpt/exp_pool/perfect_judges/base.py @@ -0,0 +1,20 @@ +"""Base perfect judge.""" + +from abc import ABC, abstractmethod + +from pydantic import BaseModel, ConfigDict + +from metagpt.exp_pool.schema import Experience + + +class BasePerfectJudge(BaseModel, ABC): + model_config = ConfigDict(arbitrary_types_allowed=True) + + @abstractmethod + async def is_perfect_exp(self, exp: Experience, serialized_req: str, *args, **kwargs) -> bool: + """Determine whether the experience is perfect. + + Args: + exp (Experience): The experience to evaluate. + serialized_req (str): The serialized request to compare against the experience's request. + """ diff --git a/metagpt/exp_pool/perfect_judges/simple.py b/metagpt/exp_pool/perfect_judges/simple.py new file mode 100644 index 000000000..37ede95c3 --- /dev/null +++ b/metagpt/exp_pool/perfect_judges/simple.py @@ -0,0 +1,27 @@ +"""Simple perfect judge.""" + + +from pydantic import ConfigDict + +from metagpt.exp_pool.perfect_judges.base import BasePerfectJudge +from metagpt.exp_pool.schema import MAX_SCORE, Experience + + +class SimplePerfectJudge(BasePerfectJudge): + model_config = ConfigDict(arbitrary_types_allowed=True) + + async def is_perfect_exp(self, exp: Experience, serialized_req: str, *args, **kwargs) -> bool: + """Determine whether the experience is perfect. + + Args: + exp (Experience): The experience to evaluate. + serialized_req (str): The serialized request to compare against the experience's request. + + Returns: + bool: True if the serialized request matches the experience's request and the experience's score is perfect, False otherwise. + """ + + if not exp.metric or not exp.metric.score: + return False + + return serialized_req == exp.req and exp.metric.score.val == MAX_SCORE diff --git a/metagpt/exp_pool/schema.py b/metagpt/exp_pool/schema.py index 9fc665cca..d59478742 100644 --- a/metagpt/exp_pool/schema.py +++ b/metagpt/exp_pool/schema.py @@ -1,13 +1,16 @@ """Experience schema.""" from enum import Enum -from typing import Any, Optional +from typing import Optional from llama_index.core.schema import TextNode from pydantic import BaseModel, Field MAX_SCORE = 10 +DEFAULT_COLLECTION_NAME = "experience_pool" +DEFAULT_SIMILARITY_TOP_K = 2 + class QueryType(str, Enum): """Type of query experiences.""" @@ -59,7 +62,7 @@ class Experience(BaseModel): """Experience.""" req: str = Field(..., description="") - resp: Any = Field(..., description="The type is string/json/code.") + resp: str = Field(..., description="The type is string/json/code.") metric: Optional[Metric] = Field(default=None, description="Metric.") exp_type: ExperienceType = Field(default=ExperienceType.SUCCESS, description="The type of experience.") entry_type: EntryType = Field(default=EntryType.AUTOMATIC, description="Type of entry: Manual or Automatic.") diff --git a/metagpt/exp_pool/scorers/__init__.py b/metagpt/exp_pool/scorers/__init__.py index 85bea88ff..caa845b14 100644 --- a/metagpt/exp_pool/scorers/__init__.py +++ b/metagpt/exp_pool/scorers/__init__.py @@ -1,6 +1,6 @@ -"""Experience scorers init.""" +"""Scorers init.""" -from metagpt.exp_pool.scorers.base import ExperienceScorer +from metagpt.exp_pool.scorers.base import BaseScorer from metagpt.exp_pool.scorers.simple import SimpleScorer -__all__ = ["ExperienceScorer", "SimpleScorer"] +__all__ = ["BaseScorer", "SimpleScorer"] diff --git a/metagpt/exp_pool/scorers/base.py b/metagpt/exp_pool/scorers/base.py index a9d30cffe..94623c30f 100644 --- a/metagpt/exp_pool/scorers/base.py +++ b/metagpt/exp_pool/scorers/base.py @@ -1,6 +1,6 @@ -"""Experience Scorers.""" +"""Base scorer.""" -from abc import abstractmethod +from abc import ABC, abstractmethod from typing import Any, Callable from pydantic import BaseModel, ConfigDict @@ -8,7 +8,7 @@ from pydantic import BaseModel, ConfigDict from metagpt.exp_pool.schema import Score -class ExperienceScorer(BaseModel): +class BaseScorer(BaseModel, ABC): model_config = ConfigDict(arbitrary_types_allowed=True) @abstractmethod diff --git a/metagpt/exp_pool/scorers/simple.py b/metagpt/exp_pool/scorers/simple.py index 84995b60f..1fda189d1 100644 --- a/metagpt/exp_pool/scorers/simple.py +++ b/metagpt/exp_pool/scorers/simple.py @@ -1,4 +1,4 @@ -"""Simple Scorer.""" +"""Simple scorer.""" import inspect import json @@ -7,7 +7,7 @@ from typing import Any, Callable from pydantic import Field from metagpt.exp_pool.schema import Score -from metagpt.exp_pool.scorers.base import ExperienceScorer +from metagpt.exp_pool.scorers.base import BaseScorer from metagpt.llm import LLM from metagpt.provider.base_llm import BaseLLM from metagpt.utils.common import CodeParser @@ -54,7 +54,7 @@ Follow instructions, generate output and make sure it follows the **Constraint** """ -class SimpleScorer(ExperienceScorer): +class SimpleScorer(BaseScorer): llm: BaseLLM = Field(default_factory=LLM) async def evaluate(self, func: Callable, result: Any, args: tuple = None, kwargs: dict = None) -> Score: diff --git a/metagpt/roles/di/role_zero.py b/metagpt/roles/di/role_zero.py index 906c5583c..e2a4cec78 100644 --- a/metagpt/roles/di/role_zero.py +++ b/metagpt/roles/di/role_zero.py @@ -1,5 +1,6 @@ from __future__ import annotations +import copy import inspect import json import re @@ -10,8 +11,14 @@ from pydantic import model_validator from metagpt.actions import Action from metagpt.actions.di.run_command import RunCommand +from metagpt.exp_pool import exp_cache +from metagpt.exp_pool.context_builders import RoleZeroContextBuilder from metagpt.logs import logger -from metagpt.prompts.di.role_zero import CMD_PROMPT, ROLE_INSTRUCTION, JSON_REPAIR_PROMPT +from metagpt.prompts.di.role_zero import ( + CMD_PROMPT, + JSON_REPAIR_PROMPT, + ROLE_INSTRUCTION, +) from metagpt.roles import Role from metagpt.schema import AIMessage, Message, UserMessage from metagpt.strategy.experience_retriever import DummyExpRetriever, ExpRetriever @@ -21,8 +28,8 @@ from metagpt.tools.libs.editor import Editor from metagpt.tools.tool_recommend import BM25ToolRecommender, ToolRecommender from metagpt.tools.tool_registry import register_tool from metagpt.utils.common import CodeParser +from metagpt.utils.repair_llm_raw_output import RepairType, repair_llm_raw_output from metagpt.utils.report import ThoughtReporter -from metagpt.utils.repair_llm_raw_output import repair_llm_raw_output, RepairType @register_tool(include_functions=["ask_human", "reply_to_human"]) @@ -154,11 +161,37 @@ class RoleZero(Role): context = self.llm.format_msg(memory + [UserMessage(content=prompt)]) # print(*context, sep="\n" + "*" * 5 + "\n") async with ThoughtReporter(enable_llm_stream=True): - self.command_rsp = await self.llm.aask(context, system_msgs=self.system_msg) + self.command_rsp = await self.llm_cached_aask(req=context, system_msgs=self.system_msg) self.rc.memory.add(AIMessage(content=self.command_rsp)) return True + @exp_cache(context_builder=RoleZeroContextBuilder(), req_serialize=lambda req: RoleZero._req_serialize(req)) + async def llm_cached_aask(self, *, req: list[dict], system_msgs: list[str]) -> str: + return await self.llm.aask(req, system_msgs=system_msgs) + + @staticmethod + def _req_serialize(req: list[dict]) -> str: + """Serialize the request for database storage, ensuring it is a string. + + This function deep copies the request and modifies the content of the last element + to remove unnecessary sections, making the request more concise. + """ + + req_copy = copy.deepcopy(req) + + last_content = req_copy[-1]["content"] + last_content = RoleZeroContextBuilder.replace_content_between_markers( + last_content, "# Data Structure", "# Current Plan", "" + ) + last_content = RoleZeroContextBuilder.replace_content_between_markers( + last_content, "# Example", "# Instruction", "" + ) + + req_copy[-1]["content"] = last_content + + return json.dumps(req_copy) + async def _act(self) -> Message: if self.use_fixed_sop: return await super()._act() @@ -166,7 +199,7 @@ class RoleZero(Role): try: commands = CodeParser.parse_code(block=None, lang="json", text=self.command_rsp) commands = json.loads(repair_llm_raw_output(output=commands, req_keys=[None], repair_type=RepairType.JSON)) - except json.JSONDecodeError as e: + except json.JSONDecodeError: commands = await self.llm.aask(msg=JSON_REPAIR_PROMPT.format(json_data=self.command_rsp)) commands = json.loads(CodeParser.parse_code(block=None, lang="json", text=commands)) except Exception as e: diff --git a/metagpt/strategy/solver.py b/metagpt/strategy/solver.py index e532f736b..4aedb42aa 100644 --- a/metagpt/strategy/solver.py +++ b/metagpt/strategy/solver.py @@ -39,7 +39,7 @@ class NaiveSolver(BaseSolver): self.graph.topological_sort() for key in self.graph.execution_order: op = self.graph.nodes[key] - await op.fill(self.context, self.llm, mode="root") + await op.fill(req=self.context, llm=self.llm, mode="root") class TOTSolver(BaseSolver): diff --git a/metagpt/utils/reflection.py b/metagpt/utils/reflection.py index fe852635f..8b8237ae7 100644 --- a/metagpt/utils/reflection.py +++ b/metagpt/utils/reflection.py @@ -1,5 +1,4 @@ """class tools, including method inspection, class attributes, inheritance relationships, etc.""" -import inspect def check_methods(C, *methods): @@ -17,25 +16,3 @@ def check_methods(C, *methods): else: return NotImplemented return True - - -def get_class_name(func) -> str: - """Returns the class name of the object that a method belongs to. - - - If `func` is a bound method or a class method, extracts the class name directly from the method. - - Returns an empty string if it's a regular function or cannot determine the class. - """ - if inspect.ismethod(func): - if inspect.isclass(func.__self__): - return func.__self__.__name__ - - return func.__self__.__class__.__name__ - - if inspect.isfunction(func): - qualname_parts = func.__qualname__.split(".") - if len(qualname_parts) > 1: - class_name = qualname_parts[-2] - if class_name.isidentifier(): - return class_name - - return "" diff --git a/tests/metagpt/actions/test_action_node.py b/tests/metagpt/actions/test_action_node.py index bc85925a8..23779c984 100644 --- a/tests/metagpt/actions/test_action_node.py +++ b/tests/metagpt/actions/test_action_node.py @@ -91,10 +91,10 @@ async def test_action_node_two_layer(): assert node_b in root.children.values() # FIXME: ADD MARKDOWN SUPPORT. NEED TO TUNE MARKDOWN SYMBOL FIRST. - answer1 = await root.fill(context="what's the answer to 123+456?", schema="json", strgy="simple", llm=LLM()) + answer1 = await root.fill(req="what's the answer to 123+456?", schema="json", strgy="simple", llm=LLM()) assert "579" in answer1.content - answer2 = await root.fill(context="what's the answer to 123+456?", schema="json", strgy="complex", llm=LLM()) + answer2 = await root.fill(req="what's the answer to 123+456?", schema="json", strgy="complex", llm=LLM()) assert "579" in answer2.content @@ -112,7 +112,7 @@ async def test_action_node_review(): with pytest.raises(RuntimeError): _ = await node_a.review() - _ = await node_a.fill(context=None, llm=LLM()) + _ = await node_a.fill(req=None, llm=LLM()) setattr(node_a.instruct_content, key, "game snake") # wrong content to review review_comments = await node_a.review(review_mode=ReviewMode.AUTO) @@ -126,7 +126,7 @@ async def test_action_node_review(): with pytest.raises(RuntimeError): _ = await node.review() - _ = await node.fill(context=None, llm=LLM()) + _ = await node.fill(req=None, llm=LLM()) review_comments = await node.review(review_mode=ReviewMode.AUTO) assert len(review_comments) == 1 @@ -151,7 +151,7 @@ async def test_action_node_revise(): with pytest.raises(RuntimeError): _ = await node_a.review() - _ = await node_a.fill(context=None, llm=LLM()) + _ = await node_a.fill(req=None, llm=LLM()) setattr(node_a.instruct_content, key, "game snake") # wrong content to revise revise_contents = await node_a.revise(revise_mode=ReviseMode.AUTO) assert len(revise_contents) == 1 @@ -164,7 +164,7 @@ async def test_action_node_revise(): with pytest.raises(RuntimeError): _ = await node.revise() - _ = await node.fill(context=None, llm=LLM()) + _ = await node.fill(req=None, llm=LLM()) setattr(node.instruct_content, key, "game snake") revise_contents = await node.revise(revise_mode=ReviseMode.AUTO) assert len(revise_contents) == 1 @@ -257,7 +257,7 @@ async def test_action_node_with_image(mocker): invoice_path = Path(__file__).parent.joinpath("..", "..", "data", "invoices", "invoice-2.png") img_base64 = encode_image(invoice_path) mocker.patch("metagpt.provider.openai_api.OpenAILLM._cons_kwargs", _cons_kwargs) - node = await invoice.fill(context="", llm=LLM(), images=[img_base64]) + node = await invoice.fill(req="", llm=LLM(), images=[img_base64]) assert node.instruct_content.invoice diff --git a/tests/metagpt/actions/test_design_api_an.py b/tests/metagpt/actions/test_design_api_an.py index 3d11f200d..4ed3cb362 100644 --- a/tests/metagpt/actions/test_design_api_an.py +++ b/tests/metagpt/actions/test_design_api_an.py @@ -38,7 +38,7 @@ async def test_write_design_an(mocker): mocker.patch("metagpt.actions.design_api_an.REFINED_DESIGN_NODE.fill", return_value=root) prompt = NEW_REQ_TEMPLATE.format(old_design=DESIGN_SAMPLE, context=dict_to_markdown(REFINED_PRD_JSON)) - node = await REFINED_DESIGN_NODE.fill(prompt, llm) + node = await REFINED_DESIGN_NODE.fill(req=prompt, llm=llm) assert "Refined Implementation Approach" in node.instruct_content.model_dump() assert "Refined File list" in node.instruct_content.model_dump() diff --git a/tests/metagpt/actions/test_project_management_an.py b/tests/metagpt/actions/test_project_management_an.py index 5a65e50c9..6d41109c9 100644 --- a/tests/metagpt/actions/test_project_management_an.py +++ b/tests/metagpt/actions/test_project_management_an.py @@ -42,7 +42,7 @@ async def test_project_management_an(mocker): root.instruct_content.model_dump = mock_task_json mocker.patch("metagpt.actions.project_management_an.PM_NODE.fill", return_value=root) - node = await PM_NODE.fill(dict_to_markdown(REFINED_DESIGN_JSON), llm) + node = await PM_NODE.fill(req=dict_to_markdown(REFINED_DESIGN_JSON), llm=llm) assert "Logic Analysis" in node.instruct_content.model_dump() assert "Task list" in node.instruct_content.model_dump() @@ -59,7 +59,7 @@ async def test_project_management_an_inc(mocker): mocker.patch("metagpt.actions.project_management_an.REFINED_PM_NODE.fill", return_value=root) prompt = NEW_REQ_TEMPLATE.format(old_task=TASK_SAMPLE, context=dict_to_markdown(REFINED_DESIGN_JSON)) - node = await REFINED_PM_NODE.fill(prompt, llm) + node = await REFINED_PM_NODE.fill(req=prompt, llm=llm) assert "Refined Logic Analysis" in node.instruct_content.model_dump() assert "Refined Task list" in node.instruct_content.model_dump() diff --git a/tests/metagpt/actions/test_write_prd_an.py b/tests/metagpt/actions/test_write_prd_an.py index 378ce42c3..b6e92d3d6 100644 --- a/tests/metagpt/actions/test_write_prd_an.py +++ b/tests/metagpt/actions/test_write_prd_an.py @@ -39,7 +39,7 @@ async def test_write_prd_an(mocker): requirements=NEW_REQUIREMENT_SAMPLE, old_prd=PRD_SAMPLE, ) - node = await REFINED_PRD_NODE.fill(prompt, llm) + node = await REFINED_PRD_NODE.fill(req=prompt, llm=llm) assert "Refined Requirements" in node.instruct_content.model_dump() assert "Refined Product Goals" in node.instruct_content.model_dump() diff --git a/tests/metagpt/exp_pool/test_context_builders/test_base_context_builder.py b/tests/metagpt/exp_pool/test_context_builders/test_base_context_builder.py new file mode 100644 index 000000000..17696e1b4 --- /dev/null +++ b/tests/metagpt/exp_pool/test_context_builders/test_base_context_builder.py @@ -0,0 +1,45 @@ +import pytest + +from metagpt.exp_pool.context_builders.base import ( + EXP_TEMPLATE, + BaseContextBuilder, + Experience, +) +from metagpt.exp_pool.schema import Metric, Score + + +class TestBaseContextBuilder: + class ConcreteContextBuilder(BaseContextBuilder): + async def build(self, *args, **kwargs): + pass + + @pytest.fixture + def context_builder(self): + return self.ConcreteContextBuilder() + + def test_format_exps(self, context_builder): + exp1 = Experience(req="req1", resp="resp1", metric=Metric(score=Score(val=8))) + exp2 = Experience(req="req2", resp="resp2", metric=Metric(score=Score(val=9))) + context_builder.exps = [exp1, exp2] + + result = context_builder.format_exps() + expected = "\n".join( + [ + f"1. {EXP_TEMPLATE.format(req='req1', resp='resp1', score=8)}", + f"2. {EXP_TEMPLATE.format(req='req2', resp='resp2', score=9)}", + ] + ) + assert result == expected + + def test_replace_content_between_markers(self): + text = "Start\n# Example\nOld content\n# Instruction\nEnd" + new_content = "New content" + result = BaseContextBuilder.replace_content_between_markers(text, "# Example", "# Instruction", new_content) + expected = "Start\n# Example\nNew content\n\n# Instruction\nEnd" + assert result == expected + + def test_replace_content_between_markers_no_match(self): + text = "Start\nNo markers\nEnd" + new_content = "New content" + result = BaseContextBuilder.replace_content_between_markers(text, "# Example", "# Instruction", new_content) + assert result == text diff --git a/tests/metagpt/exp_pool/test_context_builders/test_rolezero_context_builder.py b/tests/metagpt/exp_pool/test_context_builders/test_rolezero_context_builder.py new file mode 100644 index 000000000..0ea04432d --- /dev/null +++ b/tests/metagpt/exp_pool/test_context_builders/test_rolezero_context_builder.py @@ -0,0 +1,38 @@ +import pytest + +from metagpt.exp_pool.context_builders.base import BaseContextBuilder +from metagpt.exp_pool.context_builders.role_zero import RoleZeroContextBuilder + + +class TestRoleZeroContextBuilder: + @pytest.fixture + def context_builder(self): + return RoleZeroContextBuilder() + + @pytest.mark.asyncio + async def test_build_empty_req(self, context_builder): + result = await context_builder.build(req=[]) + assert result == [] + + @pytest.mark.asyncio + async def test_build_no_experiences(self, context_builder, mocker): + mocker.patch.object(BaseContextBuilder, "format_exps", return_value="") + req = [{"content": "Original content"}] + result = await context_builder.build(req=req) + assert result == req + + @pytest.mark.asyncio + async def test_build_with_experiences(self, context_builder, mocker): + mocker.patch.object(BaseContextBuilder, "format_exps", return_value="Formatted experiences") + mocker.patch.object(RoleZeroContextBuilder, "replace_example_content", return_value="Updated content") + req = [{"content": "Original content"}] + result = await context_builder.build(req=req) + assert result == [{"content": "Updated content"}] + + def test_replace_example_content(self, context_builder, mocker): + mocker.patch.object(BaseContextBuilder, "replace_content_between_markers", return_value="Replaced content") + result = context_builder.replace_example_content("Original text", "New example content") + assert result == "Replaced content" + context_builder.replace_content_between_markers.assert_called_once_with( + "Original text", "# Example", "# Instruction", "New example content" + ) diff --git a/tests/metagpt/exp_pool/test_context_builders/test_simple_context_builder.py b/tests/metagpt/exp_pool/test_context_builders/test_simple_context_builder.py new file mode 100644 index 000000000..e96addab9 --- /dev/null +++ b/tests/metagpt/exp_pool/test_context_builders/test_simple_context_builder.py @@ -0,0 +1,46 @@ +import pytest + +from metagpt.exp_pool.context_builders.base import BaseContextBuilder +from metagpt.exp_pool.context_builders.simple import ( + SIMPLE_CONTEXT_TEMPLATE, + SimpleContextBuilder, +) + + +class TestSimpleContextBuilder: + @pytest.fixture + def context_builder(self): + return SimpleContextBuilder() + + @pytest.mark.asyncio + async def test_build_with_experiences(self, context_builder, mocker): + # Mock the format_exps method + mock_exps = "Mocked experiences" + mocker.patch.object(BaseContextBuilder, "format_exps", return_value=mock_exps) + + req = "Test request" + result = await context_builder.build(req=req) + + expected = SIMPLE_CONTEXT_TEMPLATE.format(req=req, exps=mock_exps) + assert result == expected + + @pytest.mark.asyncio + async def test_build_without_experiences(self, context_builder, mocker): + # Mock the format_exps method to return an empty string + mocker.patch.object(BaseContextBuilder, "format_exps", return_value="") + + req = "Test request" + result = await context_builder.build(req=req) + + assert result == req + + @pytest.mark.asyncio + async def test_build_without_req(self, context_builder, mocker): + # Mock the format_exps method + mock_exps = "Mocked experiences" + mocker.patch.object(BaseContextBuilder, "format_exps", return_value=mock_exps) + + result = await context_builder.build() + + expected = SIMPLE_CONTEXT_TEMPLATE.format(req="", exps=mock_exps) + assert result == expected diff --git a/tests/metagpt/exp_pool/test_decorator.py b/tests/metagpt/exp_pool/test_decorator.py index bedc4e391..c0b3fe36d 100644 --- a/tests/metagpt/exp_pool/test_decorator.py +++ b/tests/metagpt/exp_pool/test_decorator.py @@ -1,29 +1,17 @@ import asyncio -import inspect import pytest +from metagpt.exp_pool.context_builders import SimpleContextBuilder from metagpt.exp_pool.decorator import ExpCacheHandler, exp_cache from metagpt.exp_pool.manager import ExperienceManager +from metagpt.exp_pool.perfect_judges import SimplePerfectJudge from metagpt.exp_pool.schema import Experience, QueryType, Score from metagpt.exp_pool.scorers import SimpleScorer from metagpt.rag.engines import SimpleEngine -def for_test_function(a, b, c=None): - return a + b if c is None else a + b + c - - -class ForTestClass: - def for_test_method(self, x, y): - return x * y - - @classmethod - def for_test_class_method(cls, x, y): - return x**y - - -class TestExpCache: +class TestExpCacheHandler: @pytest.fixture def mock_func(self, mocker): return mocker.AsyncMock() @@ -34,7 +22,6 @@ class TestExpCache: manager.storage = mocker.MagicMock(spec=SimpleEngine) manager.query_exps = mocker.AsyncMock() manager.create_exp = mocker.MagicMock() - manager.extract_one_perfect_exp = mocker.MagicMock() return manager @pytest.fixture @@ -44,174 +31,165 @@ class TestExpCache: return scorer @pytest.fixture - def exp_cache_handler(self, mock_func, mock_exp_manager, mock_scorer): + def mock_perfect_judge(self, mocker): + return mocker.MagicMock(spec=SimplePerfectJudge) + + @pytest.fixture + def mock_context_builder(self, mocker): + return mocker.MagicMock(spec=SimpleContextBuilder) + + @pytest.fixture + def exp_cache_handler(self, mock_func, mock_exp_manager, mock_scorer, mock_perfect_judge, mock_context_builder): return ExpCacheHandler( - func=mock_func, args=(), kwargs={}, exp_manager=mock_exp_manager, exp_scorer=mock_scorer, pass_exps=False + func=mock_func, + args=(), + kwargs={"req": "test_req"}, + exp_manager=mock_exp_manager, + exp_scorer=mock_scorer, + exp_perfect_judge=mock_perfect_judge, + context_builder=mock_context_builder, ) @pytest.mark.asyncio async def test_fetch_experiences(self, exp_cache_handler, mock_exp_manager): - await exp_cache_handler.fetch_experiences(QueryType.SEMANTIC) - mock_exp_manager.query_exps.assert_called_once() + mock_exp_manager.query_exps.return_value = [Experience(req="test_req", resp="test_resp")] + await exp_cache_handler.fetch_experiences() + mock_exp_manager.query_exps.assert_called_once_with( + "test_req", query_type=QueryType.SEMANTIC, tag=exp_cache_handler.tag + ) + assert len(exp_cache_handler._exps) == 1 @pytest.mark.asyncio - async def test_perfect_experience_found(self, exp_cache_handler, mock_exp_manager, mock_func): - # Setup: Assume perfect experience is found - perfect_exp = Experience(req="req", resp="resp") - mock_exp_manager.extract_one_perfect_exp.return_value = perfect_exp - - # Exec - exp_cache_handler._exps = [perfect_exp] # Simulate fetched experiences - result = exp_cache_handler.get_one_perfect_experience() - - # Assert - assert result.resp == "resp" - mock_func.assert_not_called() # Function should not be called + async def test_get_one_perfect_exp(self, exp_cache_handler, mock_perfect_judge): + exp = Experience(req="test_req", resp="perfect_resp") + exp_cache_handler._exps = [exp] + mock_perfect_judge.is_perfect_exp.return_value = True + result = await exp_cache_handler.get_one_perfect_exp() + assert result == "perfect_resp" @pytest.mark.asyncio - async def test_execute_function_when_no_perfect_exp(self, exp_cache_handler, mock_exp_manager, mock_func): - # Setup: No perfect experience - mock_exp_manager.extract_one_perfect_exp.return_value = None - mock_func.return_value = "Computed result" - - # Exec + async def test_execute_function(self, exp_cache_handler, mock_func, mock_context_builder): + mock_context_builder.build.return_value = "built_context" + mock_func.return_value = "function_result" await exp_cache_handler.execute_function() - - # Assert - assert exp_cache_handler._result == "Computed result" - mock_func.assert_called_once() + mock_context_builder.build.assert_called_once() + mock_func.assert_called_once_with(req="built_context") + assert exp_cache_handler._raw_resp == "function_result" + assert exp_cache_handler._resp == "function_result" @pytest.mark.asyncio - async def test_evaluate_and_save_experience(self, exp_cache_handler, mock_scorer, mock_exp_manager): - # Setup - mock_scorer.evaluate.return_value = Score(value=100) - exp_cache_handler._result = "Computed result" - - # Exec - await exp_cache_handler.evaluate_experience() - exp_cache_handler.save_experience() - - # Assert + async def test_process_experience(self, exp_cache_handler, mock_scorer, mock_exp_manager): + exp_cache_handler._resp = "test_resp" + mock_scorer.evaluate.return_value = Score(val=8) + await exp_cache_handler.process_experience() mock_scorer.evaluate.assert_called_once() mock_exp_manager.create_exp.assert_called_once() @pytest.mark.asyncio - async def test_async_function_execution_with_exps(self, exp_cache_handler, mock_exp_manager, mock_func): - # Setup - exp_cache_handler.pass_exps_to_func = True - mock_func.return_value = "Async result with exps" - mock_exp_manager.extract_one_perfect_exp.return_value = None - exp_cache_handler._exps = [Experience(req="req", resp="resp")] + async def test_evaluate_experience(self, exp_cache_handler, mock_scorer): + exp_cache_handler._resp = "test_resp" + mock_scorer.evaluate.return_value = Score(val=9) + await exp_cache_handler.evaluate_experience() + assert exp_cache_handler._score.val == 9 - # Exec - await exp_cache_handler.execute_function() - - # Assert - mock_func.assert_called_once_with(exps=exp_cache_handler._exps) - assert exp_cache_handler._result == "Async result with exps" - - def test_sync_function_execution_with_exps(self, mocker, exp_cache_handler, mock_exp_manager, mock_func): - # Setup - exp_cache_handler.func = mocker.Mock(return_value="Sync result with exps") - exp_cache_handler.pass_exps_to_func = True - mock_exp_manager.extract_one_perfect_exp.return_value = None - exp_cache_handler._exps = [Experience(req="req", resp="resp")] - - # Exec - asyncio.get_event_loop().run_until_complete(exp_cache_handler.execute_function()) - - # Assert - exp_cache_handler.func.assert_called_once_with(exps=exp_cache_handler._exps) - assert exp_cache_handler._result == "Sync result with exps" - - def test_wrapper_selection_async(self, mocker, exp_cache_handler, mock_func): - # Setup - mock_func = mocker.AsyncMock() - - # Exec - wrapper = ExpCacheHandler.choose_wrapper(mock_func, exp_cache_handler.execute_function) - - # Assert - assert asyncio.iscoroutinefunction(wrapper), "Wrapper should be asynchronous" - - def test_wrapper_selection_sync(self, exp_cache_handler, mocker): - # Setup - sync_func = mocker.Mock() - - # Exec - wrapper = ExpCacheHandler.choose_wrapper(sync_func, exp_cache_handler.execute_function) - - # Assert - assert not asyncio.iscoroutinefunction(wrapper), "Wrapper should be synchronous" - - @pytest.mark.parametrize( - "func, args, kwargs, expected", - [ - (for_test_function, (1, 2), {"c": 3}, 'for_test_function@[1~2]@{"c"!3}'), - (ForTestClass().for_test_method, (4, 5), {}, "ForTestClass.for_test_method@[4~5]@{}"), - (ForTestClass.for_test_class_method, (6, 7), {}, "ForTestClass.for_test_class_method@[6~7]@{}"), - (for_test_function, (), {}, "for_test_function@[]@{}"), - ( - for_test_function, - ("hello", [1, 2]), - {"key": "value"}, - 'for_test_function@["hello"~[1~2]]@{"key"!"value"}', - ), - ], - ) - def test_generate_req_identifier(self, func, args, kwargs, expected): - req_identifier = ExpCacheHandler.generate_req_identifier(func, *args, **kwargs) - assert req_identifier == expected - - @pytest.mark.asyncio - async def test_exp_cache_with_perfect_experience(self, mocker, mock_exp_manager): - # Mock perfect experience - perfect_exp = Experience(req="test_req", resp="perfect_response") - mock_exp_manager.query_exps = mocker.AsyncMock(return_value=[perfect_exp]) - mock_exp_manager.extract_one_perfect_exp = mocker.MagicMock(return_value=perfect_exp) - async_mock_func = mocker.AsyncMock() - - # Setup - decorated_func = exp_cache(async_mock_func, manager=mock_exp_manager) - - # Exec - result: Experience = await decorated_func() - - # Assert - assert result.resp == "perfect_response", "Should return the perfect experience response" - async_mock_func.assert_not_called() - - @pytest.mark.asyncio - async def test_exp_cache_without_perfect_experience(self, mocker, mock_exp_manager): - # Mock - mock_exp_manager.query_exps = mocker.AsyncMock(return_value=[]) - mock_exp_manager.extract_one_perfect_exp = mocker.MagicMock(return_value=None) - async_mock_func = mocker.AsyncMock(return_value="computed_response") - async_mock_func.__signature__ = inspect.signature(for_test_function) - - # Setup - decorated_func = exp_cache(async_mock_func, manager=mock_exp_manager) - - # Exec - result = await decorated_func() - - # Assert - assert result == "computed_response", "Should execute and return the function's response" - async_mock_func.assert_called_once() - - @pytest.mark.asyncio - async def test_exp_cache_saves_new_experience(self, mocker, mock_exp_manager, mock_scorer): - # Mock - mock_exp_manager.query_exps = mocker.AsyncMock(return_value=[]) - mock_exp_manager.extract_one_perfect_exp = mocker.MagicMock(return_value=None) - async_mock_func = mocker.AsyncMock(return_value="computed_response") - mock_scorer.evaluate = mocker.AsyncMock(return_value=Score(value=100)) - - # Setup - decorated_func = exp_cache(async_mock_func, manager=mock_exp_manager, scorer=mock_scorer) - - # Exec - await decorated_func() - - # Assert + def test_save_experience(self, exp_cache_handler, mock_exp_manager): + exp_cache_handler._req = "test_req" + exp_cache_handler._resp = "test_resp" + exp_cache_handler._score = Score(val=7) + exp_cache_handler.save_experience() mock_exp_manager.create_exp.assert_called_once() + + def test_choose_wrapper_async(self, mocker): + async def async_func(): + pass + + wrapper = ExpCacheHandler.choose_wrapper(async_func, mocker.AsyncMock()) + assert asyncio.iscoroutinefunction(wrapper) + + def test_choose_wrapper_sync(self, mocker): + def sync_func(): + pass + + wrapper = ExpCacheHandler.choose_wrapper(sync_func, mocker.AsyncMock()) + assert not asyncio.iscoroutinefunction(wrapper) + + def test_validate_params(self): + with pytest.raises(ValueError): + ExpCacheHandler(func=lambda x: x, args=(), kwargs={}) + + def test_generate_tag(self): + class TestClass: + def test_method(self): + pass + + handler = ExpCacheHandler(func=TestClass().test_method, args=(TestClass(),), kwargs={"req": "test"}) + assert handler._generate_tag() == "TestClass.test_method" + + handler = ExpCacheHandler(func=lambda x: x, args=(), kwargs={"req": "test"}) + assert handler._generate_tag() == "" + + +class TestExpCache: + @pytest.fixture + def mock_exp_manager(self, mocker): + manager = mocker.MagicMock(spec=ExperienceManager) + manager.storage = mocker.MagicMock(spec=SimpleEngine) + manager.query_exps = mocker.AsyncMock() + manager.create_exp = mocker.MagicMock() + return manager + + @pytest.fixture + def mock_scorer(self, mocker): + scorer = mocker.MagicMock(spec=SimpleScorer) + scorer.evaluate = mocker.AsyncMock(return_value=Score()) + return scorer + + @pytest.fixture + def mock_perfect_judge(self, mocker): + return mocker.MagicMock(spec=SimplePerfectJudge) + + @pytest.fixture + def mock_config(self, mocker): + return mocker.patch("metagpt.exp_pool.decorator.config") + + @pytest.mark.asyncio + async def test_exp_cache_disabled(self, mock_config, mock_exp_manager): + mock_config.exp_pool.enable_read = False + + @exp_cache(manager=mock_exp_manager) + async def test_func(req): + return "result" + + result = await test_func(req="test") + assert result == "result" + mock_exp_manager.query_exps.assert_not_called() + + @pytest.mark.asyncio + async def test_exp_cache_enabled_no_perfect_exp(self, mock_config, mock_exp_manager, mock_scorer): + mock_config.exp_pool.enable_read = True + mock_exp_manager.query_exps.return_value = [] + + @exp_cache(manager=mock_exp_manager, scorer=mock_scorer) + async def test_func(req): + return "computed_result" + + result = await test_func(req="test") + assert result == "computed_result" + mock_exp_manager.query_exps.assert_called() + mock_exp_manager.create_exp.assert_called() + + @pytest.mark.asyncio + async def test_exp_cache_enabled_with_perfect_exp(self, mock_config, mock_exp_manager, mock_perfect_judge): + mock_config.exp_pool.enable_read = True + perfect_exp = Experience(req="test", resp="perfect_result") + mock_exp_manager.query_exps.return_value = [perfect_exp] + mock_perfect_judge.is_perfect_exp.return_value = True + + @exp_cache(manager=mock_exp_manager, perfect_judge=mock_perfect_judge) + async def test_func(req): + return "should_not_be_called" + + result = await test_func(req="test") + assert result == "perfect_result" + mock_exp_manager.query_exps.assert_called_once() + mock_exp_manager.create_exp.assert_not_called() diff --git a/tests/metagpt/exp_pool/test_manager.py b/tests/metagpt/exp_pool/test_manager.py index 3e8f47417..c12fc7e8c 100644 --- a/tests/metagpt/exp_pool/test_manager.py +++ b/tests/metagpt/exp_pool/test_manager.py @@ -4,20 +4,25 @@ from metagpt.config2 import Config from metagpt.configs.exp_pool_config import ExperiencePoolConfig from metagpt.configs.llm_config import LLMConfig from metagpt.exp_pool.manager import ExperienceManager -from metagpt.exp_pool.schema import MAX_SCORE, Experience, Metric, Score +from metagpt.exp_pool.schema import Experience from metagpt.rag.engines import SimpleEngine class TestExperienceManager: @pytest.fixture def mock_config(self): - return Config(llm=LLMConfig(), exp_pool=ExperiencePoolConfig(enable_write=True, enable_read=True)) + return Config( + llm=LLMConfig(), exp_pool=ExperiencePoolConfig(enable_write=True, enable_read=True, init_exp=False) + ) @pytest.fixture def mock_storage(self, mocker): engine = mocker.MagicMock(spec=SimpleEngine) engine.add_objs = mocker.MagicMock() engine.aretrieve = mocker.AsyncMock(return_value=[]) + engine._retriever = mocker.MagicMock() + engine._retriever._vector_store = mocker.MagicMock() + engine._retriever._vector_store._get = mocker.MagicMock(return_value=mocker.MagicMock(ids=[])) return engine @pytest.fixture @@ -33,7 +38,7 @@ class TestExperienceManager: def test_create_exp(self, mock_experience_manager, mock_experience): mock_experience_manager.create_exp(mock_experience) - mock_experience_manager.storage.add_objs.assert_called_once_with([mock_experience]) + mock_experience_manager.storage.add_objs.assert_called_with([mock_experience]) def test_create_exp_write_disabled(self, mock_experience_manager, mock_experience, mock_config): mock_config.exp_pool.enable_write = False @@ -60,18 +65,44 @@ class TestExperienceManager: result = await mock_experience_manager.query_exps("query") assert result == [] - def test_extract_one_perfect_exp(self, mock_experience_manager): - experiences = [ - Experience(req="req", resp="resp", metric=Metric(score=Score(val=MAX_SCORE))), - Experience(req="req", resp="resp"), - ] - perfect_exp: Experience = mock_experience_manager.extract_one_perfect_exp(experiences) - assert perfect_exp is not None - assert perfect_exp.metric.score.val == MAX_SCORE + def test_init_exp_pool(self, mock_experience_manager, mock_config, mocker): + mock_experience_manager._has_exps = mocker.MagicMock(return_value=False) + mock_experience_manager._init_teamleader_exps = mocker.MagicMock() + mock_experience_manager._init_engineer2_exps = mocker.MagicMock() - def test_is_perfect_exp(self): - exp = Experience(req="req", resp="resp", metric=Metric(score=Score(val=MAX_SCORE))) - assert ExperienceManager.is_perfect_exp(exp) == True + mock_config.exp_pool.init_exp = True + mock_experience_manager.init_exp_pool() - exp = Experience(req="req", resp="resp") - assert ExperienceManager.is_perfect_exp(exp) == False + mock_experience_manager._has_exps.assert_called_once() + mock_experience_manager._init_teamleader_exps.assert_called_once() + mock_experience_manager._init_engineer2_exps.assert_called_once() + + def test_init_exp_pool_already_has_exps(self, mock_experience_manager, mock_config, mocker): + mock_experience_manager._has_exps = mocker.MagicMock(return_value=True) + mock_experience_manager._init_teamleader_exps = mocker.MagicMock() + mock_experience_manager._init_engineer2_exps = mocker.MagicMock() + + mock_config.exp_pool.init_exp = True + mock_experience_manager.init_exp_pool() + + mock_experience_manager._has_exps.assert_called_once() + mock_experience_manager._init_teamleader_exps.assert_not_called() + mock_experience_manager._init_engineer2_exps.assert_not_called() + + def test_has_exps(self, mock_experience_manager, mock_storage): + mock_storage._retriever._vector_store._get.return_value.ids = ["id1"] + + assert mock_experience_manager._has_exps() is True + + mock_storage._retriever._vector_store._get.return_value.ids = [] + assert mock_experience_manager._has_exps() is False + + def test_init_teamleader_exps(self, mock_experience_manager, mocker): + mock_experience_manager._init_exp = mocker.MagicMock() + mock_experience_manager._init_teamleader_exps() + mock_experience_manager._init_exp.assert_called_once() + + def test_init_engineer2_exps(self, mock_experience_manager, mocker): + mock_experience_manager._init_exp = mocker.MagicMock() + mock_experience_manager._init_engineer2_exps() + mock_experience_manager._init_exp.assert_called_once() diff --git a/tests/metagpt/exp_pool/test_perfect_judges/test_simple_perfect_judge.py b/tests/metagpt/exp_pool/test_perfect_judges/test_simple_perfect_judge.py new file mode 100644 index 000000000..5abd04f0d --- /dev/null +++ b/tests/metagpt/exp_pool/test_perfect_judges/test_simple_perfect_judge.py @@ -0,0 +1,40 @@ +import pytest + +from metagpt.exp_pool.perfect_judges import SimplePerfectJudge +from metagpt.exp_pool.schema import MAX_SCORE, Experience, Metric, Score + + +class TestSimplePerfectJudge: + @pytest.fixture + def simple_perfect_judge(self): + return SimplePerfectJudge() + + @pytest.mark.asyncio + async def test_is_perfect_exp_perfect_match(self, simple_perfect_judge): + exp = Experience(req="test_request", resp="resp", metric=Metric(score=Score(val=MAX_SCORE))) + result = await simple_perfect_judge.is_perfect_exp(exp, "test_request") + assert result is True + + @pytest.mark.asyncio + async def test_is_perfect_exp_imperfect_score(self, simple_perfect_judge): + exp = Experience(req="test_request", resp="resp", metric=Metric(score=Score(val=MAX_SCORE - 1))) + result = await simple_perfect_judge.is_perfect_exp(exp, "test_request") + assert result is False + + @pytest.mark.asyncio + async def test_is_perfect_exp_mismatched_request(self, simple_perfect_judge): + exp = Experience(req="test_request", resp="resp", metric=Metric(score=Score(val=MAX_SCORE))) + result = await simple_perfect_judge.is_perfect_exp(exp, "different_request") + assert result is False + + @pytest.mark.asyncio + async def test_is_perfect_exp_no_metric(self, simple_perfect_judge): + exp = Experience(req="test_request", resp="resp") + result = await simple_perfect_judge.is_perfect_exp(exp, "test_request") + assert result is False + + @pytest.mark.asyncio + async def test_is_perfect_exp_no_score(self, simple_perfect_judge): + exp = Experience(req="test_request", resp="resp", metric=Metric()) + result = await simple_perfect_judge.is_perfect_exp(exp, "test_request") + assert result is False diff --git a/tests/metagpt/exp_pool/test_scorers/test_simple_scorer.py b/tests/metagpt/exp_pool/test_scorers/test_simple_scorer.py new file mode 100644 index 000000000..043f105d0 --- /dev/null +++ b/tests/metagpt/exp_pool/test_scorers/test_simple_scorer.py @@ -0,0 +1,49 @@ +import pytest + +from metagpt.exp_pool.schema import Score +from metagpt.exp_pool.scorers.simple import SIMPLE_SCORER_TEMPLATE, SimpleScorer +from metagpt.llm import BaseLLM + + +class TestSimpleScorer: + @pytest.fixture + def mock_llm(self, mocker): + mock_llm = mocker.MagicMock(spec=BaseLLM) + return mock_llm + + @pytest.fixture + def simple_scorer(self, mock_llm): + return SimpleScorer(llm=mock_llm) + + def test_init(self, mock_llm): + scorer = SimpleScorer(llm=mock_llm) + assert isinstance(scorer.llm, BaseLLM) + + @pytest.mark.asyncio + async def test_evaluate(self, simple_scorer, mock_llm): + # Mock function to evaluate + def mock_func(a, b): + """This is a mock function.""" + return a + b + + # Mock LLM response + mock_llm.aask.return_value = '```json\n{"val": 8, "reason": "Good performance"}\n```' + + # Test evaluate method + result = await simple_scorer.evaluate(mock_func, 5, args=(2, 3), kwargs={}) + + # Assert LLM was called with correct prompt + expected_prompt = SIMPLE_SCORER_TEMPLATE.format( + func_name=mock_func.__name__, + func_doc=mock_func.__doc__, + func_signature="(a, b)", + func_args=(2, 3), + func_kwargs={}, + func_result=5, + ) + mock_llm.aask.assert_called_once_with(expected_prompt) + + # Assert the result is correct + assert isinstance(result, Score) + assert result.val == 8 + assert result.reason == "Good performance" From 3fb1432158e6240b4546580de608d684e4109767 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Mon, 8 Jul 2024 10:50:20 +0800 Subject: [PATCH 050/119] delete unnecessary file --- tests/metagpt/utils/test_reflection.py | 35 -------------------------- 1 file changed, 35 deletions(-) delete mode 100644 tests/metagpt/utils/test_reflection.py diff --git a/tests/metagpt/utils/test_reflection.py b/tests/metagpt/utils/test_reflection.py deleted file mode 100644 index 58fd81619..000000000 --- a/tests/metagpt/utils/test_reflection.py +++ /dev/null @@ -1,35 +0,0 @@ -from metagpt.utils.reflection import get_class_name - - -class SimpleFunction: - def function(self): - pass - - -class SampleClass: - @classmethod - def class_method(cls): - pass - - def instance_method(self): - pass - - -def standalone_function(): - pass - - -class TestGetClassName: - def test_instance_method(self): - instance = SampleClass() - assert get_class_name(instance.instance_method) == "SampleClass" - - def test_class_method(self): - assert get_class_name(SampleClass.class_method) == "SampleClass" - - def test_standalone_function(self): - assert get_class_name(standalone_function) == "" - - def test_function_within_simple_class(self): - instance = SimpleFunction() - assert get_class_name(instance.function) == "SimpleFunction" From 361294d31d4f2d478e7f9149cf2be08670afb261 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Mon, 8 Jul 2024 11:11:07 +0800 Subject: [PATCH 051/119] delete unnecessary code --- metagpt/exp_pool/schema.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/metagpt/exp_pool/schema.py b/metagpt/exp_pool/schema.py index d59478742..627dcbb4e 100644 --- a/metagpt/exp_pool/schema.py +++ b/metagpt/exp_pool/schema.py @@ -3,7 +3,6 @@ from enum import Enum from typing import Optional -from llama_index.core.schema import TextNode from pydantic import BaseModel, Field MAX_SCORE = 10 @@ -71,18 +70,3 @@ class Experience(BaseModel): def rag_key(self): return self.req - - -class ExperienceNodeMetadata(BaseModel): - """Metadata of ExperienceNode.""" - - resp: str = Field(..., description="") - - -class ExperienceNode(TextNode): - """ExperienceNode for RAG.""" - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.excluded_llm_metadata_keys = list(ExperienceNodeMetadata.model_fields.keys()) - self.excluded_embed_metadata_keys = self.excluded_llm_metadata_keys From a2bb67a1f0cf829e3856a1a128edb03fd25377d5 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Mon, 8 Jul 2024 15:24:18 +0800 Subject: [PATCH 052/119] move the req_serialize from di/role_zero.py to context_builders/role_zero.py --- .../exp_pool/context_builders/role_zero.py | 24 +++++++++++++++++ metagpt/exp_pool/decorator.py | 1 - metagpt/exp_pool/manager.py | 1 + metagpt/roles/di/role_zero.py | 27 +++---------------- 4 files changed, 28 insertions(+), 25 deletions(-) diff --git a/metagpt/exp_pool/context_builders/role_zero.py b/metagpt/exp_pool/context_builders/role_zero.py index 60f71ef59..e9ab83d90 100644 --- a/metagpt/exp_pool/context_builders/role_zero.py +++ b/metagpt/exp_pool/context_builders/role_zero.py @@ -1,4 +1,6 @@ """RoleZero context builder.""" +import copy +import json from metagpt.exp_pool.context_builders.base import BaseContextBuilder @@ -24,3 +26,25 @@ class RoleZeroContextBuilder(BaseContextBuilder): def replace_example_content(self, text: str, new_example_content: str) -> str: return self.replace_content_between_markers(text, "# Example", "# Instruction", new_example_content) + + @staticmethod + def req_serialize(req: list[dict]) -> str: + """Serialize the request for database storage, ensuring it is a string. + + This function deep copies the request and modifies the content of the last element + to remove unnecessary sections, making the request more concise. + """ + + req_copy = copy.deepcopy(req) + + last_content = req_copy[-1]["content"] + last_content = RoleZeroContextBuilder.replace_content_between_markers( + last_content, "# Data Structure", "# Current Plan", "" + ) + last_content = RoleZeroContextBuilder.replace_content_between_markers( + last_content, "# Example", "# Instruction", "" + ) + + req_copy[-1]["content"] = last_content + + return json.dumps(req_copy) diff --git a/metagpt/exp_pool/decorator.py b/metagpt/exp_pool/decorator.py index c518bb7ea..10f3355f9 100644 --- a/metagpt/exp_pool/decorator.py +++ b/metagpt/exp_pool/decorator.py @@ -56,7 +56,6 @@ def exp_cache( @functools.wraps(func) async def get_or_create(args: Any, kwargs: Any) -> ReturnType: - logger.info("exp_cache is enabled.") handler = ExpCacheHandler( func=func, args=args, diff --git a/metagpt/exp_pool/manager.py b/metagpt/exp_pool/manager.py index 276b1e8e3..23198eb02 100644 --- a/metagpt/exp_pool/manager.py +++ b/metagpt/exp_pool/manager.py @@ -49,6 +49,7 @@ class ExperienceManager(BaseModel): self.init_exp_pool() + logger.debug(f"exp_pool config: {self.config.exp_pool}") return self @handle_exception diff --git a/metagpt/roles/di/role_zero.py b/metagpt/roles/di/role_zero.py index 2c2c81551..103a77911 100644 --- a/metagpt/roles/di/role_zero.py +++ b/metagpt/roles/di/role_zero.py @@ -1,6 +1,5 @@ from __future__ import annotations -import copy import inspect import json import re @@ -166,32 +165,12 @@ class RoleZero(Role): return True - @exp_cache(context_builder=RoleZeroContextBuilder(), req_serialize=lambda req: RoleZero._req_serialize(req)) + @exp_cache( + context_builder=RoleZeroContextBuilder(), req_serialize=lambda req: RoleZeroContextBuilder.req_serialize(req) + ) async def llm_cached_aask(self, *, req: list[dict], system_msgs: list[str]) -> str: return await self.llm.aask(req, system_msgs=system_msgs) - @staticmethod - def _req_serialize(req: list[dict]) -> str: - """Serialize the request for database storage, ensuring it is a string. - - This function deep copies the request and modifies the content of the last element - to remove unnecessary sections, making the request more concise. - """ - - req_copy = copy.deepcopy(req) - - last_content = req_copy[-1]["content"] - last_content = RoleZeroContextBuilder.replace_content_between_markers( - last_content, "# Data Structure", "# Current Plan", "" - ) - last_content = RoleZeroContextBuilder.replace_content_between_markers( - last_content, "# Example", "# Instruction", "" - ) - - req_copy[-1]["content"] = last_content - - return json.dumps(req_copy) - async def _act(self) -> Message: if self.use_fixed_sop: return await super()._act() From f61506bd3220252351882cbdb8e0e28cf5513b13 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Mon, 8 Jul 2024 16:24:37 +0800 Subject: [PATCH 053/119] update format_exps --- metagpt/exp_pool/context_builders/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/metagpt/exp_pool/context_builders/base.py b/metagpt/exp_pool/context_builders/base.py index e3fe320a6..a261e452e 100644 --- a/metagpt/exp_pool/context_builders/base.py +++ b/metagpt/exp_pool/context_builders/base.py @@ -25,7 +25,8 @@ class BaseContextBuilder(BaseModel, ABC): result = [] for i, exp in enumerate(self.exps, start=1): - result.append(f"{i}. " + EXP_TEMPLATE.format(req=exp.req, resp=exp.resp, score=exp.metric.score.val)) + score_val = exp.metric.score.val if exp.metric and exp.metric.score else "N/A" + result.append(f"{i}. " + EXP_TEMPLATE.format(req=exp.req, resp=exp.resp, score=score_val)) return "\n".join(result) From 1ead3e4d8083c258d0d418eb3cfab3564504a188 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Mon, 8 Jul 2024 20:55:32 +0800 Subject: [PATCH 054/119] update simple_scorer --- examples/exp_pool/scorer.py | 19 ++++--- metagpt/exp_pool/decorator.py | 2 +- metagpt/exp_pool/scorers/base.py | 16 +----- metagpt/exp_pool/scorers/simple.py | 53 ++++++------------- tests/metagpt/exp_pool/test_decorator.py | 7 ++- .../test_scorers/test_simple_scorer.py | 49 +++++++++++------ 6 files changed, 71 insertions(+), 75 deletions(-) diff --git a/examples/exp_pool/scorer.py b/examples/exp_pool/scorer.py index 1efe07bdf..c412feaf3 100644 --- a/examples/exp_pool/scorer.py +++ b/examples/exp_pool/scorer.py @@ -1,20 +1,27 @@ import asyncio from metagpt.exp_pool.scorers import SimpleScorer -from metagpt.logs import logger +REQ = "Write a program to implement quicksort in python." -def echo(req: str): - """Echo from req.""" +RESP1 = """ +def quicksort(arr): + return quicksort([x for x in arr[1:] if x <= arr[0]]) + [arr[0]] + quicksort([x for x in arr[1:] if x > arr[0]]) +""" - return req +RESP2 = """ +def quicksort(arr): + if len(arr) <= 1: + return arr + return quicksort([x for x in arr[1:] if x <= arr[0]]) + [arr[0]] + quicksort([x for x in arr[1:] if x > arr[0]]) +""" async def simple(): scorer = SimpleScorer() - score = await scorer.evaluate(echo, "data", ("data",)) - logger.info(f"The score is: {score}") + await scorer.evaluate(req=REQ, resp=RESP1) + await scorer.evaluate(req=REQ, resp=RESP2) async def main(): diff --git a/metagpt/exp_pool/decorator.py b/metagpt/exp_pool/decorator.py index 10f3355f9..4e7213dfe 100644 --- a/metagpt/exp_pool/decorator.py +++ b/metagpt/exp_pool/decorator.py @@ -159,7 +159,7 @@ class ExpCacheHandler(BaseModel): async def evaluate_experience(self): """Evaluate the experience, and save the score.""" - self._score = await self.exp_scorer.evaluate(self.func, self._resp, self.args, self.kwargs) + self._score = await self.exp_scorer.evaluate(self._req, self._resp) def save_experience(self): """Save the new experience.""" diff --git a/metagpt/exp_pool/scorers/base.py b/metagpt/exp_pool/scorers/base.py index 94623c30f..97cac4992 100644 --- a/metagpt/exp_pool/scorers/base.py +++ b/metagpt/exp_pool/scorers/base.py @@ -1,7 +1,6 @@ """Base scorer.""" from abc import ABC, abstractmethod -from typing import Any, Callable from pydantic import BaseModel, ConfigDict @@ -12,16 +11,5 @@ class BaseScorer(BaseModel, ABC): model_config = ConfigDict(arbitrary_types_allowed=True) @abstractmethod - async def evaluate(self, func: Callable, result: Any, args: tuple = None, kwargs: dict = None) -> Score: - """Evaluate the quality of the result produced by the function and parameters. - - Args: - func (Callable): The function whose result is to be evaluated. - result (Any): The result produced by the function. - args (Tuple[Any, ...]): The tuple of arguments that were passed to the function. - kwargs (Dict[str, Any]): The dictionary of keyword arguments that were passed to the function. - - Example: - result = await sample(5, name="foo") - score = await scorer.evaluate(sample, result, args=(5), kwargs={"name": "foo"}) - """ + async def evaluate(self, req: str, resp: str) -> Score: + """Evaluates the quality of a response relative to a given request.""" diff --git a/metagpt/exp_pool/scorers/simple.py b/metagpt/exp_pool/scorers/simple.py index 1fda189d1..fd7b6537b 100644 --- a/metagpt/exp_pool/scorers/simple.py +++ b/metagpt/exp_pool/scorers/simple.py @@ -1,8 +1,6 @@ """Simple scorer.""" -import inspect import json -from typing import Any, Callable from pydantic import Field @@ -13,24 +11,16 @@ from metagpt.provider.base_llm import BaseLLM from metagpt.utils.common import CodeParser SIMPLE_SCORER_TEMPLATE = """ -Role: You're an expert score evaluator. You specialize in assessing the output of the given function, based on its intended requirement and produced result. +Role: You are a highly efficient assistant, tasked with evaluating a response to a given request. The response is generated by a large language model (LLM). + +I will provide you with a request and a corresponding response. Your task is to assess this response and provide a score from a human perspective. ## Context -### Function Name -{func_name} +### Request +{req} -### Function Document -{func_doc} - -### Function Signature -{func_signature} - -### Function Parameters -args: {func_args} -kwargs: {func_kwargs} - -### Produced Result By Function and Parameters -{func_result} +### Response +{resp} ## Format Example ```json @@ -41,10 +31,10 @@ kwargs: {func_kwargs} ``` ## Instructions -- Understand the function and requirements given by the user. -- Analyze the results produced by the function. -- Grade the results based on level of alignment with the requirements. -- Provide a score on a scale defined by user or a default scale (1 to 10). +- Understand the request and response given by the user. +- Evaluate the response based on its quality relative to the given request. +- Provide a score from 1 to 10, where 10 is the best. +- Provide a reason supporting your score. ## Constraint Format: Just print the result in json format like **Format Example**. @@ -57,26 +47,17 @@ Follow instructions, generate output and make sure it follows the **Constraint** class SimpleScorer(BaseScorer): llm: BaseLLM = Field(default_factory=LLM) - async def evaluate(self, func: Callable, result: Any, args: tuple = None, kwargs: dict = None) -> Score: - """Evaluates the quality of content by LLM. + async def evaluate(self, req: str, resp: str) -> Score: + """Evaluates the quality of a response relative to a given request, as scored by an LLM. Args: - func: The function to evaluate. - result: The result produced by the function. - args: The positional arguments used when calling the function, if any. - kwargs: The keyword arguments used when calling the function, if any. + req (str): The request. + resp (str): The response. Returns: - A Score object containing the evaluation results. + Score: An object containing the score (1-10) and the reasoning. """ - prompt = SIMPLE_SCORER_TEMPLATE.format( - func_name=func.__name__, - func_doc=func.__doc__, - func_signature=inspect.signature(func), - func_args=args, - func_kwargs=kwargs, - func_result=result, - ) + prompt = SIMPLE_SCORER_TEMPLATE.format(req=req, resp=resp) resp = await self.llm.aask(prompt) resp_json = json.loads(CodeParser.parse_code(resp, lang="json")) diff --git a/tests/metagpt/exp_pool/test_decorator.py b/tests/metagpt/exp_pool/test_decorator.py index c0b3fe36d..0c02dcdfc 100644 --- a/tests/metagpt/exp_pool/test_decorator.py +++ b/tests/metagpt/exp_pool/test_decorator.py @@ -2,6 +2,8 @@ import asyncio import pytest +from metagpt.config2 import Config +from metagpt.configs.exp_pool_config import ExperiencePoolConfig from metagpt.exp_pool.context_builders import SimpleContextBuilder from metagpt.exp_pool.decorator import ExpCacheHandler, exp_cache from metagpt.exp_pool.manager import ExperienceManager @@ -20,6 +22,8 @@ class TestExpCacheHandler: def mock_exp_manager(self, mocker): manager = mocker.MagicMock(spec=ExperienceManager) manager.storage = mocker.MagicMock(spec=SimpleEngine) + manager.config = mocker.MagicMock(spec=Config) + manager.config.exp_pool = ExperiencePoolConfig() manager.query_exps = mocker.AsyncMock() manager.create_exp = mocker.MagicMock() return manager @@ -131,9 +135,10 @@ class TestExpCacheHandler: class TestExpCache: @pytest.fixture - def mock_exp_manager(self, mocker): + def mock_exp_manager(self, mocker, mock_config): manager = mocker.MagicMock(spec=ExperienceManager) manager.storage = mocker.MagicMock(spec=SimpleEngine) + manager.config = mock_config manager.query_exps = mocker.AsyncMock() manager.create_exp = mocker.MagicMock() return manager diff --git a/tests/metagpt/exp_pool/test_scorers/test_simple_scorer.py b/tests/metagpt/exp_pool/test_scorers/test_simple_scorer.py index 043f105d0..e17edfca8 100644 --- a/tests/metagpt/exp_pool/test_scorers/test_simple_scorer.py +++ b/tests/metagpt/exp_pool/test_scorers/test_simple_scorer.py @@ -1,3 +1,5 @@ +import json + import pytest from metagpt.exp_pool.schema import Score @@ -20,30 +22,43 @@ class TestSimpleScorer: assert isinstance(scorer.llm, BaseLLM) @pytest.mark.asyncio - async def test_evaluate(self, simple_scorer, mock_llm): - # Mock function to evaluate - def mock_func(a, b): - """This is a mock function.""" - return a + b + async def test_evaluate(self, simple_scorer, mock_llm, mocker): + # Mock request and response + req = "What is the capital of France?" + resp = "The capital of France is Paris." # Mock LLM response - mock_llm.aask.return_value = '```json\n{"val": 8, "reason": "Good performance"}\n```' + mock_llm_response = '{"val": 9, "reason": "Accurate and concise answer"}' + mock_llm.aask.return_value = f"```json\n{mock_llm_response}\n```" + + # Mock CodeParser.parse_code + mocker.patch("metagpt.utils.common.CodeParser.parse_code", return_value=mock_llm_response) # Test evaluate method - result = await simple_scorer.evaluate(mock_func, 5, args=(2, 3), kwargs={}) + result = await simple_scorer.evaluate(req, resp) # Assert LLM was called with correct prompt - expected_prompt = SIMPLE_SCORER_TEMPLATE.format( - func_name=mock_func.__name__, - func_doc=mock_func.__doc__, - func_signature="(a, b)", - func_args=(2, 3), - func_kwargs={}, - func_result=5, - ) + expected_prompt = SIMPLE_SCORER_TEMPLATE.format(req=req, resp=resp) mock_llm.aask.assert_called_once_with(expected_prompt) # Assert the result is correct assert isinstance(result, Score) - assert result.val == 8 - assert result.reason == "Good performance" + assert result.val == 9 + assert result.reason == "Accurate and concise answer" + + @pytest.mark.asyncio + async def test_evaluate_invalid_response(self, simple_scorer, mock_llm, mocker): + # Mock request and response + req = "What is the capital of France?" + resp = "The capital of France is Paris." + + # Mock LLM response with invalid JSON + mock_llm_response = "Invalid JSON" + mock_llm.aask.return_value = f"```json\n{mock_llm_response}\n```" + + # Mock CodeParser.parse_code + mocker.patch("metagpt.utils.common.CodeParser.parse_code", return_value=mock_llm_response) + + # Test evaluate method with invalid response + with pytest.raises(json.JSONDecodeError): + await simple_scorer.evaluate(req, resp) From ff5dbfbc521d6e89c418acee21426b4e906a74a3 Mon Sep 17 00:00:00 2001 From: seeker Date: Fri, 5 Jul 2024 19:53:05 +0800 Subject: [PATCH 055/119] =?UTF-8?q?update:=20terminal=20run=20command=20?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E5=BC=82=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- metagpt/roles/di/swe_agent.py | 8 +- metagpt/tools/libs/terminal.py | 118 ++++++++++-------- .../roles/di/run_swe_agent_for_benchmark.py | 10 +- tests/metagpt/tools/libs/test_terminal.py | 11 +- 4 files changed, 81 insertions(+), 66 deletions(-) diff --git a/metagpt/roles/di/swe_agent.py b/metagpt/roles/di/swe_agent.py index 2e1fb6412..f31300c3f 100644 --- a/metagpt/roles/di/swe_agent.py +++ b/metagpt/roles/di/swe_agent.py @@ -29,7 +29,7 @@ class SWEAgent(RoleZero): async def _think(self) -> bool: self._update_system_msg() - self._format_instruction() + await self._format_instruction() res = await super()._think() if self.run_eval: await self._parse_commands_for_eval() @@ -46,14 +46,14 @@ class SWEAgent(RoleZero): self._bash_window_size = int(os.getenv("WINDOW")) self.system_msg = [self._system_msg.format(WINDOW=self._bash_window_size)] - def _format_instruction(self): + async def _format_instruction(self): """ Formats the instruction message for the SWE agent. Runs the "state" command in the terminal, parses its output as JSON, and uses it to format the `_instruction` template. """ - state_output = self.terminal.run("state") + state_output = await self.terminal.run("state") bash_state = json.loads(state_output) self.instruction = self._instruction.format( @@ -81,7 +81,7 @@ class SWEAgent(RoleZero): if "end" != cmd.get("command_name", ""): return try: - diff_output = self.terminal.run("git diff --cached") + diff_output = await self.terminal.run("git diff --cached") clear_diff = extract_patch(diff_output) logger.info(f"Diff output: \n{clear_diff}") if clear_diff: diff --git a/metagpt/tools/libs/terminal.py b/metagpt/tools/libs/terminal.py index bcf039a5e..73d5e72cf 100644 --- a/metagpt/tools/libs/terminal.py +++ b/metagpt/tools/libs/terminal.py @@ -1,8 +1,10 @@ -import subprocess -import threading -from queue import Queue +import asyncio +from asyncio import Queue +from asyncio.subprocess import PIPE +from typing import Optional from metagpt.const import DEFAULT_WORKSPACE_ROOT, SWE_SETUP_PATH +from metagpt.logs import logger from metagpt.tools.tool_registry import register_tool from metagpt.utils.report import END_MARKER_VALUE, TerminalReporter @@ -19,62 +21,54 @@ class Terminal: def __init__(self): self.shell_command = ["bash"] # FIXME: should consider windows support later self.command_terminator = "\n" - - # Start a persistent shell process - self.process = subprocess.Popen( - self.shell_command, - shell=True, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - executable="/bin/bash", - ) - self.stdout_queue = Queue() + self.stdout_queue = Queue(maxsize=1000) self.observer = TerminalReporter() + self.process: Optional[asyncio.subprocess.Process] = None - self._check_state() + async def _start_process(self): + # Start a persistent shell process + self.process = await asyncio.create_subprocess_exec( + *self.shell_command, stdin=PIPE, stdout=PIPE, stderr=PIPE, executable="/bin/bash" + ) + await self._check_state() - def _check_state(self): - """Check the state of the terminal, e.g. the current directory of the terminal process. Useful for agent to understand.""" - print("The terminal is at:", self.run_command("pwd")) + async def _check_state(self): + """ + Check the state of the terminal, e.g. the current directory of the terminal process. Useful for agent to understand. + """ + output = await self.run_command("pwd") + logger.info("The terminal is at:", output) - def run_command(self, cmd: str, daemon=False) -> str: + async def run_command(self, cmd: str, daemon=False) -> str: """ Executes a specified command in the terminal and streams the output back in real time. This command maintains state across executions, such as the current directory, - allowing for sequential commands to be contextually aware. The output from the - command execution is placed into `stdout_queue`, which can be consumed as needed. + allowing for sequential commands to be contextually aware. Args: cmd (str): The command to execute in the terminal. - daemon (bool): If True, executes the command in a background thread, allowing - the main program to continue execution. The command's output is - collected asynchronously in daemon mode and placed into `stdout_queue`. - + daemon (bool): If True, executes the command in an asynchronous task, allowing + the main program to continue execution. Returns: str: The command's output or an empty string if `daemon` is True. Remember that - when `daemon` is True, the output is collected into `stdout_queue` and must - be consumed from there. - - Note: - If `stdout_queue` is not periodically consumed, it could potentially grow indefinitely, - consuming memory. Ensure that there's a mechanism in place to consume this queue, - especially during long-running or output-heavy command executions. + when `daemon` is True, use the `get_stdout_output` method to get the output. """ + if self.process is None: + await self._start_process() # Send the command self.process.stdin.write((cmd + self.command_terminator).encode()) self.process.stdin.write( - (f'echo "{END_MARKER_VALUE}"{self.command_terminator}').encode() # write EOF + f'echo "{END_MARKER_VALUE}"{self.command_terminator}'.encode() # write EOF ) # Unique marker to signal command end - self.process.stdin.flush() + await self.process.stdin.drain() if daemon: - threading.Thread(target=self._read_and_process_output, args=(cmd,), daemon=True).start() + asyncio.create_task(self._read_and_process_output(cmd)) return "" else: - return self._read_and_process_output(cmd) + return await self._read_and_process_output(cmd) - def execute_in_conda_env(self, cmd: str, env, daemon=False) -> str: + async def execute_in_conda_env(self, cmd: str, env, daemon=False) -> str: """ Executes a given command within a specified Conda environment automatically without the need for manual activation. Users just need to provide the name of the Conda @@ -84,7 +78,7 @@ class Terminal: cmd (str): The command to execute within the Conda environment. env (str, optional): The name of the Conda environment to activate before executing the command. If not specified, the command will run in the current active environment. - daemon (bool): If True, the command is run in a background thread, similar to `run_command`, + daemon (bool): If True, the command is run in an asynchronous task, similar to `run_command`, affecting error logging and handling in the same manner. Returns: @@ -96,19 +90,32 @@ class Terminal: to ensure the specified environment is active for the command's execution. """ cmd = f"conda run -n {env} {cmd}" - return self.run_command(cmd, daemon=daemon) + return await self.run_command(cmd, daemon=daemon) - def _read_and_process_output(self, cmd): - with self.observer as observer: + async def get_stdout_output(self) -> str: + """ + Retrieves all collected output from background running commands and returns it as a string. + + Returns: + str: The collected output from background running commands, returned as a string. + """ + output_lines = [] + while not self.stdout_queue.empty(): + line = await self.stdout_queue.get() + output_lines.append(line) + return "\n".join(output_lines) + + async def _read_and_process_output(self, cmd, daemon=False) -> str: + async with self.observer as observer: cmd_output = [] - observer.report(cmd + self.command_terminator, "cmd") - # report the comman + await observer.async_report(cmd + self.command_terminator, "cmd") + # report the command # Read the output until the unique marker is found. # We read bytes directly from stdout instead of text because when reading text, # '\r' is changed to '\n', resulting in excessive output. tmp = b"" while True: - output = tmp + self.process.stdout.read(1) + output = tmp + await self.process.stdout.read(1) *lines, tmp = output.splitlines(True) for line in lines: line = line.decode() @@ -123,13 +130,13 @@ class Terminal: # log stdout in real-time observer.report(line, "output") cmd_output.append(line) - self.stdout_queue.put(line) + if daemon: + await self.stdout_queue.put(line) - def close(self): + async def close(self): """Close the persistent shell process.""" self.process.stdin.close() - self.process.terminate() - self.process.wait() + await self.process.wait() @register_tool(include_functions=["run"]) @@ -142,10 +149,13 @@ class Bash(Terminal): def __init__(self): """init""" super().__init__() - self.run_command(f"cd {DEFAULT_WORKSPACE_ROOT}") - self.run_command(f"source {SWE_SETUP_PATH}") + self.start_flag = False - def run(self, cmd) -> str: + async def start(self): + await self.run_command(f"cd {DEFAULT_WORKSPACE_ROOT}") + await self.run_command(f"source {SWE_SETUP_PATH}") + + async def run(self, cmd) -> str: """ Executes a bash command. @@ -222,4 +232,8 @@ class Bash(Terminal): Note: Make sure to use these functions as per their defined arguments and behaviors. """ - return self.run_command(cmd) + if not self.start_flag: + await self.start() + self.start_flag = True + + return await self.run_command(cmd) diff --git a/tests/metagpt/roles/di/run_swe_agent_for_benchmark.py b/tests/metagpt/roles/di/run_swe_agent_for_benchmark.py index 54b3623a4..e2aa3d17f 100644 --- a/tests/metagpt/roles/di/run_swe_agent_for_benchmark.py +++ b/tests/metagpt/roles/di/run_swe_agent_for_benchmark.py @@ -59,11 +59,11 @@ async def run(instance, swe_result_dir): # 前处理 terminal = Terminal() - terminal.run_command(f"cd {repo_path} && git reset --hard && git clean -n -d && git clean -f -d") - terminal.run_command("BRANCH=$(git remote show origin | awk '/HEAD branch/ {print $NF}')") - logger.info(terminal.run_command("echo $BRANCH")) - logger.info(terminal.run_command('git checkout "$BRANCH"')) - logger.info(terminal.run_command("git branch")) + await terminal.run_command(f"cd {repo_path} && git reset --hard && git clean -n -d && git clean -f -d") + await terminal.run_command("BRANCH=$(git remote show origin | awk '/HEAD branch/ {print $NF}')") + logger.info(await terminal.run_command("echo $BRANCH")) + logger.info(await terminal.run_command('git checkout "$BRANCH"')) + logger.info(await terminal.run_command("git branch")) user_requirement_and_issue = INSTANCE_TEMPLATE.format( issue=instance["problem_statement"], diff --git a/tests/metagpt/tools/libs/test_terminal.py b/tests/metagpt/tools/libs/test_terminal.py index 98ed63dd8..9c64009ae 100644 --- a/tests/metagpt/tools/libs/test_terminal.py +++ b/tests/metagpt/tools/libs/test_terminal.py @@ -4,16 +4,17 @@ from metagpt.const import DATA_PATH, METAGPT_ROOT from metagpt.tools.libs.terminal import Terminal -def test_terminal(): +@pytest.mark.asyncio +async def test_terminal(): terminal = Terminal() - terminal.run_command(f"cd {METAGPT_ROOT}") - output = terminal.run_command("pwd") + await terminal.run_command(f"cd {METAGPT_ROOT}") + output = await terminal.run_command("pwd") assert output.strip() == str(METAGPT_ROOT) # pwd now should be METAGPT_ROOT, cd data should land in DATA_PATH - terminal.run_command("cd data") - output = terminal.run_command("pwd") + await terminal.run_command("cd data") + output = await terminal.run_command("pwd") assert output.strip() == str(DATA_PATH) From 086ef5e8055e0ad43833bfcd985bb46a801cf1d7 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Tue, 9 Jul 2024 12:41:44 +0800 Subject: [PATCH 056/119] update comment --- metagpt/exp_pool/decorator.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/metagpt/exp_pool/decorator.py b/metagpt/exp_pool/decorator.py index 4e7213dfe..62f766b9d 100644 --- a/metagpt/exp_pool/decorator.py +++ b/metagpt/exp_pool/decorator.py @@ -109,6 +109,11 @@ class ExpCacheHandler(BaseModel): @model_validator(mode="after") def initialize(self): + """Initialize default values for optional parameters if they are None. + + This is necessary because the decorator might pass None, which would override the default values set by Field. + """ + self._validate_params() self.exp_manager = self.exp_manager or exp_manager From 5a83c4539ae86eb5d4dd46ef4f9fd97de51851b9 Mon Sep 17 00:00:00 2001 From: lidanyang Date: Tue, 9 Jul 2024 16:51:58 +0800 Subject: [PATCH 057/119] avoid truncate output of html content --- metagpt/actions/di/execute_nb_code.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/metagpt/actions/di/execute_nb_code.py b/metagpt/actions/di/execute_nb_code.py index cc2c05341..91480d3a7 100644 --- a/metagpt/actions/di/execute_nb_code.py +++ b/metagpt/actions/di/execute_nb_code.py @@ -191,7 +191,8 @@ class ExecuteNbCode(Action): output_text = remove_log_and_warning_lines(output_text) # The useful information of the exception is at the end, # the useful information of normal output is at the begining. - output_text = output_text[:keep_len] if is_success else output_text[-keep_len:] + if '' not in output_text: + output_text = output_text[:keep_len] if is_success else output_text[-keep_len:] parsed_output.append(output_text) return is_success, ",".join(parsed_output) From 3d32a5d621c93667a1945df7086f223f972d035e Mon Sep 17 00:00:00 2001 From: lidanyang Date: Tue, 9 Jul 2024 16:57:36 +0800 Subject: [PATCH 058/119] add browser memory to write code --- metagpt/actions/di/write_analysis_code.py | 10 ++++++++-- metagpt/roles/di/role_zero.py | 7 ++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/metagpt/actions/di/write_analysis_code.py b/metagpt/actions/di/write_analysis_code.py index 548555196..19afab711 100644 --- a/metagpt/actions/di/write_analysis_code.py +++ b/metagpt/actions/di/write_analysis_code.py @@ -16,6 +16,7 @@ from metagpt.prompts.di.write_analysis_code import ( REFLECTION_PROMPT, REFLECTION_SYSTEM_MSG, STRUCTUAL_PROMPT, + BROWSER_INFO, ) from metagpt.schema import Message, Plan from metagpt.utils.common import CodeParser, remove_comments @@ -41,6 +42,7 @@ class WriteAnalysisCode(Action): tool_info: str = "", working_memory: list[Message] = None, use_reflection: bool = False, + browser_memory: list[dict] = None, **kwargs, ) -> str: structual_prompt = STRUCTUAL_PROMPT.format( @@ -48,16 +50,20 @@ class WriteAnalysisCode(Action): plan_status=plan_status, tool_info=tool_info, ) + message = [Message(content=structual_prompt, role="user")] + if browser_memory: + browser_prompt = BROWSER_INFO.format(browser_memory=browser_memory) + message = [Message(content=browser_prompt, role="user")] + message working_memory = working_memory or [] - context = self.llm.format_msg([Message(content=structual_prompt, role="user")] + working_memory) + context = self.llm.format_msg(message + working_memory) # LLM call if use_reflection: code = await self._debug_with_reflection(context=context, working_memory=working_memory) else: rsp = await self.llm.aask(context, system_msgs=[INTERPRETER_SYSTEM_MSG], **kwargs) - code = CodeParser.parse_code(text=rsp) + code = CodeParser.parse_code(text=rsp, lang="python") return code diff --git a/metagpt/roles/di/role_zero.py b/metagpt/roles/di/role_zero.py index b179f5a00..01f792ed0 100644 --- a/metagpt/roles/di/role_zero.py +++ b/metagpt/roles/di/role_zero.py @@ -49,6 +49,7 @@ class RoleZero(Role): # Equipped with three basic tools by default for optional use editor: Editor = Editor() browser: Browser = Browser() + browser_memory: list[dict] = [] # store the memory of browser # terminal: Terminal = Terminal() # FIXME: TypeError: cannot pickle '_thread.lock' object # Experience @@ -151,7 +152,11 @@ class RoleZero(Role): pattern = re.compile(r"Command Browser\.(\w+) executed") for index, msg in zip(range(len(memory), 0, -1), memory[::-1]): if pattern.match(msg.content): - memory.insert(index, UserMessage(cause_by="browser", content=await self.browser.view())) + content = await self.browser.view() + memory.insert(index, UserMessage(cause_by="browser", content=content)) + browser_url = re.search('URL: (.*?)\\n', content).group(1) + browser_action = {'command': pattern.match(msg.content).group(1), 'current url': browser_url} + self.browser_memory.append(browser_action) break context = self.llm.format_msg(memory + [UserMessage(content=prompt)]) # print(*context, sep="\n" + "*" * 5 + "\n") From 79744803fd9951269c50284e8c27f664651e4c1e Mon Sep 17 00:00:00 2001 From: lidanyang Date: Tue, 9 Jul 2024 16:59:19 +0800 Subject: [PATCH 059/119] refine prompt --- metagpt/prompts/di/data_analyst.py | 7 +++++++ metagpt/prompts/di/role_zero.py | 4 ++-- metagpt/prompts/di/write_analysis_code.py | 6 ++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/metagpt/prompts/di/data_analyst.py b/metagpt/prompts/di/data_analyst.py index 8450b2fe1..27b247fb3 100644 --- a/metagpt/prompts/di/data_analyst.py +++ b/metagpt/prompts/di/data_analyst.py @@ -41,3 +41,10 @@ Some text indicating your thoughts, such as how you should update the plan statu ] ``` """ + +BROWSER_INSTRUCTION = """ +4. Carefully choose to use or not use the browser tool to assist you in web tasks. + - When no click action is required, no need to use the browser tool to navigate to the webpage before scraping. + - If you need detail HTML content, write code to get it but not to use the browser tool. + - Make sure the command_name are certainly in Available Commands when you use the browser tool. +""" diff --git a/metagpt/prompts/di/role_zero.py b/metagpt/prompts/di/role_zero.py index 2b4fe105a..ea25aab82 100644 --- a/metagpt/prompts/di/role_zero.py +++ b/metagpt/prompts/di/role_zero.py @@ -5,7 +5,7 @@ When presented a current task, tackle the task using the available commands. Pay close attention to new user message, review the conversation history, use RoleZero.reply_to_human to respond to new user requirement. Note: 1. If you keeping encountering errors, unexpected situation, or you are not sure of proceeding, use RoleZero.ask_human to ask for help. -2. Carefully review your progress at the current task, if your actions so far has not fulfilled the task instruction, you should continue with current task. Otherwise, finish current task by Plan.finish_current_task. +2. Carefully review your progress at the current task, if your actions so far has not fulfilled the task instruction, you should continue with current task. Otherwise, finish current task by Plan.finish_current_task explicitly. 3. Each time you finish a task, use RoleZero.reply_to_human to report your progress. """ @@ -41,7 +41,7 @@ Pay close attention to the Example provided, you can reuse the example for your You may use any of the available commands to create a plan or update the plan. You may output mutiple commands, they will be executed sequentially. If you finish current task, you will automatically take the next task in the existing plan, use Plan.finish_task, DON'T append a new task. -# Your commands in a json array, in the following output format with command_name and args. If there is nothing to do, use the pass or end command: +# Your commands in a json array, in the following output format with correct command_name and args. If there is nothing to do, use the pass or end command: Some text indicating your thoughts, such as how you should update the plan status, respond to inquiry, or seek for help. Then a json array of commands. You must output ONE and ONLY ONE json array. DON'T output multiple json arrays with thoughts between them. ```json [ diff --git a/metagpt/prompts/di/write_analysis_code.py b/metagpt/prompts/di/write_analysis_code.py index 1d743a719..bf67d8ba0 100644 --- a/metagpt/prompts/di/write_analysis_code.py +++ b/metagpt/prompts/di/write_analysis_code.py @@ -119,3 +119,9 @@ DATA_INFO = """ Latest data info after previous tasks: {info} """ + +BROWSER_INFO = """ +Here are ordered web actions in the browser environment, note that you can not use the browser tool in the current environment. +{browser_memory} +The latest url is the one you should use to view the page. If view page has been done, directly use the variable and html content in executing result. +""" From 0b7d7bdf559976ef6f060204bd3ddcff78bc2533 Mon Sep 17 00:00:00 2001 From: lidanyang Date: Tue, 9 Jul 2024 17:00:08 +0800 Subject: [PATCH 060/119] add prompt for scraping task --- metagpt/prompts/task_type.py | 6 ++++++ metagpt/strategy/task_type.py | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/metagpt/prompts/task_type.py b/metagpt/prompts/task_type.py index 5b1ffc744..312421c21 100644 --- a/metagpt/prompts/task_type.py +++ b/metagpt/prompts/task_type.py @@ -53,3 +53,9 @@ The current task is about converting image into webpage code. please note the fo - Single-Step Code Generation: Execute the entire code generation process in a single step, encompassing HTML, CSS, and JavaScript. Avoid fragmenting the code generation into multiple separate steps to maintain consistency and simplify the development workflow. - Save webpages: Be sure to use the save method provided. """ + +# Prompt for taking on "web_scraping" tasks +WEB_SCRAPING_PROMPT = """ +- Remember to view and print the necessary HTML content in a separate task to understand the structure first before scraping data. +- Since the data required by user may not correspond directly to the actual HTML element names, you should thoroughly analyze the HTML structure and meanings of all elements in the executing result first. Ensure the `class_` in your code should derived from the actual HTML structure directly, not based on your knowledge. To ensure it, analyse the most suitable location of the 'class_' in the actual HTML content before code. +""" diff --git a/metagpt/strategy/task_type.py b/metagpt/strategy/task_type.py index 2bc53b964..b44cc3ac0 100644 --- a/metagpt/strategy/task_type.py +++ b/metagpt/strategy/task_type.py @@ -8,7 +8,7 @@ from metagpt.prompts.task_type import ( FEATURE_ENGINEERING_PROMPT, IMAGE2WEBPAGE_PROMPT, MODEL_EVALUATE_PROMPT, - MODEL_TRAIN_PROMPT, + MODEL_TRAIN_PROMPT, WEB_SCRAPING_PROMPT, ) @@ -62,6 +62,7 @@ class TaskType(Enum): WEBSCRAPING = TaskTypeDef( name="web scraping", desc="For scraping data from web pages.", + guidance=WEB_SCRAPING_PROMPT, ) EMAIL_LOGIN = TaskTypeDef( name="email login", From 32fc2762458180cc73999b2b1e64414be8b72613 Mon Sep 17 00:00:00 2001 From: lidanyang Date: Tue, 9 Jul 2024 17:01:07 +0800 Subject: [PATCH 061/119] add experience examples for scraping task --- metagpt/roles/di/data_analyst.py | 8 +- metagpt/strategy/experience_retriever.py | 126 +++++++++++++++++++++++ 2 files changed, 133 insertions(+), 1 deletion(-) diff --git a/metagpt/roles/di/data_analyst.py b/metagpt/roles/di/data_analyst.py index f3586a6f5..647196433 100644 --- a/metagpt/roles/di/data_analyst.py +++ b/metagpt/roles/di/data_analyst.py @@ -5,8 +5,11 @@ from pydantic import Field, model_validator from metagpt.actions.di.execute_nb_code import ExecuteNbCode from metagpt.actions.di.write_analysis_code import WriteAnalysisCode from metagpt.logs import logger +from metagpt.prompts.di.data_analyst import BROWSER_INSTRUCTION +from metagpt.prompts.di.role_zero import ROLE_INSTRUCTION from metagpt.roles.di.role_zero import RoleZero from metagpt.schema import TaskResult, Message +from metagpt.strategy.experience_retriever import ExpRetriever, WebExpRetriever from metagpt.tools.tool_recommend import BM25ToolRecommender, ToolRecommender from metagpt.tools.tool_registry import register_tool @@ -16,10 +19,12 @@ class DataAnalyst(RoleZero): name: str = "David" profile: str = "DataAnalyst" goal: str = "Take on any data-related tasks, such as data analysis, machine learning, deep learning, web browsing, web scraping, web searching, web deployment, terminal operation, git and github operation, etc." + instruction: str = ROLE_INSTRUCTION + BROWSER_INSTRUCTION - tools: list[str] = ["Plan", "DataAnalyst", "RoleZero"] + tools: list[str] = ["Plan", "DataAnalyst", "RoleZero", "Browser"] custom_tools: list[str] = ["machine learning", "web scraping", "Terminal"] custom_tool_recommender: ToolRecommender = None + experience_retriever: ExpRetriever = WebExpRetriever() use_reflection: bool = True write_code: WriteAnalysisCode = Field(default_factory=WriteAnalysisCode, exclude=True) @@ -63,6 +68,7 @@ class DataAnalyst(RoleZero): tool_info=tool_info, working_memory=self.rc.working_memory.get() if use_reflection else None, use_reflection=use_reflection, + browser_memory=self.browser_memory ) self.rc.working_memory.add(Message(content=code, role="assistant", cause_by=WriteAnalysisCode)) diff --git a/metagpt/strategy/experience_retriever.py b/metagpt/strategy/experience_retriever.py index 5e85b056a..6356a0faf 100644 --- a/metagpt/strategy/experience_retriever.py +++ b/metagpt/strategy/experience_retriever.py @@ -448,3 +448,129 @@ Explanation: to review the code, call ReviewAndRewriteCode.run. ] ``` """ + + +WEB_SCRAPING_EXAMPLE = """ +## action 1 +User Requirement: Scrap and list the restaurant names of first page by searching for the keyword `beef` on the website https://www.yelp.com/. +Explanation: The requirement is to scrape data from a website and extract information about restaurants. The process involves searching for restaurants with a specific keyword, retrieving and presenting the data in a structured format. + +```json +[ + { + "command_name": "Plan.append_task", + "args": { + "task_id": "1", + "dependent_task_ids": [], + "instruction": "Navigate to the yelp website.", + "assignee": "Browser" + } + }, + { + "command_name": "Plan.append_task", + "args": { + "task_id": "2", + "dependent_task_ids": ["1"], + "instruction": "Search for restaurants with the keyword 'beef'.", + "assignee": "Browser" + } + }, + { + "command_name": "Plan.append_task", + "args": { + "task_id": "3", + "dependent_task_ids": ["2"], + "instruction": "View the html content of the search result page before scrap data to understand the structure.", + "assignee": "DataAnalyst" + } + }, + { + "command_name": "Plan.append_task", + "args": { + "task_id": "4", + "dependent_task_ids": ["3"], + "instruction": "Parse the html content to scrape the restaurant names and print it.", + "assignee": "DataAnalyst" + } + } +] +``` + +## action 2 +Explanation: To search for restaurants, I will now go to the website https://www.yelp.com/ first. +Here is the command to navigate to the website: + +```json +[ + { + "command_name": "Browser.goto", + "args": { + "url": "https://www.yelp.com/" + } + } +] +``` + +## action 3 +Explanation: Since the Browser has successfully navigated to the website, and I find that the element id of the search box is 53. I will finish the current task and then use the Browser tool to type the keyword `beef` in the search box and press enter. +Here is the command to finish the current task and type the keyword: + +```json +[ + { + "command_name": "Plan.finish_current_task", + "args": {} + }, + { + "command_name": "Browser.type", + "args": { + "element_id": 53, + "content": "beef", + "press_enter_after": true + } + } +] +``` + +## action 4 +Explanation: Since the Browser has successfully search the keyword `beef`, I will finish the current task and then write code to view the html content of the page. +Here is the command to finish the current task and view the html content: + +```json +[ + { + "command_name": "Plan.finish_current_task", + "args": {} + }, + { + "command_name": "DataAnalyst.write_and_exec_code", + "args": {} + } +] +``` + +## action 5 +Explanation: Since the DataAnalyst has successfully viewed the html content of the page, I will finish the current task and then write code to parse the html content and extract the restaurant names. +Here is the command to finish the current task and parse the html content: + +```json +[ + { + "command_name": "Plan.finish_current_task", + "args": {} + }, + { + "command_name": "DataAnalyst.write_and_exec_code", + "args": {} + } +] + +... +""" + + +class WebExpRetriever(ExpRetriever): + """A simple experience retriever that returns manually crafted examples.""" + + def retrieve(self, context: str = "") -> str: + return WEB_SCRAPING_EXAMPLE From 4c4d9547ff553f42fce1ba01a27c418804a3c044 Mon Sep 17 00:00:00 2001 From: lidanyang Date: Tue, 9 Jul 2024 17:01:54 +0800 Subject: [PATCH 062/119] support multi write_code steps for one task --- metagpt/schema.py | 4 ++-- metagpt/strategy/planner.py | 10 +++++++++- metagpt/tools/tool_recommend.py | 4 ++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/metagpt/schema.py b/metagpt/schema.py index 11610b6c3..94e64d7fa 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -464,8 +464,8 @@ class Task(BaseModel): self.is_finished = False def update_task_result(self, task_result: TaskResult): - self.code = task_result.code + "\n" + task_result.code - self.result = task_result.result + self.code = self.code + "\n" + task_result.code + self.result = self.result + "\n" + task_result.result self.is_success = task_result.is_success diff --git a/metagpt/strategy/planner.py b/metagpt/strategy/planner.py index 427e41562..95ad1f5cc 100644 --- a/metagpt/strategy/planner.py +++ b/metagpt/strategy/planner.py @@ -40,8 +40,14 @@ PLAN_STATUS = """ ## Current Task {current_task} +## Finished Section of Current Task +### code +{current_task_code} +### execution result +{current_task_result} + ## Task Guidance -Write complete code for 'Current Task'. And avoid duplicating code from 'Finished Tasks', such as repeated import of packages, reading data, etc. +Write code for the incomplete sections of 'Current Task'. And avoid duplicating code from 'Finished Tasks', such as repeated import of packages, reading data, etc. Specifically, {guidance} """ @@ -173,6 +179,8 @@ class Planner(BaseModel): code_written=code_written, task_results=task_results, current_task=self.current_task.instruction, + current_task_code=self.current_task.code if self.current_task.code else "", + current_task_result=self.current_task.result if self.current_task.result else "", guidance=guidance, ) diff --git a/metagpt/tools/tool_recommend.py b/metagpt/tools/tool_recommend.py index ab847d10e..0c596707a 100644 --- a/metagpt/tools/tool_recommend.py +++ b/metagpt/tools/tool_recommend.py @@ -106,11 +106,11 @@ class ToolRecommender(BaseModel): Wrap recommended tools with their info in a string, which can be used directly in a prompt. """ recommended_tools = await self.recommend_tools(**kwargs) + if fix: + recommended_tools.extend([self.tools[tool_name] for tool_name in fix if tool_name in self.tools]) if not recommended_tools: return "" tool_schemas = {tool.name: tool.schemas for tool in recommended_tools} - if fix: - tool_schemas.update({tool.name: tool.schemas for tool in self.tools.values() if tool.name in fix}) return TOOL_INFO_PROMPT.format(tool_schemas=tool_schemas) async def recall_tools(self, context: str = "", plan: Plan = None, topk: int = 20) -> list[Tool]: From ae861d99cdbb0fdcb1b666ce57883ae6576910de Mon Sep 17 00:00:00 2001 From: lidanyang Date: Tue, 9 Jul 2024 17:02:44 +0800 Subject: [PATCH 063/119] refine web_scraping tool --- metagpt/tools/libs/web_scraping.py | 8 +++++--- metagpt/utils/parse_html.py | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/metagpt/tools/libs/web_scraping.py b/metagpt/tools/libs/web_scraping.py index 489c3a472..9e7a8041c 100644 --- a/metagpt/tools/libs/web_scraping.py +++ b/metagpt/tools/libs/web_scraping.py @@ -8,13 +8,15 @@ from metagpt.utils.parse_html import simplify_html @register_tool(tags=["web scraping"]) -async def view_page_element_to_scrape(url: str, requirement: str, keep_links: bool = False) -> None: - """view the HTML content of current page to understand the structure. When executed, the content will be printed out +async def view_page_element_to_scrape(url: str, requirement: str, keep_links: bool = False) -> str: + """view the HTML content of current page to understand the structure. Args: url (str): The URL of the web page to scrape. requirement (str): Providing a clear and detailed requirement helps in focusing the inspection on the desired elements. keep_links (bool): Whether to keep the hyperlinks in the HTML content. Set to True if links are required + Returns: + str: The HTML content of the page. """ async with Browser() as browser: await browser.goto(url) @@ -36,7 +38,7 @@ async def view_page_element_to_scrape(url: str, requirement: str, keep_links: bo html = "\n".join(i.text for i in nodes) mem_fs.rm_file(filename) - print(html) + return html # async def get_elements_outerhtml(self, element_ids: list[int]): diff --git a/metagpt/utils/parse_html.py b/metagpt/utils/parse_html.py index 1ed3a620c..031393501 100644 --- a/metagpt/utils/parse_html.py +++ b/metagpt/utils/parse_html.py @@ -41,13 +41,13 @@ class WebPage(BaseModel): def get_slim_soup(self, keep_links: bool = False): soup = _get_soup(self.html) - keep_attrs = ["class"] + keep_attrs = ["class", "id"] if keep_links: - keep_attrs.append("href") + keep_attrs.extend(["href", "title"]) for i in soup.find_all(True): for name in list(i.attrs): - if i[name] and name not in keep_attrs: + if i[name] and name not in keep_attrs and not name.startswith("data-"): del i[name] for i in soup.find_all(["svg", "img", "video", "audio"]): From 46cd961ebaeafb3d28fc11f0921e3ffe7c26ac66 Mon Sep 17 00:00:00 2001 From: lidanyang Date: Tue, 9 Jul 2024 17:03:31 +0800 Subject: [PATCH 064/119] add test for DataAnalyst --- tests/metagpt/roles/di/run_data_analyst.py | 53 ++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 tests/metagpt/roles/di/run_data_analyst.py diff --git a/tests/metagpt/roles/di/run_data_analyst.py b/tests/metagpt/roles/di/run_data_analyst.py new file mode 100644 index 000000000..9c1f72394 --- /dev/null +++ b/tests/metagpt/roles/di/run_data_analyst.py @@ -0,0 +1,53 @@ +from metagpt.roles.di.data_analyst import DataAnalyst + +HOUSE_PRICE_TRAIN_PATH = '/data/house-prices-advanced-regression-techniques/split_train.csv' +HOUSE_PRICE_EVAL_PATH = '/data/house-prices-advanced-regression-techniques/split_eval.csv' +HOUSE_PRICE_REQ = f""" +This is a house price dataset, your goal is to predict the sale price of a property based on its features. The target column is SalePrice. Perform data analysis, data preprocessing, feature engineering, and modeling to predict the target. Report RMSE between the logarithm of the predicted value and the logarithm of the observed sales price on the eval data. Train data path: '{HOUSE_PRICE_TRAIN_PATH}', eval data path: '{HOUSE_PRICE_EVAL_PATH}'. +""" + +CALIFORNIA_HOUSING_REQ = """ +Analyze the 'Canifornia-housing-dataset' using https://scikit-learn.org/stable/modules/generated/sklearn.datasets.fetch_california_housing.html#sklearn.datasets.fetch_california_housing to predict the median house value. you need to perfrom data preprocessing, feature engineering and finally modeling to predict the target. Use machine learning techniques such as linear regression (including ridge regression and lasso regression), random forest, CatBoost, LightGBM, XGBoost or other appropriate method. You also need to report the MSE on the test dataset +""" + +PAPER_LIST_REQ = """" +Get data from `paperlist` table in https://papercopilot.com/statistics/iclr-statistics/iclr-2024-statistics/, +and save it to a csv file. paper title must include `multiagent` or `large language model`. +**Notice: view the page element before writing scraping code** +""" + +ECOMMERCE_REQ = """ +Get products data from website https://scrapeme.live/shop/ and save it as a csv file. +The first page product name, price, product URL, and image URL must be saved in the csv. +**Notice: view the page element before writing scraping code** +""" + +NEWS_36KR_REQ = """从36kr创投平台https://pitchhub.36kr.com/financing-flash 所有初创企业融资的信息, **注意: 这是一个中文网站**; +下面是一个大致流程, 你会根据每一步的运行结果对当前计划中的任务做出适当调整: +1. 爬取并本地保存html结构; +2. 直接打印第7个*`快讯`*关键词后2000个字符的html内容, 作为*快讯的html内容示例*; +3. 反思*快讯的html内容示例*中的规律, 设计正则匹配表达式来获取*`快讯`*的标题、链接、时间; +4. 筛选最近3天的初创企业融资*`快讯`*, 以list[dict]形式打印前5个。 +5. 将全部结果存在本地csv中 +**Notice: view the page element before writing scraping code** +""" + +WIKIPEDIA_SEARCH_REQ = """ +Search for `LLM` on Wikipedia and print all the meaningful significances of the entry. +""" + +STACKOVERFLOW_CLICK_REQ = """ +Click the Questions tag in https://stackoverflow.com/ and scrap question name, votes, answers and views num to csv in the first result page. +""" + + +async def main(): + di = DataAnalyst() + await di.browser.start() + await di.run(STACKOVERFLOW_CLICK_REQ) + + +if __name__ == "__main__": + import asyncio + + asyncio.run(main()) From d37c2b16819f511608592d95e2ea12e6ac841c28 Mon Sep 17 00:00:00 2001 From: zhanglei Date: Tue, 9 Jul 2024 17:20:13 +0800 Subject: [PATCH 065/119] update: swe agent prompt --- metagpt/prompts/di/swe_agent.py | 51 +++++++++++++++++++++------------ metagpt/tools/libs/terminal.py | 3 -- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/metagpt/prompts/di/swe_agent.py b/metagpt/prompts/di/swe_agent.py index ed1f8a011..5e53f29db 100644 --- a/metagpt/prompts/di/swe_agent.py +++ b/metagpt/prompts/di/swe_agent.py @@ -33,7 +33,7 @@ You should only include a *SINGLE* command in the command section and then wait If you'd like to issue two commands at once, PLEASE DO NOT DO THAT! Please instead first submit just the first command, and then after receiving a response you'll be able to issue the second command. Remember, YOU CAN ONLY ENTER ONE COMMAND AT A TIME. You should always wait for feedback after every command. -You can use any bash commands you want (e.g., find, grep, cat, ls, cd) or any custom special tools (including `edit`) by calling Bash.run. Edit all the files you need. +You can use any bash commands you want (e.g., find, grep, cat, ls, cd) or any custom special tools (including `edit`) by calling Bash.run. Edit all the files you need, but make sure to PRIORITIZE the `Available Commands` provided to accomplish your tasks,such as `git_push`. You should carefully observe the behavior and results of the previous action, and avoid triggering repeated errors. However, the Bash.run does NOT support interactive session commands (e.g. python, vim), so please do not invoke them. @@ -51,53 +51,68 @@ MINIMAL_EXAMPLE = """ User Requirement and Issue: Fix the bug in the repo. Because the environment is not available, you DO NOT need to run and modify any existing test case files or add new test case files to ensure that the bug is fixed. ### Read and understand issue(Require): -{{ +[{{ "command_name": "Browser.goto", "args": {{ "url": "https://github.com/geekan/MetaGPT/issues/1275" }} -}} +}}] -> ### Locate issue(Require): Locate the issue in the code by searching for the relevant file, function, or class and open the file to view the code. -{{ +[{{ "command_name": "Bash.run", "args": {{ "cmd": "cd /workspace/django__django_3.0" }} -}} +}}] -> Bash.run(cmd='search_dir_and_preview ASCIIUsernameValidator') -{{ +[{{ "command_name": "Bash.run", "args": {{ "cmd": "open /workspace/django__django_3.0/django/contrib/auth/validators.py" }} -}} +}}] -> ### Fix the Bug(Require): Fix the bug in the code by editing the relevant function, class or code snippet. -{{ +[{{ "command_name": "Bash.run", "args": {{ "cmd": "edit 10:20 < -### Submit the Changes(Require): Submit the changes to the repository. -{{ - "command_name": "Bash.run", +### Push the Changes: Pushes changes from a local Git repository to its remote counterpart. +[{{ + "command_name": "git_push", "args": {{ - "cmd": "submit" + "local_path": "/workspace/django__django_3.0/django/contrib/auth", + "app_name": "github", + "comments": "Fix: produced TypeError: openai.types.completion_usage.CompletionUsage() argument after ** must be a mapping, not NoneType", + "new_branch": "test-fix" }} -}} -Bash.run(cmd='submit') +}}] + +### Create pull request +[{{ + "command_name": "git_create_pull", + "args": {{ + "base": "master", + "head": "test-fix", + "base_repo_name": "Justin-ZL/langchain", + "app_name": "github", + "title": "Fix Issue #1275: produced TypeError: openai.types.completion_usage.CompletionUsage() argument after ** must be a mapping, not NoneTyp", + "body": "This pull request addresses issue #1275 by producing TypeError: openai.types.completion_usage.CompletionUsage() argument after ** must be a mapping, not NoneTyp." + }} +}}] -> -{{ +[{{ "command_name": "end", -}} +}}] """ @@ -162,7 +177,7 @@ IMPORTANT_TIPS = """ - If a search command fails, modify the search criteria and check for typos or incorrect paths, then try again. - Based on feedback of observation or bash command in trajectory to guide adjustments in your search strategy. -13. If the task results in succeed, fail, or NO PROGRESS, output `submit`. +13. If the task results in succeed, use command `git_push` to push the results in local,then use command `git_create_pull` to create PR. 14. If provided an issue link, you MUST go to the issue page using Browser tool to understand the issue before starting your fix. diff --git a/metagpt/tools/libs/terminal.py b/metagpt/tools/libs/terminal.py index bcf039a5e..05593ed57 100644 --- a/metagpt/tools/libs/terminal.py +++ b/metagpt/tools/libs/terminal.py @@ -184,9 +184,6 @@ class Bash(Terminal): Arguments: filename (str): The name of the file to create. - - submit - Submits your current code. it can only be executed once, the last action before the `end`. - - search_dir_and_preview [] Searches for search_term in all files in dir and gives their code preview with line numbers. If dir is not provided, searches in the current directory. From e530b2e473d21286c3c8f542a38b2cab2cc298cb Mon Sep 17 00:00:00 2001 From: zhanglei Date: Tue, 9 Jul 2024 17:45:27 +0800 Subject: [PATCH 066/119] update: swe agent COT --- metagpt/prompts/di/swe_agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/prompts/di/swe_agent.py b/metagpt/prompts/di/swe_agent.py index 5e53f29db..e2cddf389 100644 --- a/metagpt/prompts/di/swe_agent.py +++ b/metagpt/prompts/di/swe_agent.py @@ -5,7 +5,7 @@ https://github.com/princeton-nlp/SWE-agent/tree/main/config/configs """ SWE_AGENT_SYSTEM_TEMPLATE = """ -SETTING: You are an autonomous programmer, and you're working directly in the environment line with a special interface. +SETTING: You are an autonomous programmer, and you're working directly in the environment line with a special interface.Let's work step by step. The special interface consists of a file editor that shows you {WINDOW} lines of a file at a time. From b5934a412bbaaee25337b60ac6c4610d5077c324 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Wed, 10 Jul 2024 10:24:04 +0800 Subject: [PATCH 067/119] add serializers to support serialization and deserialization. --- config/config2.example.yaml | 1 - metagpt/actions/action_node.py | 25 +---------- metagpt/configs/exp_pool_config.py | 3 -- .../exp_pool/context_builders/action_node.py | 33 +++++++++++++++ metagpt/exp_pool/context_builders/base.py | 35 +++++----------- .../exp_pool/context_builders/role_zero.py | 42 ++++++++++--------- metagpt/exp_pool/context_builders/simple.py | 12 +++--- metagpt/exp_pool/decorator.py | 30 +++++-------- metagpt/exp_pool/manager.py | 4 +- metagpt/exp_pool/serializers/__init__.py | 9 ++++ metagpt/exp_pool/serializers/action_node.py | 36 ++++++++++++++++ metagpt/exp_pool/serializers/base.py | 22 ++++++++++ metagpt/exp_pool/serializers/role_zero.py | 40 ++++++++++++++++++ metagpt/exp_pool/serializers/simple.py | 22 ++++++++++ metagpt/roles/di/role_zero.py | 5 +-- .../test_base_context_builder.py | 13 ------ .../test_rolezero_context_builder.py | 15 ++++++- .../test_simple_context_builder.py | 3 +- tests/metagpt/exp_pool/test_manager.py | 28 +------------ 19 files changed, 234 insertions(+), 144 deletions(-) create mode 100644 metagpt/exp_pool/context_builders/action_node.py create mode 100644 metagpt/exp_pool/serializers/__init__.py create mode 100644 metagpt/exp_pool/serializers/action_node.py create mode 100644 metagpt/exp_pool/serializers/base.py create mode 100644 metagpt/exp_pool/serializers/role_zero.py create mode 100644 metagpt/exp_pool/serializers/simple.py diff --git a/config/config2.example.yaml b/config/config2.example.yaml index a3bd5c367..330b73680 100644 --- a/config/config2.example.yaml +++ b/config/config2.example.yaml @@ -78,7 +78,6 @@ exp_pool: enable_read: false enable_write: false persist_path: .chroma_exp_data # The directory. - init_exp: false # If set to true, basic experiences associated with the roles will be added to the experience pool. azure_tts_subscription_key: "YOUR_SUBSCRIPTION_KEY" azure_tts_region: "eastus" diff --git a/metagpt/actions/action_node.py b/metagpt/actions/action_node.py index e1e0bddbb..c1de16656 100644 --- a/metagpt/actions/action_node.py +++ b/metagpt/actions/action_node.py @@ -19,6 +19,7 @@ from tenacity import retry, stop_after_attempt, wait_random_exponential from metagpt.actions.action_outcls_registry import register_action_outcls from metagpt.const import MARKDOWN_TITLE_PREFIX, USE_CONFIG_TIMEOUT from metagpt.exp_pool import exp_cache +from metagpt.exp_pool.serializers import ActionNodeSerializer from metagpt.llm import BaseLLM from metagpt.logs import logger from metagpt.provider.postprocess.llm_output_postprocess import llm_output_postprocess @@ -466,29 +467,7 @@ class ActionNode: return self - @classmethod - def deserialize_to_action_node(cls, serialized_data) -> "ActionNode": - """Customized deserialization, it will be triggered when a perfect experience is found. - - ActionNode cannot be serialized, it throws an error 'cannot pickle 'SSLContext' object'. - """ - - class InstructContent: - def __init__(self, json_data): - self.json_data = json_data - - def model_dump_json(self): - return self.json_data - - action_node = cls(key="", expected_type=Type[str], instruction="", example="") - action_node.instruct_content = InstructContent(serialized_data) - - return action_node - - @exp_cache( - resp_serialize=lambda action_node: action_node.instruct_content.model_dump_json(), - resp_deserialize=lambda resp: ActionNode.deserialize_to_action_node(resp), - ) + @exp_cache(serializer=ActionNodeSerializer()) async def fill( self, *, diff --git a/metagpt/configs/exp_pool_config.py b/metagpt/configs/exp_pool_config.py index 0c92312da..786558ed9 100644 --- a/metagpt/configs/exp_pool_config.py +++ b/metagpt/configs/exp_pool_config.py @@ -7,6 +7,3 @@ class ExperiencePoolConfig(YamlModel): enable_read: bool = Field(default=False, description="Enable to read from experience pool.") enable_write: bool = Field(default=False, description="Enable to write to experience pool.") persist_path: str = Field(default=".chroma_exp_data", description="The persist path for experience pool.") - init_exp: bool = Field( - default=False, description="Put some basic experiences associated with the roles into the experience pool." - ) diff --git a/metagpt/exp_pool/context_builders/action_node.py b/metagpt/exp_pool/context_builders/action_node.py new file mode 100644 index 000000000..ade157822 --- /dev/null +++ b/metagpt/exp_pool/context_builders/action_node.py @@ -0,0 +1,33 @@ +"""Action Node context builder.""" + + +from metagpt.exp_pool.context_builders.base import BaseContextBuilder + +ACTION_NODE_CONTEXT_TEMPLATE = """ +{req} + +### Experiences +----- +{exps} +----- + +## Instruction +Consider **Experiences** to generate a better answer. +""" + + +class ActionNodeContextBuilder(BaseContextBuilder): + async def build(self, **kwargs) -> str: + """Builds the action node context string. + + Args: + **kwargs: Arbitrary keyword arguments, expecting 'req' as a key. + + Returns: + str: The formatted context string using the request and formatted experiences. + If no experiences are available, returns the request as is. + """ + req = kwargs.get("req", "") + exps = self.format_exps() + + return ACTION_NODE_CONTEXT_TEMPLATE.format(req=req, exps=exps) if exps else req diff --git a/metagpt/exp_pool/context_builders/base.py b/metagpt/exp_pool/context_builders/base.py index a261e452e..d1133c2da 100644 --- a/metagpt/exp_pool/context_builders/base.py +++ b/metagpt/exp_pool/context_builders/base.py @@ -1,6 +1,5 @@ """Base context builder.""" -import re from abc import ABC, abstractmethod from typing import Any @@ -17,11 +16,19 @@ class BaseContextBuilder(BaseModel, ABC): exps: list[Experience] = [] @abstractmethod - async def build(self, *args, **kwargs) -> Any: + async def build(self, **kwargs) -> Any: """Build context from parameters.""" def format_exps(self) -> str: - """Format experiences into a numbered list of strings.""" + """Format experiences into a numbered list of strings. + + Example: + 1. Given the request: req1, We can get the response: resp1, Which scored: 8. + 2. Given the request: req2, We can get the response: resp2, Which scored: 9. + + Returns: + str: The formatted experiences as a string. + """ result = [] for i, exp in enumerate(self.exps, start=1): @@ -29,25 +36,3 @@ class BaseContextBuilder(BaseModel, ABC): result.append(f"{i}. " + EXP_TEMPLATE.format(req=exp.req, resp=exp.resp, score=score_val)) return "\n".join(result) - - @staticmethod - def replace_content_between_markers(text: str, start_marker: str, end_marker: str, new_content: str) -> str: - """Replace the content between `start_marker` and `end_marker` in the text with `new_content`. - - Args: - text (str): The original text. - new_content (str): The new content to replace the old content. - start_marker (str): The marker indicating the start of the content to be replaced, such as '# Example'. - end_marker (str): The marker indicating the end of the content to be replaced, such as '# Instruction'. - - Returns: - str: The text with the content replaced. - """ - - pattern = re.compile(f"({start_marker}\n)(.*?)(\n{end_marker})", re.DOTALL) - - def replacement(match): - return f"{match.group(1)}{new_content}\n{match.group(3)}" - - replaced_text = pattern.sub(replacement, text) - return replaced_text diff --git a/metagpt/exp_pool/context_builders/role_zero.py b/metagpt/exp_pool/context_builders/role_zero.py index e9ab83d90..b492ca5ca 100644 --- a/metagpt/exp_pool/context_builders/role_zero.py +++ b/metagpt/exp_pool/context_builders/role_zero.py @@ -1,15 +1,19 @@ """RoleZero context builder.""" -import copy -import json + +import re from metagpt.exp_pool.context_builders.base import BaseContextBuilder class RoleZeroContextBuilder(BaseContextBuilder): - async def build(self, *args, **kwargs) -> list[dict]: + async def build(self, **kwargs) -> list[dict]: """Builds the context by updating the req with formatted experiences. - If there are no experiences, retains the original examples in req, otherwise replaces the examples with the formatted experiences. + Args: + **kwargs: Arbitrary keyword arguments, expecting 'req' as a key. + + Returns: + list[dict]: The updated request with formatted experiences or the original request if no experiences are available. """ req = kwargs.get("req", []) @@ -28,23 +32,23 @@ class RoleZeroContextBuilder(BaseContextBuilder): return self.replace_content_between_markers(text, "# Example", "# Instruction", new_example_content) @staticmethod - def req_serialize(req: list[dict]) -> str: - """Serialize the request for database storage, ensuring it is a string. + def replace_content_between_markers(text: str, start_marker: str, end_marker: str, new_content: str) -> str: + """Replace the content between `start_marker` and `end_marker` in the text with `new_content`. - This function deep copies the request and modifies the content of the last element - to remove unnecessary sections, making the request more concise. + Args: + text (str): The original text. + new_content (str): The new content to replace the old content. + start_marker (str): The marker indicating the start of the content to be replaced, such as '# Example'. + end_marker (str): The marker indicating the end of the content to be replaced, such as '# Instruction'. + + Returns: + str: The text with the content replaced. """ - req_copy = copy.deepcopy(req) + pattern = re.compile(f"({start_marker}\n)(.*?)(\n{end_marker})", re.DOTALL) - last_content = req_copy[-1]["content"] - last_content = RoleZeroContextBuilder.replace_content_between_markers( - last_content, "# Data Structure", "# Current Plan", "" - ) - last_content = RoleZeroContextBuilder.replace_content_between_markers( - last_content, "# Example", "# Instruction", "" - ) + def replacement(match): + return f"{match.group(1)}{new_content}\n{match.group(3)}" - req_copy[-1]["content"] = last_content - - return json.dumps(req_copy) + replaced_text = pattern.sub(replacement, text) + return replaced_text diff --git a/metagpt/exp_pool/context_builders/simple.py b/metagpt/exp_pool/context_builders/simple.py index 35e2e1c8a..565855664 100644 --- a/metagpt/exp_pool/context_builders/simple.py +++ b/metagpt/exp_pool/context_builders/simple.py @@ -4,21 +4,21 @@ from metagpt.exp_pool.context_builders.base import BaseContextBuilder SIMPLE_CONTEXT_TEMPLATE = """ -{req} +## Context ### Experiences ----- {exps} ----- +## User Requirement +{req} + ## Instruction Consider **Experiences** to generate a better answer. """ class SimpleContextBuilder(BaseContextBuilder): - async def build(self, *args, **kwargs) -> str: - req = kwargs.get("req", "") - exps = self.format_exps() - - return SIMPLE_CONTEXT_TEMPLATE.format(req=req, exps=exps) if exps else req + async def build(self, **kwargs) -> str: + return SIMPLE_CONTEXT_TEMPLATE.format(req=kwargs.get("req", ""), exps=self.format_exps()) diff --git a/metagpt/exp_pool/decorator.py b/metagpt/exp_pool/decorator.py index 62f766b9d..deb3faafc 100644 --- a/metagpt/exp_pool/decorator.py +++ b/metagpt/exp_pool/decorator.py @@ -1,6 +1,7 @@ """Experience Decorator.""" import asyncio +import copy import functools from typing import Any, Callable, Optional, TypeVar @@ -12,6 +13,7 @@ from metagpt.exp_pool.manager import ExperienceManager, exp_manager from metagpt.exp_pool.perfect_judges import BasePerfectJudge, SimplePerfectJudge from metagpt.exp_pool.schema import Experience, Metric, QueryType, Score from metagpt.exp_pool.scorers import BaseScorer, SimpleScorer +from metagpt.exp_pool.serializers import BaseSerializer, SimpleSerializer from metagpt.logs import logger from metagpt.utils.async_helper import NestAsyncio from metagpt.utils.exceptions import handle_exception @@ -26,9 +28,7 @@ def exp_cache( scorer: Optional[BaseScorer] = None, perfect_judge: Optional[BasePerfectJudge] = None, context_builder: Optional[BaseContextBuilder] = None, - req_serialize: Optional[Callable[..., str]] = None, - resp_serialize: Optional[Callable[..., str]] = None, - resp_deserialize: Optional[Callable[[str], Any]] = None, + serializer: Optional[BaseSerializer] = None, tag: Optional[str] = None, ): """Decorator to get a perfect experience, otherwise, it executes the function, and create a new experience. @@ -44,9 +44,7 @@ def exp_cache( scorer: Evaluate experience. Default to `SimpleScorer()`. perfect_judge: Determines if an experience is perfect. Defaults to `SimplePerfectJudge()`. context_builder: Build the context from exps and the function parameters. Default to `SimpleContextBuilder()`. - req_serialize: Serializes the request for storage. Defaults to `lambda req: str(req)`. - resp_serialize: Serializes the function's return value for storage. Defaults to `lambda resp: str(resp)`. - resp_deserialize: Deserializes the stored response back to the function's return value. Defaults to `lambda resp: resp`. + serializer: Serializes the request and the function's return value for storage, deserializes the stored response back to the function's return value. Defaults to `SimpleSerializer()`. tag: An optional tag for the experience. Default to `ClassName.method_name` or `function_name`. """ @@ -65,9 +63,7 @@ def exp_cache( exp_scorer=scorer, exp_perfect_judge=perfect_judge, context_builder=context_builder, - req_serialize=req_serialize, - resp_serialize=resp_serialize, - resp_deserialize=resp_deserialize, + serializer=serializer, tag=tag, ) @@ -96,9 +92,7 @@ class ExpCacheHandler(BaseModel): exp_scorer: Optional[BaseScorer] = None exp_perfect_judge: Optional[BasePerfectJudge] = None context_builder: Optional[BaseContextBuilder] = None - req_serialize: Optional[Callable[..., str]] = None - resp_serialize: Optional[Callable[..., str]] = None - resp_deserialize: Optional[Callable[[str], Any]] = None + serializer: Optional[BaseSerializer] = None tag: Optional[str] = None _exps: list[Experience] = None @@ -120,12 +114,10 @@ class ExpCacheHandler(BaseModel): self.exp_scorer = self.exp_scorer or SimpleScorer() self.exp_perfect_judge = self.exp_perfect_judge or SimplePerfectJudge() self.context_builder = self.context_builder or SimpleContextBuilder() - self.req_serialize = self.req_serialize or (lambda resp: str(resp)) - self.resp_serialize = self.resp_serialize or (lambda resp: str(resp)) - self.resp_deserialize = self.resp_deserialize or (lambda resp: resp) + self.serializer = self.serializer or SimpleSerializer() self.tag = self.tag or self._generate_tag() - self._req = self.req_serialize(self.kwargs["req"]) + self._req = self.serializer.serialize_req(copy.deepcopy(self.kwargs["req"])) return self @@ -140,7 +132,7 @@ class ExpCacheHandler(BaseModel): for exp in self._exps: if await self.exp_perfect_judge.is_perfect_exp(exp, self._req, *self.args, **self.kwargs): logger.info(f"Get one perfect experience: {exp.req[:20]}...") - return self.resp_deserialize(exp.resp) + return self.serializer.deserialize_resp(exp.resp) return None @@ -148,7 +140,7 @@ class ExpCacheHandler(BaseModel): """Execute the function, and save resp.""" self._raw_resp = await self._execute_function() - self._resp = self.resp_serialize(self._raw_resp) + self._resp = self.serializer.serialize_resp(copy.deepcopy(self._raw_resp)) @handle_exception async def process_experience(self): @@ -204,7 +196,7 @@ class ExpCacheHandler(BaseModel): async def _build_context(self) -> str: self.context_builder.exps = self._exps - return await self.context_builder.build(*self.args, **self.kwargs) + return await self.context_builder.build(**self.kwargs) async def _execute_function(self): self.kwargs["req"] = await self._build_context() diff --git a/metagpt/exp_pool/manager.py b/metagpt/exp_pool/manager.py index 23198eb02..649210a79 100644 --- a/metagpt/exp_pool/manager.py +++ b/metagpt/exp_pool/manager.py @@ -47,14 +47,12 @@ class ExperienceManager(BaseModel): self.storage = SimpleEngine.from_objs(retriever_configs=retriever_configs, ranker_configs=ranker_configs) - self.init_exp_pool() - logger.debug(f"exp_pool config: {self.config.exp_pool}") return self @handle_exception def init_exp_pool(self): - if not self.config.exp_pool.init_exp: + if not self.config.exp_pool.enable_write: return if self._has_exps(): diff --git a/metagpt/exp_pool/serializers/__init__.py b/metagpt/exp_pool/serializers/__init__.py new file mode 100644 index 000000000..8e1045588 --- /dev/null +++ b/metagpt/exp_pool/serializers/__init__.py @@ -0,0 +1,9 @@ +"""Serializers init.""" + +from metagpt.exp_pool.serializers.base import BaseSerializer +from metagpt.exp_pool.serializers.simple import SimpleSerializer +from metagpt.exp_pool.serializers.action_node import ActionNodeSerializer +from metagpt.exp_pool.serializers.role_zero import RoleZeroSerializer + + +__all__ = ["BaseSerializer", "SimpleSerializer", "ActionNodeSerializer", "RoleZeroSerializer"] diff --git a/metagpt/exp_pool/serializers/action_node.py b/metagpt/exp_pool/serializers/action_node.py new file mode 100644 index 000000000..7746d6be4 --- /dev/null +++ b/metagpt/exp_pool/serializers/action_node.py @@ -0,0 +1,36 @@ +"""ActionNode Serializer.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Type + +# Import ActionNode only for type checking to avoid circular imports +if TYPE_CHECKING: + from metagpt.actions.action_node import ActionNode + +from metagpt.exp_pool.serializers.simple import SimpleSerializer + + +class ActionNodeSerializer(SimpleSerializer): + def serialize_resp(self, resp: ActionNode) -> str: + return resp.instruct_content.model_dump_json() + + def deserialize_resp(self, resp: str) -> ActionNode: + """Customized deserialization, it will be triggered when a perfect experience is found. + + ActionNode cannot be serialized, it throws an error 'cannot pickle 'SSLContext' object'. + """ + + class InstructContent: + def __init__(self, json_data): + self.json_data = json_data + + def model_dump_json(self): + return self.json_data + + from metagpt.actions.action_node import ActionNode + + action_node = ActionNode(key="", expected_type=Type[str], instruction="", example="") + action_node.instruct_content = InstructContent(resp) + + return action_node diff --git a/metagpt/exp_pool/serializers/base.py b/metagpt/exp_pool/serializers/base.py new file mode 100644 index 000000000..82a0ed8c4 --- /dev/null +++ b/metagpt/exp_pool/serializers/base.py @@ -0,0 +1,22 @@ +"""Base serializer.""" + +from abc import ABC, abstractmethod +from typing import Any + +from pydantic import BaseModel, ConfigDict + + +class BaseSerializer(BaseModel, ABC): + model_config = ConfigDict(arbitrary_types_allowed=True) + + @abstractmethod + def serialize_req(self, req: Any) -> str: + """Serializes the request for storage.""" + + @abstractmethod + def serialize_resp(self, resp: Any) -> str: + """Serializes the function's return value for storage.""" + + @abstractmethod + def deserialize_resp(self, resp: str) -> Any: + """Deserializes the stored response back to the function's return value""" diff --git a/metagpt/exp_pool/serializers/role_zero.py b/metagpt/exp_pool/serializers/role_zero.py new file mode 100644 index 000000000..75e5d5ecb --- /dev/null +++ b/metagpt/exp_pool/serializers/role_zero.py @@ -0,0 +1,40 @@ +"""RoleZero Serializer.""" + +import json + +from metagpt.exp_pool.context_builders import RoleZeroContextBuilder +from metagpt.exp_pool.serializers.simple import SimpleSerializer + + +class RoleZeroSerializer(SimpleSerializer): + def serialize_req(self, req: list[dict]) -> str: + """Serialize the request for database storage, ensuring it is a string. + + This function modifies the content of the last element in the request to remove unnecessary sections, + making the request more concise. + + Args: + req (list[dict]): The request to be serialized. Example: + [ + {"role": "user", "content": "..."}, + {"role": "assistant", "content": "..."}, + {"role": "user", "content": "..."}, + ] + + Returns: + str: The serialized request as a JSON string. + """ + if not req: + return "" + + last_content = req[-1]["content"] + last_content = RoleZeroContextBuilder.replace_content_between_markers( + last_content, "# Data Structure", "# Current Plan", "" + ) + last_content = RoleZeroContextBuilder.replace_content_between_markers( + last_content, "# Example", "# Instruction", "" + ) + + req[-1]["content"] = last_content + + return json.dumps(req) diff --git a/metagpt/exp_pool/serializers/simple.py b/metagpt/exp_pool/serializers/simple.py new file mode 100644 index 000000000..32fe29c9f --- /dev/null +++ b/metagpt/exp_pool/serializers/simple.py @@ -0,0 +1,22 @@ +"""Simple Serializer.""" + +from typing import Any + +from metagpt.exp_pool.serializers.base import BaseSerializer + + +class SimpleSerializer(BaseSerializer): + def serialize_req(self, req: Any) -> str: + """Just use `str` to convert the request object into a string.""" + + return str(req) + + def serialize_resp(self, resp: Any) -> str: + """Just use `str` to convert the response object into a string.""" + + return str(resp) + + def deserialize_resp(self, resp: str) -> Any: + """Just return the string response as it is.""" + + return resp diff --git a/metagpt/roles/di/role_zero.py b/metagpt/roles/di/role_zero.py index 103a77911..59c58861f 100644 --- a/metagpt/roles/di/role_zero.py +++ b/metagpt/roles/di/role_zero.py @@ -12,6 +12,7 @@ from metagpt.actions import Action from metagpt.actions.di.run_command import RunCommand from metagpt.exp_pool import exp_cache from metagpt.exp_pool.context_builders import RoleZeroContextBuilder +from metagpt.exp_pool.serializers import RoleZeroSerializer from metagpt.logs import logger from metagpt.prompts.di.role_zero import ( CMD_PROMPT, @@ -165,9 +166,7 @@ class RoleZero(Role): return True - @exp_cache( - context_builder=RoleZeroContextBuilder(), req_serialize=lambda req: RoleZeroContextBuilder.req_serialize(req) - ) + @exp_cache(context_builder=RoleZeroContextBuilder(), serializer=RoleZeroSerializer()) async def llm_cached_aask(self, *, req: list[dict], system_msgs: list[str]) -> str: return await self.llm.aask(req, system_msgs=system_msgs) diff --git a/tests/metagpt/exp_pool/test_context_builders/test_base_context_builder.py b/tests/metagpt/exp_pool/test_context_builders/test_base_context_builder.py index 17696e1b4..0a160fb42 100644 --- a/tests/metagpt/exp_pool/test_context_builders/test_base_context_builder.py +++ b/tests/metagpt/exp_pool/test_context_builders/test_base_context_builder.py @@ -30,16 +30,3 @@ class TestBaseContextBuilder: ] ) assert result == expected - - def test_replace_content_between_markers(self): - text = "Start\n# Example\nOld content\n# Instruction\nEnd" - new_content = "New content" - result = BaseContextBuilder.replace_content_between_markers(text, "# Example", "# Instruction", new_content) - expected = "Start\n# Example\nNew content\n\n# Instruction\nEnd" - assert result == expected - - def test_replace_content_between_markers_no_match(self): - text = "Start\nNo markers\nEnd" - new_content = "New content" - result = BaseContextBuilder.replace_content_between_markers(text, "# Example", "# Instruction", new_content) - assert result == text diff --git a/tests/metagpt/exp_pool/test_context_builders/test_rolezero_context_builder.py b/tests/metagpt/exp_pool/test_context_builders/test_rolezero_context_builder.py index 0ea04432d..611d68211 100644 --- a/tests/metagpt/exp_pool/test_context_builders/test_rolezero_context_builder.py +++ b/tests/metagpt/exp_pool/test_context_builders/test_rolezero_context_builder.py @@ -30,9 +30,22 @@ class TestRoleZeroContextBuilder: assert result == [{"content": "Updated content"}] def test_replace_example_content(self, context_builder, mocker): - mocker.patch.object(BaseContextBuilder, "replace_content_between_markers", return_value="Replaced content") + mocker.patch.object(RoleZeroContextBuilder, "replace_content_between_markers", return_value="Replaced content") result = context_builder.replace_example_content("Original text", "New example content") assert result == "Replaced content" context_builder.replace_content_between_markers.assert_called_once_with( "Original text", "# Example", "# Instruction", "New example content" ) + + def test_replace_content_between_markers(self): + text = "Start\n# Example\nOld content\n# Instruction\nEnd" + new_content = "New content" + result = RoleZeroContextBuilder.replace_content_between_markers(text, "# Example", "# Instruction", new_content) + expected = "Start\n# Example\nNew content\n\n# Instruction\nEnd" + assert result == expected + + def test_replace_content_between_markers_no_match(self): + text = "Start\nNo markers\nEnd" + new_content = "New content" + result = RoleZeroContextBuilder.replace_content_between_markers(text, "# Example", "# Instruction", new_content) + assert result == text diff --git a/tests/metagpt/exp_pool/test_context_builders/test_simple_context_builder.py b/tests/metagpt/exp_pool/test_context_builders/test_simple_context_builder.py index e96addab9..b6d0f642e 100644 --- a/tests/metagpt/exp_pool/test_context_builders/test_simple_context_builder.py +++ b/tests/metagpt/exp_pool/test_context_builders/test_simple_context_builder.py @@ -32,7 +32,8 @@ class TestSimpleContextBuilder: req = "Test request" result = await context_builder.build(req=req) - assert result == req + expected = SIMPLE_CONTEXT_TEMPLATE.format(req=req, exps="") + assert result == expected @pytest.mark.asyncio async def test_build_without_req(self, context_builder, mocker): diff --git a/tests/metagpt/exp_pool/test_manager.py b/tests/metagpt/exp_pool/test_manager.py index c12fc7e8c..6d0693efd 100644 --- a/tests/metagpt/exp_pool/test_manager.py +++ b/tests/metagpt/exp_pool/test_manager.py @@ -11,9 +11,7 @@ from metagpt.rag.engines import SimpleEngine class TestExperienceManager: @pytest.fixture def mock_config(self): - return Config( - llm=LLMConfig(), exp_pool=ExperiencePoolConfig(enable_write=True, enable_read=True, init_exp=False) - ) + return Config(llm=LLMConfig(), exp_pool=ExperiencePoolConfig(enable_write=True, enable_read=True)) @pytest.fixture def mock_storage(self, mocker): @@ -65,30 +63,6 @@ class TestExperienceManager: result = await mock_experience_manager.query_exps("query") assert result == [] - def test_init_exp_pool(self, mock_experience_manager, mock_config, mocker): - mock_experience_manager._has_exps = mocker.MagicMock(return_value=False) - mock_experience_manager._init_teamleader_exps = mocker.MagicMock() - mock_experience_manager._init_engineer2_exps = mocker.MagicMock() - - mock_config.exp_pool.init_exp = True - mock_experience_manager.init_exp_pool() - - mock_experience_manager._has_exps.assert_called_once() - mock_experience_manager._init_teamleader_exps.assert_called_once() - mock_experience_manager._init_engineer2_exps.assert_called_once() - - def test_init_exp_pool_already_has_exps(self, mock_experience_manager, mock_config, mocker): - mock_experience_manager._has_exps = mocker.MagicMock(return_value=True) - mock_experience_manager._init_teamleader_exps = mocker.MagicMock() - mock_experience_manager._init_engineer2_exps = mocker.MagicMock() - - mock_config.exp_pool.init_exp = True - mock_experience_manager.init_exp_pool() - - mock_experience_manager._has_exps.assert_called_once() - mock_experience_manager._init_teamleader_exps.assert_not_called() - mock_experience_manager._init_engineer2_exps.assert_not_called() - def test_has_exps(self, mock_experience_manager, mock_storage): mock_storage._retriever._vector_store._get.return_value.ids = ["id1"] From 5a138749912893a037919963dbb2a90fe1eb6ba8 Mon Sep 17 00:00:00 2001 From: shenchucheng Date: Wed, 10 Jul 2024 11:07:35 +0800 Subject: [PATCH 068/119] terminal async reporter --- metagpt/tools/libs/terminal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/metagpt/tools/libs/terminal.py b/metagpt/tools/libs/terminal.py index 8d44c13e6..c2010ed6f 100644 --- a/metagpt/tools/libs/terminal.py +++ b/metagpt/tools/libs/terminal.py @@ -123,12 +123,12 @@ class Terminal: if ix >= 0: line = line[0:ix] if line: - observer.report(line, "output") + await observer.async_report(line, "output") # report stdout in real-time cmd_output.append(line) return "".join(cmd_output) # log stdout in real-time - observer.report(line, "output") + await observer.async_report(line, "output") cmd_output.append(line) if daemon: await self.stdout_queue.put(line) From 46e7c2c05c13ac0ac6a1b90065f60f16ced9a43b Mon Sep 17 00:00:00 2001 From: lidanyang Date: Wed, 10 Jul 2024 11:25:34 +0800 Subject: [PATCH 069/119] use keyword retriever for DataAnalyst --- metagpt/strategy/experience_retriever.py | 9 ++------- tests/metagpt/roles/di/run_data_analyst.py | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/metagpt/strategy/experience_retriever.py b/metagpt/strategy/experience_retriever.py index 63ca4fcbd..04807ebec 100644 --- a/metagpt/strategy/experience_retriever.py +++ b/metagpt/strategy/experience_retriever.py @@ -638,6 +638,8 @@ class KeywordExpRetriever(ExpRetriever): return DEPLOY_EXAMPLE elif "issue" in context.lower(): return FIX_ISSUE_EXAMPLE + elif "https:" or "http:" in context.lower(): + return WEB_SCRAPING_EXAMPLE elif exp_type == "task": if "diagnose" in context.lower(): return SEARCH_SYMBOL_EXAMPLE @@ -1018,10 +1020,3 @@ Here is the command to finish the current task and parse the html content: ... """ - - -class WebExpRetriever(ExpRetriever): - """A simple experience retriever that returns manually crafted examples.""" - - def retrieve(self, context: str = "") -> str: - return WEB_SCRAPING_EXAMPLE diff --git a/tests/metagpt/roles/di/run_data_analyst.py b/tests/metagpt/roles/di/run_data_analyst.py index 9c1f72394..445f8f800 100644 --- a/tests/metagpt/roles/di/run_data_analyst.py +++ b/tests/metagpt/roles/di/run_data_analyst.py @@ -33,7 +33,7 @@ NEWS_36KR_REQ = """从36kr创投平台https://pitchhub.36kr.com/financing-flash """ WIKIPEDIA_SEARCH_REQ = """ -Search for `LLM` on Wikipedia and print all the meaningful significances of the entry. +Search for `LLM` on https://www.wikipedia.org/ and print all the meaningful significances of the entry. """ STACKOVERFLOW_CLICK_REQ = """ From be607ba3e3a4a00ed903f37a213a94870df31380 Mon Sep 17 00:00:00 2001 From: lidanyang Date: Wed, 10 Jul 2024 11:27:10 +0800 Subject: [PATCH 070/119] use keyword retriever for DataAnalyst --- tests/metagpt/roles/di/run_data_analyst.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/metagpt/roles/di/run_data_analyst.py b/tests/metagpt/roles/di/run_data_analyst.py index 445f8f800..b7b48e0db 100644 --- a/tests/metagpt/roles/di/run_data_analyst.py +++ b/tests/metagpt/roles/di/run_data_analyst.py @@ -10,6 +10,7 @@ CALIFORNIA_HOUSING_REQ = """ Analyze the 'Canifornia-housing-dataset' using https://scikit-learn.org/stable/modules/generated/sklearn.datasets.fetch_california_housing.html#sklearn.datasets.fetch_california_housing to predict the median house value. you need to perfrom data preprocessing, feature engineering and finally modeling to predict the target. Use machine learning techniques such as linear regression (including ridge regression and lasso regression), random forest, CatBoost, LightGBM, XGBoost or other appropriate method. You also need to report the MSE on the test dataset """ +# For web scraping task, please provide url begin with `https://` or `http://` PAPER_LIST_REQ = """" Get data from `paperlist` table in https://papercopilot.com/statistics/iclr-statistics/iclr-2024-statistics/, and save it to a csv file. paper title must include `multiagent` or `large language model`. From b03ce58fde78117a7e11c235b0ecbe8cecaebdc2 Mon Sep 17 00:00:00 2001 From: lidanyang Date: Wed, 10 Jul 2024 11:29:27 +0800 Subject: [PATCH 071/119] remove task_type and parse_browser_action to DataAnalyst --- metagpt/actions/di/write_analysis_code.py | 6 +-- metagpt/prompts/di/data_analyst.py | 56 +++++------------------ metagpt/prompts/di/write_analysis_code.py | 2 +- metagpt/roles/di/data_analyst.py | 32 +++++++++---- metagpt/roles/di/role_zero.py | 17 ++++--- 5 files changed, 46 insertions(+), 67 deletions(-) diff --git a/metagpt/actions/di/write_analysis_code.py b/metagpt/actions/di/write_analysis_code.py index 19afab711..d0c4c016e 100644 --- a/metagpt/actions/di/write_analysis_code.py +++ b/metagpt/actions/di/write_analysis_code.py @@ -42,7 +42,7 @@ class WriteAnalysisCode(Action): tool_info: str = "", working_memory: list[Message] = None, use_reflection: bool = False, - browser_memory: list[dict] = None, + browser_actions: list[dict] = None, **kwargs, ) -> str: structual_prompt = STRUCTUAL_PROMPT.format( @@ -51,8 +51,8 @@ class WriteAnalysisCode(Action): tool_info=tool_info, ) message = [Message(content=structual_prompt, role="user")] - if browser_memory: - browser_prompt = BROWSER_INFO.format(browser_memory=browser_memory) + if browser_actions: + browser_prompt = BROWSER_INFO.format(browser_actions=browser_actions) message = [Message(content=browser_prompt, role="user")] + message working_memory = working_memory or [] diff --git a/metagpt/prompts/di/data_analyst.py b/metagpt/prompts/di/data_analyst.py index 08b8d0df8..7abe0ac93 100644 --- a/metagpt/prompts/di/data_analyst.py +++ b/metagpt/prompts/di/data_analyst.py @@ -1,47 +1,4 @@ -CMD_PROMPT = """ -# Data Structure -class Task(BaseModel): - task_id: str = "" - dependent_task_ids: list[str] = [] - instruction: str = "" - task_type: str = "" - assignee: str = "David" - -# Available Commands -{available_commands} - -# Current Plan -{plan_status} - -# Example -{example} - -# Instructions -Based on the context, write a plan or modify an existing plan to achieve the goal. A plan consists of one to 3 tasks. -If plan is created, you should track the progress and update the plan accordingly, such as finish_current_task, append_task, reset_task, replace_task, etc. -Pay close attention to new user message, review the conversation history, use reply_to_human to respond to new user requirement. -Note: -1. If you keeping encountering errors, unexpected situation, or you are not sure of proceeding, use ask_human to ask for help. -2. Carefully review your progress at the current task, if your actions so far has not fulfilled the task instruction, you should continue with current task. Otherwise, finish current task. -3. Each time you finish a task, use reply_to_human to report your progress. -Pay close attention to the Example provided, you can reuse the example for your current situation if it fits. - -You may use any of the available commands to create a plan or update the plan. You may output mutiple commands, they will be executed sequentially. -If you finish current task, you will automatically take the next task in the existing plan, use finish_task, DON'T append a new task. - -# Your commands in a json array, in the following output format, always output a json array, if there is nothing to do, use the pass command: -Some text indicating your thoughts, such as how you should update the plan status, respond to inquiry, or seek for help. Then a json array of commands. -```json -[ - {{ - "command_name": str, - "args": {{"arg_name": arg_value, ...}} - }}, - ... -] -``` -Notice: your output JSON data section must start with **```json [** -""" +from metagpt.strategy.task_type import TaskType BROWSER_INSTRUCTION = """ 4. Carefully choose to use or not use the browser tool to assist you in web tasks. @@ -49,3 +6,14 @@ BROWSER_INSTRUCTION = """ - If you need detail HTML content, write code to get it but not to use the browser tool. - Make sure the command_name are certainly in Available Commands when you use the browser tool. """ + +TASK_TYPE_DESC = "\n".join([f"- **{tt.type_name}**: {tt.value.desc}" for tt in TaskType]) + + +CODE_STATUS = """ +**Code written**: +{code} + +**Execution status**: {status} +**Execution result**: {result} +""" \ No newline at end of file diff --git a/metagpt/prompts/di/write_analysis_code.py b/metagpt/prompts/di/write_analysis_code.py index bf67d8ba0..6105c4182 100644 --- a/metagpt/prompts/di/write_analysis_code.py +++ b/metagpt/prompts/di/write_analysis_code.py @@ -122,6 +122,6 @@ Latest data info after previous tasks: BROWSER_INFO = """ Here are ordered web actions in the browser environment, note that you can not use the browser tool in the current environment. -{browser_memory} +{browser_actions} The latest url is the one you should use to view the page. If view page has been done, directly use the variable and html content in executing result. """ diff --git a/metagpt/roles/di/data_analyst.py b/metagpt/roles/di/data_analyst.py index 647196433..e6b734417 100644 --- a/metagpt/roles/di/data_analyst.py +++ b/metagpt/roles/di/data_analyst.py @@ -1,15 +1,18 @@ from __future__ import annotations +import re +from typing import List + from pydantic import Field, model_validator from metagpt.actions.di.execute_nb_code import ExecuteNbCode from metagpt.actions.di.write_analysis_code import WriteAnalysisCode from metagpt.logs import logger -from metagpt.prompts.di.data_analyst import BROWSER_INSTRUCTION +from metagpt.prompts.di.data_analyst import BROWSER_INSTRUCTION, TASK_TYPE_DESC, CODE_STATUS from metagpt.prompts.di.role_zero import ROLE_INSTRUCTION from metagpt.roles.di.role_zero import RoleZero from metagpt.schema import TaskResult, Message -from metagpt.strategy.experience_retriever import ExpRetriever, WebExpRetriever +from metagpt.strategy.experience_retriever import ExpRetriever, KeywordExpRetriever from metagpt.tools.tool_recommend import BM25ToolRecommender, ToolRecommender from metagpt.tools.tool_registry import register_tool @@ -20,11 +23,12 @@ class DataAnalyst(RoleZero): profile: str = "DataAnalyst" goal: str = "Take on any data-related tasks, such as data analysis, machine learning, deep learning, web browsing, web scraping, web searching, web deployment, terminal operation, git and github operation, etc." instruction: str = ROLE_INSTRUCTION + BROWSER_INSTRUCTION + task_type_desc: str = TASK_TYPE_DESC tools: list[str] = ["Plan", "DataAnalyst", "RoleZero", "Browser"] custom_tools: list[str] = ["machine learning", "web scraping", "Terminal"] custom_tool_recommender: ToolRecommender = None - experience_retriever: ExpRetriever = WebExpRetriever() + experience_retriever: ExpRetriever = KeywordExpRetriever() use_reflection: bool = True write_code: WriteAnalysisCode = Field(default_factory=WriteAnalysisCode, exclude=True) @@ -40,6 +44,17 @@ class DataAnalyst(RoleZero): "DataAnalyst.write_and_exec_code": self.write_and_exec_code, }) + def parse_browser_actions(self, memory: List[Message]): + for index, msg in enumerate(memory): + if msg.cause_by == "browser": + browser_url = re.search('URL: (.*?)\\n', msg.content).group(1) + pattern = re.compile(r"Command Browser\.(\w+) executed") + browser_action = { + 'command': pattern.match(memory[index - 1].content).group(1), + 'current url': browser_url + } + self.browser_actions.append(browser_action) + async def write_and_exec_code(self): """Write a code block for current task and execute it in an interactive notebook environment.""" counter = 0 @@ -68,7 +83,7 @@ class DataAnalyst(RoleZero): tool_info=tool_info, working_memory=self.rc.working_memory.get() if use_reflection else None, use_reflection=use_reflection, - browser_memory=self.browser_memory + browser_actions=self.browser_actions ) self.rc.working_memory.add(Message(content=code, role="assistant", cause_by=WriteAnalysisCode)) @@ -83,11 +98,8 @@ class DataAnalyst(RoleZero): if success: task_result = TaskResult(code=code, result=result, is_success=success) self.planner.current_task.update_task_result(task_result) - output = f""" - **Code written**: - {code} - **Execution status**:{'Success' if success else 'Failed'} - **Execution result**: {result} - """ + + status = 'Success' if success else 'Failed' + output = CODE_STATUS.format(code=code, status=status, result=result) self.rc.working_memory.clear() return output diff --git a/metagpt/roles/di/role_zero.py b/metagpt/roles/di/role_zero.py index a52d72c8e..fb89114c8 100644 --- a/metagpt/roles/di/role_zero.py +++ b/metagpt/roles/di/role_zero.py @@ -6,7 +6,6 @@ import re import traceback from typing import Callable, Dict, List, Literal, Tuple -from metagpt.strategy.task_type import TaskType from pydantic import model_validator from metagpt.actions import Action @@ -41,6 +40,7 @@ class RoleZero(Role): system_msg: list[str] = None # Use None to conform to the default value at llm.aask cmd_prompt: str = CMD_PROMPT instruction: str = ROLE_INSTRUCTION + task_type_desc: str = None # React Mode react_mode: Literal["react"] = "react" @@ -54,7 +54,7 @@ class RoleZero(Role): # Equipped with three basic tools by default for optional use editor: Editor = Editor() browser: Browser = Browser() - browser_memory: list[dict] = [] # store the memory of browser + browser_actions: list[dict] = [] # store the browser history actions # terminal: Terminal = Terminal() # FIXME: TypeError: cannot pickle '_thread.lock' object # Experience @@ -137,7 +137,6 @@ class RoleZero(Role): ### 2. Plan Status ### plan_status, current_task = self._get_plan_status() - task_type_desc = "\n".join([f"- **{tt.type_name}**: {tt.value.desc}" for tt in TaskType]) ### 3. Tool/Command Info ### tools = await self.tool_recommender.recommend_tools() @@ -150,19 +149,16 @@ class RoleZero(Role): example=example, available_commands=tool_info, instruction=self.instruction.strip(), - task_type_desc=task_type_desc, + task_type_desc=self.task_type_desc, ) memory = self.rc.memory.get(self.memory_k) if not self.browser.is_empty_page: pattern = re.compile(r"Command Browser\.(\w+) executed") for index, msg in zip(range(len(memory), 0, -1), memory[::-1]): if pattern.match(msg.content): - content = await self.browser.view() - memory.insert(index, UserMessage(cause_by="browser", content=content)) - browser_url = re.search('URL: (.*?)\\n', content).group(1) - browser_action = {'command': pattern.match(msg.content).group(1), 'current url': browser_url} - self.browser_memory.append(browser_action) + memory.insert(index, UserMessage(cause_by="browser", content=await self.browser.view())) break + self.parse_browser_actions(memory=memory) context = self.llm.format_msg(memory + [UserMessage(content=prompt)]) # print(*context, sep="\n" + "*" * 5 + "\n") async with ThoughtReporter(enable_llm_stream=True): @@ -171,6 +167,9 @@ class RoleZero(Role): return True + def parse_browser_actions(self, memory: List[Message]): + pass + async def _act(self) -> Message: if self.use_fixed_sop: return await super()._act() From b3dab8b4251ca0aa45ca74f52126c9edb382bc16 Mon Sep 17 00:00:00 2001 From: seeker Date: Wed, 10 Jul 2024 14:49:48 +0800 Subject: [PATCH 072/119] =?UTF-8?q?update:=20=E7=A7=BB=E9=99=A4=20swe=20ag?= =?UTF-8?q?ent=20window=20=E7=9A=84=E5=AE=9E=E6=97=B6=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=EF=BC=8C=E5=9B=BA=E5=AE=9A=E4=B8=BA=20100=20=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- metagpt/prompts/di/swe_agent.py | 2 +- metagpt/roles/di/swe_agent.py | 22 ++-------------------- 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/metagpt/prompts/di/swe_agent.py b/metagpt/prompts/di/swe_agent.py index ed1f8a011..a1c491210 100644 --- a/metagpt/prompts/di/swe_agent.py +++ b/metagpt/prompts/di/swe_agent.py @@ -7,7 +7,7 @@ https://github.com/princeton-nlp/SWE-agent/tree/main/config/configs SWE_AGENT_SYSTEM_TEMPLATE = """ SETTING: You are an autonomous programmer, and you're working directly in the environment line with a special interface. -The special interface consists of a file editor that shows you {WINDOW} lines of a file at a time. +The special interface consists of a file editor that shows you 100 lines of a file at a time. Please note that THE EDIT COMMAND REQUIRES PROPER INDENTATION. Pay attention to the original indentation when replacing the function. If you'd like to add the line ' print(x)' you must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run. diff --git a/metagpt/roles/di/swe_agent.py b/metagpt/roles/di/swe_agent.py index d0458a22f..7150c0a67 100644 --- a/metagpt/roles/di/swe_agent.py +++ b/metagpt/roles/di/swe_agent.py @@ -1,5 +1,4 @@ import json -import os from pydantic import Field @@ -18,9 +17,7 @@ class SWEAgent(RoleZero): name: str = "Swen" profile: str = "Issue Solver" goal: str = "Resolve GitHub issue" - _bash_window_size: int = 100 - _system_msg: str = SWE_AGENT_SYSTEM_TEMPLATE - system_msg: list[str] = [_system_msg.format(WINDOW=_bash_window_size)] + system_msg: str = [SWE_AGENT_SYSTEM_TEMPLATE] _instruction: str = NEXT_STEP_TEMPLATE tools: list[str] = [ "Bash", @@ -35,7 +32,6 @@ class SWEAgent(RoleZero): run_eval: bool = False async def _think(self) -> bool: - self._update_system_msg() self._format_instruction() res = await super()._think() if self.run_eval: @@ -51,17 +47,6 @@ class SWEAgent(RoleZero): } ) - def _update_system_msg(self): - """ - Sets the system message for the SWE agent. - - Sets the `_bash_window_size` from the environment variable `WINDOW` if it exists. - Formats the `_system_msg` template with the current `_bash_window_size`. - """ - if os.getenv("WINDOW"): - self._bash_window_size = int(os.getenv("WINDOW")) - self.system_msg = [self._system_msg.format(WINDOW=self._bash_window_size)] - def _format_instruction(self): """ Formats the instruction message for the SWE agent. @@ -71,10 +56,7 @@ class SWEAgent(RoleZero): """ state_output = self.terminal.run("state") bash_state = json.loads(state_output) - - self.instruction = self._instruction.format( - WINDOW=self._bash_window_size, examples=MINIMAL_EXAMPLE, **bash_state - ).strip() + self.instruction = self._instruction.format(**bash_state).strip() return self.instruction From 9dd9cf6f559bcda47951addc1d335bb61d1c6642 Mon Sep 17 00:00:00 2001 From: shenchucheng Date: Wed, 10 Jul 2024 15:20:45 +0800 Subject: [PATCH 073/119] redirect stderr to stdout --- metagpt/tools/libs/terminal.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/metagpt/tools/libs/terminal.py b/metagpt/tools/libs/terminal.py index c2010ed6f..a736d476a 100644 --- a/metagpt/tools/libs/terminal.py +++ b/metagpt/tools/libs/terminal.py @@ -1,6 +1,6 @@ import asyncio from asyncio import Queue -from asyncio.subprocess import PIPE +from asyncio.subprocess import PIPE, STDOUT from typing import Optional from metagpt.const import DEFAULT_WORKSPACE_ROOT, SWE_SETUP_PATH @@ -28,7 +28,7 @@ class Terminal: async def _start_process(self): # Start a persistent shell process self.process = await asyncio.create_subprocess_exec( - *self.shell_command, stdin=PIPE, stdout=PIPE, stderr=PIPE, executable="/bin/bash" + *self.shell_command, stdin=PIPE, stdout=PIPE, stderr=STDOUT, executable="/bin/bash" ) await self._check_state() @@ -115,6 +115,7 @@ class Terminal: # '\r' is changed to '\n', resulting in excessive output. tmp = b"" while True: + self.process.communicate() output = tmp + await self.process.stdout.read(1) *lines, tmp = output.splitlines(True) for line in lines: From 67bb90d1788fc59f192b5a3a038e545e73451abd Mon Sep 17 00:00:00 2001 From: seeker Date: Wed, 10 Jul 2024 22:14:35 +0800 Subject: [PATCH 074/119] =?UTF-8?q?update:=20=E6=8F=90=E7=A4=BA=E8=AF=8D?= =?UTF-8?q?=20ex=20=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- metagpt/prompts/di/swe_agent.py | 108 ++++++++++++------ metagpt/tools/libs/git.py | 36 +++--- metagpt/tools/libs/terminal.py | 5 +- .../tools/swe_agent_commands/setup_default.sh | 1 - 4 files changed, 99 insertions(+), 51 deletions(-) diff --git a/metagpt/prompts/di/swe_agent.py b/metagpt/prompts/di/swe_agent.py index 78a8ce846..c4db3b785 100644 --- a/metagpt/prompts/di/swe_agent.py +++ b/metagpt/prompts/di/swe_agent.py @@ -5,7 +5,7 @@ https://github.com/princeton-nlp/SWE-agent/tree/main/config/configs """ SWE_AGENT_SYSTEM_TEMPLATE = """ -SETTING: You are an autonomous programmer, and you're working directly in the environment line with a special interface.Let's work step by step. +SETTING: You are an autonomous programmer, and you're working directly in the environment line with a special interface. The special interface consists of a file editor that shows you 100 lines of a file at a time. @@ -33,7 +33,7 @@ You should only include a *SINGLE* command in the command section and then wait If you'd like to issue two commands at once, PLEASE DO NOT DO THAT! Please instead first submit just the first command, and then after receiving a response you'll be able to issue the second command. Remember, YOU CAN ONLY ENTER ONE COMMAND AT A TIME. You should always wait for feedback after every command. -You can use any bash commands you want (e.g., find, grep, cat, ls, cd) or any custom special tools (including `edit`) by calling Bash.run. Edit all the files you need, but make sure to PRIORITIZE the `Available Commands` provided to accomplish your tasks,such as `git_push`. +You can use any bash commands you want (e.g., find, grep, cat, ls, cd) or any custom special tools (including `edit`) by calling Bash.run. Edit all the files you need. You should carefully observe the behavior and results of the previous action, and avoid triggering repeated errors. However, the Bash.run does NOT support interactive session commands (e.g. python, vim), so please do not invoke them. @@ -50,69 +50,111 @@ MINIMAL_EXAMPLE = """ ## Example of a actions trajectory User Requirement and Issue: Fix the bug in the repo. Because the environment is not available, you DO NOT need to run and modify any existing test case files or add new test case files to ensure that the bug is fixed. -### Read and understand issue(Require): -[{{ +### Read and understand issue: +Thought: Firstly, I need to review the detailed information of this issue in order to understand the problem that needs fixing. +{{ "command_name": "Browser.goto", "args": {{ "url": "https://github.com/geekan/MetaGPT/issues/1275" }} -}}] +}} -> ### Locate issue(Require): Locate the issue in the code by searching for the relevant file, function, or class and open the file to view the code. -[{{ +Thought: I need to come under the repo path +{{ "command_name": "Bash.run", "args": {{ - "cmd": "cd /workspace/django__django_3.0" + "cmd": "cd /workspace/MetaGPT" }} -}}] +}} -> -Bash.run(cmd='search_dir_and_preview ASCIIUsernameValidator') -[{{ +Thought: Let's start by locating the `openai_api.py` file.\nFirst, let's search for the `openai_api.py` file. +{{ "command_name": "Bash.run", "args": {{ - "cmd": "open /workspace/django__django_3.0/django/contrib/auth/validators.py" + "cmd": "find_file 'openai_api.py'" }} -}}] +}} +-> + +Thought: We have located both the `openai_api.py` file. Let's start by opening the `openai_api.py` file to apply the necessary changes.", +{{ + "command_name": "Bash.run", + "args": {{ + "cmd": "open '/workspace/MetaGPT/provider/openai_api.py'" + }} +}} -> ### Fix the Bug(Require): Fix the bug in the code by editing the relevant function, class or code snippet. -[{{ +Thought: Let's edit the apow function to include the Pow case in the isinstance check. +{{ "command_name": "Bash.run", "args": {{ - "cmd": "edit 10:20 < +Thought: Due to a syntax error related to an undefined name 'Image', we need to address this issue even though it is not directly related to our work. Let's try importing the package to fix it. +{{ + "command_name": "Bash.run", + "args": {{ + "cmd": "edit 14:14 < -### Push the Changes: Pushes changes from a local Git repository to its remote counterpart. -[{{ +### Save the Changes (Required): After all changes have been made, save them to the repository. +> You must choose one of the following two methods. + +#### Just save the changes locally, it only need one action. +Thought: The bug has been fixed. Let's submit the changes. +{{ + "command_name": "Bash.run", + "args": {{ + "cmd": "submit" + }} +}} +-> + +#### Save the changes and commit them to the remote repository. + +##### Push the changes from the local repository to the remote repository. +Thought: All changes have been saved, let's push the code to the remote repository. +{{ "command_name": "git_push", "args": {{ - "local_path": "/workspace/django__django_3.0/django/contrib/auth", + "local_path": "/workspace/MetaGPT", "app_name": "github", - "comments": "Fix: produced TypeError: openai.types.completion_usage.CompletionUsage() argument after ** must be a mapping, not NoneType", + "comments": "Fix Issue #1275: produced TypeError: openai.types.completion_usage.CompletionUsage() argument after ** must be a mapping, not NoneType"", "new_branch": "test-fix" }} -}}] +}} +-> -### Create pull request +##### Create a pull request (Optional): Merge the changes from the new branch into the master branch. +Thought: Now that the changes have been pushed to the remote repository, due to the user's requirement, let's create a pull request to merge the changes into the master branch. [{{ "command_name": "git_create_pull", "args": {{ "base": "master", "head": "test-fix", - "base_repo_name": "Justin-ZL/langchain", + "base_repo_name": "garylin2099/MetaGPT", + "head_repo_name": "seeker-jie/MetaGPT", "app_name": "github", - "title": "Fix Issue #1275: produced TypeError: openai.types.completion_usage.CompletionUsage() argument after ** must be a mapping, not NoneTyp", - "body": "This pull request addresses issue #1275 by producing TypeError: openai.types.completion_usage.CompletionUsage() argument after ** must be a mapping, not NoneTyp." - }} + "title": "Fix Issue #1275: produced TypeError: openai.types.completion_usage.CompletionUsage() argument after ** must be a mapping, not NoneType"", + "body": "This pull request addresses issue #1275 by ensuring that chunk.usage is not None before passing it to CompletionUsage." + }} }}] -> -[{{ - "command_name": "end", -}}] + +### Finally +Thought: All task has been done, let's end the conversation. +{{ + "command_name": "end" +}} """ @@ -177,11 +219,14 @@ IMPORTANT_TIPS = """ - If a search command fails, modify the search criteria and check for typos or incorrect paths, then try again. - Based on feedback of observation or bash command in trajectory to guide adjustments in your search strategy. -13. If the task results in succeed, use command `git_push` to push the results in local,then use command `git_create_pull` to create PR. +13. Save the code change: + - If you need to submit changes to the remote repository, first use the regular git commit command to save the changes locally, then select a command from the `Available Commands: [git_push, git_create_pull]` to submit the changes to the remote repository. + + - If you don't need to submit code changes to the remote repository. use the command Bash.run('submit') to commit the changes locally. 14. If provided an issue link, you MUST go to the issue page using Browser tool to understand the issue before starting your fix. -15. When the edit fails, try to enlarge the starting line. +15. When the edit fails, try to enlarge the starting line, The edit function needs to use `Bash.Run` call. """ NEXT_STEP_TEMPLATE = f""" @@ -199,6 +244,5 @@ The current bash state is: (Open file: {{open_file}}) (Current directory: {{working_dir}}) -Avoid repeating the same command. Instead, please think about the current situation and provide the next bash command to execute in JSON format:" - +Avoid repeating the same command. Instead, please think about the current situation and provide the next bash command to execute in JSON format" """ diff --git a/metagpt/tools/libs/git.py b/metagpt/tools/libs/git.py index 4606b5815..676e9125a 100644 --- a/metagpt/tools/libs/git.py +++ b/metagpt/tools/libs/git.py @@ -9,9 +9,10 @@ from github.Issue import Issue from github.PullRequest import PullRequest from metagpt.tools.tool_registry import register_tool +from metagpt.utils.git_repository import GitBranch -@register_tool(tags=["software development", "git", "Commit the changes and push to remote git repository."]) +@register_tool(tags=["software development", "git", "Push to remote git repository."]) async def git_push( local_path: Union[str, Path], app_name: str, @@ -22,22 +23,23 @@ async def git_push( Pushes changes from a local Git repository to its remote counterpart. Args: - local_path (Union[str, Path]): The path to the local Git repository. - app_name (str): The name of the application where the repository is hosted. For example, "github", "gitlab", "bitbucket", etc. - comments (str, optional): The commit message to use. Defaults to "Commit". + local_path (Union[str, Path]): The absolute path to the local Git repository. + app_name (str): The name of the platform hosting the repository (e.g., "github", "gitlab", "bitbucket"). + comments (str, optional): Comments to be associated with the push. Defaults to "Commit". new_branch (str, optional): The name of the new branch to create and push changes to. If not provided, changes will be pushed to the current branch. Defaults to "". Returns: GitBranch: The branch to which the changes were pushed. + Raises: ValueError: If the provided local_path does not point to a valid Git repository. Example: >>> url = "https://github.com/iorisa/snake-game.git" >>> local_path = await git_clone(url=url) - >>> app_name="github" - >>> comments = "Archive" + >>> app_name = "github" + >>> comments = "Commit" >>> new_branch = "feature/new" >>> branch = await git_push(local_path=local_path, app_name=app_name, comments=comments, new_branch=new_branch) >>> base = branch.base @@ -45,8 +47,8 @@ async def git_push( >>> repo_name = branch.repo_name >>> print(f"base branch:'{base}', head branch:'{head}', repo_name:'{repo_name}'") base branch:'master', head branch:'feature/new', repo_name:'iorisa/snake-game' - """ + from metagpt.tools.libs import get_env from metagpt.utils.git_repository import GitRepository @@ -64,9 +66,9 @@ async def git_push( async def git_create_pull( base: str, head: str, - base_repo_name: str, app_name: str, - head_repo_name: Optional[str] = None, + base_repo_name: str, + head_repo_name: str = None, title: Optional[str] = None, body: Optional[str] = None, issue: Optional[Issue] = None, @@ -75,14 +77,14 @@ async def git_create_pull( Creates a pull request on a Git repository. Use this tool in priority over Browser to create a pull request. Args: - base (str): The base branch of the pull request. - head (str): The head branch of the pull request. - base_repo_name (str): The full repository name (user/repo) where the pull request will be created. - app_name (str): The name of the application where the repository is hosted. For example, "github", "gitlab", "bitbucket", etc. - head_repo_name (Optional[str], optional): The full repository name (user/repo) where the pull request will merge from. Defaults to None. - title (Optional[str], optional): The title of the pull request. Defaults to None. - body (Optional[str], optional): The body of the pull request. Defaults to None. - issue (Optional[Issue], optional): The related issue of the pull request. Defaults to None. + base (str): The name of the base branch where the pull request will be merged. + head (str): The name of the branch that contains the changes for the pull request. + app_name (str): The name of the platform hosting the repository (e.g., "github", "gitlab", "bitbucket"). + base_repo_name (str): The full name of the target repository (in the format "user/repo") where the pull request will be created. + head_repo_name (Optional[str]): The full name of the source repository (in the format "user/repo") from which the changes will be pulled. + title (Optional[str]): The title of the pull request. Defaults to None. + body (Optional[str]): The description or body content of the pull request. Defaults to None. + issue (Optional[Issue]): An optional issue related to the pull request. Defaults to None. Example: >>> # create pull request diff --git a/metagpt/tools/libs/terminal.py b/metagpt/tools/libs/terminal.py index 8d44c13e6..c23cc1a7f 100644 --- a/metagpt/tools/libs/terminal.py +++ b/metagpt/tools/libs/terminal.py @@ -28,7 +28,7 @@ class Terminal: async def _start_process(self): # Start a persistent shell process self.process = await asyncio.create_subprocess_exec( - *self.shell_command, stdin=PIPE, stdout=PIPE, stderr=PIPE, executable="/bin/bash" + *self.shell_command, stdin=PIPE, stdout=PIPE, stderr=PIPE, executable="bash" ) await self._check_state() @@ -227,6 +227,9 @@ class Bash(Terminal): end_line (int): The line number to end the edit at (inclusive), starting from 1. replacement_text (str): The text to replace the current selection with, must conform to PEP8 standards. + - submit + Submits your current code locally. it can only be executed once, the last action before the `end`. + Note: Make sure to use these functions as per their defined arguments and behaviors. """ if not self.start_flag: diff --git a/metagpt/tools/swe_agent_commands/setup_default.sh b/metagpt/tools/swe_agent_commands/setup_default.sh index dc3b335df..5e02fad5d 100644 --- a/metagpt/tools/swe_agent_commands/setup_default.sh +++ b/metagpt/tools/swe_agent_commands/setup_default.sh @@ -17,4 +17,3 @@ source $REPO_ROOT_DIR/metagpt/tools/swe_agent_commands/search.sh source $REPO_ROOT_DIR/metagpt/tools/swe_agent_commands/edit_linting.sh export SWE_CMD_WORK_DIR="$REPO_ROOT_DIR/workspace/swe_agent_workdir" -#sudo chmod 777 $REPO_ROOT_DIR/workspace/swe_agent_workdir From 866d93b7dbaa331536a1c68c9637106a73dfae84 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Thu, 11 Jul 2024 10:26:42 +0800 Subject: [PATCH 075/119] when enable_write is false, skip evaluating and saving the experience --- metagpt/exp_pool/context_builders/role_zero.py | 7 +++---- metagpt/exp_pool/decorator.py | 18 +++++++++++------- metagpt/exp_pool/serializers/base.py | 11 +++++++++-- metagpt/exp_pool/serializers/role_zero.py | 8 +++++--- metagpt/strategy/experience_retriever.py | 4 ++-- 5 files changed, 30 insertions(+), 18 deletions(-) diff --git a/metagpt/exp_pool/context_builders/role_zero.py b/metagpt/exp_pool/context_builders/role_zero.py index b492ca5ca..6407314ac 100644 --- a/metagpt/exp_pool/context_builders/role_zero.py +++ b/metagpt/exp_pool/context_builders/role_zero.py @@ -15,16 +15,15 @@ class RoleZeroContextBuilder(BaseContextBuilder): Returns: list[dict]: The updated request with formatted experiences or the original request if no experiences are available. """ - req = kwargs.get("req", []) if not req: return req - exps_str = self.format_exps() - if not exps_str: + exps = self.format_exps() + if not exps: return req - req[-1]["content"] = self.replace_example_content(req[-1].get("content", ""), exps_str) + req[-1]["content"] = self.replace_example_content(req[-1].get("content", ""), exps) return req diff --git a/metagpt/exp_pool/decorator.py b/metagpt/exp_pool/decorator.py index deb3faafc..0a9a83818 100644 --- a/metagpt/exp_pool/decorator.py +++ b/metagpt/exp_pool/decorator.py @@ -1,7 +1,6 @@ """Experience Decorator.""" import asyncio -import copy import functools from typing import Any, Callable, Optional, TypeVar @@ -33,9 +32,11 @@ def exp_cache( ): """Decorator to get a perfect experience, otherwise, it executes the function, and create a new experience. - 1. This can be applied to both synchronous and asynchronous functions. - 2. The function must have a `req` parameter, and it must be provided as a keyword argument. - 3. If `config.exp_pool.enable_read` is False, the decorator will just directly execute the function. + Note: + 1. This can be applied to both synchronous and asynchronous functions. + 2. The function must have a `req` parameter, and it must be provided as a keyword argument. + 3. If `config.exp_pool.enable_read` is False, the decorator will just directly execute the function. + 4. If `config.exp_pool.enable_write` is False, the decorator will skip evaluating and saving the experience. Args: _func: Just to make the decorator more flexible, for example, it can be used directly with @exp_cache by default, without the need for @exp_cache(). @@ -68,11 +69,14 @@ def exp_cache( ) await handler.fetch_experiences() + if exp := await handler.get_one_perfect_exp(): return exp await handler.execute_function() - await handler.process_experience() + + if config.exp_pool.enable_write: + await handler.process_experience() return handler._raw_resp @@ -117,7 +121,7 @@ class ExpCacheHandler(BaseModel): self.serializer = self.serializer or SimpleSerializer() self.tag = self.tag or self._generate_tag() - self._req = self.serializer.serialize_req(copy.deepcopy(self.kwargs["req"])) + self._req = self.serializer.serialize_req(self.kwargs["req"]) return self @@ -140,7 +144,7 @@ class ExpCacheHandler(BaseModel): """Execute the function, and save resp.""" self._raw_resp = await self._execute_function() - self._resp = self.serializer.serialize_resp(copy.deepcopy(self._raw_resp)) + self._resp = self.serializer.serialize_resp(self._raw_resp) @handle_exception async def process_experience(self): diff --git a/metagpt/exp_pool/serializers/base.py b/metagpt/exp_pool/serializers/base.py index 82a0ed8c4..9d00a05b2 100644 --- a/metagpt/exp_pool/serializers/base.py +++ b/metagpt/exp_pool/serializers/base.py @@ -11,11 +11,18 @@ class BaseSerializer(BaseModel, ABC): @abstractmethod def serialize_req(self, req: Any) -> str: - """Serializes the request for storage.""" + """Serializes the request for storage. + + Do not modify req. If modification is necessary, use copy.deepcopy to create a copy first. + Note that copy.deepcopy may raise errors, such as TypeError: cannot pickle '_thread.RLock' object. + """ @abstractmethod def serialize_resp(self, resp: Any) -> str: - """Serializes the function's return value for storage.""" + """Serializes the function's return value for storage. + + Do not modify resp. The rest is the same as `serialize_req`. + """ @abstractmethod def deserialize_resp(self, resp: str) -> Any: diff --git a/metagpt/exp_pool/serializers/role_zero.py b/metagpt/exp_pool/serializers/role_zero.py index 75e5d5ecb..7876ef12a 100644 --- a/metagpt/exp_pool/serializers/role_zero.py +++ b/metagpt/exp_pool/serializers/role_zero.py @@ -1,5 +1,6 @@ """RoleZero Serializer.""" +import copy import json from metagpt.exp_pool.context_builders import RoleZeroContextBuilder @@ -27,7 +28,8 @@ class RoleZeroSerializer(SimpleSerializer): if not req: return "" - last_content = req[-1]["content"] + req_copy = copy.deepcopy(req) + last_content = req_copy[-1]["content"] last_content = RoleZeroContextBuilder.replace_content_between_markers( last_content, "# Data Structure", "# Current Plan", "" ) @@ -35,6 +37,6 @@ class RoleZeroSerializer(SimpleSerializer): last_content, "# Example", "# Instruction", "" ) - req[-1]["content"] = last_content + req_copy[-1]["content"] = last_content - return json.dumps(req) + return json.dumps(req_copy) diff --git a/metagpt/strategy/experience_retriever.py b/metagpt/strategy/experience_retriever.py index 7bcd4be11..32f5c2316 100644 --- a/metagpt/strategy/experience_retriever.py +++ b/metagpt/strategy/experience_retriever.py @@ -798,13 +798,13 @@ Explanation: I will first need to read the system design document and the projec { "command_name": "Editor.read", "args": { - "path": "/tmp/docs/project_schedule.json" + "path": "/tmp/project_schedule.json" } }, { "command_name": "Editor.read", "args": { - "path": "/tmp/docs/system_design.json" + "path": "/tmp/system_design.json" } } ] From 1445d1f3763b70283ade73723308186b5f504f12 Mon Sep 17 00:00:00 2001 From: shenchucheng Date: Thu, 11 Jul 2024 14:08:27 +0800 Subject: [PATCH 076/119] update cr --- metagpt/ext/cr/actions/code_review.py | 17 +- metagpt/ext/cr/actions/modify_code.py | 7 +- metagpt/ext/cr/points.json | 1318 ++++++++++++------------- metagpt/tools/libs/cr.py | 17 +- 4 files changed, 683 insertions(+), 676 deletions(-) diff --git a/metagpt/ext/cr/actions/code_review.py b/metagpt/ext/cr/actions/code_review.py index 5586567fa..2cbeeb3ee 100644 --- a/metagpt/ext/cr/actions/code_review.py +++ b/metagpt/ext/cr/actions/code_review.py @@ -57,7 +57,7 @@ Just print the PR Patch comments in json format like **Output Format**. """ CODE_REVIEW_COMFIRM_SYSTEM_PROMPT = """ -You are a professional engineer with Java stack, and good at code review comment result judgement. +You are a professional engineer with {code_language} stack, and good at code review comment result judgement. """ CODE_REVIEW_COMFIRM_TEMPLATE = """ @@ -132,13 +132,20 @@ class CodeReview(Action): code = get_code_block_from_patch( patch, str(max(1, int(code_start_line) - 5)), str(int(code_end_line) + 5) ) + code_language = "Java" + code_file_ext = cmt.get("commented_file", ".java").split(".")[-1] + if code_file_ext == ".java": + code_language = "Java" + elif code_file_ext == ".py": + code_language = "Python" prompt = CODE_REVIEW_COMFIRM_TEMPLATE.format( code=code, comment=cmt.get("comment"), desc=point.text, example=point.yes_example + "\n" + point.no_example, ) - resp = await self.llm.aask(prompt, system_msgs=[CODE_REVIEW_COMFIRM_SYSTEM_PROMPT]) + system_prompt = [CODE_REVIEW_COMFIRM_SYSTEM_PROMPT.format(code_language=code_language)] + resp = await self.llm.aask(prompt, system_msgs=system_prompt) if "True" in resp or "true" in resp: new_comments.append(cmt) logger.info(f"original comments num: {len(comments)}, confirmed comments num: {len(new_comments)}") @@ -163,7 +170,11 @@ class CodeReview(Action): prompt = CODE_REVIEW_PROMPT_TEMPLATE.format(patch=str(patched_file), points=points_str) resp = await self.llm.aask(prompt) json_str = parse_json_code_block(resp)[0] - comments += json.loads(json_str) + comment = json.loads(json_str) + patched_file_path = patched_file.path + for c in comment: + c["commented_file"] = patched_file_path + comments += comment return comments diff --git a/metagpt/ext/cr/actions/modify_code.py b/metagpt/ext/cr/actions/modify_code.py index 33a368463..2c54cdbae 100644 --- a/metagpt/ext/cr/actions/modify_code.py +++ b/metagpt/ext/cr/actions/modify_code.py @@ -81,17 +81,18 @@ class ModifyCode(Action): } resp = None for patched_file in patch: - patch_target_file_name = str(patched_file.target_file).split("/", maxsplit=1)[-1] - if patch_target_file_name not in grouped_comments: + patch_target_file_name = str(patched_file.path).split("/")[-1] + if patched_file.path not in grouped_comments: continue comments_prompt = "" index = 1 - for grouped_comment in grouped_comments[patch_target_file_name]: + for grouped_comment in grouped_comments[patched_file.path]: comments_prompt += f""" {grouped_comment} \n """ + index += 1 prompt = MODIFY_CODE_PROMPT.format(patch=patched_file, comments=comments_prompt) output_dir = ( Path(output_dir) diff --git a/metagpt/ext/cr/points.json b/metagpt/ext/cr/points.json index b0497cb7b..5455d3865 100644 --- a/metagpt/ext/cr/points.json +++ b/metagpt/ext/cr/points.json @@ -1,664 +1,656 @@ [ - { - "id": 1, - "text": "避免未使用的临时变量", - "language": "java", - "detail": "缺陷类型:避免未使用的临时变量;对应Fixer:UnusedLocalVariableFixer;修复方案:删除未使用的临时变量", - "yes_example": "### 被判定为\"避免未使用的临时变量\"的例子\n<例子1>\npublic String initCreationForm(Map model) {\n\t\tOwner owner = new Owner();\n\t\tmodel.put(\"owner\", owner);\n\t\tint unusedVar = 10;\n\t\treturn VIEWS_OWNER_CREATE_OR_UPDATE_FORM;\n\t}\n上述代码中unusedVar变量未被使用,所以这个被判定为\"避免未使用的临时变量\"\n\n<例子2>\nint unusedVariable = 10;\nSystem.out.println(\"Hello, World!\");\n这段代码的变量\"unusedVariable\"未被使用或者引用,所以这个不能判定为\"避免未使用的临时变量\"\n", - "no_example": "### 不能被判定为\"避免未使用的临时变量\"的例子\n<例子1>\npublic void setTransientVariablesLocal(Map transientVariables) {\nthrow new UnsupportedOperationException(\"No execution active, no variables can be set\");\n}\n这段代码的\"transientVariables\"是函数参数而不是临时变量,虽然transientVariables没有被使用或者引用,但是这个也不能判定为\"避免未使用的临时变量\"\n\n\n<例子2>\npublic class TriggerCmd extends NeedsActiveExecutionCmd {\n protected Map transientVariables;\n public TriggerCmd(Map transientVariables) {\n this.transientVariables = transientVariables;\n }\n}\n上述代码中transientVariables不属于临时变量,它是类属性,且它在构造函数中被使用,所以这个不能被判定为\"避免未使用的临时变量\"\n" - }, - { - "id": 2, - "text": "不要使用 System.out.println 去打印", - "language": "java", - "detail": "缺陷类型:不要使用 System.out.println 去打印;对应Fixer:SystemPrintlnFixer;修复方案:注释System.out.println代码", - "yes_example": "### 被判定为\"不要使用 System.out.println 去打印\"的例子\n<例子1>\nSystem.out.println(\"Initializing new owner form.\");\n上述代码使用了\"System.out.println\"进行打印,所以这个被判定为\"不要使用 System.out.println 去打印\"\n", - "no_example": "### 不能被判定为\"不要使用 System.out.println 去打印\"的例子\n<例子1>\nthrow new IllegalStateException(\"There is no authenticated user, we need a user authenticated to find tasks\");\n上述代码是抛出异常的代码,没有使用\"System.out.print\",所以这个不能被判定为\"不要使用 System.out.println 去打印\"\n" - }, - { - "id": 3, - "text": "避免函数中未使用的形参", - "language": "java", - "detail": "缺陷类型:避免函数中未使用的形参;修复方案:忽略", - "yes_example": "### 被判定为\"避免函数中未使用的形参\"的例子\n<例子1>\npublic void setTransientVariablesLocal(Map transientVariables) {\n throw new UnsupportedOperationException(\"No execution active, no variables can be set\");\n}这段代码中的形参\"transientVariables\"未在函数体内出现,所以这个被判定为\"避免函数中未使用的形参\"\n\n\n<例子2>\nprotected void modifyFetchPersistencePackageRequest(PersistencePackageRequest ppr, Map pathVars) {}\n这段代码中的形参\"ppr\"和\"pathVars\"未在函数体内出现,所以这个被判定为\"避免函数中未使用的形参\"\n", - "no_example": "### 不能被判定为\"避免函数中未使用的形参\"的例子\n<例子1>\npublic String processFindForm(@RequestParam(value = \"pageNo\", defaultValue = \"1\") int pageNo) {\n\tlastName = owner.getLastName();\n\treturn addPaginationModel(pageNo, paginationModel, lastName, ownersResults);\n}上述代码中的形参\"pageNo\"在当前函数'processFindForm'内被'return addPaginationModel(pageNo, paginationModel, lastName, ownersResults);'这一句被使用,虽然pageNo没有被用于逻辑计算,但作为了函数调用其他函数的参数使用了,所以这个不能被判定为\"避免函数中未使用的形参\"\n" - }, - { - "id": 4, - "text": "if语句块不能为空", - "language": "java", - "detail": "缺陷类型:if 语句块不能为空;对应Fixer:EmptyIfStmtFixer;修复方案:删除if语句块 或 适当的逻辑处理 或 注释说明为何为空", - "yes_example": "### 被判定为\"if语句块不能为空\"的例子\n<例子1>\npublic void emptyIfStatement() {\n\tif (getSpecialties().isEmpty()) {\n\t}\n}这段代码中的if语句块内容是空的,所以这个被判定为\"if语句块不能为空\"\n\n\n<例子2>\npublic void judgePersion() {\n\tif (persion != null) {\n\t\t// judge persion if not null\n\t}\n}\n这段代码中的if语句块虽然有内容,但是\"// judge persion if not null\"只是代码注释,if语句块内并没有实际的逻辑代码,所以这个被判定为\"if语句块不能为空\"\n", - "no_example": "### 不能被判定为\"if语句块不能为空\"的例子\n<例子1>\npublic void judgePersion() {\n\tif (persion != null) {\n\t\treturn 0;\n\t}\n}这段代码中的if语句块里有内容,且里面有非注释代码的逻辑代码\"return 0;\",所以这个不能被判定为\"if语句块不能为空\"\n" - }, - { - "id": 5, - "text": "循环体不能为空", - "language": "java", - "detail": "缺陷类型:循环体不能为空;对应Fixer:EmptyStatementNotInLoopFixer;修复方案:删除对应while、for、foreach 循环体 或 添加适当的逻辑处理或者注释说明为何为空", - "yes_example": "### 被判定为\"循环体不能为空\"的例子\n<例子1>\npublic void emptyLoopBody() {\n\tfor (Specialty specialty : getSpecialties()) {\n\t}\n}这段代码中的for循环体的内容是空的,所以这个被判定为\"循环体不能为空\"\n\n\n<例子2>\npublic void emptyLoopBody() {\n\twhile (True) {\n\t\t// this is a code example\n\t}\n}这段代码中的while循环体的内容虽然不是空的,但内容只是代码注释,无逻辑内容,所以这个被判定为\"循环体不能为空\"\n\n\n<例子3>\npublic void emptyLoopBody() {\n\twhile (True) {\n\t\t\n\t}\n}这段代码中的while循环体内容是空的,所以这个被判定为\"循环体不能为空\"\n", - "no_example": "### 不能被判定为\"循环体不能为空\"的例子\n<例子1>\npublic void emptyLoopBody() {\n\tfor (Specialty specialty : getSpecialties()) {\n\t\ta = 1;\n\t\tif (a == 1) {\n\t\t\tretrun a;\n\t\t}\n\t}\n}上述代码的for循环体的内容不为空,且内容不全是代码注释,所以这个不能被判定为\"循环体不能为空\"\n" - }, - { - "id": 6, - "text": "避免使用 printStackTrace(),应该使用日志的方式去记录", - "language": "java", - "detail": "缺陷类型:避免使用 printStackTrace(),应该使 用日志的方式去记录;修复方案:用日志的方式去记录", - "yes_example": "### 被判定为\"避免使用 printStackTrace(),应该使用日志的方式去记录\"的例子\n<例子1>\npublic void usePrintStackTrace() {\n\ttry {\n\t\tthrow new Exception(\"Fake exception\");\n\t} catch (Exception e) {\n\t\te.printStackTrace();\n\t}\n}这段代码中的catch语句中使用了printStackTrace(),所以这个被判定为\"避免使用 printStackTrace(),应该使用日志的方式去记录\"\n", - "no_example": "### 不能被判定为\"避免使用 printStackTrace(),应该使用日志的方式去记录\"的例子\n<例子1>\npublic void usePrintStackTrace() {\n\ttry {\n\t\tthrow new Exception(\"Fake exception\");\n\t} catch (Exception e) {\n\t\tlogging.info(\"info\");\n\t}\n}这段代码的catch语句中使用的是日志记录的方式,所以这个不能被判定为\"避免使用 printStackTrace(),应该使用日志的方式去记录\"\n" - }, - { - "id": 7, - "text": "catch 语句块不能为空", - "language": "java", - "detail": "缺陷类型:catch 语句块不能为空;对应Fixer:EmptyCatchBlockFixer;修复方案:在catch里面添加注释", - "yes_example": "### 被判定为\"catch语句块不能为空\"的例子\n<例子1>\ntry {\n int[] array = new int[5];\n int number = array[10];\n} catch (ArrayIndexOutOfBoundsException e) {\n \n}\n这段代码中的catch语句中没有内容,所以这个被判定为\"catch语句块不能为空\"\n\n\n<例子2>\ntry {\n String str = null;\n str.length();\n} catch (NullPointerException e) {\n \n}这段代码中的catch语句中没有内容,所以这个被判定为\"catch语句块不能为空\"\n\n\n<例子3>\npublic class EmptyCatchExample {\n public static void main(String[] args) {\n try {\n // 尝试除以零引发异常\n int result = 10 / 0;\n } catch (ArithmeticException e) {\n \n }\n }\n}这段代码中的catch语句中没有内容,所以这个被判定为\"catch语句块不能为空\"\n\n<例子4>\ntry {\n FileReader file = new FileReader(\"nonexistentfile.txt\");\n} catch (FileNotFoundException e) {\n \n}这段代码中的catch语句中没有内容,所以这个被判定为\"catch语句块不能为空\"\n\n<例子5>\ntry {\n Object obj = \"string\";\n Integer num = (Integer) obj;\n} catch (ClassCastException e) {\n\t\n}这段代码中的catch语句中没有内容,所以这个被判定为\"catch语句块不能为空\"\n", - "no_example": "### 不能被判定为\"catch语句块不能为空\"的例子\n<例子1>\npersionNum = 1\ntry {\n\treturn True;\n} catch (Exception e) {\n\t// 如果人数为1则返回false\n\tif (persionNum == 1){\n\t\treturn False;\n\t}\n}这段代码的catch语句中不为空,所以不能把这个被判定为\"catch语句块不能为空\"\n\n\n<例子2>\ntry {\n\tthrow new Exception(\"Fake exception\");\n} catch (Exception e) {\n\te.printStackTrace();\n}这段代码的catch语句中虽然只有\"e.printStackTrace();\"但确实不为空,所以不能把这个被判定为\"catch语句块不能为空\"\n" - }, - { - "id": 8, - "text": "避免不必要的永真/永假判断", - "language": "java", - "detail": "缺陷类型:避免不必要的永真/永假判断;对应Fixer:UnconditionalIfStatement Fixer;修复方案:删除永真/永假判断逻辑", - "yes_example": "### 被判定为\"避免不必要的永真/永假判断\"的例子\n<例子1>\npublic void someMethod() {\n\twhile (true) {\n\t}\n}这段代码中的\"while (true)\"是一个使用true做判断条件,但是没有循环结束标记,所以这个被判定为\"避免不必要的永真/永假判断\"\n\n\n<例子2>\nif (true) {\n\tSystem.out.println(\"This is always true\");\n}这段代码中的\"if (true)\"是一个使用true条件做条件,但是没有循环结束标记,所以这个被判定为\"避免不必要的永真/永假判断\"\n\n\n<例子3>\na = 1;\nwhile(a > 0){\n\ta = a + 1\n}这段代码初始化a=1,是大于0的,while循环体的逻辑是每次加1,那么判断条件a > 0会永远是真的,不会退出循环,所以这个被判定为\"避免不必要的永真/永假判断\"\n<例子3>", - "no_example": "### 不能被判定为\"避免不必要的永真/永假判断\"的例子\n<例子1>\na = 0;\nwhile (a < 5) {\n\ta = a + 1;\n}这段代码中的a<5是一个判断,当执行了5次while语句中的逻辑a=a+1之后,a会满足a < 5,就会退出循环,所以这个能被判定为\"避免不必要的永真/永假判断\"\n" - }, - { - "id": 9, - "text": "switch 中 default 必须放在最后", - "language": "java", - "detail": "缺陷类型:switch 中 default 必须放在最后;对应Fixer:DefaultLabelNotLastInSwitchStmtFixer;修复方案:switch 中 default 放在最后", - "yes_example": "### 被判定为\"switch 中 default 必须放在最后\"的例子\n<例子1>\nswitch (number) {\n\tdefault:\n\t\tSystem.out.println(\"This is the default block, which is incorrectly placed here.\");\n\t\tbreak;\n\tcase 1:\n\t\tSystem.out.println(\"Number one\");\n\t\tbreak;\n\tcase 2:\n\t\tSystem.out.println(\"Number two\");\n\t\tbreak;\n}这段代码是一个switch语句,但是里面的default没有放在最后,所以这个被判定为\"switch 中 default 必须放在最后\"\n", - "no_example": "### 不能被判定为\"switch 中 default 必须放在最后\"的例子\n<例子1>\nswitch (number) {\ncase 3:\n\tSystem.out.println(\"Number one\");\n\tbreak;\ncase 4:\n\tSystem.out.println(\"Number two\");\n\tbreak;\ndefault:\n\tSystem.out.println(\"This is the default block, which is incorrectly placed here.\");\n\tbreak;\n}这段代码是一个switch语句且里面的default放在了最后,所以这个不能被判定为\"switch 中 default 必须放在最后\"\n" - }, - { - "id": 10, - "text": "未使用equals()函数对 String 作比较", - "language": "java", - "detail": "缺陷类型:未使用equals()函数对 String 作比较;对应Fixer:UnSynStaticDateFormatter Fixer;修复方案:使用equals()函数对 String 作比较", - "yes_example": "### 被判定为\"未使用equals()函数对 String 作比较\"的例子\n<例子1>\nif (existingPet != null && existingPet.getName() == petName) {\n\tresult.rejectValue(\"name\", \"duplicate\", \"already exists\");\n}这段代码中所涉及的existingPet.getName()和petName均是字符串,但是在if语句里做比较的时候使用了==而没有使用equals()对string做比较,所以这个被判定为\"未使用equals()函数对 String 作比较\"\n\n\n<例子2>\nString isOk = \"ok\";\nif (\"ok\" == isOk) {\n\tresult.rejectValue(\"name\", \"duplicate\", \"already exists\");\n}这段代码中的isOk是个字符串,但在if判断中与\"ok\"比较的时候使用的是==,未使用equals()对string做比较,所以这个被判定为\"未使用equals()函数对 String 作比较\"\n", - "no_example": "### 不能被判定为\"未使用equals()函数对 String 作比较\"的例子\n<例子1>\nif (PROPERTY_VALUE_YES.equalsIgnoreCase(readWriteReqNode))\n formProperty.setRequired(true);\n这段代码中的PROPERTY_VALUE_YES和readWriteReqNode均是字符串,在if语句里比较PROPERTY_VALUE_YES和readWriteReqNode的使用的是equalsIgnoreCase(字符串比较忽略大小写),所以equalsIgnoreCase也是符合使用equals()函数对 String 作比较的,所以这个不能被判定为\"未使用equals()函数对 String 作比较\"\n\n\n<例子2>\nString isOk = \"ok\";\nif (\"ok\".equals(isOk)) {\n\tresult.rejectValue(\"name\", \"duplicate\", \"already exists\");\n}这段代码中的isOk是个字符串,在if判断中与\"ok\"比较的时候使用的是equals()对string做比较,所以这个不能被判定为\"未使用equals()函数对 String 作比较\"\n" - }, - { - "id": 11, - "text": "禁止在日志中直接使用字符串输出异常,请使用占位符传递异常对象", - "language": "java", - "detail": "缺陷类型:禁止在日志中直接使用字符串输出异常,请使用占位符传递异常对象 输出异常;对应Fixer:ConcatExceptionFixer;修复方案:使用占位符传递异常对象", - "yes_example": "### 被判定为\"禁止在日志中直接使用字符串输出异常,请使用占位符传递异常对象\"的例子\n<例子1>\ntry {\n listenersNode = objectMapper.readTree(listenersNode.asText());\n} catch (Exception e) {\n LOGGER.info(\"Listeners node can not be read\", e);\n}这段代码中日志输出内容内容是直接使用字符串\"Listeners node can not be read\"拼接,日志输出异常时,应使用占位符输出异常信息,而不是直接使用字符串拼接,所以这个被判定为\"禁止在日志中直接使用字符串输出异常,请使用占位符传递异常对象\"\n", - "no_example": "### 不能被判定为\"禁止在日志中直接使用字符串输出异常,请使用占位符传递异常对象\"的例子\n<例子1>\nPersion persion = persionService.getPersion(1);\nif (persion == null){\n\tLOGGER.error(PERSION_NOT_EXIT);\n}这段代码中的PERSION_NOT_EXIT是一个用户自定义的异常常量,代表persion不存在,没有直接使用字符串\"persion not exit\"拼接,所以这个不能被判定为\"禁止在日志中直接使用字符串输出异常,请使用占位符传递异常对象\"\n<例子1>\n\n<例子2>\ntry {\n a = a + 1;\n} catch (Exception e) {\n Persion persion = persionService.getPersion(1);\n LOGGER.info(persion);\n}这段代码中输出日志没有直接使用字符串拼接,而是使用的Persion对象输出,所以这个不能被判定为\"禁止在日志中直接使用字符串输出异常,请使用占位符传递异常对象\"\n" - }, - { - "id": 12, - "text": "finally 语句块不能为空", - "language": "java", - "detail": "缺陷类型:finally 语句块不能为空;对应Fixer:EmptyFinallyBlockFixer;修复方案:删除空 finally 语句块", - "yes_example": "### 被判定为\"finally 语句块不能为空\"的例子\n<例子1>\ntry {\n\tPersion persion = persionService.getPersion(1);\n\treturn persion;\n} finally {\n\t\n}这段代码中的finally语句块内没有内容,所以这个被判定为\"finally 语句块不能为空\"\n\n\n<例子2>\ntry {\n\tSystem.out.println(\"Inside try block\");\n} finally {\n\t// 空的finally块,没有任何语句,这是一个缺陷\n}这段代码中的finally语句块内没有内容,所以这个被判定为\"finally 语句块不能为空\"\n\n\n<例子3>\ntry {\n int result = 10 / 0;\n} catch (ArithmeticException e) {\n e.printStackTrace();\n} finally {\n \n}这段代码中的finally语句块内没有内容,所以这个被判定为\"finally 语句块不能为空\"\n\n\n<例子4>\ntry {\n String str = null;\n System.out.println(str.length());\n} catch (NullPointerException e) {\n e.printStackTrace();\n} finally {\n \n}这段代码中的finally语句块内没有内容,所以这个被判定为\"finally 语句块不能为空\"\n\n\n<例子5>\ntry {\n int[] array = new int[5];\n int number = array[10];\n} catch (ArrayIndexOutOfBoundsException e) {\n e.printStackTrace();\n} finally {\n // 只有注释的 finally 语句块\n // 这是一个空的 finally 块\n}这段代码中的finally语句块内没有内容,所以这个被判定为\"finally 语句块不能为空\"\n\n\n<例子6>\ntry {\n FileReader file = new FileReader(\"nonexistentfile.txt\");\n} catch (FileNotFoundException e) {\n e.printStackTrace();\n} finally {\n // 只有空行的 finally 语句块\n \n}这段代码中的finally语句块内没有内容,所以这个被判定为\"finally 语句块不能为空\"\n", - "no_example": "### 不能被判定为\"finally 语句块不能为空\"的例子\n<例子1>\npublic void getPersion() {\n\ttry {\n\t\tPersion persion = persionService.getPersion(1);\n\t\tif (persion != null){ \n\t\t\treturn persion;\n\t\t}\n\t} finally {\n\t\treturn null;\n\t}\n}这段代码中的finally语句块中有非注释意外的内容\"return null;\",所以这个不能被判定为\"finally 语句块不能为空\"\n" - }, - { - "id": 13, - "text": "try 语句块不能为空", - "language": "java", - "detail": "缺陷类型:try 语句块不能为空;对应Fixer:EmptyTryBlockFixer;修复方案:删除整个 try 语句", - "yes_example": "### 被判定为\"try 语句块不能为空\"的例子\n<例子1>\npublic void getPersion() {\n\ttry {\n\n\t}\n\treturn null;\n}这段代码中的try语句块内没有内容,所以这个被判定为\"try 语句块不能为空\"\n\n\n<例子2>\npublic void demoFinallyBlock() {\n\ttry {\n\n\t} finally {\n\t\treturn null;\n\t}\n}这段代码中的try语句块内没有内容,所以这个被判定为\"try 语句块不能为空\"\n\n\n<例子3>\ntry {\n \n} catch (Exception e) {\n e.printStackTrace();\n}这段代码中的try语句块内没有内容,所以这个被判定为\"try 语句块不能为空\"\n\n\n<例子4>\ntry {\n // 只有注释的 try 语句块\n\t\n} catch (Exception e) {\n e.printStackTrace();\n}这段代码中的try语句块内只有注释和空行,也可以认定为这种情况是try语句块内没有内容,所以这个被判定为\"try 语句块不能为空\"\n", - "no_example": "### 不能被判定为\"try 语句块不能为空\"的例子\n<例子1>\ntry {\n\ta = a + 1;\n} catch (Exception e) {\n\te.printStackTrace();\n}\n这段代码中的try语句块中有非注释意外的内容\"return null;\",所以这个不能被判定为\"try 语句块不能为空\"\n" - }, - { - "id": 14, - "text": "避免对象进行不必要的 NULL或者null 检查", - "language": "java", - "detail": "缺陷类型:避免对象进行不必要的 NULL或者null 检查;对应Fixer:LogicalOpNpeFixer;修复方案:删除对对象不必要的 NULL 检查的逻辑", - "yes_example": "### 被判定为\"避免对象进行不必要的 NULL或者null 检查\"的例子\n<例子1>\na = \"dog\";\nif (a != null){\n\treturn a;\n}这段代码中的对象a已经是确定的值\"dog\",所以if条件句的判断\"a != null\"是不必要的,所以这个被判定为\"避免对象进行不必要的 NULL或者null 检查\"\n\n\n<例子2>\nif (authenticatedUserId != null && !authenticatedUserId.isEmpty() && userGroupManager!=null){\n\treturn authenticatedUserId;\n}这段代码中的\"authenticatedUserId != null\"和\"!authenticatedUserId.isEmpty()\"都是对\"authenticatedUserId\"的空判断,重复了,所以这个被判定为\"避免对象进行不必要的 NULL或者null 检查\"\n\n\n<例子3>\nList list = new ArrayList<>();\nif (list != null) {\n list.add(1);\n}这段代码中的list已经被初始化,不需要进行 null 检查,所以这个被判定为\"避免对象进行不必要的 NULL或者null 检查\"\n\n\n<例子4>\nif (this.type != null && this.type.getName() != null) {\n\tSystem.out.println(\"Type name is not null\");\n}这段代码中的对象type已经检查过非null,再次检查getName()是否为null是不必要的,所以这个被判定为\"避免对象进行不必要的 NULL或者null 检查\"\n\n\n\n<例子5>\nif (\"dog\".equals(null)){\n\treturn a;\n}这段代码中的\"dog\"是个确定的字符串,不需要进行null 检查,所以这个被判定为\"避免对象进行不必要的 NULL或者null 检查\"\n\n\n<例子6>\nInteger num = 10;\nif (num != null) {\n System.out.println(num);\n}这段代码中的num 已经被初始化,不需要进行 null 检查,所以这个被判定为\"避免对象进行不必要的 NULL或者null 检查\"\n", - "no_example": "### 不能被判定为\"避免对象进行不必要的 NULL或者null 检查\"的例子\n<例子1>\nCat cat = catService.get(1);\nif (cat != null){\n\tretrun cat;\n}这段代码中的对象\"cat\"是通过service获取到的,不确定是否为空,所以if条件句的判断的\"cat != null\"是必要的,所以这个不能被判定为\"避免对象进行不必要的 NULL或者null 检查\"\n" - }, - { - "id": 15, - "text": "避免 finally 块中出现 return", - "language": "java", - "detail": "缺陷类型:避免 finally 块中出现 return;修复方案:无需修复", - "yes_example": "### 被判定为\"避免 finally 块中出现 return\"的例子\n<例子1>\npublic void getPersion() {\n\ttry {\n\t\tPersion persion = persionService.getPersion(1);\n\t\tif (persion != null){ \n\t\t\treturn persion;\n\t\t}\n\t} finally {\n\t\treturn null;\n\t}\n}这段代码中的finally语句块内容包含\"return\",所以这个被判定为\"避免 finally 块中出现 return\"\n", - "no_example": "### 不能被判定为\"避免 finally 块中出现 return\"的例子\n<例子1>\npublic void getPersion() {\n\ttry {\n\t\tPersion persion = persionService.getPersion(1);\n\t\tif (persion != null){ \n\t\t\treturn persion;\n\t\t}\n\t} finally {\n\t\tLOGGER.info(PERSION_NOT_EXIT);\n\t}\n}这段代码中的finally语句块中内容不包含\"return\",所以这个不能被判定为\"避免 finally 块中出现 return\"\n" - }, - { - "id": 16, - "text": "避免空的 static 初始化", - "language": "java", - "detail": "缺陷类型:避免空的 static 初始化;对应Fixer:EmptyInitializerFixer;修复方案:删除整个空初始化块", - "yes_example": "### 被判定为\"避免空的 static 初始化\"的例子\n<例子1>\npublic class PetValidator implements Validator {\n\tstatic {\n\n\t}\n}这段代码中的static语句块没有内容,是空的,所以这个被判定为\"避免空的 static 初始化\"\n\n\n<例子2>\npublic class Persion {\n\tstatic {\n\t\t// 初始化的静态块\n\t}\n}这段代码中的static语句块是有内容的,不是空的,但是static初始化语句块中只有注释代码,没有实际的逻辑,所以这个被判定为\"避免空的 static 初始化\"\n", - "no_example": "### 不能被判定为\"避免空的 static 初始化\"的例子\n<例子1>\npublic class Cat {\n\tstatic {\n\t\t// 初始化的静态块\n\t\tcat = null;\n\t}\n}这段代码中的static语句块是有内容的,不是空的,且static初始化语句块中有非注释代码,有实际的逻辑,所以这个不能被判定为\"避免空的 static 初始化\"\n" - }, - { - "id": 17, - "text": "避免日历类用法不当风险", - "language": "java", - "detail": "缺陷类型:避免日历类用法不当风险;修复方案:使用Java 8 及以上版本中的 java.time 包的LocalDate", - "yes_example": "### 被判定为\"避免日历类用法不当风险\"的例子\n<例子1>\nprivate static final Calendar calendar = new GregorianCalendar(2020, Calendar.JANUARY, 1);\n这段代码中的Calendar和GregorianCalendar是线程不安全的,所以这个被判定为\"避免日历类用法不当风险\"\n", - "no_example": "### 不能被判定为\"避免日历类用法不当风险\"的例子\n<例子1>\nprivate static final LocalDate calendar = LocalDate.of(2020, 1, 1);\n这段代码中的LocalDate使用的是Java 8 及以上版本中的 java.time 包,LocalDate 是不可变的并且是线程安全的,不会有线程安全和性能方面的问题,所以这个不能被判定为\"避免日历类用法不当风险\"\n" - }, - { - "id": 18, - "text": "使用集合转数组的方法,必须使用集合的toArray(T[]array),传入的是类型完全一样的数组,大小就是list.size()", - "language": "java", - "detail": "缺陷类型:使用集合转数组的方法,必须使用集合的toArray(T[]array),传入的是类型完全一样的数组,大小就是list.size();对应Fixer:ClassCastExpWithToArrayF ixer;修复方案:使用集合的toArray(T[]array),且传入的是类型完全一样的数组", - "yes_example": "### 被判定为\"使用集合转数组的方法,必须使用集合的toArray(T[]array),传入的是类型完全一样的数组,大小就是list.size()\"的例子\n<例子1>\nList stringList = new ArrayList<>();\nstringList.add(\"Apple\");\nstringList.add(\"Banana\");\nObject[] objectArray = stringList.toArray(new Object[5]);\n这段代码使用集合转数组的方法的时候使用了toArray(new Object[5]),但是传入的数组类型不一致,所以这个被判定为\"使用集合转数组的方法,必须使用集合的toArray(T[]array),传入的是类型完全一样的数组,大小就是list.size()\"\n", - "no_example": "### 不能被判定为\"使用集合转数组的方法,必须使用集合的toArray(T[]array),传入的是类型完全一样的数组,大小就是list.size()\"的例子\n<例子1>\nList stringList = new ArrayList<>();\nstringList.add(\"Apple\");\nstringList.add(\"Banana\");\nString[] stringArray = stringList.toArray(new String[stringList.size()]);\n这段代码使用集合转数组的方法的时候使用了toArray(new String[stringList.size()]),传入的是类型完全一样的数组,所以这个不能被判定为\"使用集合转数组的方法,必须使用集合的toArray(T[]array),传入的是类型完全一样的数组,大小就是list.size()\"\n" - }, - { - "id": 19, - "text": "禁止在 equals()中使用 NULL或者null 做比较", - "language": "java", - "detail": "缺陷类型:禁止在 equals()中使用 NULL或者null 做比较;对应Fixer:EqualsNullFixer;修复方案:使用Object的判空函数 做比较", - "yes_example": "### 被判定为\"禁止在 equals()中使用 NULL或者null 做比较\"的例子\n<例子1>\nif (\"test\".equals(null)) {\n\tSystem.out.println(\"test\");\n}这段代码中if条件中的代码\"test\".equals(null)使用equals()函数与null进行了比较,所以这个被判定为\"禁止在 equals()中使用 NULL或者null 做比较\"\n\n\n<例子2>\nif (!rangeValues[1].equals(\"null\")) {\n\tmaxValue = new BigDecimal(rangeValues[1]);\n}这段代码中if条件中的代码!rangeValues[1].equals(\"null\")使用equals()函数与Nnull进行了比较,所以这个被判定为\"禁止在 equals()中使用 NULL或者null 做比较\"\n\n\n<例子3>\nString str1 = \"example\";\nif (str1.equals(\"null\")) {\n System.out.println(\"str1 is null\");\n}这段代码中if条件中的代码str1.equals(null)使用equals()函数与null进行了比较,所以这个被判定为\"禁止在 equals()中使用 NULL或者null 做比较\"\n\n\n<例子4>\nString str3 = \"example\";\nif (str3 != null && str3.equals(\"null\")) {\n System.out.println(\"str3 is null\");\n}这段代码中if条件中的代码str3.equals(\"null\")使用equals()函数与\"null\"进行了比较,所以这个被判定为\"禁止在 equals()中使用 NULL或者null 做比较\"\n\n\n<例子5>\nInteger num1 = 10;\nif (num1.equals(null)) {\n System.out.println(\"num1 is null\");\n}这段代码中if条件中的代码num1.equals(null)使用equals()函数与\"null\"进行了比较,所以这个被判定为\"禁止在 equals()中使用 NULL或者null 做比较\"\n\n\n<例子6>\nObject obj = new Object();\nif (obj.equals(null)) {\n System.out.println(\"obj is null\");\n}这段代码中if条件中的代码obj.equals(null)使用equals()函数与\"null\"进行了比较,所以这个被判定为\"禁止在 equals()中使用 NULL或者null 做比较\"\n", - "no_example": "### 不能被判定为\"禁止在 equals()中使用 NULL或者null 做比较\"的例子\n<例子1>\na = \"test\";\nif (a.equals(\"test\")) {\n\tSystem.out.println(\"test\");\n}这段代码中if条件中的代码a.equals(\"test\")使用equals()函数与\"test\"进行了比较,所以这个不能被判定为\"禁止在 equals()中使用 NULL或者null 做比较\"\n" - }, - { - "id": 20, - "text": "switch 语句块不能为空", - "language": "java", - "detail": "缺陷类型:switch 语句块不能为空;对应Fixer:EmptySwitchStatementsFix;修复方案:删除整个空 switch 语句块", - "yes_example": "### 被判定为\"switch 语句块不能为空\"的例子\n<例子1>\nswitch (number) {\n\t\n}这段代码是一个switch语句块,但是里面没有内容,所以这个被判定为\"switch 语句块不能为空\"\n\n\n<例子2>\nswitch (number) {\n\t// 这是一个switch语句块\n}这段代码是一个switch语句块,里面虽然有内容,但是内容仅仅是注释内容,没有实际的逻辑,所以这个被判定为\"switch 语句块不能为空\"\n", - "no_example": "### 不能被判定为\"switch 语句块不能为空\"的例子\n<例子1>\nswitch (number) {\n\tcase 1:\n\t\tSystem.out.println(\"Number one\");\n\t\tbreak;\n\tdefault:\n\t\tSystem.out.println(\"This is the default block, which is incorrectly placed here.\");\n\t\tbreak;\n}这段代码是一个switch语句块,里面有内容,而且内容里有非注释的代码,有实际的逻辑,所以这个不能被判定为\"switch 语句块不能为空\"\n" - }, - { - "id": 21, - "text": "在进行类型强制转换时,右括号与强制转换值之间不需要任何空格隔开", - "detail": "缺陷类型:类型强制转换时空格使用不当;修复方案:在进行类型强制转换时,右括号与强制转换值之间不需要任何空格隔开。", - "language": "Java", - "yes_example": "### 被判定为\"在进行类型强制转换时,右括号与强制转换值之间不需要任何空格隔开\"的例子\n<例子1>\nint a = (int) 3.5;\n", - "no_example": "### 不能被判定为\"在进行类型强制转换时,右括号与强制转换值之间不需要任何空格隔开\"的例子\n<例子1>\nint a = (int)3.5;\n" - }, - { - "id": 22, - "text": "方法参数在定义和传入时,多个参数逗号后面必须加空格", - "detail": "缺陷类型:方法参数逗号后缺少空格;修复方案:方法参数在定义和传入时,多个参数逗号后面必须加空格。", - "language": "Java", - "yes_example": "### 被判定为\"方法参数在定义和传入时,多个参数逗号后面必须加空格\"的例子\n<例子1>\npublic void exampleMethod(int a,int b,int c) {}\n", - "no_example": "### 不能被判定为\"方法参数在定义和传入时,多个参数逗号后面必须加空格\"的例子\n<例子1>\npublic void exampleMethod(int a, int b, int c) {}\n" - }, - { - "id": 23, - "text": "禁止使用构造方法 BigDecimal(double) 的方式把 double 值转化为 BigDecimal 对象", - "detail": "缺陷类型:使用不推荐的 BigDecimal 构造方法;修复方案:禁止使用构造方法 BigDecimal(double) 的方式把 double 值转化为 BigDecimal 对象,推荐使用 BigDecimal 的 valueOf 方法。", - "language": "Java", - "yes_example": "### 被判定为\"禁止使用构造方法 BigDecimal(double) 的方式把 double 值转化为 BigDecimal 对象\"的例子\n<例子1>\nBigDecimal bd = new BigDecimal(0.1);\n", - "no_example": "### 不能被判定为\"禁止使用构造方法 BigDecimal(double) 的方式把 double 值转化为 BigDecimal 对象\"的例子\n<例子1>\nBigDecimal bd = BigDecimal.valueOf(0.1);\n" - }, - { - "id": 24, - "text": "不能有多余的分号", - "detail": "缺陷类型:多余的分号;修复方案:删除多余的分号", - "yes_example": "### 被判定为\"不能有多余的分号\"的例子\n<例子1>\npublic void trigger(String executionId, Map processVariables) {\n commandExecutor.execute(new TriggerCmd(executionId, processVariables));\n}\n;\na = 1;\nb = 2;\nsum = a + b;\n这段代码中包含一个多余的分号\";\",所以这个被判定为\"不能有多余的分号\"\n", - "no_example": "### 不能被判定为\"不能有多余的分号\"的例子\n<例子1>\nwhile (True) {\n\ta = a + 1;\n\tbreak;\n}这段代码每个分号都是必须要的,所以这个能被判定为\"不能有多余的分号\"\n" - }, - { - "id": 25, - "text": "非线程安全的 SimpleDateFormat 使用,必须在函数或代码块级别使用synchronized", - "detail": "缺陷类型:非线程安全的 SimpleDateFormat 使用;修复方案:在函数或代码块级别加上synchronized修饰 或 使用其他线程安全的方式", - "yes_example": "### 被判定为\"非线程安全的 SimpleDateFormat 使用,必须在函数或代码块级别使用synchronized\"的例子\n<例子1>\npublic void formatDate(Date date) {\n\tSimpleDateFormat sdf = new SimpleDateFormat(\"yyyy-MM-dd\");\n\tSystem.out.println(\"Formatted date: \" + sdf.format(date));\n}这段代码中的函数formatDate在未使用synchronized同步修饰的情况下使用了SimpleDateFormat,这是线程不安全的,所以这个被判定为\"非线程安全的 SimpleDateFormat 使用,必须在函数或代码块级别使用synchronized\"\n", - "no_example": "### 不能被判定为\"非线程安全的 SimpleDateFormat 使用,必须在函数或代码块级别使用synchronized\"的例子\n<例子1>\npublic synchronized void formatDate(Date date) {\n\tSimpleDateFormat sdf = new SimpleDateFormat(\"yyyy-MM-dd\");\n\tSystem.out.println(\"Formatted date: \" + sdf.format(date));\n}这段代码是在synchronized同步块对函数'formatDate'进行保护,保证了线程安全,所以这个不能被判定为\"非线程安全的 SimpleDateFormat 使用,必须在函数或代码块级别使用synchronized\"\n" - }, - { - "id": 26, - "text": "类名使用驼峰式UpperCamelCase风格, 方法名、参数名、成员变量、局部变量都统一使用lowerCamelCase风格", - "detail": "缺陷类型:命名规范;修复方案:类名使用UpperCamelCase风格,方法名、参数名、成员变量、局部变量使用lowerCamelCase风格。", - "language": "Java", - "yes_example": "### 被判定为命名规范的例子\n<例子1>\npublic class myClass {\n private int MyVariable;\n public void MyMethod() {}\n}\n这段代码中的类名、成员变量和方法名没有遵循驼峰命名法,所以被判定为命名规范问题。\n", - "no_example": "### 不能被判定为命名规范的例子\n<例子1>\npublic class MyClass {\n private int myVariable;\n public void myMethod() {}\n}\n这段代码中的类名、成员变量和方法名都遵循了驼峰命名法,所以不能被判定为命名规范问题。\n" - }, - { - "id": 27, - "text": "抽象类命名使用 Abstract 或 Base 开头;异常类命名使用 Exception 结尾,测试类命名以它要测试的类的名称开始,以 Test 结尾", - "detail": "缺陷类型:命名规范;修复方案:抽象类命名使用 Abstract 或 Base 开头,异常类命名使用 Exception 结尾,测试类命名以它要测试的类的名称开始,以 Test 结尾。", - "language": "Java", - "yes_example": "### 被判定为命名规范的例子\n<例子1>\npublic class MyAbstractClass {}\npublic class MyExceptionClass {}\npublic class TestMyClass {}\n这段代码中的抽象类、异常类和测试类的命名不符合规范,所以被判定为命名规范问题。\n", - "no_example": "### 不能被判定为命名规范的例子\n<例子1>\npublic abstract class AbstractMyClass {}\npublic class MyCustomException extends Exception {}\npublic class MyClassTest {}\n这段代码中的抽象类、异常类和测试类的命名都符合规范,所以不能被判定为命名规范问题。\n" - }, - { - "id": 28, - "text": "POJO 类中的任何布尔类型的变量,都不要加 is 前缀", - "detail": "缺陷类型:命名规范;修复方案:POJO 类中的布尔类型变量不要加 is 前缀。", - "language": "Java", - "yes_example": "### 被判定为命名规范的例子\n<例子1>\npublic class User {\n private boolean isActive;\n}\n这段代码中的布尔类型变量加了 is 前缀,所以被判定为命名规范问题。\n", - "no_example": "### 不能被判定为命名规范的例子\n<例子1>\npublic class User {\n private boolean active;\n}\n这段代码中的布尔类型变量没有加 is 前缀,所以不能被判定为命名规范问题。\n" - }, - { - "id": 29, - "text": "杜绝完全不规范的英文缩写,避免望文不知义。", - "detail": "缺陷类型:命名规范;修复方案:避免使用不规范的英文缩写,确保代码可读性。", - "language": "Java", - "yes_example": "### 被判定为命名规范的例子\n<例子1>\npublic class CfgMgr {\n private int cnt;\n}\n这段代码中的类名和变量名使用了不规范的英文缩写,所以被判定为命名规范问题。\n", - "no_example": "### 不能被判定为命名规范的例子\n<例子1>\npublic class ConfigManager {\n private int count;\n}\n这段代码中的类名和变量名没有使用不规范的英文缩写,所以不能被判定为命名规范问题。\n" - }, - { - "id": 30, - "text": "不允许任何魔法值(即未经预先定义的常量)直接出现在代码中", - "detail": "缺陷类型:代码规范;修复方案:将魔法值定义为常量。", - "language": "Java", - "yes_example": "### 被判定为代码规范的例子\n<例子1>\npublic class MagicNumberExample {\n public void calculate() {\n int result = 42 * 2;\n }\n}\n这段代码中直接使用了魔法值 42,所以被判定为代码规范问题。\n", - "no_example": "### 不能被判定为代码规范的例子\n<例子1>\npublic class MagicNumberExample {\n private static final int MULTIPLIER = 42;\n public void calculate() {\n int result = MULTIPLIER * 2;\n }\n}\n这段代码中将魔法值定义为了常量,所以不能被判定为代码规范问题。\n" - }, - { - "id": 31, - "text": "long 或 Long 赋值时,数值后使用大写 L,不能是小写 l,浮点数类型的数值后缀统一为大写的 D 或 F", - "detail": "缺陷类型:代码规范;修复方案:long 或 Long 赋值时使用大写 L,浮点数类型的数值后缀使用大写的 D 或 F。", - "language": "Java", - "yes_example": "### 被判定为代码规范的例子\n<例子1>\npublic class NumberExample {\n private long value = 1000l;\n private double pi = 3.14d;\n}\n这段代码中使用了小写的 l 和 d,所以被判定为代码规范问题。\n", - "no_example": "### 不能被判定为代码规范的例子\n<例子1>\npublic class NumberExample {\n private long value = 1000L;\n private double pi = 3.14D;\n}\n这段代码中使用了大写的 L 和 D,所以不能被判定为代码规范问题。\n" - }, - { - "id": 32, - "text": "如果大括号内为空,简洁地写成{}即可,大括号中间无需换行和空格;如果是非空代码块,则:1)左大括号前不换行。2)左大括号后换行。3)右大括号前换行。4)右大括号后还有 else 等代码则不换行;表示终止的右大括号后必须换行。", - "detail": "缺陷类型:代码格式;修复方案:遵循大括号的使用规范。", - "language": "Java", - "yes_example": "### 被判定为代码格式的例子\n<例子1>\npublic class BracketExample{public void method(){\n if (true) {\n }}\n}\n这段代码中的大括号使用不符合规范,所以被判定为代码格式问题。\n", - "no_example": "### 不能被判定为代码格式的例子\n<例子1>\npublic class BracketExample {\n public void method() {\n if (true) {\n // do something\n }\n }\n}\n这段代码中的大括号使用符合规范,所以不能被判定为代码格式问题。\n" - }, - { - "id": 33, - "text": "左小括号和右边相邻字符之间不需要空格;右小括号和左边相邻字符之间也不需要空格;而左大括号前需要加空格。", - "detail": "缺陷类型:代码格式;修复方案:遵循括号和空格的使用规范。", - "language": "Java", - "yes_example": "### 被判定为代码格式的例子\n<例子1>\npublic class SpaceExample {\n public void method (){\n }\n}\n这段代码中的括号和空格使用不符合规范,所以被判定为代码格式问题。\n", - "no_example": "### 不能被判定为代码格式的例子\n<例子1>\npublic class SpaceExample {\n public void method() {}\n}\n这段代码中的括号和空格使用符合规范,所以不能被判定为代码格式问题。\n" - }, - { - "id": 34, - "text": "if / for / while / switch / do 等保留字与左右括号之间都必须加空格。", - "detail": "缺陷类型:代码格式;修复方案:保留字与左右括号之间加空格。", - "language": "Java", - "yes_example": "### 被判定为代码格式的例子\n<例子1>\npublic class KeywordExample {\n public void method() {\n if(true) {\n }\n }\n}\n这段代码中的 if 关键字与括号之间没有空格,所以被判定为代码格式问题。\n", - "no_example": "### 不能被判定为代码格式的例子\n<例子1>\npublic class KeywordExample {\n public void method() {\n if (true) {\n }\n }\n}\n这段代码中的 if 关键字与括号之间有空格,所以不能被判定为代码格式问题。\n" - }, - { - "id": 35, - "text": "所有整型包装类对象之间值的比较,全部使用 equals 方法比较", - "detail": "缺陷类型:代码规范;修复方案:整型包装类对象之间的值比较使用 equals 方法。", - "language": "Java", - "yes_example": "### 被判定为代码规范的例子\n<例子1>\npublic class IntegerComparison {\n public void compare() {\n Integer a = 100;\n Integer b = 100;\n if (a == b) {\n }\n }\n}\n这段代码中使用了 == 比较整型包装类对象,所以被判定为代码规范问题。\n", - "no_example": "### 不能被判定为代码规范的例子\n<例子1>\npublic class IntegerComparison {\n public void compare() {\n Integer a = 100;\n Integer b = 100;\n if (a.equals(b)) {\n }\n }\n}\n这段代码中使用了 equals 方法比较整型包装类对象,所以不能被判定为代码规范问题。\n" - }, - { - "id": 36, - "text": "BigDecimal 的等值比较应使用 compareTo() 方法,而不是 equals() 方法。", - "detail": "缺陷类型:BigDecimal 等值比较错误;修复方案:使用 compareTo() 方法进行比较。", - "language": "Java", - "yes_example": "### 被判定为\"BigDecimal 的等值比较应使用 compareTo() 方法,而不是 equals() 方法\"的例子\n<例子1>\nBigDecimal a = new BigDecimal(\"1.0\");\nBigDecimal b = new BigDecimal(\"1.00\");\nif (a.equals(b)) {\n // 这段代码会返回 false,因为 equals() 方法会比较精度\n}\n", - "no_example": "### 不能被判定为\"BigDecimal 的等值比较应使用 compareTo() 方法,而不是 equals() 方法\"的例子\n<例子1>\nBigDecimal a = new BigDecimal(\"1.0\");\nBigDecimal b = new BigDecimal(\"1.00\");\nif (a.compareTo(b) == 0) {\n // 这段代码会返回 true,因为 compareTo() 方法只比较数值\n}\n" - }, - { - "id": 37, - "text": "禁止在 POJO 类中,同时存在对应属性 xxx 的 isXxx() 和 getXxx() 方法。", - "detail": "缺陷类型:POJO 类中存在重复的 getter 方法;修复方案:确保只存在一个 getter 方法。", - "language": "Java", - "yes_example": "### 被判定为\"禁止在 POJO 类中,同时存在对应属性 xxx 的 isXxx() 和 getXxx() 方法\"的例子\n<例子1>\npublic class User {\n private boolean active;\n public boolean isActive() {\n return active;\n }\n public boolean getActive() {\n return active;\n }\n}\n", - "no_example": "### 不能被判定为\"禁止在 POJO 类中,同时存在对应属性 xxx 的 isXxx() 和 getXxx() 方法\"的例子\n<例子1>\npublic class User {\n private int age;\n public int getAge() {\n return age;\n }\n}\n" - }, - { - "id": 38, - "text": "日期格式化时,传入 pattern 中表示年份统一使用小写的 y。", - "detail": "缺陷类型:日期格式化错误;修复方案:使用小写的 y 表示年份。", - "language": "Java", - "yes_example": "### 被判定为\"日期格式化时,传入 pattern 中表示年份统一使用小写的 y\"的例子\n<例子1>\nSimpleDateFormat sdf = new SimpleDateFormat(\"YYYY-MM-dd\");\n", - "no_example": "### 不能被判定为\"日期格式化时,传入 pattern 中表示年份统一使用小写的 y\"的例子\n<例子1>\nSimpleDateFormat sdf = new SimpleDateFormat(\"yyyy-MM-dd\");\n" - }, - { - "id": 39, - "text": "不允许在程序任何地方中使用:1)java.sql.Date 2)java.sql.Time 3)java.sql.Timestamp。", - "detail": "缺陷类型:使用了 java.sql 包中的日期类;修复方案:使用 java.time 包中的日期类。", - "language": "Java", - "yes_example": "### 被判定为\"不允许在程序任何地方中使用:1)java.sql.Date 2)java.sql.Time 3)java.sql.Timestamp\"的例子\n<例子1>\njava.sql.Date sqlDate = new java.sql.Date(System.currentTimeMillis());\n", - "no_example": "### 不能被判定为\"不允许在程序任何地方中使用:1)java.sql.Date 2)java.sql.Time 3)java.sql.Timestamp\"的例子\n<例子1>\njava.time.LocalDate localDate = java.time.LocalDate.now();\n" - }, - { - "id": 40, - "text": "判断所有集合内部的元素是否为空,使用 isEmpty() 方法,而不是 size() == 0 的方式。", - "detail": "缺陷类型:集合判空方式错误;修复方案:使用 isEmpty() 方法。", - "language": "Java", - "yes_example": "### 被判定为\"判断所有集合内部的元素是否为空,使用 isEmpty() 方法,而不是 size() == 0 的方式\"的例子\n<例子1>\nList list = new ArrayList<>();\nif (list.size() == 0) {\n // 判空逻辑\n}\n", - "no_example": "### 不能被判定为\"判断所有集合内部的元素是否为空,使用 isEmpty() 方法,而不是 size() == 0 的方式\"的例子\n<例子1>\nList list = new ArrayList<>();\nif (list.isEmpty()) {\n // 判空逻辑\n}\n" - }, - { - "id": 41, - "text": "只要覆写 equals,就必须覆写 hashCode。", - "detail": "缺陷类型:未覆写 hashCode 方法;修复方案:同时覆写 equals 和 hashCode 方法。", - "language": "Java", - "yes_example": "### 被判定为\"只要覆写 equals,就必须覆写 hashCode\"的例子\n<例子1>\npublic class User {\n private String name;\n @Override\n public boolean equals(Object o) {\n if (this == o) return true;\n if (o == null || getClass() != o.getClass()) return false;\n User user = (User) o;\n return Objects.equals(name, user.name);\n }\n}\n", - "no_example": "### 不能被判定为\"只要覆写 equals,就必须覆写 hashCode\"的例子\n<例子1>\npublic class User {\n private String name;\n @Override\n public boolean equals(Object o) {\n if (this == o) return true;\n if (o == null || getClass() != o.getClass()) return false;\n User user = (User) o;\n return Objects.equals(name, user.name);\n }\n @Override\n public int hashCode() {\n return Objects.hash(name);\n }\n}\n" - }, - { - "id": 42, - "text": "使用 Map 的方法 keySet() / values() / entrySet() 返回集合对象时,不可以对其进行添加元素操作,否则会抛出 UnsupportedOperationException 异常。", - "detail": "缺陷类型:对 Map 的 keySet() / values() / entrySet() 返回的集合进行添加操作;修复方案:避免对这些集合进行添加操作。", - "language": "Java", - "yes_example": "### 被判定为\"使用 Map 的方法 keySet() / values() / entrySet() 返回集合对象时,不可以对其进行添加元素操作,否则会抛出 UnsupportedOperationException 异常\"的例子\n<例子1>\nMap map = new HashMap<>();\nmap.put(\"key1\", \"value1\");\nSet keys = map.keySet();\nkeys.add(\"key2\");\n", - "no_example": "### 不能被判定为\"使用 Map 的方法 keySet() / values() / entrySet() 返回集合对象时,不可以对其进行添加元素操作,否则会抛出 UnsupportedOperationException 异常\"的例子\n<例子1>\nMap map = new HashMap<>();\nmap.put(\"key1\", \"value1\");\nSet keys = map.keySet();\n// 不进行添加操作\n" - }, - { - "id": 43, - "text": "不要在 foreach 循环里进行元素的 remove / add 操作。remove 元素请使用 iterator 方式,如果并发操作,需要对 iterator", - "detail": "缺陷类型:在 foreach 循环中进行元素的 remove / add 操作;修复方案:使用 iterator 进行元素的 remove 操作。", - "language": "Java", - "yes_example": "### 被判定为\"不要在 foreach 循环里进行元素的 remove / add 操作。remove 元素请使用 iterator 方式,如果并发操作,需要对 iterator\"的例子\n<例子1>\nList list = new ArrayList<>(Arrays.asList(\"a\", \"b\", \"c\"));\nfor (String s : list) {\n if (s.equals(\"a\")) {\n list.remove(s);\n }\n}\n", - "no_example": "### 不能被判定为\"不要在 foreach 循环里进行元素的 remove / add 操作。remove 元素请使用 iterator 方式,如果并发操作,需要对 iterator\"的例子\n<例子1>\nList list = new ArrayList<>(Arrays.asList(\"a\", \"b\", \"c\"));\nIterator iterator = list.iterator();\nwhile (iterator.hasNext()) {\n String s = iterator.next();\n if (s.equals(\"a\")) {\n iterator.remove();\n }\n}\n" - }, - { - "id": 44, - "text": "类、类属性、类方法的注释必须使用 Javadoc 规范,使用 /** 内容 */ 格式,不得使用 // xxx方式。", - "detail": "缺陷类型:注释不符合 Javadoc 规范;修复方案:使用 Javadoc 规范的注释格式。", - "language": "Java", - "yes_example": "### 被判定为\"类、类属性、类方法的注释必须使用 Javadoc 规范,使用 /** 内容 */ 格式,不得使用 // xxx方式\"的例子\n<例子1>\npublic class Example {\n // 这是一个类注释\n private String name;\n // 这是一个属性注释\n public String getName() {\n return name;\n }\n // 这是一个方法注释\n}\n", - "no_example": "### 不能被判定为\"类、类属性、类方法的注释必须使用 Javadoc 规范,使用 /** 内容 */ 格式,不得使用 // xxx方式\"的例子\n<例子1>\n/**\n * 这是一个类注释\n */\npublic class Example {\n /**\n * 这是一个属性注释\n */\n private String name;\n /**\n * 这是一个方法注释\n */\n public String getName() {\n return name;\n }\n}\n" - }, - { - "id": 45, - "text": "所有的抽象方法(包括接口中的方法)必须要用 Javadoc 注释", - "detail": "缺陷类型:缺少 Javadoc 注释;修复方案:为所有的抽象方法(包括接口中的方法)添加 Javadoc 注释,除了返回值、参数异常说明外,还必须指出该方法做什么事情,实现什么功能。", - "language": "Java", - "yes_example": "### 被判定为缺少 Javadoc 注释的例子\n<例子1>\npublic interface MyInterface {\n void doSomething();\n}\n这段代码中的接口方法 doSomething() 没有 Javadoc 注释,所以被判定为缺少 Javadoc 注释。\n", - "no_example": "### 不能被判定为缺少 Javadoc 注释的例子\n<例子1>\n/**\n * 执行某个操作\n * @param param 参数说明\n * @return 返回值说明\n * @throws Exception 异常说明\n */\npublic interface MyInterface {\n void doSomething(String param) throws Exception;\n}\n这段代码中的接口方法 doSomething() 有完整的 Javadoc 注释,所以不能被判定为缺少 Javadoc 注释。\n" - }, - { - "id": 46, - "text": "方法内部单行注释和多行注释的使用规范", - "detail": "缺陷类型:注释使用不规范;修复方案:方法内部单行注释,在被注释语句上方另起一行,使用 // 注释。方法内部多行注释使用 /* */注释,注意与代码对齐。", - "language": "Java", - "yes_example": "### 被判定为注释使用不规范的例子\n<例子1>\npublic void exampleMethod() {\n int a = 1; // 初始化变量a\n int b = 2; /* 初始化变量b */\n}\n这段代码中的单行注释和多行注释没有按照规范使用,所以被判定为注释使用不规范。\n", - "no_example": "### 不能被判定为注释使用不规范的例子\n<例子1>\npublic void exampleMethod() {\n // 初始化变量a\n int a = 1;\n /*\n * 初始化变量b\n */\n int b = 2;\n}\n这段代码中的单行注释和多行注释按照规范使用,所以不能被判定为注释使用不规范。\n" - }, - { - "id": 47, - "text": "所有的枚举类型字段必须要有注释", - "detail": "缺陷类型:枚举类型字段缺少注释;修复方案:为所有的枚举类型字段添加注释,说明每个数据项的用途。", - "language": "Java", - "yes_example": "### 被判定为枚举类型字段缺少注释的例子\n<例子1>\npublic enum Status {\n ACTIVE,\n INACTIVE\n}\n这段代码中的枚举类型字段没有注释,所以被判定为枚举类型字段缺少注释。\n", - "no_example": "### 不能被判定为枚举类型字段缺少注释的例子\n<例子1>\npublic enum Status {\n /**\n * 活跃状态\n */\n ACTIVE,\n /**\n * 非活跃状态\n */\n INACTIVE\n}\n这段代码中的枚举类型字段有注释,所以不能被判定为枚举类型字段缺少注释。\n" - }, - { - "id": 48, - "text": "finally 块必须对资源对象、流对象进行关闭", - "detail": "缺陷类型:资源对象、流对象未在 finally 块中关闭;修复方案:在 finally 块中对资源对象、流对象进行关闭,有异常也要做 try-catch。", - "language": "Java", - "yes_example": "### 被判定为资源对象、流对象未在 finally 块中关闭的例子\n<例子1>\npublic void readFile() {\n FileInputStream fis = null;\n try {\n fis = new FileInputStream(\"file.txt\");\n // 读取文件内容\n } catch (IOException e) {\n e.printStackTrace();\n }\n}\n这段代码中的 FileInputStream 对象没有在 finally 块中关闭,所以被判定为资源对象、流对象未在 finally 块中关闭。\n", - "no_example": "### 不能被判定为资源对象、流对象未在 finally 块中关闭的例子\n<例子1>\npublic void readFile() {\n FileInputStream fis = null;\n try {\n fis = new FileInputStream(\"file.txt\");\n // 读取文件内容\n } catch (IOException e) {\n e.printStackTrace();\n } finally {\n if (fis != null) {\n try {\n fis.close();\n } catch (IOException e) {\n e.printStackTrace();\n }\n }\n }\n}\n这段代码中的 FileInputStream 对象在 finally 块中关闭,所以不能被判定为资源对象、流对象未在 finally 块中关闭。\n" - }, - { - "id": 49, - "text": "常量命名应该全部大写,单词间用下划线隔开", - "detail": "缺陷类型:常量命名不规范;修复方案:常量命名应该全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。", - "language": "Java", - "yes_example": "### 被判定为\"常量命名应该全部大写,单词间用下划线隔开\"的例子\n<例子1>\npublic static final int maxCount = 100;\n", - "no_example": "### 不能被判定为\"常量命名应该全部大写,单词间用下划线隔开\"的例子\n<例子1>\npublic static final int MAX_COUNT = 100;\n" - }, - { - "id": 50, - "text": "任何二目、三目运算符的左右两边都需要加一个空格", - "detail": "缺陷类型:运算符两边缺少空格;修复方案:任何二目、三目运算符的左右两边都需要加一个空格。", - "language": "Java", - "yes_example": "### 被判定为\"任何二目、三目运算符的左右两边都需要加一个空格\"的例子\n<例子1>\nint a=b+c;\n", - "no_example": "### 不能被判定为\"任何二目、三目运算符的左右两边都需要加一个空格\"的例子\n<例子1>\nint a = b + c;\n" - }, - { - "id": 1, - "text": "避免使用from import *", - "detail": "缺陷类型:避免使用from import *,导入所有内容会造成命名冲突;修复方案:每个使用到的子依赖需分别导入。", - "language": "Python", - "yes_example": "### 被判定为'避免使用from import *'的例子\n<例子1>\nfrom math import *\n", - "no_example": "### 不能被判定为'避免使用from import *'的例子\n<例子1>\nfrom math import sqrt, pi\n" - }, - { - "id": 2, - "text": "避免使用__import__()函数动态导入模块", - "detail": "缺陷类型:避免使用__import__()函数动态导入模块;修复方案:使用标准的import语句。", - "language": "Python", - "yes_example": "### 被判定为'避免使用__import__()函数动态导入模块'的例子\n<例子1>\nmodule = __import__('math')\n", - "no_example": "### 不能被判定为'避免使用__import__()函数动态导入模块'的例子\n<例子1>\nimport math\n" - }, - { - "id": 3, - "text": "导入语句未按标准库导入、相关第三方导入、本地应用/库特定导入的顺序分组", - "detail": "缺陷类型:导入语句未按标准库导入、相关第三方导入、本地应用/库特定导入的顺序分组;修复方案:按顺序分组导入语句。", - "language": "Python", - "yes_example": "### 被判定为'导入语句未按标准库导入、相关第三方导入、本地应用/库特定导入的顺序分组'的例子\n<例子1>\nimport requests\n\nimport mymodule\n\nimport os\n", - "no_example": "### 不能被判定为'导入语句未按标准库导入、相关第三方导入、本地应用/库特定导入的顺序分组'的例子\n<例子1>\nimport os\nimport requests\n\nimport mymodule\n" - }, - { - "id": 4, - "text": "避免未使用的函数形参", - "detail": "缺陷类型:存在未使用的函数形参;修复方案:移除未使用的函数形参。", - "language": "Python", - "yes_example": "### 被判定为'避免未使用的函数形参'的例子\n<例子1>\ndef func(a, b):\n return a\n", - "no_example": "### 不能被判定为'避免未使用的函数形参'的例子\n<例子1>\ndef func(a):\n return a\n" - }, - { - "id": 5, - "text": "使用is not None来检查一个变量是否不是None", - "detail": "缺陷类型:未使用is not None来检查一个变量是否不是None;修复方案:使用is not None来检查。", - "language": "Python", - "yes_example": "### 被判定为'使用is not None来检查一个变量是否不是None'的例子\n<例子1>\nif variable != None:\n pass\n", - "no_example": "### 不能被判定为'使用is not None来检查一个变量是否不是None'的例子\n<例子1>\nif variable is not None:\n pass\n" - }, - { - "id": 6, - "text": "避免使用==或!=来比较实例的等价性", - "detail": "缺陷类型:使用==或!=来比较实例的等价性;修复方案:应使用equals比较。", - "language": "Python", - "yes_example": "### 被判定为'避免使用==或!=来比较实例的等价性'的例子\n<例子1>\nif obj1 == obj2:\n pass\n", - "no_example": "### 不能被判定为'避免使用==或!=来比较实例的等价性'的例子\n<例子1>\nif obj1.equals(obj2):\n pass\n" - }, - { - "id": 7, - "text": "使用描述性变量名,避免使用单字母变量名", - "detail": "缺陷类型:使用单字母变量名;修复方案:使用描述性变量名。", - "language": "Python", - "yes_example": "### 被判定为'使用描述性变量名,避免使用单字母变量名'的例子\n<例子1>\nx = 10\n", - "no_example": "### 不能被判定为'使用描述性变量名,避免使用单字母变量名'的例子\n<例子1>\ncount = 10\n" - }, - { - "id": 8, - "text": "常量命名使用全大写字母,并用下划线分隔", - "detail": "缺陷类型:常量命名未使用全大写字母或未用下划线分隔;修复方案:常量命名使用全大写字母,并用下划线分隔。", - "language": "Python", - "yes_example": "### 被判定为'常量命名未使用全大写字母,并用下划线分隔'的例子\n<例子1>\npi = 3.14\n", - "no_example": "### 不能被判定为'常量命名未使用全大写字母,并用下划线分隔'的例子\n<例子1>\nPI = 3.14\n" - }, - { - "id": 9, - "text": "类名应该使用驼峰式命名(CamelCase)", - "detail": "缺陷类型:类名未使用驼峰式命名;修复方案:类名使用驼峰式命名。", - "language": "Python", - "yes_example": "### 被判定为'类名未使用驼峰式命名(CamelCase)'的例子\n<例子1>\nclass my_class:\n pass\n", - "no_example": "### 不能被判定为'类名未使用驼峰式命名(CamelCase)'的例子\n<例子1>\nclass MyClass:\n pass\n" - }, - { - "id": 10, - "text": "尽量使用with语句来管理资源", - "detail": "缺陷类型:未使用with语句来管理资源;修复方案:使用with语句来管理资源。", - "language": "Python", - "yes_example": "### 被判定为'未使用with语句来管理资源'的例子\n<例子1>\nfile = open('file.txt', 'r')\ncontent = file.read()\nfile.close()\n", - "no_example": "### 不能被判定为'未使用with语句来管理资源'的例子\n<例子1>\nwith open('file.txt', 'r') as file:\n content = file.read()\n" - }, - { - "id": 11, - "text": "避免使用except:来捕获所有异常,应该指定异常类型", - "detail": "缺陷类型:捕获所有异常;修复方案:指定具体的异常类型。", - "language": "Python", - "yes_example": "### 被判定为'避免使用except:来捕获所有异常,应该指定异常类型'的例子\n<例子1>\ntry:\n # some code\nexcept:\n handle_error()\n这段代码中使用了except:来捕获所有异常,所以这个被判定为'避免使用except:来捕获所有异常,应该指定异常类型'\n", - "no_example": "### 不能被判定为'避免使用except:来捕获所有异常,应该指定异常类型'的例子\n<例子1>\ntry:\n # some code\nexcept ValueError:\n handle_value_error()\n这段代码中指定了具体的异常类型ValueError,所以这个不能被判定为'避免使用except:来捕获所有异常,应该指定异常类型'\n" - }, - { - "id": 12, - "text": "尽量避免手动拼接字符串", - "detail": "缺陷类型:手动拼接字符串;修复方案:使用格式化字符串或join方法。", - "language": "Python", - "yes_example": "### 被判定为'尽量避免手动拼接字符串'的例子\n<例子1>\nname = 'John'\ngreeting = 'Hello, ' + name + '!'\n这段代码中使用了手动拼接字符串,所以这个被判定为'尽量避免手动拼接字符串'\n", - "no_example": "### 不能被判定为'尽量避免手动拼接字符串'的例子\n<例子1>\nname = 'John'\ngreeting = f'Hello, {name}!'\n这段代码中使用了格式化字符串,所以这个不能被判定为'尽量避免手动拼接字符串'\n" - }, - { - "id": 13, - "text": "尽量避免出现魔法字符和数字,声明为常量", - "detail": "缺陷类型:使用魔法字符和数字;修复方案:将其声明为常量。", - "language": "Python", - "yes_example": "### 被判定为'尽量避免出现魔法字符和数字,声明为常量'的例子\n<例子1>\nif status == 1:\n print('Active')\n这段代码中使用了魔法数字1,所以这个被判定为'尽量避免出现魔法字符和数字,声明为常量'\n", - "no_example": "### 不能被判定为'尽量避免出现魔法字符和数字,声明为常量'的例子\n<例子1>\nACTIVE_STATUS = 1\nif status == ACTIVE_STATUS:\n print('Active')\n这段代码中将魔法数字声明为了常量ACTIVE_STATUS,所以这个不能被判定为'尽量避免出现魔法字符和数字,声明为常量'\n" - }, - { - "id": 14, - "text": "boolean变量判断无需显式比较", - "detail": "缺陷类型:显式比较boolean变量;修复方案:直接使用boolean变量进行判断。", - "language": "Python", - "yes_example": "### 被判定为'boolean变量判断无需显式比较'的例子\n<例子1>\nflag = True\nif flag == True:\n print('Flag is true')\n这段代码中对boolean变量进行了显式比较,所以这个被判定为'boolean变量判断无需显式比较'\n", - "no_example": "### 不能被判定为'boolean变量判断无需显式比较'的例子\n<例子1>\nflag = True\nif flag:\n print('Flag is true')\n这段代码中直接使用了boolean变量进行判断,所以这个不能被判定为'boolean变量判断无需显式比较'\n" - }, - { - "id": 15, - "text": "使用isinstance()来检查对象的类型", - "detail": "缺陷类型:使用type()检查对象类型;修复方案:使用isinstance()函数。", - "language": "Python", - "yes_example": "### 被判定为'使用isinstance()来检查对象的类型'的例子\n<例子1>\nif type(obj) == list:\n print('obj is a list')\n这段代码中使用了type()来检查对象类型,所以这个被判定为'使用isinstance()来检查对象的类型'\n", - "no_example": "### 不能被判定为'使用isinstance()来检查对象的类型'的例子\n<例子1>\nif isinstance(obj, list):\n print('obj is a list')\n这段代码中使用了isinstance()来检查对象类型,所以这个不能被判定为'使用isinstance()来检查对象的类型'\n" - }, - { - "id": 16, - "text": "避免使用os.system()来调用外部命令", - "detail": "缺陷类型:使用os.system()调用外部命令;修复方案:使用subprocess模块。", - "language": "Python", - "yes_example": "### 被判定为'避免使用os.system()来调用外部命令'的例子\n<例子1>\nos.system('ls -l')\n这段代码中使用了os.system()来调用外部命令,所以这个被判定为'避免使用os.system()来调用外部命令'\n", - "no_example": "### 不能被判定为'避免使用os.system()来调用外部命令'的例子\n<例子1>\nimport subprocess\nsubprocess.run(['ls', '-l'])\n这段代码中使用了subprocess模块来调用外部命令,所以这个不能被判定为'避免使用os.system()来调用外部命令'\n" - }, - { - "id": 17, - "text": "只使用@property装饰器来创建只读属性,而非修改属性", - "detail": "缺陷类型:使用@property装饰器创建可修改属性;修复方案:只使用@property装饰器创建只读属性。", - "language": "Python", - "yes_example": "### 被判定为'只使用@property装饰器来创建只读属性,而非修改属性'的例子\n<例子1>\nclass MyClass:\n def __init__(self, value):\n self._value = value\n\n @property\n def value(self, new_value):\n self._value = new_value\n这段代码中使用@property装饰器创建了可修改属性,所以这个被判定为'只使用@property装饰器来创建只读属性,而非修改属性'\n", - "no_example": "### 不能被判定为'只使用@property装饰器来创建只读属性,而非修改属性'的例子\n<例子1>\nclass MyClass:\n def __init__(self, value):\n self._value = value\n\n @property\n def value(self):\n return self._value\n这段代码中使用@property装饰器创建了只读属性,所以这个不能被判定为'只使用@property装饰器来创建只读属性,而非修改属性'\n" - }, - { - "id": 18, - "text": "在使用索引或切片时,不要在方括号或冒号内加空格", - "detail": "缺陷类型:在索引或切片的方括号或冒号内加空格;修复方案:去掉方括号或冒号内的空格。", - "language": "Python", - "yes_example": "### 被判定为'在使用索引或切片时,不要在方括号或冒号内加空格'的例子\n<例子1>\nlist = [1, 2, 3, 4]\nsublist = list[ 1 : 3 ]\n这段代码中在索引或切片的方括号或冒号内加了空格,所以这个被判定为'在使用索引或切片时,不要在方括号或冒号内加空格'\n", - "no_example": "### 不能被判定为'在使用索引或切片时,不要在方括号或冒号内加空格'的例子\n<例子1>\nlist = [1, 2, 3, 4]\nsublist = list[1:3]\n这段代码中在索引或切片的方括号或冒号内没有加空格,所以这个不能被判定为'在使用索引或切片时,不要在方括号或冒号内加空格'\n" - }, - { - "id": 19, - "text": "在逗号、分号或冒号前不要加空格,但在它们之后要加空格", - "detail": "缺陷类型:在逗号、分号或冒号前加空格或在它们之后不加空格;修复方案:在逗号、分号或冒号前不要加空格,但在它们之后要加空格。", - "language": "Python", - "yes_example": "### 被判定为'在逗号、分号或冒号前不要加空格,但在它们之后要加空格'的例子\n<例子1>\nif x == 4 : print(x , y)\n这段代码中在逗号、分号或冒号前加了空格或在它们之后没有加空格,所以这个被判定为'在逗号、分号或冒号前不要加空格,但在它们之后要加空格'\n", - "no_example": "### 不能被判定为'在逗号、分号或冒号前不要加空格,但在它们之后要加空格'的例子\n<例子1>\nif x == 4: print(x, y)\n这段代码中在逗号、分号或冒号前没有加空格且在它们之后加了空格,所以这个不能被判定为'在逗号、分号或冒号前不要加空格,但在它们之后要加空格'\n" - }, - { - "id": 20, - "text": "对于二元操作符,两边都应有空格,例如 a = b + 1", - "detail": "缺陷类型:二元操作符两边没有空格;修复方案:在二元操作符两边加空格。", - "language": "Python", - "yes_example": "### 被判定为'对于二元操作符,两边都应有空格,例如 a = b + 1'的例子\n<例子1>\na=b+1\n这段代码中二元操作符两边没有空格,所以这个被判定为'对于二元操作符,两边都应有空格,例如 a = b + 1'\n", - "no_example": "### 不能被判定为'对于二元操作符,两边都应有空格,例如 a = b + 1'的例子\n<例子1>\na = b + 1\n这段代码中二元操作符两边有空格,所以这个不能被判定为'对于二元操作符,两边都应有空格,例如 a = b + 1'\n" - }, - { - "id": 21, - "text": "避免使用Python关键字作为变量名或函数名", - "detail": "缺陷类型:使用Python关键字作为变量名或函数名;修复方案:使用非关键字的名称。", - "language": "Python", - "yes_example": "### 被判定为'避免使用Python关键字作为变量名或函数名'的例子\n<例子1>\ndef class():\n pass\n\n<例子2>\nfor = 5\n", - "no_example": "### 不能被判定为'避免使用Python关键字作为变量名或函数名'的例子\n<例子1>\ndef my_function():\n pass\n\n<例子2>\nnumber = 5\n" - }, - { - "id": 22, - "text": "避免使用特殊字符作为变量名,例如$或@", - "detail": "缺陷类型:使用特殊字符作为变量名;修复方案:使用合法的变量名。", - "language": "Python", - "yes_example": "### 被判定为'避免使用特殊字符作为变量名,例如$或@'的例子\n<例子1>\nmy$var = 10\n\n<例子2>\n@var = 20\n", - "no_example": "### 不能被判定为'避免使用特殊字符作为变量名,例如$或@'的例子\n<例子1>\nmy_var = 10\n\n<例子2>\nvar_20 = 20\n" - }, - { - "id": 23, - "text": "避免使用raise来重新抛出当前的异常,这会丢失原始的栈跟踪", - "detail": "缺陷类型:使用raise重新抛出当前异常;修复方案:使用raise ... from ...语法。", - "language": "Python", - "yes_example": "### 被判定为'避免使用raise来重新抛出当前的异常,这会丢失原始的栈跟踪'的例子\n<例子1>\ntry:\n 1 / 0\nexcept ZeroDivisionError:\n raise\n", - "no_example": "### 不能被判定为'避免使用raise来重新抛出当前的异常,这会丢失原始的栈跟踪'的例子\n<例子1>\ntry:\n 1 / 0\nexcept ZeroDivisionError as e:\n raise RuntimeError('Error occurred') from e\n" - }, - { - "id": 24, - "text": "避免在except块中使用pass,这会捕获并忽略异常", - "detail": "缺陷类型:在except块中使用pass;修复方案:处理异常或记录日志。", - "language": "Python", - "yes_example": "### 被判定为'避免在except块中使用pass,这会捕获并忽略异常'的例子\n<例子1>\ntry:\n 1 / 0\nexcept ZeroDivisionError:\n pass\n", - "no_example": "### 不能被判定为'避免在except块中使用pass,这会捕获并忽略异常'的例子\n<例子1>\ntry:\n 1 / 0\nexcept ZeroDivisionError as e:\n logging.error('Error occurred: %s', e)\n" - }, - { - "id": 25, - "text": "避免使用assert语句来执行重要的运行时检查", - "detail": "缺陷类型:使用assert语句执行重要的运行时检查;修复方案:使用显式的条件检查和异常处理。", - "language": "Python", - "yes_example": "### 被判定为'避免使用assert语句来执行重要的运行时检查'的例子\n<例子1>\ndef divide(a, b):\n assert b != 0\n return a / b\n", - "no_example": "### 不能被判定为'避免使用assert语句来执行重要的运行时检查'的例子\n<例子1>\ndef divide(a, b):\n if b == 0:\n raise ValueError('b cannot be zero')\n return a / b\n" - }, - { - "id": 26, - "text": "避免使用eval()和exec(),这些函数可能会带来安全风险", - "detail": "缺陷类型:使用eval()和exec()函数;修复方案:使用安全的替代方案。", - "language": "Python", - "yes_example": "### 被判定为'避免使用eval()和exec(),这些函数可能会带来安全风险'的例子\n<例子1>\neval('print(1)')\n\n<例子2>\nexec('a = 1')\n", - "no_example": "### 不能被判定为'避免使用eval()和exec(),这些函数可能会带来安全风险'的例子\n<例子1>\ncompiled_code = compile('print(1)', '', 'exec')\nexec(compiled_code)\n" - }, - { - "id": 27, - "text": "避免使用open()函数的exec模式,这可能会带来安全风险", - "detail": "缺陷类型:使用open()函数的exec模式;修复方案:使用安全的文件操作模式。", - "language": "Python", - "yes_example": "### 被判定为'避免使用open()函数的exec模式,这可能会带来安全风险'的例子\n<例子1>\nopen('file.txt', 'w+').write('data')\n", - "no_example": "### 不能被判定为'避免使用open()函数的exec模式,这可能会带来安全风险'的例子\n<例子1>\nwith open('file.txt', 'w') as f:\n f.write('data')\n" - }, - { - "id": 28, - "text": "避免使用sys.exit(),应使用异常来控制程序的退出", - "detail": "缺陷类型:使用sys.exit()退出程序;修复方案:使用异常处理机制。", - "language": "Python", - "yes_example": "### 被判定为'避免使用sys.exit(),应使用异常来控制程序的退出'的例子\n<例子1>\nimport sys\nsys.exit(1)\n", - "no_example": "### 不能被判定为'避免使用sys.exit(),应使用异常来控制程序的退出'的例子\n<例子1>\nraise SystemExit(1)\n" - }, - { - "id": 29, - "text": "避免使用time.sleep()进行线程同步,应使用同步原语,如锁或事件", - "detail": "缺陷类型:使用time.sleep()进行线程同步;修复方案:使用同步原语。", - "language": "Python", - "yes_example": "### 被判定为'避免使用time.sleep()进行线程同步,应使用同步原语,如锁或事件'的例子\n<例子1>\nimport time\n\ndef worker():\n time.sleep(1)\n", - "no_example": "### 不能被判定为'避免使用time.sleep()进行线程同步,应使用同步原语,如锁或事件'的例子\n<例子1>\nimport threading\n\nevent = threading.Event()\n\ndef worker():\n event.wait()\n" - }, - { - "id": 30, - "text": "每行代码尽量不超过79个字符", - "detail": "缺陷类型:每行代码超过79个字符;修复方案:将长行代码拆分为多行。", - "language": "Python", - "yes_example": "### 被判定为'每行代码尽量不超过79个字符'的例子\n<例子1>\nprint('This is a very long line of code that exceeds the 79 characters limit')\n", - "no_example": "### 不能被判定为'每行代码尽量不超过79个字符'的例子\n<例子1>\nprint('This is a very long line of code that exceeds the 79 characters limit' +\n ' but it is split into two lines')\n" - }, - { - "id": 31, - "text": "模块级别的函数和类定义之间用两个空行分隔,类内部的方法定义之间用一个空行分隔", - "detail": "缺陷类型:模块级别的函数和类定义之间没有用两个空行分隔,类内部的方法定义之间没有用一个空行分隔;修复方案:按照规范添加空行。", - "language": "Python", - "yes_example": "### 被判定为'模块级别的函数和类定义之间用两个空行分隔,类内部的方法定义之间用一个空行分隔'的例子\n<例子1>\ndef func1():\n pass\ndef func2():\n pass\n\n<例子2>\nclass MyClass:\n def method1(self):\n pass\n def method2(self):\n pass\n", - "no_example": "### 不能被判定为'模块级别的函数和类定义之间用两个空行分隔,类内部的方法定义之间用一个空行分隔'的例子\n<例子1>\ndef func1():\n pass\n\n\ndef func2():\n pass\n\n<例子2>\nclass MyClass:\n def method1(self):\n pass\n\n def method2(self):\n pass\n" - }, - { - "id": 32, - "text": "使用小写字母和下划线分隔的方式命名变量和函数", - "detail": "缺陷类型:变量和函数命名不符合小写字母和下划线分隔的方式;修复方案:使用小写字母和下划线分隔的方式命名。", - "language": "Python", - "yes_example": "### 被判定为'使用小写字母和下划线分隔的方式命名变量和函数'的例子\n<例子1>\ndef myFunction():\n pass\n\n<例子2>\nmyVariable = 10\n", - "no_example": "### 不能被判定为'使用小写字母和下划线分隔的方式命名变量和函数'的例子\n<例子1>\ndef my_function():\n pass\n\n<例子2>\nmy_variable = 10\n" - }, - { - "id": 33, - "text": "不允许使用print()函数来记录日志,使用logging模块等来记录日志", - "detail": "缺陷类型:使用print()函数记录日志;修复方案:使用logging模块记录日志。", - "language": "Python", - "yes_example": "### 被判定为'不允许使用print()函数来记录日志,使用logging模块等来记录日志'的例子\n<例子1>\nprint('Error occurred')\n", - "no_example": "### 不能被判定为'不允许使用print()函数来记录日志,使用logging模块等来记录日志'的例子\n<例子1>\nimport logging\nlogging.error('Error occurred')\n" - } -] \ No newline at end of file + { + "id": 1, + "text": "避免未使用的临时变量", + "language": "Java", + "detail": "缺陷类型:避免未使用的临时变量;对应Fixer:UnusedLocalVariableFixer;修复方案:删除未使用的临时变量", + "yes_example": "### 被判定为\"避免未使用的临时变量\"的例子\n<例子1>\npublic String initCreationForm(Map model) {\n\t\tOwner owner = new Owner();\n\t\tmodel.put(\"owner\", owner);\n\t\tint unusedVar = 10;\n\t\treturn VIEWS_OWNER_CREATE_OR_UPDATE_FORM;\n\t}\n上述代码中unusedVar变量未被使用,所以这个被判定为\"避免未使用的临时变量\"\n\n<例子2>\nint unusedVariable = 10;\nSystem.out.println(\"Hello, World!\");\n这段代码的变量\"unusedVariable\"未被使用或者引用,所以这个不能判定为\"避免未使用的临时变量\"\n", + "no_example": "### 不能被判定为\"避免未使用的临时变量\"的例子\n<例子1>\npublic void setTransientVariablesLocal(Map transientVariables) {\nthrow new UnsupportedOperationException(\"No execution active, no variables can be set\");\n}\n这段代码的\"transientVariables\"是函数参数而不是临时变量,虽然transientVariables没有被使用或者引用,但是这个也不能判定为\"避免未使用的临时变量\"\n\n\n<例子2>\npublic class TriggerCmd extends NeedsActiveExecutionCmd {\n protected Map transientVariables;\n public TriggerCmd(Map transientVariables) {\n this.transientVariables = transientVariables;\n }\n}\n上述代码中transientVariables不属于临时变量,它是类属性,且它在构造函数中被使用,所以这个不能被判定为\"避免未使用的临时变量\"\n" + }, + { + "id": 2, + "text": "不要使用 System.out.println 去打印", + "language": "Java", + "detail": "缺陷类型:不要使用 System.out.println 去打印;对应Fixer:SystemPrintlnFixer;修复方案:注释System.out.println代码", + "yes_example": "### 被判定为\"不要使用 System.out.println 去打印\"的例子\n<例子1>\nSystem.out.println(\"Initializing new owner form.\");\n上述代码使用了\"System.out.println\"进行打印,所以这个被判定为\"不要使用 System.out.println 去打印\"\n", + "no_example": "### 不能被判定为\"不要使用 System.out.println 去打印\"的例子\n<例子1>\nthrow new IllegalStateException(\"There is no authenticated user, we need a user authenticated to find tasks\");\n上述代码是抛出异常的代码,没有使用\"System.out.print\",所以这个不能被判定为\"不要使用 System.out.println 去打印\"\n" + }, + { + "id": 3, + "text": "避免函数中未使用的形参", + "language": "Java", + "detail": "缺陷类型:避免函数中未使用的形参;修复方案:忽略", + "yes_example": "### 被判定为\"避免函数中未使用的形参\"的例子\n<例子1>\npublic void setTransientVariablesLocal(Map transientVariables) {\n throw new UnsupportedOperationException(\"No execution active, no variables can be set\");\n}这段代码中的形参\"transientVariables\"未在函数体内出现,所以这个被判定为\"避免函数中未使用的形参\"\n\n\n<例子2>\nprotected void modifyFetchPersistencePackageRequest(PersistencePackageRequest ppr, Map pathVars) {}\n这段代码中的形参\"ppr\"和\"pathVars\"未在函数体内出现,所以这个被判定为\"避免函数中未使用的形参\"\n", + "no_example": "### 不能被判定为\"避免函数中未使用的形参\"的例子\n<例子1>\npublic String processFindForm(@RequestParam(value = \"pageNo\", defaultValue = \"1\") int pageNo) {\n\tlastName = owner.getLastName();\n\treturn addPaginationModel(pageNo, paginationModel, lastName, ownersResults);\n}上述代码中的形参\"pageNo\"在当前函数'processFindForm'内被'return addPaginationModel(pageNo, paginationModel, lastName, ownersResults);'这一句被使用,虽然pageNo没有被用于逻辑计算,但作为了函数调用其他函数的参数使用了,所以这个不能被判定为\"避免函数中未使用的形参\"\n" + }, + { + "id": 4, + "text": "if语句块不能为空", + "language": "Java", + "detail": "缺陷类型:if 语句块不能为空;对应Fixer:EmptyIfStmtFixer;修复方案:删除if语句块 或 适当的逻辑处理 或 注释说明为何为空", + "yes_example": "### 被判定为\"if语句块不能为空\"的例子\n<例子1>\npublic void emptyIfStatement() {\n\tif (getSpecialties().isEmpty()) {\n\t}\n}这段代码中的if语句块内容是空的,所以这个被判定为\"if语句块不能为空\"\n\n\n<例子2>\npublic void judgePersion() {\n\tif (persion != null) {\n\t\t// judge persion if not null\n\t}\n}\n这段代码中的if语句块虽然有内容,但是\"// judge persion if not null\"只是代码注释,if语句块内并没有实际的逻辑代码,所以这个被判定为\"if语句块不能为空\"\n", + "no_example": "### 不能被判定为\"if语句块不能为空\"的例子\n<例子1>\npublic void judgePersion() {\n\tif (persion != null) {\n\t\treturn 0;\n\t}\n}这段代码中的if语句块里有内容,且里面有非注释代码的逻辑代码\"return 0;\",所以这个不能被判定为\"if语句块不能为空\"\n" + }, + { + "id": 5, + "text": "循环体不能为空", + "language": "Java", + "detail": "缺陷类型:循环体不能为空;对应Fixer:EmptyStatementNotInLoopFixer;修复方案:删除对应while、for、foreach 循环体 或 添加适当的逻辑处理或者注释说明为何为空", + "yes_example": "### 被判定为\"循环体不能为空\"的例子\n<例子1>\npublic void emptyLoopBody() {\n\tfor (Specialty specialty : getSpecialties()) {\n\t}\n}这段代码中的for循环体的内容是空的,所以这个被判定为\"循环体不能为空\"\n\n\n<例子2>\npublic void emptyLoopBody() {\n\twhile (True) {\n\t\t// this is a code example\n\t}\n}这段代码中的while循环体的内容虽然不是空的,但内容只是代码注释,无逻辑内容,所以这个被判定为\"循环体不能为空\"\n\n\n<例子3>\npublic void emptyLoopBody() {\n\twhile (True) {\n\t\t\n\t}\n}这段代码中的while循环体内容是空的,所以这个被判定为\"循环体不能为空\"\n", + "no_example": "### 不能被判定为\"循环体不能为空\"的例子\n<例子1>\npublic void emptyLoopBody() {\n\tfor (Specialty specialty : getSpecialties()) {\n\t\ta = 1;\n\t\tif (a == 1) {\n\t\t\tretrun a;\n\t\t}\n\t}\n}上述代码的for循环体的内容不为空,且内容不全是代码注释,所以这个不能被判定为\"循环体不能为空\"\n" + }, + { + "id": 6, + "text": "避免使用 printStackTrace(),应该使用日志的方式去记录", + "language": "Java", + "detail": "缺陷类型:避免使用 printStackTrace(),应该使 用日志的方式去记录;修复方案:用日志的方式去记录", + "yes_example": "### 被判定为\"避免使用 printStackTrace(),应该使用日志的方式去记录\"的例子\n<例子1>\npublic void usePrintStackTrace() {\n\ttry {\n\t\tthrow new Exception(\"Fake exception\");\n\t} catch (Exception e) {\n\t\te.printStackTrace();\n\t}\n}这段代码中的catch语句中使用了printStackTrace(),所以这个被判定为\"避免使用 printStackTrace(),应该使用日志的方式去记录\"\n", + "no_example": "### 不能被判定为\"避免使用 printStackTrace(),应该使用日志的方式去记录\"的例子\n<例子1>\npublic void usePrintStackTrace() {\n\ttry {\n\t\tthrow new Exception(\"Fake exception\");\n\t} catch (Exception e) {\n\t\tlogging.info(\"info\");\n\t}\n}这段代码的catch语句中使用的是日志记录的方式,所以这个不能被判定为\"避免使用 printStackTrace(),应该使用日志的方式去记录\"\n" + }, + { + "id": 7, + "text": "catch 语句块不能为空", + "language": "Java", + "detail": "缺陷类型:catch 语句块不能为空;对应Fixer:EmptyCatchBlockFixer;修复方案:在catch里面添加注释", + "yes_example": "### 被判定为\"catch语句块不能为空\"的例子\n<例子1>\ntry {\n int[] array = new int[5];\n int number = array[10];\n} catch (ArrayIndexOutOfBoundsException e) {\n \n}\n这段代码中的catch语句中没有内容,所以这个被判定为\"catch语句块不能为空\"\n\n\n<例子2>\ntry {\n String str = null;\n str.length();\n} catch (NullPointerException e) {\n \n}这段代码中的catch语句中没有内容,所以这个被判定为\"catch语句块不能为空\"\n\n\n<例子3>\npublic class EmptyCatchExample {\n public static void main(String[] args) {\n try {\n // 尝试除以零引发异常\n int result = 10 / 0;\n } catch (ArithmeticException e) {\n \n }\n }\n}这段代码中的catch语句中没有内容,所以这个被判定为\"catch语句块不能为空\"\n\n<例子4>\ntry {\n FileReader file = new FileReader(\"nonexistentfile.txt\");\n} catch (FileNotFoundException e) {\n \n}这段代码中的catch语句中没有内容,所以这个被判定为\"catch语句块不能为空\"\n\n<例子5>\ntry {\n Object obj = \"string\";\n Integer num = (Integer) obj;\n} catch (ClassCastException e) {\n\t\n}这段代码中的catch语句中没有内容,所以这个被判定为\"catch语句块不能为空\"\n", + "no_example": "### 不能被判定为\"catch语句块不能为空\"的例子\n<例子1>\npersionNum = 1\ntry {\n\treturn True;\n} catch (Exception e) {\n\t// 如果人数为1则返回false\n\tif (persionNum == 1){\n\t\treturn False;\n\t}\n}这段代码的catch语句中不为空,所以不能把这个被判定为\"catch语句块不能为空\"\n\n\n<例子2>\ntry {\n\tthrow new Exception(\"Fake exception\");\n} catch (Exception e) {\n\te.printStackTrace();\n}这段代码的catch语句中虽然只有\"e.printStackTrace();\"但确实不为空,所以不能把这个被判定为\"catch语句块不能为空\"\n" + }, + { + "id": 8, + "text": "避免不必要的永真/永假判断", + "language": "Java", + "detail": "缺陷类型:避免不必要的永真/永假判断;对应Fixer:UnconditionalIfStatement Fixer;修复方案:删除永真/永假判断逻辑", + "yes_example": "### 被判定为\"避免不必要的永真/永假判断\"的例子\n<例子1>\npublic void someMethod() {\n\twhile (true) {\n\t}\n}这段代码中的\"while (true)\"是一个使用true做判断条件,但是没有循环结束标记,所以这个被判定为\"避免不必要的永真/永假判断\"\n\n\n<例子2>\nif (true) {\n\tSystem.out.println(\"This is always true\");\n}这段代码中的\"if (true)\"是一个使用true条件做条件,但是没有循环结束标记,所以这个被判定为\"避免不必要的永真/永假判断\"\n\n\n<例子3>\na = 1;\nwhile(a > 0){\n\ta = a + 1\n}这段代码初始化a=1,是大于0的,while循环体的逻辑是每次加1,那么判断条件a > 0会永远是真的,不会退出循环,所以这个被判定为\"避免不必要的永真/永假判断\"\n<例子3>", + "no_example": "### 不能被判定为\"避免不必要的永真/永假判断\"的例子\n<例子1>\na = 0;\nwhile (a < 5) {\n\ta = a + 1;\n}这段代码中的a<5是一个判断,当执行了5次while语句中的逻辑a=a+1之后,a会满足a < 5,就会退出循环,所以这个能被判定为\"避免不必要的永真/永假判断\"\n" + }, + { + "id": 9, + "text": "switch 中 default 必须放在最后", + "language": "Java", + "detail": "缺陷类型:switch 中 default 必须放在最后;对应Fixer:DefaultLabelNotLastInSwitchStmtFixer;修复方案:switch 中 default 放在最后", + "yes_example": "### 被判定为\"switch 中 default 必须放在最后\"的例子\n<例子1>\nswitch (number) {\n\tdefault:\n\t\tSystem.out.println(\"This is the default block, which is incorrectly placed here.\");\n\t\tbreak;\n\tcase 1:\n\t\tSystem.out.println(\"Number one\");\n\t\tbreak;\n\tcase 2:\n\t\tSystem.out.println(\"Number two\");\n\t\tbreak;\n}这段代码是一个switch语句,但是里面的default没有放在最后,所以这个被判定为\"switch 中 default 必须放在最后\"\n", + "no_example": "### 不能被判定为\"switch 中 default 必须放在最后\"的例子\n<例子1>\nswitch (number) {\ncase 3:\n\tSystem.out.println(\"Number one\");\n\tbreak;\ncase 4:\n\tSystem.out.println(\"Number two\");\n\tbreak;\ndefault:\n\tSystem.out.println(\"This is the default block, which is incorrectly placed here.\");\n\tbreak;\n}这段代码是一个switch语句且里面的default放在了最后,所以这个不能被判定为\"switch 中 default 必须放在最后\"\n" + }, + { + "id": 10, + "text": "未使用equals()函数对 String 作比较", + "language": "Java", + "detail": "缺陷类型:未使用equals()函数对 String 作比较;对应Fixer:UnSynStaticDateFormatter Fixer;修复方案:使用equals()函数对 String 作比较", + "yes_example": "### 被判定为\"未使用equals()函数对 String 作比较\"的例子\n<例子1>\nif (existingPet != null && existingPet.getName() == petName) {\n\tresult.rejectValue(\"name\", \"duplicate\", \"already exists\");\n}这段代码中所涉及的existingPet.getName()和petName均是字符串,但是在if语句里做比较的时候使用了==而没有使用equals()对string做比较,所以这个被判定为\"未使用equals()函数对 String 作比较\"\n\n\n<例子2>\nString isOk = \"ok\";\nif (\"ok\" == isOk) {\n\tresult.rejectValue(\"name\", \"duplicate\", \"already exists\");\n}这段代码中的isOk是个字符串,但在if判断中与\"ok\"比较的时候使用的是==,未使用equals()对string做比较,所以这个被判定为\"未使用equals()函数对 String 作比较\"\n", + "no_example": "### 不能被判定为\"未使用equals()函数对 String 作比较\"的例子\n<例子1>\nif (PROPERTY_VALUE_YES.equalsIgnoreCase(readWriteReqNode))\n formProperty.setRequired(true);\n这段代码中的PROPERTY_VALUE_YES和readWriteReqNode均是字符串,在if语句里比较PROPERTY_VALUE_YES和readWriteReqNode的使用的是equalsIgnoreCase(字符串比较忽略大小写),所以equalsIgnoreCase也是符合使用equals()函数对 String 作比较的,所以这个不能被判定为\"未使用equals()函数对 String 作比较\"\n\n\n<例子2>\nString isOk = \"ok\";\nif (\"ok\".equals(isOk)) {\n\tresult.rejectValue(\"name\", \"duplicate\", \"already exists\");\n}这段代码中的isOk是个字符串,在if判断中与\"ok\"比较的时候使用的是equals()对string做比较,所以这个不能被判定为\"未使用equals()函数对 String 作比较\"\n" + }, + { + "id": 11, + "text": "禁止在日志中直接使用字符串输出异常,请使用占位符传递异常对象", + "language": "Java", + "detail": "缺陷类型:禁止在日志中直接使用字符串输出异常,请使用占位符传递异常对象 输出异常;对应Fixer:ConcatExceptionFixer;修复方案:使用占位符传递异常对象", + "yes_example": "### 被判定为\"禁止在日志中直接使用字符串输出异常,请使用占位符传递异常对象\"的例子\n<例子1>\ntry {\n listenersNode = objectMapper.readTree(listenersNode.asText());\n} catch (Exception e) {\n LOGGER.info(\"Listeners node can not be read\", e);\n}这段代码中日志输出内容内容是直接使用字符串\"Listeners node can not be read\"拼接,日志输出异常时,应使用占位符输出异常信息,而不是直接使用字符串拼接,所以这个被判定为\"禁止在日志中直接使用字符串输出异常,请使用占位符传递异常对象\"\n", + "no_example": "### 不能被判定为\"禁止在日志中直接使用字符串输出异常,请使用占位符传递异常对象\"的例子\n<例子1>\nPersion persion = persionService.getPersion(1);\nif (persion == null){\n\tLOGGER.error(PERSION_NOT_EXIT);\n}这段代码中的PERSION_NOT_EXIT是一个用户自定义的异常常量,代表persion不存在,没有直接使用字符串\"persion not exit\"拼接,所以这个不能被判定为\"禁止在日志中直接使用字符串输出异常,请使用占位符传递异常对象\"\n<例子1>\n\n<例子2>\ntry {\n a = a + 1;\n} catch (Exception e) {\n Persion persion = persionService.getPersion(1);\n LOGGER.info(persion);\n}这段代码中输出日志没有直接使用字符串拼接,而是使用的Persion对象输出,所以这个不能被判定为\"禁止在日志中直接使用字符串输出异常,请使用占位符传递异常对象\"\n" + }, + { + "id": 12, + "text": "finally 语句块不能为空", + "language": "Java", + "detail": "缺陷类型:finally 语句块不能为空;对应Fixer:EmptyFinallyBlockFixer;修复方案:删除空 finally 语句块", + "yes_example": "### 被判定为\"finally 语句块不能为空\"的例子\n<例子1>\ntry {\n\tPersion persion = persionService.getPersion(1);\n\treturn persion;\n} finally {\n\t\n}这段代码中的finally语句块内没有内容,所以这个被判定为\"finally 语句块不能为空\"\n\n\n<例子2>\ntry {\n\tSystem.out.println(\"Inside try block\");\n} finally {\n\t// 空的finally块,没有任何语句,这是一个缺陷\n}这段代码中的finally语句块内没有内容,所以这个被判定为\"finally 语句块不能为空\"\n\n\n<例子3>\ntry {\n int result = 10 / 0;\n} catch (ArithmeticException e) {\n e.printStackTrace();\n} finally {\n \n}这段代码中的finally语句块内没有内容,所以这个被判定为\"finally 语句块不能为空\"\n\n\n<例子4>\ntry {\n String str = null;\n System.out.println(str.length());\n} catch (NullPointerException e) {\n e.printStackTrace();\n} finally {\n \n}这段代码中的finally语句块内没有内容,所以这个被判定为\"finally 语句块不能为空\"\n\n\n<例子5>\ntry {\n int[] array = new int[5];\n int number = array[10];\n} catch (ArrayIndexOutOfBoundsException e) {\n e.printStackTrace();\n} finally {\n // 只有注释的 finally 语句块\n // 这是一个空的 finally 块\n}这段代码中的finally语句块内没有内容,所以这个被判定为\"finally 语句块不能为空\"\n\n\n<例子6>\ntry {\n FileReader file = new FileReader(\"nonexistentfile.txt\");\n} catch (FileNotFoundException e) {\n e.printStackTrace();\n} finally {\n // 只有空行的 finally 语句块\n \n}这段代码中的finally语句块内没有内容,所以这个被判定为\"finally 语句块不能为空\"\n", + "no_example": "### 不能被判定为\"finally 语句块不能为空\"的例子\n<例子1>\npublic void getPersion() {\n\ttry {\n\t\tPersion persion = persionService.getPersion(1);\n\t\tif (persion != null){ \n\t\t\treturn persion;\n\t\t}\n\t} finally {\n\t\treturn null;\n\t}\n}这段代码中的finally语句块中有非注释意外的内容\"return null;\",所以这个不能被判定为\"finally 语句块不能为空\"\n" + }, + { + "id": 13, + "text": "try 语句块不能为空", + "language": "Java", + "detail": "缺陷类型:try 语句块不能为空;对应Fixer:EmptyTryBlockFixer;修复方案:删除整个 try 语句", + "yes_example": "### 被判定为\"try 语句块不能为空\"的例子\n<例子1>\npublic void getPersion() {\n\ttry {\n\n\t}\n\treturn null;\n}这段代码中的try语句块内没有内容,所以这个被判定为\"try 语句块不能为空\"\n\n\n<例子2>\npublic void demoFinallyBlock() {\n\ttry {\n\n\t} finally {\n\t\treturn null;\n\t}\n}这段代码中的try语句块内没有内容,所以这个被判定为\"try 语句块不能为空\"\n\n\n<例子3>\ntry {\n \n} catch (Exception e) {\n e.printStackTrace();\n}这段代码中的try语句块内没有内容,所以这个被判定为\"try 语句块不能为空\"\n\n\n<例子4>\ntry {\n // 只有注释的 try 语句块\n\t\n} catch (Exception e) {\n e.printStackTrace();\n}这段代码中的try语句块内只有注释和空行,也可以认定为这种情况是try语句块内没有内容,所以这个被判定为\"try 语句块不能为空\"\n", + "no_example": "### 不能被判定为\"try 语句块不能为空\"的例子\n<例子1>\ntry {\n\ta = a + 1;\n} catch (Exception e) {\n\te.printStackTrace();\n}\n这段代码中的try语句块中有非注释意外的内容\"return null;\",所以这个不能被判定为\"try 语句块不能为空\"\n" + }, + { + "id": 14, + "text": "避免对象进行不必要的 NULL或者null 检查", + "language": "Java", + "detail": "缺陷类型:避免对象进行不必要的 NULL或者null 检查;对应Fixer:LogicalOpNpeFixer;修复方案:删除对对象不必要的 NULL 检查的逻辑", + "yes_example": "### 被判定为\"避免对象进行不必要的 NULL或者null 检查\"的例子\n<例子1>\na = \"dog\";\nif (a != null){\n\treturn a;\n}这段代码中的对象a已经是确定的值\"dog\",所以if条件句的判断\"a != null\"是不必要的,所以这个被判定为\"避免对象进行不必要的 NULL或者null 检查\"\n\n\n<例子2>\nif (authenticatedUserId != null && !authenticatedUserId.isEmpty() && userGroupManager!=null){\n\treturn authenticatedUserId;\n}这段代码中的\"authenticatedUserId != null\"和\"!authenticatedUserId.isEmpty()\"都是对\"authenticatedUserId\"的空判断,重复了,所以这个被判定为\"避免对象进行不必要的 NULL或者null 检查\"\n\n\n<例子3>\nList list = new ArrayList<>();\nif (list != null) {\n list.add(1);\n}这段代码中的list已经被初始化,不需要进行 null 检查,所以这个被判定为\"避免对象进行不必要的 NULL或者null 检查\"\n\n\n<例子4>\nif (this.type != null && this.type.getName() != null) {\n\tSystem.out.println(\"Type name is not null\");\n}这段代码中的对象type已经检查过非null,再次检查getName()是否为null是不必要的,所以这个被判定为\"避免对象进行不必要的 NULL或者null 检查\"\n\n\n\n<例子5>\nif (\"dog\".equals(null)){\n\treturn a;\n}这段代码中的\"dog\"是个确定的字符串,不需要进行null 检查,所以这个被判定为\"避免对象进行不必要的 NULL或者null 检查\"\n\n\n<例子6>\nInteger num = 10;\nif (num != null) {\n System.out.println(num);\n}这段代码中的num 已经被初始化,不需要进行 null 检查,所以这个被判定为\"避免对象进行不必要的 NULL或者null 检查\"\n", + "no_example": "### 不能被判定为\"避免对象进行不必要的 NULL或者null 检查\"的例子\n<例子1>\nCat cat = catService.get(1);\nif (cat != null){\n\tretrun cat;\n}这段代码中的对象\"cat\"是通过service获取到的,不确定是否为空,所以if条件句的判断的\"cat != null\"是必要的,所以这个不能被判定为\"避免对象进行不必要的 NULL或者null 检查\"\n" + }, + { + "id": 15, + "text": "避免 finally 块中出现 return", + "language": "Java", + "detail": "缺陷类型:避免 finally 块中出现 return;修复方案:无需修复", + "yes_example": "### 被判定为\"避免 finally 块中出现 return\"的例子\n<例子1>\npublic void getPersion() {\n\ttry {\n\t\tPersion persion = persionService.getPersion(1);\n\t\tif (persion != null){ \n\t\t\treturn persion;\n\t\t}\n\t} finally {\n\t\treturn null;\n\t}\n}这段代码中的finally语句块内容包含\"return\",所以这个被判定为\"避免 finally 块中出现 return\"\n", + "no_example": "### 不能被判定为\"避免 finally 块中出现 return\"的例子\n<例子1>\npublic void getPersion() {\n\ttry {\n\t\tPersion persion = persionService.getPersion(1);\n\t\tif (persion != null){ \n\t\t\treturn persion;\n\t\t}\n\t} finally {\n\t\tLOGGER.info(PERSION_NOT_EXIT);\n\t}\n}这段代码中的finally语句块中内容不包含\"return\",所以这个不能被判定为\"避免 finally 块中出现 return\"\n" + }, + { + "id": 16, + "text": "避免空的 static 初始化", + "language": "Java", + "detail": "缺陷类型:避免空的 static 初始化;对应Fixer:EmptyInitializerFixer;修复方案:删除整个空初始化块", + "yes_example": "### 被判定为\"避免空的 static 初始化\"的例子\n<例子1>\npublic class PetValidator implements Validator {\n\tstatic {\n\n\t}\n}这段代码中的static语句块没有内容,是空的,所以这个被判定为\"避免空的 static 初始化\"\n\n\n<例子2>\npublic class Persion {\n\tstatic {\n\t\t// 初始化的静态块\n\t}\n}这段代码中的static语句块是有内容的,不是空的,但是static初始化语句块中只有注释代码,没有实际的逻辑,所以这个被判定为\"避免空的 static 初始化\"\n", + "no_example": "### 不能被判定为\"避免空的 static 初始化\"的例子\n<例子1>\npublic class Cat {\n\tstatic {\n\t\t// 初始化的静态块\n\t\tcat = null;\n\t}\n}这段代码中的static语句块是有内容的,不是空的,且static初始化语句块中有非注释代码,有实际的逻辑,所以这个不能被判定为\"避免空的 static 初始化\"\n" + }, + { + "id": 17, + "text": "避免日历类用法不当风险", + "language": "Java", + "detail": "缺陷类型:避免日历类用法不当风险;修复方案:使用Java 8 及以上版本中的 java.time 包的LocalDate", + "yes_example": "### 被判定为\"避免日历类用法不当风险\"的例子\n<例子1>\nprivate static final Calendar calendar = new GregorianCalendar(2020, Calendar.JANUARY, 1);\n这段代码中的Calendar和GregorianCalendar是线程不安全的,所以这个被判定为\"避免日历类用法不当风险\"\n", + "no_example": "### 不能被判定为\"避免日历类用法不当风险\"的例子\n<例子1>\nprivate static final LocalDate calendar = LocalDate.of(2020, 1, 1);\n这段代码中的LocalDate使用的是Java 8 及以上版本中的 java.time 包,LocalDate 是不可变的并且是线程安全的,不会有线程安全和性能方面的问题,所以这个不能被判定为\"避免日历类用法不当风险\"\n" + }, + { + "id": 18, + "text": "使用集合转数组的方法,必须使用集合的toArray(T[]array),传入的是类型完全一样的数组,大小就是list.size()", + "language": "Java", + "detail": "缺陷类型:使用集合转数组的方法,必须使用集合的toArray(T[]array),传入的是类型完全一样的数组,大小就是list.size();对应Fixer:ClassCastExpWithToArrayF ixer;修复方案:使用集合的toArray(T[]array),且传入的是类型完全一样的数组", + "yes_example": "### 被判定为\"使用集合转数组的方法,必须使用集合的toArray(T[]array),传入的是类型完全一样的数组,大小就是list.size()\"的例子\n<例子1>\nList stringList = new ArrayList<>();\nstringList.add(\"Apple\");\nstringList.add(\"Banana\");\nObject[] objectArray = stringList.toArray(new Object[5]);\n这段代码使用集合转数组的方法的时候使用了toArray(new Object[5]),但是传入的数组类型不一致,所以这个被判定为\"使用集合转数组的方法,必须使用集合的toArray(T[]array),传入的是类型完全一样的数组,大小就是list.size()\"\n", + "no_example": "### 不能被判定为\"使用集合转数组的方法,必须使用集合的toArray(T[]array),传入的是类型完全一样的数组,大小就是list.size()\"的例子\n<例子1>\nList stringList = new ArrayList<>();\nstringList.add(\"Apple\");\nstringList.add(\"Banana\");\nString[] stringArray = stringList.toArray(new String[stringList.size()]);\n这段代码使用集合转数组的方法的时候使用了toArray(new String[stringList.size()]),传入的是类型完全一样的数组,所以这个不能被判定为\"使用集合转数组的方法,必须使用集合的toArray(T[]array),传入的是类型完全一样的数组,大小就是list.size()\"\n" + }, + { + "id": 19, + "text": "禁止在 equals()中使用 NULL或者null 做比较", + "language": "Java", + "detail": "缺陷类型:禁止在 equals()中使用 NULL或者null 做比较;对应Fixer:EqualsNullFixer;修复方案:使用Object的判空函数 做比较", + "yes_example": "### 被判定为\"禁止在 equals()中使用 NULL或者null 做比较\"的例子\n<例子1>\nif (\"test\".equals(null)) {\n\tSystem.out.println(\"test\");\n}这段代码中if条件中的代码\"test\".equals(null)使用equals()函数与null进行了比较,所以这个被判定为\"禁止在 equals()中使用 NULL或者null 做比较\"\n\n\n<例子2>\nif (!rangeValues[1].equals(\"null\")) {\n\tmaxValue = new BigDecimal(rangeValues[1]);\n}这段代码中if条件中的代码!rangeValues[1].equals(\"null\")使用equals()函数与Nnull进行了比较,所以这个被判定为\"禁止在 equals()中使用 NULL或者null 做比较\"\n\n\n<例子3>\nString str1 = \"example\";\nif (str1.equals(\"null\")) {\n System.out.println(\"str1 is null\");\n}这段代码中if条件中的代码str1.equals(null)使用equals()函数与null进行了比较,所以这个被判定为\"禁止在 equals()中使用 NULL或者null 做比较\"\n\n\n<例子4>\nString str3 = \"example\";\nif (str3 != null && str3.equals(\"null\")) {\n System.out.println(\"str3 is null\");\n}这段代码中if条件中的代码str3.equals(\"null\")使用equals()函数与\"null\"进行了比较,所以这个被判定为\"禁止在 equals()中使用 NULL或者null 做比较\"\n\n\n<例子5>\nInteger num1 = 10;\nif (num1.equals(null)) {\n System.out.println(\"num1 is null\");\n}这段代码中if条件中的代码num1.equals(null)使用equals()函数与\"null\"进行了比较,所以这个被判定为\"禁止在 equals()中使用 NULL或者null 做比较\"\n\n\n<例子6>\nObject obj = new Object();\nif (obj.equals(null)) {\n System.out.println(\"obj is null\");\n}这段代码中if条件中的代码obj.equals(null)使用equals()函数与\"null\"进行了比较,所以这个被判定为\"禁止在 equals()中使用 NULL或者null 做比较\"\n", + "no_example": "### 不能被判定为\"禁止在 equals()中使用 NULL或者null 做比较\"的例子\n<例子1>\na = \"test\";\nif (a.equals(\"test\")) {\n\tSystem.out.println(\"test\");\n}这段代码中if条件中的代码a.equals(\"test\")使用equals()函数与\"test\"进行了比较,所以这个不能被判定为\"禁止在 equals()中使用 NULL或者null 做比较\"\n" + }, + { + "id": 20, + "text": "switch 语句块不能为空", + "language": "Java", + "detail": "缺陷类型:switch 语句块不能为空;对应Fixer:EmptySwitchStatementsFix;修复方案:删除整个空 switch 语句块", + "yes_example": "### 被判定为\"switch 语句块不能为空\"的例子\n<例子1>\nswitch (number) {\n\t\n}这段代码是一个switch语句块,但是里面没有内容,所以这个被判定为\"switch 语句块不能为空\"\n\n\n<例子2>\nswitch (number) {\n\t// 这是一个switch语句块\n}这段代码是一个switch语句块,里面虽然有内容,但是内容仅仅是注释内容,没有实际的逻辑,所以这个被判定为\"switch 语句块不能为空\"\n", + "no_example": "### 不能被判定为\"switch 语句块不能为空\"的例子\n<例子1>\nswitch (number) {\n\tcase 1:\n\t\tSystem.out.println(\"Number one\");\n\t\tbreak;\n\tdefault:\n\t\tSystem.out.println(\"This is the default block, which is incorrectly placed here.\");\n\t\tbreak;\n}这段代码是一个switch语句块,里面有内容,而且内容里有非注释的代码,有实际的逻辑,所以这个不能被判定为\"switch 语句块不能为空\"\n" + }, + { + "id": 21, + "text": "在进行类型强制转换时,右括号与强制转换值之间不需要任何空格隔开", + "detail": "缺陷类型:在进行类型强制转换时,右括号与强制转换值之间不需要任何空格隔开;修复方案:在进行类型强制转换时,右括号与强制转换值之间不需要任何空格隔开。", + "language": "Java", + "yes_example": "### 被判定为\"在进行类型强制转换时,右括号与强制转换值之间不需要任何空格隔开\"的例子\n<例子1>\nint a = (int) 3.0;\n\n<例子2>\nint b = (int) 4.0;\n\n<例子3>\nlong a = (long) 5;\n\n<例子4>\nstring a = (string) 3.5;\n\n<例子5>\nPersion a = (Persion) \"zhangsan\";\n", + "no_example": "### 不能被判定为\"在进行类型强制转换时,右括号与强制转换值之间不需要任何空格隔开\"的例子\n<例子1>\nint a = (int)3.0;\n" + }, + { + "id": 22, + "text": "方法参数在定义和传入时,多个参数逗号后面必须加空格", + "detail": "缺陷类型:方法参数在定义和传入时,多个参数逗号后面必须加空格;修复方案:方法参数在定义和传入时,多个参数逗号后面必须加空格。", + "language": "Java", + "yes_example": "### 被判定为\"方法参数在定义和传入时,多个参数逗号后面必须加空格\"的例子\n<例子1>\npublic void exampleMethod(int a,int b,int c) {}\n", + "no_example": "### 不能被判定为\"方法参数在定义和传入时,多个参数逗号后面必须加空格\"的例子\n<例子1>\npublic void exampleMethod(int a, int b, int c) {}\n" + }, + { + "id": 23, + "text": "禁止使用构造方法 BigDecimal(double) 的方式把 double 值转化为 BigDecimal 对象", + "detail": "缺陷类型:禁止使用构造方法 BigDecimal(double) 的方式把 double 值转化为 BigDecimal 对象;修复方案:推荐使用 BigDecimal 的 valueOf 方法。", + "language": "Java", + "yes_example": "### 被判定为\"禁止使用构造方法 BigDecimal(double) 的方式把 double 值转化为 BigDecimal 对象\"的例子\n<例子1>\nBigDecimal bd = new BigDecimal(0.1);\n", + "no_example": "### 不能被判定为\"禁止使用构造方法 BigDecimal(double) 的方式把 double 值转化为 BigDecimal 对象\"的例子\n<例子1>\nBigDecimal bd = BigDecimal.valueOf(0.1);\n" + }, + { + "id": 24, + "text": "不能有多余的分号", + "detail": "缺陷类型:多余的分号;修复方案:删除多余的分号", + "yes_example": "### 被判定为\"不能有多余的分号\"的例子\n<例子1>\npublic void trigger(String executionId, Map processVariables) {\n commandExecutor.execute(new TriggerCmd(executionId, processVariables));\n}\n;\na = 1;\nb = 2;\nsum = a + b;\n这段代码中包含一个多余的分号\";\",所以这个被判定为\"不能有多余的分号\"\n", + "no_example": "### 不能被判定为\"不能有多余的分号\"的例子\n<例子1>\nwhile (True) {\n\ta = a + 1;\n\tbreak;\n}这段代码每个分号都是必须要的,所以这个能被判定为\"不能有多余的分号\"\n" + }, + { + "id": 25, + "text": "非线程安全的 SimpleDateFormat 使用,必须在函数或代码块级别使用synchronized", + "detail": "缺陷类型:非线程安全的 SimpleDateFormat 使用;修复方案:在函数或代码块级别加上synchronized修饰 或 使用其他线程安全的方式", + "yes_example": "### 被判定为\"非线程安全的 SimpleDateFormat 使用,必须在函数或代码块级别使用synchronized\"的例子\n<例子1>\npublic void formatDate(Date date) {\n\tSimpleDateFormat sdf = new SimpleDateFormat(\"yyyy-MM-dd\");\n\tSystem.out.println(\"Formatted date: \" + sdf.format(date));\n}这段代码中的函数formatDate在未使用synchronized同步修饰的情况下使用了SimpleDateFormat,这是线程不安全的,所以这个被判定为\"非线程安全的 SimpleDateFormat 使用,必须在函数或代码块级别使用synchronized\"\n", + "no_example": "### 不能被判定为\"非线程安全的 SimpleDateFormat 使用,必须在函数或代码块级别使用synchronized\"的例子\n<例子1>\npublic synchronized void formatDate(Date date) {\n\tSimpleDateFormat sdf = new SimpleDateFormat(\"yyyy-MM-dd\");\n\tSystem.out.println(\"Formatted date: \" + sdf.format(date));\n}这段代码是在synchronized同步块对函数'formatDate'进行保护,保证了线程安全,所以这个不能被判定为\"非线程安全的 SimpleDateFormat 使用,必须在函数或代码块级别使用synchronized\"\n" + }, + { + "id": 26, + "text": "未按驼峰命名规范进行命名,类名使用驼峰式UpperCamelCase风格, 方法名、参数名、成员变量、局部变量都统一使用lowerCamelCase风格", + "detail": "缺陷类型:未按驼峰命名规范进行命名;修复方案:类名使用UpperCamelCase风格,方法名、参数名、成员变量、局部变量使用lowerCamelCase风格。", + "language": "Java", + "yes_example": "### 被判定为\"未按驼峰命名规范进行命名\"的例子\n<例子1>\npublic class myClass {\n private int MyVariable;\n public void MyMethod() {}\n}\n这段代码中的类名、成员变量和方法名没有遵循驼峰命名法,所以被判定为命名规范问题。\n", + "no_example": "### 不能被判定为\"未按驼峰命名规范进行命名\"的例子\n<例子1>\npublic class MyClass {\n private int myVariable;\n public void myMethod() {}\n}\n这段代码中的类名、成员变量和方法名都遵循了驼峰命名法,所以不能被判定为命名规范问题。\n" + }, + { + "id": 27, + "text": "抽象类命名使用 Abstract 或 Base 开头;异常类命名使用 Exception 结尾,测试类命名以它要测试的类的名称开始,以 Test 结尾", + "detail": "缺陷类型:命名规范;修复方案:抽象类命名使用 Abstract 或 Base 开头,异常类命名使用 Exception 结尾,测试类命名以它要测试的类的名称开始,以 Test 结尾。", + "language": "Java", + "yes_example": "### 被判定为\"命名规范\"的例子\n<例子1>\npublic class MyAbstractClass {}\npublic class MyExceptionClass {}\npublic class TestMyClass {}\n这段代码中的抽象类、异常类和测试类的命名不符合规范,所以被判定为命名规范问题。\n", + "no_example": "### 不能被判定为\"命名规范\"的例子\n<例子1>\npublic abstract class AbstractMyClass {}\npublic class MyCustomException extends Exception {}\npublic class MyClassTest {}\n这段代码中的抽象类、异常类和测试类的命名都符合规范,所以不能被判定为命名规范问题。\n" + }, + { + "id": 28, + "text": "POJO 类中的任何布尔类型的变量,避免加\"is\" 前缀", + "detail": "缺陷类型:命名规范;修复方案:POJO 类中的布尔类型变量不要加 is 前缀。", + "language": "Java", + "yes_example": "### 被判定为\"命名规范\"的例子\n<例子1>\npublic class User {\n private boolean isActive;\n}\n这段代码中的布尔类型变量加了 is 前缀,所以被判定为命名规范问题。\n", + "no_example": "### 不能被判定为\"命名规范\"的例子\n<例子1>\npublic class User {\n private boolean active;\n}\n这段代码中的布尔类型变量没有加 is 前缀,所以不能被判定为命名规范问题。\n" + }, + { + "id": 29, + "text": "杜绝完全不规范的英文缩写,避免望文不知义。", + "detail": "缺陷类型:命名规范;修复方案:避免使用不规范的英文缩写,确保代码可读性。", + "language": "Java", + "yes_example": "### 被判定为\"命名规范\"的例子\n<例子1>\npublic class CfgMgr {\n private int cnt;\n}\n这段代码中的类名和变量名使用了不规范的英文缩写,所以被判定为命名规范问题。\n", + "no_example": "### 不能被判定为\"命名规范\"的例子\n<例子1>\npublic class ConfigManager {\n private int count;\n}\n这段代码中的类名和变量名没有使用不规范的英文缩写,所以不能被判定为命名规范问题。\n" + }, + { + "id": 30, + "text": "避免出现魔法字符和数字,应声明为常量", + "detail": "缺陷类型:避免出现魔法字符和数字,应声明为常量;修复方案:将魔法值定义为常量。", + "language": "Java", + "yes_example": "### 被判定为\"避免出现魔法字符和数字,应声明为常量\"的例子\n<例子1>\npublic class MagicNumberExample {\n public void calculate() {\n int result = 42 * 2;\n }\n}\n这段代码中直接使用了魔法值 42,所以被判定为代码规范问题。\n\n<例子2>\npublic class MagicNumberExample {\n public void calculate() {\n String result = \"This is a result\";\n }\n}\n这段代码中直接使用了魔法值 \"This is a result\",所以被判定为代码规范问题。\n", + "no_example": "### 不能被判定为\"避免出现魔法字符和数字,应声明为常量\"的例子\n<例子1>\npublic class MagicNumberExample {\n private static final int MULTIPLIER = 42;\n public void calculate() {\n int result = MULTIPLIER * 2;\n }\n}\n这段代码中将魔法值定义为了常量,所以不能被判定为代码规范问题。\n" + }, + { + "id": 31, + "text": "long 或 Long 赋值时,数值后使用大写 L,不能是小写 l,浮点数类型的数值后缀统一为大写的 D 或 F", + "detail": "缺陷类型:代码规范;修复方案:long 或 Long 赋值时使用大写 L,浮点数类型的数值后缀使用大写的 D 或 F。", + "language": "Java", + "yes_example": "### 被判定为\"代码规范\"的例子\n<例子1>\npublic class NumberExample {\n private long value = 1000l;\n private double pi = 3.14d;\n}\n这段代码中使用了小写的 l 和 d,所以被判定为代码规范问题。\n", + "no_example": "### 不能被判定为\"代码规范\"的例子\n<例子1>\npublic class NumberExample {\n private long value = 1000L;\n private double pi = 3.14D;\n}\n这段代码中使用了大写的 L 和 D,所以不能被判定为代码规范问题。\n" + }, + { + "id": 32, + "text": "如果大括号内为空,简洁地写成{}即可,大括号中间无需换行和空格;如果是非空代码块,则:1)左大括号前不换行。2)左大括号后换行。3)右大括号前换行。4)右大括号后还有 else 等代码则不换行;表示终止的右大括号后必须换行。", + "detail": "缺陷类型:代码格式;修复方案:遵循大括号的使用规范。", + "language": "Java", + "yes_example": "### 被判定为\"代码格式\"的例子\n<例子1>\npublic class BracketExample{public void method(){\n if (true) {\n }}\n}\n这段代码中的大括号使用不符合规范,所以被判定为代码格式问题。\n", + "no_example": "### 不能被判定为\"代码格式\"的例子\n<例子1>\npublic class BracketExample {\n public void method() {\n if (true) {\n // do something\n }\n }\n}\n这段代码中的大括号使用符合规范,所以不能被判定为代码格式问题。\n" + }, + { + "id": 33, + "text": "左小括号和右边相邻字符之间不需要空格;右小括号和左边相邻字符之间也不需要空格;而左大括号前需要加空格。", + "detail": "缺陷类型:代码格式;修复方案:遵循括号和空格的使用规范。", + "language": "Java", + "yes_example": "### 被判定为\"代码格式\"的例子\n<例子1>\npublic class SpaceExample {\n public void method (){\n }\n}\n这段代码中的括号和空格使用不符合规范,所以被判定为代码格式问题。\n", + "no_example": "### 不能被判定为\"代码规范\"的例子\n<例子1>\npublic class SpaceExample {\n public void method() {}\n}\n这段代码中的括号和空格使用符合规范,所以不能被判定为代码格式问题。\n" + }, + { + "id": 34, + "text": "if / for / while / switch / do 等保留字与左右括号之间都必须加空格。", + "detail": "缺陷类型:代码格式;修复方案:保留字与左右括号之间加空格。", + "language": "Java", + "yes_example": "### 被判定为\"代码规范\"的例子\n<例子1>\npublic class KeywordExample {\n public void method() {\n if(true) {\n }\n }\n}\n这段代码中的 if 关键字与括号之间没有空格,所以被判定为代码格式问题。\n", + "no_example": "### 不能被判定为\"代码规范\"的例子\n<例子1>\npublic class KeywordExample {\n public void method() {\n if (true) {\n }\n }\n}\n这段代码中的 if 关键字与括号之间有空格,所以不能被判定为代码格式问题。\n" + }, + { + "id": 35, + "text": "所有整型包装类对象之间值的比较,全部使用 equals 方法比较", + "detail": "缺陷类型:代码规范;修复方案:整型包装类对象之间的值比较使用 equals 方法。", + "language": "Java", + "yes_example": "### 被判定为\"代码规范\"的例子\n<例子1>\npublic class IntegerComparison {\n public void compare() {\n Integer a = 100;\n Integer b = 100;\n if (a == b) {\n }\n }\n}\n这段代码中使用了 == 比较整型包装类对象,所以被判定为代码规范问题。\n", + "no_example": "### 不能被判定为\"代码规范\"的例子\n<例子1>\npublic class IntegerComparison {\n public void compare() {\n Integer a = 100;\n Integer b = 100;\n if (a.equals(b)) {\n }\n }\n}\n这段代码中使用了 equals 方法比较整型包装类对象,所以不能被判定为代码规范问题。\n" + }, + { + "id": 36, + "text": "BigDecimal 的等值比较应使用 compareTo() 方法,而不是 equals() 方法。", + "detail": "缺陷类型:BigDecimal 的等值比较应使用 compareTo() 方法,而不是 equals() 方法;修复方案:使用 compareTo() 方法进行比较。", + "language": "Java", + "yes_example": "### 被判定为\"BigDecimal 的等值比较应使用 compareTo() 方法,而不是 equals() 方法\"的例子\n<例子1>\nBigDecimal a = new BigDecimal(\"1.0\");\nBigDecimal b = new BigDecimal(\"1.00\");\nif (a.equals(b)) {\n // 这段代码会返回 false,因为 equals() 方法会比较精度\n}\n", + "no_example": "### 不能被判定为\"BigDecimal 的等值比较应使用 compareTo() 方法,而不是 equals() 方法\"的例子\n<例子1>\nBigDecimal a = new BigDecimal(\"1.0\");\nBigDecimal b = new BigDecimal(\"1.00\");\nif (a.compareTo(b) == 0) {\n // 这段代码会返回 true,因为 compareTo() 方法只比较数值\n}\n" + }, + { + "id": 37, + "text": "禁止在 POJO 类中,同时存在对应属性 xxx 的 isXxx() 和 getXxx() 方法。", + "detail": "缺陷类型:POJO 类中存在重复的 getter 方法;修复方案:确保只存在一个 getter 方法。", + "language": "Java", + "yes_example": "### 被判定为\"禁止在 POJO 类中,同时存在对应属性 xxx 的 isXxx() 和 getXxx() 方法\"的例子\n<例子1>\npublic class User {\n private boolean active;\n public boolean isActive() {\n return active;\n }\n public boolean getActive() {\n return active;\n }\n}\n", + "no_example": "### 不能被判定为\"禁止在 POJO 类中,同时存在对应属性 xxx 的 isXxx() 和 getXxx() 方法\"的例子\n<例子1>\npublic class User {\n private int age;\n public int getAge() {\n return age;\n }\n}\n" + }, + { + "id": 38, + "text": "日期格式化时,传入 pattern 中表示年份统一使用小写的 y。", + "detail": "缺陷类型:日期格式化错误;修复方案:使用小写的 y 表示年份。", + "language": "Java", + "yes_example": "### 被判定为\"日期格式化时,传入 pattern 中表示年份统一使用小写的 y\"的例子\n<例子1>\nSimpleDateFormat sdf = new SimpleDateFormat(\"YYYY-MM-dd\");\n", + "no_example": "### 不能被判定为\"日期格式化时,传入 pattern 中表示年份统一使用小写的 y\"的例子\n<例子1>\nSimpleDateFormat sdf = new SimpleDateFormat(\"yyyy-MM-dd\");\n" + }, + { + "id": 39, + "text": "禁止在程序任何地方中使用:1)java.sql.Date 2)java.sql.Time 3)java.sql.Timestamp。", + "detail": "缺陷类型:使用了 java.sql 包中的日期类;修复方案:使用 java.time 包中的日期类。", + "language": "Java", + "yes_example": "### 被判定为\"禁止在程序任何地方中使用:1)java.sql.Date 2)java.sql.Time 3)java.sql.Timestamp\"的例子\n<例子1>\njava.sql.Date sqlDate = new java.sql.Date(System.currentTimeMillis());\n", + "no_example": "### 不能被判定为\"禁止在程序任何地方中使用:1)java.sql.Date 2)java.sql.Time 3)java.sql.Timestamp\"的例子\n<例子1>\njava.time.LocalDate localDate = java.time.LocalDate.now();\n" + }, + { + "id": 40, + "text": "判断所有集合内部的元素是否为空,使用 isEmpty() 方法,而不是 size() == 0 的方式。", + "detail": "缺陷类型:集合判空方式错误;修复方案:使用 isEmpty() 方法。", + "language": "Java", + "yes_example": "### 被判定为\"判断所有集合内部的元素是否为空,使用 isEmpty() 方法,而不是 size() == 0 的方式\"的例子\n<例子1>\nList list = new ArrayList<>();\nif (list.size() == 0) {\n // 判空逻辑\n}\n", + "no_example": "### 不能被判定为\"判断所有集合内部的元素是否为空,使用 isEmpty() 方法,而不是 size() == 0 的方式\"的例子\n<例子1>\nList list = new ArrayList<>();\nif (list.isEmpty()) {\n // 判空逻辑\n}\n" + }, + { + "id": 41, + "text": "只要重写 equals,就必须重写 hashCode。", + "detail": "缺陷类型:未重写 hashCode 方法;修复方案:同时重写 equals 和 hashCode 方法。", + "language": "Java", + "yes_example": "### 被判定为\"只要重写 equals,就必须重写 hashCode\"的例子\n<例子1>\npublic class User {\n private String name;\n @Override\n public boolean equals(Object o) {\n if (this == o) return true;\n if (o == null || getClass() != o.getClass()) return false;\n User user = (User) o;\n return Objects.equals(name, user.name);\n }\n}\n", + "no_example": "### 不能被判定为\"只要重写 equals,就必须重写 hashCode\"的例子\n<例子1>\npublic class User {\n private String name;\n @Override\n public boolean equals(Object o) {\n if (this == o) return true;\n if (o == null || getClass() != o.getClass()) return false;\n User user = (User) o;\n return Objects.equals(name, user.name);\n }\n @Override\n public int hashCode() {\n return Objects.hash(name);\n }\n}\n" + }, + { + "id": 42, + "text": "使用 Map 的方法 keySet() / values() / entrySet() 返回集合对象时,不可以对其进行添加元素操作,否则会抛出 UnsupportedOperationException 异常。", + "detail": "缺陷类型:对 Map 的 keySet() / values() / entrySet() 返回的集合进行添加操作;修复方案:避免对这些集合进行添加操作。", + "language": "Java", + "yes_example": "### 被判定为\"使用 Map 的方法 keySet() / values() / entrySet() 返回集合对象时,不可以对其进行添加元素操作,否则会抛出 UnsupportedOperationException 异常\"的例子\n<例子1>\nMap map = new HashMap<>();\nmap.put(\"key1\", \"value1\");\nSet keys = map.keySet();\nkeys.add(\"key2\");\n", + "no_example": "### 不能被判定为\"使用 Map 的方法 keySet() / values() / entrySet() 返回集合对象时,不可以对其进行添加元素操作,否则会抛出 UnsupportedOperationException 异常\"的例子\n<例子1>\nMap map = new HashMap<>();\nmap.put(\"key1\", \"value1\");\nSet keys = map.keySet();\n// 不进行添加操作\n" + }, + { + "id": 43, + "text": "不要在 foreach 循环里进行元素的 remove / add 操作。remove 元素请使用 iterator 方式,如果并发操作,需要对 iterator", + "detail": "缺陷类型:在 foreach 循环中进行元素的 remove / add 操作;修复方案:使用 iterator 进行元素的 remove 操作。", + "language": "Java", + "yes_example": "### 被判定为\"不要在 foreach 循环里进行元素的 remove / add 操作。remove 元素请使用 iterator 方式,如果并发操作,需要对 iterator\"的例子\n<例子1>\nList list = new ArrayList<>(Arrays.asList(\"a\", \"b\", \"c\"));\nfor (String s : list) {\n if (s.equals(\"a\")) {\n list.remove(s);\n }\n}\n", + "no_example": "### 不能被判定为\"不要在 foreach 循环里进行元素的 remove / add 操作。remove 元素请使用 iterator 方式,如果并发操作,需要对 iterator\"的例子\n<例子1>\nList list = new ArrayList<>(Arrays.asList(\"a\", \"b\", \"c\"));\nIterator iterator = list.iterator();\nwhile (iterator.hasNext()) {\n String s = iterator.next();\n if (s.equals(\"a\")) {\n iterator.remove();\n }\n}\n" + }, + { + "id": 44, + "text": "类、类属性、类方法的注释必须使用 Javadoc 规范,使用 /** 内容 */ 格式,不得使用 // xxx方式。", + "detail": "缺陷类型:注释不符合 Javadoc 规范;修复方案:使用 Javadoc 规范的注释格式。", + "language": "Java", + "yes_example": "### 被判定为\"类、类属性、类方法的注释必须使用 Javadoc 规范,使用 /** 内容 */ 格式,不得使用 // xxx方式\"的例子\n<例子1>\npublic class Example {\n // 这是一个类注释\n private String name;\n // 这是一个属性注释\n public String getName() {\n return name;\n }\n // 这是一个方法注释\n}\n", + "no_example": "### 不能被判定为\"类、类属性、类方法的注释必须使用 Javadoc 规范,使用 /** 内容 */ 格式,不得使用 // xxx方式\"的例子\n<例子1>\n/**\n * 这是一个类注释\n */\npublic class Example {\n /**\n * 这是一个属性注释\n */\n private String name;\n /**\n * 这是一个方法注释\n */\n public String getName() {\n return name;\n }\n}\n" + }, + { + "id": 45, + "text": "所有的抽象方法(包括接口中的方法)必须要用 Javadoc 注释", + "detail": "缺陷类型:所有的抽象方法(包括接口中的方法)必须要用 Javadoc 注释;修复方案:为所有的抽象方法(包括接口中的方法)添加 Javadoc 注释,除了返回值、参数异常说明外,还必须指出该方法做什么事情,实现什么功能。", + "language": "Java", + "yes_example": "### 被判定为\"所有的抽象方法(包括接口中的方法)必须要用 Javadoc 注释\"的例子\n<例子1>\npublic interface MyInterface {\n void doSomething();\n}\n这段代码中的接口方法 doSomething() 没有 Javadoc 注释,所以被判定为缺少 Javadoc 注释。\n", + "no_example": "### 不能被判定为\"所有的抽象方法(包括接口中的方法)必须要用 Javadoc 注释\"的例子\n<例子1>\n/**\n * 执行某个操作\n * @param param 参数说明\n * @return 返回值说明\n * @throws Exception 异常说明\n */\npublic interface MyInterface {\n void doSomething(String param) throws Exception;\n}\n这段代码中的接口方法 doSomething() 有完整的 Javadoc 注释,所以不能被判定为缺少 Javadoc 注释。\n" + }, + { + "id": 46, + "text": "方法内部单行注释和多行注释的使用规范", + "detail": "缺陷类型:注释使用不规范;修复方案:方法内部单行注释,在被注释语句上方另起一行,使用 // 注释。方法内部多行注释使用 /* */注释,注意与代码对齐。", + "language": "Java", + "yes_example": "### 被判定为\"注释使用不规范\"的例子\n<例子1>\npublic void exampleMethod() {\n int a = 1; // 初始化变量a\n int b = 2; /* 初始化变量b */\n}\n这段代码中的单行注释和多行注释没有按照规范使用,所以被判定为注释使用不规范。\n", + "no_example": "### 不能被判定为\"注释使用不规范\"的例子\n<例子1>\npublic void exampleMethod() {\n // 初始化变量a\n int a = 1;\n /*\n * 初始化变量b\n */\n int b = 2;\n}\n这段代码中的单行注释和多行注释按照规范使用,所以不能被判定为注释使用不规范。\n" + }, + { + "id": 47, + "text": "所有的枚举类型字段必须要有注释", + "detail": "缺陷类型:枚举类型字段缺少注释;修复方案:为所有的枚举类型字段添加注释,说明每个数据项的用途。", + "language": "Java", + "yes_example": "### 被判定为\"枚举类型字段缺少注释\"的例子\n<例子1>\npublic enum Status {\n ACTIVE,\n INACTIVE\n}\n这段代码中的枚举类型字段没有注释,所以被判定为枚举类型字段缺少注释。\n", + "no_example": "### 不能被判定为\"枚举类型字段缺少注释\"的例子\n<例子1>\npublic enum Status {\n /**\n * 活跃状态\n */\n ACTIVE,\n /**\n * 非活跃状态\n */\n INACTIVE\n}\n这段代码中的枚举类型字段有注释,所以不能被判定为枚举类型字段缺少注释。\n" + }, + { + "id": 48, + "text": "finally 块必须对资源对象、流对象进行关闭", + "detail": "缺陷类型:资源对象、流对象未在 finally 块中关闭;修复方案:在 finally 块中对资源对象、流对象进行关闭,有异常也要做 try-catch。", + "language": "Java", + "yes_example": "### 被判定为\"资源对象、流对象未在 finally 块中关闭\"的例子\n<例子1>\npublic void readFile() {\n FileInputStream fis = null;\n try {\n fis = new FileInputStream(\"file.txt\");\n // 读取文件内容\n } catch (IOException e) {\n e.printStackTrace();\n }\n}\n这段代码中的 FileInputStream 对象没有在 finally 块中关闭,所以被判定为资源对象、流对象未在 finally 块中关闭。\n", + "no_example": "### 不能被判定为\"资源对象、流对象未在 finally 块中关闭\"的例子\n<例子1>\npublic void readFile() {\n FileInputStream fis = null;\n try {\n fis = new FileInputStream(\"file.txt\");\n // 读取文件内容\n } catch (IOException e) {\n e.printStackTrace();\n } finally {\n if (fis != null) {\n try {\n fis.close();\n } catch (IOException e) {\n e.printStackTrace();\n }\n }\n }\n}\n这段代码中的 FileInputStream 对象在 finally 块中关闭,所以不能被判定为资源对象、流对象未在 finally 块中关闭。\n" + }, + { + "id": 49, + "text": "常量命名应该全部大写,单词间用下划线隔开", + "detail": "缺陷类型:常量命名不规范;修复方案:常量命名应该全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。", + "language": "Java", + "yes_example": "### 被判定为\"常量命名应该全部大写,单词间用下划线隔开\"的例子\n<例子1>\npublic static final int maxCount = 100;\n", + "no_example": "### 不能被判定为\"常量命名应该全部大写,单词间用下划线隔开\"的例子\n<例子1>\npublic static final int MAX_COUNT = 100;\n" + }, + { + "id": 50, + "text": "任何二目、三目运算符的左右两边都需要加一个空格", + "detail": "缺陷类型:运算符两边缺少空格;修复方案:任何二目、三目运算符的左右两边都需要加一个空格。", + "language": "Java", + "yes_example": "### 被判定为\"任何二目、三目运算符的左右两边都需要加一个空格\"的例子\n<例子1>\nint a=b+c;\n", + "no_example": "### 不能被判定为\"任何二目、三目运算符的左右两边都需要加一个空格\"的例子\n<例子1>\nint a = b + c;\n" + }, + { + "id": 51, + "text": "避免使用from import *", + "detail": "缺陷类型:避免使用from import *,导入所有内容会造成命名冲突;修复方案:每个使用到的子依赖需分别导入。", + "language": "Python", + "yes_example": "### 被判定为\"避免使用from import *\"的例子\n<例子1>from math import * \n", + "no_example": "### 不能被判定为\"避免使用from import *\"的例子\n<例子1>from math import sqrt, pi \n" + }, + { + "id": 52, + "text": "避免使用__import__()函数动态导入模块", + "detail": "缺陷类型:避免使用__import__()函数动态导入模块;修复方案:使用标准的import语句。", + "language": "Python", + "yes_example": "### 被判定为\"使用__import__()函数动态导入模块\"的例子\n<例子1>module = __import__('math') \n", + "no_example": "### 不能被判定为\"使用__import__()函数动态导入模块\"的例子\n<例子1>import math \n" + }, + { + "id": 53, + "text": "导入语句未按标准库导入、相关第三方导入、本地应用/库特定导入的顺序分组", + "detail": "缺陷类型:导入语句未按标准库导入、相关第三方导入、本地应用/库特定导入的顺序分组;修复方案:按顺序分组导入语句。", + "language": "Python", + "yes_example": "### 被判定为'导入语句未按标准库导入、相关第三方导入、本地应用/库特定导入的顺序分组'的例子\n<例子1>\nimport numpy as np\nimport os\nimport sys\nfrom my_local_module import my_function\n在这个样例中,先导入了第三方库,然后导入了标准库。\n\n<例子2>\nfrom my_project import my_local_function\nimport datetime\nimport requests\n在这个样例中,先导入了本地模块,然后导入了标准库。\n\n<例子3>\nimport os\nfrom my_project.local_module import some_function\nimport pandas as pd\nimport sys\nfrom another_local_module import another_function\nimport math\n在这个样例中,导入语句完全混乱,没有遵循任何顺序。\n\n<例子4>\nimport os\nimport requests\nimport sys\nimport numpy as np\nfrom local_package import local_module\n在这个样例中,导入标准库和第三方库交替进行。\n", + "no_example": "### 不能被判定为'导入语句未按标准库导入、相关第三方导入、本地应用/库特定导入的顺序分组'的例子\n<例子1>import os \n\n import requests \n\n import mymodule \n" + }, + { + "id": 54, + "text": "避免未使用的函数形参", + "detail": "缺陷类型:避免未使用的函数形参;修复方案:移除未使用的函数形参。", + "language": "Python", + "yes_example": "### 被判定为'避免未使用的函数形参'的例子\n<例子1>def func(a, b): \n return a\n<例子2>def start_game(unused_param): \npuzzle = Puzzle() \npuzzle.solve()\n<例子3>def make_move(self, board):\npass \n\n<例子4>def move(self, direction):\npass \n", + "no_example": "### 不能被判定为'避免未使用的函数形参'的例子\n<例子1>def func(a): \n return a" + }, + { + "id": 55, + "text": "使用is not None来检查一个变量是否不是None", + "detail": "缺陷类型:未使用is not None来检查一个变量是否不是None;修复方案:使用is not None来检查。", + "language": "Python", + "yes_example": "### 被判定为'未使用is not None来检查一个变量是否不是None'的例子\n<例子1>if variable != None:\n pass", + "no_example": "### 不能被判定为'未使用is not None来检查一个变量是否不是None'的例子\n<例子1>if variable is not None:\n pass" + }, + { + "id": 56, + "text": "避免使用==或!=来比较对象实例的等价性", + "detail": "缺陷类型:使用==或!=来比较对象实例的等价性;修复方案:应使用equals比较。", + "language": "Python", + "yes_example": "### 被判定为'使用==或!=来比较对象实例的等价性'的例子\n<例子1>obj1 = MyClass() \n obj2 = MyClass() if obj1 == obj2: \n pass\n", + "no_example": "### 不能被判定为'使用==或!=来比较对象实例的等价性'的例子\n<例子1>obj1 = MyClass() \n obj2 = MyClass() if obj1.equals(obj2): \n pass\n\n<例子2>obj1 = 21 \n obj2 = 22 \n if obj1.equals(obj2):\n pass" + }, + { + "id": 57, + "text": "避免使用单字母变量名,使用描述性变量名", + "detail": "缺陷类型:避免使用单字母变量名,使用描述性变量名;修复方案:使用描述性变量名。", + "language": "Python", + "yes_example": "### 被判定为'避免使用单字母变量名,使用描述性变量名'的例子\n<例子1>x = 10 \n\n<例子2>y = 10 \n", + "no_example": "### 不能被判定为'避免使用单字母变量名,使用描述性变量名'的例子\n<例子1>count = 10 \n" + }, + { + "id": 58, + "text": "常量命名使用全大写字母,并用下划线分隔", + "detail": "缺陷类型:常量命名未使用全大写字母或未用下划线分隔;修复方案:常量命名使用全大写字母,并用下划线分隔。", + "language": "Python", + "yes_example": "### 被判定为'常量命名未使用全大写字母,并用下划线分隔'的例子\n<例子1>pi = 3.14159", + "no_example": "### 不能被判定为'常量命名未使用全大写字母,并用下划线分隔'的例子\n<例子1>PI = 3.14159\n<例子2>max_size = 1 \n max_size += 1" + }, + { + "id": 59, + "text": "类名应使用驼峰式命名(CamelCase)", + "detail": "缺陷类型:类名未使用驼峰式命名;修复方案:类名使用驼峰式命名。", + "language": "Python", + "yes_example": "### 被判定为'类名未使用驼峰式命名(CamelCase)'的例子\n<例子1>class my_class: \n pass\n<例子2>class my_class: \n def solve(self):\n pass", + "no_example": "### 不能被判定为'类名未使用驼峰式命名(CamelCase)'的例子\n<例子1>class MyClass: \n pass" + }, + { + "id": 60, + "text": "尽量使用with语句来管理资源", + "detail": "缺陷类型:未使用with语句来管理资源;修复方案:使用with语句来管理资源。", + "language": "Python", + "yes_example": "### 被判定为'未使用with语句来管理资源'的例子\n<例子1>file = open('file.txt', 'r') \n content = file.read() \n file.close()", + "no_example": "### 不能被判定为'未使用with语句来管理资源'的例子\n<例子1>with open('file.txt', 'r') as file: \n content = file.read()" + }, + { + "id": 61, + "text": "避免使用except 或 通用的Exception来捕获所有异常,应该指定异常类型", + "detail": "缺陷类型:捕获所有异常;修复方案:指定具体的异常类型。", + "language": "Python", + "yes_example": "### 被判定为'使用except:来捕获所有异常'的例子\n<例子1>try: \n # some code \n except: \n handle_error()\n### 被判定为'抛出通用的Exception异常'的例子\n<例子2>\n try:\n process_data(data) \n except: \n raise Exception('An error occurred') \n ", + "no_example": "### 不能被判定为'使用except:来捕获所有异常'的例子\n<例子1>try: \n # some code \n except ValueError: \n handle_value_error()" + }, + { + "id": 62, + "text": "尽量避免手动拼接字符串", + "detail": "缺陷类型:手动拼接字符串;修复方案:使用格式化字符串或join方法。", + "language": "Python", + "yes_example": "### 被判定为'手动拼接字符串'的例子\n<例子1>\n name = 'John' \n greeting = 'Hello, ' + name + '!' \n \n <例子2>greeting = '2048' + 'game' \n \n <例子3>pygame.display.set_caption('贪吃蛇' + '游戏')", + "no_example": "### 不能被判定为'手动拼接字符串'的例子\n<例子1>\n name = 'John' \n greeting = f'Hello, {name}!' \n" + }, + { + "id": 63, + "text": "避免出现魔法字符和数字,应声明为常量", + "detail": "缺陷类型:使用魔法字符和数字;修复方案:将其声明为常量。", + "language": "Python", + "yes_example": "### 被判定为'出现魔法字符和数字'的例子\n<例子1>\n if status == 1: \n print('Active')' \n\n<例子2>\n self.board = [[0] * 4 for _ in range(4)] \n self.score = 0\n<例子3>\ndef __init__(self, width=10, height=10, mines=15):\n\n<例子4>\nx, y = event.x // 20, event.y // 20\n\n<例子5>\nraise ValueError(\"余额不足\")\n\n<例子6>\ntransfer(bank, \"123\", \"456\", 200)\n\n<例子7>\nbank.add_account(Account(\"123\", 1000))\n", + "no_example": "### 不能被判定为'出现魔法字符和数字'的例子\n<例子1>\n ACTIVE_STATUS = 1 \n if status == ACTIVE_STATUS:\n print(ACTIVE_STATUS)' \n" + }, + { + "id": 64, + "text": "boolean变量判断无需显式比较", + "detail": "缺陷类型:显式比较boolean变量;修复方案:直接使用boolean变量进行判断。", + "language": "Python", + "yes_example": "### 被判定为'显式比较boolean变量'的例子\n<例子1>flag = True \n if flag == True: \n print('Flag is true')\n<例子2>if self.game.is_game_over() == True: \n return<例子3>if self.canvas.drawings ==True:", + "no_example": "### 不能被判定为'显式比较boolean变量'的例子\n<例子1>flag = True \n if flag: \n print('Flag is true') \n" + }, + { + "id": 65, + "text": "避免使用type()检查对象类型", + "detail": "缺陷类型:避免使用type()检查对象类型;修复方案:使用isinstance()函数。", + "language": "Python", + "yes_example": "### 被判定为'避免使用type()检查对象类型'的例子\n<例子1>\n if type(obj) == list: \n print('obj is a list')", + "no_example": "### 不能被判定为'避免使用type()检查对象类型'的例子\n<例子1>\n if isinstance(obj, list): \n print('obj is a list') \n" + }, + { + "id": 66, + "text": "避免使用os.system()来调用外部命令", + "detail": "缺陷类型:使用os.system()调用外部命令;修复方案:使用subprocess模块。", + "language": "Python", + "yes_example": "### 被判定为'使用os.system()来调用外部命令'的例子\n<例子1>os.system('ls -l')\n<例子2>os.system('ls -l')", + "no_example": "### 不能被判定为'使用os.system()来调用外部命令'的例子\n<例子1>import subprocess \n subprocess.run(['ls', '-l'])" + }, + { + "id": 67, + "text": "只使用@property装饰器创建只读属性,而非修改属性", + "detail": "缺陷类型:使用@property装饰器创建可修改属性;修复方案:只使用@property装饰器创建只读属性。", + "language": "Python", + "yes_example": "### 被判定为'使用@property装饰器来创建可修改属性'的例子\n<例子1>@property \n def value(self, new_value): \n self._value = new_value\n<例子2>@property \n def game_over(self): \n return self._is_game_over() \n def _is_game_over(self): \n pass", + "no_example": "### 不能被判定为'使用@property装饰器来创建可修改属性'的例子\n<例子1>@property \n def value(self): \n return self._value\n<例子2>@property \n def __str__(self): \n return 'Maze Game State'" + }, + { + "id": 68, + "text": "在使用索引或切片时,不要在方括号或冒号内加空格", + "detail": "缺陷类型:在索引或切片的方括号或冒号内加空格;修复方案:去掉方括号或冒号内的空格。", + "language": "Python", + "yes_example": "### 被判定为'在使用索引或切片时,在方括号或冒号内加空格'的例子\n<例子1>list = [1, 2, 3, 4] \n sublist = list[ 1 : 3 ]\n<例子2>start_point = self.canvas.drawings[ -1] \n<例子3>if head[ 0] < 0 or head[ 0] >= GRID_WIDTH or head[ 1] < 0 or head[ 1] >= GRID_HEIGHT:\n<例子4>for segment in self.snake[ 1:]:", + "no_example": "### 不能被判定为'在使用索引或切片时,在方括号或冒号内加空格'的例子\n<例子1>list = [1, 2, 3, 4] \n sublist = list[1:3]" + }, + { + "id": 69, + "text": "在逗号、分号或冒号前不要加空格,但在它们之后要加空格", + "detail": "缺陷类型:在逗号、分号或冒号前加空格或在它们之后不加空格;修复方案:在逗号、分号或冒号前不要加空格,但在它们之后要加空格。", + "language": "Python", + "yes_example": "### 被判定为'在逗号、分号或冒号前加空格,或没在它们之后加空格'的例子\n<例子1>if x == 4 : \n print(x , y)\n<例子2>if event.keysym == 'Up' or event.keysym == 'Down' or event.keysym == 'Left' or event.keysym == 'Right' :\n<例子3>x ,y = 1 ,2\n<例子4>def on_key_press(self , event) :\n<例子5>elif event.keysym == 'Down' ; \n<例子6>def update_status(self ,message: str) : \n pass ", + "no_example": "### 不能被判定为'在逗号、分号或冒号前加空格,或没在它们之后加空格'的例子\n<例子1>if x == 4: \n print(x, y)" + }, + { + "id": 70, + "text": "对于二元操作符,两边都应有空格", + "detail": "缺陷类型:二元操作符两边没有空格;修复方案:在二元操作符两边加空格", + "language": "Python", + "yes_example": "### 被判定为'二元操作符两边没有空格'的例子\n<例子1>a=b+1", + "no_example": "### 不能被判定为'二元操作符两边没有空格'的例子\n<例子1>a = b + 1\n<例子2>label = tk.Label(self.root, text=str(cell), bg='white')\n<例子3>label.grid(row=i, column=j)" + }, + { + "id": 71, + "text": "避免使用Python关键字作为变量名或函数名", + "detail": "缺陷类型:使用Python关键字作为变量名或函数名;修复方案:使用非关键字的名称。", + "language": "Python", + "yes_example": "### 被判定为'使用Python关键字作为变量名或函数名'的例子\n<例子1>def class(): \n pass\n<例子2>for = 5\n<例子3>def if(self): ", + "no_example": "### 不能被判定为'使用Python关键字作为变量名或函数名'的例子\n<例子1>def my_function(): \n pass\n<例子2>number = 5" + }, + { + "id": 72, + "text": "避免使用特殊字符作为变量名/方法名/类名,例如$或@", + "detail": "缺陷类型:使用特殊字符作为变量名/方法名/类名;修复方案:使用合法的变量名。", + "language": "Python", + "yes_example": "### 被判定为'使用特殊字符作为变量名/方法名/类名,例如$或@'的例子\n<例子1>my$var = 10\n<例子2>@var = 20\n<例子3>def add_score@(self, points): \n self.score += points\n<例子4>class @MyClass: \n pass\n<例子5>def mine@(self):", + "no_example": "### 不能被判定为'使用特殊字符作为变量名/方法名/类名,例如$或@'的例子\n<例子1>my_var = 10\n<例子2>var_20 = 20" + }, + { + "id": 73, + "text": "避免使用raise来重新抛出当前的异常,这会丢失原始的栈跟踪", + "detail": "缺陷类型:使用raise重新抛出当前异常;修复方案:使用raise ... from ...语法。", + "language": "Python", + "yes_example": "### 被判定为'避免使用raise来重新抛出当前的异常,这会丢失原始的栈跟踪'的例子\n<例子1>\n try: \n 1 / 0 \n except ZeroDivisionError: \n raise SomeException('新的异常信息')\n\n<例子2>\ntry:\n db.get_data()\nexcept ValueError as e:\n raise ValueError(\"Something went wrong!\")\n\n<例子3>\ntry:\n\traise Exception(\"形状添加失败\")\nexcept Exception as e:\n\tpass\n", + "no_example": "### 不能被判定为'避免使用raise来重新抛出当前的异常,这会丢失原始的栈跟踪'的例子\n<例子1>\n try: \n 1 / 0 \n except ZeroDivisionError as e: \n raise RuntimeError('Error occurred') from e \n\n<例子2>\n try: \n 1 / 0 \n except ZeroDivisionError as e: \n\tlogger.error(e)\n raise \n" + }, + { + "id": 74, + "text": "避免在except块中使用pass,这会捕获并忽略异常", + "detail": "缺陷类型:在except块中使用pass;修复方案:处理异常或记录日志。", + "language": "Python", + "yes_example": "### 被判定为'在except块中使用pass'的例子\n<例子1>\n try: \n 1 / 0 \n except ZeroDivisionError: \n pass \n \n<例子2>\n try: \n 1 / 0 \n except ZeroDivisionError: \n pass \n", + "no_example": "### 不能被判定为'在except块中使用pass'的例子\n<例子1>\n try: \n 1 / 0 \n except ZeroDivisionError as e: \n logging.error('Error occurred: %s', e) \n" + }, + { + "id": 75, + "text": "避免使用assert语句来执行重要的运行时检查", + "detail": "缺陷类型:使用assert语句执行重要的运行时检查;修复方案:使用显式的条件检查和异常处理。", + "language": "Python", + "yes_example": "### 被判定为'使用assert语句来执行重要的运行时检查'的例子\n<例子1>\n def divide(a, b): \n assert b != 0 \n return a / b \n", + "no_example": "### 不能被判定为'使用assert语句来执行重要的运行时检查'的例子\n<例子1>\n def divide(a, b): \n if b == 0: \n raise ValueError('b cannot be zero') \n return a / b \n" + }, + { + "id": 76, + "text": "避免使用eval()和exec(),这些函数可能会带来安全风险", + "detail": "缺陷类型:使用eval()和exec()函数;修复方案:使用安全的替代方案。", + "language": "Python", + "yes_example": "### 被判定为'使用eval()和exec()'的例子\n<例子1>\n eval('print(1)') \n\n<例子2> \n exec('a = 1') \n", + "no_example": "### 不能被判定为'使用eval()和exec()'的例子\n<例子1>\n compiled_code = compile('print(1)', '', 'exec') \n exec(compiled_code) \n" + }, + { + "id": 77, + "text": "避免使用sys.exit(),应使用异常来控制程序的退出", + "detail": "缺陷类型:避免使用sys.exit(),应使用异常来控制程序的退出;修复方案:使用异常来控制程序的退出。", + "language": "Python", + "yes_example": "### 被判定为'避免使用sys.exit(),应使用异常来控制程序的退出'的例子\n<例子1>\n import sys\nsys.exit(1)\n\n<例子2>\n import sys \n sys.exit()\n\n<例子3>\nif event.type == pygame.QUIT:\n\tpygame.quit()\n\texit()\n\n<例子4>\n import sys \n sys.exit('退出程序'))\n", + "no_example": "### 不能被判定为'避免使用sys.exit(),应使用异常来控制程序的退出'的例子\n<例子1>\n raise SystemExit(1)\n" + }, + { + "id": 78, + "text": "避免使用time.sleep()进行线程同步,应使用同步原语,如锁或事件", + "detail": "缺陷类型:使用time.sleep()进行线程同步;修复方案:使用同步原语。", + "language": "Python", + "yes_example": "### 被判定为'使用time.sleep()进行线程同步'的例子\n<例子1>\n import time \n\n def worker(): \n time.sleep(1) \n\n<例子2>\n import time \n\n time.sleep(1) \n", + "no_example": "### 不能被判定为'使用time.sleep()进行线程同步'的例子\n<例子1>\n import threading \n\n event = threading.Event() \n\n def worker(): \n event.wait()\n" + }, + { + "id": 79, + "text": "每行代码避免超过79个字符", + "detail": "缺陷类型:每行代码避免超过79个字符;修复方案:将长行代码格式化为多行。", + "language": "Python", + "yes_example": "### 被判定为'每行代码避免超过79个字符'的例子\n<例子1>\n print('This is a very long line of code that exceeds the 79 characters limit........') \n", + "no_example": "### 不能被判定为'每行代码避免超过79个字符'的例子\n<例子1>\n print('This is a very long line of code that exceeds the 79 characters limit' + \n ' but it is split into two lines')\n" + }, + { + "id": 80, + "text": "模块级别的函数和类定义之间用两个空行分隔,类内部的方法定义之间用一个空行分隔", + "detail": "缺陷类型:模块级别的函数和类定义之间没有用两个空行分隔,类内部的方法定义之间没有用一个空行分隔;修复方案:按照规范添加空行。", + "language": "Python", + "yes_example": "### 被判定为'模块级别的函数和类定义之间没用两个空行分隔,类内部的方法定义之间没用一个空行分隔'的例子\n<例子1>\n def func1(): \n pass \n def func2(): \n pass \n\n<例子2>\n class MyClass: \n def method1(self): \n pass \n def method2(self): \n pass \n", + "no_example": "### 不能被判定为'模块级别的函数和类定义之间没用两个空行分隔,类内部的方法定义之间没用一个空行分隔'的例子\n<例子1>\n def func1(): \n pass \n\n\n def func2(): \n pass \n\n<例子2>\n class MyClass: \n def method1(self): \n pass \n\n def method2(self): \n pass \n" + }, + { + "id": 81, + "text": "使用小写字母和下划线分隔的方式命名变量和函数名", + "detail": "缺陷类型:变量和函数命名不符合小写字母和下划线分隔的方式;修复方案:使用小写字母和下划线分隔的方式命名。", + "language": "Python", + "yes_example": "### 被判定为'未使用小写字母和下划线分隔的方式命名变量和函数'的例子\n<例子1>\n def myFunction(): \n pass \n\n<例子2>\n myVariable = 10 \n\n<例子3>\n def Calculatesquareroot(self, x): \n return 1 \n", + "no_example": "### 不能被判定为'未使用小写字母和下划线分隔的方式命名变量和函数'的例子\n<例子1>\n def my_function(): \n pass \n\n<例子2>\n my_variable = 10 \n" + }, + { + "id": 82, + "text": "不允许使用print()函数来记录日志,使用logging模块等来记录日志", + "detail": "缺陷类型:使用print()函数记录日志;修复方案:使用logging模块记录日志。", + "language": "Python", + "yes_example": "### 被判定为'使用print()函数来记录日志'的例子\n<例子1>\n print('Error occurred') \n\n<例子2>\n print('打印的日志字符串内容') \n\n<例子3>\n task = 'xxx' \n print(task) \n\n<例子4>\n print(1)\n", + "no_example": "### 不能被判定为'使用print()函数来记录日志'的例子\n<例子1>\n import logging \n logging.error('Error occurred') \n" + } +] diff --git a/metagpt/tools/libs/cr.py b/metagpt/tools/libs/cr.py index 7f9a4716c..f802af620 100644 --- a/metagpt/tools/libs/cr.py +++ b/metagpt/tools/libs/cr.py @@ -3,6 +3,7 @@ from pathlib import Path from typing import Optional import aiofiles +from bs4 import BeautifulSoup from unidiff import PatchSet import metagpt.ext.cr @@ -29,7 +30,7 @@ class CodeReview: Args: patch_path: The local path of the patch file or the url of the pull request. Example: "/data/xxx-pr-1.patch", "https://github.com/xx/XX/pull/1362" cr_output_file: Output file path where code review comments will be saved. Example: "cr/xxx-pr-1.json" - cr_point_file: File path for specifying code review points. Defaults to a predefined file. + cr_point_file: File path for specifying code review points. Set `None` to use a predefined file. """ patch = await self._get_patch_content(patch_path) cr_point_file = cr_point_file if cr_point_file else Path(metagpt.ext.cr.__file__).parent / "points.json" @@ -45,7 +46,7 @@ class CodeReview: ) comments = await CodeReview_().run(patch, cr_points) cr_output_path.parent.mkdir(exist_ok=True, parents=True) - async with aiofiles.open(cr_output_path, "w") as f: + async with aiofiles.open(cr_output_path, "w", encoding="utf-8") as f: await f.write(json.dumps(comments, ensure_ascii=False)) await reporter.async_report(cr_output_path) @@ -65,7 +66,7 @@ class CodeReview: output_dir: File path where code review comments are stored. """ patch = await self._get_patch_content(patch_path) - async with aiofiles.open(cr_file, "r") as f: + async with aiofiles.open(cr_file, "r", encoding="utf-8") as f: comments = json.loads(await f.read()) await ModifyCode(pr="").run(patch, comments, output_dir) return f"The fixed patch files store in {output_dir}" @@ -75,12 +76,14 @@ class CodeReview: # async with aiohttp.ClientSession(trust_env=True) as client: # async with client.get(f"{patch_path}.diff", ) as resp: # patch_file_content = await resp.text() - browser = Browser() - browser.proxy = {"server": "http://127.0.0.1:20172"} - async with browser: + async with Browser() as browser: await browser.goto(f"{patch_path}.diff") patch_file_content = await browser.page.content() - + if patch_file_content.startswith(""): + soup = BeautifulSoup(patch_file_content, "html.parser") + pre = soup.find("pre") + if pre: + patch_file_content = pre.text else: async with aiofiles.open(patch_path) as f: patch_file_content = await f.read() From dbc017c5c742a28f81027dfc474f9502ab741644 Mon Sep 17 00:00:00 2001 From: shenchucheng Date: Thu, 11 Jul 2024 14:11:57 +0800 Subject: [PATCH 077/119] add proxy for browser --- metagpt/tools/libs/browser.py | 3 ++- metagpt/utils/proxy_env.py | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 metagpt/utils/proxy_env.py diff --git a/metagpt/tools/libs/browser.py b/metagpt/tools/libs/browser.py index 864996e8c..072eebde1 100644 --- a/metagpt/tools/libs/browser.py +++ b/metagpt/tools/libs/browser.py @@ -24,6 +24,7 @@ from metagpt.utils.a11y_tree import ( scroll_page, type_text, ) +from metagpt.utils.proxy_env import get_proxy_from_env from metagpt.utils.report import BrowserReporter @@ -72,7 +73,7 @@ class Browser: self.page: Optional[Page] = None self.accessibility_tree: list = [] self.headless: bool = True - self.proxy = None + self.proxy = get_proxy_from_env() self.is_empty_page = True self.reporter = BrowserReporter() diff --git a/metagpt/utils/proxy_env.py b/metagpt/utils/proxy_env.py new file mode 100644 index 000000000..bcb5c84f5 --- /dev/null +++ b/metagpt/utils/proxy_env.py @@ -0,0 +1,19 @@ +import os + + +def get_proxy_from_env(): + proxy_config = {} + server = None + for i in ("ALL_PROXY", "all_proxy", "HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"): + if os.environ.get(i): + server = os.environ.get(i) + if server: + proxy_config["server"] = server + no_proxy = os.environ.get("NO_PROXY") or os.environ.get("no_proxy") + if no_proxy: + proxy_config["bypass"] = no_proxy + + if not proxy_config: + proxy_config = None + + return proxy_config From ec021bd0e14baf06dc7c992fddb5488780e4fd96 Mon Sep 17 00:00:00 2001 From: shenchucheng Date: Thu, 11 Jul 2024 14:25:24 +0800 Subject: [PATCH 078/119] fix some bugs --- metagpt/environment/mgx/mgx_env.py | 2 +- metagpt/tools/libs/browser.py | 2 +- metagpt/tools/libs/terminal.py | 3 ++- metagpt/utils/token_counter.py | 4 +++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/metagpt/environment/mgx/mgx_env.py b/metagpt/environment/mgx/mgx_env.py index 33160cfd8..69f80c2ff 100644 --- a/metagpt/environment/mgx/mgx_env.py +++ b/metagpt/environment/mgx/mgx_env.py @@ -27,7 +27,7 @@ class MGXEnv(Environment): def publish_message(self, message: Message, user_defined_recipient: str = "", publicer: str = "") -> bool: """let the team leader take over message publishing""" - tl = self.get_role("Tim") # TeamLeader's name is Tim + tl = self.get_role("Mike") # TeamLeader's name is Mike if user_defined_recipient: # human user's direct chat message to a certain role diff --git a/metagpt/tools/libs/browser.py b/metagpt/tools/libs/browser.py index 072eebde1..4a6d13eec 100644 --- a/metagpt/tools/libs/browser.py +++ b/metagpt/tools/libs/browser.py @@ -121,7 +121,7 @@ class Browser: await scroll_page(self.page, direction) return await self._wait_page() - async def goto(self, url: str, timeout: float = 30000): + async def goto(self, url: str, timeout: float = 90000): """Navigate to a specific URL.""" if self.page is None: await self.start() diff --git a/metagpt/tools/libs/terminal.py b/metagpt/tools/libs/terminal.py index a736d476a..b54adf0d4 100644 --- a/metagpt/tools/libs/terminal.py +++ b/metagpt/tools/libs/terminal.py @@ -115,8 +115,9 @@ class Terminal: # '\r' is changed to '\n', resulting in excessive output. tmp = b"" while True: - self.process.communicate() output = tmp + await self.process.stdout.read(1) + if not output: + continue *lines, tmp = output.splitlines(True) for line in lines: line = line.decode() diff --git a/metagpt/utils/token_counter.py b/metagpt/utils/token_counter.py index 63e2f8736..f6440b6ef 100644 --- a/metagpt/utils/token_counter.py +++ b/metagpt/utils/token_counter.py @@ -31,9 +31,9 @@ TOKEN_COSTS = { "gpt-4-0125-preview": {"prompt": 0.01, "completion": 0.03}, "gpt-4-1106-preview": {"prompt": 0.01, "completion": 0.03}, "gpt-4-vision-preview": {"prompt": 0.01, "completion": 0.03}, # TODO add extra image price calculator - "gpt-4-1106-vision-preview": {"prompt": 0.01, "completion": 0.03}, "gpt-4o": {"prompt": 0.005, "completion": 0.015}, "gpt-4o-2024-05-13": {"prompt": 0.005, "completion": 0.015}, + "gpt-4-1106-vision-preview": {"prompt": 0.01, "completion": 0.03}, "text-embedding-ada-002": {"prompt": 0.0004, "completion": 0.0}, "glm-3-turbo": {"prompt": 0.0007, "completion": 0.0007}, # 128k version, prompt + completion tokens=0.005¥/k-tokens "glm-4": {"prompt": 0.014, "completion": 0.014}, # 128k version, prompt + completion tokens=0.1¥/k-tokens @@ -147,6 +147,8 @@ FIREWORKS_GRADE_TOKEN_COSTS = { # https://platform.openai.com/docs/models/gpt-4-and-gpt-4-turbo TOKEN_MAX = { + "gpt-4o-2024-05-13": 128000, + "gpt-4o": 128000, "gpt-4-0125-preview": 128000, "gpt-4-turbo-preview": 128000, "gpt-4-1106-preview": 128000, From 4e141aeb495480f7aa65bae19db5f43287495874 Mon Sep 17 00:00:00 2001 From: your-username Date: Thu, 11 Jul 2024 16:10:26 +0800 Subject: [PATCH 079/119] =?UTF-8?q?update:=20=E6=8F=90=E7=A4=BA=E8=AF=8D?= =?UTF-8?q?=20ex=20=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- metagpt/prompts/di/swe_agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/prompts/di/swe_agent.py b/metagpt/prompts/di/swe_agent.py index 51fc1621e..fd95dc997 100644 --- a/metagpt/prompts/di/swe_agent.py +++ b/metagpt/prompts/di/swe_agent.py @@ -226,7 +226,7 @@ IMPORTANT_TIPS = """ 14. If provided an issue link, you MUST go to the issue page using Browser tool to understand the issue before starting your fix. -15. When the edit fails, try to enlarge the starting line, The edit function needs to use `Bash.Run` call. +15. When the edit fails, try to enlarge the starting line. """ NEXT_STEP_TEMPLATE = f""" From eebec4a5e88a0be8e17f2cfbbd76c30679c5d668 Mon Sep 17 00:00:00 2001 From: shenchucheng Date: Thu, 11 Jul 2024 17:56:28 +0800 Subject: [PATCH 080/119] add quick think reporter --- metagpt/roles/di/role_zero.py | 7 +++++-- metagpt/utils/report.py | 4 ---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/metagpt/roles/di/role_zero.py b/metagpt/roles/di/role_zero.py index 234064088..995e7e5d8 100644 --- a/metagpt/roles/di/role_zero.py +++ b/metagpt/roles/di/role_zero.py @@ -158,7 +158,8 @@ class RoleZero(Role): break context = self.llm.format_msg(memory + [UserMessage(content=prompt)]) # print(*context, sep="\n" + "*" * 5 + "\n") - async with ThoughtReporter(enable_llm_stream=True): + async with ThoughtReporter(enable_llm_stream=True) as reporter: + await reporter.async_report({"type": "react"}) self.command_rsp = await self.llm.aask(context, system_msgs=self.system_msg) self.rc.memory.add(AIMessage(content=self.command_rsp)) @@ -216,7 +217,9 @@ class RoleZero(Role): return rsp_msg context = self.llm.format_msg(self.get_memories(k=4) + [UserMessage(content=QUICK_THINK_PROMPT)]) - rsp = await self.llm.aask(context) + async with ThoughtReporter(enable_llm_stream=True) as reporter: + await reporter.async_report({"type": "quick"}) + rsp = await self.llm.aask(context) pattern = r"#YES#,? ?" if re.search(pattern, rsp): diff --git a/metagpt/utils/report.py b/metagpt/utils/report.py index ed40e5dde..5d1cd93e4 100644 --- a/metagpt/utils/report.py +++ b/metagpt/utils/report.py @@ -266,10 +266,6 @@ class ThoughtReporter(ObjectReporter): block: Literal[BlockType.THOUGHT] = BlockType.THOUGHT - async def __aenter__(self): - await self.async_report({}) - return await super().__aenter__() - class FileReporter(ResourceReporter): """File resource callback for reporting complete file paths. From fb4446c0a963918598296e7095b375088ea19acd Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Thu, 11 Jul 2024 18:01:35 +0800 Subject: [PATCH 081/119] update serialize_req to make the request more concise --- metagpt/exp_pool/serializers/role_zero.py | 36 ++++++++++++++++++----- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/metagpt/exp_pool/serializers/role_zero.py b/metagpt/exp_pool/serializers/role_zero.py index 7876ef12a..82a32e36b 100644 --- a/metagpt/exp_pool/serializers/role_zero.py +++ b/metagpt/exp_pool/serializers/role_zero.py @@ -11,8 +11,7 @@ class RoleZeroSerializer(SimpleSerializer): def serialize_req(self, req: list[dict]) -> str: """Serialize the request for database storage, ensuring it is a string. - This function modifies the content of the last element in the request to remove unnecessary sections, - making the request more concise. + This function does not modify `req`; it only extracts the necessary content from `req` because `req` may be very lengthy and could cause embedding errors. Args: req (list[dict]): The request to be serialized. Example: @@ -28,8 +27,33 @@ class RoleZeroSerializer(SimpleSerializer): if not req: return "" - req_copy = copy.deepcopy(req) - last_content = req_copy[-1]["content"] + filtered_req = self._filter_req(req) + self._clean_last_entry_content(filtered_req) + + return json.dumps(filtered_req) + + def _filter_req(self, req: list[dict]) -> list[dict]: + """Filter the request to include only necessary items and the last entry. + + Args: + req (list[dict]): The original request. + + Returns: + list[dict]: The filtered request. + """ + + filtered_req = [ + copy.deepcopy(item) for item in req if "Command Editor.read executed: file_path" in item["content"] + ] + filtered_req.append(copy.deepcopy(req[-1])) + + return filtered_req + + def _clean_last_entry_content(self, req: list[dict]): + """Modifies the content of the last element in the request to remove unnecessary sections, making the request more concise.""" + + last_content = req[-1]["content"] + last_content = RoleZeroContextBuilder.replace_content_between_markers( last_content, "# Data Structure", "# Current Plan", "" ) @@ -37,6 +61,4 @@ class RoleZeroSerializer(SimpleSerializer): last_content, "# Example", "# Instruction", "" ) - req_copy[-1]["content"] = last_content - - return json.dumps(req_copy) + req[-1]["content"] = last_content From 54ac38c093f26bda00dbdb3826e044ba8a7a489e Mon Sep 17 00:00:00 2001 From: zhanglei Date: Thu, 11 Jul 2024 20:02:06 +0800 Subject: [PATCH 082/119] update:cr --- metagpt/ext/cr/actions/code_review.py | 100 +++++++++++++------------- metagpt/ext/cr/points.json | 2 +- 2 files changed, 49 insertions(+), 53 deletions(-) diff --git a/metagpt/ext/cr/actions/code_review.py b/metagpt/ext/cr/actions/code_review.py index 2cbeeb3ee..99fef3bc4 100644 --- a/metagpt/ext/cr/actions/code_review.py +++ b/metagpt/ext/cr/actions/code_review.py @@ -5,7 +5,7 @@ import json import re -from unidiff import PatchedFile, PatchSet +from unidiff import PatchSet from metagpt.actions.action import Action from metagpt.ext.cr.utils.cleaner import ( @@ -19,6 +19,7 @@ from metagpt.utils.common import parse_json_code_block CODE_REVIEW_PROMPT_TEMPLATE = """ NOTICE +Let's think and work step by step. With the given pull-request(PR) Patch, and referenced Points(Code Standards), you should compare each point with the code one-by-one. The Patch code has added line number at the first character each line for reading, but the review should focus on new added code inside the `Patch` (lines starting with line number and '+'). @@ -52,12 +53,13 @@ CodeReview guidelines: - Don't suggest to add docstring unless it's necessary indeed. - If the same code error occurs multiple times, it cannot be omitted, and all places need to be identified.But Don't duplicate at the same place with the same comment! - Every line of code in the patch needs to be carefully checked, and laziness cannot be omitted. It is necessary to find out all the places. +- The `comment` and `point_id` in the Output must correspond to and belong to the same one `Point`. Just print the PR Patch comments in json format like **Output Format**. """ CODE_REVIEW_COMFIRM_SYSTEM_PROMPT = """ -You are a professional engineer with {code_language} stack, and good at code review comment result judgement. +You are a professional engineer with {code_language} stack, and good at code review comment result judgement.Let's think and work step by step. """ CODE_REVIEW_COMFIRM_TEMPLATE = """ @@ -89,25 +91,35 @@ class CodeReview(Action): new_comments = [] logger.debug(f"original comments: {comments}") for cmt in comments: - for p in points: - if int(cmt.get("point_id", -1)) == p.id: - code_start_line = cmt.get("code_start_line") - code_end_line = cmt.get("code_end_line") - code = get_code_block_from_patch(patch, code_start_line, code_end_line) + try: + if cmt.get("commented_file").endswith(".py"): + points = [p for p in points if p.language == "Python"] + elif cmt.get("commented_file").endswith(".java"): + points = [p for p in points if p.language == "Java"] + else: + continue + for p in points: + point_id = int(cmt.get("point_id", -1)) + if point_id == p.id: + code_start_line = cmt.get("code_start_line") + code_end_line = cmt.get("code_end_line") + code = get_code_block_from_patch(patch, code_start_line, code_end_line) - new_comments.append( - { - "commented_file": cmt.get("commented_file"), - "code": code, - "code_start_line": code_start_line, - "code_end_line": code_end_line, - "comment": cmt.get("comment"), - "point_id": p.id, - "point": p.text, - "point_detail": p.detail, - } - ) - break + new_comments.append( + { + "commented_file": cmt.get("commented_file"), + "code": code, + "code_start_line": code_start_line, + "code_end_line": code_end_line, + "comment": cmt.get("comment"), + "point_id": p.id, + "point": p.text, + "point_detail": p.detail, + } + ) + break + except Exception: + pass logger.debug(f"new_comments: {new_comments}") return new_comments @@ -151,43 +163,27 @@ class CodeReview(Action): logger.info(f"original comments num: {len(comments)}, confirmed comments num: {len(new_comments)}") return new_comments - async def cr_by_full_points(self, patch: PatchSet, points: list[Point]): + async def cr_by_points(self, patch: PatchSet, points: list[Point]): comments = [] - points_str = "\n".join([f"{p.id} {p.text}" for p in points]) for patched_file in patch: if patched_file.path.endswith(".py"): - points_str = "\n".join([f"{p.id} {p.text}" for p in points if p.language == "Python"]) + points = [p for p in points if p.language == "Python"] elif patched_file.path.endswith(".java"): - points_str = "\n".join([f"{p.id} {p.text}" for p in points if p.language == "Java"]) + points = [p for p in points if p.language == "Java"] else: continue - if len(str(patched_file).splitlines()) >= 50: - cr_by_segment_points_comments = await self.cr_by_segment_points( - patched_file=patched_file, points=points - ) - comments += cr_by_segment_points_comments - continue - prompt = CODE_REVIEW_PROMPT_TEMPLATE.format(patch=str(patched_file), points=points_str) - resp = await self.llm.aask(prompt) - json_str = parse_json_code_block(resp)[0] - comment = json.loads(json_str) - patched_file_path = patched_file.path - for c in comment: - c["commented_file"] = patched_file_path - comments += comment - - return comments - - async def cr_by_segment_points(self, patched_file: PatchedFile, points: list[Point]): - comments = [] - group_points = [points[i : i + 3] for i in range(0, len(points), 3)] - for group_point in group_points: - points_str = "\n".join([f"{p.id} {p.text}" for p in group_point]) - prompt = CODE_REVIEW_PROMPT_TEMPLATE.format(patch=str(patched_file), points=points_str) - resp = await self.llm.aask(prompt) - json_str = parse_json_code_block(resp)[0] - comments_batch = json.loads(json_str) - comments += comments_batch + group_points = [points[i : i + 3] for i in range(0, len(points), 3)] + for group_point in group_points: + points_str = "id description\n" + points_str += "\n".join([f"{p.id} {p.text}" for p in group_point]) + prompt = CODE_REVIEW_PROMPT_TEMPLATE.format(patch=str(patched_file), points=points_str) + resp = await self.llm.aask(prompt) + json_str = parse_json_code_block(resp)[0] + comments_batch = json.loads(json_str) + patched_file_path = patched_file.path + for c in comments_batch: + c["commented_file"] = patched_file_path + comments += comments_batch return comments @@ -196,7 +192,7 @@ class CodeReview(Action): patch: PatchSet = add_line_num_on_patch(patch) result = [] - comments = await self.cr_by_full_points(patch=patch, points=points) + comments = await self.cr_by_points(patch=patch, points=points) if len(comments) != 0: comments = self.format_comments(comments, points, patch) comments = await self.confirm_comments(patch=patch, comments=comments, points=points) diff --git a/metagpt/ext/cr/points.json b/metagpt/ext/cr/points.json index 5455d3865..6cdcd275d 100644 --- a/metagpt/ext/cr/points.json +++ b/metagpt/ext/cr/points.json @@ -21,7 +21,7 @@ "language": "Java", "detail": "缺陷类型:避免函数中未使用的形参;修复方案:忽略", "yes_example": "### 被判定为\"避免函数中未使用的形参\"的例子\n<例子1>\npublic void setTransientVariablesLocal(Map transientVariables) {\n throw new UnsupportedOperationException(\"No execution active, no variables can be set\");\n}这段代码中的形参\"transientVariables\"未在函数体内出现,所以这个被判定为\"避免函数中未使用的形参\"\n\n\n<例子2>\nprotected void modifyFetchPersistencePackageRequest(PersistencePackageRequest ppr, Map pathVars) {}\n这段代码中的形参\"ppr\"和\"pathVars\"未在函数体内出现,所以这个被判定为\"避免函数中未使用的形参\"\n", - "no_example": "### 不能被判定为\"避免函数中未使用的形参\"的例子\n<例子1>\npublic String processFindForm(@RequestParam(value = \"pageNo\", defaultValue = \"1\") int pageNo) {\n\tlastName = owner.getLastName();\n\treturn addPaginationModel(pageNo, paginationModel, lastName, ownersResults);\n}上述代码中的形参\"pageNo\"在当前函数'processFindForm'内被'return addPaginationModel(pageNo, paginationModel, lastName, ownersResults);'这一句被使用,虽然pageNo没有被用于逻辑计算,但作为了函数调用其他函数的参数使用了,所以这个不能被判定为\"避免函数中未使用的形参\"\n" + "no_example": "### 不能被判定为\"避免函数中未使用的形参\"的例子\n<例子1>\npublic String processFindForm(@RequestParam(value = \"pageNo\", defaultValue = \"1\") int pageNo) {\n\tlastName = owner.getLastName();\n\treturn addPaginationModel(pageNo, paginationModel, lastName, ownersResults);\n}这段代码中的形参\"pageNo\"在当前函数'processFindForm'内被'return addPaginationModel(pageNo, paginationModel, lastName, ownersResults);'这一句被使用,虽然pageNo没有被用于逻辑计算,但作为了函数调用其他函数的参数使用了,所以这个不能被判定为\"避免函数中未使用的形参\"\n\n<例子2>\npublic void formatDate(Date date) {\n\tSimpleDateFormat sdf = new SimpleDateFormat(\"yyyy-MM-dd\");\n\tSystem.out.println(\"Formatted date: \" + sdf.format(date));\n}这段代码中的形参date在System.out.println(\"Formatted date: \" + sdf.format(date))这一句中被引用到,所以这个不能被判定为\"避免函数中未使用的形参\"\n" }, { "id": 4, From 67c2e2c3c74994e7d4853d1b729e53aa72018421 Mon Sep 17 00:00:00 2001 From: zhanglei Date: Thu, 11 Jul 2024 20:33:26 +0800 Subject: [PATCH 083/119] update: CR --- metagpt/ext/cr/actions/code_review.py | 3 ++- metagpt/ext/cr/points.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/metagpt/ext/cr/actions/code_review.py b/metagpt/ext/cr/actions/code_review.py index 99fef3bc4..6d1519867 100644 --- a/metagpt/ext/cr/actions/code_review.py +++ b/metagpt/ext/cr/actions/code_review.py @@ -78,7 +78,8 @@ CODE_REVIEW_COMFIRM_TEMPLATE = """ ## Your Task: 1. First, check if the code meets the requirements and does not violate any defects. If it meets the requirements and does not violate any defects, print `False` and do not proceed with further judgment. -2. If the check in step 1 does not print `False`, proceed to further judgment. Based on the "Reference Example for Judgment" provided, determine if the "Code" and "Code Review Comments" match. If they match, print `True`; otherwise, print `False`. +2. Then, check if the "Code Review Comments" meets "Description of Defects".If they don't meet,print `False` and do not proceed with further judgment. +3. If the check in step 1 and step 2 do not print `False`, proceed to further judgment. Based on the "Reference Example for Judgment" provided, determine if the "Code" and "Code Review Comments" match. If they match, print `True`; otherwise, print `False`. Note: Your output should only be `True` or `False` without any explanations. """ diff --git a/metagpt/ext/cr/points.json b/metagpt/ext/cr/points.json index 6cdcd275d..10fc951c0 100644 --- a/metagpt/ext/cr/points.json +++ b/metagpt/ext/cr/points.json @@ -76,7 +76,7 @@ "text": "未使用equals()函数对 String 作比较", "language": "Java", "detail": "缺陷类型:未使用equals()函数对 String 作比较;对应Fixer:UnSynStaticDateFormatter Fixer;修复方案:使用equals()函数对 String 作比较", - "yes_example": "### 被判定为\"未使用equals()函数对 String 作比较\"的例子\n<例子1>\nif (existingPet != null && existingPet.getName() == petName) {\n\tresult.rejectValue(\"name\", \"duplicate\", \"already exists\");\n}这段代码中所涉及的existingPet.getName()和petName均是字符串,但是在if语句里做比较的时候使用了==而没有使用equals()对string做比较,所以这个被判定为\"未使用equals()函数对 String 作比较\"\n\n\n<例子2>\nString isOk = \"ok\";\nif (\"ok\" == isOk) {\n\tresult.rejectValue(\"name\", \"duplicate\", \"already exists\");\n}这段代码中的isOk是个字符串,但在if判断中与\"ok\"比较的时候使用的是==,未使用equals()对string做比较,所以这个被判定为\"未使用equals()函数对 String 作比较\"\n", + "yes_example": "### 被判定为\"未使用equals()函数对 String 作比较\"的例子\n<例子1>\nif (existingPet != null && existingPet.getName() == petName) {\n\tresult.rejectValue(\"name\", \"duplicate\", \"already exists\");\n}这段代码中所涉及的existingPet.getName()和petName均是字符串,但是在if语句里做比较的时候使用了==而没有使用equals()对string做比较,所以这个被判定为\"未使用equals()函数对 String 作比较\"\n\n\n<例子2>\nString isOk = \"ok\";\nif (\"ok\" == isOk) {\n\tresult.rejectValue(\"name\", \"duplicate\", \"already exists\");\n}这段代码中的isOk是个字符串,但在if判断中与\"ok\"比较的时候使用的是==,未使用equals()对string做比较,应该使用\"ok\".equals(isOk),所以这个被判定为\"未使用equals()函数对 String 作比较\"\n\n\n<例子3>\nString str1 = \"Hello\";\nString str2 = \"Hello\";\nif (str1 == str2) {\n\tSystem.out.println(\"str1 和 str2 引用相同\");\n} else {\n\tSystem.out.println(\"str1 和 str2 引用不同\");\n}\n这段代码中的if (str1 == str2) 使用了==进行str1和str2的比较,未使用equals()对string做比较,应该使用str1.equals(str2),所以这个被判定为\"未使用equals()函数对 String 作比较\"\n\n\n<例子4>\nString str = \"This is string\";\nif (str == \"This is not str\") {\n\treturn str;\n}这段代码中的if (str == \"This is not str\")使用了==进行字符串比较,未使用equals()对string做比较,\"This is not str\".equals(str),所以这个被判定为\"未使用equals()函数对 String 作比较\"\n", "no_example": "### 不能被判定为\"未使用equals()函数对 String 作比较\"的例子\n<例子1>\nif (PROPERTY_VALUE_YES.equalsIgnoreCase(readWriteReqNode))\n formProperty.setRequired(true);\n这段代码中的PROPERTY_VALUE_YES和readWriteReqNode均是字符串,在if语句里比较PROPERTY_VALUE_YES和readWriteReqNode的使用的是equalsIgnoreCase(字符串比较忽略大小写),所以equalsIgnoreCase也是符合使用equals()函数对 String 作比较的,所以这个不能被判定为\"未使用equals()函数对 String 作比较\"\n\n\n<例子2>\nString isOk = \"ok\";\nif (\"ok\".equals(isOk)) {\n\tresult.rejectValue(\"name\", \"duplicate\", \"already exists\");\n}这段代码中的isOk是个字符串,在if判断中与\"ok\"比较的时候使用的是equals()对string做比较,所以这个不能被判定为\"未使用equals()函数对 String 作比较\"\n" }, { From 39360b41c5c69c2b926e56562a5d627b28afb000 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Thu, 11 Jul 2024 22:14:04 +0800 Subject: [PATCH 084/119] init exp pool --- .../exp_pool/init_exp_pool/engineer_exps.py | 115 +++++++++++ examples/exp_pool/init_exp_pool/main.py | 55 ++++++ .../init_exp_pool/team_leader_exps.py | 181 ++++++++++++++++++ metagpt/exp_pool/manager.py | 40 +--- tests/metagpt/exp_pool/test_manager.py | 18 -- .../test_serializers/test_action_node.py | 35 ++++ .../test_serializers/test_role_zero.py | 77 ++++++++ .../exp_pool/test_serializers/test_simple.py | 44 +++++ 8 files changed, 512 insertions(+), 53 deletions(-) create mode 100644 examples/exp_pool/init_exp_pool/engineer_exps.py create mode 100644 examples/exp_pool/init_exp_pool/main.py create mode 100644 examples/exp_pool/init_exp_pool/team_leader_exps.py create mode 100644 tests/metagpt/exp_pool/test_serializers/test_action_node.py create mode 100644 tests/metagpt/exp_pool/test_serializers/test_role_zero.py create mode 100644 tests/metagpt/exp_pool/test_serializers/test_simple.py diff --git a/examples/exp_pool/init_exp_pool/engineer_exps.py b/examples/exp_pool/init_exp_pool/engineer_exps.py new file mode 100644 index 000000000..e494fdaa3 --- /dev/null +++ b/examples/exp_pool/init_exp_pool/engineer_exps.py @@ -0,0 +1,115 @@ +import json + +OBJ1 = [ + { + "role": "user", + "content": '\n# Data Structure\n\n\n# Current Plan\n{\'goal\': "Please write a 1024 game using JavaScript and HTML code without using any frameworks, user can play with keyboard. Refer to the system design located at \'/tmp/system_design.json\' and the project schedule at \'/tmp/project_schedule.json\' for detailed information.", \'tasks\': []}\n\n# Current Task\n\n\n# Example\n\n\n# Instruction\nBased on the context, write a plan or modify an existing plan to achieve the goal. A plan consists of one to 3 tasks.\nIf plan is created, you should track the progress and update the plan accordingly, such as Plan.finish_current_task, Plan.append_task, Plan.reset_task, Plan.replace_task, etc.\nWhen presented a current task, tackle the task using the available commands.\nPay close attention to new user message, review the conversation history, use RoleZero.reply_to_human to respond to new user requirement.\nNote:\n1. If you keeping encountering errors, unexpected situation, or you are not sure of proceeding, use RoleZero.ask_human to ask for help.\n2. Carefully review your progress at the current task, if your actions so far has not fulfilled the task instruction, you should continue with current task. Otherwise, finish current task.\n3. Each time you finish a task, use RoleZero.reply_to_human to report your progress.\n4. Each time you write a code in your response, write with the Editor directly without preparing a repetitive code block beforehand.\n5. Take on ONE task and write ONE code file in each response. DON\'T attempt all tasks in one response.\n6. When not specified, you should write files in a folder named "src". If you know the project path, then write in a "src" folder under the project path.\n7. When provided system design or project schedule, you MUST read them first before making a plan, then adhere to them in your implementation, especially in the programming language, package, or framework. You MUST implement all code files prescribed in the system design or project schedule. You can create a plan first with each task corresponding to implementing one code file.\n8. Write at most one file per task, do your best to implement THE ONLY ONE FILE. CAREFULLY CHECK THAT YOU DONT MISS ANY NECESSARY CLASS/FUNCTION IN THIS FILE.\n9. COMPLETE CODE: Your code will be part of the entire project, so please implement complete, reliable, reusable code snippets.\n10. When provided system design, YOU MUST FOLLOW "Data structures and interfaces". DONT CHANGE ANY DESIGN. Do not use public member functions that do not exist in your design.\n11. Write out EVERY CODE DETAIL, DON\'T LEAVE TODO.\n12. To modify code in a file, read the entire file, make changes, and update the file with the complete code, ensuring that no line numbers are included in the final write.\n13. When a system design or project schedule is provided, at the end of the plan, add a CodeRview Task for each file; for example, if there are three files, add three CodeRview Tasks. For each CodeRview Task, just call ReviewAndRewriteCode.run.\n\nPay close attention to the Example provided, you can reuse the example for your current situation if it fits.\nYou may use any of the available commands to create a plan or update the plan. You may output mutiple commands, they will be executed sequentially.\nIf you finish current task, you will automatically take the next task in the existing plan, use Plan.finish_task, DON\'T append a new task.\n\n# Your commands in a json array, in the following output format. If there is nothing to do, use the pass or end command:\nSome text indicating your thoughts, such as how you should update the plan status, respond to inquiry, or seek for help. Then a json array of commands. You must output ONE and ONLY ONE json array. DON\'T output multiple json arrays with thoughts between them.\n```json\n[\n {\n "command_name": str,\n "args": {"arg_name": arg_value, ...}\n },\n ...\n]\n```\nNotice: your output JSON data section must start with **```json [**\n', + } +] +OBJ2 = [ + { + "role": "user", + "content": "Command Editor.read executed: file_path='/tmp/system_design.json' block_content=\"...\"\n\nCommand Editor.read executed: file_path='/tmp/project_schedule.json' block_content=\"...\"", + }, + { + "role": "user", + "content": '\n# Data Structure\n\n\n# Current Plan\n{\'goal\': "Please write a 1024 game using JavaScript and HTML code without using any frameworks, user can play with keyboard. Refer to the system design located at \'/tmp/system_design.json\' and the project schedule at \'/tmp/project_schedule.json\' for detailed information.", \'tasks\': []}\n\n# Current Task\n\n\n# Example\n\n\n# Instruction\nBased on the context, write a plan or modify an existing plan to achieve the goal. A plan consists of one to 3 tasks.\nIf plan is created, you should track the progress and update the plan accordingly, such as Plan.finish_current_task, Plan.append_task, Plan.reset_task, Plan.replace_task, etc.\nWhen presented a current task, tackle the task using the available commands.\nPay close attention to new user message, review the conversation history, use RoleZero.reply_to_human to respond to new user requirement.\nNote:\n1. If you keeping encountering errors, unexpected situation, or you are not sure of proceeding, use RoleZero.ask_human to ask for help.\n2. Carefully review your progress at the current task, if your actions so far has not fulfilled the task instruction, you should continue with current task. Otherwise, finish current task.\n3. Each time you finish a task, use RoleZero.reply_to_human to report your progress.\n4. Each time you write a code in your response, write with the Editor directly without preparing a repetitive code block beforehand.\n5. Take on ONE task and write ONE code file in each response. DON\'T attempt all tasks in one response.\n6. When not specified, you should write files in a folder named "src". If you know the project path, then write in a "src" folder under the project path.\n7. When provided system design or project schedule, you MUST read them first before making a plan, then adhere to them in your implementation, especially in the programming language, package, or framework. You MUST implement all code files prescribed in the system design or project schedule. You can create a plan first with each task corresponding to implementing one code file.\n8. Write at most one file per task, do your best to implement THE ONLY ONE FILE. CAREFULLY CHECK THAT YOU DONT MISS ANY NECESSARY CLASS/FUNCTION IN THIS FILE.\n9. COMPLETE CODE: Your code will be part of the entire project, so please implement complete, reliable, reusable code snippets.\n10. When provided system design, YOU MUST FOLLOW "Data structures and interfaces". DONT CHANGE ANY DESIGN. Do not use public member functions that do not exist in your design.\n11. Write out EVERY CODE DETAIL, DON\'T LEAVE TODO.\n12. To modify code in a file, read the entire file, make changes, and update the file with the complete code, ensuring that no line numbers are included in the final write.\n13. When a system design or project schedule is provided, at the end of the plan, add a CodeRview Task for each file; for example, if there are three files, add three CodeRview Tasks. For each CodeRview Task, just call ReviewAndRewriteCode.run.\n\nPay close attention to the Example provided, you can reuse the example for your current situation if it fits.\nYou may use any of the available commands to create a plan or update the plan. You may output mutiple commands, they will be executed sequentially.\nIf you finish current task, you will automatically take the next task in the existing plan, use Plan.finish_task, DON\'T append a new task.\n\n# Your commands in a json array, in the following output format. If there is nothing to do, use the pass or end command:\nSome text indicating your thoughts, such as how you should update the plan status, respond to inquiry, or seek for help. Then a json array of commands. You must output ONE and ONLY ONE json array. DON\'T output multiple json arrays with thoughts between them.\n```json\n[\n {\n "command_name": str,\n "args": {"arg_name": arg_value, ...}\n },\n ...\n]\n```\nNotice: your output JSON data section must start with **```json [**\n', + }, +] + +REQ1 = json.dumps(OBJ1) +REQ2 = json.dumps(OBJ2) + +RESP1 = """ +Explanation: To begin the task of creating a 2048 game using JavaScript and HTML, I need to first read the system design and project schedule documents to understand the specific requirements and architecture outlined for the game development. + +```json +[ + { + "command_name": "Editor.read", + "args": { + "path": "/tmp/system_design.json" + } + }, + { + "command_name": "Editor.read", + "args": { + "path": "/tmp/project_schedule.json" + } + } +] +``` +""" +RESP2 = """ +Explanation: Based on the system design and project schedule, I will create a plan to implement the 2048 game. The plan will consist of tasks to create the necessary files (`index.html`, `style.css`, and `script.js`) and review each file to ensure they meet the design specifications. + +Here's the plan: + +1. **Task 1**: Create `index.html` - This file will contain the HTML structure necessary for the game's UI. +2. **Task 2**: Create `style.css` - This file will define the CSS styles to make the game visually appealing and responsive. +3. **Task 3**: Create `script.js` - This file will contain the JavaScript code for the game logic and UI interactions. +4. **Code Review Tasks**: Review each file to ensure they meet the project requirements and adhere to the system design. + +Let's start by appending the first task to the plan. + +```json +[ + { + "command_name": "Plan.append_task", + "args": { + "task_id": "1", + "dependent_task_ids": [], + "instruction": "Create the index.html file with the basic HTML structure for the 2048 game.", + "assignee": "Alex" + } + }, + { + "command_name": "Plan.append_task", + "args": { + "task_id": "2", + "dependent_task_ids": ["1"], + "instruction": "Create the style.css file with the necessary CSS to style the 2048 game.", + "assignee": "Alex" + } + }, + { + "command_name": "Plan.append_task", + "args": { + "task_id": "3", + "dependent_task_ids": ["1", "2"], + "instruction": "Create the script.js file containing the JavaScript logic for the 2048 game.", + "assignee": "Alex" + } + }, + { + "command_name": "Plan.append_task", + "args": { + "task_id": "4", + "dependent_task_ids": ["1"], + "instruction": "Use ReviewAndRewriteCode to review the code in index.html to ensure it meets the design specifications.", + "assignee": "Alex" + } + }, + { + "command_name": "Plan.append_task", + "args": { + "task_id": "5", + "dependent_task_ids": ["2"], + "instruction": "Use ReviewAndRewriteCode to review the code in style.css to ensure it meets the design specifications.", + "assignee": "Alex" + } + }, + { + "command_name": "Plan.append_task", + "args": { + "task_id": "6", + "dependent_task_ids": ["3"], + "instruction": "Use ReviewAndRewriteCode to review the code in script.js to ensure it meets the design specifications.", + "assignee": "Alex" + } + } +] +``` +""" + +ENGINEER_EXPERIENCES = ((REQ1, RESP1), (REQ2, RESP2)) diff --git a/examples/exp_pool/init_exp_pool/main.py b/examples/exp_pool/init_exp_pool/main.py new file mode 100644 index 000000000..849834513 --- /dev/null +++ b/examples/exp_pool/init_exp_pool/main.py @@ -0,0 +1,55 @@ +"""Init experience pool. + +Put some useful experiences into the experience pool. +""" + +import asyncio + +from examples.exp_pool.init_exp_pool.engineer_exps import ENGINEER_EXPERIENCES +from examples.exp_pool.init_exp_pool.team_leader_exps import TEAM_LEADER_EXPERIENCES +from metagpt.exp_pool import exp_manager +from metagpt.exp_pool.schema import EntryType, Experience, Metric, Score +from metagpt.logs import logger + + +async def add_exp(req: str, resp: str, tag: str, metric: Metric = None): + exp = Experience( + req=req, + resp=resp, + entry_type=EntryType.MANUAL, + tag=tag, + metric=metric or Metric(score=Score(val=10, reason="Manual")), + ) + + exp_manager.config.exp_pool.enable_write = True + exp_manager.create_exp(exp) + logger.info(f"New experience created for the request `{req[:10]}`.") + + +async def add_teamleader_exps(): + tag = "TeamLeader.llm_cached_aask" + + for req, resp in TEAM_LEADER_EXPERIENCES: + await add_exp(req=req, resp=resp, tag=tag) + + +async def add_engineer_exps(): + tag = "Engineer2.llm_cached_aask" + + for req, resp in ENGINEER_EXPERIENCES: + await add_exp(req=req, resp=resp, tag=tag) + + +def query_exps_count(): + count = exp_manager.get_exps_count() + logger.info(f"Experiences Count: {count}") + + +async def main(): + await add_teamleader_exps() + await add_engineer_exps() + query_exps_count() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/exp_pool/init_exp_pool/team_leader_exps.py b/examples/exp_pool/init_exp_pool/team_leader_exps.py new file mode 100644 index 000000000..da0bc3d81 --- /dev/null +++ b/examples/exp_pool/init_exp_pool/team_leader_exps.py @@ -0,0 +1,181 @@ +import json + +OBJ1 = [ + { + "role": "user", + "content": "\n# Data Structure\n\n\n# Current Plan\n{'goal': \"from to {''}: Create a cli snake game using Python.\", 'tasks': []}\n\n# Current Task\n\n\n# Example\n\n\n# Instruction\nYou are a team leader, and you are responsible for drafting tasks and routing tasks to your team members.\nYour team member:\nTim: Team Leader, \nAlice: Product Manager, efficiently create a successful product that meets market demands and user expectations\nBob: Architect, design a concise, usable, complete software system\nEve: Project Manager, break down tasks according to PRD/technical design, generate a task list, and analyze task dependencies to start with the prerequisite modules\nAlex: Engineer, Take on game, app, and web development\nDavid: DataAnalyst, Take on any data-related tasks, such as data analysis, machine learning, deep learning, web browsing, web scraping, web searching, web deployment, terminal operation, git and github operation, etc.\n\nYou should NOT assign consecutive tasks to the same team member, instead, assign an aggregated task (or the complete requirement) and let the team member to decompose it.\nWhen creating a new plan involving multiple members, create all tasks at once.\nIf plan is created, you should track the progress based on team member feedback message, and update plan accordingly, such as Plan.finish_current_task, Plan.reset_task, Plan.replace_task, etc.\nYou should use TeamLeader.publish_team_message to team members, asking them to start their task. DONT omit any necessary info such as path, link, environment, programming language, framework, requirement, constraint from original content to team members because you are their sole info source.\nPay close attention to new user message, review the conversation history, use RoleZero.reply_to_human to respond to the user directly, DON'T ask your team members.\n\nNote:\n1. If the requirement is a pure DATA-RELATED requirement, such as bug fixes, issue reporting, environment setup, terminal operations, pip install, web browsing, web scraping, web searching, web imitation, data science, data analysis, machine learning, deep learning, text-to-image etc. DON'T decompose it, assign a single task with the original user requirement as instruction directly to Data Analyst.\n2. If the requirement is developing a software, game, app, or website, excluding the above data-related tasks, you should decompose the requirement into multiple tasks and assign them to different team members based on their expertise, usually the sequence of Product Manager -> Architect -> Project Manager -> Engineer -> (optional: QaEngine if present) -> (optional: DataAnalyst if user requests deployment), each assigned ONE task. When publishing message to Product Manager, you should directly copy the full original user requirement.\n3. If the requirement contains both DATA-RELATED part mentioned in 1 and software development part mentioned in 2, you should decompose the software development part and assign them to different team members based on their expertise, and assign the DATA-RELATED part to Data Analyst David directly.\n4. If the requirement is a common-sense, logical, or math problem, you should respond directly without assigning any task to team members.\n5. If you think the requirement is not clear or ambiguous, you should ask the user for clarification immediately. Assign tasks only after all info is clear.\n6. It is helpful for Engineer to have both the system design and the project schedule for writing the code, so include paths of both files (if available) and remind Engineer to definitely read them when publishing message to Engineer.\n7. If the requirement is writing a TRD and software framework, you should assign it to Architect. When publishing message to Architect, you should directly copy the full original user requirement.\n\nPay close attention to the Example provided, you can reuse the example for your current situation if it fits.\nYou may use any of the available commands to create a plan or update the plan. You may output mutiple commands, they will be executed sequentially.\nIf you finish current task, you will automatically take the next task in the existing plan, use Plan.finish_task, DON'T append a new task.\n\n# Your commands in a json array, in the following output format. If there is nothing to do, use the pass or end command:\nSome text indicating your thoughts, such as how you should update the plan status, respond to inquiry, or seek for help. Then a json array of commands. You must output ONE and ONLY ONE json array. DON'T output multiple json arrays with thoughts between them.\n```json\n[\n {\n \"command_name\": str,\n \"args\": {\"arg_name\": arg_value, ...}\n },\n ...\n]\n```\nNotice: your output JSON data section must start with **```json [**\n", + } +] +OBJ2 = [ + { + "role": "user", + "content": "\n# Data Structure\n\n\n# Current Plan\n{'goal': \"from to {''}: Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.\", 'tasks': []}\n\n# Current Task\n\n\n# Example\n\n\n# Instruction\nYou are a team leader, and you are responsible for drafting tasks and routing tasks to your team members.\nYour team member:\nTim: Team Leader, \nAlice: Product Manager, efficiently create a successful product that meets market demands and user expectations\nBob: Architect, design a concise, usable, complete software system\nEve: Project Manager, break down tasks according to PRD/technical design, generate a task list, and analyze task dependencies to start with the prerequisite modules\nAlex: Engineer, Take on game, app, and web development\nDavid: DataAnalyst, Take on any data-related tasks, such as data analysis, machine learning, deep learning, web browsing, web scraping, web searching, web deployment, terminal operation, git and github operation, etc.\n\nYou should NOT assign consecutive tasks to the same team member, instead, assign an aggregated task (or the complete requirement) and let the team member to decompose it.\nWhen creating a new plan involving multiple members, create all tasks at once.\nIf plan is created, you should track the progress based on team member feedback message, and update plan accordingly, such as Plan.finish_current_task, Plan.reset_task, Plan.replace_task, etc.\nYou should use TeamLeader.publish_team_message to team members, asking them to start their task. DONT omit any necessary info such as path, link, environment, programming language, framework, requirement, constraint from original content to team members because you are their sole info source.\nPay close attention to new user message, review the conversation history, use RoleZero.reply_to_human to respond to the user directly, DON'T ask your team members.\n\nNote:\n1. If the requirement is a pure DATA-RELATED requirement, such as bug fixes, issue reporting, environment setup, terminal operations, pip install, web browsing, web scraping, web searching, web imitation, data science, data analysis, machine learning, deep learning, text-to-image etc. DON'T decompose it, assign a single task with the original user requirement as instruction directly to Data Analyst.\n2. If the requirement is developing a software, game, app, or website, excluding the above data-related tasks, you should decompose the requirement into multiple tasks and assign them to different team members based on their expertise, usually the sequence of Product Manager -> Architect -> Project Manager -> Engineer -> (optional: QaEngine if present) -> (optional: DataAnalyst if user requests deployment), each assigned ONE task. When publishing message to Product Manager, you should directly copy the full original user requirement.\n3. If the requirement contains both DATA-RELATED part mentioned in 1 and software development part mentioned in 2, you should decompose the software development part and assign them to different team members based on their expertise, and assign the DATA-RELATED part to Data Analyst David directly.\n4. If the requirement is a common-sense, logical, or math problem, you should respond directly without assigning any task to team members.\n5. If you think the requirement is not clear or ambiguous, you should ask the user for clarification immediately. Assign tasks only after all info is clear.\n6. It is helpful for Engineer to have both the system design and the project schedule for writing the code, so include paths of both files (if available) and remind Engineer to definitely read them when publishing message to Engineer.\n7. If the requirement is writing a TRD and software framework, you should assign it to Architect. When publishing message to Architect, you should directly copy the full original user requirement.\n\nPay close attention to the Example provided, you can reuse the example for your current situation if it fits.\nYou may use any of the available commands to create a plan or update the plan. You may output mutiple commands, they will be executed sequentially.\nIf you finish current task, you will automatically take the next task in the existing plan, use Plan.finish_task, DON'T append a new task.\n\n# Your commands in a json array, in the following output format. If there is nothing to do, use the pass or end command:\nSome text indicating your thoughts, such as how you should update the plan status, respond to inquiry, or seek for help. Then a json array of commands. You must output ONE and ONLY ONE json array. DON'T output multiple json arrays with thoughts between them.\n```json\n[\n {\n \"command_name\": str,\n \"args\": {\"arg_name\": arg_value, ...}\n },\n ...\n]\n```\nNotice: your output JSON data section must start with **```json [**\n", + } +] +OBJ4 = [ + { + "role": "user", + "content": "\n# Data Structure\n\n\n# Current Plan\n{'goal': \"from to {''}: how does the project go?\", 'tasks': []}\n\n# Current Task\n\n\n# Example\n\n\n# Instruction\nYou are a team leader, and you are responsible for drafting tasks and routing tasks to your team members.\nYour team member:\nTim: Team Leader, \nAlice: Product Manager, efficiently create a successful product that meets market demands and user expectations\nBob: Architect, design a concise, usable, complete software system\nEve: Project Manager, break down tasks according to PRD/technical design, generate a task list, and analyze task dependencies to start with the prerequisite modules\nAlex: Engineer, Take on game, app, and web development\nDavid: DataAnalyst, Take on any data-related tasks, such as data analysis, machine learning, deep learning, web browsing, web scraping, web searching, web deployment, terminal operation, git and github operation, etc.\n\nYou should NOT assign consecutive tasks to the same team member, instead, assign an aggregated task (or the complete requirement) and let the team member to decompose it.\nWhen creating a new plan involving multiple members, create all tasks at once.\nIf plan is created, you should track the progress based on team member feedback message, and update plan accordingly, such as Plan.finish_current_task, Plan.reset_task, Plan.replace_task, etc.\nYou should use TeamLeader.publish_team_message to team members, asking them to start their task. DONT omit any necessary info such as path, link, environment, programming language, framework, requirement, constraint from original content to team members because you are their sole info source.\nPay close attention to new user message, review the conversation history, use RoleZero.reply_to_human to respond to the user directly, DON'T ask your team members.\n\nNote:\n1. If the requirement is a pure DATA-RELATED requirement, such as bug fixes, issue reporting, environment setup, terminal operations, pip install, web browsing, web scraping, web searching, web imitation, data science, data analysis, machine learning, deep learning, text-to-image etc. DON'T decompose it, assign a single task with the original user requirement as instruction directly to Data Analyst.\n2. If the requirement is developing a software, game, app, or website, excluding the above data-related tasks, you should decompose the requirement into multiple tasks and assign them to different team members based on their expertise, usually the sequence of Product Manager -> Architect -> Project Manager -> Engineer -> (optional: QaEngine if present) -> (optional: DataAnalyst if user requests deployment), each assigned ONE task. When publishing message to Product Manager, you should directly copy the full original user requirement.\n3. If the requirement contains both DATA-RELATED part mentioned in 1 and software development part mentioned in 2, you should decompose the software development part and assign them to different team members based on their expertise, and assign the DATA-RELATED part to Data Analyst David directly.\n4. If the requirement is a common-sense, logical, or math problem, you should respond directly without assigning any task to team members.\n5. If you think the requirement is not clear or ambiguous, you should ask the user for clarification immediately. Assign tasks only after all info is clear.\n6. It is helpful for Engineer to have both the system design and the project schedule for writing the code, so include paths of both files (if available) and remind Engineer to definitely read them when publishing message to Engineer.\n7. If the requirement is writing a TRD and software framework, you should assign it to Architect. When publishing message to Architect, you should directly copy the full original user requirement.\n\nPay close attention to the Example provided, you can reuse the example for your current situation if it fits.\nYou may use any of the available commands to create a plan or update the plan. You may output mutiple commands, they will be executed sequentially.\nIf you finish current task, you will automatically take the next task in the existing plan, use Plan.finish_task, DON'T append a new task.\n\n# Your commands in a json array, in the following output format. If there is nothing to do, use the pass or end command:\nSome text indicating your thoughts, such as how you should update the plan status, respond to inquiry, or seek for help. Then a json array of commands. You must output ONE and ONLY ONE json array. DON'T output multiple json arrays with thoughts between them.\n```json\n[\n {\n \"command_name\": str,\n \"args\": {\"arg_name\": arg_value, ...}\n },\n ...\n]\n```\nNotice: your output JSON data section must start with **```json [**\n", + } +] + +REQ1 = json.dumps(OBJ1) +REQ2 = json.dumps(OBJ2) +REQ3 = """Conversation History: +[ + ..., + {'role': 'assistant', 'content': 'from Alice(Product Manager) to {'Bob'}: {'docs': {'20240424153821.json': {'root_path': 'docs/prd', 'filename': '20240424153821.json', 'content': '{"Language":"en_us","Programming Language":"Python","Original Requirements":"create a cli snake game","Project Name":"snake_game","Product Goals":["Develop an intuitive and addictive snake game",...], ...}}}}}, +] +""" +REQ4 = json.dumps(OBJ4) + +RESP1 = """ +Explanation: The requirement is about software development. Assign each tasks to a different team member based on their expertise. When publishing message to Product Manager, we copy original user requirement directly to ensure no information loss. +```json +[ + { + "command_name": "Plan.append_task", + "args": { + "task_id": "1", + "dependent_task_ids": [], + "instruction": "Create a product requirement document (PRD) outlining the features, user interface, and user experience of the CLI python snake game.", + "assignee": "Alice" + } + }, + { + "command_name": "Plan.append_task", + "args": { + "task_id": "2", + "dependent_task_ids": ["1"], + "instruction": "Design the software architecture for the CLI snake game, including the choice of programming language, libraries, and data flow.", + "assignee": "Bob" + } + }, + { + "command_name": "Plan.append_task", + "args": { + "task_id": "3", + "dependent_task_ids": ["2"], + "instruction": "Break down the architecture into manageable tasks, identify task dependencies, and prepare a detailed task list for implementation.", + "assignee": "Eve" + } + }, + { + "command_name": "Plan.append_task", + "args": { + "task_id": "4", + "dependent_task_ids": ["3"], + "instruction": "Implement the core game logic for the CLI snake game, including snake movement, food generation, and score tracking.", + "assignee": "Alex" + } + }, + { + "command_name": "Plan.append_task", + "args": { + "task_id": "5", + "dependent_task_ids": ["4"], + "instruction": "Write comprehensive tests for the game logic and user interface to ensure functionality and reliability.", + "assignee": "Edward" + } + }, + { + "command_name": "TeamLeader.publish_message", + "args": { + "content": "Create a cli snake game using Python", + "send_to": "Alice" + } + }, + { + "command_name": "RoleZero.reply_to_human", + "args": { + "content": "I have assigned the tasks to the team members. Alice will create the PRD, Bob will design the software architecture, Eve will break down the architecture into tasks, Alex will implement the core game logic, and Edward will write comprehensive tests. The team will work on the project accordingly", + } + }, + { + "command_name": "end" + } +] +``` +""" +RESP2 = """ +Explanation: DON'T decompose requirement if it is a DATA-RELATED task, assign a single task directly to Data Analyst David. He will manage the decomposition and implementation. +```json +[ + { + "command_name": "Plan.append_task", + "args": { + "task_id": "1", + "dependent_task_ids": [], + "instruction": "Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.", + "assignee": "David" + } + }, + { + "command_name": "TeamLeader.publish_message", + "args": { + "content": "Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.", + "send_to": "David" + } + }, + { + "command_name": "RoleZero.reply_to_human", + "args": { + "content": "I have assigned the task to David. He will break down the task further by himself and starts solving it.", + } + }, + { + "command_name": "end" + } +] +``` +""" +RESP3 = """ +Explanation: You received a message from Alice, the Product Manager, that she has completed the PRD, use Plan.finish_current_task to mark her task as finished and moves the plan to the next task. Based on plan status, next task is for Bob (Architect), publish a message asking him to start. The message content should contain important path info. +```json +[ + { + "command_name": "Plan.finish_current_task", + "args": {} + }, + { + "command_name": "TeamLeader.publish_message", + "args": { + "content": "Please design the software architecture for the snake game based on the PRD created by Alice. The PRD is at 'docs/prd/20240424153821.json'. Include the choice of programming language, libraries, and data flow, etc.", + "send_to": "Bob" + } + }, + { + "command_name": "RoleZero.reply_to_human", + "args": { + "content": "Alice has completed the PRD. I have marked her task as finished and sent the PRD to Bob. Bob will work on the software architecture.", + } + }, + { + "command_name": "end" + } +] +``` +""" +RESP4 = """ +Explanation: The user is asking for a general update on the project status. Give a straight answer about the current task the team is working on and provide a summary of the completed tasks. +```json +[ + { + "command_name": "RoleZero.reply_to_human", + "args": { + "content": "The team is currently working on ... We have completed ...", + } + }, + { + "command_name": "end" + } +] +``` +""" + +TEAM_LEADER_EXPERIENCES = ( + (REQ1, RESP1), + (REQ2, RESP2), + (REQ3, RESP3), + (REQ4, RESP4), +) diff --git a/metagpt/exp_pool/manager.py b/metagpt/exp_pool/manager.py index 649210a79..ba1a8bcf0 100644 --- a/metagpt/exp_pool/manager.py +++ b/metagpt/exp_pool/manager.py @@ -7,16 +7,12 @@ from metagpt.config2 import Config, config from metagpt.exp_pool.schema import ( DEFAULT_COLLECTION_NAME, DEFAULT_SIMILARITY_TOP_K, - EntryType, Experience, - Metric, QueryType, - Score, ) from metagpt.logs import logger from metagpt.rag.engines import SimpleEngine from metagpt.rag.schema import ChromaRetrieverConfig, LLMRankerConfig -from metagpt.strategy.experience_retriever import ENGINEER_EXAMPLE, TL_EXAMPLE from metagpt.utils.exceptions import handle_exception @@ -50,17 +46,9 @@ class ExperienceManager(BaseModel): logger.debug(f"exp_pool config: {self.config.exp_pool}") return self - @handle_exception - def init_exp_pool(self): - if not self.config.exp_pool.enable_write: - return - - if self._has_exps(): - return - - self._init_teamleader_exps() - self._init_engineer2_exps() - logger.info("`init_exp_pool` done.") + @property + def vector_store(self) -> ChromaVectorStore: + return self.storage._retriever._vector_store @handle_exception def create_exp(self, exp: Experience): @@ -101,26 +89,8 @@ class ExperienceManager(BaseModel): return exps - def _has_exps(self) -> bool: - vector_store: ChromaVectorStore = self.storage._retriever._vector_store - - return bool(vector_store._get(limit=1, where={}).ids) - - def _init_exp(self, req: str, resp: str, tag: str, metric: Metric = None): - exp = Experience( - req=req, - resp=resp, - entry_type=EntryType.MANUAL, - tag=tag, - metric=metric or Metric(score=Score(val=9, reason="Manual")), - ) - self.create_exp(exp) - - def _init_teamleader_exps(self): - self._init_exp(req=TL_EXAMPLE, resp=TL_EXAMPLE, tag="TeamLeader.llm_cached_aask") - - def _init_engineer2_exps(self): - self._init_exp(req=ENGINEER_EXAMPLE, resp=ENGINEER_EXAMPLE, tag="Engineer2.llm_cached_aask") + def get_exps_count(self) -> int: + return self.vector_store._collection.count() exp_manager = ExperienceManager() diff --git a/tests/metagpt/exp_pool/test_manager.py b/tests/metagpt/exp_pool/test_manager.py index 6d0693efd..2f712ad44 100644 --- a/tests/metagpt/exp_pool/test_manager.py +++ b/tests/metagpt/exp_pool/test_manager.py @@ -62,21 +62,3 @@ class TestExperienceManager: mock_config.exp_pool.enable_read = False result = await mock_experience_manager.query_exps("query") assert result == [] - - def test_has_exps(self, mock_experience_manager, mock_storage): - mock_storage._retriever._vector_store._get.return_value.ids = ["id1"] - - assert mock_experience_manager._has_exps() is True - - mock_storage._retriever._vector_store._get.return_value.ids = [] - assert mock_experience_manager._has_exps() is False - - def test_init_teamleader_exps(self, mock_experience_manager, mocker): - mock_experience_manager._init_exp = mocker.MagicMock() - mock_experience_manager._init_teamleader_exps() - mock_experience_manager._init_exp.assert_called_once() - - def test_init_engineer2_exps(self, mock_experience_manager, mocker): - mock_experience_manager._init_exp = mocker.MagicMock() - mock_experience_manager._init_engineer2_exps() - mock_experience_manager._init_exp.assert_called_once() diff --git a/tests/metagpt/exp_pool/test_serializers/test_action_node.py b/tests/metagpt/exp_pool/test_serializers/test_action_node.py new file mode 100644 index 000000000..e4ab4684d --- /dev/null +++ b/tests/metagpt/exp_pool/test_serializers/test_action_node.py @@ -0,0 +1,35 @@ +from typing import Type + +import pytest + +from metagpt.actions.action_node import ActionNode +from metagpt.exp_pool.serializers.action_node import ActionNodeSerializer + + +class TestActionNodeSerializer: + @pytest.fixture + def serializer(self): + return ActionNodeSerializer() + + @pytest.fixture + def action_node(self): + class InstructContent: + def __init__(self, json_data): + self.json_data = json_data + + def model_dump_json(self): + return self.json_data + + action_node = ActionNode(key="", expected_type=Type[str], instruction="", example="") + action_node.instruct_content = InstructContent('{"key": "value"}') + + return action_node + + def test_serialize_resp(self, serializer: ActionNodeSerializer, action_node: ActionNode): + serialized = serializer.serialize_resp(action_node) + assert serialized == '{"key": "value"}' + + def test_deserialize_resp(self, serializer: ActionNodeSerializer): + deserialized = serializer.deserialize_resp('{"key": "value"}') + assert isinstance(deserialized, ActionNode) + assert deserialized.instruct_content.model_dump_json() == '{"key": "value"}' diff --git a/tests/metagpt/exp_pool/test_serializers/test_role_zero.py b/tests/metagpt/exp_pool/test_serializers/test_role_zero.py new file mode 100644 index 000000000..4c1f3daf3 --- /dev/null +++ b/tests/metagpt/exp_pool/test_serializers/test_role_zero.py @@ -0,0 +1,77 @@ +import json + +import pytest + +from metagpt.exp_pool.serializers import RoleZeroSerializer + + +class TestRoleZeroSerializer: + @pytest.fixture + def serializer(self): + return RoleZeroSerializer() + + def test_serialize_req_empty_input(self, serializer: RoleZeroSerializer): + assert serializer.serialize_req([]) == "" + + def test_serialize_req_with_content(self, serializer: RoleZeroSerializer): + req = [ + {"content": "Command Editor.read executed: file_path=test.py"}, + {"content": "Some other content"}, + { + "content": "# Data Structure\nsome data\n# Current Plan\nsome plan\n# Example\nsome example\n# Instruction\nsome instruction" + }, + ] + expected_output = json.dumps( + [ + {"content": "Command Editor.read executed: file_path=test.py"}, + { + "content": "# Data Structure\n\n\n# Current Plan\nsome plan\n# Example\n\n\n# Instruction\nsome instruction" + }, + ] + ) + assert serializer.serialize_req(req) == expected_output + + def test_filter_req(self, serializer: RoleZeroSerializer): + req = [ + {"content": "Command Editor.read executed: file_path=test1.py"}, + {"content": "Some other content"}, + {"content": "Command Editor.read executed: file_path=test2.py"}, + {"content": "Final content"}, + ] + filtered_req = serializer._filter_req(req) + assert len(filtered_req) == 3 + assert filtered_req[0]["content"] == "Command Editor.read executed: file_path=test1.py" + assert filtered_req[1]["content"] == "Command Editor.read executed: file_path=test2.py" + assert filtered_req[2]["content"] == "Final content" + + def test_clean_last_entry_content(self, serializer: RoleZeroSerializer): + req = [ + {"content": "Some content"}, + { + "content": "# Data Structure\nsome data\n# Current Plan\nsome plan\n# Example\nsome example\n# Instruction\nsome instruction" + }, + ] + serializer._clean_last_entry_content(req) + expected_content = ( + "# Data Structure\n\n\n# Current Plan\nsome plan\n# Example\n\n\n# Instruction\nsome instruction" + ) + assert req[-1]["content"] == expected_content + + def test_integration(self, serializer: RoleZeroSerializer): + req = [ + {"content": "Command Editor.read executed: file_path=test.py"}, + {"content": "Some other content"}, + { + "content": "# Data Structure\nsome data\n# Current Plan\nsome plan\n# Example\nsome example\n# Instruction\nsome instruction" + }, + ] + result = serializer.serialize_req(req) + expected_output = json.dumps( + [ + {"content": "Command Editor.read executed: file_path=test.py"}, + { + "content": "# Data Structure\n\n\n# Current Plan\nsome plan\n# Example\n\n\n# Instruction\nsome instruction" + }, + ] + ) + assert result == expected_output diff --git a/tests/metagpt/exp_pool/test_serializers/test_simple.py b/tests/metagpt/exp_pool/test_serializers/test_simple.py new file mode 100644 index 000000000..05ef1ca11 --- /dev/null +++ b/tests/metagpt/exp_pool/test_serializers/test_simple.py @@ -0,0 +1,44 @@ +import pytest + +from metagpt.exp_pool.serializers.simple import SimpleSerializer + + +class TestSimpleSerializer: + @pytest.fixture + def serializer(self): + return SimpleSerializer() + + def test_serialize_req(self, serializer): + # Test with different types of input + assert serializer.serialize_req(123) == "123" + assert serializer.serialize_req("test") == "test" + assert serializer.serialize_req([1, 2, 3]) == "[1, 2, 3]" + assert serializer.serialize_req({"a": 1}) == "{'a': 1}" + + def test_serialize_resp(self, serializer): + # Test with different types of input + assert serializer.serialize_resp(456) == "456" + assert serializer.serialize_resp("response") == "response" + assert serializer.serialize_resp([4, 5, 6]) == "[4, 5, 6]" + assert serializer.serialize_resp({"b": 2}) == "{'b': 2}" + + def test_deserialize_resp(self, serializer): + # Test with different types of input + assert serializer.deserialize_resp("789") == "789" + assert serializer.deserialize_resp("test_response") == "test_response" + assert serializer.deserialize_resp("[7, 8, 9]") == "[7, 8, 9]" + assert serializer.deserialize_resp("{'c': 3}") == "{'c': 3}" + + def test_roundtrip(self, serializer): + # Test serialization and deserialization roundtrip + original = "test_roundtrip" + serialized = serializer.serialize_resp(original) + deserialized = serializer.deserialize_resp(serialized) + assert deserialized == original + + @pytest.mark.parametrize("input_value", [123, "test", [1, 2, 3], {"a": 1}, None]) + def test_serialize_req_types(self, serializer, input_value): + # Test serialize_req with various input types + result = serializer.serialize_req(input_value) + assert isinstance(result, str) + assert result == str(input_value) From be1f16bf92e2111312409c2228dc4330cb7b89b7 Mon Sep 17 00:00:00 2001 From: zhanglei Date: Fri, 12 Jul 2024 13:39:29 +0800 Subject: [PATCH 085/119] update: CR --- metagpt/ext/cr/actions/code_review.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/metagpt/ext/cr/actions/code_review.py b/metagpt/ext/cr/actions/code_review.py index 6d1519867..79210e5a3 100644 --- a/metagpt/ext/cr/actions/code_review.py +++ b/metagpt/ext/cr/actions/code_review.py @@ -78,8 +78,7 @@ CODE_REVIEW_COMFIRM_TEMPLATE = """ ## Your Task: 1. First, check if the code meets the requirements and does not violate any defects. If it meets the requirements and does not violate any defects, print `False` and do not proceed with further judgment. -2. Then, check if the "Code Review Comments" meets "Description of Defects".If they don't meet,print `False` and do not proceed with further judgment. -3. If the check in step 1 and step 2 do not print `False`, proceed to further judgment. Based on the "Reference Example for Judgment" provided, determine if the "Code" and "Code Review Comments" match. If they match, print `True`; otherwise, print `False`. +3. Based on the `Reference Example for Judgment` provided, determine if the `Code` and `Code Review Comments` match. If they match, print `True`; otherwise, print `False`. Note: Your output should only be `True` or `False` without any explanations. """ From 7d22d8f872edbcb563dd3b389a3e161c97c271b8 Mon Sep 17 00:00:00 2001 From: zhanglei Date: Fri, 12 Jul 2024 13:40:25 +0800 Subject: [PATCH 086/119] update: CR --- metagpt/ext/cr/actions/code_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/ext/cr/actions/code_review.py b/metagpt/ext/cr/actions/code_review.py index 79210e5a3..db9893aee 100644 --- a/metagpt/ext/cr/actions/code_review.py +++ b/metagpt/ext/cr/actions/code_review.py @@ -78,7 +78,7 @@ CODE_REVIEW_COMFIRM_TEMPLATE = """ ## Your Task: 1. First, check if the code meets the requirements and does not violate any defects. If it meets the requirements and does not violate any defects, print `False` and do not proceed with further judgment. -3. Based on the `Reference Example for Judgment` provided, determine if the `Code` and `Code Review Comments` match. If they match, print `True`; otherwise, print `False`. +2. Based on the `Reference Example for Judgment` provided, determine if the `Code` and `Code Review Comments` match. If they match, print `True`; otherwise, print `False`. Note: Your output should only be `True` or `False` without any explanations. """ From 7508ff66cdadf72e2c349631ef2bed7dccaab1cf Mon Sep 17 00:00:00 2001 From: lidanyang Date: Fri, 12 Jul 2024 14:33:05 +0800 Subject: [PATCH 087/119] move loc of task_type --- metagpt/prompts/di/role_zero.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/metagpt/prompts/di/role_zero.py b/metagpt/prompts/di/role_zero.py index 83ad5cd8e..b642df9c7 100644 --- a/metagpt/prompts/di/role_zero.py +++ b/metagpt/prompts/di/role_zero.py @@ -18,13 +18,13 @@ class Task(BaseModel): task_type: str = "" assignee: str = "" +# Available Task Types +{task_type_desc} + # Available Commands {available_commands} Special Command: Use {{"command_name": "end"}} to do nothing or indicate completion of all requirements and the end of actions. -# Available Task Types -{task_type_desc} - # Current Plan {plan_status} From aab7ffcff8c7e544962df638babf591f7efe1885 Mon Sep 17 00:00:00 2001 From: zhanglei Date: Fri, 12 Jul 2024 15:27:40 +0800 Subject: [PATCH 088/119] update: CR --- metagpt/ext/cr/actions/code_review.py | 11 +++++++---- metagpt/tools/libs/cr.py | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/metagpt/ext/cr/actions/code_review.py b/metagpt/ext/cr/actions/code_review.py index db9893aee..ae6086f11 100644 --- a/metagpt/ext/cr/actions/code_review.py +++ b/metagpt/ext/cr/actions/code_review.py @@ -166,6 +166,8 @@ class CodeReview(Action): async def cr_by_points(self, patch: PatchSet, points: list[Point]): comments = [] for patched_file in patch: + if not patched_file: + continue if patched_file.path.endswith(".py"): points = [p for p in points if p.language == "Python"] elif patched_file.path.endswith(".java"): @@ -180,10 +182,11 @@ class CodeReview(Action): resp = await self.llm.aask(prompt) json_str = parse_json_code_block(resp)[0] comments_batch = json.loads(json_str) - patched_file_path = patched_file.path - for c in comments_batch: - c["commented_file"] = patched_file_path - comments += comments_batch + if comments_batch: + patched_file_path = patched_file.path + for c in comments_batch: + c["commented_file"] = patched_file_path + comments += comments_batch return comments diff --git a/metagpt/tools/libs/cr.py b/metagpt/tools/libs/cr.py index f802af620..ea6acf654 100644 --- a/metagpt/tools/libs/cr.py +++ b/metagpt/tools/libs/cr.py @@ -30,7 +30,7 @@ class CodeReview: Args: patch_path: The local path of the patch file or the url of the pull request. Example: "/data/xxx-pr-1.patch", "https://github.com/xx/XX/pull/1362" cr_output_file: Output file path where code review comments will be saved. Example: "cr/xxx-pr-1.json" - cr_point_file: File path for specifying code review points. Set `None` to use a predefined file. + cr_point_file: File path for specifying code review points. If not specified, this parameter is not passed.. """ patch = await self._get_patch_content(patch_path) cr_point_file = cr_point_file if cr_point_file else Path(metagpt.ext.cr.__file__).parent / "points.json" From df340cb33084ad1d0b03a7fd688d2119d842955c Mon Sep 17 00:00:00 2001 From: lidanyang Date: Fri, 12 Jul 2024 15:55:52 +0800 Subject: [PATCH 089/119] remove browser_actions from role --- metagpt/actions/di/write_analysis_code.py | 4 ++-- metagpt/roles/di/data_analyst.py | 13 ++++++++----- metagpt/roles/di/role_zero.py | 21 ++++++++++----------- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/metagpt/actions/di/write_analysis_code.py b/metagpt/actions/di/write_analysis_code.py index d0c4c016e..06e0ba4e2 100644 --- a/metagpt/actions/di/write_analysis_code.py +++ b/metagpt/actions/di/write_analysis_code.py @@ -42,7 +42,6 @@ class WriteAnalysisCode(Action): tool_info: str = "", working_memory: list[Message] = None, use_reflection: bool = False, - browser_actions: list[dict] = None, **kwargs, ) -> str: structual_prompt = STRUCTUAL_PROMPT.format( @@ -51,11 +50,12 @@ class WriteAnalysisCode(Action): tool_info=tool_info, ) message = [Message(content=structual_prompt, role="user")] + browser_actions = [msg for msg in working_memory if msg.cause_by == "browser"] if browser_actions: browser_prompt = BROWSER_INFO.format(browser_actions=browser_actions) message = [Message(content=browser_prompt, role="user")] + message - working_memory = working_memory or [] + working_memory = [msg for msg in working_memory if msg.cause_by != "browser"] if use_reflection else [] context = self.llm.format_msg(message + working_memory) # LLM call diff --git a/metagpt/roles/di/data_analyst.py b/metagpt/roles/di/data_analyst.py index e6b734417..066b82874 100644 --- a/metagpt/roles/di/data_analyst.py +++ b/metagpt/roles/di/data_analyst.py @@ -1,7 +1,7 @@ from __future__ import annotations +import json import re -from typing import List from pydantic import Field, model_validator @@ -44,7 +44,8 @@ class DataAnalyst(RoleZero): "DataAnalyst.write_and_exec_code": self.write_and_exec_code, }) - def parse_browser_actions(self, memory: List[Message]): + async def parse_browser_actions(self): + memory = await super().parse_browser_actions() for index, msg in enumerate(memory): if msg.cause_by == "browser": browser_url = re.search('URL: (.*?)\\n', msg.content).group(1) @@ -53,7 +54,10 @@ class DataAnalyst(RoleZero): 'command': pattern.match(memory[index - 1].content).group(1), 'current url': browser_url } - self.browser_actions.append(browser_action) + self.rc.working_memory.add( + Message(content=json.dumps(browser_action), role="user", cause_by="browser") + ) + return memory async def write_and_exec_code(self): """Write a code block for current task and execute it in an interactive notebook environment.""" @@ -81,9 +85,8 @@ class DataAnalyst(RoleZero): user_requirement=self.planner.plan.goal, plan_status=plan_status, tool_info=tool_info, - working_memory=self.rc.working_memory.get() if use_reflection else None, + working_memory=self.rc.working_memory.get(), use_reflection=use_reflection, - browser_actions=self.browser_actions ) self.rc.working_memory.add(Message(content=code, role="assistant", cause_by=WriteAnalysisCode)) diff --git a/metagpt/roles/di/role_zero.py b/metagpt/roles/di/role_zero.py index fb89114c8..ed617eb81 100644 --- a/metagpt/roles/di/role_zero.py +++ b/metagpt/roles/di/role_zero.py @@ -54,7 +54,6 @@ class RoleZero(Role): # Equipped with three basic tools by default for optional use editor: Editor = Editor() browser: Browser = Browser() - browser_actions: list[dict] = [] # store the browser history actions # terminal: Terminal = Terminal() # FIXME: TypeError: cannot pickle '_thread.lock' object # Experience @@ -151,14 +150,7 @@ class RoleZero(Role): instruction=self.instruction.strip(), task_type_desc=self.task_type_desc, ) - memory = self.rc.memory.get(self.memory_k) - if not self.browser.is_empty_page: - pattern = re.compile(r"Command Browser\.(\w+) executed") - for index, msg in zip(range(len(memory), 0, -1), memory[::-1]): - if pattern.match(msg.content): - memory.insert(index, UserMessage(cause_by="browser", content=await self.browser.view())) - break - self.parse_browser_actions(memory=memory) + memory = await self.parse_browser_actions() context = self.llm.format_msg(memory + [UserMessage(content=prompt)]) # print(*context, sep="\n" + "*" * 5 + "\n") async with ThoughtReporter(enable_llm_stream=True): @@ -167,8 +159,15 @@ class RoleZero(Role): return True - def parse_browser_actions(self, memory: List[Message]): - pass + async def parse_browser_actions(self): + memory = self.rc.memory.get(self.memory_k) + if not self.browser.is_empty_page: + pattern = re.compile(r"Command Browser\.(\w+) executed") + for index, msg in zip(range(len(memory), 0, -1), memory[::-1]): + if pattern.match(msg.content): + memory.insert(index, UserMessage(cause_by="browser", content=await self.browser.view())) + break + return memory async def _act(self) -> Message: if self.use_fixed_sop: From df29e16e2b78b2a1c351f0b689109ef9b41a2e05 Mon Sep 17 00:00:00 2001 From: lidanyang Date: Fri, 12 Jul 2024 16:44:05 +0800 Subject: [PATCH 090/119] refine code --- metagpt/actions/di/write_analysis_code.py | 10 ++-------- metagpt/prompts/di/data_analyst.py | 9 ++++++++- metagpt/prompts/di/write_analysis_code.py | 6 ------ metagpt/roles/di/data_analyst.py | 19 ++++++++++--------- metagpt/roles/di/role_zero.py | 6 +++--- 5 files changed, 23 insertions(+), 27 deletions(-) diff --git a/metagpt/actions/di/write_analysis_code.py b/metagpt/actions/di/write_analysis_code.py index 06e0ba4e2..00e6d174d 100644 --- a/metagpt/actions/di/write_analysis_code.py +++ b/metagpt/actions/di/write_analysis_code.py @@ -16,7 +16,6 @@ from metagpt.prompts.di.write_analysis_code import ( REFLECTION_PROMPT, REFLECTION_SYSTEM_MSG, STRUCTUAL_PROMPT, - BROWSER_INFO, ) from metagpt.schema import Message, Plan from metagpt.utils.common import CodeParser, remove_comments @@ -49,14 +48,9 @@ class WriteAnalysisCode(Action): plan_status=plan_status, tool_info=tool_info, ) - message = [Message(content=structual_prompt, role="user")] - browser_actions = [msg for msg in working_memory if msg.cause_by == "browser"] - if browser_actions: - browser_prompt = BROWSER_INFO.format(browser_actions=browser_actions) - message = [Message(content=browser_prompt, role="user")] + message - working_memory = [msg for msg in working_memory if msg.cause_by != "browser"] if use_reflection else [] - context = self.llm.format_msg(message + working_memory) + working_memory = working_memory or [] + context = self.llm.format_msg([Message(content=structual_prompt, role="user")] + working_memory) # LLM call if use_reflection: diff --git a/metagpt/prompts/di/data_analyst.py b/metagpt/prompts/di/data_analyst.py index 7abe0ac93..56ae0b68b 100644 --- a/metagpt/prompts/di/data_analyst.py +++ b/metagpt/prompts/di/data_analyst.py @@ -16,4 +16,11 @@ CODE_STATUS = """ **Execution status**: {status} **Execution result**: {result} -""" \ No newline at end of file +""" + + +BROWSER_INFO = """ +Here are ordered web actions in the browser environment, note that you can not use the browser tool in the current environment. +{browser_actions} +The latest url is the one you should use to view the page. If view page has been done, directly use the variable and html content in executing result. +""" diff --git a/metagpt/prompts/di/write_analysis_code.py b/metagpt/prompts/di/write_analysis_code.py index 6105c4182..1d743a719 100644 --- a/metagpt/prompts/di/write_analysis_code.py +++ b/metagpt/prompts/di/write_analysis_code.py @@ -119,9 +119,3 @@ DATA_INFO = """ Latest data info after previous tasks: {info} """ - -BROWSER_INFO = """ -Here are ordered web actions in the browser environment, note that you can not use the browser tool in the current environment. -{browser_actions} -The latest url is the one you should use to view the page. If view page has been done, directly use the variable and html content in executing result. -""" diff --git a/metagpt/roles/di/data_analyst.py b/metagpt/roles/di/data_analyst.py index 066b82874..2e5315f33 100644 --- a/metagpt/roles/di/data_analyst.py +++ b/metagpt/roles/di/data_analyst.py @@ -1,14 +1,14 @@ from __future__ import annotations -import json import re +from typing import List from pydantic import Field, model_validator from metagpt.actions.di.execute_nb_code import ExecuteNbCode from metagpt.actions.di.write_analysis_code import WriteAnalysisCode from metagpt.logs import logger -from metagpt.prompts.di.data_analyst import BROWSER_INSTRUCTION, TASK_TYPE_DESC, CODE_STATUS +from metagpt.prompts.di.data_analyst import BROWSER_INSTRUCTION, TASK_TYPE_DESC, CODE_STATUS, BROWSER_INFO from metagpt.prompts.di.role_zero import ROLE_INSTRUCTION from metagpt.roles.di.role_zero import RoleZero from metagpt.schema import TaskResult, Message @@ -44,19 +44,20 @@ class DataAnalyst(RoleZero): "DataAnalyst.write_and_exec_code": self.write_and_exec_code, }) - async def parse_browser_actions(self): - memory = await super().parse_browser_actions() + async def parse_browser_actions(self, memory: List[Message]) -> List[Message]: + memory = await super().parse_browser_actions(memory) + browser_actions = [] for index, msg in enumerate(memory): if msg.cause_by == "browser": browser_url = re.search('URL: (.*?)\\n', msg.content).group(1) pattern = re.compile(r"Command Browser\.(\w+) executed") - browser_action = { + browser_actions.append({ 'command': pattern.match(memory[index - 1].content).group(1), 'current url': browser_url - } - self.rc.working_memory.add( - Message(content=json.dumps(browser_action), role="user", cause_by="browser") - ) + }) + if browser_actions: + browser_actions = BROWSER_INFO.format(browser_actions=browser_actions) + self.rc.working_memory.add(Message(content=browser_actions, role="user", cause_by="browser")) return memory async def write_and_exec_code(self): diff --git a/metagpt/roles/di/role_zero.py b/metagpt/roles/di/role_zero.py index ed617eb81..671cddc79 100644 --- a/metagpt/roles/di/role_zero.py +++ b/metagpt/roles/di/role_zero.py @@ -150,7 +150,8 @@ class RoleZero(Role): instruction=self.instruction.strip(), task_type_desc=self.task_type_desc, ) - memory = await self.parse_browser_actions() + memory = self.rc.memory.get(self.memory_k) + memory = await self.parse_browser_actions(memory) context = self.llm.format_msg(memory + [UserMessage(content=prompt)]) # print(*context, sep="\n" + "*" * 5 + "\n") async with ThoughtReporter(enable_llm_stream=True): @@ -159,8 +160,7 @@ class RoleZero(Role): return True - async def parse_browser_actions(self): - memory = self.rc.memory.get(self.memory_k) + async def parse_browser_actions(self, memory: List[Message]) -> List[Message]: if not self.browser.is_empty_page: pattern = re.compile(r"Command Browser\.(\w+) executed") for index, msg in zip(range(len(memory), 0, -1), memory[::-1]): From 857085e8592ee3f6d49db8d23138823f53d40090 Mon Sep 17 00:00:00 2001 From: lidanyang Date: Fri, 12 Jul 2024 17:09:11 +0800 Subject: [PATCH 091/119] rename fix to fixed --- metagpt/roles/di/data_analyst.py | 4 ++-- metagpt/tools/tool_recommend.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/metagpt/roles/di/data_analyst.py b/metagpt/roles/di/data_analyst.py index 2e5315f33..5040385ea 100644 --- a/metagpt/roles/di/data_analyst.py +++ b/metagpt/roles/di/data_analyst.py @@ -72,8 +72,8 @@ class DataAnalyst(RoleZero): # tool info if self.custom_tool_recommender: plan = self.planner.plan - fix = ["Terminal"] if "Terminal" in self.custom_tools else None - tool_info = await self.custom_tool_recommender.get_recommended_tool_info(fix=fix, plan=plan) + fixed = ["Terminal"] if "Terminal" in self.custom_tools else None + tool_info = await self.custom_tool_recommender.get_recommended_tool_info(fixed=fixed, plan=plan) else: tool_info = "" diff --git a/metagpt/tools/tool_recommend.py b/metagpt/tools/tool_recommend.py index b0fd0f39b..4bea137c3 100644 --- a/metagpt/tools/tool_recommend.py +++ b/metagpt/tools/tool_recommend.py @@ -104,13 +104,13 @@ class ToolRecommender(BaseModel): return ranked_tools - async def get_recommended_tool_info(self, fix: list[str] = None, **kwargs) -> str: + async def get_recommended_tool_info(self, fixed: list[str] = None, **kwargs) -> str: """ Wrap recommended tools with their info in a string, which can be used directly in a prompt. """ recommended_tools = await self.recommend_tools(**kwargs) - if fix: - recommended_tools.extend([self.tools[tool_name] for tool_name in fix if tool_name in self.tools]) + if fixed: + recommended_tools.extend([self.tools[tool_name] for tool_name in fixed if tool_name in self.tools]) if not recommended_tools: return "" tool_schemas = {tool.name: tool.schemas for tool in recommended_tools} From 27e8fdf32065f44f98f72ae2718a7923be387bc7 Mon Sep 17 00:00:00 2001 From: lidanyang Date: Fri, 12 Jul 2024 18:40:43 +0800 Subject: [PATCH 092/119] recover --- metagpt/utils/parse_html.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/metagpt/utils/parse_html.py b/metagpt/utils/parse_html.py index 031393501..985e54d96 100644 --- a/metagpt/utils/parse_html.py +++ b/metagpt/utils/parse_html.py @@ -43,11 +43,11 @@ class WebPage(BaseModel): soup = _get_soup(self.html) keep_attrs = ["class", "id"] if keep_links: - keep_attrs.extend(["href", "title"]) + keep_attrs.append("href") for i in soup.find_all(True): for name in list(i.attrs): - if i[name] and name not in keep_attrs and not name.startswith("data-"): + if i[name] and name not in keep_attrs: del i[name] for i in soup.find_all(["svg", "img", "video", "audio"]): From b214e49733bc2b21375eb3aae7e99c194080c3cc Mon Sep 17 00:00:00 2001 From: lidanyang Date: Fri, 12 Jul 2024 18:41:29 +0800 Subject: [PATCH 093/119] change _init_code to public --- metagpt/actions/di/execute_nb_code.py | 8 ++++---- metagpt/roles/di/data_analyst.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/metagpt/actions/di/execute_nb_code.py b/metagpt/actions/di/execute_nb_code.py index 91480d3a7..f3dfd1601 100644 --- a/metagpt/actions/di/execute_nb_code.py +++ b/metagpt/actions/di/execute_nb_code.py @@ -85,12 +85,12 @@ class ExecuteNbCode(Action): ) self.reporter = NotebookReporter() self.set_nb_client() - self._init_called = False + self.init_called = False - async def _init_code(self): - if not self._init_called: + async def init_code(self): + if not self.init_called: await self.run(INI_CODE) - self._init_called = True + self.init_called = True def set_nb_client(self): self.nb_client = RealtimeOutputNotebookClient( diff --git a/metagpt/roles/di/data_analyst.py b/metagpt/roles/di/data_analyst.py index 5040385ea..2b1bb10b1 100644 --- a/metagpt/roles/di/data_analyst.py +++ b/metagpt/roles/di/data_analyst.py @@ -64,7 +64,7 @@ class DataAnalyst(RoleZero): """Write a code block for current task and execute it in an interactive notebook environment.""" counter = 0 success = False - await self.execute_code._init_code() + await self.execute_code.init_code() # plan info plan_status = self.planner.get_plan_status() From bf21bbf12e43ba9188012ba1317e0491ab2c8cc9 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Fri, 12 Jul 2024 19:30:33 +0800 Subject: [PATCH 094/119] determine the content to be saved to the experience pool using cmd_prompt_exp_part. --- .../exp_pool/init_exp_pool/engineer_exps.py | 7 +- .../init_exp_pool/team_leader_exps.py | 22 +++--- .../exp_pool/context_builders/action_node.py | 12 +-- metagpt/exp_pool/context_builders/base.py | 7 +- .../exp_pool/context_builders/role_zero.py | 26 ++++--- metagpt/exp_pool/context_builders/simple.py | 6 +- metagpt/exp_pool/decorator.py | 2 +- metagpt/exp_pool/serializers/role_zero.py | 25 ++----- metagpt/prompts/di/role_zero.py | 24 +++--- metagpt/roles/di/role_zero.py | 22 ++++-- .../test_rolezero_context_builder.py | 18 +++-- .../test_simple_context_builder.py | 8 +- .../test_serializers/test_role_zero.py | 73 ++++++------------- 13 files changed, 113 insertions(+), 139 deletions(-) diff --git a/examples/exp_pool/init_exp_pool/engineer_exps.py b/examples/exp_pool/init_exp_pool/engineer_exps.py index e494fdaa3..022a0c829 100644 --- a/examples/exp_pool/init_exp_pool/engineer_exps.py +++ b/examples/exp_pool/init_exp_pool/engineer_exps.py @@ -3,20 +3,19 @@ import json OBJ1 = [ { "role": "user", - "content": '\n# Data Structure\n\n\n# Current Plan\n{\'goal\': "Please write a 1024 game using JavaScript and HTML code without using any frameworks, user can play with keyboard. Refer to the system design located at \'/tmp/system_design.json\' and the project schedule at \'/tmp/project_schedule.json\' for detailed information.", \'tasks\': []}\n\n# Current Task\n\n\n# Example\n\n\n# Instruction\nBased on the context, write a plan or modify an existing plan to achieve the goal. A plan consists of one to 3 tasks.\nIf plan is created, you should track the progress and update the plan accordingly, such as Plan.finish_current_task, Plan.append_task, Plan.reset_task, Plan.replace_task, etc.\nWhen presented a current task, tackle the task using the available commands.\nPay close attention to new user message, review the conversation history, use RoleZero.reply_to_human to respond to new user requirement.\nNote:\n1. If you keeping encountering errors, unexpected situation, or you are not sure of proceeding, use RoleZero.ask_human to ask for help.\n2. Carefully review your progress at the current task, if your actions so far has not fulfilled the task instruction, you should continue with current task. Otherwise, finish current task.\n3. Each time you finish a task, use RoleZero.reply_to_human to report your progress.\n4. Each time you write a code in your response, write with the Editor directly without preparing a repetitive code block beforehand.\n5. Take on ONE task and write ONE code file in each response. DON\'T attempt all tasks in one response.\n6. When not specified, you should write files in a folder named "src". If you know the project path, then write in a "src" folder under the project path.\n7. When provided system design or project schedule, you MUST read them first before making a plan, then adhere to them in your implementation, especially in the programming language, package, or framework. You MUST implement all code files prescribed in the system design or project schedule. You can create a plan first with each task corresponding to implementing one code file.\n8. Write at most one file per task, do your best to implement THE ONLY ONE FILE. CAREFULLY CHECK THAT YOU DONT MISS ANY NECESSARY CLASS/FUNCTION IN THIS FILE.\n9. COMPLETE CODE: Your code will be part of the entire project, so please implement complete, reliable, reusable code snippets.\n10. When provided system design, YOU MUST FOLLOW "Data structures and interfaces". DONT CHANGE ANY DESIGN. Do not use public member functions that do not exist in your design.\n11. Write out EVERY CODE DETAIL, DON\'T LEAVE TODO.\n12. To modify code in a file, read the entire file, make changes, and update the file with the complete code, ensuring that no line numbers are included in the final write.\n13. When a system design or project schedule is provided, at the end of the plan, add a CodeRview Task for each file; for example, if there are three files, add three CodeRview Tasks. For each CodeRview Task, just call ReviewAndRewriteCode.run.\n\nPay close attention to the Example provided, you can reuse the example for your current situation if it fits.\nYou may use any of the available commands to create a plan or update the plan. You may output mutiple commands, they will be executed sequentially.\nIf you finish current task, you will automatically take the next task in the existing plan, use Plan.finish_task, DON\'T append a new task.\n\n# Your commands in a json array, in the following output format. If there is nothing to do, use the pass or end command:\nSome text indicating your thoughts, such as how you should update the plan status, respond to inquiry, or seek for help. Then a json array of commands. You must output ONE and ONLY ONE json array. DON\'T output multiple json arrays with thoughts between them.\n```json\n[\n {\n "command_name": str,\n "args": {"arg_name": arg_value, ...}\n },\n ...\n]\n```\nNotice: your output JSON data section must start with **```json [**\n', + "content": "\n# Current Plan\n{'goal': \"Please write a 1048 game using JavaScript and HTML code without using any frameworks, user can play with keyboard. Refer to the system design located at '/tmp/system_design.json' and the project schedule at '/tmp/project_schedule.json' for detailed information.\", 'tasks': []}\n\n# Current Task\n\n\n# Instruction\nBased on the context, write a plan or modify an existing plan to achieve the goal. A plan consists of one to 3 tasks.\nIf plan is created, you should track the progress and update the plan accordingly, such as Plan.finish_current_task, Plan.append_task, Plan.reset_task, Plan.replace_task, etc.\nWhen presented a current task, tackle the task using the available commands.\nPay close attention to new user message, review the conversation history, use RoleZero.reply_to_human to respond to new user requirement.\nNote:\n1. If you keeping encountering errors, unexpected situation, or you are not sure of proceeding, use RoleZero.ask_human to ask for help.\n2. Carefully review your progress at the current task, if your actions so far has not fulfilled the task instruction, you should continue with current task. Otherwise, finish current task.\n3. Each time you finish a task, use RoleZero.reply_to_human to report your progress.\n4. Each time you write a code in your response, write with the Editor directly without preparing a repetitive code block beforehand.\n5. Take on ONE task and write ONE code file in each response. DON'T attempt all tasks in one response.\n6. When not specified, you should write files in a folder named \"src\". If you know the project path, then write in a \"src\" folder under the project path.\n7. When provided system design or project schedule, you MUST read them first before making a plan, then adhere to them in your implementation, especially in the programming language, package, or framework. You MUST implement all code files prescribed in the system design or project schedule. You can create a plan first with each task corresponding to implementing one code file.\n8. Write at most one file per task, do your best to implement THE ONLY ONE FILE. CAREFULLY CHECK THAT YOU DONT MISS ANY NECESSARY CLASS/FUNCTION IN THIS FILE.\n9. COMPLETE CODE: Your code will be part of the entire project, so please implement complete, reliable, reusable code snippets.\n10. When provided system design, YOU MUST FOLLOW \"Data structures and interfaces\". DONT CHANGE ANY DESIGN. Do not use public member functions that do not exist in your design.\n11. Write out EVERY CODE DETAIL, DON'T LEAVE TODO.\n12. To modify code in a file, read the entire file, make changes, and update the file with the complete code, ensuring that no line numbers are included in the final write.\n13. When a system design or project schedule is provided, at the end of the plan, add a CodeRview Task for each file; for example, if there are three files, add three CodeRview Tasks. For each CodeRview Task, just call ReviewAndRewriteCode.run.\n", } ] OBJ2 = [ { "role": "user", - "content": "Command Editor.read executed: file_path='/tmp/system_design.json' block_content=\"...\"\n\nCommand Editor.read executed: file_path='/tmp/project_schedule.json' block_content=\"...\"", + "content": 'Command Editor.read executed: file_path=\'/tmp/system_design.json\' block_content=\'001|{"Implementation approach":"We will implement the 2048 game using plain JavaScript and HTML, ensuring no frameworks are used. The game logic will handle tile movements, merging, and game state updates. The UI will be simple and clean, with a responsive design to fit different screen sizes. We will use CSS for styling and ensure the game is playable with keyboard arrow keys. The game will display the current score, have a restart button, and show a game over message when no more moves are possible.","File list":["index.html","style.css","script.js"],"Data structures and interfaces":"\\\\nclassDiagram\\\\n class Game {\\\\n -grid: int[][]\\\\n -score: int\\\\n +init(): void\\\\n +move(direction: str): void\\\\n +merge(direction: str): void\\\\n +isGameOver(): bool\\\\n +restart(): void\\\\n }\\\\n class UI {\\\\n -game: Game\\\\n +init(): void\\\\n +update(): void\\\\n +showGameOver(): void\\\\n +bindEvents(): void\\\\n }\\\\n Game --> UI\\\\n","Program call flow":"\\\\nsequenceDiagram\\\\n participant U as UI\\\\n participant G as Game\\\\n U->>G: init()\\\\n G-->>U: return\\\\n U->>U: bindEvents()\\\\n U->>G: move(direction)\\\\n G->>G: merge(direction)\\\\n G->>U: update()\\\\n U->>U: update()\\\\n U->>G: isGameOver()\\\\n G-->>U: return bool\\\\n alt Game Over\\\\n U->>U: showGameOver()\\\\n end\\\\n U->>G: restart()\\\\n G-->>U: return\\\\n","Anything UNCLEAR":"Clarify if there are any specific design preferences or additional features required beyond the basic 2048 game functionality."}\\n\'\n\nCommand Editor.read executed: file_path=\'/tmp/project_schedule.json\' block_content=\'001|{"Required packages":["No third-party dependencies required"],"Required Other language third-party packages":["No third-party dependencies required"],"Logic Analysis":[["script.js","Contains Game and UI classes, and their methods: init, move, merge, isGameOver, restart, update, showGameOver, bindEvents"],["index.html","Contains the HTML structure for the game UI"],["style.css","Contains the CSS styles for the game UI"]],"Task list":["index.html","style.css","script.js"],"Full API spec":"","Shared Knowledge":"The `script.js` file will contain the core game logic and UI handling. The `index.html` file will provide the structure for the game, and `style.css` will handle the styling.","Anything UNCLEAR":"Clarify if there are any specific design preferences or additional features required beyond the basic 2048 game functionality."}\\n\'', }, { "role": "user", - "content": '\n# Data Structure\n\n\n# Current Plan\n{\'goal\': "Please write a 1024 game using JavaScript and HTML code without using any frameworks, user can play with keyboard. Refer to the system design located at \'/tmp/system_design.json\' and the project schedule at \'/tmp/project_schedule.json\' for detailed information.", \'tasks\': []}\n\n# Current Task\n\n\n# Example\n\n\n# Instruction\nBased on the context, write a plan or modify an existing plan to achieve the goal. A plan consists of one to 3 tasks.\nIf plan is created, you should track the progress and update the plan accordingly, such as Plan.finish_current_task, Plan.append_task, Plan.reset_task, Plan.replace_task, etc.\nWhen presented a current task, tackle the task using the available commands.\nPay close attention to new user message, review the conversation history, use RoleZero.reply_to_human to respond to new user requirement.\nNote:\n1. If you keeping encountering errors, unexpected situation, or you are not sure of proceeding, use RoleZero.ask_human to ask for help.\n2. Carefully review your progress at the current task, if your actions so far has not fulfilled the task instruction, you should continue with current task. Otherwise, finish current task.\n3. Each time you finish a task, use RoleZero.reply_to_human to report your progress.\n4. Each time you write a code in your response, write with the Editor directly without preparing a repetitive code block beforehand.\n5. Take on ONE task and write ONE code file in each response. DON\'T attempt all tasks in one response.\n6. When not specified, you should write files in a folder named "src". If you know the project path, then write in a "src" folder under the project path.\n7. When provided system design or project schedule, you MUST read them first before making a plan, then adhere to them in your implementation, especially in the programming language, package, or framework. You MUST implement all code files prescribed in the system design or project schedule. You can create a plan first with each task corresponding to implementing one code file.\n8. Write at most one file per task, do your best to implement THE ONLY ONE FILE. CAREFULLY CHECK THAT YOU DONT MISS ANY NECESSARY CLASS/FUNCTION IN THIS FILE.\n9. COMPLETE CODE: Your code will be part of the entire project, so please implement complete, reliable, reusable code snippets.\n10. When provided system design, YOU MUST FOLLOW "Data structures and interfaces". DONT CHANGE ANY DESIGN. Do not use public member functions that do not exist in your design.\n11. Write out EVERY CODE DETAIL, DON\'T LEAVE TODO.\n12. To modify code in a file, read the entire file, make changes, and update the file with the complete code, ensuring that no line numbers are included in the final write.\n13. When a system design or project schedule is provided, at the end of the plan, add a CodeRview Task for each file; for example, if there are three files, add three CodeRview Tasks. For each CodeRview Task, just call ReviewAndRewriteCode.run.\n\nPay close attention to the Example provided, you can reuse the example for your current situation if it fits.\nYou may use any of the available commands to create a plan or update the plan. You may output mutiple commands, they will be executed sequentially.\nIf you finish current task, you will automatically take the next task in the existing plan, use Plan.finish_task, DON\'T append a new task.\n\n# Your commands in a json array, in the following output format. If there is nothing to do, use the pass or end command:\nSome text indicating your thoughts, such as how you should update the plan status, respond to inquiry, or seek for help. Then a json array of commands. You must output ONE and ONLY ONE json array. DON\'T output multiple json arrays with thoughts between them.\n```json\n[\n {\n "command_name": str,\n "args": {"arg_name": arg_value, ...}\n },\n ...\n]\n```\nNotice: your output JSON data section must start with **```json [**\n', + "content": "\n# Current Plan\n{'goal': \"Please write a 1048 game using JavaScript and HTML code without using any frameworks, user can play with keyboard. Refer to the system design located at '/tmp/system_design.json' and the project schedule at '/tmp/project_schedule.json' for detailed information.\", 'tasks': []}\n\n# Current Task\n\n\n# Instruction\nBased on the context, write a plan or modify an existing plan to achieve the goal. A plan consists of one to 3 tasks.\nIf plan is created, you should track the progress and update the plan accordingly, such as Plan.finish_current_task, Plan.append_task, Plan.reset_task, Plan.replace_task, etc.\nWhen presented a current task, tackle the task using the available commands.\nPay close attention to new user message, review the conversation history, use RoleZero.reply_to_human to respond to new user requirement.\nNote:\n1. If you keeping encountering errors, unexpected situation, or you are not sure of proceeding, use RoleZero.ask_human to ask for help.\n2. Carefully review your progress at the current task, if your actions so far has not fulfilled the task instruction, you should continue with current task. Otherwise, finish current task.\n3. Each time you finish a task, use RoleZero.reply_to_human to report your progress.\n4. Each time you write a code in your response, write with the Editor directly without preparing a repetitive code block beforehand.\n5. Take on ONE task and write ONE code file in each response. DON'T attempt all tasks in one response.\n6. When not specified, you should write files in a folder named \"src\". If you know the project path, then write in a \"src\" folder under the project path.\n7. When provided system design or project schedule, you MUST read them first before making a plan, then adhere to them in your implementation, especially in the programming language, package, or framework. You MUST implement all code files prescribed in the system design or project schedule. You can create a plan first with each task corresponding to implementing one code file.\n8. Write at most one file per task, do your best to implement THE ONLY ONE FILE. CAREFULLY CHECK THAT YOU DONT MISS ANY NECESSARY CLASS/FUNCTION IN THIS FILE.\n9. COMPLETE CODE: Your code will be part of the entire project, so please implement complete, reliable, reusable code snippets.\n10. When provided system design, YOU MUST FOLLOW \"Data structures and interfaces\". DONT CHANGE ANY DESIGN. Do not use public member functions that do not exist in your design.\n11. Write out EVERY CODE DETAIL, DON'T LEAVE TODO.\n12. To modify code in a file, read the entire file, make changes, and update the file with the complete code, ensuring that no line numbers are included in the final write.\n13. When a system design or project schedule is provided, at the end of the plan, add a CodeRview Task for each file; for example, if there are three files, add three CodeRview Tasks. For each CodeRview Task, just call ReviewAndRewriteCode.run.\n", }, ] - REQ1 = json.dumps(OBJ1) REQ2 = json.dumps(OBJ2) diff --git a/examples/exp_pool/init_exp_pool/team_leader_exps.py b/examples/exp_pool/init_exp_pool/team_leader_exps.py index da0bc3d81..347faac45 100644 --- a/examples/exp_pool/init_exp_pool/team_leader_exps.py +++ b/examples/exp_pool/init_exp_pool/team_leader_exps.py @@ -3,19 +3,19 @@ import json OBJ1 = [ { "role": "user", - "content": "\n# Data Structure\n\n\n# Current Plan\n{'goal': \"from to {''}: Create a cli snake game using Python.\", 'tasks': []}\n\n# Current Task\n\n\n# Example\n\n\n# Instruction\nYou are a team leader, and you are responsible for drafting tasks and routing tasks to your team members.\nYour team member:\nTim: Team Leader, \nAlice: Product Manager, efficiently create a successful product that meets market demands and user expectations\nBob: Architect, design a concise, usable, complete software system\nEve: Project Manager, break down tasks according to PRD/technical design, generate a task list, and analyze task dependencies to start with the prerequisite modules\nAlex: Engineer, Take on game, app, and web development\nDavid: DataAnalyst, Take on any data-related tasks, such as data analysis, machine learning, deep learning, web browsing, web scraping, web searching, web deployment, terminal operation, git and github operation, etc.\n\nYou should NOT assign consecutive tasks to the same team member, instead, assign an aggregated task (or the complete requirement) and let the team member to decompose it.\nWhen creating a new plan involving multiple members, create all tasks at once.\nIf plan is created, you should track the progress based on team member feedback message, and update plan accordingly, such as Plan.finish_current_task, Plan.reset_task, Plan.replace_task, etc.\nYou should use TeamLeader.publish_team_message to team members, asking them to start their task. DONT omit any necessary info such as path, link, environment, programming language, framework, requirement, constraint from original content to team members because you are their sole info source.\nPay close attention to new user message, review the conversation history, use RoleZero.reply_to_human to respond to the user directly, DON'T ask your team members.\n\nNote:\n1. If the requirement is a pure DATA-RELATED requirement, such as bug fixes, issue reporting, environment setup, terminal operations, pip install, web browsing, web scraping, web searching, web imitation, data science, data analysis, machine learning, deep learning, text-to-image etc. DON'T decompose it, assign a single task with the original user requirement as instruction directly to Data Analyst.\n2. If the requirement is developing a software, game, app, or website, excluding the above data-related tasks, you should decompose the requirement into multiple tasks and assign them to different team members based on their expertise, usually the sequence of Product Manager -> Architect -> Project Manager -> Engineer -> (optional: QaEngine if present) -> (optional: DataAnalyst if user requests deployment), each assigned ONE task. When publishing message to Product Manager, you should directly copy the full original user requirement.\n3. If the requirement contains both DATA-RELATED part mentioned in 1 and software development part mentioned in 2, you should decompose the software development part and assign them to different team members based on their expertise, and assign the DATA-RELATED part to Data Analyst David directly.\n4. If the requirement is a common-sense, logical, or math problem, you should respond directly without assigning any task to team members.\n5. If you think the requirement is not clear or ambiguous, you should ask the user for clarification immediately. Assign tasks only after all info is clear.\n6. It is helpful for Engineer to have both the system design and the project schedule for writing the code, so include paths of both files (if available) and remind Engineer to definitely read them when publishing message to Engineer.\n7. If the requirement is writing a TRD and software framework, you should assign it to Architect. When publishing message to Architect, you should directly copy the full original user requirement.\n\nPay close attention to the Example provided, you can reuse the example for your current situation if it fits.\nYou may use any of the available commands to create a plan or update the plan. You may output mutiple commands, they will be executed sequentially.\nIf you finish current task, you will automatically take the next task in the existing plan, use Plan.finish_task, DON'T append a new task.\n\n# Your commands in a json array, in the following output format. If there is nothing to do, use the pass or end command:\nSome text indicating your thoughts, such as how you should update the plan status, respond to inquiry, or seek for help. Then a json array of commands. You must output ONE and ONLY ONE json array. DON'T output multiple json arrays with thoughts between them.\n```json\n[\n {\n \"command_name\": str,\n \"args\": {\"arg_name\": arg_value, ...}\n },\n ...\n]\n```\nNotice: your output JSON data section must start with **```json [**\n", + "content": "\n# Current Plan\n{'goal': \"from to {''}: Write a 1024 game using JavaScript and HTML code without using any frameworks, user can play with keyboard.\", 'tasks': []}\n\n# Current Task\n\n\n# Instruction\nYou are a team leader, and you are responsible for drafting tasks and routing tasks to your team members.\nYour team member:\nTim: Team Leader, \nAlice: Product Manager, efficiently create a successful product that meets market demands and user expectations\nBob: Architect, design a concise, usable, complete software system\nEve: Project Manager, break down tasks according to PRD/technical design, generate a task list, and analyze task dependencies to start with the prerequisite modules\nAlex: Engineer, Take on game, app, and web development\nDavid: DataAnalyst, Take on any data-related tasks, such as data analysis, machine learning, deep learning, web browsing, web scraping, web searching, web deployment, terminal operation, git and github operation, etc.\n\nYou should NOT assign consecutive tasks to the same team member, instead, assign an aggregated task (or the complete requirement) and let the team member to decompose it.\nWhen creating a new plan involving multiple members, create all tasks at once.\nIf plan is created, you should track the progress based on team member feedback message, and update plan accordingly, such as Plan.finish_current_task, Plan.reset_task, Plan.replace_task, etc.\nYou should use TeamLeader.publish_team_message to team members, asking them to start their task. DONT omit any necessary info such as path, link, environment, programming language, framework, requirement, constraint from original content to team members because you are their sole info source.\nPay close attention to new user message, review the conversation history, use RoleZero.reply_to_human to respond to the user directly, DON'T ask your team members.\n\nNote:\n1. If the requirement is a pure DATA-RELATED requirement, such as bug fixes, issue reporting, environment setup, terminal operations, pip install, web browsing, web scraping, web searching, web imitation, data science, data analysis, machine learning, deep learning, text-to-image etc. DON'T decompose it, assign a single task with the original user requirement as instruction directly to Data Analyst.\n2. If the requirement is developing a software, game, app, or website, excluding the above data-related tasks, you should decompose the requirement into multiple tasks and assign them to different team members based on their expertise, usually the sequence of Product Manager -> Architect -> Project Manager -> Engineer -> (optional: QaEngine if present) -> (optional: DataAnalyst if user requests deployment), each assigned ONE task. When publishing message to Product Manager, you should directly copy the full original user requirement.\n3. If the requirement contains both DATA-RELATED part mentioned in 1 and software development part mentioned in 2, you should decompose the software development part and assign them to different team members based on their expertise, and assign the DATA-RELATED part to Data Analyst David directly.\n4. If the requirement is a common-sense, logical, or math problem, you should respond directly without assigning any task to team members.\n5. If you think the requirement is not clear or ambiguous, you should ask the user for clarification immediately. Assign tasks only after all info is clear.\n6. It is helpful for Engineer to have both the system design and the project schedule for writing the code, so include paths of both files (if available) and remind Engineer to definitely read them when publishing message to Engineer.\n7. If the requirement is writing a TRD and software framework, you should assign it to Architect. When publishing message to Architect, you should directly copy the full original user requirement.\n", } ] OBJ2 = [ { "role": "user", - "content": "\n# Data Structure\n\n\n# Current Plan\n{'goal': \"from to {''}: Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.\", 'tasks': []}\n\n# Current Task\n\n\n# Example\n\n\n# Instruction\nYou are a team leader, and you are responsible for drafting tasks and routing tasks to your team members.\nYour team member:\nTim: Team Leader, \nAlice: Product Manager, efficiently create a successful product that meets market demands and user expectations\nBob: Architect, design a concise, usable, complete software system\nEve: Project Manager, break down tasks according to PRD/technical design, generate a task list, and analyze task dependencies to start with the prerequisite modules\nAlex: Engineer, Take on game, app, and web development\nDavid: DataAnalyst, Take on any data-related tasks, such as data analysis, machine learning, deep learning, web browsing, web scraping, web searching, web deployment, terminal operation, git and github operation, etc.\n\nYou should NOT assign consecutive tasks to the same team member, instead, assign an aggregated task (or the complete requirement) and let the team member to decompose it.\nWhen creating a new plan involving multiple members, create all tasks at once.\nIf plan is created, you should track the progress based on team member feedback message, and update plan accordingly, such as Plan.finish_current_task, Plan.reset_task, Plan.replace_task, etc.\nYou should use TeamLeader.publish_team_message to team members, asking them to start their task. DONT omit any necessary info such as path, link, environment, programming language, framework, requirement, constraint from original content to team members because you are their sole info source.\nPay close attention to new user message, review the conversation history, use RoleZero.reply_to_human to respond to the user directly, DON'T ask your team members.\n\nNote:\n1. If the requirement is a pure DATA-RELATED requirement, such as bug fixes, issue reporting, environment setup, terminal operations, pip install, web browsing, web scraping, web searching, web imitation, data science, data analysis, machine learning, deep learning, text-to-image etc. DON'T decompose it, assign a single task with the original user requirement as instruction directly to Data Analyst.\n2. If the requirement is developing a software, game, app, or website, excluding the above data-related tasks, you should decompose the requirement into multiple tasks and assign them to different team members based on their expertise, usually the sequence of Product Manager -> Architect -> Project Manager -> Engineer -> (optional: QaEngine if present) -> (optional: DataAnalyst if user requests deployment), each assigned ONE task. When publishing message to Product Manager, you should directly copy the full original user requirement.\n3. If the requirement contains both DATA-RELATED part mentioned in 1 and software development part mentioned in 2, you should decompose the software development part and assign them to different team members based on their expertise, and assign the DATA-RELATED part to Data Analyst David directly.\n4. If the requirement is a common-sense, logical, or math problem, you should respond directly without assigning any task to team members.\n5. If you think the requirement is not clear or ambiguous, you should ask the user for clarification immediately. Assign tasks only after all info is clear.\n6. It is helpful for Engineer to have both the system design and the project schedule for writing the code, so include paths of both files (if available) and remind Engineer to definitely read them when publishing message to Engineer.\n7. If the requirement is writing a TRD and software framework, you should assign it to Architect. When publishing message to Architect, you should directly copy the full original user requirement.\n\nPay close attention to the Example provided, you can reuse the example for your current situation if it fits.\nYou may use any of the available commands to create a plan or update the plan. You may output mutiple commands, they will be executed sequentially.\nIf you finish current task, you will automatically take the next task in the existing plan, use Plan.finish_task, DON'T append a new task.\n\n# Your commands in a json array, in the following output format. If there is nothing to do, use the pass or end command:\nSome text indicating your thoughts, such as how you should update the plan status, respond to inquiry, or seek for help. Then a json array of commands. You must output ONE and ONLY ONE json array. DON'T output multiple json arrays with thoughts between them.\n```json\n[\n {\n \"command_name\": str,\n \"args\": {\"arg_name\": arg_value, ...}\n },\n ...\n]\n```\nNotice: your output JSON data section must start with **```json [**\n", + "content": "\n# Current Plan\n{'goal': \"from to {''}: Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.\", 'tasks': []}\n\n# Current Task\n\n\n# Instruction\nYou are a team leader, and you are responsible for drafting tasks and routing tasks to your team members.\nYour team member:\nTim: Team Leader, \nAlice: Product Manager, efficiently create a successful product that meets market demands and user expectations\nBob: Architect, design a concise, usable, complete software system\nEve: Project Manager, break down tasks according to PRD/technical design, generate a task list, and analyze task dependencies to start with the prerequisite modules\nAlex: Engineer, Take on game, app, and web development\nDavid: DataAnalyst, Take on any data-related tasks, such as data analysis, machine learning, deep learning, web browsing, web scraping, web searching, web deployment, terminal operation, git and github operation, etc.\n\nYou should NOT assign consecutive tasks to the same team member, instead, assign an aggregated task (or the complete requirement) and let the team member to decompose it.\nWhen creating a new plan involving multiple members, create all tasks at once.\nIf plan is created, you should track the progress based on team member feedback message, and update plan accordingly, such as Plan.finish_current_task, Plan.reset_task, Plan.replace_task, etc.\nYou should use TeamLeader.publish_team_message to team members, asking them to start their task. DONT omit any necessary info such as path, link, environment, programming language, framework, requirement, constraint from original content to team members because you are their sole info source.\nPay close attention to new user message, review the conversation history, use RoleZero.reply_to_human to respond to the user directly, DON'T ask your team members.\n\nNote:\n1. If the requirement is a pure DATA-RELATED requirement, such as bug fixes, issue reporting, environment setup, terminal operations, pip install, web browsing, web scraping, web searching, web imitation, data science, data analysis, machine learning, deep learning, text-to-image etc. DON'T decompose it, assign a single task with the original user requirement as instruction directly to Data Analyst.\n2. If the requirement is developing a software, game, app, or website, excluding the above data-related tasks, you should decompose the requirement into multiple tasks and assign them to different team members based on their expertise, usually the sequence of Product Manager -> Architect -> Project Manager -> Engineer -> (optional: QaEngine if present) -> (optional: DataAnalyst if user requests deployment), each assigned ONE task. When publishing message to Product Manager, you should directly copy the full original user requirement.\n3. If the requirement contains both DATA-RELATED part mentioned in 1 and software development part mentioned in 2, you should decompose the software development part and assign them to different team members based on their expertise, and assign the DATA-RELATED part to Data Analyst David directly.\n4. If the requirement is a common-sense, logical, or math problem, you should respond directly without assigning any task to team members.\n5. If you think the requirement is not clear or ambiguous, you should ask the user for clarification immediately. Assign tasks only after all info is clear.\n6. It is helpful for Engineer to have both the system design and the project schedule for writing the code, so include paths of both files (if available) and remind Engineer to definitely read them when publishing message to Engineer.\n7. If the requirement is writing a TRD and software framework, you should assign it to Architect. When publishing message to Architect, you should directly copy the full original user requirement.\n", } ] OBJ4 = [ { "role": "user", - "content": "\n# Data Structure\n\n\n# Current Plan\n{'goal': \"from to {''}: how does the project go?\", 'tasks': []}\n\n# Current Task\n\n\n# Example\n\n\n# Instruction\nYou are a team leader, and you are responsible for drafting tasks and routing tasks to your team members.\nYour team member:\nTim: Team Leader, \nAlice: Product Manager, efficiently create a successful product that meets market demands and user expectations\nBob: Architect, design a concise, usable, complete software system\nEve: Project Manager, break down tasks according to PRD/technical design, generate a task list, and analyze task dependencies to start with the prerequisite modules\nAlex: Engineer, Take on game, app, and web development\nDavid: DataAnalyst, Take on any data-related tasks, such as data analysis, machine learning, deep learning, web browsing, web scraping, web searching, web deployment, terminal operation, git and github operation, etc.\n\nYou should NOT assign consecutive tasks to the same team member, instead, assign an aggregated task (or the complete requirement) and let the team member to decompose it.\nWhen creating a new plan involving multiple members, create all tasks at once.\nIf plan is created, you should track the progress based on team member feedback message, and update plan accordingly, such as Plan.finish_current_task, Plan.reset_task, Plan.replace_task, etc.\nYou should use TeamLeader.publish_team_message to team members, asking them to start their task. DONT omit any necessary info such as path, link, environment, programming language, framework, requirement, constraint from original content to team members because you are their sole info source.\nPay close attention to new user message, review the conversation history, use RoleZero.reply_to_human to respond to the user directly, DON'T ask your team members.\n\nNote:\n1. If the requirement is a pure DATA-RELATED requirement, such as bug fixes, issue reporting, environment setup, terminal operations, pip install, web browsing, web scraping, web searching, web imitation, data science, data analysis, machine learning, deep learning, text-to-image etc. DON'T decompose it, assign a single task with the original user requirement as instruction directly to Data Analyst.\n2. If the requirement is developing a software, game, app, or website, excluding the above data-related tasks, you should decompose the requirement into multiple tasks and assign them to different team members based on their expertise, usually the sequence of Product Manager -> Architect -> Project Manager -> Engineer -> (optional: QaEngine if present) -> (optional: DataAnalyst if user requests deployment), each assigned ONE task. When publishing message to Product Manager, you should directly copy the full original user requirement.\n3. If the requirement contains both DATA-RELATED part mentioned in 1 and software development part mentioned in 2, you should decompose the software development part and assign them to different team members based on their expertise, and assign the DATA-RELATED part to Data Analyst David directly.\n4. If the requirement is a common-sense, logical, or math problem, you should respond directly without assigning any task to team members.\n5. If you think the requirement is not clear or ambiguous, you should ask the user for clarification immediately. Assign tasks only after all info is clear.\n6. It is helpful for Engineer to have both the system design and the project schedule for writing the code, so include paths of both files (if available) and remind Engineer to definitely read them when publishing message to Engineer.\n7. If the requirement is writing a TRD and software framework, you should assign it to Architect. When publishing message to Architect, you should directly copy the full original user requirement.\n\nPay close attention to the Example provided, you can reuse the example for your current situation if it fits.\nYou may use any of the available commands to create a plan or update the plan. You may output mutiple commands, they will be executed sequentially.\nIf you finish current task, you will automatically take the next task in the existing plan, use Plan.finish_task, DON'T append a new task.\n\n# Your commands in a json array, in the following output format. If there is nothing to do, use the pass or end command:\nSome text indicating your thoughts, such as how you should update the plan status, respond to inquiry, or seek for help. Then a json array of commands. You must output ONE and ONLY ONE json array. DON'T output multiple json arrays with thoughts between them.\n```json\n[\n {\n \"command_name\": str,\n \"args\": {\"arg_name\": arg_value, ...}\n },\n ...\n]\n```\nNotice: your output JSON data section must start with **```json [**\n", + "content": "\n# Current Plan\n{'goal': \"from to {''}: how does the project go?\", 'tasks': []}\n\n# Current Task\n\n\n# Instruction\nYou are a team leader, and you are responsible for drafting tasks and routing tasks to your team members.\nYour team member:\nTim: Team Leader, \nAlice: Product Manager, efficiently create a successful product that meets market demands and user expectations\nBob: Architect, design a concise, usable, complete software system\nEve: Project Manager, break down tasks according to PRD/technical design, generate a task list, and analyze task dependencies to start with the prerequisite modules\nAlex: Engineer, Take on game, app, and web development\nDavid: DataAnalyst, Take on any data-related tasks, such as data analysis, machine learning, deep learning, web browsing, web scraping, web searching, web deployment, terminal operation, git and github operation, etc.\n\nYou should NOT assign consecutive tasks to the same team member, instead, assign an aggregated task (or the complete requirement) and let the team member to decompose it.\nWhen creating a new plan involving multiple members, create all tasks at once.\nIf plan is created, you should track the progress based on team member feedback message, and update plan accordingly, such as Plan.finish_current_task, Plan.reset_task, Plan.replace_task, etc.\nYou should use TeamLeader.publish_team_message to team members, asking them to start their task. DONT omit any necessary info such as path, link, environment, programming language, framework, requirement, constraint from original content to team members because you are their sole info source.\nPay close attention to new user message, review the conversation history, use RoleZero.reply_to_human to respond to the user directly, DON'T ask your team members.\n\nNote:\n1. If the requirement is a pure DATA-RELATED requirement, such as bug fixes, issue reporting, environment setup, terminal operations, pip install, web browsing, web scraping, web searching, web imitation, data science, data analysis, machine learning, deep learning, text-to-image etc. DON'T decompose it, assign a single task with the original user requirement as instruction directly to Data Analyst.\n2. If the requirement is developing a software, game, app, or website, excluding the above data-related tasks, you should decompose the requirement into multiple tasks and assign them to different team members based on their expertise, usually the sequence of Product Manager -> Architect -> Project Manager -> Engineer -> (optional: QaEngine if present) -> (optional: DataAnalyst if user requests deployment), each assigned ONE task. When publishing message to Product Manager, you should directly copy the full original user requirement.\n3. If the requirement contains both DATA-RELATED part mentioned in 1 and software development part mentioned in 2, you should decompose the software development part and assign them to different team members based on their expertise, and assign the DATA-RELATED part to Data Analyst David directly.\n4. If the requirement is a common-sense, logical, or math problem, you should respond directly without assigning any task to team members.\n5. If you think the requirement is not clear or ambiguous, you should ask the user for clarification immediately. Assign tasks only after all info is clear.\n6. It is helpful for Engineer to have both the system design and the project schedule for writing the code, so include paths of both files (if available) and remind Engineer to definitely read them when publishing message to Engineer.\n7. If the requirement is writing a TRD and software framework, you should assign it to Architect. When publishing message to Architect, you should directly copy the full original user requirement.\n", } ] @@ -88,7 +88,7 @@ Explanation: The requirement is about software development. Assign each tasks to { "command_name": "RoleZero.reply_to_human", "args": { - "content": "I have assigned the tasks to the team members. Alice will create the PRD, Bob will design the software architecture, Eve will break down the architecture into tasks, Alex will implement the core game logic, and Edward will write comprehensive tests. The team will work on the project accordingly", + "content": "I have assigned the tasks to the team members. Alice will create the PRD, Bob will design the software architecture, Eve will break down the architecture into tasks, Alex will implement the core game logic, and Edward will write comprehensive tests. The team will work on the project accordingly" } }, { @@ -120,7 +120,7 @@ Explanation: DON'T decompose requirement if it is a DATA-RELATED task, assign a { "command_name": "RoleZero.reply_to_human", "args": { - "content": "I have assigned the task to David. He will break down the task further by himself and starts solving it.", + "content": "I have assigned the task to David. He will break down the task further by himself and starts solving it." } }, { @@ -139,15 +139,15 @@ Explanation: You received a message from Alice, the Product Manager, that she ha }, { "command_name": "TeamLeader.publish_message", - "args": { - "content": "Please design the software architecture for the snake game based on the PRD created by Alice. The PRD is at 'docs/prd/20240424153821.json'. Include the choice of programming language, libraries, and data flow, etc.", - "send_to": "Bob" - } + "args": { + "content": "Please design the software architecture for the snake game based on the PRD created by Alice. The PRD is at 'docs/prd/20240424153821.json'. Include the choice of programming language, libraries, and data flow, etc.", + "send_to": "Bob" + } }, { "command_name": "RoleZero.reply_to_human", "args": { - "content": "Alice has completed the PRD. I have marked her task as finished and sent the PRD to Bob. Bob will work on the software architecture.", + "content": "Alice has completed the PRD. I have marked her task as finished and sent the PRD to Bob. Bob will work on the software architecture." } }, { @@ -163,7 +163,7 @@ Explanation: The user is asking for a general update on the project status. Give { "command_name": "RoleZero.reply_to_human", "args": { - "content": "The team is currently working on ... We have completed ...", + "content": "The team is currently working on ... We have completed ..." } }, { diff --git a/metagpt/exp_pool/context_builders/action_node.py b/metagpt/exp_pool/context_builders/action_node.py index ade157822..a3362875c 100644 --- a/metagpt/exp_pool/context_builders/action_node.py +++ b/metagpt/exp_pool/context_builders/action_node.py @@ -1,5 +1,6 @@ """Action Node context builder.""" +from typing import Any from metagpt.exp_pool.context_builders.base import BaseContextBuilder @@ -17,17 +18,12 @@ Consider **Experiences** to generate a better answer. class ActionNodeContextBuilder(BaseContextBuilder): - async def build(self, **kwargs) -> str: + async def build(self, req: Any) -> str: """Builds the action node context string. - Args: - **kwargs: Arbitrary keyword arguments, expecting 'req' as a key. - - Returns: - str: The formatted context string using the request and formatted experiences. - If no experiences are available, returns the request as is. + If there are no experiences, returns the original `req`; + otherwise returns context with `req` and formatted experiences. """ - req = kwargs.get("req", "") exps = self.format_exps() return ACTION_NODE_CONTEXT_TEMPLATE.format(req=req, exps=exps) if exps else req diff --git a/metagpt/exp_pool/context_builders/base.py b/metagpt/exp_pool/context_builders/base.py index d1133c2da..f937f5c7b 100644 --- a/metagpt/exp_pool/context_builders/base.py +++ b/metagpt/exp_pool/context_builders/base.py @@ -16,8 +16,11 @@ class BaseContextBuilder(BaseModel, ABC): exps: list[Experience] = [] @abstractmethod - async def build(self, **kwargs) -> Any: - """Build context from parameters.""" + async def build(self, req: Any) -> Any: + """Build context from req. + + Do not modify `req`. If modification is necessary, use copy.deepcopy to create a copy first. + """ def format_exps(self) -> str: """Format experiences into a numbered list of strings. diff --git a/metagpt/exp_pool/context_builders/role_zero.py b/metagpt/exp_pool/context_builders/role_zero.py index 6407314ac..2ee469661 100644 --- a/metagpt/exp_pool/context_builders/role_zero.py +++ b/metagpt/exp_pool/context_builders/role_zero.py @@ -1,34 +1,36 @@ """RoleZero context builder.""" +import copy import re +from typing import Any from metagpt.exp_pool.context_builders.base import BaseContextBuilder class RoleZeroContextBuilder(BaseContextBuilder): - async def build(self, **kwargs) -> list[dict]: - """Builds the context by updating the req with formatted experiences. + async def build(self, req: Any) -> list[dict]: + """Builds the role zero context string. - Args: - **kwargs: Arbitrary keyword arguments, expecting 'req' as a key. - - Returns: - list[dict]: The updated request with formatted experiences or the original request if no experiences are available. + Note: + 1. The expected format for `req`, e.g., [{...}, {"role": "user", "content": "context"}, {"role": "user", "content": "context exp part"}]. + 2. Returns the original `req` if it is empty, incorrectly formatted or there are no experiences. + 3. Creates a copy of req and replaces the example content in the copied req with actual experiences. """ - req = kwargs.get("req", []) - if not req: + if not req or len(req) < 2: return req exps = self.format_exps() if not exps: return req - req[-1]["content"] = self.replace_example_content(req[-1].get("content", ""), exps) + req_copy = copy.deepcopy(req) - return req + req_copy[-2]["content"] = self.replace_example_content(req_copy[-2].get("content", ""), exps) + + return req_copy def replace_example_content(self, text: str, new_example_content: str) -> str: - return self.replace_content_between_markers(text, "# Example", "# Instruction", new_example_content) + return self.replace_content_between_markers(text, "# Example", "# Available Commands", new_example_content) @staticmethod def replace_content_between_markers(text: str, start_marker: str, end_marker: str, new_content: str) -> str: diff --git a/metagpt/exp_pool/context_builders/simple.py b/metagpt/exp_pool/context_builders/simple.py index 565855664..d7b8d0be9 100644 --- a/metagpt/exp_pool/context_builders/simple.py +++ b/metagpt/exp_pool/context_builders/simple.py @@ -1,6 +1,8 @@ """Simple context builder.""" +from typing import Any + from metagpt.exp_pool.context_builders.base import BaseContextBuilder SIMPLE_CONTEXT_TEMPLATE = """ @@ -20,5 +22,5 @@ Consider **Experiences** to generate a better answer. class SimpleContextBuilder(BaseContextBuilder): - async def build(self, **kwargs) -> str: - return SIMPLE_CONTEXT_TEMPLATE.format(req=kwargs.get("req", ""), exps=self.format_exps()) + async def build(self, req: Any) -> str: + return SIMPLE_CONTEXT_TEMPLATE.format(req=req, exps=self.format_exps()) diff --git a/metagpt/exp_pool/decorator.py b/metagpt/exp_pool/decorator.py index 0a9a83818..566127f59 100644 --- a/metagpt/exp_pool/decorator.py +++ b/metagpt/exp_pool/decorator.py @@ -200,7 +200,7 @@ class ExpCacheHandler(BaseModel): async def _build_context(self) -> str: self.context_builder.exps = self._exps - return await self.context_builder.build(**self.kwargs) + return await self.context_builder.build(self.kwargs["req"]) async def _execute_function(self): self.kwargs["req"] = await self._build_context() diff --git a/metagpt/exp_pool/serializers/role_zero.py b/metagpt/exp_pool/serializers/role_zero.py index 82a32e36b..f5363b1ff 100644 --- a/metagpt/exp_pool/serializers/role_zero.py +++ b/metagpt/exp_pool/serializers/role_zero.py @@ -3,7 +3,6 @@ import copy import json -from metagpt.exp_pool.context_builders import RoleZeroContextBuilder from metagpt.exp_pool.serializers.simple import SimpleSerializer @@ -11,14 +10,15 @@ class RoleZeroSerializer(SimpleSerializer): def serialize_req(self, req: list[dict]) -> str: """Serialize the request for database storage, ensuring it is a string. - This function does not modify `req`; it only extracts the necessary content from `req` because `req` may be very lengthy and could cause embedding errors. + Only extracts the necessary content from `req` because `req` may be very lengthy and could cause embedding errors. Args: req (list[dict]): The request to be serialized. Example: [ {"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}, - {"role": "user", "content": "..."}, + {"role": "user", "content": "context"}, + {"role": "user", "content": "context exp part"}, ] Returns: @@ -28,12 +28,12 @@ class RoleZeroSerializer(SimpleSerializer): return "" filtered_req = self._filter_req(req) - self._clean_last_entry_content(filtered_req) + filtered_req.append(req[-1]) return json.dumps(filtered_req) def _filter_req(self, req: list[dict]) -> list[dict]: - """Filter the request to include only necessary items and the last entry. + """Filter the `req` to include only necessary items. Args: req (list[dict]): The original request. @@ -45,20 +45,5 @@ class RoleZeroSerializer(SimpleSerializer): filtered_req = [ copy.deepcopy(item) for item in req if "Command Editor.read executed: file_path" in item["content"] ] - filtered_req.append(copy.deepcopy(req[-1])) return filtered_req - - def _clean_last_entry_content(self, req: list[dict]): - """Modifies the content of the last element in the request to remove unnecessary sections, making the request more concise.""" - - last_content = req[-1]["content"] - - last_content = RoleZeroContextBuilder.replace_content_between_markers( - last_content, "# Data Structure", "# Current Plan", "" - ) - last_content = RoleZeroContextBuilder.replace_content_between_markers( - last_content, "# Example", "# Instruction", "" - ) - - req[-1]["content"] = last_content diff --git a/metagpt/prompts/di/role_zero.py b/metagpt/prompts/di/role_zero.py index 8f4a8804e..436ad7cdd 100644 --- a/metagpt/prompts/di/role_zero.py +++ b/metagpt/prompts/di/role_zero.py @@ -8,7 +8,16 @@ Note: 2. Carefully review your progress at the current task, if your actions so far has not fulfilled the task instruction, you should continue with current task. Otherwise, finish current task. 3. Each time you finish a task, use RoleZero.reply_to_human to report your progress. """ +CMD_PROMPT_EXP_PART = """ +# Current Plan +{plan_status} +# Current Task +{current_task} + +# Instruction +{instruction} +""" CMD_PROMPT = """ # Data Structure class Task(BaseModel): @@ -18,21 +27,14 @@ class Task(BaseModel): task_type: str = "" assignee: str = "" +# Example +{example} + # Available Commands {available_commands} Special Command: Use {{"command_name": "end"}} to do nothing or indicate completion of all requirements and the end of actions. -# Current Plan -{plan_status} - -# Current Task -{current_task} - -# Example -{example} - -# Instruction -{instruction} +{cmd_prompt_exp_part} Pay close attention to the Example provided, you can reuse the example for your current situation if it fits. You may use any of the available commands to create a plan or update the plan. You may output mutiple commands, they will be executed sequentially. diff --git a/metagpt/roles/di/role_zero.py b/metagpt/roles/di/role_zero.py index 59c58861f..5b002d994 100644 --- a/metagpt/roles/di/role_zero.py +++ b/metagpt/roles/di/role_zero.py @@ -16,6 +16,7 @@ from metagpt.exp_pool.serializers import RoleZeroSerializer from metagpt.logs import logger from metagpt.prompts.di.role_zero import ( CMD_PROMPT, + CMD_PROMPT_EXP_PART, JSON_REPAIR_PROMPT, ROLE_INSTRUCTION, ) @@ -144,13 +145,14 @@ class RoleZero(Role): tool_info = json.dumps({tool.name: tool.schemas for tool in tools}) ### Make Decision Dynamically ### - prompt = self.cmd_prompt.format( + cmd_prompt_exp_part = CMD_PROMPT_EXP_PART.format( plan_status=plan_status, current_task=current_task, - example=example, - available_commands=tool_info, instruction=self.instruction.strip(), ) + prompt = self.cmd_prompt.format( + example=example, available_commands=tool_info, cmd_prompt_exp_part=cmd_prompt_exp_part + ) memory = self.rc.memory.get(self.memory_k) if not self.browser.is_empty_page: pattern = re.compile(r"Command Browser\.(\w+) executed") @@ -158,16 +160,24 @@ class RoleZero(Role): if pattern.match(msg.content): memory.insert(index, UserMessage(cause_by="browser", content=await self.browser.view())) break - context = self.llm.format_msg(memory + [UserMessage(content=prompt)]) - # print(*context, sep="\n" + "*" * 5 + "\n") + req = self.llm.format_msg(memory + [UserMessage(content=prompt), UserMessage(content=cmd_prompt_exp_part)]) async with ThoughtReporter(enable_llm_stream=True): - self.command_rsp = await self.llm_cached_aask(req=context, system_msgs=self.system_msg) + self.command_rsp = await self.llm_cached_aask(req=req, system_msgs=self.system_msg) self.rc.memory.add(AIMessage(content=self.command_rsp)) return True @exp_cache(context_builder=RoleZeroContextBuilder(), serializer=RoleZeroSerializer()) async def llm_cached_aask(self, *, req: list[dict], system_msgs: list[str]) -> str: + """Use `exp_cache` to automatically manage experiences. + + The `RoleZeroContextBuilder` attempts to add experiences to `req`. + The `RoleZeroSerializer` extracts essential parts of `req` for the experience pool, trimming lengthy entries to retain only necessary parts. + """ + # Remove the "cmd_prompt_exp_part", it is only used within the exp_cache decorator. + if req: + req.pop() + return await self.llm.aask(req, system_msgs=system_msgs) async def _act(self) -> Message: diff --git a/tests/metagpt/exp_pool/test_context_builders/test_rolezero_context_builder.py b/tests/metagpt/exp_pool/test_context_builders/test_rolezero_context_builder.py index 611d68211..a95566ed1 100644 --- a/tests/metagpt/exp_pool/test_context_builders/test_rolezero_context_builder.py +++ b/tests/metagpt/exp_pool/test_context_builders/test_rolezero_context_builder.py @@ -25,27 +25,31 @@ class TestRoleZeroContextBuilder: async def test_build_with_experiences(self, context_builder, mocker): mocker.patch.object(BaseContextBuilder, "format_exps", return_value="Formatted experiences") mocker.patch.object(RoleZeroContextBuilder, "replace_example_content", return_value="Updated content") - req = [{"content": "Original content"}] + req = [{"content": "Original content 1"}, {"content": "Original content exp part"}] result = await context_builder.build(req=req) - assert result == [{"content": "Updated content"}] + assert result == [{"content": "Updated content"}, {"content": "Original content exp part"}] def test_replace_example_content(self, context_builder, mocker): mocker.patch.object(RoleZeroContextBuilder, "replace_content_between_markers", return_value="Replaced content") result = context_builder.replace_example_content("Original text", "New example content") assert result == "Replaced content" context_builder.replace_content_between_markers.assert_called_once_with( - "Original text", "# Example", "# Instruction", "New example content" + "Original text", "# Example", "# Available Commands", "New example content" ) def test_replace_content_between_markers(self): - text = "Start\n# Example\nOld content\n# Instruction\nEnd" + text = "Start\n# Example\nOld content\n# Available Commands\nEnd" new_content = "New content" - result = RoleZeroContextBuilder.replace_content_between_markers(text, "# Example", "# Instruction", new_content) - expected = "Start\n# Example\nNew content\n\n# Instruction\nEnd" + result = RoleZeroContextBuilder.replace_content_between_markers( + text, "# Example", "# Available Commands", new_content + ) + expected = "Start\n# Example\nNew content\n\n# Available Commands\nEnd" assert result == expected def test_replace_content_between_markers_no_match(self): text = "Start\nNo markers\nEnd" new_content = "New content" - result = RoleZeroContextBuilder.replace_content_between_markers(text, "# Example", "# Instruction", new_content) + result = RoleZeroContextBuilder.replace_content_between_markers( + text, "# Example", "# Available Commands", new_content + ) assert result == text diff --git a/tests/metagpt/exp_pool/test_context_builders/test_simple_context_builder.py b/tests/metagpt/exp_pool/test_context_builders/test_simple_context_builder.py index b6d0f642e..cf1a42f27 100644 --- a/tests/metagpt/exp_pool/test_context_builders/test_simple_context_builder.py +++ b/tests/metagpt/exp_pool/test_context_builders/test_simple_context_builder.py @@ -13,7 +13,7 @@ class TestSimpleContextBuilder: return SimpleContextBuilder() @pytest.mark.asyncio - async def test_build_with_experiences(self, context_builder, mocker): + async def test_build_with_experiences(self, mocker, context_builder: SimpleContextBuilder): # Mock the format_exps method mock_exps = "Mocked experiences" mocker.patch.object(BaseContextBuilder, "format_exps", return_value=mock_exps) @@ -25,7 +25,7 @@ class TestSimpleContextBuilder: assert result == expected @pytest.mark.asyncio - async def test_build_without_experiences(self, context_builder, mocker): + async def test_build_without_experiences(self, mocker, context_builder: SimpleContextBuilder): # Mock the format_exps method to return an empty string mocker.patch.object(BaseContextBuilder, "format_exps", return_value="") @@ -36,12 +36,12 @@ class TestSimpleContextBuilder: assert result == expected @pytest.mark.asyncio - async def test_build_without_req(self, context_builder, mocker): + async def test_build_without_req(self, mocker, context_builder: SimpleContextBuilder): # Mock the format_exps method mock_exps = "Mocked experiences" mocker.patch.object(BaseContextBuilder, "format_exps", return_value=mock_exps) - result = await context_builder.build() + result = await context_builder.build(req="") expected = SIMPLE_CONTEXT_TEMPLATE.format(req="", exps=mock_exps) assert result == expected diff --git a/tests/metagpt/exp_pool/test_serializers/test_role_zero.py b/tests/metagpt/exp_pool/test_serializers/test_role_zero.py index 4c1f3daf3..d4525d535 100644 --- a/tests/metagpt/exp_pool/test_serializers/test_role_zero.py +++ b/tests/metagpt/exp_pool/test_serializers/test_role_zero.py @@ -7,71 +7,42 @@ from metagpt.exp_pool.serializers import RoleZeroSerializer class TestRoleZeroSerializer: @pytest.fixture - def serializer(self): + def serializer(self) -> RoleZeroSerializer: return RoleZeroSerializer() + @pytest.fixture + def last_item(self) -> dict: + return { + "role": "user", + "content": "# Current Plan\nsome plan\n# Current Plan\nsome plan\n# Instruction\nsome instruction", + } + + @pytest.fixture + def sample_req(self): + return [{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}] + def test_serialize_req_empty_input(self, serializer: RoleZeroSerializer): assert serializer.serialize_req([]) == "" - def test_serialize_req_with_content(self, serializer: RoleZeroSerializer): + def test_serialize_req_with_content(self, serializer: RoleZeroSerializer, last_item: dict): req = [ - {"content": "Command Editor.read executed: file_path=test.py"}, - {"content": "Some other content"}, - { - "content": "# Data Structure\nsome data\n# Current Plan\nsome plan\n# Example\nsome example\n# Instruction\nsome instruction" - }, + {"role": "user", "content": "Command Editor.read executed: file_path=test.py"}, + {"role": "assistant", "content": "Some other content"}, + last_item, ] expected_output = json.dumps( - [ - {"content": "Command Editor.read executed: file_path=test.py"}, - { - "content": "# Data Structure\n\n\n# Current Plan\nsome plan\n# Example\n\n\n# Instruction\nsome instruction" - }, - ] + [{"role": "user", "content": "Command Editor.read executed: file_path=test.py"}, last_item] ) assert serializer.serialize_req(req) == expected_output def test_filter_req(self, serializer: RoleZeroSerializer): req = [ - {"content": "Command Editor.read executed: file_path=test1.py"}, - {"content": "Some other content"}, - {"content": "Command Editor.read executed: file_path=test2.py"}, - {"content": "Final content"}, + {"role": "user", "content": "Command Editor.read executed: file_path=test1.py"}, + {"role": "assistant", "content": "Some other content"}, + {"role": "user", "content": "Command Editor.read executed: file_path=test2.py"}, + {"role": "assistant", "content": "Final content"}, ] filtered_req = serializer._filter_req(req) - assert len(filtered_req) == 3 + assert len(filtered_req) == 2 assert filtered_req[0]["content"] == "Command Editor.read executed: file_path=test1.py" assert filtered_req[1]["content"] == "Command Editor.read executed: file_path=test2.py" - assert filtered_req[2]["content"] == "Final content" - - def test_clean_last_entry_content(self, serializer: RoleZeroSerializer): - req = [ - {"content": "Some content"}, - { - "content": "# Data Structure\nsome data\n# Current Plan\nsome plan\n# Example\nsome example\n# Instruction\nsome instruction" - }, - ] - serializer._clean_last_entry_content(req) - expected_content = ( - "# Data Structure\n\n\n# Current Plan\nsome plan\n# Example\n\n\n# Instruction\nsome instruction" - ) - assert req[-1]["content"] == expected_content - - def test_integration(self, serializer: RoleZeroSerializer): - req = [ - {"content": "Command Editor.read executed: file_path=test.py"}, - {"content": "Some other content"}, - { - "content": "# Data Structure\nsome data\n# Current Plan\nsome plan\n# Example\nsome example\n# Instruction\nsome instruction" - }, - ] - result = serializer.serialize_req(req) - expected_output = json.dumps( - [ - {"content": "Command Editor.read executed: file_path=test.py"}, - { - "content": "# Data Structure\n\n\n# Current Plan\nsome plan\n# Example\n\n\n# Instruction\nsome instruction" - }, - ] - ) - assert result == expected_output From b20316d6cd2b87a2888fbf71c2594cb3520f36e7 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Mon, 15 Jul 2024 16:00:05 +0800 Subject: [PATCH 095/119] format code --- metagpt/exp_pool/context_builders/action_node.py | 1 + metagpt/exp_pool/context_builders/role_zero.py | 1 + metagpt/exp_pool/manager.py | 6 +++++- metagpt/exp_pool/scorers/simple.py | 1 + metagpt/exp_pool/serializers/role_zero.py | 1 + 5 files changed, 9 insertions(+), 1 deletion(-) diff --git a/metagpt/exp_pool/context_builders/action_node.py b/metagpt/exp_pool/context_builders/action_node.py index a3362875c..891b898be 100644 --- a/metagpt/exp_pool/context_builders/action_node.py +++ b/metagpt/exp_pool/context_builders/action_node.py @@ -24,6 +24,7 @@ class ActionNodeContextBuilder(BaseContextBuilder): If there are no experiences, returns the original `req`; otherwise returns context with `req` and formatted experiences. """ + exps = self.format_exps() return ACTION_NODE_CONTEXT_TEMPLATE.format(req=req, exps=exps) if exps else req diff --git a/metagpt/exp_pool/context_builders/role_zero.py b/metagpt/exp_pool/context_builders/role_zero.py index 2ee469661..aa5524ab4 100644 --- a/metagpt/exp_pool/context_builders/role_zero.py +++ b/metagpt/exp_pool/context_builders/role_zero.py @@ -16,6 +16,7 @@ class RoleZeroContextBuilder(BaseContextBuilder): 2. Returns the original `req` if it is empty, incorrectly formatted or there are no experiences. 3. Creates a copy of req and replaces the example content in the copied req with actual experiences. """ + if not req or len(req) < 2: return req diff --git a/metagpt/exp_pool/manager.py b/metagpt/exp_pool/manager.py index ba1a8bcf0..d6922ff00 100644 --- a/metagpt/exp_pool/manager.py +++ b/metagpt/exp_pool/manager.py @@ -39,7 +39,7 @@ class ExperienceManager(BaseModel): similarity_top_k=DEFAULT_SIMILARITY_TOP_K, ) ] - ranker_configs = [LLMRankerConfig()] + ranker_configs = [LLMRankerConfig(top_n=DEFAULT_SIMILARITY_TOP_K)] self.storage = SimpleEngine.from_objs(retriever_configs=retriever_configs, ranker_configs=ranker_configs) @@ -57,6 +57,7 @@ class ExperienceManager(BaseModel): Args: exp (Experience): The experience to add. """ + if not self.config.exp_pool.enable_write: return @@ -74,6 +75,7 @@ class ExperienceManager(BaseModel): Returns: list[Experience]: A list of experiences that match the args. """ + if not self.config.exp_pool.enable_read: return [] @@ -90,6 +92,8 @@ class ExperienceManager(BaseModel): return exps def get_exps_count(self) -> int: + """Get the total number of experiences.""" + return self.vector_store._collection.count() diff --git a/metagpt/exp_pool/scorers/simple.py b/metagpt/exp_pool/scorers/simple.py index fd7b6537b..4b060aac4 100644 --- a/metagpt/exp_pool/scorers/simple.py +++ b/metagpt/exp_pool/scorers/simple.py @@ -57,6 +57,7 @@ class SimpleScorer(BaseScorer): Returns: Score: An object containing the score (1-10) and the reasoning. """ + prompt = SIMPLE_SCORER_TEMPLATE.format(req=req, resp=resp) resp = await self.llm.aask(prompt) resp_json = json.loads(CodeParser.parse_code(resp, lang="json")) diff --git a/metagpt/exp_pool/serializers/role_zero.py b/metagpt/exp_pool/serializers/role_zero.py index f5363b1ff..720bf5078 100644 --- a/metagpt/exp_pool/serializers/role_zero.py +++ b/metagpt/exp_pool/serializers/role_zero.py @@ -24,6 +24,7 @@ class RoleZeroSerializer(SimpleSerializer): Returns: str: The serialized request as a JSON string. """ + if not req: return "" From e315f61f60fe1a3bcbcdde3df115a2314673a2dd Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Mon, 15 Jul 2024 17:10:34 +0800 Subject: [PATCH 096/119] update comment --- metagpt/exp_pool/context_builders/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/metagpt/exp_pool/context_builders/base.py b/metagpt/exp_pool/context_builders/base.py index f937f5c7b..691d51c8c 100644 --- a/metagpt/exp_pool/context_builders/base.py +++ b/metagpt/exp_pool/context_builders/base.py @@ -7,7 +7,7 @@ from pydantic import BaseModel, ConfigDict from metagpt.exp_pool.schema import Experience -EXP_TEMPLATE = """Given the request: {req}, We can get the response: {resp}, Which scored: {score}.""" +EXP_TEMPLATE = """Given the request: {req}, We can get the response: {resp}, which scored: {score}.""" class BaseContextBuilder(BaseModel, ABC): @@ -26,8 +26,8 @@ class BaseContextBuilder(BaseModel, ABC): """Format experiences into a numbered list of strings. Example: - 1. Given the request: req1, We can get the response: resp1, Which scored: 8. - 2. Given the request: req2, We can get the response: resp2, Which scored: 9. + 1. Given the request: req1, We can get the response: resp1, which scored: 8. + 2. Given the request: req2, We can get the response: resp2, which scored: 9. Returns: str: The formatted experiences as a string. From f68b26d70391266dd420ed6b21dbce39f153ffac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97=E4=B9=89=E7=AB=A0?= Date: Mon, 15 Jul 2024 09:15:49 +0000 Subject: [PATCH 097/119] add default task_type value --- metagpt/schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/schema.py b/metagpt/schema.py index 94e64d7fa..de49c8886 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -669,7 +669,7 @@ class Plan(BaseModel): """ return [task for task in self.tasks if task.is_finished] - def append_task(self, task_id: str, dependent_task_ids: list[str], instruction: str, assignee: str, task_type: str): + def append_task(self, task_id: str, dependent_task_ids: list[str], instruction: str, assignee: str, task_type: str = ""): """Append a new task with task_id (number) to the end of existing task sequences. If dependent_task_ids is not empty, the task will depend on the tasks with the ids in the list.""" new_task = Task( task_id=task_id, From 0c6786feaaac5fa3cd9a684877f5c68450c285d5 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Mon, 15 Jul 2024 18:22:05 +0800 Subject: [PATCH 098/119] update comment --- metagpt/exp_pool/serializers/role_zero.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/metagpt/exp_pool/serializers/role_zero.py b/metagpt/exp_pool/serializers/role_zero.py index 720bf5078..967e53e41 100644 --- a/metagpt/exp_pool/serializers/role_zero.py +++ b/metagpt/exp_pool/serializers/role_zero.py @@ -43,8 +43,14 @@ class RoleZeroSerializer(SimpleSerializer): list[dict]: The filtered request. """ - filtered_req = [ - copy.deepcopy(item) for item in req if "Command Editor.read executed: file_path" in item["content"] - ] + filtered_req = [copy.deepcopy(item) for item in req if self._is_useful_content(item["content"])] return filtered_req + + def _is_useful_content(self, content: str) -> bool: + """Currently, only the content of the file is considered, and more judgments can be added later.""" + + if "Command Editor.read executed: file_path" in content: + return True + + return False From 8b9e992b566bf2bab8b582e219200ec5e8a46a84 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Tue, 16 Jul 2024 16:39:43 +0800 Subject: [PATCH 099/119] update serializers --- metagpt/exp_pool/decorator.py | 2 +- metagpt/exp_pool/serializers/base.py | 4 ++-- metagpt/exp_pool/serializers/role_zero.py | 8 +++++--- metagpt/exp_pool/serializers/simple.py | 4 ++-- metagpt/prompts/di/role_zero.py | 19 ++++++++--------- metagpt/roles/di/role_zero.py | 25 +++++++++++------------ 6 files changed, 30 insertions(+), 32 deletions(-) diff --git a/metagpt/exp_pool/decorator.py b/metagpt/exp_pool/decorator.py index 566127f59..7a2f926c5 100644 --- a/metagpt/exp_pool/decorator.py +++ b/metagpt/exp_pool/decorator.py @@ -121,7 +121,7 @@ class ExpCacheHandler(BaseModel): self.serializer = self.serializer or SimpleSerializer() self.tag = self.tag or self._generate_tag() - self._req = self.serializer.serialize_req(self.kwargs["req"]) + self._req = self.serializer.serialize_req(**self.kwargs) return self diff --git a/metagpt/exp_pool/serializers/base.py b/metagpt/exp_pool/serializers/base.py index 9d00a05b2..c09488e12 100644 --- a/metagpt/exp_pool/serializers/base.py +++ b/metagpt/exp_pool/serializers/base.py @@ -10,10 +10,10 @@ class BaseSerializer(BaseModel, ABC): model_config = ConfigDict(arbitrary_types_allowed=True) @abstractmethod - def serialize_req(self, req: Any) -> str: + def serialize_req(self, **kwargs) -> str: """Serializes the request for storage. - Do not modify req. If modification is necessary, use copy.deepcopy to create a copy first. + Do not modify kwargs. If modification is necessary, use copy.deepcopy to create a copy first. Note that copy.deepcopy may raise errors, such as TypeError: cannot pickle '_thread.RLock' object. """ diff --git a/metagpt/exp_pool/serializers/role_zero.py b/metagpt/exp_pool/serializers/role_zero.py index 967e53e41..89dd73f39 100644 --- a/metagpt/exp_pool/serializers/role_zero.py +++ b/metagpt/exp_pool/serializers/role_zero.py @@ -7,7 +7,7 @@ from metagpt.exp_pool.serializers.simple import SimpleSerializer class RoleZeroSerializer(SimpleSerializer): - def serialize_req(self, req: list[dict]) -> str: + def serialize_req(self, **kwargs) -> str: """Serialize the request for database storage, ensuring it is a string. Only extracts the necessary content from `req` because `req` may be very lengthy and could cause embedding errors. @@ -18,18 +18,20 @@ class RoleZeroSerializer(SimpleSerializer): {"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}, {"role": "user", "content": "context"}, - {"role": "user", "content": "context exp part"}, ] Returns: str: The serialized request as a JSON string. """ + req = kwargs.get("req", []) if not req: return "" filtered_req = self._filter_req(req) - filtered_req.append(req[-1]) + + if state_data := kwargs.get("state_data"): + filtered_req.append({"role": "user", "content": state_data}) return json.dumps(filtered_req) diff --git a/metagpt/exp_pool/serializers/simple.py b/metagpt/exp_pool/serializers/simple.py index 32fe29c9f..ebd06e0e0 100644 --- a/metagpt/exp_pool/serializers/simple.py +++ b/metagpt/exp_pool/serializers/simple.py @@ -6,10 +6,10 @@ from metagpt.exp_pool.serializers.base import BaseSerializer class SimpleSerializer(BaseSerializer): - def serialize_req(self, req: Any) -> str: + def serialize_req(self, **kwargs) -> str: """Just use `str` to convert the request object into a string.""" - return str(req) + return str(kwargs.get("req", "")) def serialize_resp(self, resp: Any) -> str: """Just use `str` to convert the response object into a string.""" diff --git a/metagpt/prompts/di/role_zero.py b/metagpt/prompts/di/role_zero.py index f1de5bf1b..41b9e023e 100644 --- a/metagpt/prompts/di/role_zero.py +++ b/metagpt/prompts/di/role_zero.py @@ -8,16 +8,6 @@ Note: 2. Carefully review your progress at the current task, if your actions so far has not fulfilled the task instruction, you should continue with current task. Otherwise, finish current task by Plan.finish_current_task explicitly. 3. Each time you finish a task, use RoleZero.reply_to_human to report your progress. """ -CMD_PROMPT_EXP_PART = """ -# Current Plan -{plan_status} - -# Current Task -{current_task} - -# Instruction -{instruction} -""" # To ensure compatibility with hard-coded experience, do not add any other content between "# Example" and "# Available Commands". CMD_PROMPT = """ # Data Structure @@ -38,7 +28,14 @@ Special Command: Use {{"command_name": "end"}} to do nothing or indicate complet # Available Task Types {task_type_desc} -{cmd_prompt_exp_part} +# Current Plan +{plan_status} + +# Current Task +{current_task} + +# Instruction +{instruction} Pay close attention to the Example provided, you can reuse the example for your current situation if it fits. You may use any of the available commands to create a plan or update the plan. You may output mutiple commands, they will be executed sequentially. diff --git a/metagpt/roles/di/role_zero.py b/metagpt/roles/di/role_zero.py index 8411cf24b..ce305707e 100644 --- a/metagpt/roles/di/role_zero.py +++ b/metagpt/roles/di/role_zero.py @@ -16,7 +16,6 @@ from metagpt.exp_pool.serializers import RoleZeroSerializer from metagpt.logs import logger from metagpt.prompts.di.role_zero import ( CMD_PROMPT, - CMD_PROMPT_EXP_PART, JSON_REPAIR_PROMPT, QUICK_THINK_PROMPT, ROLE_INSTRUCTION, @@ -147,39 +146,39 @@ class RoleZero(Role): tool_info = json.dumps({tool.name: tool.schemas for tool in tools}) ### Make Decision Dynamically ### - cmd_prompt_exp_part = CMD_PROMPT_EXP_PART.format( - plan_status=plan_status, - current_task=current_task, - instruction=self.instruction.strip(), - ) + instruction = self.instruction.strip() prompt = self.cmd_prompt.format( example=example, available_commands=tool_info, task_type_desc=self.task_type_desc, - cmd_prompt_exp_part=cmd_prompt_exp_part, + plan_status=plan_status, + current_task=current_task, + instruction=instruction, ) memory = self.rc.memory.get(self.memory_k) memory = await self.parse_browser_actions(memory) - req = self.llm.format_msg(memory + [UserMessage(content=prompt), UserMessage(content=cmd_prompt_exp_part)]) + req = self.llm.format_msg(memory + [UserMessage(content=prompt)]) async with ThoughtReporter(enable_llm_stream=True) as reporter: await reporter.async_report({"type": "react"}) - self.command_rsp = await self.llm_cached_aask(req=req, system_msgs=self.system_msg) + state_data = dict( + plan_status=plan_status, + current_task=current_task, + instruction=instruction, + ) + self.command_rsp = await self.llm_cached_aask(req=req, system_msgs=self.system_msg, state_data=state_data) self.rc.memory.add(AIMessage(content=self.command_rsp)) return True @exp_cache(context_builder=RoleZeroContextBuilder(), serializer=RoleZeroSerializer()) - async def llm_cached_aask(self, *, req: list[dict], system_msgs: list[str]) -> str: + async def llm_cached_aask(self, *, req: list[dict], system_msgs: list[str], **kwargs) -> str: """Use `exp_cache` to automatically manage experiences. The `RoleZeroContextBuilder` attempts to add experiences to `req`. The `RoleZeroSerializer` extracts essential parts of `req` for the experience pool, trimming lengthy entries to retain only necessary parts. """ - # Remove the "cmd_prompt_exp_part", it is only used within the exp_cache decorator. - if req: - req.pop() return await self.llm.aask(req, system_msgs=system_msgs) From 0b604c42b5f3af2f2fefa522ac526e51b472f8e0 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Tue, 16 Jul 2024 17:28:54 +0800 Subject: [PATCH 100/119] lazy import rag --- metagpt/exp_pool/manager.py | 44 +++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/metagpt/exp_pool/manager.py b/metagpt/exp_pool/manager.py index d6922ff00..9bf289038 100644 --- a/metagpt/exp_pool/manager.py +++ b/metagpt/exp_pool/manager.py @@ -1,7 +1,8 @@ """Experience Manager.""" -from llama_index.vector_stores.chroma import ChromaVectorStore -from pydantic import BaseModel, ConfigDict, model_validator +from typing import TYPE_CHECKING, Any + +from pydantic import BaseModel, ConfigDict from metagpt.config2 import Config, config from metagpt.exp_pool.schema import ( @@ -11,27 +12,37 @@ from metagpt.exp_pool.schema import ( QueryType, ) from metagpt.logs import logger -from metagpt.rag.engines import SimpleEngine -from metagpt.rag.schema import ChromaRetrieverConfig, LLMRankerConfig from metagpt.utils.exceptions import handle_exception +if TYPE_CHECKING: + from llama_index.vector_stores.chroma import ChromaVectorStore + class ExperienceManager(BaseModel): """ExperienceManager manages the lifecycle of experiences, including CRUD and optimization. Args: config (Config): Configuration for managing experiences. - storage (SimpleEngine): Engine to handle the storage and retrieval of experiences. + _storage (SimpleEngine): Engine to handle the storage and retrieval of experiences. + _vector_store (ChromaVectorStore): The actual place where vectors are stored. """ model_config = ConfigDict(arbitrary_types_allowed=True) config: Config = config - storage: SimpleEngine = None - @model_validator(mode="after") - def initialize(self): - if self.storage is None: + _storage: Any = None + _vector_store: Any = None + + @property + def storage(self): + if self._storage is None: + try: + from metagpt.rag.engines import SimpleEngine + from metagpt.rag.schema import ChromaRetrieverConfig, LLMRankerConfig + except ImportError: + raise ImportError("To use the experience pool, you need to install the rag module.") + retriever_configs = [ ChromaRetrieverConfig( persist_path=self.config.exp_pool.persist_path, @@ -41,14 +52,19 @@ class ExperienceManager(BaseModel): ] ranker_configs = [LLMRankerConfig(top_n=DEFAULT_SIMILARITY_TOP_K)] - self.storage = SimpleEngine.from_objs(retriever_configs=retriever_configs, ranker_configs=ranker_configs) + self._storage: SimpleEngine = SimpleEngine.from_objs( + retriever_configs=retriever_configs, ranker_configs=ranker_configs + ) + logger.debug(f"exp_pool config: {self.config.exp_pool}") - logger.debug(f"exp_pool config: {self.config.exp_pool}") - return self + return self._storage @property - def vector_store(self) -> ChromaVectorStore: - return self.storage._retriever._vector_store + def vector_store(self): + if not self._vector_store: + self._vector_store: ChromaVectorStore = self.storage._retriever._vector_store + + return self._vector_store @handle_exception def create_exp(self, exp: Experience): From 9a7b4df20340f0e9fdb4fdf9709d8632609a0c79 Mon Sep 17 00:00:00 2001 From: lidanyang Date: Tue, 16 Jul 2024 17:43:31 +0800 Subject: [PATCH 101/119] add experience examples for scraping task without using browser --- metagpt/strategy/experience_retriever.py | 75 ++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 6 deletions(-) diff --git a/metagpt/strategy/experience_retriever.py b/metagpt/strategy/experience_retriever.py index 04807ebec..f50a90e1c 100644 --- a/metagpt/strategy/experience_retriever.py +++ b/metagpt/strategy/experience_retriever.py @@ -638,8 +638,10 @@ class KeywordExpRetriever(ExpRetriever): return DEPLOY_EXAMPLE elif "issue" in context.lower(): return FIX_ISSUE_EXAMPLE - elif "https:" or "http:" in context.lower(): - return WEB_SCRAPING_EXAMPLE + elif "https:" in context.lower() or "http:" in context.lower(): + if "search" in context.lower() or "click" in context.lower(): + return WEB_SCRAPING_EXAMPLE + return WEB_SCRAPING_EXAMPLE_SIMPLE elif exp_type == "task": if "diagnose" in context.lower(): return SEARCH_SYMBOL_EXAMPLE @@ -916,7 +918,7 @@ Explanation: The requirement is to scrape data from a website and extract inform "task_id": "1", "dependent_task_ids": [], "instruction": "Navigate to the yelp website.", - "assignee": "Browser" + "assignee": "David" } }, { @@ -925,7 +927,7 @@ Explanation: The requirement is to scrape data from a website and extract inform "task_id": "2", "dependent_task_ids": ["1"], "instruction": "Search for restaurants with the keyword 'beef'.", - "assignee": "Browser" + "assignee": "David" } }, { @@ -934,7 +936,7 @@ Explanation: The requirement is to scrape data from a website and extract inform "task_id": "3", "dependent_task_ids": ["2"], "instruction": "View the html content of the search result page before scrap data to understand the structure.", - "assignee": "DataAnalyst" + "assignee": "David" } }, { @@ -943,7 +945,7 @@ Explanation: The requirement is to scrape data from a website and extract inform "task_id": "4", "dependent_task_ids": ["3"], "instruction": "Parse the html content to scrape the restaurant names and print it.", - "assignee": "DataAnalyst" + "assignee": "David" } } ] @@ -1020,3 +1022,64 @@ Here is the command to finish the current task and parse the html content: ... """ + + +WEB_SCRAPING_EXAMPLE_SIMPLE = """ +## action 1 +User Requirement: List the restaurant names on the website https://www.yelp.com/search?find_desc=beef&find_loc=New+York%2C+NY. +Explanation: The requirement is to scrape data from a website and extract information about restaurants. The process involves retrieving and presenting the data in a structured format. + +```json +[ + { + "command_name": "Plan.append_task", + "args": { + "task_id": "1", + "dependent_task_ids": [], + "instruction": "View the html content of the page before scrap data to understand the structure.", + "assignee": "David" + } + }, + { + "command_name": "Plan.append_task", + "args": { + "task_id": "2", + "dependent_task_ids": ["3"], + "instruction": "Parse the html content to scrape the restaurant names and print it.", + "assignee": "David" + } + } +] +``` + +## action 2 +Explanation: To scrap data from the website, I will first view the html content of the page. +Here is the command to view the html content: + +```json +[ + { + "command_name": "DataAnalyst.write_and_exec_code", + "args": {} + } +] +``` + +## action 3 +Explanation: Since the DataAnalyst has successfully viewed the html content of the page, I will finish the current task and then write code to parse the html content and extract the restaurant names. +Here is the command to finish the current task and parse the html content: + +```json +[ + { + "command_name": "Plan.finish_current_task", + "args": {} + }, + { + "command_name": "DataAnalyst.write_and_exec_code", + "args": {} + } +] +``` +... +""" From 5f1bb59f1e177d18fba9137af8e8ffd947658477 Mon Sep 17 00:00:00 2001 From: lidanyang Date: Tue, 16 Jul 2024 19:02:00 +0800 Subject: [PATCH 102/119] keep len of web view content --- metagpt/actions/di/execute_nb_code.py | 2 ++ metagpt/tools/libs/browser.py | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/metagpt/actions/di/execute_nb_code.py b/metagpt/actions/di/execute_nb_code.py index f3dfd1601..b3bf4fcb8 100644 --- a/metagpt/actions/di/execute_nb_code.py +++ b/metagpt/actions/di/execute_nb_code.py @@ -193,6 +193,8 @@ class ExecuteNbCode(Action): # the useful information of normal output is at the begining. if '' not in output_text: output_text = output_text[:keep_len] if is_success else output_text[-keep_len:] + else: + output_text = output_text[:20000] if is_success else output_text[-keep_len:] parsed_output.append(output_text) return is_success, ",".join(parsed_output) diff --git a/metagpt/tools/libs/browser.py b/metagpt/tools/libs/browser.py index 864996e8c..8736ab98c 100644 --- a/metagpt/tools/libs/browser.py +++ b/metagpt/tools/libs/browser.py @@ -161,7 +161,7 @@ class Browser: await self._wait_until_page_idle(page) self.accessibility_tree = await get_accessibility_tree(page) await self.reporter.async_report(page, "page") - return f"SUCCESS, URL: {page.url}" + return f"SUCCESS, URL: {page.url} have been loaded." def _register_page_event(self, page: Page): page.last_busy_time = time.time() @@ -196,9 +196,9 @@ class Browser: async def _on_frame_change(self, frame: Frame): await self._update_page_last_busy_time(frame.page) - async def view(self): + async def view(self, keep_len: int = 10000): observation = parse_accessibility_tree(self.accessibility_tree) - return f"Current Browser Viewer\n URL: {self.page.url}\nOBSERVATION:\n{observation[0]}\n" + return f"Current Browser Viewer\n URL: {self.page.url}\nOBSERVATION:\n{observation[0][:keep_len]}\n" async def __aenter__(self): await self.start() From 55d89f740c3c83ab0b5292e9432cd1caf900a283 Mon Sep 17 00:00:00 2001 From: lidanyang Date: Tue, 16 Jul 2024 19:02:36 +0800 Subject: [PATCH 103/119] refine prompt --- metagpt/prompts/di/role_zero.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/metagpt/prompts/di/role_zero.py b/metagpt/prompts/di/role_zero.py index b642df9c7..2e32e9325 100644 --- a/metagpt/prompts/di/role_zero.py +++ b/metagpt/prompts/di/role_zero.py @@ -7,6 +7,8 @@ Note: 1. If you keeping encountering errors, unexpected situation, or you are not sure of proceeding, use RoleZero.ask_human to ask for help. 2. Carefully review your progress at the current task, if your actions so far has not fulfilled the task instruction, you should continue with current task. Otherwise, finish current task by Plan.finish_current_task explicitly. 3. Each time you finish a task, use RoleZero.reply_to_human to report your progress. +4. Don't forget to append task first when all existing tasks are finished and new tasks are required. +5. Don't repeat the same command if it has been executed successfully. """ CMD_PROMPT = """ From 91997aba5a5134acca9b9990824424c4cf51c39b Mon Sep 17 00:00:00 2001 From: lidanyang Date: Tue, 16 Jul 2024 19:03:57 +0800 Subject: [PATCH 104/119] default for task_type --- metagpt/schema.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/metagpt/schema.py b/metagpt/schema.py index 94e64d7fa..18fd7b013 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -669,7 +669,14 @@ class Plan(BaseModel): """ return [task for task in self.tasks if task.is_finished] - def append_task(self, task_id: str, dependent_task_ids: list[str], instruction: str, assignee: str, task_type: str): + def append_task( + self, + task_id: str, + dependent_task_ids: list[str], + instruction: str, + assignee: str, + task_type: str = "" + ): """Append a new task with task_id (number) to the end of existing task sequences. If dependent_task_ids is not empty, the task will depend on the tasks with the ids in the list.""" new_task = Task( task_id=task_id, From 5693594afe175de1ddc5702d2f29047e4004dd95 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Tue, 16 Jul 2024 19:13:10 +0800 Subject: [PATCH 105/119] add README.md --- examples/data/exp_pool/engineer_exps.json | 16 ++ examples/data/exp_pool/team_leader_exps.json | 22 +++ examples/exp_pool/README.md | 19 ++ examples/exp_pool/decorator.py | 6 +- examples/exp_pool/init_exp_pool.py | 94 +++++++++ .../exp_pool/init_exp_pool/engineer_exps.py | 114 ----------- examples/exp_pool/init_exp_pool/main.py | 55 ------ .../init_exp_pool/team_leader_exps.py | 181 ------------------ examples/exp_pool/manager.py | 14 +- examples/exp_pool/scorer.py | 12 ++ tests/metagpt/exp_pool/test_manager.py | 82 ++++---- .../test_serializers/test_role_zero.py | 4 +- .../exp_pool/test_serializers/test_simple.py | 20 +- 13 files changed, 238 insertions(+), 401 deletions(-) create mode 100644 examples/data/exp_pool/engineer_exps.json create mode 100644 examples/data/exp_pool/team_leader_exps.json create mode 100644 examples/exp_pool/README.md create mode 100644 examples/exp_pool/init_exp_pool.py delete mode 100644 examples/exp_pool/init_exp_pool/engineer_exps.py delete mode 100644 examples/exp_pool/init_exp_pool/main.py delete mode 100644 examples/exp_pool/init_exp_pool/team_leader_exps.py diff --git a/examples/data/exp_pool/engineer_exps.json b/examples/data/exp_pool/engineer_exps.json new file mode 100644 index 000000000..89c0bc186 --- /dev/null +++ b/examples/data/exp_pool/engineer_exps.json @@ -0,0 +1,16 @@ +[{ + "req": [{ + "role": "user", + "content": "\n# Current Plan\n{'goal': \"Please write a 1048 game using JavaScript and HTML code without using any frameworks, user can play with keyboard. Refer to the system design located at '/tmp/system_design.json' and the project schedule at '/tmp/project_schedule.json' for detailed information.\", 'tasks': []}\n\n# Current Task\n\n\n# Instruction\nBased on the context, write a plan or modify an existing plan to achieve the goal. A plan consists of one to 3 tasks.\nIf plan is created, you should track the progress and update the plan accordingly, such as Plan.finish_current_task, Plan.append_task, Plan.reset_task, Plan.replace_task, etc.\nWhen presented a current task, tackle the task using the available commands.\nPay close attention to new user message, review the conversation history, use RoleZero.reply_to_human to respond to new user requirement.\nNote:\n1. If you keeping encountering errors, unexpected situation, or you are not sure of proceeding, use RoleZero.ask_human to ask for help.\n2. Carefully review your progress at the current task, if your actions so far has not fulfilled the task instruction, you should continue with current task. Otherwise, finish current task.\n3. Each time you finish a task, use RoleZero.reply_to_human to report your progress.\n4. Each time you write a code in your response, write with the Editor directly without preparing a repetitive code block beforehand.\n5. Take on ONE task and write ONE code file in each response. DON'T attempt all tasks in one response.\n6. When not specified, you should write files in a folder named \"src\". If you know the project path, then write in a \"src\" folder under the project path.\n7. When provided system design or project schedule, you MUST read them first before making a plan, then adhere to them in your implementation, especially in the programming language, package, or framework. You MUST implement all code files prescribed in the system design or project schedule. You can create a plan first with each task corresponding to implementing one code file.\n8. Write at most one file per task, do your best to implement THE ONLY ONE FILE. CAREFULLY CHECK THAT YOU DONT MISS ANY NECESSARY CLASS/FUNCTION IN THIS FILE.\n9. COMPLETE CODE: Your code will be part of the entire project, so please implement complete, reliable, reusable code snippets.\n10. When provided system design, YOU MUST FOLLOW \"Data structures and interfaces\". DONT CHANGE ANY DESIGN. Do not use public member functions that do not exist in your design.\n11. Write out EVERY CODE DETAIL, DON'T LEAVE TODO.\n12. To modify code in a file, read the entire file, make changes, and update the file with the complete code, ensuring that no line numbers are included in the final write.\n13. When a system design or project schedule is provided, at the end of the plan, add a CodeRview Task for each file; for example, if there are three files, add three CodeRview Tasks. For each CodeRview Task, just call ReviewAndRewriteCode.run.\n" + }], + "resp": "\nExplanation: To begin the task of creating a 2048 game using JavaScript and HTML, I need to first read the system design and project schedule documents to understand the specific requirements and architecture outlined for the game development.\n\n```json\n[\n {\n \"command_name\": \"Editor.read\",\n \"args\": {\n \"path\": \"/tmp/system_design.json\"\n }\n },\n {\n \"command_name\": \"Editor.read\",\n \"args\": {\n \"path\": \"/tmp/project_schedule.json\"\n }\n }\n]\n```\n" +}, { + "req": [{ + "role": "user", + "content": "Command Editor.read executed: file_path='/tmp/system_design.json' block_content='001|{\"Implementation approach\":\"We will implement the 2048 game using plain JavaScript and HTML, ensuring no frameworks are used. The game logic will handle tile movements, merging, and game state updates. The UI will be simple and clean, with a responsive design to fit different screen sizes. We will use CSS for styling and ensure the game is playable with keyboard arrow keys. The game will display the current score, have a restart button, and show a game over message when no more moves are possible.\",\"File list\":[\"index.html\",\"style.css\",\"script.js\"],\"Data structures and interfaces\":\"\\\\nclassDiagram\\\\n class Game {\\\\n -grid: int[][]\\\\n -score: int\\\\n +init(): void\\\\n +move(direction: str): void\\\\n +merge(direction: str): void\\\\n +isGameOver(): bool\\\\n +restart(): void\\\\n }\\\\n class UI {\\\\n -game: Game\\\\n +init(): void\\\\n +update(): void\\\\n +showGameOver(): void\\\\n +bindEvents(): void\\\\n }\\\\n Game --> UI\\\\n\",\"Program call flow\":\"\\\\nsequenceDiagram\\\\n participant U as UI\\\\n participant G as Game\\\\n U->>G: init()\\\\n G-->>U: return\\\\n U->>U: bindEvents()\\\\n U->>G: move(direction)\\\\n G->>G: merge(direction)\\\\n G->>U: update()\\\\n U->>U: update()\\\\n U->>G: isGameOver()\\\\n G-->>U: return bool\\\\n alt Game Over\\\\n U->>U: showGameOver()\\\\n end\\\\n U->>G: restart()\\\\n G-->>U: return\\\\n\",\"Anything UNCLEAR\":\"Clarify if there are any specific design preferences or additional features required beyond the basic 2048 game functionality.\"}\\n'\n\nCommand Editor.read executed: file_path='/tmp/project_schedule.json' block_content='001|{\"Required packages\":[\"No third-party dependencies required\"],\"Required Other language third-party packages\":[\"No third-party dependencies required\"],\"Logic Analysis\":[[\"script.js\",\"Contains Game and UI classes, and their methods: init, move, merge, isGameOver, restart, update, showGameOver, bindEvents\"],[\"index.html\",\"Contains the HTML structure for the game UI\"],[\"style.css\",\"Contains the CSS styles for the game UI\"]],\"Task list\":[\"index.html\",\"style.css\",\"script.js\"],\"Full API spec\":\"\",\"Shared Knowledge\":\"The `script.js` file will contain the core game logic and UI handling. The `index.html` file will provide the structure for the game, and `style.css` will handle the styling.\",\"Anything UNCLEAR\":\"Clarify if there are any specific design preferences or additional features required beyond the basic 2048 game functionality.\"}\\n'" + }, { + "role": "user", + "content": "\n# Current Plan\n{'goal': \"Please write a 1048 game using JavaScript and HTML code without using any frameworks, user can play with keyboard. Refer to the system design located at '/tmp/system_design.json' and the project schedule at '/tmp/project_schedule.json' for detailed information.\", 'tasks': []}\n\n# Current Task\n\n\n# Instruction\nBased on the context, write a plan or modify an existing plan to achieve the goal. A plan consists of one to 3 tasks.\nIf plan is created, you should track the progress and update the plan accordingly, such as Plan.finish_current_task, Plan.append_task, Plan.reset_task, Plan.replace_task, etc.\nWhen presented a current task, tackle the task using the available commands.\nPay close attention to new user message, review the conversation history, use RoleZero.reply_to_human to respond to new user requirement.\nNote:\n1. If you keeping encountering errors, unexpected situation, or you are not sure of proceeding, use RoleZero.ask_human to ask for help.\n2. Carefully review your progress at the current task, if your actions so far has not fulfilled the task instruction, you should continue with current task. Otherwise, finish current task.\n3. Each time you finish a task, use RoleZero.reply_to_human to report your progress.\n4. Each time you write a code in your response, write with the Editor directly without preparing a repetitive code block beforehand.\n5. Take on ONE task and write ONE code file in each response. DON'T attempt all tasks in one response.\n6. When not specified, you should write files in a folder named \"src\". If you know the project path, then write in a \"src\" folder under the project path.\n7. When provided system design or project schedule, you MUST read them first before making a plan, then adhere to them in your implementation, especially in the programming language, package, or framework. You MUST implement all code files prescribed in the system design or project schedule. You can create a plan first with each task corresponding to implementing one code file.\n8. Write at most one file per task, do your best to implement THE ONLY ONE FILE. CAREFULLY CHECK THAT YOU DONT MISS ANY NECESSARY CLASS/FUNCTION IN THIS FILE.\n9. COMPLETE CODE: Your code will be part of the entire project, so please implement complete, reliable, reusable code snippets.\n10. When provided system design, YOU MUST FOLLOW \"Data structures and interfaces\". DONT CHANGE ANY DESIGN. Do not use public member functions that do not exist in your design.\n11. Write out EVERY CODE DETAIL, DON'T LEAVE TODO.\n12. To modify code in a file, read the entire file, make changes, and update the file with the complete code, ensuring that no line numbers are included in the final write.\n13. When a system design or project schedule is provided, at the end of the plan, add a CodeRview Task for each file; for example, if there are three files, add three CodeRview Tasks. For each CodeRview Task, just call ReviewAndRewriteCode.run.\n" + }], + "resp": "\nExplanation: Based on the system design and project schedule, I will create a plan to implement the 2048 game. The plan will consist of tasks to create the necessary files (`index.html`, `style.css`, and `script.js`) and review each file to ensure they meet the design specifications.\n\nHere's the plan:\n\n1. **Task 1**: Create `index.html` - This file will contain the HTML structure necessary for the game's UI.\n2. **Task 2**: Create `style.css` - This file will define the CSS styles to make the game visually appealing and responsive.\n3. **Task 3**: Create `script.js` - This file will contain the JavaScript code for the game logic and UI interactions.\n4. **Code Review Tasks**: Review each file to ensure they meet the project requirements and adhere to the system design.\n\nLet's start by appending the first task to the plan.\n\n```json\n[\n {\n \"command_name\": \"Plan.append_task\",\n \"args\": {\n \"task_id\": \"1\",\n \"dependent_task_ids\": [],\n \"instruction\": \"Create the index.html file with the basic HTML structure for the 2048 game.\",\n \"assignee\": \"Alex\"\n }\n },\n {\n \"command_name\": \"Plan.append_task\",\n \"args\": {\n \"task_id\": \"2\",\n \"dependent_task_ids\": [\"1\"],\n \"instruction\": \"Create the style.css file with the necessary CSS to style the 2048 game.\",\n \"assignee\": \"Alex\"\n }\n },\n {\n \"command_name\": \"Plan.append_task\",\n \"args\": {\n \"task_id\": \"3\",\n \"dependent_task_ids\": [\"1\", \"2\"],\n \"instruction\": \"Create the script.js file containing the JavaScript logic for the 2048 game.\",\n \"assignee\": \"Alex\"\n }\n },\n {\n \"command_name\": \"Plan.append_task\",\n \"args\": {\n \"task_id\": \"4\",\n \"dependent_task_ids\": [\"1\"],\n \"instruction\": \"Use ReviewAndRewriteCode to review the code in index.html to ensure it meets the design specifications.\",\n \"assignee\": \"Alex\"\n }\n },\n {\n \"command_name\": \"Plan.append_task\",\n \"args\": {\n \"task_id\": \"5\",\n \"dependent_task_ids\": [\"2\"],\n \"instruction\": \"Use ReviewAndRewriteCode to review the code in style.css to ensure it meets the design specifications.\",\n \"assignee\": \"Alex\"\n }\n },\n {\n \"command_name\": \"Plan.append_task\",\n \"args\": {\n \"task_id\": \"6\",\n \"dependent_task_ids\": [\"3\"],\n \"instruction\": \"Use ReviewAndRewriteCode to review the code in script.js to ensure it meets the design specifications.\",\n \"assignee\": \"Alex\"\n }\n }\n]\n```\n" +}] \ No newline at end of file diff --git a/examples/data/exp_pool/team_leader_exps.json b/examples/data/exp_pool/team_leader_exps.json new file mode 100644 index 000000000..125f0a48d --- /dev/null +++ b/examples/data/exp_pool/team_leader_exps.json @@ -0,0 +1,22 @@ +[{ + "req": [{ + "role": "user", + "content": "\n# Current Plan\n{'goal': \"from to {''}: Write a 1024 game using JavaScript and HTML code without using any frameworks, user can play with keyboard.\", 'tasks': []}\n\n# Current Task\n\n\n# Instruction\nYou are a team leader, and you are responsible for drafting tasks and routing tasks to your team members.\nYour team member:\nTim: Team Leader, \nAlice: Product Manager, efficiently create a successful product that meets market demands and user expectations\nBob: Architect, design a concise, usable, complete software system\nEve: Project Manager, break down tasks according to PRD/technical design, generate a task list, and analyze task dependencies to start with the prerequisite modules\nAlex: Engineer, Take on game, app, and web development\nDavid: DataAnalyst, Take on any data-related tasks, such as data analysis, machine learning, deep learning, web browsing, web scraping, web searching, web deployment, terminal operation, git and github operation, etc.\n\nYou should NOT assign consecutive tasks to the same team member, instead, assign an aggregated task (or the complete requirement) and let the team member to decompose it.\nWhen creating a new plan involving multiple members, create all tasks at once.\nIf plan is created, you should track the progress based on team member feedback message, and update plan accordingly, such as Plan.finish_current_task, Plan.reset_task, Plan.replace_task, etc.\nYou should use TeamLeader.publish_team_message to team members, asking them to start their task. DONT omit any necessary info such as path, link, environment, programming language, framework, requirement, constraint from original content to team members because you are their sole info source.\nPay close attention to new user message, review the conversation history, use RoleZero.reply_to_human to respond to the user directly, DON'T ask your team members.\n\nNote:\n1. If the requirement is a pure DATA-RELATED requirement, such as bug fixes, issue reporting, environment setup, terminal operations, pip install, web browsing, web scraping, web searching, web imitation, data science, data analysis, machine learning, deep learning, text-to-image etc. DON'T decompose it, assign a single task with the original user requirement as instruction directly to Data Analyst.\n2. If the requirement is developing a software, game, app, or website, excluding the above data-related tasks, you should decompose the requirement into multiple tasks and assign them to different team members based on their expertise, usually the sequence of Product Manager -> Architect -> Project Manager -> Engineer -> (optional: QaEngine if present) -> (optional: DataAnalyst if user requests deployment), each assigned ONE task. When publishing message to Product Manager, you should directly copy the full original user requirement.\n3. If the requirement contains both DATA-RELATED part mentioned in 1 and software development part mentioned in 2, you should decompose the software development part and assign them to different team members based on their expertise, and assign the DATA-RELATED part to Data Analyst David directly.\n4. If the requirement is a common-sense, logical, or math problem, you should respond directly without assigning any task to team members.\n5. If you think the requirement is not clear or ambiguous, you should ask the user for clarification immediately. Assign tasks only after all info is clear.\n6. It is helpful for Engineer to have both the system design and the project schedule for writing the code, so include paths of both files (if available) and remind Engineer to definitely read them when publishing message to Engineer.\n7. If the requirement is writing a TRD and software framework, you should assign it to Architect. When publishing message to Architect, you should directly copy the full original user requirement.\n" + }], + "resp": "\nExplanation: The requirement is about software development. Assign each tasks to a different team member based on their expertise. When publishing message to Product Manager, we copy original user requirement directly to ensure no information loss.\n```json\n[\n {\n \"command_name\": \"Plan.append_task\",\n \"args\": {\n \"task_id\": \"1\",\n \"dependent_task_ids\": [],\n \"instruction\": \"Create a product requirement document (PRD) outlining the features, user interface, and user experience of the CLI python snake game.\",\n \"assignee\": \"Alice\"\n }\n },\n {\n \"command_name\": \"Plan.append_task\",\n \"args\": {\n \"task_id\": \"2\",\n \"dependent_task_ids\": [\"1\"],\n \"instruction\": \"Design the software architecture for the CLI snake game, including the choice of programming language, libraries, and data flow.\",\n \"assignee\": \"Bob\"\n }\n },\n {\n \"command_name\": \"Plan.append_task\",\n \"args\": {\n \"task_id\": \"3\",\n \"dependent_task_ids\": [\"2\"],\n \"instruction\": \"Break down the architecture into manageable tasks, identify task dependencies, and prepare a detailed task list for implementation.\",\n \"assignee\": \"Eve\"\n }\n },\n {\n \"command_name\": \"Plan.append_task\",\n \"args\": {\n \"task_id\": \"4\",\n \"dependent_task_ids\": [\"3\"],\n \"instruction\": \"Implement the core game logic for the CLI snake game, including snake movement, food generation, and score tracking.\",\n \"assignee\": \"Alex\"\n }\n },\n {\n \"command_name\": \"Plan.append_task\",\n \"args\": {\n \"task_id\": \"5\",\n \"dependent_task_ids\": [\"4\"],\n \"instruction\": \"Write comprehensive tests for the game logic and user interface to ensure functionality and reliability.\",\n \"assignee\": \"Edward\"\n }\n },\n {\n \"command_name\": \"TeamLeader.publish_message\",\n \"args\": {\n \"content\": \"Create a cli snake game using Python\",\n \"send_to\": \"Alice\"\n }\n },\n {\n \"command_name\": \"RoleZero.reply_to_human\",\n \"args\": {\n \"content\": \"I have assigned the tasks to the team members. Alice will create the PRD, Bob will design the software architecture, Eve will break down the architecture into tasks, Alex will implement the core game logic, and Edward will write comprehensive tests. The team will work on the project accordingly\"\n }\n },\n {\n \"command_name\": \"end\"\n }\n]\n```\n" +}, { + "req": [{ + "role": "user", + "content": "\n# Current Plan\n{'goal': \"from to {''}: Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.\", 'tasks': []}\n\n# Current Task\n\n\n# Instruction\nYou are a team leader, and you are responsible for drafting tasks and routing tasks to your team members.\nYour team member:\nTim: Team Leader, \nAlice: Product Manager, efficiently create a successful product that meets market demands and user expectations\nBob: Architect, design a concise, usable, complete software system\nEve: Project Manager, break down tasks according to PRD/technical design, generate a task list, and analyze task dependencies to start with the prerequisite modules\nAlex: Engineer, Take on game, app, and web development\nDavid: DataAnalyst, Take on any data-related tasks, such as data analysis, machine learning, deep learning, web browsing, web scraping, web searching, web deployment, terminal operation, git and github operation, etc.\n\nYou should NOT assign consecutive tasks to the same team member, instead, assign an aggregated task (or the complete requirement) and let the team member to decompose it.\nWhen creating a new plan involving multiple members, create all tasks at once.\nIf plan is created, you should track the progress based on team member feedback message, and update plan accordingly, such as Plan.finish_current_task, Plan.reset_task, Plan.replace_task, etc.\nYou should use TeamLeader.publish_team_message to team members, asking them to start their task. DONT omit any necessary info such as path, link, environment, programming language, framework, requirement, constraint from original content to team members because you are their sole info source.\nPay close attention to new user message, review the conversation history, use RoleZero.reply_to_human to respond to the user directly, DON'T ask your team members.\n\nNote:\n1. If the requirement is a pure DATA-RELATED requirement, such as bug fixes, issue reporting, environment setup, terminal operations, pip install, web browsing, web scraping, web searching, web imitation, data science, data analysis, machine learning, deep learning, text-to-image etc. DON'T decompose it, assign a single task with the original user requirement as instruction directly to Data Analyst.\n2. If the requirement is developing a software, game, app, or website, excluding the above data-related tasks, you should decompose the requirement into multiple tasks and assign them to different team members based on their expertise, usually the sequence of Product Manager -> Architect -> Project Manager -> Engineer -> (optional: QaEngine if present) -> (optional: DataAnalyst if user requests deployment), each assigned ONE task. When publishing message to Product Manager, you should directly copy the full original user requirement.\n3. If the requirement contains both DATA-RELATED part mentioned in 1 and software development part mentioned in 2, you should decompose the software development part and assign them to different team members based on their expertise, and assign the DATA-RELATED part to Data Analyst David directly.\n4. If the requirement is a common-sense, logical, or math problem, you should respond directly without assigning any task to team members.\n5. If you think the requirement is not clear or ambiguous, you should ask the user for clarification immediately. Assign tasks only after all info is clear.\n6. It is helpful for Engineer to have both the system design and the project schedule for writing the code, so include paths of both files (if available) and remind Engineer to definitely read them when publishing message to Engineer.\n7. If the requirement is writing a TRD and software framework, you should assign it to Architect. When publishing message to Architect, you should directly copy the full original user requirement.\n" + }], + "resp": "\nExplanation: DON'T decompose requirement if it is a DATA-RELATED task, assign a single task directly to Data Analyst David. He will manage the decomposition and implementation.\n```json\n[\n {\n \"command_name\": \"Plan.append_task\",\n \"args\": {\n \"task_id\": \"1\",\n \"dependent_task_ids\": [],\n \"instruction\": \"Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.\",\n \"assignee\": \"David\"\n }\n },\n {\n \"command_name\": \"TeamLeader.publish_message\",\n \"args\": {\n \"content\": \"Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.\",\n \"send_to\": \"David\"\n }\n },\n {\n \"command_name\": \"RoleZero.reply_to_human\",\n \"args\": {\n \"content\": \"I have assigned the task to David. He will break down the task further by himself and starts solving it.\"\n }\n },\n {\n \"command_name\": \"end\"\n }\n]\n```\n" +}, { + "req": "Conversation History:\n[\n ...,\n {'role': 'assistant', 'content': 'from Alice(Product Manager) to {'Bob'}: {'docs': {'20240424153821.json': {'root_path': 'docs/prd', 'filename': '20240424153821.json', 'content': '{\"Language\":\"en_us\",\"Programming Language\":\"Python\",\"Original Requirements\":\"create a cli snake game\",\"Project Name\":\"snake_game\",\"Product Goals\":[\"Develop an intuitive and addictive snake game\",...], ...}}}}},\n]\n", + "resp": "\nExplanation: You received a message from Alice, the Product Manager, that she has completed the PRD, use Plan.finish_current_task to mark her task as finished and moves the plan to the next task. Based on plan status, next task is for Bob (Architect), publish a message asking him to start. The message content should contain important path info.\n```json\n[\n {\n \"command_name\": \"Plan.finish_current_task\",\n \"args\": {}\n },\n {\n \"command_name\": \"TeamLeader.publish_message\",\n \"args\": {\n \"content\": \"Please design the software architecture for the snake game based on the PRD created by Alice. The PRD is at 'docs/prd/20240424153821.json'. Include the choice of programming language, libraries, and data flow, etc.\",\n \"send_to\": \"Bob\"\n }\n },\n {\n \"command_name\": \"RoleZero.reply_to_human\",\n \"args\": {\n \"content\": \"Alice has completed the PRD. I have marked her task as finished and sent the PRD to Bob. Bob will work on the software architecture.\"\n }\n },\n {\n \"command_name\": \"end\"\n }\n]\n```\n" +}, { + "req": [{ + "role": "user", + "content": "\n# Current Plan\n{'goal': \"from to {''}: how does the project go?\", 'tasks': []}\n\n# Current Task\n\n\n# Instruction\nYou are a team leader, and you are responsible for drafting tasks and routing tasks to your team members.\nYour team member:\nTim: Team Leader, \nAlice: Product Manager, efficiently create a successful product that meets market demands and user expectations\nBob: Architect, design a concise, usable, complete software system\nEve: Project Manager, break down tasks according to PRD/technical design, generate a task list, and analyze task dependencies to start with the prerequisite modules\nAlex: Engineer, Take on game, app, and web development\nDavid: DataAnalyst, Take on any data-related tasks, such as data analysis, machine learning, deep learning, web browsing, web scraping, web searching, web deployment, terminal operation, git and github operation, etc.\n\nYou should NOT assign consecutive tasks to the same team member, instead, assign an aggregated task (or the complete requirement) and let the team member to decompose it.\nWhen creating a new plan involving multiple members, create all tasks at once.\nIf plan is created, you should track the progress based on team member feedback message, and update plan accordingly, such as Plan.finish_current_task, Plan.reset_task, Plan.replace_task, etc.\nYou should use TeamLeader.publish_team_message to team members, asking them to start their task. DONT omit any necessary info such as path, link, environment, programming language, framework, requirement, constraint from original content to team members because you are their sole info source.\nPay close attention to new user message, review the conversation history, use RoleZero.reply_to_human to respond to the user directly, DON'T ask your team members.\n\nNote:\n1. If the requirement is a pure DATA-RELATED requirement, such as bug fixes, issue reporting, environment setup, terminal operations, pip install, web browsing, web scraping, web searching, web imitation, data science, data analysis, machine learning, deep learning, text-to-image etc. DON'T decompose it, assign a single task with the original user requirement as instruction directly to Data Analyst.\n2. If the requirement is developing a software, game, app, or website, excluding the above data-related tasks, you should decompose the requirement into multiple tasks and assign them to different team members based on their expertise, usually the sequence of Product Manager -> Architect -> Project Manager -> Engineer -> (optional: QaEngine if present) -> (optional: DataAnalyst if user requests deployment), each assigned ONE task. When publishing message to Product Manager, you should directly copy the full original user requirement.\n3. If the requirement contains both DATA-RELATED part mentioned in 1 and software development part mentioned in 2, you should decompose the software development part and assign them to different team members based on their expertise, and assign the DATA-RELATED part to Data Analyst David directly.\n4. If the requirement is a common-sense, logical, or math problem, you should respond directly without assigning any task to team members.\n5. If you think the requirement is not clear or ambiguous, you should ask the user for clarification immediately. Assign tasks only after all info is clear.\n6. It is helpful for Engineer to have both the system design and the project schedule for writing the code, so include paths of both files (if available) and remind Engineer to definitely read them when publishing message to Engineer.\n7. If the requirement is writing a TRD and software framework, you should assign it to Architect. When publishing message to Architect, you should directly copy the full original user requirement.\n" + }], + "resp": "\nExplanation: The user is asking for a general update on the project status. Give a straight answer about the current task the team is working on and provide a summary of the completed tasks.\n```json\n[\n {\n \"command_name\": \"RoleZero.reply_to_human\",\n \"args\": {\n \"content\": \"The team is currently working on ... We have completed ...\"\n }\n },\n {\n \"command_name\": \"end\"\n }\n]\n```\n" +}] \ No newline at end of file diff --git a/examples/exp_pool/README.md b/examples/exp_pool/README.md new file mode 100644 index 000000000..d405bfa22 --- /dev/null +++ b/examples/exp_pool/README.md @@ -0,0 +1,19 @@ +# Experience Pool + +## Prerequisites +- Ensure the RAG module is installed: https://docs.deepwisdom.ai/main/en/guide/in_depth_guides/rag_module.html +- Set both `enable_read` and `enable_write` to `true` in the `exp_pool` section of `config2.yaml` + +## Example Files + +### 1. decorator.py +Showcases the implementation of the `@exp_cache` decorator. + +### 2. init_exp_pool.py +Demonstrates the process of initializing the experience pool. + +### 3. manager.py +Illustrates CRUD (Create, Read, Update, Delete) operations for managing experiences in the pool. + +### 4. scorer.py +Outlines methods for evaluating and scoring experiences within the pool. diff --git a/examples/exp_pool/decorator.py b/examples/exp_pool/decorator.py index 00726a0a8..d25949e8d 100644 --- a/examples/exp_pool/decorator.py +++ b/examples/exp_pool/decorator.py @@ -1,4 +1,6 @@ -"""Decorator example of experience pool.""" +""" +This script demonstrates how to automatically store experiences using @exp_cache and query the stored experiences. +""" import asyncio import uuid @@ -16,7 +18,7 @@ async def main(): req = "Water" resp = await produce(req=req) - logger.info(f"The resp of `produce{req}` is: {resp}") + logger.info(f"The response of `produce({req})` is: {resp}") exps = await exp_manager.query_exps(req) logger.info(f"Find experiences: {exps}") diff --git a/examples/exp_pool/init_exp_pool.py b/examples/exp_pool/init_exp_pool.py new file mode 100644 index 000000000..14c415be7 --- /dev/null +++ b/examples/exp_pool/init_exp_pool.py @@ -0,0 +1,94 @@ +"""Init experience pool. + +Put some useful experiences into the experience pool. +""" + +import asyncio +import json +from pathlib import Path + +from metagpt.const import EXAMPLE_DATA_PATH +from metagpt.exp_pool import exp_manager +from metagpt.exp_pool.schema import EntryType, Experience, Metric, Score +from metagpt.logs import logger +from metagpt.utils.common import aread + + +async def load_file(filepath) -> list[dict]: + """Asynchronously loads and parses a JSON file. + + Args: + filepath: Path to the JSON file. + + Returns: + A list of dictionaries parsed from the JSON file. + """ + + return json.loads(await aread(filepath)) + + +async def add_exp(req: str, resp: str, tag: str, metric: Metric = None): + """Adds a new experience to the experience pool. + + Args: + req: The request string. + resp: The response string. + tag: A tag for categorizing the experience. + metric: Optional metric for the experience. Defaults to a score of 10. + + """ + + exp = Experience( + req=req, + resp=resp, + entry_type=EntryType.MANUAL, + tag=tag, + metric=metric or Metric(score=Score(val=10, reason="Manual")), + ) + + exp_manager.config.exp_pool.enable_write = True + exp_manager.create_exp(exp) + logger.info(f"New experience created for the request `{req[:10]}`.") + + +async def add_exps(exps: list, tag: str): + """Adds multiple experiences to the experience pool. + + Args: + exps: A list of experience dictionaries. + tag: A tag for categorizing the experiences. + + """ + + tasks = [add_exp(req=json.dumps(exp["req"]), resp=exp["resp"], tag=tag) for exp in exps] + await asyncio.gather(*tasks) + + +async def add_exps_from_file(tag: str, filepath: Path): + """Loads experiences from a file and adds them to the experience pool. + + Args: + tag: A tag for categorizing the experiences. + filepath: Path to the file containing experiences. + + """ + + exps = await load_file(filepath) + await add_exps(exps, tag) + + +def query_exps_count(): + """Queries and logs the total count of experiences in the pool.""" + + count = exp_manager.get_exps_count() + logger.info(f"Experiences Count: {count}") + + +async def main(): + await add_exps_from_file("TeamLeader.llm_cached_aask", EXAMPLE_DATA_PATH / "exp_pool/team_leader_exps.json"), + await add_exps_from_file("Engineer2.llm_cached_aask", EXAMPLE_DATA_PATH / "exp_pool/engineer_exps.json") + query_exps_count() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/exp_pool/init_exp_pool/engineer_exps.py b/examples/exp_pool/init_exp_pool/engineer_exps.py deleted file mode 100644 index 022a0c829..000000000 --- a/examples/exp_pool/init_exp_pool/engineer_exps.py +++ /dev/null @@ -1,114 +0,0 @@ -import json - -OBJ1 = [ - { - "role": "user", - "content": "\n# Current Plan\n{'goal': \"Please write a 1048 game using JavaScript and HTML code without using any frameworks, user can play with keyboard. Refer to the system design located at '/tmp/system_design.json' and the project schedule at '/tmp/project_schedule.json' for detailed information.\", 'tasks': []}\n\n# Current Task\n\n\n# Instruction\nBased on the context, write a plan or modify an existing plan to achieve the goal. A plan consists of one to 3 tasks.\nIf plan is created, you should track the progress and update the plan accordingly, such as Plan.finish_current_task, Plan.append_task, Plan.reset_task, Plan.replace_task, etc.\nWhen presented a current task, tackle the task using the available commands.\nPay close attention to new user message, review the conversation history, use RoleZero.reply_to_human to respond to new user requirement.\nNote:\n1. If you keeping encountering errors, unexpected situation, or you are not sure of proceeding, use RoleZero.ask_human to ask for help.\n2. Carefully review your progress at the current task, if your actions so far has not fulfilled the task instruction, you should continue with current task. Otherwise, finish current task.\n3. Each time you finish a task, use RoleZero.reply_to_human to report your progress.\n4. Each time you write a code in your response, write with the Editor directly without preparing a repetitive code block beforehand.\n5. Take on ONE task and write ONE code file in each response. DON'T attempt all tasks in one response.\n6. When not specified, you should write files in a folder named \"src\". If you know the project path, then write in a \"src\" folder under the project path.\n7. When provided system design or project schedule, you MUST read them first before making a plan, then adhere to them in your implementation, especially in the programming language, package, or framework. You MUST implement all code files prescribed in the system design or project schedule. You can create a plan first with each task corresponding to implementing one code file.\n8. Write at most one file per task, do your best to implement THE ONLY ONE FILE. CAREFULLY CHECK THAT YOU DONT MISS ANY NECESSARY CLASS/FUNCTION IN THIS FILE.\n9. COMPLETE CODE: Your code will be part of the entire project, so please implement complete, reliable, reusable code snippets.\n10. When provided system design, YOU MUST FOLLOW \"Data structures and interfaces\". DONT CHANGE ANY DESIGN. Do not use public member functions that do not exist in your design.\n11. Write out EVERY CODE DETAIL, DON'T LEAVE TODO.\n12. To modify code in a file, read the entire file, make changes, and update the file with the complete code, ensuring that no line numbers are included in the final write.\n13. When a system design or project schedule is provided, at the end of the plan, add a CodeRview Task for each file; for example, if there are three files, add three CodeRview Tasks. For each CodeRview Task, just call ReviewAndRewriteCode.run.\n", - } -] -OBJ2 = [ - { - "role": "user", - "content": 'Command Editor.read executed: file_path=\'/tmp/system_design.json\' block_content=\'001|{"Implementation approach":"We will implement the 2048 game using plain JavaScript and HTML, ensuring no frameworks are used. The game logic will handle tile movements, merging, and game state updates. The UI will be simple and clean, with a responsive design to fit different screen sizes. We will use CSS for styling and ensure the game is playable with keyboard arrow keys. The game will display the current score, have a restart button, and show a game over message when no more moves are possible.","File list":["index.html","style.css","script.js"],"Data structures and interfaces":"\\\\nclassDiagram\\\\n class Game {\\\\n -grid: int[][]\\\\n -score: int\\\\n +init(): void\\\\n +move(direction: str): void\\\\n +merge(direction: str): void\\\\n +isGameOver(): bool\\\\n +restart(): void\\\\n }\\\\n class UI {\\\\n -game: Game\\\\n +init(): void\\\\n +update(): void\\\\n +showGameOver(): void\\\\n +bindEvents(): void\\\\n }\\\\n Game --> UI\\\\n","Program call flow":"\\\\nsequenceDiagram\\\\n participant U as UI\\\\n participant G as Game\\\\n U->>G: init()\\\\n G-->>U: return\\\\n U->>U: bindEvents()\\\\n U->>G: move(direction)\\\\n G->>G: merge(direction)\\\\n G->>U: update()\\\\n U->>U: update()\\\\n U->>G: isGameOver()\\\\n G-->>U: return bool\\\\n alt Game Over\\\\n U->>U: showGameOver()\\\\n end\\\\n U->>G: restart()\\\\n G-->>U: return\\\\n","Anything UNCLEAR":"Clarify if there are any specific design preferences or additional features required beyond the basic 2048 game functionality."}\\n\'\n\nCommand Editor.read executed: file_path=\'/tmp/project_schedule.json\' block_content=\'001|{"Required packages":["No third-party dependencies required"],"Required Other language third-party packages":["No third-party dependencies required"],"Logic Analysis":[["script.js","Contains Game and UI classes, and their methods: init, move, merge, isGameOver, restart, update, showGameOver, bindEvents"],["index.html","Contains the HTML structure for the game UI"],["style.css","Contains the CSS styles for the game UI"]],"Task list":["index.html","style.css","script.js"],"Full API spec":"","Shared Knowledge":"The `script.js` file will contain the core game logic and UI handling. The `index.html` file will provide the structure for the game, and `style.css` will handle the styling.","Anything UNCLEAR":"Clarify if there are any specific design preferences or additional features required beyond the basic 2048 game functionality."}\\n\'', - }, - { - "role": "user", - "content": "\n# Current Plan\n{'goal': \"Please write a 1048 game using JavaScript and HTML code without using any frameworks, user can play with keyboard. Refer to the system design located at '/tmp/system_design.json' and the project schedule at '/tmp/project_schedule.json' for detailed information.\", 'tasks': []}\n\n# Current Task\n\n\n# Instruction\nBased on the context, write a plan or modify an existing plan to achieve the goal. A plan consists of one to 3 tasks.\nIf plan is created, you should track the progress and update the plan accordingly, such as Plan.finish_current_task, Plan.append_task, Plan.reset_task, Plan.replace_task, etc.\nWhen presented a current task, tackle the task using the available commands.\nPay close attention to new user message, review the conversation history, use RoleZero.reply_to_human to respond to new user requirement.\nNote:\n1. If you keeping encountering errors, unexpected situation, or you are not sure of proceeding, use RoleZero.ask_human to ask for help.\n2. Carefully review your progress at the current task, if your actions so far has not fulfilled the task instruction, you should continue with current task. Otherwise, finish current task.\n3. Each time you finish a task, use RoleZero.reply_to_human to report your progress.\n4. Each time you write a code in your response, write with the Editor directly without preparing a repetitive code block beforehand.\n5. Take on ONE task and write ONE code file in each response. DON'T attempt all tasks in one response.\n6. When not specified, you should write files in a folder named \"src\". If you know the project path, then write in a \"src\" folder under the project path.\n7. When provided system design or project schedule, you MUST read them first before making a plan, then adhere to them in your implementation, especially in the programming language, package, or framework. You MUST implement all code files prescribed in the system design or project schedule. You can create a plan first with each task corresponding to implementing one code file.\n8. Write at most one file per task, do your best to implement THE ONLY ONE FILE. CAREFULLY CHECK THAT YOU DONT MISS ANY NECESSARY CLASS/FUNCTION IN THIS FILE.\n9. COMPLETE CODE: Your code will be part of the entire project, so please implement complete, reliable, reusable code snippets.\n10. When provided system design, YOU MUST FOLLOW \"Data structures and interfaces\". DONT CHANGE ANY DESIGN. Do not use public member functions that do not exist in your design.\n11. Write out EVERY CODE DETAIL, DON'T LEAVE TODO.\n12. To modify code in a file, read the entire file, make changes, and update the file with the complete code, ensuring that no line numbers are included in the final write.\n13. When a system design or project schedule is provided, at the end of the plan, add a CodeRview Task for each file; for example, if there are three files, add three CodeRview Tasks. For each CodeRview Task, just call ReviewAndRewriteCode.run.\n", - }, -] -REQ1 = json.dumps(OBJ1) -REQ2 = json.dumps(OBJ2) - -RESP1 = """ -Explanation: To begin the task of creating a 2048 game using JavaScript and HTML, I need to first read the system design and project schedule documents to understand the specific requirements and architecture outlined for the game development. - -```json -[ - { - "command_name": "Editor.read", - "args": { - "path": "/tmp/system_design.json" - } - }, - { - "command_name": "Editor.read", - "args": { - "path": "/tmp/project_schedule.json" - } - } -] -``` -""" -RESP2 = """ -Explanation: Based on the system design and project schedule, I will create a plan to implement the 2048 game. The plan will consist of tasks to create the necessary files (`index.html`, `style.css`, and `script.js`) and review each file to ensure they meet the design specifications. - -Here's the plan: - -1. **Task 1**: Create `index.html` - This file will contain the HTML structure necessary for the game's UI. -2. **Task 2**: Create `style.css` - This file will define the CSS styles to make the game visually appealing and responsive. -3. **Task 3**: Create `script.js` - This file will contain the JavaScript code for the game logic and UI interactions. -4. **Code Review Tasks**: Review each file to ensure they meet the project requirements and adhere to the system design. - -Let's start by appending the first task to the plan. - -```json -[ - { - "command_name": "Plan.append_task", - "args": { - "task_id": "1", - "dependent_task_ids": [], - "instruction": "Create the index.html file with the basic HTML structure for the 2048 game.", - "assignee": "Alex" - } - }, - { - "command_name": "Plan.append_task", - "args": { - "task_id": "2", - "dependent_task_ids": ["1"], - "instruction": "Create the style.css file with the necessary CSS to style the 2048 game.", - "assignee": "Alex" - } - }, - { - "command_name": "Plan.append_task", - "args": { - "task_id": "3", - "dependent_task_ids": ["1", "2"], - "instruction": "Create the script.js file containing the JavaScript logic for the 2048 game.", - "assignee": "Alex" - } - }, - { - "command_name": "Plan.append_task", - "args": { - "task_id": "4", - "dependent_task_ids": ["1"], - "instruction": "Use ReviewAndRewriteCode to review the code in index.html to ensure it meets the design specifications.", - "assignee": "Alex" - } - }, - { - "command_name": "Plan.append_task", - "args": { - "task_id": "5", - "dependent_task_ids": ["2"], - "instruction": "Use ReviewAndRewriteCode to review the code in style.css to ensure it meets the design specifications.", - "assignee": "Alex" - } - }, - { - "command_name": "Plan.append_task", - "args": { - "task_id": "6", - "dependent_task_ids": ["3"], - "instruction": "Use ReviewAndRewriteCode to review the code in script.js to ensure it meets the design specifications.", - "assignee": "Alex" - } - } -] -``` -""" - -ENGINEER_EXPERIENCES = ((REQ1, RESP1), (REQ2, RESP2)) diff --git a/examples/exp_pool/init_exp_pool/main.py b/examples/exp_pool/init_exp_pool/main.py deleted file mode 100644 index 849834513..000000000 --- a/examples/exp_pool/init_exp_pool/main.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Init experience pool. - -Put some useful experiences into the experience pool. -""" - -import asyncio - -from examples.exp_pool.init_exp_pool.engineer_exps import ENGINEER_EXPERIENCES -from examples.exp_pool.init_exp_pool.team_leader_exps import TEAM_LEADER_EXPERIENCES -from metagpt.exp_pool import exp_manager -from metagpt.exp_pool.schema import EntryType, Experience, Metric, Score -from metagpt.logs import logger - - -async def add_exp(req: str, resp: str, tag: str, metric: Metric = None): - exp = Experience( - req=req, - resp=resp, - entry_type=EntryType.MANUAL, - tag=tag, - metric=metric or Metric(score=Score(val=10, reason="Manual")), - ) - - exp_manager.config.exp_pool.enable_write = True - exp_manager.create_exp(exp) - logger.info(f"New experience created for the request `{req[:10]}`.") - - -async def add_teamleader_exps(): - tag = "TeamLeader.llm_cached_aask" - - for req, resp in TEAM_LEADER_EXPERIENCES: - await add_exp(req=req, resp=resp, tag=tag) - - -async def add_engineer_exps(): - tag = "Engineer2.llm_cached_aask" - - for req, resp in ENGINEER_EXPERIENCES: - await add_exp(req=req, resp=resp, tag=tag) - - -def query_exps_count(): - count = exp_manager.get_exps_count() - logger.info(f"Experiences Count: {count}") - - -async def main(): - await add_teamleader_exps() - await add_engineer_exps() - query_exps_count() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/examples/exp_pool/init_exp_pool/team_leader_exps.py b/examples/exp_pool/init_exp_pool/team_leader_exps.py deleted file mode 100644 index 347faac45..000000000 --- a/examples/exp_pool/init_exp_pool/team_leader_exps.py +++ /dev/null @@ -1,181 +0,0 @@ -import json - -OBJ1 = [ - { - "role": "user", - "content": "\n# Current Plan\n{'goal': \"from to {''}: Write a 1024 game using JavaScript and HTML code without using any frameworks, user can play with keyboard.\", 'tasks': []}\n\n# Current Task\n\n\n# Instruction\nYou are a team leader, and you are responsible for drafting tasks and routing tasks to your team members.\nYour team member:\nTim: Team Leader, \nAlice: Product Manager, efficiently create a successful product that meets market demands and user expectations\nBob: Architect, design a concise, usable, complete software system\nEve: Project Manager, break down tasks according to PRD/technical design, generate a task list, and analyze task dependencies to start with the prerequisite modules\nAlex: Engineer, Take on game, app, and web development\nDavid: DataAnalyst, Take on any data-related tasks, such as data analysis, machine learning, deep learning, web browsing, web scraping, web searching, web deployment, terminal operation, git and github operation, etc.\n\nYou should NOT assign consecutive tasks to the same team member, instead, assign an aggregated task (or the complete requirement) and let the team member to decompose it.\nWhen creating a new plan involving multiple members, create all tasks at once.\nIf plan is created, you should track the progress based on team member feedback message, and update plan accordingly, such as Plan.finish_current_task, Plan.reset_task, Plan.replace_task, etc.\nYou should use TeamLeader.publish_team_message to team members, asking them to start their task. DONT omit any necessary info such as path, link, environment, programming language, framework, requirement, constraint from original content to team members because you are their sole info source.\nPay close attention to new user message, review the conversation history, use RoleZero.reply_to_human to respond to the user directly, DON'T ask your team members.\n\nNote:\n1. If the requirement is a pure DATA-RELATED requirement, such as bug fixes, issue reporting, environment setup, terminal operations, pip install, web browsing, web scraping, web searching, web imitation, data science, data analysis, machine learning, deep learning, text-to-image etc. DON'T decompose it, assign a single task with the original user requirement as instruction directly to Data Analyst.\n2. If the requirement is developing a software, game, app, or website, excluding the above data-related tasks, you should decompose the requirement into multiple tasks and assign them to different team members based on their expertise, usually the sequence of Product Manager -> Architect -> Project Manager -> Engineer -> (optional: QaEngine if present) -> (optional: DataAnalyst if user requests deployment), each assigned ONE task. When publishing message to Product Manager, you should directly copy the full original user requirement.\n3. If the requirement contains both DATA-RELATED part mentioned in 1 and software development part mentioned in 2, you should decompose the software development part and assign them to different team members based on their expertise, and assign the DATA-RELATED part to Data Analyst David directly.\n4. If the requirement is a common-sense, logical, or math problem, you should respond directly without assigning any task to team members.\n5. If you think the requirement is not clear or ambiguous, you should ask the user for clarification immediately. Assign tasks only after all info is clear.\n6. It is helpful for Engineer to have both the system design and the project schedule for writing the code, so include paths of both files (if available) and remind Engineer to definitely read them when publishing message to Engineer.\n7. If the requirement is writing a TRD and software framework, you should assign it to Architect. When publishing message to Architect, you should directly copy the full original user requirement.\n", - } -] -OBJ2 = [ - { - "role": "user", - "content": "\n# Current Plan\n{'goal': \"from to {''}: Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.\", 'tasks': []}\n\n# Current Task\n\n\n# Instruction\nYou are a team leader, and you are responsible for drafting tasks and routing tasks to your team members.\nYour team member:\nTim: Team Leader, \nAlice: Product Manager, efficiently create a successful product that meets market demands and user expectations\nBob: Architect, design a concise, usable, complete software system\nEve: Project Manager, break down tasks according to PRD/technical design, generate a task list, and analyze task dependencies to start with the prerequisite modules\nAlex: Engineer, Take on game, app, and web development\nDavid: DataAnalyst, Take on any data-related tasks, such as data analysis, machine learning, deep learning, web browsing, web scraping, web searching, web deployment, terminal operation, git and github operation, etc.\n\nYou should NOT assign consecutive tasks to the same team member, instead, assign an aggregated task (or the complete requirement) and let the team member to decompose it.\nWhen creating a new plan involving multiple members, create all tasks at once.\nIf plan is created, you should track the progress based on team member feedback message, and update plan accordingly, such as Plan.finish_current_task, Plan.reset_task, Plan.replace_task, etc.\nYou should use TeamLeader.publish_team_message to team members, asking them to start their task. DONT omit any necessary info such as path, link, environment, programming language, framework, requirement, constraint from original content to team members because you are their sole info source.\nPay close attention to new user message, review the conversation history, use RoleZero.reply_to_human to respond to the user directly, DON'T ask your team members.\n\nNote:\n1. If the requirement is a pure DATA-RELATED requirement, such as bug fixes, issue reporting, environment setup, terminal operations, pip install, web browsing, web scraping, web searching, web imitation, data science, data analysis, machine learning, deep learning, text-to-image etc. DON'T decompose it, assign a single task with the original user requirement as instruction directly to Data Analyst.\n2. If the requirement is developing a software, game, app, or website, excluding the above data-related tasks, you should decompose the requirement into multiple tasks and assign them to different team members based on their expertise, usually the sequence of Product Manager -> Architect -> Project Manager -> Engineer -> (optional: QaEngine if present) -> (optional: DataAnalyst if user requests deployment), each assigned ONE task. When publishing message to Product Manager, you should directly copy the full original user requirement.\n3. If the requirement contains both DATA-RELATED part mentioned in 1 and software development part mentioned in 2, you should decompose the software development part and assign them to different team members based on their expertise, and assign the DATA-RELATED part to Data Analyst David directly.\n4. If the requirement is a common-sense, logical, or math problem, you should respond directly without assigning any task to team members.\n5. If you think the requirement is not clear or ambiguous, you should ask the user for clarification immediately. Assign tasks only after all info is clear.\n6. It is helpful for Engineer to have both the system design and the project schedule for writing the code, so include paths of both files (if available) and remind Engineer to definitely read them when publishing message to Engineer.\n7. If the requirement is writing a TRD and software framework, you should assign it to Architect. When publishing message to Architect, you should directly copy the full original user requirement.\n", - } -] -OBJ4 = [ - { - "role": "user", - "content": "\n# Current Plan\n{'goal': \"from to {''}: how does the project go?\", 'tasks': []}\n\n# Current Task\n\n\n# Instruction\nYou are a team leader, and you are responsible for drafting tasks and routing tasks to your team members.\nYour team member:\nTim: Team Leader, \nAlice: Product Manager, efficiently create a successful product that meets market demands and user expectations\nBob: Architect, design a concise, usable, complete software system\nEve: Project Manager, break down tasks according to PRD/technical design, generate a task list, and analyze task dependencies to start with the prerequisite modules\nAlex: Engineer, Take on game, app, and web development\nDavid: DataAnalyst, Take on any data-related tasks, such as data analysis, machine learning, deep learning, web browsing, web scraping, web searching, web deployment, terminal operation, git and github operation, etc.\n\nYou should NOT assign consecutive tasks to the same team member, instead, assign an aggregated task (or the complete requirement) and let the team member to decompose it.\nWhen creating a new plan involving multiple members, create all tasks at once.\nIf plan is created, you should track the progress based on team member feedback message, and update plan accordingly, such as Plan.finish_current_task, Plan.reset_task, Plan.replace_task, etc.\nYou should use TeamLeader.publish_team_message to team members, asking them to start their task. DONT omit any necessary info such as path, link, environment, programming language, framework, requirement, constraint from original content to team members because you are their sole info source.\nPay close attention to new user message, review the conversation history, use RoleZero.reply_to_human to respond to the user directly, DON'T ask your team members.\n\nNote:\n1. If the requirement is a pure DATA-RELATED requirement, such as bug fixes, issue reporting, environment setup, terminal operations, pip install, web browsing, web scraping, web searching, web imitation, data science, data analysis, machine learning, deep learning, text-to-image etc. DON'T decompose it, assign a single task with the original user requirement as instruction directly to Data Analyst.\n2. If the requirement is developing a software, game, app, or website, excluding the above data-related tasks, you should decompose the requirement into multiple tasks and assign them to different team members based on their expertise, usually the sequence of Product Manager -> Architect -> Project Manager -> Engineer -> (optional: QaEngine if present) -> (optional: DataAnalyst if user requests deployment), each assigned ONE task. When publishing message to Product Manager, you should directly copy the full original user requirement.\n3. If the requirement contains both DATA-RELATED part mentioned in 1 and software development part mentioned in 2, you should decompose the software development part and assign them to different team members based on their expertise, and assign the DATA-RELATED part to Data Analyst David directly.\n4. If the requirement is a common-sense, logical, or math problem, you should respond directly without assigning any task to team members.\n5. If you think the requirement is not clear or ambiguous, you should ask the user for clarification immediately. Assign tasks only after all info is clear.\n6. It is helpful for Engineer to have both the system design and the project schedule for writing the code, so include paths of both files (if available) and remind Engineer to definitely read them when publishing message to Engineer.\n7. If the requirement is writing a TRD and software framework, you should assign it to Architect. When publishing message to Architect, you should directly copy the full original user requirement.\n", - } -] - -REQ1 = json.dumps(OBJ1) -REQ2 = json.dumps(OBJ2) -REQ3 = """Conversation History: -[ - ..., - {'role': 'assistant', 'content': 'from Alice(Product Manager) to {'Bob'}: {'docs': {'20240424153821.json': {'root_path': 'docs/prd', 'filename': '20240424153821.json', 'content': '{"Language":"en_us","Programming Language":"Python","Original Requirements":"create a cli snake game","Project Name":"snake_game","Product Goals":["Develop an intuitive and addictive snake game",...], ...}}}}}, -] -""" -REQ4 = json.dumps(OBJ4) - -RESP1 = """ -Explanation: The requirement is about software development. Assign each tasks to a different team member based on their expertise. When publishing message to Product Manager, we copy original user requirement directly to ensure no information loss. -```json -[ - { - "command_name": "Plan.append_task", - "args": { - "task_id": "1", - "dependent_task_ids": [], - "instruction": "Create a product requirement document (PRD) outlining the features, user interface, and user experience of the CLI python snake game.", - "assignee": "Alice" - } - }, - { - "command_name": "Plan.append_task", - "args": { - "task_id": "2", - "dependent_task_ids": ["1"], - "instruction": "Design the software architecture for the CLI snake game, including the choice of programming language, libraries, and data flow.", - "assignee": "Bob" - } - }, - { - "command_name": "Plan.append_task", - "args": { - "task_id": "3", - "dependent_task_ids": ["2"], - "instruction": "Break down the architecture into manageable tasks, identify task dependencies, and prepare a detailed task list for implementation.", - "assignee": "Eve" - } - }, - { - "command_name": "Plan.append_task", - "args": { - "task_id": "4", - "dependent_task_ids": ["3"], - "instruction": "Implement the core game logic for the CLI snake game, including snake movement, food generation, and score tracking.", - "assignee": "Alex" - } - }, - { - "command_name": "Plan.append_task", - "args": { - "task_id": "5", - "dependent_task_ids": ["4"], - "instruction": "Write comprehensive tests for the game logic and user interface to ensure functionality and reliability.", - "assignee": "Edward" - } - }, - { - "command_name": "TeamLeader.publish_message", - "args": { - "content": "Create a cli snake game using Python", - "send_to": "Alice" - } - }, - { - "command_name": "RoleZero.reply_to_human", - "args": { - "content": "I have assigned the tasks to the team members. Alice will create the PRD, Bob will design the software architecture, Eve will break down the architecture into tasks, Alex will implement the core game logic, and Edward will write comprehensive tests. The team will work on the project accordingly" - } - }, - { - "command_name": "end" - } -] -``` -""" -RESP2 = """ -Explanation: DON'T decompose requirement if it is a DATA-RELATED task, assign a single task directly to Data Analyst David. He will manage the decomposition and implementation. -```json -[ - { - "command_name": "Plan.append_task", - "args": { - "task_id": "1", - "dependent_task_ids": [], - "instruction": "Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.", - "assignee": "David" - } - }, - { - "command_name": "TeamLeader.publish_message", - "args": { - "content": "Run data analysis on sklearn Wine recognition dataset, include a plot, and train a model to predict wine class (20% as validation), and show validation accuracy.", - "send_to": "David" - } - }, - { - "command_name": "RoleZero.reply_to_human", - "args": { - "content": "I have assigned the task to David. He will break down the task further by himself and starts solving it." - } - }, - { - "command_name": "end" - } -] -``` -""" -RESP3 = """ -Explanation: You received a message from Alice, the Product Manager, that she has completed the PRD, use Plan.finish_current_task to mark her task as finished and moves the plan to the next task. Based on plan status, next task is for Bob (Architect), publish a message asking him to start. The message content should contain important path info. -```json -[ - { - "command_name": "Plan.finish_current_task", - "args": {} - }, - { - "command_name": "TeamLeader.publish_message", - "args": { - "content": "Please design the software architecture for the snake game based on the PRD created by Alice. The PRD is at 'docs/prd/20240424153821.json'. Include the choice of programming language, libraries, and data flow, etc.", - "send_to": "Bob" - } - }, - { - "command_name": "RoleZero.reply_to_human", - "args": { - "content": "Alice has completed the PRD. I have marked her task as finished and sent the PRD to Bob. Bob will work on the software architecture." - } - }, - { - "command_name": "end" - } -] -``` -""" -RESP4 = """ -Explanation: The user is asking for a general update on the project status. Give a straight answer about the current task the team is working on and provide a summary of the completed tasks. -```json -[ - { - "command_name": "RoleZero.reply_to_human", - "args": { - "content": "The team is currently working on ... We have completed ..." - } - }, - { - "command_name": "end" - } -] -``` -""" - -TEAM_LEADER_EXPERIENCES = ( - (REQ1, RESP1), - (REQ2, RESP2), - (REQ3, RESP3), - (REQ4, RESP4), -) diff --git a/examples/exp_pool/manager.py b/examples/exp_pool/manager.py index 3216e78b8..ae998214a 100644 --- a/examples/exp_pool/manager.py +++ b/examples/exp_pool/manager.py @@ -1,4 +1,8 @@ -"""Simple example of experience pool.""" +""" +Demonstrate the creation and querying of experiences. + +This script creates a new experience, logs its creation, and then queries for experiences matching the same request. +""" import asyncio @@ -8,12 +12,16 @@ from metagpt.logs import logger async def main(): - req = "Simple task." - exp = Experience(req=req, resp="echo", entry_type=EntryType.MANUAL) + # Define the simple request and response + req = "Simple req" + resp = "Simple resp" + # Add the new experience + exp = Experience(req=req, resp=resp, entry_type=EntryType.MANUAL) exp_manager.create_exp(exp) logger.info(f"New experience created for the request `{req}`.") + # Query for experiences matching the request exps = await exp_manager.query_exps(req) logger.info(f"Got experiences: {exps}") diff --git a/examples/exp_pool/scorer.py b/examples/exp_pool/scorer.py index c412feaf3..aafcee63f 100644 --- a/examples/exp_pool/scorer.py +++ b/examples/exp_pool/scorer.py @@ -2,13 +2,16 @@ import asyncio from metagpt.exp_pool.scorers import SimpleScorer +# Request to implement quicksort in Python REQ = "Write a program to implement quicksort in python." +# First response: Quicksort implementation without base case RESP1 = """ def quicksort(arr): return quicksort([x for x in arr[1:] if x <= arr[0]]) + [arr[0]] + quicksort([x for x in arr[1:] if x > arr[0]]) """ +# Second response: Quicksort implementation with base case RESP2 = """ def quicksort(arr): if len(arr) <= 1: @@ -18,6 +21,15 @@ def quicksort(arr): async def simple(): + """Evaluates two quicksort implementations using SimpleScorer. + + Example: + { + "val": 3, + "reason": "The response attempts to implement quicksort but contains a critical flaw: it lacks a base case to terminate the recursion, which will lead to a maximum recursion depth exceeded error for non-empty lists. Additionally, the function does not handle empty lists properly. A correct implementation should include a base case to handle lists of length 0 or 1." + } + """ + scorer = SimpleScorer() await scorer.evaluate(req=REQ, resp=RESP1) diff --git a/tests/metagpt/exp_pool/test_manager.py b/tests/metagpt/exp_pool/test_manager.py index 2f712ad44..3fb2aaa6d 100644 --- a/tests/metagpt/exp_pool/test_manager.py +++ b/tests/metagpt/exp_pool/test_manager.py @@ -3,9 +3,8 @@ import pytest from metagpt.config2 import Config from metagpt.configs.exp_pool_config import ExperiencePoolConfig from metagpt.configs.llm_config import LLMConfig -from metagpt.exp_pool.manager import ExperienceManager -from metagpt.exp_pool.schema import Experience -from metagpt.rag.engines import SimpleEngine +from metagpt.exp_pool.manager import Experience, ExperienceManager +from metagpt.exp_pool.schema import QueryType class TestExperienceManager: @@ -15,50 +14,65 @@ class TestExperienceManager: @pytest.fixture def mock_storage(self, mocker): - engine = mocker.MagicMock(spec=SimpleEngine) + engine = mocker.MagicMock() engine.add_objs = mocker.MagicMock() engine.aretrieve = mocker.AsyncMock(return_value=[]) engine._retriever = mocker.MagicMock() engine._retriever._vector_store = mocker.MagicMock() - engine._retriever._vector_store._get = mocker.MagicMock(return_value=mocker.MagicMock(ids=[])) + engine._retriever._vector_store._collection = mocker.MagicMock() + engine._retriever._vector_store._collection.count = mocker.MagicMock(return_value=10) return engine @pytest.fixture - def mock_experience_manager(self, mock_config, mock_storage): - return ExperienceManager(config=mock_config, storage=mock_storage) + def exp_manager(self, mock_config, mock_storage): + manager = ExperienceManager(config=mock_config) + manager._storage = mock_storage + return manager - @pytest.fixture - def mock_experience(self): - return Experience(req="req", resp="resp") - - def test_initialize_storage(self, mock_experience_manager, mock_storage): - assert mock_experience_manager.storage is mock_storage - - def test_create_exp(self, mock_experience_manager, mock_experience): - mock_experience_manager.create_exp(mock_experience) - mock_experience_manager.storage.add_objs.assert_called_with([mock_experience]) - - def test_create_exp_write_disabled(self, mock_experience_manager, mock_experience, mock_config): - mock_config.exp_pool.enable_write = False - mock_experience_manager.create_exp(mock_experience) - mock_experience_manager.storage.add_objs.assert_not_called() + def test_vector_store_property(self, exp_manager): + assert exp_manager.vector_store == exp_manager.storage._retriever._vector_store @pytest.mark.asyncio - async def test_query_exps(self, mock_experience_manager, mocker): - req = "req" - resp = "resp" - tag = "test" - experiences = [Experience(req=req, resp=resp, tag="test"), Experience(req=req, resp=resp, tag="other")] - mock_experience_manager.storage.aretrieve.return_value = [ - mocker.MagicMock(metadata={"obj": exp}) for exp in experiences - ] + async def test_query_exps_with_exact_match(self, exp_manager, mocker): + req = "exact query" + exp1 = Experience(req=req, resp="response1") + exp2 = Experience(req="different query", resp="response2") - result = await mock_experience_manager.query_exps(req, tag) + mock_node1 = mocker.MagicMock(metadata={"obj": exp1}) + mock_node2 = mocker.MagicMock(metadata={"obj": exp2}) + + exp_manager.storage.aretrieve.return_value = [mock_node1, mock_node2] + + result = await exp_manager.query_exps(req, query_type=QueryType.EXACT) assert len(result) == 1 - assert result[0].tag == "test" + assert result[0].req == req @pytest.mark.asyncio - async def test_query_exps_no_read_permission(self, mock_experience_manager, mock_config): + async def test_query_exps_with_tag_filter(self, exp_manager, mocker): + tag = "test_tag" + exp1 = Experience(req="query1", resp="response1", tag=tag) + exp2 = Experience(req="query2", resp="response2", tag="other_tag") + + mock_node1 = mocker.MagicMock(metadata={"obj": exp1}) + mock_node2 = mocker.MagicMock(metadata={"obj": exp2}) + + exp_manager.storage.aretrieve.return_value = [mock_node1, mock_node2] + + result = await exp_manager.query_exps("query", tag=tag) + assert len(result) == 1 + assert result[0].tag == tag + + def test_get_exps_count(self, exp_manager): + assert exp_manager.get_exps_count() == 10 + + def test_create_exp_write_disabled(self, exp_manager, mock_config): + mock_config.exp_pool.enable_write = False + exp = Experience(req="test", resp="response") + exp_manager.create_exp(exp) + exp_manager.storage.add_objs.assert_not_called() + + @pytest.mark.asyncio + async def test_query_exps_read_disabled(self, exp_manager, mock_config): mock_config.exp_pool.enable_read = False - result = await mock_experience_manager.query_exps("query") + result = await exp_manager.query_exps("query") assert result == [] diff --git a/tests/metagpt/exp_pool/test_serializers/test_role_zero.py b/tests/metagpt/exp_pool/test_serializers/test_role_zero.py index d4525d535..a168650fc 100644 --- a/tests/metagpt/exp_pool/test_serializers/test_role_zero.py +++ b/tests/metagpt/exp_pool/test_serializers/test_role_zero.py @@ -22,7 +22,7 @@ class TestRoleZeroSerializer: return [{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}] def test_serialize_req_empty_input(self, serializer: RoleZeroSerializer): - assert serializer.serialize_req([]) == "" + assert serializer.serialize_req(req=[]) == "" def test_serialize_req_with_content(self, serializer: RoleZeroSerializer, last_item: dict): req = [ @@ -33,7 +33,7 @@ class TestRoleZeroSerializer: expected_output = json.dumps( [{"role": "user", "content": "Command Editor.read executed: file_path=test.py"}, last_item] ) - assert serializer.serialize_req(req) == expected_output + assert serializer.serialize_req(req=req) == expected_output def test_filter_req(self, serializer: RoleZeroSerializer): req = [ diff --git a/tests/metagpt/exp_pool/test_serializers/test_simple.py b/tests/metagpt/exp_pool/test_serializers/test_simple.py index 05ef1ca11..2a6bf96e3 100644 --- a/tests/metagpt/exp_pool/test_serializers/test_simple.py +++ b/tests/metagpt/exp_pool/test_serializers/test_simple.py @@ -8,28 +8,28 @@ class TestSimpleSerializer: def serializer(self): return SimpleSerializer() - def test_serialize_req(self, serializer): + def test_serialize_req(self, serializer: SimpleSerializer): # Test with different types of input - assert serializer.serialize_req(123) == "123" - assert serializer.serialize_req("test") == "test" - assert serializer.serialize_req([1, 2, 3]) == "[1, 2, 3]" - assert serializer.serialize_req({"a": 1}) == "{'a': 1}" + assert serializer.serialize_req(req=123) == "123" + assert serializer.serialize_req(req="test") == "test" + assert serializer.serialize_req(req=[1, 2, 3]) == "[1, 2, 3]" + assert serializer.serialize_req(req={"a": 1}) == "{'a': 1}" - def test_serialize_resp(self, serializer): + def test_serialize_resp(self, serializer: SimpleSerializer): # Test with different types of input assert serializer.serialize_resp(456) == "456" assert serializer.serialize_resp("response") == "response" assert serializer.serialize_resp([4, 5, 6]) == "[4, 5, 6]" assert serializer.serialize_resp({"b": 2}) == "{'b': 2}" - def test_deserialize_resp(self, serializer): + def test_deserialize_resp(self, serializer: SimpleSerializer): # Test with different types of input assert serializer.deserialize_resp("789") == "789" assert serializer.deserialize_resp("test_response") == "test_response" assert serializer.deserialize_resp("[7, 8, 9]") == "[7, 8, 9]" assert serializer.deserialize_resp("{'c': 3}") == "{'c': 3}" - def test_roundtrip(self, serializer): + def test_roundtrip(self, serializer: SimpleSerializer): # Test serialization and deserialization roundtrip original = "test_roundtrip" serialized = serializer.serialize_resp(original) @@ -37,8 +37,8 @@ class TestSimpleSerializer: assert deserialized == original @pytest.mark.parametrize("input_value", [123, "test", [1, 2, 3], {"a": 1}, None]) - def test_serialize_req_types(self, serializer, input_value): + def test_serialize_req_types(self, serializer: SimpleSerializer, input_value): # Test serialize_req with various input types - result = serializer.serialize_req(input_value) + result = serializer.serialize_req(req=input_value) assert isinstance(result, str) assert result == str(input_value) From 4e1955eba8199cdcdee890f26b24d7b8bfcb427b Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Tue, 16 Jul 2024 20:52:44 +0800 Subject: [PATCH 106/119] update comment --- metagpt/exp_pool/context_builders/role_zero.py | 10 +++++----- metagpt/prompts/di/role_zero.py | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/metagpt/exp_pool/context_builders/role_zero.py b/metagpt/exp_pool/context_builders/role_zero.py index aa5524ab4..924bd56aa 100644 --- a/metagpt/exp_pool/context_builders/role_zero.py +++ b/metagpt/exp_pool/context_builders/role_zero.py @@ -12,12 +12,12 @@ class RoleZeroContextBuilder(BaseContextBuilder): """Builds the role zero context string. Note: - 1. The expected format for `req`, e.g., [{...}, {"role": "user", "content": "context"}, {"role": "user", "content": "context exp part"}]. - 2. Returns the original `req` if it is empty, incorrectly formatted or there are no experiences. + 1. The expected format for `req`, e.g., [{...}, {"role": "user", "content": "context"}]. + 2. Returns the original `req` if it is empty. 3. Creates a copy of req and replaces the example content in the copied req with actual experiences. """ - if not req or len(req) < 2: + if not req: return req exps = self.format_exps() @@ -26,12 +26,12 @@ class RoleZeroContextBuilder(BaseContextBuilder): req_copy = copy.deepcopy(req) - req_copy[-2]["content"] = self.replace_example_content(req_copy[-2].get("content", ""), exps) + req_copy[-1]["content"] = self.replace_example_content(req_copy[-1].get("content", ""), exps) return req_copy def replace_example_content(self, text: str, new_example_content: str) -> str: - return self.replace_content_between_markers(text, "# Example", "# Available Commands", new_example_content) + return self.replace_content_between_markers(text, "# Example", "# Instruction", new_example_content) @staticmethod def replace_content_between_markers(text: str, start_marker: str, end_marker: str, new_content: str) -> str: diff --git a/metagpt/prompts/di/role_zero.py b/metagpt/prompts/di/role_zero.py index 41b9e023e..b2e931e23 100644 --- a/metagpt/prompts/di/role_zero.py +++ b/metagpt/prompts/di/role_zero.py @@ -18,9 +18,6 @@ class Task(BaseModel): task_type: str = "" assignee: str = "" -# Example -{example} - # Available Commands {available_commands} Special Command: Use {{"command_name": "end"}} to do nothing or indicate completion of all requirements and the end of actions. @@ -34,6 +31,9 @@ Special Command: Use {{"command_name": "end"}} to do nothing or indicate complet # Current Task {current_task} +# Example +{example} + # Instruction {instruction} From d612d826d52f90eda32b745bb1cc2ebc4242f2d8 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Tue, 16 Jul 2024 21:13:16 +0800 Subject: [PATCH 107/119] update comment --- metagpt/prompts/di/role_zero.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/metagpt/prompts/di/role_zero.py b/metagpt/prompts/di/role_zero.py index b2e931e23..890c1d562 100644 --- a/metagpt/prompts/di/role_zero.py +++ b/metagpt/prompts/di/role_zero.py @@ -17,14 +17,14 @@ class Task(BaseModel): instruction: str = "" task_type: str = "" assignee: str = "" + +# Available Task Types +{task_type_desc} # Available Commands {available_commands} Special Command: Use {{"command_name": "end"}} to do nothing or indicate completion of all requirements and the end of actions. -# Available Task Types -{task_type_desc} - # Current Plan {plan_status} From e70a08045460cedab612849c2a5b6ff9b92269eb Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Tue, 16 Jul 2024 21:20:01 +0800 Subject: [PATCH 108/119] update exp_pool tests --- .../test_rolezero_context_builder.py | 18 +++++++----------- .../test_serializers/test_role_zero.py | 4 +--- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/tests/metagpt/exp_pool/test_context_builders/test_rolezero_context_builder.py b/tests/metagpt/exp_pool/test_context_builders/test_rolezero_context_builder.py index a95566ed1..b7182602d 100644 --- a/tests/metagpt/exp_pool/test_context_builders/test_rolezero_context_builder.py +++ b/tests/metagpt/exp_pool/test_context_builders/test_rolezero_context_builder.py @@ -25,31 +25,27 @@ class TestRoleZeroContextBuilder: async def test_build_with_experiences(self, context_builder, mocker): mocker.patch.object(BaseContextBuilder, "format_exps", return_value="Formatted experiences") mocker.patch.object(RoleZeroContextBuilder, "replace_example_content", return_value="Updated content") - req = [{"content": "Original content 1"}, {"content": "Original content exp part"}] + req = [{"content": "Original content 1"}] result = await context_builder.build(req=req) - assert result == [{"content": "Updated content"}, {"content": "Original content exp part"}] + assert result == [{"content": "Updated content"}] def test_replace_example_content(self, context_builder, mocker): mocker.patch.object(RoleZeroContextBuilder, "replace_content_between_markers", return_value="Replaced content") result = context_builder.replace_example_content("Original text", "New example content") assert result == "Replaced content" context_builder.replace_content_between_markers.assert_called_once_with( - "Original text", "# Example", "# Available Commands", "New example content" + "Original text", "# Example", "# Instruction", "New example content" ) def test_replace_content_between_markers(self): - text = "Start\n# Example\nOld content\n# Available Commands\nEnd" + text = "Start\n# Example\nOld content\n# Instruction\nEnd" new_content = "New content" - result = RoleZeroContextBuilder.replace_content_between_markers( - text, "# Example", "# Available Commands", new_content - ) - expected = "Start\n# Example\nNew content\n\n# Available Commands\nEnd" + result = RoleZeroContextBuilder.replace_content_between_markers(text, "# Example", "# Instruction", new_content) + expected = "Start\n# Example\nNew content\n\n# Instruction\nEnd" assert result == expected def test_replace_content_between_markers_no_match(self): text = "Start\nNo markers\nEnd" new_content = "New content" - result = RoleZeroContextBuilder.replace_content_between_markers( - text, "# Example", "# Available Commands", new_content - ) + result = RoleZeroContextBuilder.replace_content_between_markers(text, "# Example", "# Instruction", new_content) assert result == text diff --git a/tests/metagpt/exp_pool/test_serializers/test_role_zero.py b/tests/metagpt/exp_pool/test_serializers/test_role_zero.py index a168650fc..964443f29 100644 --- a/tests/metagpt/exp_pool/test_serializers/test_role_zero.py +++ b/tests/metagpt/exp_pool/test_serializers/test_role_zero.py @@ -30,9 +30,7 @@ class TestRoleZeroSerializer: {"role": "assistant", "content": "Some other content"}, last_item, ] - expected_output = json.dumps( - [{"role": "user", "content": "Command Editor.read executed: file_path=test.py"}, last_item] - ) + expected_output = json.dumps([{"role": "user", "content": "Command Editor.read executed: file_path=test.py"}]) assert serializer.serialize_req(req=req) == expected_output def test_filter_req(self, serializer: RoleZeroSerializer): From 5a309005c480c81db958404eb5b1e9e9075f76bf Mon Sep 17 00:00:00 2001 From: lidanyang Date: Wed, 17 Jul 2024 15:16:24 +0800 Subject: [PATCH 109/119] refine prompt --- metagpt/roles/di/data_analyst.py | 2 ++ metagpt/strategy/experience_retriever.py | 2 +- metagpt/strategy/planner.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/metagpt/roles/di/data_analyst.py b/metagpt/roles/di/data_analyst.py index 2b1bb10b1..c52127527 100644 --- a/metagpt/roles/di/data_analyst.py +++ b/metagpt/roles/di/data_analyst.py @@ -105,5 +105,7 @@ class DataAnalyst(RoleZero): status = 'Success' if success else 'Failed' output = CODE_STATUS.format(code=code, status=status, result=result) + if success: + output += 'The code written has been executed successfully.' self.rc.working_memory.clear() return output diff --git a/metagpt/strategy/experience_retriever.py b/metagpt/strategy/experience_retriever.py index 5f1c443c9..f667f03a5 100644 --- a/metagpt/strategy/experience_retriever.py +++ b/metagpt/strategy/experience_retriever.py @@ -1035,7 +1035,7 @@ Explanation: The requirement is to scrape data from a website and extract inform "command_name": "Plan.append_task", "args": { "task_id": "2", - "dependent_task_ids": ["3"], + "dependent_task_ids": ["1"], "instruction": "Parse the html content to scrape the restaurant names and print it.", "assignee": "David" } diff --git a/metagpt/strategy/planner.py b/metagpt/strategy/planner.py index 95ad1f5cc..d195cc03b 100644 --- a/metagpt/strategy/planner.py +++ b/metagpt/strategy/planner.py @@ -47,7 +47,7 @@ PLAN_STATUS = """ {current_task_result} ## Task Guidance -Write code for the incomplete sections of 'Current Task'. And avoid duplicating code from 'Finished Tasks', such as repeated import of packages, reading data, etc. +Write code for the incomplete sections of 'Current Task'. And avoid duplicating code from 'Finished Tasks' and 'Finished Section of Current Task', such as repeated import of packages, reading data, etc. Specifically, {guidance} """ From 08975c3a0838cfda45fe69afe8bd804c9891e889 Mon Sep 17 00:00:00 2001 From: lidanyang Date: Wed, 17 Jul 2024 15:16:56 +0800 Subject: [PATCH 110/119] keep len to 20000 --- metagpt/actions/di/execute_nb_code.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/metagpt/actions/di/execute_nb_code.py b/metagpt/actions/di/execute_nb_code.py index b3bf4fcb8..dfeea7e67 100644 --- a/metagpt/actions/di/execute_nb_code.py +++ b/metagpt/actions/di/execute_nb_code.py @@ -158,7 +158,7 @@ class ExecuteNbCode(Action): else: cell["outputs"].append(new_output(output_type="stream", name="stdout", text=str(output))) - def parse_outputs(self, outputs: list[str], keep_len: int = 5000) -> Tuple[bool, str]: + def parse_outputs(self, outputs: list[str], keep_len: int = 20000) -> Tuple[bool, str]: """Parses the outputs received from notebook execution.""" assert isinstance(outputs, list) parsed_output, is_success = [], True @@ -191,10 +191,8 @@ class ExecuteNbCode(Action): output_text = remove_log_and_warning_lines(output_text) # The useful information of the exception is at the end, # the useful information of normal output is at the begining. - if '' not in output_text: - output_text = output_text[:keep_len] if is_success else output_text[-keep_len:] - else: - output_text = output_text[:20000] if is_success else output_text[-keep_len:] + # if '' not in output_text: + output_text = output_text[:keep_len] if is_success else output_text[-keep_len:] parsed_output.append(output_text) return is_success, ",".join(parsed_output) From 775a327fbdb7e1e4a9c01b2798b80ffb7c263979 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E4=BC=9F=E9=9F=AC?= Date: Thu, 18 Jul 2024 11:49:44 +0800 Subject: [PATCH 111/119] =?UTF-8?q?=E4=BF=AE=E6=94=B9Editor.read=20?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E8=B7=AF=E5=BE=84=E9=94=99=E8=AF=AF=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- metagpt/environment/mgx/mgx_env.py | 2 +- metagpt/roles/di/role_zero.py | 34 ++++++++++++++++++------ metagpt/strategy/experience_retriever.py | 13 ++++++++- 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/metagpt/environment/mgx/mgx_env.py b/metagpt/environment/mgx/mgx_env.py index 69f80c2ff..1f74a16c8 100644 --- a/metagpt/environment/mgx/mgx_env.py +++ b/metagpt/environment/mgx/mgx_env.py @@ -91,7 +91,7 @@ class MGXEnv(Environment): async def reply_to_human(self, content: str, sent_from: Role = None) -> str: # NOTE: Can be overwritten in remote setting - return content + return "Message send successfully." def message_within_software_sop(self, message: Message) -> bool: # Engineer, QaEngineer can be end of the SOP. Their msg requires routing outside. diff --git a/metagpt/roles/di/role_zero.py b/metagpt/roles/di/role_zero.py index 5f27c79ef..c945f4d3d 100644 --- a/metagpt/roles/di/role_zero.py +++ b/metagpt/roles/di/role_zero.py @@ -28,6 +28,7 @@ from metagpt.tools.tool_registry import register_tool from metagpt.utils.common import CodeParser, any_to_str from metagpt.utils.repair_llm_raw_output import RepairType, repair_llm_raw_output from metagpt.utils.report import ThoughtReporter +from metagpt.const import DEFAULT_WORKSPACE_ROOT @register_tool(include_functions=["ask_human", "reply_to_human"]) @@ -148,7 +149,9 @@ class RoleZero(Role): current_task=current_task, example=example, available_commands=tool_info, + pre_task = await self._parse_commands(), instruction=self.instruction.strip(), + root_direction = DEFAULT_WORKSPACE_ROOT, task_type_desc=self.task_type_desc, ) memory = self.rc.memory.get(self.memory_k) @@ -158,8 +161,17 @@ class RoleZero(Role): async with ThoughtReporter(enable_llm_stream=True) as reporter: await reporter.async_report({"type": "react"}) self.command_rsp = await self.llm.aask(context, system_msgs=self.system_msg) - self.rc.memory.add(AIMessage(content=self.command_rsp)) - + try : + commands = CodeParser.parse_code(block=None, lang="json", text=self.command_rsp) + commands = json.loads(repair_llm_raw_output(output=commands, req_keys=[None], repair_type=RepairType.JSON)) + except : + logger.warning('Trying to repair json string with repair tool...') + commands = CodeParser.parse_code(block=None, lang="json", text=self.command_rsp).strip() + if commands.endswith(']') and not commands.startswith('['): + commands = '['+ commands + self.command_rsp = f"```json\n{self.commands}\n```" + print(self.command_rsp) + self.rc.memory.add(AIMessage(content=self.command_rsp)) return True async def parse_browser_actions(self, memory: List[Message]) -> List[Message]: @@ -275,8 +287,9 @@ class RoleZero(Role): for cmd in commands: output = f"Command {cmd['command_name']} executed" # handle special command first - if await self._run_special_command(cmd): - outputs.append(output) + if self._is_special_command(cmd): + special_command_output = await self._run_special_command(cmd) + outputs.append(output+':'+special_command_output) continue # run command as specified by tool_execute_map if cmd["command_name"] in self.tool_execution_map: @@ -300,20 +313,25 @@ class RoleZero(Role): outputs = "\n\n".join(outputs) return outputs - - async def _run_special_command(self, cmd) -> bool: + def _is_special_command(self,cmd) -> bool: + + return cmd["command_name"] in self.special_tool_commands + + async def _run_special_command(self, cmd) -> str: """command requiring special check or parsing""" - is_special_cmd = cmd["command_name"] in self.special_tool_commands + command_output = "" if cmd["command_name"] == "Plan.finish_current_task" and not self.planner.plan.is_plan_finished(): # task_result = TaskResult(code=str(commands), result=outputs, is_success=is_success) # self.planner.plan.current_task.update_task_result(task_result=task_result) self.planner.plan.finish_current_task() + command_output = "Current task is finished. " elif cmd["command_name"] == "end": self._set_state(-1) + command_output = "Everything Done" - return is_special_cmd + return command_output def _get_plan_status(self) -> Tuple[str, str]: plan_status = self.planner.plan.model_dump(include=["goal", "tasks"]) diff --git a/metagpt/strategy/experience_retriever.py b/metagpt/strategy/experience_retriever.py index 668b44c40..4e9817f2c 100644 --- a/metagpt/strategy/experience_retriever.py +++ b/metagpt/strategy/experience_retriever.py @@ -585,7 +585,7 @@ Explanation: You received a message from Alice, the Product Manager, that she ha { "command_name": "RoleZero.reply_to_human", "args": { - "content": "Alice has completed the PRD. I have marked her task as finished and sent the PRD to Bob. Bob will work on the software architecture.", + "content": "Alice has completed the PRD. I have marked her task as finished and sent the PRD to Bob. Bob will work on the software architecture." } }, { @@ -610,6 +610,17 @@ Explanation: The user is asking for a general update on the project status. Give } ] ``` + +## example 4 +OBSERVATION : current task is none and all task is finished. +Explanation: Last task is "Plan.finish_current_task" and now the current task is none, it means everything is done.Just coutput command "end". +```json +[ + { + "command_name": "end" + } +] +``` """ From 94aada5e0db69e4b74b810e42c72d8fc5eadae52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E4=BC=9F=E9=9F=AC?= Date: Thu, 18 Jul 2024 14:20:26 +0800 Subject: [PATCH 112/119] =?UTF-8?q?=E4=BF=AE=E6=94=B9role=5Fzore=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E8=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- metagpt/prompts/di/role_zero.py | 6 ++++-- metagpt/roles/di/role_zero.py | 9 +++++---- metagpt/tools/libs/editor.py | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/metagpt/prompts/di/role_zero.py b/metagpt/prompts/di/role_zero.py index 243f33604..5561d0942 100644 --- a/metagpt/prompts/di/role_zero.py +++ b/metagpt/prompts/di/role_zero.py @@ -40,6 +40,7 @@ Special Command: Use {{"command_name": "end"}} to do nothing or indicate complet Pay close attention to the Example provided, you can reuse the example for your current situation if it fits. You may use any of the available commands to create a plan or update the plan. You may output mutiple commands, they will be executed sequentially. If you finish current task, you will automatically take the next task in the existing plan, use Plan.finish_task, DON'T append a new task. +Pay close attention to what you have done. Be different with your previous action. # Your commands in a json array, in the following output format with correct command_name and args. If there is nothing to do, use the pass or end command: Some text indicating your thoughts, such as how you should update the plan status, respond to inquiry, or seek for help. Then a json array of commands. You must output ONE and ONLY ONE json array. DON'T output multiple json arrays with thoughts between them. @@ -52,6 +53,7 @@ Some text indicating your thoughts, such as how you should update the plan statu ... ] ``` +Notice: your output JSON data must be a command list. Notice: your output JSON data section must start with **```json [** """ @@ -61,9 +63,9 @@ JSON_REPAIR_PROMPT = """ ## Output Format ```json -Formatted JSON data + ``` -Help check if there are any formatting issues with the JSON data? If so, please help format it +Help check if there are any formatting issues with the JSON data? If so, please help format it. """ QUICK_THINK_PROMPT = """ diff --git a/metagpt/roles/di/role_zero.py b/metagpt/roles/di/role_zero.py index c945f4d3d..c33e398be 100644 --- a/metagpt/roles/di/role_zero.py +++ b/metagpt/roles/di/role_zero.py @@ -149,13 +149,12 @@ class RoleZero(Role): current_task=current_task, example=example, available_commands=tool_info, - pre_task = await self._parse_commands(), instruction=self.instruction.strip(), - root_direction = DEFAULT_WORKSPACE_ROOT, task_type_desc=self.task_type_desc, ) memory = self.rc.memory.get(self.memory_k) memory = await self.parse_browser_actions(memory) + memory = await self.add_editor_root_directory(memory) context = self.llm.format_msg(memory + [UserMessage(content=prompt)]) # print(*context, sep="\n" + "*" * 5 + "\n") async with ThoughtReporter(enable_llm_stream=True) as reporter: @@ -173,7 +172,10 @@ class RoleZero(Role): print(self.command_rsp) self.rc.memory.add(AIMessage(content=self.command_rsp)) return True - + + async def add_editor_root_directory(self,memory) -> List[Message]: + memory.append(UserMessage(cause_by="editory", content=f'Root directory is {DEFAULT_WORKSPACE_ROOT}')) + return memory async def parse_browser_actions(self, memory: List[Message]) -> List[Message]: if not self.browser.is_empty_page: pattern = re.compile(r"Command Browser\.(\w+) executed") @@ -251,7 +253,6 @@ class RoleZero(Role): ) return rsp_msg - async def _parse_commands(self) -> Tuple[List[Dict], bool]: """Retrieves commands from the Large Language Model (LLM). diff --git a/metagpt/tools/libs/editor.py b/metagpt/tools/libs/editor.py index c9f02c4f1..edc228b12 100644 --- a/metagpt/tools/libs/editor.py +++ b/metagpt/tools/libs/editor.py @@ -38,7 +38,7 @@ class Editor: # self.resource.report(path, "path") def read(self, path: str) -> FileBlock: - """Read the whole content of a file.""" + """Read the whole content of a file.It is strongly advised to utilize absolute paths""" with open(path, "r") as f: self.resource.report(path, "path") lines = f.readlines() From 56efc6d155a89b8407cdbd59181451bf2fa6abc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E4=BC=9F=E9=9F=AC?= Date: Thu, 18 Jul 2024 14:43:59 +0800 Subject: [PATCH 113/119] =?UTF-8?q?=E4=BF=AE=E6=94=B9role=5Fzore=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E6=8C=87=E4=BB=A4=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- metagpt/roles/di/role_zero.py | 36 ++++++++++++------------ metagpt/strategy/experience_retriever.py | 4 +-- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/metagpt/roles/di/role_zero.py b/metagpt/roles/di/role_zero.py index d4bba89de..23755d3e8 100644 --- a/metagpt/roles/di/role_zero.py +++ b/metagpt/roles/di/role_zero.py @@ -10,9 +10,7 @@ from pydantic import model_validator from metagpt.actions import Action, UserRequirement from metagpt.actions.di.run_command import RunCommand -from metagpt.exp_pool import exp_cache -from metagpt.exp_pool.context_builders import RoleZeroContextBuilder -from metagpt.exp_pool.serializers import RoleZeroSerializer +from metagpt.const import DEFAULT_WORKSPACE_ROOT from metagpt.logs import logger from metagpt.prompts.di.role_zero import ( CMD_PROMPT, @@ -31,7 +29,6 @@ from metagpt.tools.tool_registry import register_tool from metagpt.utils.common import CodeParser, any_to_str from metagpt.utils.repair_llm_raw_output import RepairType, repair_llm_raw_output from metagpt.utils.report import ThoughtReporter -from metagpt.const import DEFAULT_WORKSPACE_ROOT @register_tool(include_functions=["ask_human", "reply_to_human"]) @@ -164,22 +161,24 @@ class RoleZero(Role): async with ThoughtReporter(enable_llm_stream=True) as reporter: await reporter.async_report({"type": "react"}) self.command_rsp = await self.llm.aask(context, system_msgs=self.system_msg) - try : + # check and repair + try: commands = CodeParser.parse_code(block=None, lang="json", text=self.command_rsp) commands = json.loads(repair_llm_raw_output(output=commands, req_keys=[None], repair_type=RepairType.JSON)) - except : - logger.warning('Trying to repair json string with repair tool...') + except: + logger.warning("Trying to repair json string with repair tool...") commands = CodeParser.parse_code(block=None, lang="json", text=self.command_rsp).strip() - if commands.endswith(']') and not commands.startswith('['): - commands = '['+ commands - self.command_rsp = f"```json\n{self.commands}\n```" + if commands.endswith("]") and not commands.startswith("["): + commands = "[" + commands + self.command_rsp = f"```json\n{self.command_rsp}\n```" print(self.command_rsp) - self.rc.memory.add(AIMessage(content=self.command_rsp)) + self.rc.memory.add(AIMessage(content=self.command_rsp)) return True - - async def add_editor_root_directory(self,memory) -> List[Message]: - memory.append(UserMessage(cause_by="editory", content=f'Root directory is {DEFAULT_WORKSPACE_ROOT}')) + + async def add_editor_root_directory(self, memory) -> List[Message]: + memory.append(UserMessage(cause_by="editory", content=f"Root directory is {DEFAULT_WORKSPACE_ROOT}")) return memory + async def parse_browser_actions(self, memory: List[Message]) -> List[Message]: if not self.browser.is_empty_page: pattern = re.compile(r"Command Browser\.(\w+) executed") @@ -257,6 +256,7 @@ class RoleZero(Role): ) return rsp_msg + async def _parse_commands(self) -> Tuple[List[Dict], bool]: """Retrieves commands from the Large Language Model (LLM). @@ -294,7 +294,7 @@ class RoleZero(Role): # handle special command first if self._is_special_command(cmd): special_command_output = await self._run_special_command(cmd) - outputs.append(output+':'+special_command_output) + outputs.append(output + ":" + special_command_output) continue # run command as specified by tool_execute_map if cmd["command_name"] in self.tool_execution_map: @@ -318,10 +318,10 @@ class RoleZero(Role): outputs = "\n\n".join(outputs) return outputs - def _is_special_command(self,cmd) -> bool: - + + def _is_special_command(self, cmd) -> bool: return cmd["command_name"] in self.special_tool_commands - + async def _run_special_command(self, cmd) -> str: """command requiring special check or parsing""" command_output = "" diff --git a/metagpt/strategy/experience_retriever.py b/metagpt/strategy/experience_retriever.py index ee034cb16..bb1565f66 100644 --- a/metagpt/strategy/experience_retriever.py +++ b/metagpt/strategy/experience_retriever.py @@ -520,7 +520,7 @@ Explanation: The requirement is about software development. Assign each tasks to { "command_name": "RoleZero.reply_to_human", "args": { - "content": "I have assigned the tasks to the team members. Alice will create the PRD, Bob will design the software architecture, Eve will break down the architecture into tasks, Alex will implement the core game logic, and Edward will write comprehensive tests. The team will work on the project accordingly", + "content": "I have assigned the tasks to the team members. Alice will create the PRD, Bob will design the software architecture, Eve will break down the architecture into tasks, Alex will implement the core game logic, and Edward will write comprehensive tests. The team will work on the project accordingly" } }, { @@ -602,7 +602,7 @@ Explanation: The user is asking for a general update on the project status. Give { "command_name": "RoleZero.reply_to_human", "args": { - "content": "The team is currently working on ... We have completed ...", + "content": "The team is currently working on ... We have completed ..." } }, { From 838892e17d2e0e1ceda11c890285f906c05dbd6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E4=BC=9F=E9=9F=AC?= Date: Thu, 18 Jul 2024 15:18:18 +0800 Subject: [PATCH 114/119] =?UTF-8?q?=E4=BF=AE=E6=94=B9role=5Fzero=E6=8C=87?= =?UTF-8?q?=E4=BB=A4=E5=BE=AA=E7=8E=AF=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- metagpt/environment/mgx/mgx_env.py | 2 +- metagpt/roles/di/role_zero.py | 6 ------ metagpt/strategy/experience_retriever.py | 2 +- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/metagpt/environment/mgx/mgx_env.py b/metagpt/environment/mgx/mgx_env.py index 1f74a16c8..3a492b3ac 100644 --- a/metagpt/environment/mgx/mgx_env.py +++ b/metagpt/environment/mgx/mgx_env.py @@ -91,7 +91,7 @@ class MGXEnv(Environment): async def reply_to_human(self, content: str, sent_from: Role = None) -> str: # NOTE: Can be overwritten in remote setting - return "Message send successfully." + return "The monitor has verified the message, confirmation acknowledged. Refrain from resending duplicate messages." def message_within_software_sop(self, message: Message) -> bool: # Engineer, QaEngineer can be end of the SOP. Their msg requires routing outside. diff --git a/metagpt/roles/di/role_zero.py b/metagpt/roles/di/role_zero.py index 23755d3e8..4a23110d3 100644 --- a/metagpt/roles/di/role_zero.py +++ b/metagpt/roles/di/role_zero.py @@ -10,7 +10,6 @@ from pydantic import model_validator from metagpt.actions import Action, UserRequirement from metagpt.actions.di.run_command import RunCommand -from metagpt.const import DEFAULT_WORKSPACE_ROOT from metagpt.logs import logger from metagpt.prompts.di.role_zero import ( CMD_PROMPT, @@ -155,7 +154,6 @@ class RoleZero(Role): ) memory = self.rc.memory.get(self.memory_k) memory = await self.parse_browser_actions(memory) - memory = await self.add_editor_root_directory(memory) context = self.llm.format_msg(memory + [UserMessage(content=prompt)]) # print(*context, sep="\n" + "*" * 5 + "\n") async with ThoughtReporter(enable_llm_stream=True) as reporter: @@ -175,10 +173,6 @@ class RoleZero(Role): self.rc.memory.add(AIMessage(content=self.command_rsp)) return True - async def add_editor_root_directory(self, memory) -> List[Message]: - memory.append(UserMessage(cause_by="editory", content=f"Root directory is {DEFAULT_WORKSPACE_ROOT}")) - return memory - async def parse_browser_actions(self, memory: List[Message]) -> List[Message]: if not self.browser.is_empty_page: pattern = re.compile(r"Command Browser\.(\w+) executed") diff --git a/metagpt/strategy/experience_retriever.py b/metagpt/strategy/experience_retriever.py index bb1565f66..7a943fc7f 100644 --- a/metagpt/strategy/experience_retriever.py +++ b/metagpt/strategy/experience_retriever.py @@ -613,7 +613,7 @@ Explanation: The user is asking for a general update on the project status. Give ## example 4 OBSERVATION : current task is none and all task is finished. -Explanation: Last task is "Plan.finish_current_task" and now the current task is none, it means everything is done.Just coutput command "end". +Explanation: Last task is "Plan.finish_current_task" or 'RoleZero.reply_to_human' and now the current task is none, it means everything is done.Just coutput command "end". ```json [ { From 8fec1d17758f4a5fdb5c743267de76461f1cb050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E4=BC=9F=E9=9F=AC?= Date: Thu, 18 Jul 2024 15:45:09 +0800 Subject: [PATCH 115/119] =?UTF-8?q?=E5=90=88=E5=B9=B6role=5Fzero?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- metagpt/roles/di/role_zero.py | 39 ++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/metagpt/roles/di/role_zero.py b/metagpt/roles/di/role_zero.py index 4a23110d3..40bc2e3c7 100644 --- a/metagpt/roles/di/role_zero.py +++ b/metagpt/roles/di/role_zero.py @@ -10,6 +10,9 @@ from pydantic import model_validator from metagpt.actions import Action, UserRequirement from metagpt.actions.di.run_command import RunCommand +from metagpt.exp_pool import exp_cache +from metagpt.exp_pool.context_builders import RoleZeroContextBuilder +from metagpt.exp_pool.serializers import RoleZeroSerializer from metagpt.logs import logger from metagpt.prompts.di.role_zero import ( CMD_PROMPT, @@ -154,25 +157,31 @@ class RoleZero(Role): ) memory = self.rc.memory.get(self.memory_k) memory = await self.parse_browser_actions(memory) - context = self.llm.format_msg(memory + [UserMessage(content=prompt)]) - # print(*context, sep="\n" + "*" * 5 + "\n") + + req = self.llm.format_msg(memory + [UserMessage(content=prompt)]) async with ThoughtReporter(enable_llm_stream=True) as reporter: await reporter.async_report({"type": "react"}) - self.command_rsp = await self.llm.aask(context, system_msgs=self.system_msg) - # check and repair - try: - commands = CodeParser.parse_code(block=None, lang="json", text=self.command_rsp) - commands = json.loads(repair_llm_raw_output(output=commands, req_keys=[None], repair_type=RepairType.JSON)) - except: - logger.warning("Trying to repair json string with repair tool...") - commands = CodeParser.parse_code(block=None, lang="json", text=self.command_rsp).strip() - if commands.endswith("]") and not commands.startswith("["): - commands = "[" + commands - self.command_rsp = f"```json\n{self.command_rsp}\n```" - print(self.command_rsp) + state_data = dict( + plan_status=plan_status, + current_task=current_task, + instruction=instruction, + ) + self.command_rsp = await self.llm_cached_aask(req=req, system_msgs=self.system_msg, state_data=state_data) + self.rc.memory.add(AIMessage(content=self.command_rsp)) + return True + @exp_cache(context_builder=RoleZeroContextBuilder(), serializer=RoleZeroSerializer()) + async def llm_cached_aask(self, *, req: list[dict], system_msgs: list[str], **kwargs) -> str: + """Use `exp_cache` to automatically manage experiences. + + The `RoleZeroContextBuilder` attempts to add experiences to `req`. + The `RoleZeroSerializer` extracts essential parts of `req` for the experience pool, trimming lengthy entries to retain only necessary parts. + """ + + return await self.llm.aask(req, system_msgs=system_msgs) + async def parse_browser_actions(self, memory: List[Message]) -> List[Message]: if not self.browser.is_empty_page: pattern = re.compile(r"Command Browser\.(\w+) executed") @@ -264,6 +273,8 @@ class RoleZero(Role): """ try: commands = CodeParser.parse_code(block=None, lang="json", text=self.command_rsp) + if commands.endswith("]") and not commands.startswith("["): + commands = "[" + commands commands = json.loads(repair_llm_raw_output(output=commands, req_keys=[None], repair_type=RepairType.JSON)) except json.JSONDecodeError: logger.warning(f"Failed to parse JSON for: {self.command_rsp}. Trying to repair...") From 08c4af29b5cc913627fb71c96f5172e6ca98b070 Mon Sep 17 00:00:00 2001 From: lidanyang Date: Thu, 18 Jul 2024 17:06:42 +0800 Subject: [PATCH 116/119] recover code --- metagpt/actions/di/execute_nb_code.py | 6 +++--- metagpt/tools/libs/browser.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/metagpt/actions/di/execute_nb_code.py b/metagpt/actions/di/execute_nb_code.py index dfeea7e67..f3dfd1601 100644 --- a/metagpt/actions/di/execute_nb_code.py +++ b/metagpt/actions/di/execute_nb_code.py @@ -158,7 +158,7 @@ class ExecuteNbCode(Action): else: cell["outputs"].append(new_output(output_type="stream", name="stdout", text=str(output))) - def parse_outputs(self, outputs: list[str], keep_len: int = 20000) -> Tuple[bool, str]: + def parse_outputs(self, outputs: list[str], keep_len: int = 5000) -> Tuple[bool, str]: """Parses the outputs received from notebook execution.""" assert isinstance(outputs, list) parsed_output, is_success = [], True @@ -191,8 +191,8 @@ class ExecuteNbCode(Action): output_text = remove_log_and_warning_lines(output_text) # The useful information of the exception is at the end, # the useful information of normal output is at the begining. - # if '' not in output_text: - output_text = output_text[:keep_len] if is_success else output_text[-keep_len:] + if '' not in output_text: + output_text = output_text[:keep_len] if is_success else output_text[-keep_len:] parsed_output.append(output_text) return is_success, ",".join(parsed_output) diff --git a/metagpt/tools/libs/browser.py b/metagpt/tools/libs/browser.py index 4fd6127b3..a458109e6 100644 --- a/metagpt/tools/libs/browser.py +++ b/metagpt/tools/libs/browser.py @@ -197,9 +197,9 @@ class Browser: async def _on_frame_change(self, frame: Frame): await self._update_page_last_busy_time(frame.page) - async def view(self, keep_len: int = 10000): + async def view(self): observation = parse_accessibility_tree(self.accessibility_tree) - return f"Current Browser Viewer\n URL: {self.page.url}\nOBSERVATION:\n{observation[0][:keep_len]}\n" + return f"Current Browser Viewer\n URL: {self.page.url}\nOBSERVATION:\n{observation[0]}\n" async def __aenter__(self): await self.start() From 5c758facc509b400525dc50163433e8293301c14 Mon Sep 17 00:00:00 2001 From: lidanyang Date: Thu, 18 Jul 2024 17:22:18 +0800 Subject: [PATCH 117/119] refine prompt --- metagpt/prompts/di/role_zero.py | 4 ++-- metagpt/prompts/task_type.py | 2 +- metagpt/strategy/experience_retriever.py | 10 ++-------- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/metagpt/prompts/di/role_zero.py b/metagpt/prompts/di/role_zero.py index 5f5178aee..0037d9384 100644 --- a/metagpt/prompts/di/role_zero.py +++ b/metagpt/prompts/di/role_zero.py @@ -8,7 +8,7 @@ Note: 2. Carefully review your progress at the current task, if your actions so far has not fulfilled the task instruction, you should continue with current task. Otherwise, finish current task by Plan.finish_current_task explicitly. 3. Each time you finish a task, use RoleZero.reply_to_human to report your progress. 4. Don't forget to append task first when all existing tasks are finished and new tasks are required. -5. Don't repeat the same command if it has been executed successfully. +5. Avoid repeating tasks you have already completed. And end loop when all requirements are met. """ # To ensure compatibility with hard-coded experience, do not add any other content between "# Example" and "# Available Commands". CMD_PROMPT = """ @@ -44,7 +44,7 @@ You may use any of the available commands to create a plan or update the plan. Y If you finish current task, you will automatically take the next task in the existing plan, use Plan.finish_task, DON'T append a new task. # Your commands in a json array, in the following output format with correct command_name and args. If there is nothing to do, use the pass or end command: -Some text indicating your thoughts, such as how you should update the plan status, respond to inquiry, or seek for help. Then a json array of commands. You must output ONE and ONLY ONE json array. DON'T output multiple json arrays with thoughts between them. +Some text indicating your thoughts before JSON is required, such as what tasks have been completed, what tasks are next, how you should update the plan status, respond to inquiry, or seek for help. Then a json array of commands. You must output ONE and ONLY ONE json array. DON'T output multiple json arrays with thoughts between them. ```json [ {{ diff --git a/metagpt/prompts/task_type.py b/metagpt/prompts/task_type.py index 312421c21..2e4af4c1d 100644 --- a/metagpt/prompts/task_type.py +++ b/metagpt/prompts/task_type.py @@ -56,6 +56,6 @@ The current task is about converting image into webpage code. please note the fo # Prompt for taking on "web_scraping" tasks WEB_SCRAPING_PROMPT = """ -- Remember to view and print the necessary HTML content in a separate task to understand the structure first before scraping data. +- Remember to view and print the necessary HTML content in a separate task to understand the structure first before scraping data. Such as `html_content = await view_page_element_to_scrape(...)\nprint(html_content)`. - Since the data required by user may not correspond directly to the actual HTML element names, you should thoroughly analyze the HTML structure and meanings of all elements in the executing result first. Ensure the `class_` in your code should derived from the actual HTML structure directly, not based on your knowledge. To ensure it, analyse the most suitable location of the 'class_' in the actual HTML content before code. """ diff --git a/metagpt/strategy/experience_retriever.py b/metagpt/strategy/experience_retriever.py index f667f03a5..e9adb6f10 100644 --- a/metagpt/strategy/experience_retriever.py +++ b/metagpt/strategy/experience_retriever.py @@ -944,7 +944,6 @@ Explanation: The requirement is to scrape data from a website and extract inform ## action 2 Explanation: To search for restaurants, I will now go to the website https://www.yelp.com/ first. -Here is the command to navigate to the website: ```json [ @@ -959,7 +958,6 @@ Here is the command to navigate to the website: ## action 3 Explanation: Since the Browser has successfully navigated to the website, and I find that the element id of the search box is 53. I will finish the current task and then use the Browser tool to type the keyword `beef` in the search box and press enter. -Here is the command to finish the current task and type the keyword: ```json [ @@ -980,7 +978,6 @@ Here is the command to finish the current task and type the keyword: ## action 4 Explanation: Since the Browser has successfully search the keyword `beef`, I will finish the current task and then write code to view the html content of the page. -Here is the command to finish the current task and view the html content: ```json [ @@ -996,8 +993,7 @@ Here is the command to finish the current task and view the html content: ``` ## action 5 -Explanation: Since the DataAnalyst has successfully viewed the html content of the page, I will finish the current task and then write code to parse the html content and extract the restaurant names. -Here is the command to finish the current task and parse the html content: +Explanation: Since I has successfully viewed the html content in the context, I will first finish the current task and then write code to parse the html content and extract the restaurant names. ```json [ @@ -1045,7 +1041,6 @@ Explanation: The requirement is to scrape data from a website and extract inform ## action 2 Explanation: To scrap data from the website, I will first view the html content of the page. -Here is the command to view the html content: ```json [ @@ -1057,8 +1052,7 @@ Here is the command to view the html content: ``` ## action 3 -Explanation: Since the DataAnalyst has successfully viewed the html content of the page, I will finish the current task and then write code to parse the html content and extract the restaurant names. -Here is the command to finish the current task and parse the html content: +Explanation: Since I has successfully viewed the html content in the context, I will first finish the current task and then write code to parse the html content and extract the restaurant names. ```json [ From 0181bd293f3e6489ef0e1f4a1a47da27c35e410c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E4=BC=9F=E9=9F=AC?= Date: Thu, 18 Jul 2024 18:09:03 +0800 Subject: [PATCH 118/119] formate --- metagpt/prompts/di/engineer2.py | 1 + 1 file changed, 1 insertion(+) diff --git a/metagpt/prompts/di/engineer2.py b/metagpt/prompts/di/engineer2.py index f3f2155b6..c4dec8d5e 100644 --- a/metagpt/prompts/di/engineer2.py +++ b/metagpt/prompts/di/engineer2.py @@ -11,6 +11,7 @@ EXTRA_INSTRUCTION = """ 11. Write out EVERY CODE DETAIL, DON'T LEAVE TODO. 12. To modify code in a file, read the entire file, make changes, and update the file with the complete code, ensuring that no line numbers are included in the final write. 13. When a system design or project schedule is provided, at the end of the plan, add a CodeRview Task for each file; for example, if there are three files, add three CodeRview Tasks. For each CodeRview Task, just call ReviewAndRewriteCode.run. +14. When you are making plan.it is hightly recommand to plan all the coding plan and reviews plan in first response. """ From f62e66ec586281dd5518874297c5184e56620527 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E4=BC=9F=E9=9F=AC?= Date: Thu, 18 Jul 2024 18:54:54 +0800 Subject: [PATCH 119/119] format --- metagpt/tools/libs/editor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/tools/libs/editor.py b/metagpt/tools/libs/editor.py index edc228b12..24eca65fc 100644 --- a/metagpt/tools/libs/editor.py +++ b/metagpt/tools/libs/editor.py @@ -38,7 +38,7 @@ class Editor: # self.resource.report(path, "path") def read(self, path: str) -> FileBlock: - """Read the whole content of a file.It is strongly advised to utilize absolute paths""" + """Read the whole content of a file. It is strongly advised to utilize absolute paths""" with open(path, "r") as f: self.resource.report(path, "path") lines = f.readlines()