From 5649fac62dca8a3e24439edb70ff9a4a20096735 Mon Sep 17 00:00:00 2001 From: geekan Date: Tue, 2 Jan 2024 18:14:03 +0800 Subject: [PATCH 01/15] 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 02/15] 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 03/15] 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 04/15] 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 05/15] 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 06/15] 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 07/15] 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 08/15] 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 09/15] 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 10/15] 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 11/15] 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 12/15] 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 13/15] 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 14/15] 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 15/15] 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)