From 2f3e4c7f1555745718bcfab002723c2d4fc654df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Tue, 2 Jan 2024 11:59:03 +0800 Subject: [PATCH 01/23] feat: +unit test --- metagpt/roles/role.py | 33 -------------------- metagpt/utils/redis.py | 13 ++++++-- tests/metagpt/test_role.py | 51 +++++++++++++++++++++++++++++-- tests/metagpt/test_schema.py | 24 +++++++++++++++ tests/metagpt/utils/test_redis.py | 8 +++++ tests/metagpt/utils/test_s3.py | 10 +++--- 6 files changed, 96 insertions(+), 43 deletions(-) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index 81815e91b..f74c32fea 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -372,16 +372,6 @@ class Role(SerializationMixin, is_polymorphic_base=True): return msg - def _find_news(self, observed: list[Message], existed: list[Message]) -> list[Message]: - news = [] - # Warning, remove `id` here to make it work for recover - observed_pure = [msg.dict(exclude={"id": True}) for msg in observed] - existed_pure = [msg.dict(exclude={"id": True}) for msg in existed] - for idx, new in enumerate(observed_pure): - if (new["cause_by"] in self.rc.watch or self.name in new["send_to"]) and new not in existed_pure: - news.append(observed[idx]) - return news - async def _observe(self, ignore_memory=False) -> int: """Prepare new messages for processing from the message buffer and other sources.""" # Read unprocessed messages from the msg buffer. @@ -407,29 +397,6 @@ class Role(SerializationMixin, is_polymorphic_base=True): logger.debug(f"{self._setting} observed: {news_text}") return len(self.rc.news) - # async def _observe(self, ignore_memory=False) -> int: - # """Prepare new messages for processing from the message buffer and other sources.""" - # # Read unprocessed messages from the msg buffer. - # news = self.rc.msg_buffer.pop_all() - # if self.recovered: - # news = [self.latest_observed_msg] if self.latest_observed_msg else [] - # else: - # self.latest_observed_msg = news[-1] if len(news) > 0 else None # record the latest observed msg - # - # # Store the read messages in your own memory to prevent duplicate processing. - # old_messages = [] if ignore_memory else self.rc.memory.get() - # self.rc.memory.add_batch(news) - # # Filter out messages of interest. - # self.rc.news = self._find_news(news, old_messages) - # - # # Design Rules: - # # If you need to further categorize Message objects, you can do so using the Message.set_meta function. - # # msg_buffer is a receiving buffer, avoid adding message data and operations to msg_buffer. - # news_text = [f"{i.role}: {i.content[:20]}..." for i in self.rc.news] - # if news_text: - # logger.debug(f"{self._setting} observed: {news_text}") - # return len(self.rc.news) - def publish_message(self, msg): """If the role belongs to env, then the role's messages will be broadcast to env""" if not msg: diff --git a/metagpt/utils/redis.py b/metagpt/utils/redis.py index 1ad39be59..e4b455c6b 100644 --- a/metagpt/utils/redis.py +++ b/metagpt/utils/redis.py @@ -5,6 +5,7 @@ @Author : mashenquan @File : redis.py """ +from __future__ import annotations import traceback from datetime import timedelta @@ -22,7 +23,15 @@ class Redis: async def _connect(self, force=False): if self._client and not force: return True - if not CONFIG.REDIS_HOST or not CONFIG.REDIS_PORT or CONFIG.REDIS_DB is None or CONFIG.REDIS_PASSWORD is None: + is_ready = ( + CONFIG.REDIS_HOST + and CONFIG.REDIS_HOST != "YOUR_REDIS_HOST" + and CONFIG.REDIS_PORT + and CONFIG.REDIS_PORT != "YOUR_REDIS_PORT" + and CONFIG.REDIS_DB is not None + and CONFIG.REDIS_PASSWORD is not None + ) + if not is_ready: return False try: @@ -37,7 +46,7 @@ class Redis: logger.warning(f"Redis initialization has failed:{e}") return False - async def get(self, key: str) -> bytes: + async def get(self, key: str) -> bytes | None: if not await self._connect() or not key: return None try: diff --git a/tests/metagpt/test_role.py b/tests/metagpt/test_role.py index 33320715c..52d08e92e 100644 --- a/tests/metagpt/test_role.py +++ b/tests/metagpt/test_role.py @@ -10,15 +10,17 @@ functionality is to be consolidated into the `Environment` class. """ import uuid +from unittest.mock import MagicMock import pytest from pydantic import BaseModel from metagpt.actions import Action, ActionOutput, UserRequirement from metagpt.environment import Environment +from metagpt.provider.base_llm import BaseLLM from metagpt.roles import Role from metagpt.schema import Message -from metagpt.utils.common import any_to_str +from metagpt.utils.common import any_to_name, any_to_str class MockAction(Action): @@ -96,7 +98,7 @@ async def test_react(): @pytest.mark.asyncio -async def test_msg_to(): +async def test_send_to(): m = Message(content="a", send_to=["a", MockRole, Message]) assert m.send_to == {"a", any_to_str(MockRole), any_to_str(Message)} @@ -107,5 +109,50 @@ async def test_msg_to(): assert m.send_to == {"a", any_to_str(MockRole), any_to_str(Message)} +def test_init_action(): + role = Role() + role.init_actions([MockAction, MockAction]) + assert role.action_count == 2 + + +@pytest.mark.asyncio +async def test_recover(): + # Mock LLM actions + mock_llm = MagicMock(spec=BaseLLM) + mock_llm.aask.side_effect = ["1"] + + role = Role() + assert role.is_watch(any_to_str(UserRequirement)) + role.put_message(None) + role.publish_message(None) + + role.llm = mock_llm + role.init_actions([MockAction, MockAction]) + role.recovered = True + role.latest_observed_msg = Message(content="recover_test") + role.rc.state = 0 + assert role.todo == any_to_name(MockAction) + + rsp = await role.run() + assert rsp.cause_by == any_to_str(MockAction) + + +@pytest.mark.asyncio +async def test_think_act(): + # Mock LLM actions + mock_llm = MagicMock(spec=BaseLLM) + mock_llm.aask.side_effect = ["ok"] + + role = Role() + role.init_actions([MockAction]) + await role.think() + role.rc.memory.add(Message("run")) + assert len(role.get_memories()) == 1 + rsp = await role.act() + assert rsp + assert isinstance(rsp, ActionOutput) + assert rsp.content == "run" + + if __name__ == "__main__": pytest.main([__file__, "-s"]) diff --git a/tests/metagpt/test_schema.py b/tests/metagpt/test_schema.py index 1bf0d4c4c..816c186e2 100644 --- a/tests/metagpt/test_schema.py +++ b/tests/metagpt/test_schema.py @@ -16,8 +16,10 @@ from metagpt.actions import Action from metagpt.actions.action_node import ActionNode from metagpt.actions.write_code import WriteCode from metagpt.config import CONFIG +from metagpt.const import SYSTEM_DESIGN_FILE_REPO, TASK_FILE_REPO from metagpt.schema import ( AIMessage, + CodeSummarizeContext, Document, Message, MessageQueue, @@ -61,6 +63,8 @@ def test_message(): assert m.role == "b" assert m.send_to == {"c"} assert m.cause_by == "c" + m.sent_from = "e" + assert m.sent_from == "e" m.cause_by = "Message" assert m.cause_by == "Message" @@ -121,6 +125,8 @@ def test_document(): @pytest.mark.asyncio async def test_message_queue(): mq = MessageQueue() + val = await mq.dump() + assert val == "[]" mq.push(Message(content="1")) mq.push(Message(content="2中文测试aaa")) msg = mq.pop() @@ -132,5 +138,23 @@ async def test_message_queue(): assert new_mq.pop_all() == mq.pop_all() +@pytest.mark.parametrize( + ("file_list", "want"), + [ + ( + [f"{SYSTEM_DESIGN_FILE_REPO}/a.txt", f"{TASK_FILE_REPO}/b.txt"], + CodeSummarizeContext( + design_filename=f"{SYSTEM_DESIGN_FILE_REPO}/a.txt", task_filename=f"{TASK_FILE_REPO}/b.txt" + ), + ) + ], +) +def test_CodeSummarizeContext(file_list, want): + ctx = CodeSummarizeContext.loads(file_list) + assert ctx == want + m = {ctx: ctx} + assert want in m + + if __name__ == "__main__": pytest.main([__file__, "-s"]) diff --git a/tests/metagpt/utils/test_redis.py b/tests/metagpt/utils/test_redis.py index 7c3fd26a9..a75341433 100644 --- a/tests/metagpt/utils/test_redis.py +++ b/tests/metagpt/utils/test_redis.py @@ -27,6 +27,14 @@ async def test_redis(): assert await conn.get("test") == b"test" await conn.close() + key = CONFIG.REDIS_HOST + CONFIG.REDIS_HOST = "YOUR_REDIS_HOST" + conn = Redis() + await conn.set("test", "test", timeout_sec=0) + assert not await conn.get("test") == b"test" + CONFIG.REDIS_HOST = key + await conn.close() + if __name__ == "__main__": pytest.main([__file__, "-s"]) diff --git a/tests/metagpt/utils/test_s3.py b/tests/metagpt/utils/test_s3.py index edf198028..9906d566f 100644 --- a/tests/metagpt/utils/test_s3.py +++ b/tests/metagpt/utils/test_s3.py @@ -41,17 +41,15 @@ async def test_s3(): res = await conn.cache(data, ".bak", "script") assert "http" in res - -@pytest.mark.asyncio -async def test_s3_no_error(): + key = CONFIG.S3_ACCESS_KEY + CONFIG.S3_ACCESS_KEY = "YOUR_S3_ACCESS_KEY" conn = S3() - key = conn.auth_config["aws_secret_access_key"] - conn.auth_config["aws_secret_access_key"] = "" + assert not conn.is_valid try: res = await conn.cache("ABC", ".bak", "script") assert not res finally: - conn.auth_config["aws_secret_access_key"] = key + CONFIG.S3_ACCESS_KEY = key if __name__ == "__main__": From d9c5809ccd6acc74aea3307c9ef0f5069f530c61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Tue, 2 Jan 2024 14:21:55 +0800 Subject: [PATCH 02/23] feat: +qa unit test --- tests/data/demo_project/game.py | 92 +++++++++++++++++++++++++ tests/metagpt/roles/test_qa_engineer.py | 56 +++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 tests/data/demo_project/game.py diff --git a/tests/data/demo_project/game.py b/tests/data/demo_project/game.py new file mode 100644 index 000000000..22e77b260 --- /dev/null +++ b/tests/data/demo_project/game.py @@ -0,0 +1,92 @@ +## game.py + +import random +from typing import List, Tuple + + +class Game: + def __init__(self): + self.grid: List[List[int]] = [[0 for _ in range(4)] for _ in range(4)] + self.score: int = 0 + self.game_over: bool = False + + def reset_game(self): + self.grid = [[0 for _ in range(4)] for _ in range(4)] + self.score = 0 + self.game_over = False + self.add_new_tile() + self.add_new_tile() + + def move(self, direction: str): + if direction == "up": + self._move_up() + elif direction == "down": + self._move_down() + elif direction == "left": + self._move_left() + elif direction == "right": + self._move_right() + + def is_game_over(self) -> bool: + for i in range(4): + for j in range(4): + if self.grid[i][j] == 0: + return False + if j < 3 and self.grid[i][j] == self.grid[i][j + 1]: + return False + if i < 3 and self.grid[i][j] == self.grid[i + 1][j]: + return False + return True + + def get_empty_cells(self) -> List[Tuple[int, int]]: + empty_cells = [] + for i in range(4): + for j in range(4): + if self.grid[i][j] == 0: + empty_cells.append((i, j)) + return empty_cells + + def add_new_tile(self): + empty_cells = self.get_empty_cells() + if empty_cells: + x, y = random.choice(empty_cells) + self.grid[x][y] = 2 if random.random() < 0.9 else 4 + + def get_score(self) -> int: + return self.score + + def _move_up(self): + for j in range(4): + for i in range(1, 4): + if self.grid[i][j] != 0: + for k in range(i, 0, -1): + if self.grid[k - 1][j] == 0: + self.grid[k - 1][j] = self.grid[k][j] + self.grid[k][j] = 0 + + def _move_down(self): + for j in range(4): + for i in range(2, -1, -1): + if self.grid[i][j] != 0: + for k in range(i, 3): + if self.grid[k + 1][j] == 0: + self.grid[k + 1][j] = self.grid[k][j] + self.grid[k][j] = 0 + + def _move_left(self): + for i in range(4): + for j in range(1, 4): + if self.grid[i][j] != 0: + for k in range(j, 0, -1): + if self.grid[i][k - 1] == 0: + self.grid[i][k - 1] = self.grid[i][k] + self.grid[i][k] = 0 + + def _move_right(self): + for i in range(4): + for j in range(2, -1, -1): + if self.grid[i][j] != 0: + for k in range(j, 3): + if self.grid[i][k + 1] == 0: + self.grid[i][k + 1] = self.grid[i][k] + self.grid[i][k] = 0 diff --git a/tests/metagpt/roles/test_qa_engineer.py b/tests/metagpt/roles/test_qa_engineer.py index 8fd7c0373..784c26a06 100644 --- a/tests/metagpt/roles/test_qa_engineer.py +++ b/tests/metagpt/roles/test_qa_engineer.py @@ -5,3 +5,59 @@ @Author : alexanderwu @File : test_qa_engineer.py """ +from pathlib import Path +from typing import List + +import pytest +from pydantic import Field + +from metagpt.actions import DebugError, RunCode, WriteTest +from metagpt.actions.summarize_code import SummarizeCode +from metagpt.config import CONFIG +from metagpt.environment import Environment +from metagpt.roles import QaEngineer +from metagpt.schema import Message +from metagpt.utils.common import any_to_str, aread, awrite + + +async def test_qa(): + # Prerequisites + demo_path = Path(__file__).parent / "../../data/demo_project" + CONFIG.src_workspace = Path(CONFIG.git_repo.workdir) / "qa/game_2048" + data = await aread(filename=demo_path / "game.py", encoding="utf-8") + await awrite(filename=CONFIG.src_workspace / "game.py", data=data, encoding="utf-8") + await awrite(filename=Path(CONFIG.git_repo.workdir) / "requirements.txt", data="") + + class MockEnv(Environment): + msgs: List[Message] = Field(default_factory=list) + + def publish_message(self, message: Message, peekable: bool = True) -> bool: + self.msgs.append(message) + return True + + env = MockEnv() + + role = QaEngineer() + role.set_env(env) + await role.run(with_message=Message(content="", cause_by=SummarizeCode)) + assert env.msgs + assert env.msgs[0].cause_by == any_to_str(WriteTest) + msg = env.msgs[0] + env.msgs.clear() + await role.run(with_message=msg) + assert env.msgs + assert env.msgs[0].cause_by == any_to_str(RunCode) + msg = env.msgs[0] + env.msgs.clear() + await role.run(with_message=msg) + assert env.msgs + assert env.msgs[0].cause_by == any_to_str(DebugError) + msg = env.msgs[0] + env.msgs.clear() + role.test_round_allowed = 1 + rsp = await role.run(with_message=msg) + assert "Exceeding" in rsp.content + + +if __name__ == "__main__": + pytest.main([__file__, "-s"]) From 786f862a8bc24ca38966f8ef3d63da50be3373e3 Mon Sep 17 00:00:00 2001 From: geekan Date: Tue, 2 Jan 2024 15:16:59 +0800 Subject: [PATCH 03/23] fix azure --- metagpt/provider/azure_openai_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/provider/azure_openai_api.py b/metagpt/provider/azure_openai_api.py index b59326c7f..d15d1c82e 100644 --- a/metagpt/provider/azure_openai_api.py +++ b/metagpt/provider/azure_openai_api.py @@ -27,7 +27,7 @@ class AzureOpenAILLM(OpenAILLM): def _init_client(self): kwargs = self._make_client_kwargs() # https://learn.microsoft.com/zh-cn/azure/ai-services/openai/how-to/migration?tabs=python-new%2Cdalle-fix - self.async_client = AsyncAzureOpenAI(**kwargs) + self.aclient = AsyncAzureOpenAI(**kwargs) self.model = self.config.DEPLOYMENT_NAME # Used in _calc_usage & _cons_kwargs def _make_client_kwargs(self) -> dict: From fe5e5005015d9a26df26242382ee5bea69720ade Mon Sep 17 00:00:00 2001 From: geekan Date: Tue, 2 Jan 2024 15:26:23 +0800 Subject: [PATCH 04/23] add comments to SerializationMixin --- metagpt/schema.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/metagpt/schema.py b/metagpt/schema.py index 91158ffeb..e36bef395 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -55,7 +55,16 @@ from metagpt.utils.serialize import ( class SerializationMixin(BaseModel): - """SereDeserMixin for subclass' ser&deser""" + """ + PolyMorphic subclasses Serialization / Deserialization Mixin + - First of all, we need to know that pydantic is not designed for polymorphism. + - If Engineer is subclass of Role, it would be serialized as Role. If we want to serialize it as Engineer, we need + to add `class name` to Engineer. So we need Engineer inherit SerializationMixin. + + More details: + - https://docs.pydantic.dev/latest/concepts/serialization/ + - https://github.com/pydantic/pydantic/discussions/7008 discuss about avoid `__get_pydantic_core_schema__` + """ __is_polymorphic_base = False __subclasses_map__ = {} From 939f807677db03e052aaeedcad866c172c7c9147 Mon Sep 17 00:00:00 2001 From: better629 Date: Tue, 2 Jan 2024 15:37:49 +0800 Subject: [PATCH 05/23] fix AzureOpenAILLM --- metagpt/provider/azure_openai_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/provider/azure_openai_api.py b/metagpt/provider/azure_openai_api.py index b59326c7f..d15d1c82e 100644 --- a/metagpt/provider/azure_openai_api.py +++ b/metagpt/provider/azure_openai_api.py @@ -27,7 +27,7 @@ class AzureOpenAILLM(OpenAILLM): def _init_client(self): kwargs = self._make_client_kwargs() # https://learn.microsoft.com/zh-cn/azure/ai-services/openai/how-to/migration?tabs=python-new%2Cdalle-fix - self.async_client = AsyncAzureOpenAI(**kwargs) + self.aclient = AsyncAzureOpenAI(**kwargs) self.model = self.config.DEPLOYMENT_NAME # Used in _calc_usage & _cons_kwargs def _make_client_kwargs(self) -> dict: From b7d74c64836f6b6ab293a9952a5b8fe04c6613b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Tue, 2 Jan 2024 15:31:49 +0800 Subject: [PATCH 06/23] fixbug: azure openai --- metagpt/provider/azure_openai_api.py | 2 +- metagpt/utils/redis.py | 21 +++++++++-------- metagpt/utils/s3.py | 25 +++++++++++---------- tests/metagpt/learn/test_text_to_image.py | 12 ++++++---- tests/metagpt/learn/test_text_to_speech.py | 9 +++++--- tests/metagpt/roles/test_architect.py | 26 +++++++++++++++++++--- tests/metagpt/utils/test_redis.py | 19 ++++++++++------ tests/metagpt/utils/test_s3.py | 13 ++++++----- 8 files changed, 83 insertions(+), 44 deletions(-) diff --git a/metagpt/provider/azure_openai_api.py b/metagpt/provider/azure_openai_api.py index b59326c7f..d15d1c82e 100644 --- a/metagpt/provider/azure_openai_api.py +++ b/metagpt/provider/azure_openai_api.py @@ -27,7 +27,7 @@ class AzureOpenAILLM(OpenAILLM): def _init_client(self): kwargs = self._make_client_kwargs() # https://learn.microsoft.com/zh-cn/azure/ai-services/openai/how-to/migration?tabs=python-new%2Cdalle-fix - self.async_client = AsyncAzureOpenAI(**kwargs) + self.aclient = AsyncAzureOpenAI(**kwargs) self.model = self.config.DEPLOYMENT_NAME # Used in _calc_usage & _cons_kwargs def _make_client_kwargs(self) -> dict: diff --git a/metagpt/utils/redis.py b/metagpt/utils/redis.py index e4b455c6b..10f33285c 100644 --- a/metagpt/utils/redis.py +++ b/metagpt/utils/redis.py @@ -23,15 +23,7 @@ class Redis: async def _connect(self, force=False): if self._client and not force: return True - is_ready = ( - CONFIG.REDIS_HOST - and CONFIG.REDIS_HOST != "YOUR_REDIS_HOST" - and CONFIG.REDIS_PORT - and CONFIG.REDIS_PORT != "YOUR_REDIS_PORT" - and CONFIG.REDIS_DB is not None - and CONFIG.REDIS_PASSWORD is not None - ) - if not is_ready: + if not self.is_configured: return False try: @@ -74,3 +66,14 @@ class Redis: @property def is_valid(self) -> bool: return self._client is not None + + @property + def is_configured(self) -> bool: + return bool( + CONFIG.REDIS_HOST + and CONFIG.REDIS_HOST != "YOUR_REDIS_HOST" + and CONFIG.REDIS_PORT + and CONFIG.REDIS_PORT != "YOUR_REDIS_PORT" + and CONFIG.REDIS_DB is not None + and CONFIG.REDIS_PASSWORD is not None + ) diff --git a/metagpt/utils/s3.py b/metagpt/utils/s3.py index 6a38a80a4..2a2c1a31c 100644 --- a/metagpt/utils/s3.py +++ b/metagpt/utils/s3.py @@ -154,16 +154,17 @@ class S3: @property def is_valid(self): - is_invalid = ( - not CONFIG.S3_ACCESS_KEY - or CONFIG.S3_ACCESS_KEY == "YOUR_S3_ACCESS_KEY" - or not CONFIG.S3_SECRET_KEY - or CONFIG.S3_SECRET_KEY == "YOUR_S3_SECRET_KEY" - or not CONFIG.S3_ENDPOINT_URL - or CONFIG.S3_ENDPOINT_URL == "YOUR_S3_ENDPOINT_URL" - or not CONFIG.S3_BUCKET - or CONFIG.S3_BUCKET == "YOUR_S3_BUCKET" + return self.is_configured + + @property + def is_configured(self) -> bool: + return bool( + CONFIG.S3_ACCESS_KEY + and CONFIG.S3_ACCESS_KEY != "YOUR_S3_ACCESS_KEY" + and CONFIG.S3_SECRET_KEY + and CONFIG.S3_SECRET_KEY != "YOUR_S3_SECRET_KEY" + and CONFIG.S3_ENDPOINT_URL + and CONFIG.S3_ENDPOINT_URL != "YOUR_S3_ENDPOINT_URL" + and CONFIG.S3_BUCKET + and CONFIG.S3_BUCKET != "YOUR_S3_BUCKET" ) - if is_invalid: - logger.info("S3 is invalid") - return not is_invalid diff --git a/tests/metagpt/learn/test_text_to_image.py b/tests/metagpt/learn/test_text_to_image.py index 0afe8534d..760b9d09c 100644 --- a/tests/metagpt/learn/test_text_to_image.py +++ b/tests/metagpt/learn/test_text_to_image.py @@ -15,20 +15,24 @@ from metagpt.learn.text_to_image import text_to_image @pytest.mark.asyncio -async def test(): +async def test_metagpt_llm(): # Prerequisites assert CONFIG.METAGPT_TEXT_TO_IMAGE_MODEL_URL assert CONFIG.OPENAI_API_KEY data = await text_to_image("Panda emoji", size_type="512x512") assert "base64" in data or "http" in data - key = CONFIG.METAGPT_TEXT_TO_IMAGE_MODEL_URL - CONFIG.METAGPT_TEXT_TO_IMAGE_MODEL_URL = None + + # Mock session env + old_options = CONFIG.options.copy() + new_options = old_options.copy() + new_options["METAGPT_TEXT_TO_IMAGE_MODEL_URL"] = None + CONFIG.set_context(new_options) try: data = await text_to_image("Panda emoji", size_type="512x512") assert "base64" in data or "http" in data finally: - CONFIG.METAGPT_TEXT_TO_IMAGE_MODEL_URL = key + CONFIG.set_context(old_options) if __name__ == "__main__": diff --git a/tests/metagpt/learn/test_text_to_speech.py b/tests/metagpt/learn/test_text_to_speech.py index 02faecdde..aca08b9a2 100644 --- a/tests/metagpt/learn/test_text_to_speech.py +++ b/tests/metagpt/learn/test_text_to_speech.py @@ -27,13 +27,16 @@ async def test_text_to_speech(): assert "base64" in data or "http" in data # test iflytek - key = CONFIG.AZURE_TTS_SUBSCRIPTION_KEY - CONFIG.AZURE_TTS_SUBSCRIPTION_KEY = "" + ## Mock session env + old_options = CONFIG.options.copy() + new_options = old_options.copy() + new_options["AZURE_TTS_SUBSCRIPTION_KEY"] = "" + CONFIG.set_context(new_options) try: data = await text_to_speech("panda emoji") assert "base64" in data or "http" in data finally: - CONFIG.AZURE_TTS_SUBSCRIPTION_KEY = key + CONFIG.set_context(old_options) if __name__ == "__main__": diff --git a/tests/metagpt/roles/test_architect.py b/tests/metagpt/roles/test_architect.py index 0c8fbfe04..06e4b2d11 100644 --- a/tests/metagpt/roles/test_architect.py +++ b/tests/metagpt/roles/test_architect.py @@ -7,18 +7,38 @@ @Modified By: mashenquan, 2023-11-1. In accordance with Chapter 2.2.1 and 2.2.2 of RFC 116, utilize the new message distribution feature for message handling. """ +import uuid + import pytest +from metagpt.actions import WriteDesign, WritePRD +from metagpt.config import CONFIG +from metagpt.const import PRDS_FILE_REPO from metagpt.logs import logger from metagpt.roles import Architect +from metagpt.schema import Message +from metagpt.utils.common import any_to_str, awrite from tests.metagpt.roles.mock import MockMessages @pytest.mark.asyncio async def test_architect(): - # FIXME: make git as env? Or should we support + # Prerequisites + filename = uuid.uuid4().hex + ".json" + await awrite(CONFIG.git_repo.workdir / PRDS_FILE_REPO / filename, data=MockMessages.prd.content) + role = Architect() - role.put_message(MockMessages.req) - rsp = await role.run(MockMessages.prd) + rsp = await role.run(with_message=Message(content="", cause_by=WritePRD)) logger.info(rsp) assert len(rsp.content) > 0 + assert rsp.cause_by == any_to_str(WriteDesign) + + # test update + rsp = await role.run(with_message=Message(content="", cause_by=WritePRD)) + assert rsp + assert rsp.cause_by == any_to_str(WriteDesign) + assert len(rsp.content) > 0 + + +if __name__ == "__main__": + pytest.main([__file__, "-s"]) diff --git a/tests/metagpt/utils/test_redis.py b/tests/metagpt/utils/test_redis.py index a75341433..b93ff0cdb 100644 --- a/tests/metagpt/utils/test_redis.py +++ b/tests/metagpt/utils/test_redis.py @@ -27,13 +27,18 @@ async def test_redis(): assert await conn.get("test") == b"test" await conn.close() - key = CONFIG.REDIS_HOST - CONFIG.REDIS_HOST = "YOUR_REDIS_HOST" - conn = Redis() - await conn.set("test", "test", timeout_sec=0) - assert not await conn.get("test") == b"test" - CONFIG.REDIS_HOST = key - await conn.close() + # Mock session env + old_options = CONFIG.options.copy() + new_options = old_options.copy() + new_options["REDIS_HOST"] = "YOUR_REDIS_HOST" + CONFIG.set_context(new_options) + try: + conn = Redis() + await conn.set("test", "test", timeout_sec=0) + assert not await conn.get("test") == b"test" + await conn.close() + finally: + CONFIG.set_context(old_options) if __name__ == "__main__": diff --git a/tests/metagpt/utils/test_s3.py b/tests/metagpt/utils/test_s3.py index 9906d566f..f74e7b52a 100644 --- a/tests/metagpt/utils/test_s3.py +++ b/tests/metagpt/utils/test_s3.py @@ -41,15 +41,18 @@ async def test_s3(): res = await conn.cache(data, ".bak", "script") assert "http" in res - key = CONFIG.S3_ACCESS_KEY - CONFIG.S3_ACCESS_KEY = "YOUR_S3_ACCESS_KEY" - conn = S3() - assert not conn.is_valid + # Mock session env + old_options = CONFIG.options.copy() + new_options = old_options.copy() + new_options["S3_ACCESS_KEY"] = "YOUR_S3_ACCESS_KEY" + CONFIG.set_context(new_options) try: + conn = S3() + assert not conn.is_valid res = await conn.cache("ABC", ".bak", "script") assert not res finally: - CONFIG.S3_ACCESS_KEY = key + CONFIG.set_context(old_options) if __name__ == "__main__": From ea64e6ad47647b9befa15220cee69a3148626ad2 Mon Sep 17 00:00:00 2001 From: geekan Date: Tue, 2 Jan 2024 16:28:03 +0800 Subject: [PATCH 07/23] add comments --- tests/conftest.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 54a042e90..d88b31ce5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -27,6 +27,10 @@ class Context: @property def llm_api(self): + # 1. 初始化llm,带有缓存结果 + # 2. 如果缓存query,那么直接返回缓存结果 + # 3. 如果没有缓存query,那么调用llm_api,返回结果 + # 4. 如果有缓存query,那么更新缓存结果 return self._llm_api From 5649fac62dca8a3e24439edb70ff9a4a20096735 Mon Sep 17 00:00:00 2001 From: geekan Date: Tue, 2 Jan 2024 18:14:03 +0800 Subject: [PATCH 08/23] fix pylint warnings --- metagpt/roles/role.py | 2 +- metagpt/utils/common.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index f74c32fea..356b9e33f 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -152,7 +152,7 @@ class Role(SerializationMixin, is_polymorphic_base=True): __hash__ = object.__hash__ # support Role as hashable type in `Environment.members` @model_validator(mode="after") - def check_subscription(self) -> set: + def check_subscription(self): if not self.subscription: self.subscription = {any_to_str(self), self.name} if self.name else {any_to_str(self)} return self diff --git a/metagpt/utils/common.py b/metagpt/utils/common.py index 5999b2e11..60acd7e3c 100644 --- a/metagpt/utils/common.py +++ b/metagpt/utils/common.py @@ -23,7 +23,7 @@ import sys import traceback import typing from pathlib import Path -from typing import Any, Callable, List, Tuple, Union, get_args, get_origin +from typing import Any, List, Tuple, Union, get_args, get_origin import aiofiles import loguru @@ -365,14 +365,14 @@ def get_class_name(cls) -> str: return f"{cls.__module__}.{cls.__name__}" -def any_to_str(val: str | Callable) -> str: +def any_to_str(val: Any) -> str: """Return the class name or the class name of the object, or 'val' if it's a string type.""" if isinstance(val, str): return val - if not callable(val): + elif not callable(val): return get_class_name(type(val)) - - return get_class_name(val) + else: + return get_class_name(val) def any_to_str_set(val) -> set: From 0b9becf93f2a84b2ee1103851834e8d384c77f07 Mon Sep 17 00:00:00 2001 From: geekan Date: Tue, 2 Jan 2024 19:27:42 +0800 Subject: [PATCH 09/23] fix pydantic v2 model validation for custom class --- metagpt/actions/action_node.py | 23 +++++++++-------------- tests/metagpt/actions/test_action_node.py | 17 +++++++++++++++-- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/metagpt/actions/action_node.py b/metagpt/actions/action_node.py index 4c06d0d1d..6c65b33ef 100644 --- a/metagpt/actions/action_node.py +++ b/metagpt/actions/action_node.py @@ -11,7 +11,7 @@ NOTE: You should use typing.List instead of list to do type annotation. Because import json from typing import Any, Dict, List, Optional, Tuple, Type -from pydantic import BaseModel, create_model, field_validator, model_validator +from pydantic import BaseModel, create_model, model_validator from tenacity import retry, stop_after_attempt, wait_random_exponential from metagpt.config import CONFIG @@ -135,26 +135,21 @@ class ActionNode: @classmethod def create_model_class(cls, class_name: str, mapping: Dict[str, Tuple[Type, Any]]): """基于pydantic v1的模型动态生成,用来检验结果类型正确性""" - new_class = create_model(class_name, **mapping) - @field_validator("*", mode="before") - @classmethod - def check_name(v, field): - if field.name not in mapping.keys(): - raise ValueError(f"Unrecognized block: {field.name}") - return v - - @model_validator(mode="before") - @classmethod - def check_missing_fields(values): + def check_fields(cls, values): required_fields = set(mapping.keys()) missing_fields = required_fields - set(values.keys()) if missing_fields: raise ValueError(f"Missing fields: {missing_fields}") + + unrecognized_fields = set(values.keys()) - required_fields + if unrecognized_fields: + logger.warning(f"Unrecognized fields: {unrecognized_fields}") return values - new_class.__validator_check_name = classmethod(check_name) - new_class.__root_validator_check_missing_fields = classmethod(check_missing_fields) + validators = {"check_missing_fields_validator": model_validator(mode="before")(check_fields)} + + new_class = create_model(class_name, __validators__=validators, **mapping) return new_class def create_children_class(self, exclude=None): diff --git a/tests/metagpt/actions/test_action_node.py b/tests/metagpt/actions/test_action_node.py index 74b4df27f..25aceaa2e 100644 --- a/tests/metagpt/actions/test_action_node.py +++ b/tests/metagpt/actions/test_action_node.py @@ -8,6 +8,7 @@ from typing import List, Tuple import pytest +from pydantic import ValidationError from metagpt.actions import Action from metagpt.actions.action_node import ActionNode @@ -113,6 +114,10 @@ t_dict = { "Anything UNCLEAR": "We need clarification on how the high score should be stored. Should it persist across sessions (stored in a database or a file) or should it reset every time the game is restarted? Also, should the game speed increase as the snake grows, or should it remain constant throughout the game?", } +t_dict_min = { + "Required Python third-party packages": '"""\nflask==1.1.2\npygame==2.0.1\n"""\n', +} + WRITE_TASKS_OUTPUT_MAPPING = { "Required Python third-party packages": (str, ...), "Required Other language third-party packages": (str, ...), @@ -139,11 +144,19 @@ def test_create_model_class(): assert output.schema()["properties"]["Full API spec"] -def test_create_model_class_missing(): +def test_create_model_class_with_fields_unrecognized(): test_class = ActionNode.create_model_class("test_class", WRITE_TASKS_OUTPUT_MAPPING_MISSING) assert test_class.__name__ == "test_class" - _ = test_class(**t_dict) # 这里应该要挂掉 + _ = test_class(**t_dict) # just warning + + +def test_create_model_class_with_fields_missing(): + test_class = ActionNode.create_model_class("test_class", WRITE_TASKS_OUTPUT_MAPPING) + assert test_class.__name__ == "test_class" + + with pytest.raises(ValidationError): + _ = test_class(**t_dict_min) def test_create_model_class_with_mapping(): From 2a15ec424514e7c5ad15a477f88a557b847d4e2c Mon Sep 17 00:00:00 2001 From: geekan Date: Tue, 2 Jan 2024 20:09:15 +0800 Subject: [PATCH 10/23] change tqdm version --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9caea13f3..c04c6cc7f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,7 +29,7 @@ PyYAML==6.0.1 setuptools==65.6.3 tenacity==8.2.2 tiktoken==0.5.2 -tqdm==4.65.0 +tqdm==4.64.0 #unstructured[local-inference] # selenium>4 # webdriver_manager<3.9 From 8d5e4d6969b59aed715cf8958fa8802325c91f6d Mon Sep 17 00:00:00 2001 From: geekan Date: Tue, 2 Jan 2024 20:19:58 +0800 Subject: [PATCH 11/23] requirements update --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index c04c6cc7f..7a4b42a7e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,7 +29,7 @@ PyYAML==6.0.1 setuptools==65.6.3 tenacity==8.2.2 tiktoken==0.5.2 -tqdm==4.64.0 +tqdm==4.65.0 #unstructured[local-inference] # selenium>4 # webdriver_manager<3.9 @@ -56,6 +56,6 @@ gitignore-parser==0.1.9 # connexion[uvicorn]~=3.0.5 # Used by metagpt/tools/hello.py websockets~=12.0 networkx~=3.2.1 -google-generativeai==0.3.1 +google-generativeai==0.3.2 playwright==1.40.0 anytree From f5ed1349bae2eb337cffcae613763b51d007f9f2 Mon Sep 17 00:00:00 2001 From: geekan Date: Tue, 2 Jan 2024 20:25:16 +0800 Subject: [PATCH 12/23] remove document_store/document.py --- metagpt/document_store/document.py | 81 ------------------------------ 1 file changed, 81 deletions(-) delete mode 100644 metagpt/document_store/document.py diff --git a/metagpt/document_store/document.py b/metagpt/document_store/document.py deleted file mode 100644 index 90abc54de..000000000 --- a/metagpt/document_store/document.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -@Time : 2023/6/8 14:03 -@Author : alexanderwu -@File : document.py -@Desc : Classes and Operations Related to Vector Files in the Vector Database. Still under design. -""" -from pathlib import Path - -import pandas as pd -from langchain.document_loaders import ( - TextLoader, - UnstructuredPDFLoader, - UnstructuredWordDocumentLoader, -) -from langchain.text_splitter import CharacterTextSplitter -from tqdm import tqdm - - -def validate_cols(content_col: str, df: pd.DataFrame): - if content_col not in df.columns: - raise ValueError - - -def read_data(data_path: Path): - suffix = data_path.suffix - if ".xlsx" == suffix: - data = pd.read_excel(data_path) - elif ".csv" == suffix: - data = pd.read_csv(data_path) - elif ".json" == suffix: - data = pd.read_json(data_path) - elif suffix in (".docx", ".doc"): - data = UnstructuredWordDocumentLoader(str(data_path), mode="elements").load() - elif ".txt" == suffix: - data = TextLoader(str(data_path)).load() - text_splitter = CharacterTextSplitter(separator="\n", chunk_size=256, chunk_overlap=0) - texts = text_splitter.split_documents(data) - data = texts - elif ".pdf" == suffix: - data = UnstructuredPDFLoader(str(data_path), mode="elements").load() - else: - raise NotImplementedError - return data - - -class Document: - def __init__(self, data_path, content_col="content", meta_col="metadata"): - self.data = read_data(data_path) - if isinstance(self.data, pd.DataFrame): - validate_cols(content_col, self.data) - self.content_col = content_col - self.meta_col = meta_col - - def _get_docs_and_metadatas_by_df(self) -> (list, list): - df = self.data - docs = [] - metadatas = [] - for i in tqdm(range(len(df))): - docs.append(df[self.content_col].iloc[i]) - if self.meta_col: - metadatas.append({self.meta_col: df[self.meta_col].iloc[i]}) - else: - metadatas.append({}) - - return docs, metadatas - - def _get_docs_and_metadatas_by_langchain(self) -> (list, list): - data = self.data - docs = [i.page_content for i in data] - metadatas = [i.metadata for i in data] - return docs, metadatas - - def get_docs_and_metadatas(self) -> (list, list): - if isinstance(self.data, pd.DataFrame): - return self._get_docs_and_metadatas_by_df() - elif isinstance(self.data, list): - return self._get_docs_and_metadatas_by_langchain() - else: - raise NotImplementedError From c50ae4d8d78944f363056fee14e763a75f7a49fc Mon Sep 17 00:00:00 2001 From: geekan Date: Tue, 2 Jan 2024 20:49:20 +0800 Subject: [PATCH 13/23] refine code --- metagpt/actions/action.py | 25 +++++++++++++---------- tests/metagpt/actions/test_action_node.py | 24 +++++++++++----------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/metagpt/actions/action.py b/metagpt/actions/action.py index 9b94ce461..b586bcc22 100644 --- a/metagpt/actions/action.py +++ b/metagpt/actions/action.py @@ -8,9 +8,9 @@ from __future__ import annotations -from typing import Any, Optional, Union +from typing import Optional, Union -from pydantic import ConfigDict, Field +from pydantic import ConfigDict, Field, model_validator from metagpt.actions.action_node import ActionNode from metagpt.llm import LLM @@ -34,16 +34,19 @@ class Action(SerializationMixin, is_polymorphic_base=True): desc: str = "" # for skill manager node: ActionNode = Field(default=None, exclude=True) - def __init_with_instruction(self, instruction: str): - """Initialize action with instruction""" - self.node = ActionNode(key=self.name, expected_type=str, instruction=instruction, example="", schema="raw") - return self + @model_validator(mode="before") + def set_name_if_empty(cls, values): + if "name" not in values or not values["name"]: + values["name"] = cls.__name__ + return values - def __init__(self, **data: Any): - super().__init__(**data) - - if "instruction" in data: - self.__init_with_instruction(data["instruction"]) + @model_validator(mode="before") + def _init_with_instruction(cls, values): + if "instruction" in values: + name = values["name"] + i = values["instruction"] + values["node"] = ActionNode(key=name, expected_type=str, instruction=i, example="", schema="raw") + return values def set_prefix(self, prefix): """Set prefix for later usage""" diff --git a/tests/metagpt/actions/test_action_node.py b/tests/metagpt/actions/test_action_node.py index 25aceaa2e..384c4507b 100644 --- a/tests/metagpt/actions/test_action_node.py +++ b/tests/metagpt/actions/test_action_node.py @@ -21,35 +21,35 @@ from metagpt.team import Team @pytest.mark.asyncio async def test_debate_two_roles(): - action1 = Action(name="BidenSay", instruction="Express opinions and argue vigorously, and strive to gain votes") - action2 = Action(name="TrumpSay", instruction="Express opinions and argue vigorously, and strive to gain votes") + action1 = Action(name="AlexSay", instruction="Express your opinion with emotion and don't repeat it") + action2 = Action(name="BobSay", instruction="Express your opinion with emotion and don't repeat it") biden = Role( - name="Biden", profile="Democratic candidate", goal="Win the election", actions=[action1], watch=[action2] + name="Alex", profile="Democratic candidate", goal="Win the election", actions=[action1], watch=[action2] ) trump = Role( - name="Trump", profile="Republican candidate", goal="Win the election", actions=[action2], watch=[action1] + name="Bob", profile="Republican candidate", goal="Win the election", actions=[action2], watch=[action1] ) env = Environment(desc="US election live broadcast") team = Team(investment=10.0, env=env, roles=[biden, trump]) - history = await team.run(idea="Topic: climate change. Under 80 words per message.", send_to="Biden", n_round=3) - assert "Biden" in history + history = await team.run(idea="Topic: climate change. Under 80 words per message.", send_to="Alex", n_round=3) + assert "Alex" in history @pytest.mark.asyncio async def test_debate_one_role_in_env(): - action = Action(name="Debate", instruction="Express opinions and argue vigorously, and strive to gain votes") - biden = Role(name="Biden", profile="Democratic candidate", goal="Win the election", actions=[action]) + action = Action(name="Debate", instruction="Express your opinion with emotion and don't repeat it") + biden = Role(name="Alex", profile="Democratic candidate", goal="Win the election", actions=[action]) env = Environment(desc="US election live broadcast") team = Team(investment=10.0, env=env, roles=[biden]) - history = await team.run(idea="Topic: climate change. Under 80 words per message.", send_to="Biden", n_round=3) - assert "Biden" in history + history = await team.run(idea="Topic: climate change. Under 80 words per message.", send_to="Alex", n_round=3) + assert "Alex" in history @pytest.mark.asyncio async def test_debate_one_role(): - action = Action(name="Debate", instruction="Express opinions and argue vigorously, and strive to gain votes") - biden = Role(name="Biden", profile="Democratic candidate", goal="Win the election", actions=[action]) + action = Action(name="Debate", instruction="Express your opinion with emotion and don't repeat it") + biden = Role(name="Alex", profile="Democratic candidate", goal="Win the election", actions=[action]) msg: Message = await biden.run("Topic: climate change. Under 80 words per message.") assert len(msg.content) > 10 From 66d3e8448d16d251a819dd95e0af7928f705d48d Mon Sep 17 00:00:00 2001 From: geekan Date: Tue, 2 Jan 2024 20:51:02 +0800 Subject: [PATCH 14/23] refine code --- examples/debate_simple.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/debate_simple.py b/examples/debate_simple.py index 1a80bf8f4..aa95c5b85 100644 --- a/examples/debate_simple.py +++ b/examples/debate_simple.py @@ -12,11 +12,11 @@ from metagpt.environment import Environment from metagpt.roles import Role from metagpt.team import Team -action1 = Action(name="BidenSay", instruction="Express opinions and argue vigorously, and strive to gain votes") -action2 = Action(name="TrumpSay", instruction="Express opinions and argue vigorously, and strive to gain votes") -biden = Role(name="Biden", profile="Democratic candidate", goal="Win the election", actions=[action1], watch=[action2]) -trump = Role(name="Trump", profile="Republican candidate", goal="Win the election", actions=[action2], watch=[action1]) +action1 = Action(name="AlexSay", instruction="Express your opinion with emotion and don't repeat it") +action2 = Action(name="BobSay", instruction="Express your opinion with emotion and don't repeat it") +alex = Role(name="Alex", profile="Democratic candidate", goal="Win the election", actions=[action1], watch=[action2]) +bob = Role(name="Bob", profile="Republican candidate", goal="Win the election", actions=[action2], watch=[action1]) env = Environment(desc="US election live broadcast") -team = Team(investment=10.0, env=env, roles=[biden, trump]) +team = Team(investment=10.0, env=env, roles=[alex, bob]) -asyncio.run(team.run(idea="Topic: climate change. Under 80 words per message.", send_to="Biden", n_round=5)) +asyncio.run(team.run(idea="Topic: climate change. Under 80 words per message.", send_to="Alex", n_round=5)) From 54201b14592e18214500b4079254716734a97d54 Mon Sep 17 00:00:00 2001 From: geekan Date: Tue, 2 Jan 2024 21:07:03 +0800 Subject: [PATCH 15/23] add test document --- metagpt/document.py | 25 +-------------------- tests/metagpt/actions/test_action.py | 21 ++++++++++++++++++ tests/metagpt/test_document.py | 33 ++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 24 deletions(-) create mode 100644 tests/metagpt/test_document.py diff --git a/metagpt/document.py b/metagpt/document.py index 022e5d6f1..dcbd19d4d 100644 --- a/metagpt/document.py +++ b/metagpt/document.py @@ -20,8 +20,6 @@ from langchain.text_splitter import CharacterTextSplitter from pydantic import BaseModel, ConfigDict, Field from tqdm import tqdm -from metagpt.config import CONFIG -from metagpt.logs import logger from metagpt.repo_parser import RepoParser @@ -213,7 +211,7 @@ class Repo(BaseModel): self.assets[path] = doc return doc - def set(self, content: str, filename: str): + def set(self, filename: str, content: str): """Set a document and persist it to disk.""" path = self._path(filename) doc = self._set(content, path) @@ -232,24 +230,3 @@ class Repo(BaseModel): n_chars = sum(sum(len(j.content) for j in i.values()) for i in [self.docs, self.codes, self.assets]) symbols = RepoParser(base_directory=self.path).generate_symbols() return RepoMetadata(name=self.name, n_docs=n_docs, n_chars=n_chars, symbols=symbols) - - -def set_existing_repo(path=CONFIG.workspace_path / "t1"): - repo1 = Repo.from_path(path) - repo1.set("wtf content", "doc/wtf_file.md") - repo1.set("wtf code", "code/wtf_file.py") - logger.info(repo1) # check doc - - -def load_existing_repo(path=CONFIG.workspace_path / "web_tetris"): - repo = Repo.from_path(path) - logger.info(repo) - logger.info(repo.eda()) - - -def main(): - load_existing_repo() - - -if __name__ == "__main__": - main() diff --git a/tests/metagpt/actions/test_action.py b/tests/metagpt/actions/test_action.py index f750b5e6f..97818ca22 100644 --- a/tests/metagpt/actions/test_action.py +++ b/tests/metagpt/actions/test_action.py @@ -5,6 +5,8 @@ @Author : alexanderwu @File : test_action.py """ +import pytest + from metagpt.actions import Action, ActionType, WritePRD, WriteTest @@ -18,3 +20,22 @@ def test_action_type(): assert ActionType.WRITE_TEST.value == WriteTest assert ActionType.WRITE_PRD.name == "WRITE_PRD" assert ActionType.WRITE_TEST.name == "WRITE_TEST" + + +def test_simple_action(): + action = Action(name="AlexSay", instruction="Express your opinion with emotion and don't repeat it") + assert action.name == "AlexSay" + assert action.node.instruction == "Express your opinion with emotion and don't repeat it" + + +def test_empty_action(): + action = Action() + assert action.name == "Action" + assert not action.node + + +@pytest.mark.asyncio +async def test_empty_action_exception(): + action = Action() + with pytest.raises(NotImplementedError): + await action.run() diff --git a/tests/metagpt/test_document.py b/tests/metagpt/test_document.py new file mode 100644 index 000000000..18650e112 --- /dev/null +++ b/tests/metagpt/test_document.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@Time : 2024/1/2 21:00 +@Author : alexanderwu +@File : test_document.py +""" +from metagpt.config import CONFIG +from metagpt.document import Repo +from metagpt.logs import logger + + +def set_existing_repo(path): + repo1 = Repo.from_path(path) + repo1.set("doc/wtf_file.md", "wtf content") + repo1.set("code/wtf_file.py", "def hello():\n print('hello')") + logger.info(repo1) # check doc + + +def load_existing_repo(path): + repo = Repo.from_path(path) + logger.info(repo) + logger.info(repo.eda()) + + assert repo + assert repo.get("doc/wtf_file.md").content == "wtf content" + assert repo.get("code/wtf_file.py").content == "def hello():\n print('hello')" + + +def test_repo_set_load(): + repo_path = CONFIG.workspace_path / "test_repo" + set_existing_repo(repo_path) + load_existing_repo(repo_path) From 1d35cab9d77adb2828a579d5e398b176d672e920 Mon Sep 17 00:00:00 2001 From: better629 Date: Tue, 2 Jan 2024 21:21:10 +0800 Subject: [PATCH 16/23] rm useless code and increase UT ratio --- metagpt/provider/general_api_base.py | 98 ------------------- .../metagpt/provider/test_general_api_base.py | 37 +++++++ tests/metagpt/provider/test_human_provider.py | 20 ++-- tests/metagpt/provider/test_spark_api.py | 16 ++- .../provider/zhipuai/test_async_sse_client.py | 8 ++ .../provider/zhipuai/test_zhipu_model_api.py | 5 +- 6 files changed, 75 insertions(+), 109 deletions(-) diff --git a/metagpt/provider/general_api_base.py b/metagpt/provider/general_api_base.py index bbe03774c..1b9149396 100644 --- a/metagpt/provider/general_api_base.py +++ b/metagpt/provider/general_api_base.py @@ -15,7 +15,6 @@ from enum import Enum from typing import ( AsyncGenerator, AsyncIterator, - Callable, Dict, Iterator, Optional, @@ -240,54 +239,6 @@ class APIRequestor: self.api_version = api_version or openai.api_version self.organization = organization or openai.organization - def _check_polling_response(self, response: OpenAIResponse, predicate: Callable[[OpenAIResponse], bool]): - if not predicate(response): - return - error_data = response.data["error"] - message = error_data.get("message", "Operation failed") - code = error_data.get("code") - raise openai.APIError(message=message, body=dict(code=code)) - - def _poll( - self, method, url, until, failed, params=None, headers=None, interval=None, delay=None - ) -> Tuple[Iterator[OpenAIResponse], bool, str]: - if delay: - time.sleep(delay) - - response, b, api_key = self.request(method, url, params, headers) - self._check_polling_response(response, failed) - start_time = time.time() - while not until(response): - if time.time() - start_time > TIMEOUT_SECS: - raise openai.APITimeoutError("Operation polling timed out.") - - time.sleep(interval or response.retry_after or 10) - response, b, api_key = self.request(method, url, params, headers) - self._check_polling_response(response, failed) - - response.data = response.data["result"] - return response, b, api_key - - async def _apoll( - self, method, url, until, failed, params=None, headers=None, interval=None, delay=None - ) -> Tuple[Iterator[OpenAIResponse], bool, str]: - if delay: - await asyncio.sleep(delay) - - response, b, api_key = await self.arequest(method, url, params, headers) - self._check_polling_response(response, failed) - start_time = time.time() - while not until(response): - if time.time() - start_time > TIMEOUT_SECS: - raise openai.APITimeoutError("Operation polling timed out.") - - await asyncio.sleep(interval or response.retry_after or 10) - response, b, api_key = await self.arequest(method, url, params, headers) - self._check_polling_response(response, failed) - - response.data = response.data["result"] - return response, b, api_key - @overload def request( self, @@ -469,55 +420,6 @@ class APIRequestor: await ctx.__aexit__(None, None, None) return resp, got_stream, self.api_key - def handle_error_response(self, rbody, rcode, resp, rheaders, stream_error=False): - try: - error_data = resp["error"] - except (KeyError, TypeError): - raise openai.APIError( - "Invalid response object from API: %r (HTTP response code " "was %d)" % (rbody, rcode) - ) - - if "internal_message" in error_data: - error_data["message"] += "\n\n" + error_data["internal_message"] - - log_info( - "LLM API error received", - error_code=error_data.get("code"), - error_type=error_data.get("type"), - error_message=error_data.get("message"), - error_param=error_data.get("param"), - stream_error=stream_error, - ) - - # Rate limits were previously coded as 400's with code 'rate_limit' - if rcode == 429: - return openai.RateLimitError(f"{error_data.get('message')} {rbody} {rcode} {resp} {rheaders}", body=rbody) - elif rcode in [400, 404, 415]: - return openai.BadRequestError( - message=f'{error_data.get("message")}, {error_data.get("param")}, {error_data.get("code")} {rbody} {rcode} {resp} {rheaders}', - body=rbody, - ) - elif rcode == 401: - return openai.AuthenticationError( - f"{error_data.get('message')} {rbody} {rcode} {resp} {rheaders}", body=rbody - ) - elif rcode == 403: - return openai.PermissionDeniedError( - f"{error_data.get('message')} {rbody} {rcode} {resp} {rheaders}", body=rbody - ) - elif rcode == 409: - return openai.ConflictError(f"{error_data.get('message')} {rbody} {rcode} {resp} {rheaders}", body=rbody) - elif stream_error: - # TODO: we will soon attach status codes to stream errors - parts = [error_data.get("message"), "(Error occurred while streaming.)"] - message = " ".join([p for p in parts if p is not None]) - return openai.APIError(f"{message} {rbody} {rcode} {resp} {rheaders}", body=rbody) - else: - return openai.APIError( - f"{error_data.get('message')} {rbody} {rcode} {resp} {rheaders}", - body=rbody, - ) - def request_headers(self, method: str, extra, request_id: Optional[str]) -> Dict[str, str]: user_agent = "LLM/v1 PythonBindings/%s" % (version.VERSION,) diff --git a/tests/metagpt/provider/test_general_api_base.py b/tests/metagpt/provider/test_general_api_base.py index ae768ce95..b8ab619f7 100644 --- a/tests/metagpt/provider/test_general_api_base.py +++ b/tests/metagpt/provider/test_general_api_base.py @@ -14,11 +14,14 @@ from metagpt.provider.general_api_base import ( APIRequestor, ApiType, OpenAIResponse, + _aiohttp_proxies_arg, + _build_api_url, _make_session, _requests_proxies_arg, log_debug, log_info, log_warn, + logfmt, parse_stream, parse_stream_helper, ) @@ -36,6 +39,10 @@ def test_basic(): log_warn("warn") log_info("info") + logfmt({"k1": b"v1", "k2": 1, "k3": "a b"}) + + _build_api_url(url="http://www.baidu.com/s?wd=", query="baidu") + def test_openai_response(): resp = OpenAIResponse(data=[], headers={"retry-after": 3}) @@ -53,11 +60,18 @@ def test_proxy(): assert _requests_proxies_arg(proxy=proxy) == {"http": proxy, "https": proxy} proxy_dict = {"http": proxy} assert _requests_proxies_arg(proxy=proxy_dict) == proxy_dict + assert _aiohttp_proxies_arg(proxy_dict) == proxy proxy_dict = {"https": proxy} assert _requests_proxies_arg(proxy=proxy_dict) == proxy_dict + assert _aiohttp_proxies_arg(proxy_dict) == proxy assert _make_session() is not None + assert _aiohttp_proxies_arg(None) is None + assert _aiohttp_proxies_arg("test") == "test" + with pytest.raises(ValueError): + _aiohttp_proxies_arg(-1) + def test_parse_stream(): assert parse_stream_helper(None) is None @@ -83,6 +97,29 @@ async def mock_interpret_async_response( return b"baidu", True +def test_requestor_headers(): + # validate_headers + headers = api_requestor._validate_headers(None) + assert not headers + with pytest.raises(Exception): + api_requestor._validate_headers(-1) + with pytest.raises(Exception): + api_requestor._validate_headers({1: 2}) + with pytest.raises(Exception): + api_requestor._validate_headers({"test": 1}) + supplied_headers = {"test": "test"} + assert api_requestor._validate_headers(supplied_headers) == supplied_headers + + api_requestor.organization = "test" + api_requestor.api_version = "test123" + api_requestor.api_type = ApiType.OPEN_AI + request_id = "test123" + headers = api_requestor.request_headers(method="post", extra={}, request_id=request_id) + assert headers["LLM-Organization"] == api_requestor.organization + assert headers["LLM-Version"] == api_requestor.api_version + assert headers["X-Request-Id"] == request_id + + def test_api_requestor(mocker): mocker.patch("metagpt.provider.general_api_base.APIRequestor._interpret_response", mock_interpret_response) resp, _, _ = api_requestor.request(method="get", url="/s?wd=baidu") diff --git a/tests/metagpt/provider/test_human_provider.py b/tests/metagpt/provider/test_human_provider.py index 8ba532781..3f63410c0 100644 --- a/tests/metagpt/provider/test_human_provider.py +++ b/tests/metagpt/provider/test_human_provider.py @@ -7,23 +7,25 @@ import pytest from metagpt.provider.human_provider import HumanProvider resp_content = "test" - - -def mock_llm_ask(msg: str, timeout: int = 3) -> str: - return resp_content - - -async def mock_llm_aask(msg: str, timeout: int = 3) -> str: - return mock_llm_ask(msg) +resp_exit = "exit" @pytest.mark.asyncio async def test_async_human_provider(mocker): - mocker.patch("metagpt.provider.human_provider.HumanProvider.aask", mock_llm_aask) + mocker.patch("builtins.input", lambda _: resp_content) human_provider = HumanProvider() + resp = human_provider.ask(resp_content) + assert resp == resp_content resp = await human_provider.aask(None) assert resp_content == resp + mocker.patch("builtins.input", lambda _: resp_exit) + with pytest.raises(SystemExit): + human_provider.ask(resp_exit) + resp = await human_provider.acompletion([]) assert not resp + + resp = await human_provider.acompletion_text([]) + assert resp == "" diff --git a/tests/metagpt/provider/test_spark_api.py b/tests/metagpt/provider/test_spark_api.py index 6d5a0e1f6..ee2d02c97 100644 --- a/tests/metagpt/provider/test_spark_api.py +++ b/tests/metagpt/provider/test_spark_api.py @@ -17,10 +17,23 @@ prompt_msg = "who are you" resp_content = "I'm Spark" -def test_get_msg_from_web(): +class MockWebSocketApp(object): + def __init__(self, ws_url, on_message=None, on_error=None, on_close=None, on_open=None): + pass + + def run_forever(self, sslopt=None): + pass + + +def test_get_msg_from_web(mocker): + mocker.patch("websocket.WebSocketApp", MockWebSocketApp) + get_msg_from_web = GetMessageFromWeb(text=prompt_msg) assert get_msg_from_web.gen_params()["parameter"]["chat"]["domain"] == "xxxxxx" + ret = get_msg_from_web.run() + assert ret == "" + def mock_spark_get_msg_from_web_run(self) -> str: return resp_content @@ -29,6 +42,7 @@ def mock_spark_get_msg_from_web_run(self) -> str: @pytest.mark.asyncio async def test_spark_acompletion(mocker): mocker.patch("metagpt.provider.spark_api.GetMessageFromWeb.run", mock_spark_get_msg_from_web_run) + spark_gpt = SparkLLM() resp = await spark_gpt.acompletion([]) diff --git a/tests/metagpt/provider/zhipuai/test_async_sse_client.py b/tests/metagpt/provider/zhipuai/test_async_sse_client.py index 9e5bd5f2e..2649f595b 100644 --- a/tests/metagpt/provider/zhipuai/test_async_sse_client.py +++ b/tests/metagpt/provider/zhipuai/test_async_sse_client.py @@ -16,3 +16,11 @@ async def test_async_sse_client(): async_sse_client = AsyncSSEClient(event_source=Iterator()) async for event in async_sse_client.async_events(): assert event.data, "test_value" + + class InvalidIterator(object): + async def __aiter__(self): + yield b"invalid: test_value" + + async_sse_client = AsyncSSEClient(event_source=InvalidIterator()) + async for event in async_sse_client.async_events(): + assert not event diff --git a/tests/metagpt/provider/zhipuai/test_zhipu_model_api.py b/tests/metagpt/provider/zhipuai/test_zhipu_model_api.py index 83ae2de60..1f0a42fa6 100644 --- a/tests/metagpt/provider/zhipuai/test_zhipu_model_api.py +++ b/tests/metagpt/provider/zhipuai/test_zhipu_model_api.py @@ -14,7 +14,7 @@ from metagpt.provider.zhipuai.zhipu_model_api import ZhiPuModelAPI api_key = "xxx.xxx" zhipuai.api_key = api_key -default_resp = {"result": "test response"} +default_resp = b'{"result": "test response"}' async def mock_requestor_arequest(self, **kwargs) -> Tuple[Any, Any, str]: @@ -39,3 +39,6 @@ async def test_zhipu_model_api(mocker): InvokeType.SYNC, stream=False, method="get", headers={}, kwargs={"model": "chatglm_turbo"} ) assert result == default_resp + + result = await ZhiPuModelAPI.ainvoke() + assert result["result"] == "test response" From c3dd03671d10864c0752387c7248ca50b2ba6f6f Mon Sep 17 00:00:00 2001 From: geekan Date: Tue, 2 Jan 2024 21:30:35 +0800 Subject: [PATCH 17/23] faiss store: add tests --- .gitignore | 1 + examples/{faq.xlsx => example.xlsx} | Bin metagpt/document.py | 7 +++++-- metagpt/document_store/faiss_store.py | 8 -------- tests/metagpt/document_store/test_faiss_store.py | 8 ++++++++ 5 files changed, 14 insertions(+), 10 deletions(-) rename examples/{faq.xlsx => example.xlsx} (100%) diff --git a/.gitignore b/.gitignore index 1613a638d..2c59f3b59 100644 --- a/.gitignore +++ b/.gitignore @@ -171,3 +171,4 @@ tests/metagpt/utils/file_repo_git *.png htmlcov htmlcov.* +*.pkl diff --git a/examples/faq.xlsx b/examples/example.xlsx similarity index 100% rename from examples/faq.xlsx rename to examples/example.xlsx diff --git a/metagpt/document.py b/metagpt/document.py index dcbd19d4d..f4fa0a489 100644 --- a/metagpt/document.py +++ b/metagpt/document.py @@ -101,6 +101,7 @@ class Document(BaseModel): raise ValueError("File path is not set.") self.path.parent.mkdir(parents=True, exist_ok=True) + # TODO: excel, csv, json, etc. self.path.write_text(self.content, encoding="utf-8") def persist(self): @@ -126,10 +127,12 @@ class IndexableDocument(Document): if not data_path.exists(): raise FileNotFoundError(f"File {data_path} not found.") data = read_data(data_path) - content = data_path.read_text() if isinstance(data, pd.DataFrame): validate_cols(content_col, data) - return cls(data=data, content=content, content_col=content_col, meta_col=meta_col) + return cls(data=data, content=str(data), content_col=content_col, meta_col=meta_col) + else: + content = data_path.read_text() + return cls(data=data, content=content, content_col=content_col, meta_col=meta_col) def _get_docs_and_metadatas_by_df(self) -> (list, list): df = self.data diff --git a/metagpt/document_store/faiss_store.py b/metagpt/document_store/faiss_store.py index bfba1d386..1271f1c23 100644 --- a/metagpt/document_store/faiss_store.py +++ b/metagpt/document_store/faiss_store.py @@ -14,7 +14,6 @@ from langchain.vectorstores import FAISS from langchain_core.embeddings import Embeddings from metagpt.config import CONFIG -from metagpt.const import DATA_PATH from metagpt.document import IndexableDocument from metagpt.document_store.base_store import LocalStore from metagpt.logs import logger @@ -76,10 +75,3 @@ class FaissStore(LocalStore): def delete(self, *args, **kwargs): """Currently, langchain does not provide a delete interface.""" raise NotImplementedError - - -if __name__ == "__main__": - faiss_store = FaissStore(DATA_PATH / "qcs/qcs_4w.json") - logger.info(faiss_store.search("Oily Skin Facial Cleanser")) - faiss_store.add([f"Oily Skin Facial Cleanser-{i}" for i in range(3)]) - logger.info(faiss_store.search("Oily Skin Facial Cleanser")) diff --git a/tests/metagpt/document_store/test_faiss_store.py b/tests/metagpt/document_store/test_faiss_store.py index 75bb5427f..7e2979bd4 100644 --- a/tests/metagpt/document_store/test_faiss_store.py +++ b/tests/metagpt/document_store/test_faiss_store.py @@ -30,3 +30,11 @@ async def test_search_xlsx(): query = "Which facial cleanser is good for oily skin?" result = await role.run(query) logger.info(result) + + +@pytest.mark.asyncio +async def test_write(): + store = FaissStore(EXAMPLE_PATH / "example.xlsx", meta_col="Answer", content_col="Question") + _faiss_store = store.write() + assert _faiss_store.docstore + assert _faiss_store.index From a8df4f85f02b632ec77e1bcb9c952e92e5cb4061 Mon Sep 17 00:00:00 2001 From: geekan Date: Tue, 2 Jan 2024 21:35:58 +0800 Subject: [PATCH 18/23] remove prompts of mineraft --- metagpt/prompts/decompose.py | 22 -------- metagpt/prompts/structure_action.py | 22 -------- metagpt/prompts/structure_goal.py | 46 --------------- metagpt/prompts/use_lib_sop.py | 88 ----------------------------- 4 files changed, 178 deletions(-) delete mode 100644 metagpt/prompts/decompose.py delete mode 100644 metagpt/prompts/structure_action.py delete mode 100644 metagpt/prompts/structure_goal.py delete mode 100644 metagpt/prompts/use_lib_sop.py diff --git a/metagpt/prompts/decompose.py b/metagpt/prompts/decompose.py deleted file mode 100644 index ab0c360d3..000000000 --- a/metagpt/prompts/decompose.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -@Time : 2023/5/30 10:09 -@Author : alexanderwu -@File : decompose.py -""" - -DECOMPOSE_SYSTEM = """SYSTEM: -You serve as an assistant that helps me play Minecraft. -I will give you my goal in the game, please break it down as a tree-structure plan to achieve this goal. -The requirements of the tree-structure plan are: -1. The plan tree should be exactly of depth 2. -2. Describe each step in one line. -3. You should index the two levels like ’1.’, ’1.1.’, ’1.2.’, ’2.’, ’2.1.’, etc. -4. The sub-goals at the bottom level should be basic actions so that I can easily execute them in the game. -""" - - -DECOMPOSE_USER = """USER: -The goal is to {goal description}. Generate the plan according to the requirements. -""" diff --git a/metagpt/prompts/structure_action.py b/metagpt/prompts/structure_action.py deleted file mode 100644 index 97c57cf24..000000000 --- a/metagpt/prompts/structure_action.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -@Time : 2023/5/30 10:12 -@Author : alexanderwu -@File : structure_action.py -""" - -ACTION_SYSTEM = """SYSTEM: -You serve as an assistant that helps me play Minecraft. -I will give you a sentence. Please convert this sentence into one or several actions according to the following instructions. -Each action should be a tuple of four items, written in the form (’verb’, ’object’, ’tools’, ’materials’) -’verb’ is the verb of this action. -’object’ refers to the target object of the action. -’tools’ specifies the tools required for the action. -’material’ specifies the materials required for the action. -If some of the items are not required, set them to be ’None’. -""" - -ACTION_USER = """USER: -The sentence is {sentence}. Generate the action tuple according to the requirements. -""" diff --git a/metagpt/prompts/structure_goal.py b/metagpt/prompts/structure_goal.py deleted file mode 100644 index e4b1a3bee..000000000 --- a/metagpt/prompts/structure_goal.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -@Time : 2023/5/30 09:51 -@Author : alexanderwu -@File : structure_goal.py -""" - -GOAL_SYSTEM = """SYSTEM: -You are an assistant for the game Minecraft. -I will give you some target object and some knowledge related to the object. Please write the obtaining of the object as a goal in the standard form. -The standard form of the goal is as follows: -{ -"object": "the name of the target object", -"count": "the target quantity", -"material": "the materials required for this goal, a dictionary in the form {material_name: material_quantity}. If no material is required, set it to None", -"tool": "the tool used for this goal. If multiple tools can be used for this goal, only write the most basic one. If no tool is required, set it to None", -"info": "the knowledge related to this goal" -} -The information I will give you: -Target object: the name and the quantity of the target object -Knowledge: some knowledge related to the object. -Requirements: -1. You must generate the goal based on the provided knowledge instead of purely depending on your own knowledge. -2. The "info" should be as compact as possible, at most 3 sentences. The knowledge I give you may be raw texts from Wiki documents. Please extract and summarize important information instead of directly copying all the texts. -Goal Example: -{ -"object": "iron_ore", -"count": 1, -"material": None, -"tool": "stone_pickaxe", -"info": "iron ore is obtained by mining iron ore. iron ore is most found in level 53. iron ore can only be mined with a stone pickaxe or better; using a wooden or gold pickaxe will yield nothing." -} -{ -"object": "wooden_pickaxe", -"count": 1, -"material": {"planks": 3, "stick": 2}, -"tool": "crafting_table", -"info": "wooden pickaxe can be crafted with 3 planks and 2 stick as the material and crafting table as the tool." -} -""" - -GOAL_USER = """USER: -Target object: {object quantity} {object name} -Knowledge: {related knowledge} -""" diff --git a/metagpt/prompts/use_lib_sop.py b/metagpt/prompts/use_lib_sop.py deleted file mode 100644 index b43ed5125..000000000 --- a/metagpt/prompts/use_lib_sop.py +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -@Time : 2023/5/30 10:45 -@Author : alexanderwu -@File : use_lib_sop.py -""" - -SOP_SYSTEM = """SYSTEM: -You serve as an assistant that helps me play the game Minecraft. -I will give you a goal in the game. Please think of a plan to achieve the goal, and then write a sequence of actions to realize the plan. The requirements and instructions are as follows: -1. You can only use the following functions. Don’t make plans purely based on your experience, think about how to use these functions. -explore(object, strategy) -Move around to find the object with the strategy: used to find objects including block items and entities. This action is finished once the object is visible (maybe at the distance). -Augments: -- object: a string, the object to explore. -- strategy: a string, the strategy for exploration. -approach(object) -Move close to a visible object: used to approach the object you want to attack or mine. It may fail if the target object is not accessible. -Augments: -- object: a string, the object to approach. -craft(object, materials, tool) -Craft the object with the materials and tool: used for crafting new object that is not in the inventory or is not enough. The required materials must be in the inventory and will be consumed, and the newly crafted objects will be added to the inventory. The tools like the crafting table and furnace should be in the inventory and this action will directly use them. Don’t try to place or approach the crafting table or furnace, you will get failed since this action does not support using tools placed on the ground. You don’t need to collect the items after crafting. If the quantity you require is more than a unit, this action will craft the objects one unit by one unit. If the materials run out halfway through, this action will stop, and you will only get part of the objects you want that have been crafted. -Augments: -- object: a dict, whose key is the name of the object and value is the object quantity. -- materials: a dict, whose keys are the names of the materials and values are the quantities. -- tool: a string, the tool used for crafting. Set to null if no tool is required. -mine(object, tool) -Mine the object with the tool: can only mine the object within reach, cannot mine object from a distance. If there are enough objects within reach, this action will mine as many as you specify. The obtained objects will be added to the inventory. -Augments: -- object: a string, the object to mine. -- tool: a string, the tool used for mining. Set to null if no tool is required. -attack(object, tool) -Attack the object with the tool: used to attack the object within reach. This action will keep track of and attack the object until it is killed. -Augments: -- object: a string, the object to attack. -- tool: a string, the tool used for mining. Set to null if no tool is required. -equip(object) -Equip the object from the inventory: used to equip equipment, including tools, weapons, and armor. The object must be in the inventory and belong to the items for equipping. -Augments: -- object: a string, the object to equip. -digdown(object, tool) -Dig down to the y-level with the tool: the only action you can take if you want to go underground for mining some ore. -Augments: -- object: an int, the y-level (absolute y coordinate) to dig to. -- tool: a string, the tool used for digging. Set to null if no tool is required. -go_back_to_ground(tool) -Go back to the ground from underground: the only action you can take for going back to the ground if you are underground. -Augments: -- tool: a string, the tool used for digging. Set to null if no tool is required. -apply(object, tool) -Apply the tool on the object: used for fetching water, milk, lava with the tool bucket, pooling water or lava to the object with the tool water bucket or lava bucket, shearing sheep with the tool shears, blocking attacks with the tool shield. -Augments: -- object: a string, the object to apply to. -- tool: a string, the tool used to apply. -2. You cannot define any new function. Note that the "Generated structures" world creation option is turned off. -3. There is an inventory that stores all the objects I have. It is not an entity, but objects can be added to it or retrieved from it anytime at anywhere without specific actions. The mined or crafted objects will be added to this inventory, and the materials and tools to use are also from this inventory. Objects in the inventory can be directly used. Don’t write the code to obtain them. If you plan to use some object not in the inventory, you should first plan to obtain it. You can view the inventory as one of my states, and it is written in form of a dictionary whose keys are the name of the objects I have and the values are their quantities. -4. You will get the following information about my current state: -- inventory: a dict representing the inventory mentioned above, whose keys are the name of the objects and the values are their quantities -- environment: a string including my surrounding biome, the y-level of my current location, and whether I am on the ground or underground -Pay attention to this information. Choose the easiest way to achieve the goal conditioned on my current state. Do not provide options, always make the final decision. -5. You must describe your thoughts on the plan in natural language at the beginning. After that, you should write all the actions together. The response should follow the format: -{ -"explanation": "explain why the last action failed, set to null for the first planning", -"thoughts": "Your thoughts on the plan in natural languag", -"action_list": [ -{"name": "action name", "args": {"arg name": value}, "expectation": "describe the expected results of this action"}, -{"name": "action name", "args": {"arg name": value}, "expectation": "describe the expected results of this action"}, -{"name": "action name", "args": {"arg name": value}, "expectation": "describe the expected results of this action"} -] -} -The action_list can contain arbitrary number of actions. The args of each action should correspond to the type mentioned in the Arguments part. Remember to add “‘dict“‘ at the beginning and the end of the dict. Ensure that you response can be parsed by Python json.loads -6. I will execute your code step by step and give you feedback. If some action fails, I will stop at that action and will not execute its following actions. The feedback will include error messages about the failed action. At that time, you should replan and write the new code just starting from that failed action. -""" - - -SOP_USER = """USER: -My current state: -- inventory: {inventory} -- environment: {environment} -The goal is to {goal}. -Here is one plan to achieve similar goal for reference: {reference plan}. -Begin your plan. Remember to follow the response format. -or Action {successful action} succeeded, and {feedback message}. Continue your -plan. Do not repeat successful action. Remember to follow the response format. -or Action {failed action} failed, because {feedback message}. Revise your plan from -the failed action. Remember to follow the response format. -""" From 05749fad31987c3ffb9c7f5f716ae60c1f2219b3 Mon Sep 17 00:00:00 2001 From: geekan Date: Tue, 2 Jan 2024 21:44:51 +0800 Subject: [PATCH 19/23] refactor filename --- metagpt/tools/{hello.py => openapi_v3_hello.py} | 2 +- requirements.txt | 2 +- tests/metagpt/tools/test_hello.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename metagpt/tools/{hello.py => openapi_v3_hello.py} (96%) diff --git a/metagpt/tools/hello.py b/metagpt/tools/openapi_v3_hello.py similarity index 96% rename from metagpt/tools/hello.py rename to metagpt/tools/openapi_v3_hello.py index ec7fc9231..c8f5de42d 100644 --- a/metagpt/tools/hello.py +++ b/metagpt/tools/openapi_v3_hello.py @@ -3,7 +3,7 @@ """ @Time : 2023/5/2 16:03 @Author : mashenquan -@File : hello.py +@File : openapi_v3_hello.py @Desc : Implement the OpenAPI Specification 3.0 demo and use the following command to test the HTTP service: curl -X 'POST' \ diff --git a/requirements.txt b/requirements.txt index 7a4b42a7e..9c90034cb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -53,7 +53,7 @@ gitpython==3.1.40 zhipuai==1.0.7 socksio~=1.0.0 gitignore-parser==0.1.9 -# connexion[uvicorn]~=3.0.5 # Used by metagpt/tools/hello.py +# connexion[uvicorn]~=3.0.5 # Used by metagpt/tools/openapi_v3_hello.py websockets~=12.0 networkx~=3.2.1 google-generativeai==0.3.2 diff --git a/tests/metagpt/tools/test_hello.py b/tests/metagpt/tools/test_hello.py index 243206991..7e61532ab 100644 --- a/tests/metagpt/tools/test_hello.py +++ b/tests/metagpt/tools/test_hello.py @@ -18,7 +18,7 @@ from metagpt.config import CONFIG @pytest.mark.asyncio async def test_hello(): workdir = Path(__file__).parent.parent.parent.parent - script_pathname = workdir / "metagpt/tools/hello.py" + script_pathname = workdir / "metagpt/tools/openapi_v3_hello.py" env = CONFIG.new_environ() env["PYTHONPATH"] = str(workdir) + ":" + env.get("PYTHONPATH", "") process = subprocess.Popen(["python", str(script_pathname)], cwd=workdir, env=env) From 339d9de5c77350f6f8856dce59c0272e4dbef558 Mon Sep 17 00:00:00 2001 From: geekan Date: Tue, 2 Jan 2024 21:49:32 +0800 Subject: [PATCH 20/23] refine tests --- tests/metagpt/utils/test_output_parser.py | 94 +---------------------- 1 file changed, 3 insertions(+), 91 deletions(-) diff --git a/tests/metagpt/utils/test_output_parser.py b/tests/metagpt/utils/test_output_parser.py index afacc28ea..d4bc04d0a 100644 --- a/tests/metagpt/utils/test_output_parser.py +++ b/tests/metagpt/utils/test_output_parser.py @@ -119,95 +119,7 @@ def test_extract_struct( case() -if __name__ == "__main__": - t_text = ''' -## Required Python third-party packages -```python -""" -flask==1.1.2 -pygame==2.0.1 -""" -``` - -## Required Other language third-party packages -```python -""" -No third-party packages required for other languages. -""" -``` - -## Full API spec -```python -""" -openapi: 3.0.0 -info: - title: Web Snake Game API - version: 1.0.0 -paths: - /game: - get: - summary: Get the current game state - responses: - '200': - description: A JSON object of the game state - post: - summary: Send a command to the game - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - command: - type: string - responses: - '200': - description: A JSON object of the updated game state -""" -``` - -## Logic Analysis -```python -[ - ("app.py", "Main entry point for the Flask application. Handles HTTP requests and responses."), - ("game.py", "Contains the Game and Snake classes. Handles the game logic."), - ("static/js/script.js", "Handles user interactions and updates the game UI."), - ("static/css/styles.css", "Defines the styles for the game UI."), - ("templates/index.html", "The main page of the web application. Displays the game UI.") -] -``` - -## Task list -```python -[ - "game.py", - "app.py", - "static/css/styles.css", - "static/js/script.js", - "templates/index.html" -] -``` - -## Shared Knowledge -```python -""" -'game.py' contains the Game and Snake classes which are responsible for the game logic. The Game class uses an instance of the Snake class. - -'app.py' is the main entry point for the Flask application. It creates an instance of the Game class and handles HTTP requests and responses. - -'static/js/script.js' is responsible for handling user interactions and updating the game UI based on the game state returned by 'app.py'. - -'static/css/styles.css' defines the styles for the game UI. - -'templates/index.html' is the main page of the web application. It displays the game UI and loads 'static/js/script.js' and 'static/css/styles.css'. -""" -``` - -## Anything UNCLEAR -We need clarification on how the high score should be stored. Should it persist across sessions (stored in a database or a file) or should it reset every time the game is restarted? Also, should the game speed increase as the snake grows, or should it remain constant throughout the game? - ''' - +def test_parse_with_markdown_mapping(): OUTPUT_MAPPING = { "Original Requirements": (str, ...), "Product Goals": (List[str], ...), @@ -218,7 +130,7 @@ We need clarification on how the high score should be stored. Should it persist "Requirement Pool": (List[Tuple[str, str]], ...), "Anything UNCLEAR": (str, ...), } - t_text1 = """## Original Requirements: + t_text1 = """[CONTENT]## Original Requirements: The user wants to create a web-based version of the game "Fly Bird". @@ -286,7 +198,7 @@ The product should be a web-based version of the game "Fly Bird" that is engagin ## Anything UNCLEAR: There are no unclear points. - """ +[/CONTENT]""" d = OutputParser.parse_data_with_mapping(t_text1, OUTPUT_MAPPING) import json From 665ddba1c04252b0d920c57517c02f8b786e336f Mon Sep 17 00:00:00 2001 From: geekan Date: Tue, 2 Jan 2024 21:51:24 +0800 Subject: [PATCH 21/23] refine tests --- tests/metagpt/utils/test_output_parser.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/metagpt/utils/test_output_parser.py b/tests/metagpt/utils/test_output_parser.py index d4bc04d0a..f7717e360 100644 --- a/tests/metagpt/utils/test_output_parser.py +++ b/tests/metagpt/utils/test_output_parser.py @@ -130,7 +130,7 @@ def test_parse_with_markdown_mapping(): "Requirement Pool": (List[Tuple[str, str]], ...), "Anything UNCLEAR": (str, ...), } - t_text1 = """[CONTENT]## Original Requirements: + t_text_with_content_tag = """[CONTENT]## Original Requirements: The user wants to create a web-based version of the game "Fly Bird". @@ -199,7 +199,10 @@ The product should be a web-based version of the game "Fly Bird" that is engagin There are no unclear points. [/CONTENT]""" - d = OutputParser.parse_data_with_mapping(t_text1, OUTPUT_MAPPING) + t_text_raw = t_text_with_content_tag.replace("[CONTENT]", "").replace("[/CONTENT]", "") + d = OutputParser.parse_data_with_mapping(t_text_with_content_tag, OUTPUT_MAPPING) + import json print(json.dumps(d)) + assert d["Original Requirements"] == t_text_raw.split("## Original Requirements:")[1].split("##")[0].strip() From 78b7e164f93ca22efff55f34574c482fde0723ac Mon Sep 17 00:00:00 2001 From: geekan Date: Tue, 2 Jan 2024 21:58:31 +0800 Subject: [PATCH 22/23] refine tests --- metagpt/utils/common.py | 16 ++-------------- tests/metagpt/utils/test_common.py | 4 ++++ 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/metagpt/utils/common.py b/metagpt/utils/common.py index 60acd7e3c..c7751c2af 100644 --- a/metagpt/utils/common.py +++ b/metagpt/utils/common.py @@ -23,7 +23,7 @@ import sys import traceback import typing from pathlib import Path -from typing import Any, List, Tuple, Union, get_args, get_origin +from typing import Any, List, Tuple, Union import aiofiles import loguru @@ -147,19 +147,7 @@ class OutputParser: if extracted_content: return extracted_content.group(1).strip() else: - return "No content found between [CONTENT] and [/CONTENT] tags." - - @staticmethod - def is_supported_list_type(i): - origin = get_origin(i) - if origin is not List: - return False - - args = get_args(i) - if args == (str,) or args == (Tuple[str, str],) or args == (List[str],): - return True - - return False + raise ValueError(f"Could not find content between [{tag}] and [/{tag}]") @classmethod def parse_data_with_mapping(cls, data, mapping): diff --git a/tests/metagpt/utils/test_common.py b/tests/metagpt/utils/test_common.py index 3a0ec18fc..0342a92af 100644 --- a/tests/metagpt/utils/test_common.py +++ b/tests/metagpt/utils/test_common.py @@ -91,6 +91,10 @@ class TestGetProjectRoot: x=(TutorialAssistant, RunCode(), "a"), want={"metagpt.roles.tutorial_assistant.TutorialAssistant", "metagpt.actions.run_code.RunCode", "a"}, ), + Input( + x={"a": TutorialAssistant, "b": RunCode(), "c": "a"}, + want={"a", "metagpt.roles.tutorial_assistant.TutorialAssistant", "metagpt.actions.run_code.RunCode"}, + ), ] for i in inputs: v = any_to_str_set(i.x) From 38015322b6fbd2176712dfe137dd8adcb975d830 Mon Sep 17 00:00:00 2001 From: geekan Date: Tue, 2 Jan 2024 23:38:51 +0800 Subject: [PATCH 23/23] fix bugs --- docs/scripts/coverage.sh | 2 +- examples/example.faiss | Bin 12333 -> 12333 bytes examples/example.pkl | Bin 624 -> 624 bytes setup.py | 1 + .../serialize_deserialize/test_action.py | 2 +- .../test_write_design.py | 4 ++-- .../test_write_docstring.py | 2 +- 7 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/scripts/coverage.sh b/docs/scripts/coverage.sh index 648d9b412..a56571399 100755 --- a/docs/scripts/coverage.sh +++ b/docs/scripts/coverage.sh @@ -1 +1 @@ -coverage run --source ./metagpt -m pytest --durations=0 && coverage report -m && coverage html && open htmlcov/index.html +coverage run --source ./metagpt -m pytest --durations=0 --timeout=100 && coverage report -m && coverage html && open htmlcov/index.html diff --git a/examples/example.faiss b/examples/example.faiss index a5a539dc4ec271205810dfaaab925d8f9cff74f0..58094619004ac7b01cf52e596e1bb4bf254f4322 100644 GIT binary patch literal 12333 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 diff --git a/examples/example.pkl b/examples/example.pkl index a0e839763b4f54093d471e1e06f107c8449f464a..f706fd803328b14547ee12efb4cf90f9fd2be99c 100644 GIT binary patch delta 175 zcmeys@_}VSv`Uhtv8knng}JU_T55`}i9xEluBCaJscvdgN@|Lcfth)tq2a{(pvlsV zdZIlXxv3?IDTyVCQ);L5usP-xm!}p@Ja0PLlTm@0KSOYG4x@vKahgT4Nm^QxZjyzm zsji7dvXO3Ll7*pel2LN1DUe}olxQ|3gG;j!Xof}xFNoluEYBpxmLXU>B|~`f14c{! U;tbIY&e9CAevl;4Tn{D<0H*mhe*gdg delta 174 zcmeys@_}VSw2Fbbp+TCZnSri(ib0~TiG_u^u7ydGg|3-NvWbzYS&~7rnd!v(pvhv4 z#-cqOxv3?IDTyVCQ);L5uodT*7A2=nJZ~`BlTm?LFoS<`4x@vKfl*?jSz?O0u9>+} zvaX4lv9YeDiIKUkX{u$av5{eFQmTQ;lngG-MxYrQ8N48Ze-e`=TZUlmlnmj?4;ZER Ui!($sI7>6c`hjvFV?CHO0Jt18yZ`_I diff --git a/setup.py b/setup.py index 29c44d3c1..a81be6115 100644 --- a/setup.py +++ b/setup.py @@ -39,6 +39,7 @@ extras_require["test"] = [ "pytest-mock", "pytest-html", "pytest-xdist", + "pytest-timeout", "connexion[uvicorn]~=3.0.5", "azure-cognitiveservices-speech~=1.31.0", "aioboto3~=11.3.0", diff --git a/tests/metagpt/serialize_deserialize/test_action.py b/tests/metagpt/serialize_deserialize/test_action.py index 677988e2f..81879e34e 100644 --- a/tests/metagpt/serialize_deserialize/test_action.py +++ b/tests/metagpt/serialize_deserialize/test_action.py @@ -27,6 +27,6 @@ async def test_action_deserialize(): new_action = Action(**serialized_data) - assert new_action.name == "" + assert new_action.name == "Action" assert isinstance(new_action.llm, type(LLM())) assert len(await new_action._aask("who are you")) > 0 diff --git a/tests/metagpt/serialize_deserialize/test_write_design.py b/tests/metagpt/serialize_deserialize/test_write_design.py index a2fce8047..7bcba3fc8 100644 --- a/tests/metagpt/serialize_deserialize/test_write_design.py +++ b/tests/metagpt/serialize_deserialize/test_write_design.py @@ -26,7 +26,7 @@ async def test_write_design_deserialize(): action = WriteDesign() serialized_data = action.model_dump() new_action = WriteDesign(**serialized_data) - assert new_action.name == "" + assert new_action.name == "WriteDesign" await new_action.run(with_messages="write a cli snake game") @@ -35,5 +35,5 @@ async def test_write_task_deserialize(): action = WriteTasks() serialized_data = action.model_dump() new_action = WriteTasks(**serialized_data) - assert new_action.name == "CreateTasks" + assert new_action.name == "WriteTasks" await new_action.run(with_messages="write a cli snake game") diff --git a/tests/metagpt/serialize_deserialize/test_write_docstring.py b/tests/metagpt/serialize_deserialize/test_write_docstring.py index 89ef6796b..e4116ab30 100644 --- a/tests/metagpt/serialize_deserialize/test_write_docstring.py +++ b/tests/metagpt/serialize_deserialize/test_write_docstring.py @@ -38,7 +38,7 @@ async def test_action_deserialize(style: str, part: str): new_action = WriteDocstring(**serialized_data) - assert not new_action.name + assert new_action.name == "WriteDocstring" assert new_action.desc == "Write docstring for code." ret = await new_action.run(code, style=style) assert part in ret