From b9089380bdc8722581d8a9056c3456479a0161f3 Mon Sep 17 00:00:00 2001 From: shenchucheng Date: Thu, 15 Aug 2024 17:04:56 +0800 Subject: [PATCH 01/12] fix get rag llm error --- metagpt/rag/factories/llm.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/metagpt/rag/factories/llm.py b/metagpt/rag/factories/llm.py index 5d27cde3a..59f6db4d9 100644 --- a/metagpt/rag/factories/llm.py +++ b/metagpt/rag/factories/llm.py @@ -10,7 +10,7 @@ from llama_index.core.llms import ( LLMMetadata, ) from llama_index.core.llms.callbacks import llm_completion_callback -from pydantic import Field, model_validator +from pydantic import Field from metagpt.config2 import Config from metagpt.llm import LLM @@ -30,19 +30,30 @@ class RAGLLM(CustomLLM): num_output: int = -1 model_name: str = "" - @model_validator(mode="after") - def update_from_config(self): + def __init__( + self, + model_infer: BaseLLM, + context_window: int = -1, + num_output: int = -1, + model_name: str = "", + *args, + **kwargs + ): + super().__init__(*args, **kwargs) config = Config.default() - if self.context_window < 0: - self.context_window = TOKEN_MAX.get(config.llm.model, DEFAULT_CONTEXT_WINDOW) + if context_window < 0: + context_window = TOKEN_MAX.get(config.llm.model, DEFAULT_CONTEXT_WINDOW) - if self.num_output < 0: - self.num_output = config.llm.max_token + if num_output < 0: + num_output = config.llm.max_token - if not self.model_name: - self.model_name = config.llm.model + if not model_name: + model_name = config.llm.model - return self + self.model_infer = model_infer + self.context_window = context_window + self.num_output = num_output + self.model_name = model_name @property def metadata(self) -> LLMMetadata: From 2392d1e2012ae2e086ff4b047dfbb4705fa9f65a Mon Sep 17 00:00:00 2001 From: shenchucheng Date: Thu, 15 Aug 2024 18:59:23 +0800 Subject: [PATCH 02/12] fix RAGEmbeddingFactory init error --- metagpt/rag/factories/embedding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/rag/factories/embedding.py b/metagpt/rag/factories/embedding.py index 8a9d4bc95..d647883bd 100644 --- a/metagpt/rag/factories/embedding.py +++ b/metagpt/rag/factories/embedding.py @@ -29,7 +29,7 @@ class RAGEmbeddingFactory(GenericFactory): LLMType.AZURE: self._create_azure, } super().__init__(creators) - self.config = config if self.config else Config.default() + self.config = config if config else Config.default() def get_rag_embedding(self, key: EmbeddingType = None) -> BaseEmbedding: """Key is EmbeddingType.""" From c6b2fdbbd6c0a77200217eaad09999c6b8c42073 Mon Sep 17 00:00:00 2001 From: shenchucheng Date: Thu, 15 Aug 2024 19:10:22 +0800 Subject: [PATCH 03/12] fix init_exp_pool error --- examples/exp_pool/init_exp_pool.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/exp_pool/init_exp_pool.py b/examples/exp_pool/init_exp_pool.py index 1601abe0b..321c38d78 100644 --- a/examples/exp_pool/init_exp_pool.py +++ b/examples/exp_pool/init_exp_pool.py @@ -8,7 +8,7 @@ import json from pathlib import Path from metagpt.const import EXAMPLE_DATA_PATH -from metagpt.exp_pool import exp_manager +from metagpt.exp_pool import get_exp_manager from metagpt.exp_pool.schema import EntryType, Experience, Metric, Score from metagpt.logs import logger from metagpt.utils.common import aread @@ -45,7 +45,7 @@ async def add_exp(req: str, resp: str, tag: str, metric: Metric = None): tag=tag, metric=metric or Metric(score=Score(val=10, reason="Manual")), ) - + exp_manager = get_exp_manager() exp_manager.config.exp_pool.enable_write = True exp_manager.create_exp(exp) logger.info(f"New experience created for the request `{req[:10]}`.") @@ -79,7 +79,7 @@ async def add_exps_from_file(tag: str, filepath: Path): def query_exps_count(): """Queries and logs the total count of experiences in the pool.""" - + exp_manager = get_exp_manager() count = exp_manager.get_exps_count() logger.info(f"Experiences Count: {count}") From 7f3befdf056eefc6d160fe2476d113aeaf23e5c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 15 Aug 2024 19:39:25 +0800 Subject: [PATCH 04/12] =?UTF-8?q?fixbug:=20output=5Fpathname=E6=98=AF?= =?UTF-8?q?=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/exp_pool/init_exp_pool.py | 8 ++++---- examples/exp_pool/manager.py | 3 ++- metagpt/actions/design_api.py | 5 +++-- metagpt/actions/project_management.py | 3 ++- metagpt/actions/write_prd.py | 3 ++- metagpt/roles/di/team_leader.py | 4 ++-- metagpt/utils/common.py | 22 ++++++++++++++++++++++ 7 files changed, 37 insertions(+), 11 deletions(-) diff --git a/examples/exp_pool/init_exp_pool.py b/examples/exp_pool/init_exp_pool.py index 1601abe0b..42edd9637 100644 --- a/examples/exp_pool/init_exp_pool.py +++ b/examples/exp_pool/init_exp_pool.py @@ -8,7 +8,7 @@ import json from pathlib import Path from metagpt.const import EXAMPLE_DATA_PATH -from metagpt.exp_pool import exp_manager +from metagpt.exp_pool import get_exp_manager from metagpt.exp_pool.schema import EntryType, Experience, Metric, Score from metagpt.logs import logger from metagpt.utils.common import aread @@ -45,10 +45,10 @@ async def add_exp(req: str, resp: str, tag: str, metric: Metric = None): tag=tag, metric=metric or Metric(score=Score(val=10, reason="Manual")), ) - + exp_manager = get_exp_manager() exp_manager.config.exp_pool.enable_write = True exp_manager.create_exp(exp) - logger.info(f"New experience created for the request `{req[:10]}`.") + logger.info(f"New experience created for the request `{req[:10] if len(req) > 10 else req}`.") async def add_exps(exps: list, tag: str): @@ -79,7 +79,7 @@ async def add_exps_from_file(tag: str, filepath: Path): def query_exps_count(): """Queries and logs the total count of experiences in the pool.""" - + exp_manager = get_exp_manager() count = exp_manager.get_exps_count() logger.info(f"Experiences Count: {count}") diff --git a/examples/exp_pool/manager.py b/examples/exp_pool/manager.py index ae998214a..c9ec46da5 100644 --- a/examples/exp_pool/manager.py +++ b/examples/exp_pool/manager.py @@ -6,7 +6,7 @@ This script creates a new experience, logs its creation, and then queries for ex import asyncio -from metagpt.exp_pool import exp_manager +from metagpt.exp_pool import get_exp_manager from metagpt.exp_pool.schema import EntryType, Experience from metagpt.logs import logger @@ -18,6 +18,7 @@ async def main(): # Add the new experience exp = Experience(req=req, resp=resp, entry_type=EntryType.MANUAL) + exp_manager = get_exp_manager() exp_manager.create_exp(exp) logger.info(f"New experience created for the request `{req}`.") diff --git a/metagpt/actions/design_api.py b/metagpt/actions/design_api.py index 6ae21ebd9..4476e7c0a 100644 --- a/metagpt/actions/design_api.py +++ b/metagpt/actions/design_api.py @@ -32,6 +32,7 @@ from metagpt.tools.tool_registry import register_tool from metagpt.utils.common import ( aread, awrite, + rectify_pathname, save_json_to_markdown, to_markdown_code_block, ) @@ -262,10 +263,10 @@ class WriteDesign(Action): ) if not output_pathname: - output_pathname = Path(output_pathname) / "docs" / "sytem_design.json" + output_pathname = Path(output_pathname) / "docs" / "system_design.json" elif not Path(output_pathname).is_absolute(): output_pathname = self.config.workspace.path / output_pathname - output_pathname = Path(output_pathname) + output_pathname = rectify_pathname(path=output_pathname, default_filename="system_design.json") 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) diff --git a/metagpt/actions/project_management.py b/metagpt/actions/project_management.py index b9d3bc3ba..2d54ffe08 100644 --- a/metagpt/actions/project_management.py +++ b/metagpt/actions/project_management.py @@ -26,6 +26,7 @@ from metagpt.tools.tool_registry import register_tool from metagpt.utils.common import ( aread, awrite, + rectify_pathname, save_json_to_markdown, to_markdown_code_block, ) @@ -191,7 +192,7 @@ class WriteTasks(Action): output_pathname = Path(output_pathname) / "docs" / "project_schedule.json" elif not Path(output_pathname).is_absolute(): output_pathname = self.config.workspace.path / output_pathname - output_pathname = Path(output_pathname) + output_pathname = rectify_pathname(path=output_pathname, default_filename="project_schedule.json") 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) diff --git a/metagpt/actions/write_prd.py b/metagpt/actions/write_prd.py index e10e7333a..4b2015145 100644 --- a/metagpt/actions/write_prd.py +++ b/metagpt/actions/write_prd.py @@ -43,6 +43,7 @@ from metagpt.utils.common import ( CodeParser, aread, awrite, + rectify_pathname, save_json_to_markdown, to_markdown_code_block, ) @@ -314,7 +315,7 @@ class WritePRD(Action): output_pathname = self.config.workspace.path / "docs" / "prd.json" elif not Path(output_pathname).is_absolute(): output_pathname = self.config.workspace.path / output_pathname - output_pathname = Path(output_pathname) + output_pathname = rectify_pathname(path=output_pathname, default_filename="prd.json") 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)) diff --git a/metagpt/roles/di/team_leader.py b/metagpt/roles/di/team_leader.py index 4a39193a2..97100f295 100644 --- a/metagpt/roles/di/team_leader.py +++ b/metagpt/roles/di/team_leader.py @@ -47,12 +47,12 @@ class TeamLeader(RoleZero): # continue team_info += f"{role.name}: {role.profile}, {role.goal}\n" return team_info - + def _get_prefix(self) -> str: role_info = super()._get_prefix() team_info = self._get_team_info() return TL_INFO.format(role_info=role_info, team_info=team_info) - + async def _think(self) -> bool: self.instruction = TL_INSTRUCTION.format(team_info=self._get_team_info()) return await super()._think() diff --git a/metagpt/utils/common.py b/metagpt/utils/common.py index fa2f3cbbc..def127fc9 100644 --- a/metagpt/utils/common.py +++ b/metagpt/utils/common.py @@ -1118,3 +1118,25 @@ def log_time(method): return result return timeit_wrapper_async if iscoroutinefunction(method) else timeit_wrapper + + +def rectify_pathname(path: Union[str, Path], default_filename: str) -> Path: + """ + Rectifies the given path to ensure a valid output file path. + + If the given `path` is a directory, it creates the directory (if it doesn't exist) and appends the `default_filename` to it. If the `path` is a file path, it creates the parent directory (if it doesn't exist) and returns the `path`. + + Args: + path (Union[str, Path]): The input path, which can be a string or a `Path` object. + default_filename (str): The default filename to use if the `path` is a directory. + + Returns: + Path: The rectified output path. + """ + output_pathname = Path(path) + if output_pathname.is_dir(): + output_pathname.mkdir(parents=True, exist_ok=True) + output_pathname = output_pathname / default_filename + else: + output_pathname.parent.mkdir(parents=True, exist_ok=True) + return output_pathname From 646bafbe9e065556da3413c346c951c8aa5aaffb Mon Sep 17 00:00:00 2001 From: garylin2099 Date: Thu, 15 Aug 2024 20:47:09 +0800 Subject: [PATCH 05/12] doc read optimized for DA --- metagpt/prompts/di/data_analyst.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/prompts/di/data_analyst.py b/metagpt/prompts/di/data_analyst.py index f534977aa..8e5b888d3 100644 --- a/metagpt/prompts/di/data_analyst.py +++ b/metagpt/prompts/di/data_analyst.py @@ -8,7 +8,7 @@ EXTRA_INSTRUCTION = """ - For information searching requirement, you should use the Browser tool instead of web scraping. - When no link is provided, you should use the Browser tool to search for the information. 7. When you are making plan. It is highly recommend to plan and append all the tasks in first response once time, except for 7.1. -7.1. When the requirement is given with a file, read the file first through either Editor.read (write code instead for csv or excel) WITHOUT a plan. After reading the file content, use RoleZero.reply_to_human if the requirement can be answered straightaway, otherwise, make a plan if further calculation is needed. +7.1. When the requirement is inquiring about a pdf, docx, md, or txt document, read the document first through either Editor.read WITHOUT a plan. After reading the document, use RoleZero.reply_to_human if the requirement can be answered straightaway, otherwise, make a plan if further calculation is needed. 8. Don't finish_current_task multiple times for the same task. 9. Finish current task timely, such as when the code is written and executed successfully. 10. When using the command 'end', add the command 'finish_current_task' before it. From c7519462a2fbc8e4925dab7d97a8332fafb66573 Mon Sep 17 00:00:00 2001 From: shenchucheng Date: Thu, 15 Aug 2024 21:01:45 +0800 Subject: [PATCH 06/12] fix rag unittest error --- tests/metagpt/exp_pool/test_decorator.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/metagpt/exp_pool/test_decorator.py b/tests/metagpt/exp_pool/test_decorator.py index 0ca4c6ce1..9d104fca4 100644 --- a/tests/metagpt/exp_pool/test_decorator.py +++ b/tests/metagpt/exp_pool/test_decorator.py @@ -155,7 +155,10 @@ class TestExpCache: @pytest.fixture def mock_config(self, mocker): - return mocker.patch("metagpt.exp_pool.decorator.config") + config = Config.default().model_copy(deep=True) + default = mocker.patch("metagpt.config2.Config.default") + default.return_value = config + return config @pytest.mark.asyncio async def test_exp_cache_disabled(self, mock_config, mock_exp_manager): @@ -171,7 +174,9 @@ class TestExpCache: @pytest.mark.asyncio async def test_exp_cache_enabled_no_perfect_exp(self, mock_config, mock_exp_manager, mock_scorer): + mock_config.exp_pool.enabled = True mock_config.exp_pool.enable_read = True + mock_config.exp_pool.enable_write = True mock_exp_manager.query_exps.return_value = [] @exp_cache(manager=mock_exp_manager, scorer=mock_scorer) @@ -185,6 +190,7 @@ class TestExpCache: @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.enabled = True mock_config.exp_pool.enable_read = True perfect_exp = Experience(req="test", resp="perfect_result") mock_exp_manager.query_exps.return_value = [perfect_exp] From 73b242ccfe881e6a3667047bf0c37a259bd6ef7e Mon Sep 17 00:00:00 2001 From: shenchucheng Date: Thu, 15 Aug 2024 21:10:18 +0800 Subject: [PATCH 07/12] fix examples/exp_pool/manager.py error --- examples/exp_pool/manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/exp_pool/manager.py b/examples/exp_pool/manager.py index ae998214a..5aead08e9 100644 --- a/examples/exp_pool/manager.py +++ b/examples/exp_pool/manager.py @@ -6,7 +6,7 @@ This script creates a new experience, logs its creation, and then queries for ex import asyncio -from metagpt.exp_pool import exp_manager +from metagpt.exp_pool import get_exp_manager from metagpt.exp_pool.schema import EntryType, Experience from metagpt.logs import logger @@ -15,6 +15,7 @@ async def main(): # Define the simple request and response req = "Simple req" resp = "Simple resp" + exp_manager = get_exp_manager() # Add the new experience exp = Experience(req=req, resp=resp, entry_type=EntryType.MANUAL) From b53511711ebcd82e3f4bc29556387a7c9cc9ef3c Mon Sep 17 00:00:00 2001 From: shenchucheng Date: Thu, 15 Aug 2024 21:19:08 +0800 Subject: [PATCH 08/12] add thought report --- metagpt/roles/di/role_zero.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/metagpt/roles/di/role_zero.py b/metagpt/roles/di/role_zero.py index 91b39f5ba..49b6c3616 100644 --- a/metagpt/roles/di/role_zero.py +++ b/metagpt/roles/di/role_zero.py @@ -288,7 +288,9 @@ class RoleZero(Role): # routing memory = self.get_memories(k=4) # FIXME: A magic number for two rounds of Q&A context = self.llm.format_msg(memory + [UserMessage(content=QUICK_THINK_PROMPT)]) - intent_result = await self.llm.aask(context, system_msgs=[self.format_quick_system_prompt()]) + async with ThoughtReporter() as reporter: + await reporter.async_report({"type": "classify"}) + intent_result = await self.llm.aask(context, system_msgs=[self.format_quick_system_prompt()]) if "QUICK" in intent_result or "AMBIGUOUS" in intent_result: # llm call with the original context async with ThoughtReporter(enable_llm_stream=True) as reporter: From ec78ef00b53a7a8bfaef6241f1f579a0275ad6ec Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Thu, 15 Aug 2024 21:19:19 +0800 Subject: [PATCH 09/12] update exp example --- examples/exp_pool/init_exp_pool.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/exp_pool/init_exp_pool.py b/examples/exp_pool/init_exp_pool.py index 321c38d78..62747b8d8 100644 --- a/examples/exp_pool/init_exp_pool.py +++ b/examples/exp_pool/init_exp_pool.py @@ -46,6 +46,7 @@ async def add_exp(req: str, resp: str, tag: str, metric: Metric = None): metric=metric or Metric(score=Score(val=10, reason="Manual")), ) exp_manager = get_exp_manager() + exp_manager.config.exp_pool.enabled = True exp_manager.config.exp_pool.enable_write = True exp_manager.create_exp(exp) logger.info(f"New experience created for the request `{req[:10]}`.") @@ -59,8 +60,10 @@ async def add_exps(exps: list, tag: str): tag: A tag for categorizing the experiences. """ - - tasks = [add_exp(req=json.dumps(exp["req"]), resp=exp["resp"], tag=tag) for exp in exps] + tasks = [ + add_exp(req=exp["req"] if isinstance(exp["req"], str) else json.dumps(exp["req"]), resp=exp["resp"], tag=tag) + for exp in exps + ] await asyncio.gather(*tasks) From 86643b621468f42c89f8d1b2e87268371c9a1a14 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Thu, 15 Aug 2024 21:23:39 +0800 Subject: [PATCH 10/12] update comment --- metagpt/exp_pool/decorator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/metagpt/exp_pool/decorator.py b/metagpt/exp_pool/decorator.py index ed5f5e068..bb3d434c0 100644 --- a/metagpt/exp_pool/decorator.py +++ b/metagpt/exp_pool/decorator.py @@ -35,8 +35,10 @@ def exp_cache( 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. + 3. If `config.exp_pool.enabled` 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. + 5. If `config.exp_pool.enable_read` is False, the decorator will skip reading from the experience pool. + 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(). From ddd45ce0a04daedb7e5443c53f8c14daf2130be8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 15 Aug 2024 21:47:00 +0800 Subject: [PATCH 11/12] feat: -req --- examples/exp_pool/init_exp_pool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/exp_pool/init_exp_pool.py b/examples/exp_pool/init_exp_pool.py index 42edd9637..321c38d78 100644 --- a/examples/exp_pool/init_exp_pool.py +++ b/examples/exp_pool/init_exp_pool.py @@ -48,7 +48,7 @@ async def add_exp(req: str, resp: str, tag: str, metric: Metric = None): exp_manager = get_exp_manager() exp_manager.config.exp_pool.enable_write = True exp_manager.create_exp(exp) - logger.info(f"New experience created for the request `{req[:10] if len(req) > 10 else req}`.") + logger.info(f"New experience created for the request `{req[:10]}`.") async def add_exps(exps: list, tag: str): From 2d5c4413581fef71a19c90bd755ab1edee9c2a7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Thu, 15 Aug 2024 22:10:03 +0800 Subject: [PATCH 12/12] feat: -exp_manager = get_exp_manager() --- examples/exp_pool/manager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/exp_pool/manager.py b/examples/exp_pool/manager.py index 94969a38b..c9ec46da5 100644 --- a/examples/exp_pool/manager.py +++ b/examples/exp_pool/manager.py @@ -15,7 +15,6 @@ async def main(): # Define the simple request and response req = "Simple req" resp = "Simple resp" - exp_manager = get_exp_manager() # Add the new experience exp = Experience(req=req, resp=resp, entry_type=EntryType.MANUAL)