From 17916b84f696c28a8e2579da62b4cffb14a5c1ed Mon Sep 17 00:00:00 2001 From: Shashank Harinath <9397524+ShankHarinath@users.noreply.github.com> Date: Thu, 19 Oct 2023 00:52:53 -0700 Subject: [PATCH 01/11] Fix indentation --- metagpt/provider/openai_api.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py index 7e865f288..4c71e1077 100644 --- a/metagpt/provider/openai_api.py +++ b/metagpt/provider/openai_api.py @@ -111,19 +111,19 @@ class CostManager(metaclass=Singleton): return self.total_completion_tokens -def get_total_cost(self): - """ - Get the total cost of API calls. - - Returns: - float: The total cost of API calls. - """ - return self.total_cost - - -def get_costs(self) -> Costs: - """Get all costs""" - return Costs(self.total_prompt_tokens, self.total_completion_tokens, self.total_cost, self.total_budget) + def get_total_cost(self): + """ + Get the total cost of API calls. + + Returns: + float: The total cost of API calls. + """ + return self.total_cost + + + def get_costs(self) -> Costs: + """Get all costs""" + return Costs(self.total_prompt_tokens, self.total_completion_tokens, self.total_cost, self.total_budget) def log_and_reraise(retry_state): From 53fa9ef83844ac27a0c0c0d8a6d598428c183f94 Mon Sep 17 00:00:00 2001 From: Shashank Harinath Date: Thu, 19 Oct 2023 00:59:32 -0700 Subject: [PATCH 02/11] Rename file --- metagpt/provider/openai_api.py | 4 +--- metagpt/roles/__init__.py | 2 +- metagpt/roles/{seacher.py => searcher.py} | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) rename metagpt/roles/{seacher.py => searcher.py} (99%) diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py index 4c71e1077..f0110b148 100644 --- a/metagpt/provider/openai_api.py +++ b/metagpt/provider/openai_api.py @@ -110,7 +110,6 @@ class CostManager(metaclass=Singleton): """ return self.total_completion_tokens - def get_total_cost(self): """ Get the total cost of API calls. @@ -119,8 +118,7 @@ class CostManager(metaclass=Singleton): float: The total cost of API calls. """ return self.total_cost - - + def get_costs(self) -> Costs: """Get all costs""" return Costs(self.total_prompt_tokens, self.total_completion_tokens, self.total_cost, self.total_budget) diff --git a/metagpt/roles/__init__.py b/metagpt/roles/__init__.py index 1768b786c..f033a5dfa 100644 --- a/metagpt/roles/__init__.py +++ b/metagpt/roles/__init__.py @@ -12,7 +12,7 @@ from metagpt.roles.project_manager import ProjectManager from metagpt.roles.product_manager import ProductManager from metagpt.roles.engineer import Engineer from metagpt.roles.qa_engineer import QaEngineer -from metagpt.roles.seacher import Searcher +from metagpt.roles.searcher import Searcher from metagpt.roles.sales import Sales from metagpt.roles.customer_service import CustomerService diff --git a/metagpt/roles/seacher.py b/metagpt/roles/searcher.py similarity index 99% rename from metagpt/roles/seacher.py rename to metagpt/roles/searcher.py index 0b6e089da..3a19f32f2 100644 --- a/metagpt/roles/seacher.py +++ b/metagpt/roles/searcher.py @@ -3,7 +3,7 @@ """ @Time : 2023/5/23 17:25 @Author : alexanderwu -@File : seacher.py +@File : searcher.py """ from metagpt.actions import ActionOutput, SearchAndSummarize from metagpt.logs import logger From 9ccf1e8b82fe8b7b92acc81e170299750e578409 Mon Sep 17 00:00:00 2001 From: Bian Jiang Date: Wed, 1 Nov 2023 10:48:54 +0800 Subject: [PATCH 03/11] Fixed the workspace directory does not exist --- examples/agent_creator.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/agent_creator.py b/examples/agent_creator.py index 325e7c260..6269dac10 100644 --- a/examples/agent_creator.py +++ b/examples/agent_creator.py @@ -49,6 +49,8 @@ class CreateAgent(Action): pattern = r'```python(.*)```' match = re.search(pattern, rsp, re.DOTALL) code_text = match.group(1) if match else "" + if not WORKSPACE_ROOT.exists(): + WORKSPACE_ROOT.mkdir(parents=True) with open(WORKSPACE_ROOT / "agent_created_agent.py", "w") as f: f.write(code_text) return code_text From 7e8532037e9739f5e57877821cb73c98105306f9 Mon Sep 17 00:00:00 2001 From: Auster Cid Date: Sun, 12 Nov 2023 20:19:51 -0300 Subject: [PATCH 04/11] replaced wait_fixed with wait_exponential --- metagpt/actions/action.py | 4 ++-- metagpt/actions/write_code.py | 4 ++-- metagpt/actions/write_code_review.py | 4 ++-- metagpt/provider/openai_api.py | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/metagpt/actions/action.py b/metagpt/actions/action.py index 790295d55..f1a267468 100644 --- a/metagpt/actions/action.py +++ b/metagpt/actions/action.py @@ -9,7 +9,7 @@ import re from abc import ABC from typing import Optional -from tenacity import retry, stop_after_attempt, wait_fixed +from tenacity import retry, stop_after_attempt, wait_exponential from metagpt.actions.action_output import ActionOutput from metagpt.llm import LLM @@ -49,7 +49,7 @@ class Action(ABC): system_msgs.append(self.prefix) return await self.llm.aask(prompt, system_msgs) - @retry(stop=stop_after_attempt(3), wait=wait_fixed(1)) + @retry(stop=stop_after_attempt(4), wait=wait_exponential(10,60,3)) async def _aask_v1( self, prompt: str, diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index c000805c5..b9b2ab228 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -11,7 +11,7 @@ from metagpt.const import WORKSPACE_ROOT from metagpt.logs import logger from metagpt.schema import Message from metagpt.utils.common import CodeParser -from tenacity import retry, stop_after_attempt, wait_fixed +from tenacity import retry, stop_after_attempt, wait_exponential, wait_exponential PROMPT_TEMPLATE = """ NOTICE @@ -66,7 +66,7 @@ class WriteCode(Action): code_path.write_text(code) logger.info(f"Saving Code to {code_path}") - @retry(stop=stop_after_attempt(2), wait=wait_fixed(1)) + @retry(stop=stop_after_attempt(4), wait=wait_exponential(10,60,3)) async def write_code(self, prompt): code_rsp = await self._aask(prompt) code = CodeParser.parse_code(block="", text=code_rsp) diff --git a/metagpt/actions/write_code_review.py b/metagpt/actions/write_code_review.py index 4ff4d6cf6..84ccc96fc 100644 --- a/metagpt/actions/write_code_review.py +++ b/metagpt/actions/write_code_review.py @@ -10,7 +10,7 @@ from metagpt.actions.action import Action from metagpt.logs import logger from metagpt.schema import Message from metagpt.utils.common import CodeParser -from tenacity import retry, stop_after_attempt, wait_fixed +from tenacity import retry, stop_after_attempt, wait_exponential PROMPT_TEMPLATE = """ NOTICE @@ -65,7 +65,7 @@ class WriteCodeReview(Action): def __init__(self, name="WriteCodeReview", context: list[Message] = None, llm=None): super().__init__(name, context, llm) - @retry(stop=stop_after_attempt(2), wait=wait_fixed(1)) + @retry(stop=stop_after_attempt(4), wait=wait_exponential(10,60,3)) async def write_code(self, prompt): code_rsp = await self._aask(prompt) code = CodeParser.parse_code(block="", text=code_rsp) diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py index 6ebed2c16..fce19c16e 100644 --- a/metagpt/provider/openai_api.py +++ b/metagpt/provider/openai_api.py @@ -15,7 +15,7 @@ from tenacity import ( retry, retry_if_exception_type, stop_after_attempt, - wait_fixed, + wait_exponential, ) from metagpt.config import CONFIG @@ -226,8 +226,8 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter): return await self._achat_completion(messages) @retry( - stop=stop_after_attempt(3), - wait=wait_fixed(1), + stop=stop_after_attempt(4), + wait=wait_exponential(10,60,3), after=after_log(logger, logger.level("WARNING").name), retry=retry_if_exception_type(APIConnectionError), retry_error_callback=log_and_reraise, From 8f24808c8a39763b823bd85f643390ca458c7a5c Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Wed, 22 Nov 2023 14:25:07 +0800 Subject: [PATCH 05/11] make examples/search_kb.py work --- examples/search_kb.py | 26 +++++++++++++++++++++---- metagpt/actions/search_and_summarize.py | 1 - metagpt/document_store/faiss_store.py | 18 ++++++++++------- metagpt/roles/sales.py | 23 +++++++++++----------- 4 files changed, 44 insertions(+), 24 deletions(-) diff --git a/examples/search_kb.py b/examples/search_kb.py index b6f7d87a0..7a9911ca2 100644 --- a/examples/search_kb.py +++ b/examples/search_kb.py @@ -5,22 +5,40 @@ """ import asyncio +from metagpt.actions import Action from metagpt.const import DATA_PATH from metagpt.document_store import FaissStore from metagpt.logs import logger from metagpt.roles import Sales +from metagpt.schema import Message + +""" example.json, e.g. +[ + { + "source": "Which facial cleanser is good for oily skin?", + "output": "ABC cleanser is preferred by many with oily skin." + }, + { + "source": "Is L'Oreal good to use?", + "output": "L'Oreal is a popular brand with many positive reviews." + } +] +""" async def search(): - store = FaissStore(DATA_PATH / 'example.json') + store = FaissStore(DATA_PATH / "example.json") role = Sales(profile="Sales", store=store) - - queries = ["Which facial cleanser is good for oily skin?", "Is L'Oreal good to use?"] + role._watch({Action}) + queries = [ + Message("Which facial cleanser is good for oily skin?", cause_by=Action), + Message("Is L'Oreal good to use?", cause_by=Action), + ] for query in queries: logger.info(f"User: {query}") result = await role.run(query) logger.info(result) -if __name__ == '__main__': +if __name__ == "__main__": asyncio.run(search()) diff --git a/metagpt/actions/search_and_summarize.py b/metagpt/actions/search_and_summarize.py index 069f2a977..5e4cdaea0 100644 --- a/metagpt/actions/search_and_summarize.py +++ b/metagpt/actions/search_and_summarize.py @@ -140,4 +140,3 @@ class SearchAndSummarize(Action): logger.debug(prompt) logger.debug(result) return result - \ No newline at end of file diff --git a/metagpt/document_store/faiss_store.py b/metagpt/document_store/faiss_store.py index dd450010d..5c6459179 100644 --- a/metagpt/document_store/faiss_store.py +++ b/metagpt/document_store/faiss_store.py @@ -5,6 +5,7 @@ @Author : alexanderwu @File : faiss_store.py """ +import asyncio import pickle from pathlib import Path from typing import Optional @@ -20,7 +21,7 @@ from metagpt.logs import logger class FaissStore(LocalStore): - def __init__(self, raw_data: Path, cache_dir=None, meta_col='source', content_col='output'): + def __init__(self, raw_data: Path, cache_dir=None, meta_col="source", content_col="output"): self.meta_col = meta_col self.content_col = content_col super().__init__(raw_data, cache_dir) @@ -50,7 +51,7 @@ class FaissStore(LocalStore): pickle.dump(store, f) store.index = index - def search(self, query, expand_cols=False, sep='\n', *args, k=5, **kwargs): + def search(self, query, expand_cols=False, sep="\n", *args, k=5, **kwargs): rsp = self.store.similarity_search(query, k=k, **kwargs) logger.debug(rsp) if expand_cols: @@ -58,6 +59,9 @@ class FaissStore(LocalStore): else: return str(sep.join([f"{x.page_content}" for x in rsp])) + async def asearch(self, *args, **kwargs): + return await asyncio.to_thread(self.search, *args, **kwargs) + def write(self): """Initialize the index and library based on the Document (JSON / XLSX, etc.) file provided by the user.""" if not self.raw_data.exists(): @@ -78,8 +82,8 @@ class FaissStore(LocalStore): 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')) +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/metagpt/roles/sales.py b/metagpt/roles/sales.py index a45ad6f1b..d5aac1824 100644 --- a/metagpt/roles/sales.py +++ b/metagpt/roles/sales.py @@ -12,24 +12,23 @@ from metagpt.tools import SearchEngineType class Sales(Role): def __init__( - self, - name="Xiaomei", - profile="Retail sales guide", - desc="I am a sales guide in retail. My name is Xiaomei. I will answer some customer questions next, and I " - "will answer questions only based on the information in the knowledge base." - "If I feel that you can't get the answer from the reference material, then I will directly reply that" - " I don't know, and I won't tell you that this is from the knowledge base," - "but pretend to be what I know. Note that each of my replies will be replied in the tone of a " - "professional guide", - store=None + self, + name="Xiaomei", + profile="Retail sales guide", + desc="I am a sales guide in retail. My name is Xiaomei. I will answer some customer questions next, and I " + "will answer questions only based on the information in the knowledge base." + "If I feel that you can't get the answer from the reference material, then I will directly reply that" + " I don't know, and I won't tell you that this is from the knowledge base," + "but pretend to be what I know. Note that each of my replies will be replied in the tone of a " + "professional guide", + store=None, ): super().__init__(name, profile, desc=desc) self._set_store(store) def _set_store(self, store): if store: - action = SearchAndSummarize("", engine=SearchEngineType.CUSTOM_ENGINE, search_func=store.search) + action = SearchAndSummarize("", engine=SearchEngineType.CUSTOM_ENGINE, search_func=store.asearch) else: action = SearchAndSummarize() self._init_actions([action]) - \ No newline at end of file From 5ef3076f20ee9d3591090da709e33f9667026711 Mon Sep 17 00:00:00 2001 From: Auster Cid Date: Wed, 29 Nov 2023 12:04:05 -0300 Subject: [PATCH 06/11] reimplemented retries following suggestions in OpenAI's cookbook --- metagpt/actions/action.py | 4 ++-- metagpt/actions/write_code.py | 4 ++-- metagpt/actions/write_code_review.py | 4 ++-- metagpt/provider/openai_api.py | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/metagpt/actions/action.py b/metagpt/actions/action.py index f1a267468..6bdcc027d 100644 --- a/metagpt/actions/action.py +++ b/metagpt/actions/action.py @@ -9,7 +9,7 @@ import re from abc import ABC from typing import Optional -from tenacity import retry, stop_after_attempt, wait_exponential +from tenacity import retry, stop_after_attempt, wait_random_exponential from metagpt.actions.action_output import ActionOutput from metagpt.llm import LLM @@ -49,7 +49,7 @@ class Action(ABC): system_msgs.append(self.prefix) return await self.llm.aask(prompt, system_msgs) - @retry(stop=stop_after_attempt(4), wait=wait_exponential(10,60,3)) + @retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6)) async def _aask_v1( self, prompt: str, diff --git a/metagpt/actions/write_code.py b/metagpt/actions/write_code.py index b9b2ab228..a5dc8e059 100644 --- a/metagpt/actions/write_code.py +++ b/metagpt/actions/write_code.py @@ -11,7 +11,7 @@ from metagpt.const import WORKSPACE_ROOT from metagpt.logs import logger from metagpt.schema import Message from metagpt.utils.common import CodeParser -from tenacity import retry, stop_after_attempt, wait_exponential, wait_exponential +from tenacity import retry, stop_after_attempt, wait_random_exponential PROMPT_TEMPLATE = """ NOTICE @@ -66,7 +66,7 @@ class WriteCode(Action): code_path.write_text(code) logger.info(f"Saving Code to {code_path}") - @retry(stop=stop_after_attempt(4), wait=wait_exponential(10,60,3)) + @retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6)) async def write_code(self, prompt): code_rsp = await self._aask(prompt) code = CodeParser.parse_code(block="", text=code_rsp) diff --git a/metagpt/actions/write_code_review.py b/metagpt/actions/write_code_review.py index 84ccc96fc..06282411a 100644 --- a/metagpt/actions/write_code_review.py +++ b/metagpt/actions/write_code_review.py @@ -10,7 +10,7 @@ from metagpt.actions.action import Action from metagpt.logs import logger from metagpt.schema import Message from metagpt.utils.common import CodeParser -from tenacity import retry, stop_after_attempt, wait_exponential +from tenacity import retry, stop_after_attempt, wait_random_exponential PROMPT_TEMPLATE = """ NOTICE @@ -65,7 +65,7 @@ class WriteCodeReview(Action): def __init__(self, name="WriteCodeReview", context: list[Message] = None, llm=None): super().__init__(name, context, llm) - @retry(stop=stop_after_attempt(4), wait=wait_exponential(10,60,3)) + @retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6)) async def write_code(self, prompt): code_rsp = await self._aask(prompt) code = CodeParser.parse_code(block="", text=code_rsp) diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py index fce19c16e..fa9397f20 100644 --- a/metagpt/provider/openai_api.py +++ b/metagpt/provider/openai_api.py @@ -15,7 +15,7 @@ from tenacity import ( retry, retry_if_exception_type, stop_after_attempt, - wait_exponential, + wait_random_exponential, ) from metagpt.config import CONFIG @@ -226,8 +226,8 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter): return await self._achat_completion(messages) @retry( - stop=stop_after_attempt(4), - wait=wait_exponential(10,60,3), + wait=wait_random_exponential(min=1, max=60), + stop=stop_after_attempt(6), after=after_log(logger, logger.level("WARNING").name), retry=retry_if_exception_type(APIConnectionError), retry_error_callback=log_and_reraise, From dfc6e13ac3b2888e95574c2ff38b7a038cc9938d Mon Sep 17 00:00:00 2001 From: shenchucheng Date: Fri, 1 Dec 2023 16:10:38 +0800 Subject: [PATCH 07/11] add agent subscription --- metagpt/subscription.py | 101 ++++++++++++++++++++++++++++ tests/conftest.py | 16 ++++- tests/metagpt/test_subscription.py | 102 +++++++++++++++++++++++++++++ 3 files changed, 217 insertions(+), 2 deletions(-) create mode 100644 metagpt/subscription.py create mode 100644 tests/metagpt/test_subscription.py diff --git a/metagpt/subscription.py b/metagpt/subscription.py new file mode 100644 index 000000000..0d2b30821 --- /dev/null +++ b/metagpt/subscription.py @@ -0,0 +1,101 @@ +import asyncio +from typing import AsyncGenerator, Awaitable, Callable + +from pydantic import BaseModel, Field + +from metagpt.logs import logger +from metagpt.roles import Role +from metagpt.schema import Message + + +class SubscriptionRunner(BaseModel): + """A simple wrapper to manage subscription tasks for different roles using asyncio. + + Example: + >>> import asyncio + >>> from metagpt.subscription import SubscriptionRunner + >>> from metagpt.roles import Searcher + >>> from metagpt.schema import Message + + >>> async def trigger(): + ... while True: + ... yield Message("the latest news about OpenAI") + ... await asyncio.sleep(3600 * 24) + + >>> async def callback(msg: Message): + ... print(msg.content) + + >>> async def main(): + ... pb = SubscriptionRunner() + ... await pb.subscribe(Searcher(), trigger(), callback) + ... await pb.run() + + >>> asyncio.run(main()) + """ + + tasks: dict[Role, asyncio.Task] = Field(default_factory=dict) + + class Config: + arbitrary_types_allowed = True + + async def subscribe( + self, + role: Role, + trigger: AsyncGenerator[Message, None], + callback: Callable[ + [ + Message, + ], + Awaitable[None], + ], + ): + """Subscribes a role to a trigger and sets up a callback to be called with the role's response. + + Args: + role: The role to subscribe. + trigger: An asynchronous generator that yields Messages to be processed by the role. + callback: An asynchronous function to be called with the response from the role. + """ + loop = asyncio.get_running_loop() + + async def _start_role(): + async for msg in trigger: + resp = await role.run(msg) + await callback(resp) + + self.tasks[role] = loop.create_task(_start_role(), name=f"Subscription-{role}") + + async def unsubscribe(self, role: Role): + """Unsubscribes a role from its trigger and cancels the associated task. + + Args: + role: The role to unsubscribe. + """ + task = self.tasks.pop(role) + task.cancel() + + async def run(self, raise_exception: bool = True): + """Runs all subscribed tasks and handles their completion or exception. + + Args: + raise_exception: _description_. Defaults to True. + + Raises: + task.exception: _description_ + """ + while True: + for role, task in self.tasks.items(): + if task.done(): + if task.exception(): + if raise_exception: + raise task.exception() + logger.opt(exception=task.exception()).error(f"Task {task.get_name()} run error") + else: + logger.warning( + f"Task {task.get_name()} has completed. " + "If this is unexpected behavior, please check the trigger function." + ) + self.tasks.pop(role) + break + else: + await asyncio.sleep(1) diff --git a/tests/conftest.py b/tests/conftest.py index feecc7715..804c60e71 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,14 +6,15 @@ @File : conftest.py """ +import asyncio +import logging +import re from unittest.mock import Mock import pytest from metagpt.logs import logger from metagpt.provider.openai_api import OpenAIGPTAPI as GPTAPI -import asyncio -import re class Context: @@ -68,3 +69,14 @@ def proxy(): server = asyncio.get_event_loop().run_until_complete(asyncio.start_server(handle_client, "127.0.0.1", 0)) return "http://{}:{}".format(*server.sockets[0].getsockname()) + + +# see https://github.com/Delgan/loguru/issues/59#issuecomment-466591978 +@pytest.fixture +def loguru_caplog(caplog): + class PropogateHandler(logging.Handler): + def emit(self, record): + logging.getLogger(record.name).handle(record) + + logger.add(PropogateHandler(), format="{message}") + yield caplog diff --git a/tests/metagpt/test_subscription.py b/tests/metagpt/test_subscription.py new file mode 100644 index 000000000..2e898424d --- /dev/null +++ b/tests/metagpt/test_subscription.py @@ -0,0 +1,102 @@ +import asyncio + +import pytest + +from metagpt.roles import Role +from metagpt.schema import Message +from metagpt.subscription import SubscriptionRunner + + +@pytest.mark.asyncio +async def test_subscription_run(): + callback_done = 0 + + async def trigger(): + while True: + yield Message("the latest news about OpenAI") + await asyncio.sleep(3600 * 24) + + class MockRole(Role): + async def run(self, message=None): + return Message("") + + async def callback(message): + nonlocal callback_done + callback_done += 1 + + runner = SubscriptionRunner() + + roles = [] + for _ in range(2): + role = MockRole() + roles.append(role) + await runner.subscribe(role, trigger(), callback) + + task = asyncio.get_running_loop().create_task(runner.run()) + + for _ in range(10): + if callback_done == 2: + break + await asyncio.sleep(0) + else: + raise TimeoutError("callback not call") + + role = roles[0] + assert role in runner.tasks + await runner.unsubscribe(roles[0]) + + for _ in range(10): + if role not in runner.tasks: + break + await asyncio.sleep(0) + else: + raise TimeoutError("callback not call") + + task.cancel() + for i in runner.tasks.values(): + i.cancel() + + +@pytest.mark.asyncio +async def test_subscription_run_error(loguru_caplog): + async def trigger1(): + while True: + yield Message("the latest news about OpenAI") + await asyncio.sleep(3600 * 24) + + async def trigger2(): + yield Message("the latest news about OpenAI") + + class MockRole1(Role): + async def run(self, message=None): + raise RuntimeError + + class MockRole2(Role): + async def run(self, message=None): + return Message("") + + async def callback(msg: Message): + print(msg) + + runner = SubscriptionRunner() + await runner.subscribe(MockRole1(), trigger1(), callback) + with pytest.raises(RuntimeError): + await runner.run() + + await runner.subscribe(MockRole2(), trigger2(), callback) + task = asyncio.get_running_loop().create_task(runner.run(False)) + + for _ in range(10): + if not runner.tasks: + break + await asyncio.sleep(0) + else: + raise TimeoutError("wait runner tasks empty timeout") + + task.cancel() + for i in runner.tasks.values(): + i.cancel() + assert len(loguru_caplog.records) >= 2 + logs = "".join(loguru_caplog.messages) + assert "run error" in logs + assert "has completed" in logs From 70fcf354925aa68f9260e06c7f84dc81f4ce233c Mon Sep 17 00:00:00 2001 From: garylin2099 Date: Fri, 8 Dec 2023 15:29:11 +0800 Subject: [PATCH 08/11] openai requirement bug fix --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f0169d7fa..14a9f485d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ langchain==0.0.231 loguru==0.6.0 meilisearch==0.21.0 numpy==1.24.3 -openai>=0.28.0 +openai==0.28.0 openpyxl beautifulsoup4==4.12.2 pandas==2.0.3 From 4a9b85f268b3a0d0a01bbc7b1fc997862fe7449a Mon Sep 17 00:00:00 2001 From: garylin2099 Date: Fri, 8 Dec 2023 15:30:02 +0800 Subject: [PATCH 09/11] update version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 239156ae3..494a4614d 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ with open(path.join(here, "requirements.txt"), encoding="utf-8") as f: setup( name="metagpt", - version="0.3.0", + version="0.4.0", description="The Multi-Role Meta Programming Framework", long_description=long_description, long_description_content_type="text/markdown", From 6f1ff01e0c58c02107558bf9466e6167db05bfae Mon Sep 17 00:00:00 2001 From: garylin2099 Date: Mon, 11 Dec 2023 11:52:50 +0800 Subject: [PATCH 10/11] roadmap update as of v0.4.0 --- docs/ROADMAP.md | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 005a59ab2..afc9ff445 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -16,12 +16,12 @@ ### Tasks To reach version v0.5, approximately 70% of the following tasks need to be completed. 1. Usability - 1. Release v0.01 pip package to try to solve issues like npm installation (though not necessarily successfully) + 1. ~~Release v0.01 pip package to try to solve issues like npm installation (though not necessarily successfully)~~ (v0.3.0) 2. Support for overall save and recovery of software companies - 3. Support human confirmation and modification during the process + 3. ~~Support human confirmation and modification during the process~~ (v0.3.0) New: Support human confirmation and modification with fewer constrainsts and a more user-friendly interface 4. Support process caching: Consider carefully whether to add server caching mechanism - 5. Resolve occasional failure to follow instruction under current prompts, causing code parsing errors, through stricter system prompts - 6. Write documentation, describing the current features and usage at all levels + 5. ~~Resolve occasional failure to follow instruction under current prompts, causing code parsing errors, through stricter system prompts~~ (v0.4.0, with function call) + 6. Write documentation, describing the current features and usage at all levels (ongoing, continuously adding contents to [documentation site](https://docs.deepwisdom.ai/guide/get_started/introduction.html)) 7. ~~Support Docker~~ 2. Features 1. Support a more standard and stable parser (need to analyze the format that the current LLM is better at) @@ -30,31 +30,33 @@ ### Tasks 4. Complete the design and implementation of module breakdown 5. Support various modes of memory: clearly distinguish between long-term and short-term memory 6. Perfect the test role, and carry out necessary interactions with humans - 7. Provide full mode instead of the current fast mode, allowing natural communication between roles - 8. Implement SkillManager and the process of incremental Skill learning + 7. Allowing natural communication between roles (expected v0.5.0) + 8. Implement SkillManager and the process of incremental Skill learning (experimentation done with game agents) 9. Automatically get RPM and configure it by calling the corresponding openai page, so that each key does not need to be manually configured + 10. IMPORTANT: Support incremental development (expected v0.5.0) 3. Strategies - 1. Support ReAct strategy - 2. Support CoT strategy + 1. Support ReAct strategy (experimentation done with game agents) + 2. Support CoT strategy (experimentation done with game agents) 3. Support ToT strategy - 4. Support Reflection strategy + 4. Support Reflection strategy (experimentation done with game agents) + 5. Support planning 4. Actions - 1. Implementation: Search + 1. ~~Implementation: Search~~ (v0.2.1) 2. Implementation: Knowledge search, supporting 10+ data formats - 3. Implementation: Data EDA + 3. Implementation: Data EDA (expected v0.6.0) 4. Implementation: Review - 5. Implementation: Add Document - 6. Implementation: Delete Document + 5. Implementation: Add Document (expected v0.5.0) + 6. Implementation: Delete Document (expected v0.5.0) 7. Implementation: Self-training - 8. Implementation: DebugError + 8. ~~Implementation: DebugError~~ (v0.2.1) 9. Implementation: Generate reliable unit tests based on YAPI 10. Implementation: Self-evaluation 11. Implementation: AI Invocation 12. Implementation: Learning and using third-party standard libraries 13. Implementation: Data collection 14. Implementation: AI training - 15. Implementation: Run code - 16. Implementation: Web access + 15. ~~Implementation: Run code~~ (v0.2.1) + 16. ~~Implementation: Web access~~ (v0.2.1) 5. Plugins: Compatibility with plugin system 6. Tools 1. ~~Support SERPER api~~ @@ -64,13 +66,13 @@ ### Tasks 1. Perfect the action pool/skill pool for each role 2. Red Book blogger 3. E-commerce seller - 4. Data analyst + 4. Data analyst (expected v0.6.0) 5. News observer - 6. Institutional researcher + 6. ~~Institutional researcher~~ (v0.2.1) 8. Evaluation - 1. Support an evaluation on a game dataset - 2. Reproduce papers, implement full skill acquisition for a single game role, achieving SOTA results - 3. Support an evaluation on a math dataset + 1. Support an evaluation on a game dataset (experimentation done with game agents) + 2. Reproduce papers, implement full skill acquisition for a single game role, achieving SOTA results (experimentation done with game agents) + 3. Support an evaluation on a math dataset (expected v0.6.0) 4. Reproduce papers, achieving SOTA results for current mathematical problem solving process 9. LLM 1. Support Claude underlying API From 9d922941cf468e89a3202ce230e20f878e22f072 Mon Sep 17 00:00:00 2001 From: Yi Lin Date: Mon, 11 Dec 2023 22:42:13 +0800 Subject: [PATCH 11/11] add gpt-4-turbo and gpt-3-turbo-1106 in token count Signed-off-by: Yi Lin --- metagpt/utils/token_counter.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/metagpt/utils/token_counter.py b/metagpt/utils/token_counter.py index 1af96f272..ba63e90a9 100644 --- a/metagpt/utils/token_counter.py +++ b/metagpt/utils/token_counter.py @@ -16,11 +16,13 @@ TOKEN_COSTS = { "gpt-3.5-turbo-0613": {"prompt": 0.0015, "completion": 0.002}, "gpt-3.5-turbo-16k": {"prompt": 0.003, "completion": 0.004}, "gpt-3.5-turbo-16k-0613": {"prompt": 0.003, "completion": 0.004}, + "gpt-3.5-turbo-1106": {"prompt": 0.001, "completion": 0.002}, "gpt-4-0314": {"prompt": 0.03, "completion": 0.06}, "gpt-4": {"prompt": 0.03, "completion": 0.06}, "gpt-4-32k": {"prompt": 0.06, "completion": 0.12}, "gpt-4-32k-0314": {"prompt": 0.06, "completion": 0.12}, "gpt-4-0613": {"prompt": 0.06, "completion": 0.12}, + "gpt-4-1106-preview": {"prompt": 0.01, "completion": 0.03}, "text-embedding-ada-002": {"prompt": 0.0004, "completion": 0.0}, "chatglm_turbo": {"prompt": 0.0, "completion": 0.00069} # 32k version, prompt + completion tokens=0.005¥/k-tokens } @@ -32,11 +34,13 @@ TOKEN_MAX = { "gpt-3.5-turbo-0613": 4096, "gpt-3.5-turbo-16k": 16384, "gpt-3.5-turbo-16k-0613": 16384, + "gpt-3.5-turbo-1106": 16384, "gpt-4-0314": 8192, "gpt-4": 8192, "gpt-4-32k": 32768, "gpt-4-32k-0314": 32768, "gpt-4-0613": 8192, + "gpt-4-1106-preview": 128000, "text-embedding-ada-002": 8192, "chatglm_turbo": 32768 } @@ -52,10 +56,12 @@ def count_message_tokens(messages, model="gpt-3.5-turbo-0613"): if model in { "gpt-3.5-turbo-0613", "gpt-3.5-turbo-16k-0613", + "gpt-3.5-turbo-1106", "gpt-4-0314", "gpt-4-32k-0314", "gpt-4-0613", "gpt-4-32k-0613", + "gpt-4-1106-preview", }: tokens_per_message = 3 tokens_per_name = 1