From 37f2c825f70f8ff609ed6d8141e5c3e28b8c0b66 Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Fri, 22 Dec 2023 00:07:06 +0900 Subject: [PATCH 01/28] Update README.md exisiting -> existing --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a03c1eabf..dcc56caf8 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ # MetaGPT: The Multi-Agent Framework

Software Company Multi-Role Schematic (Gradually Implementing)

## News -- Dec 15: [v0.5.0](https://github.com/geekan/MetaGPT/releases/tag/v0.5.0) is released! We introduce **incremental development**, facilitating agents to build up larger projects on top of their previous efforts or exisiting codebase. We also launch a whole collection of important features, including **multilingual support** (experimental), multiple **programming languages support** (experimental), **incremental development** (experimental), CLI support, pip support, enhanced code review, documentation mechanism, and optimized messaging mechanism! +- Dec 15: [v0.5.0](https://github.com/geekan/MetaGPT/releases/tag/v0.5.0) is released! We introduce **incremental development**, facilitating agents to build up larger projects on top of their previous efforts or existing codebase. We also launch a whole collection of important features, including **multilingual support** (experimental), multiple **programming languages support** (experimental), **incremental development** (experimental), CLI support, pip support, enhanced code review, documentation mechanism, and optimized messaging mechanism! ## Install From 4b0cb0084a438afab8f9a4c213700d12c838d65e Mon Sep 17 00:00:00 2001 From: better629 Date: Fri, 22 Dec 2023 02:20:43 +0800 Subject: [PATCH 02/28] add ollama support --- config/config.yaml | 4 + metagpt/config.py | 6 +- metagpt/const.py | 2 + metagpt/provider/__init__.py | 3 +- metagpt/provider/general_api_base.py | 65 ++++---- metagpt/provider/general_api_requestor.py | 52 +++++- metagpt/provider/ollama_api.py | 151 ++++++++++++++++++ metagpt/utils/repair_llm_raw_output.py | 2 + .../provider/test_google_gemini_api.py | 2 +- tests/metagpt/provider/test_ollama_api.py | 33 ++++ 10 files changed, 284 insertions(+), 36 deletions(-) create mode 100644 metagpt/provider/ollama_api.py create mode 100644 tests/metagpt/provider/test_ollama_api.py diff --git a/config/config.yaml b/config/config.yaml index e724897ee..a9c764c56 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -48,6 +48,10 @@ RPM: 10 #FIREWORKS_API_BASE: "https://api.fireworks.ai/inference/v1" #FIREWORKS_API_MODEL: "YOUR_LLM_MODEL" # example, accounts/fireworks/models/llama-v2-13b-chat +#### if use self-host open llm model by ollama +# OLLAMA_API_BASE: http://127.0.0.1:11434/api +# OLLAMA_API_MODEL: llama2 + #### for Search ## Supported values: serpapi/google/serper/ddg diff --git a/metagpt/config.py b/metagpt/config.py index 5176a7677..208b4fd7b 100644 --- a/metagpt/config.py +++ b/metagpt/config.py @@ -42,6 +42,7 @@ class LLMProviderEnum(Enum): FIREWORKS = "fireworks" OPEN_LLM = "open_llm" GEMINI = "gemini" + OLLAMA = "ollama" class Config(metaclass=Singleton): @@ -78,7 +79,8 @@ class Config(metaclass=Singleton): (self.zhipuai_api_key, LLMProviderEnum.ZHIPUAI), (self.fireworks_api_key, LLMProviderEnum.FIREWORKS), (self.open_llm_api_base, LLMProviderEnum.OPEN_LLM), - (self.gemini_api_key, LLMProviderEnum.GEMINI), # reuse logic. but not a key + (self.gemini_api_key, LLMProviderEnum.GEMINI), + (self.ollama_api_base, LLMProviderEnum.OLLAMA), # reuse logic. but not a key ]: if self._is_valid_llm_key(k): # logger.debug(f"Use LLMProvider: {v.value}") @@ -103,6 +105,8 @@ class Config(metaclass=Singleton): self.open_llm_api_model = self._get("OPEN_LLM_API_MODEL") self.fireworks_api_key = self._get("FIREWORKS_API_KEY") self.gemini_api_key = self._get("GEMINI_API_KEY") + self.ollama_api_base = self._get("OLLAMA_API_BASE") + self.ollama_api_model = self._get("OLLAMA_API_MODEL") _ = self.get_default_llm_provider_enum() self.openai_base_url = self._get("OPENAI_BASE_URL") diff --git a/metagpt/const.py b/metagpt/const.py index 3b4f2ae4b..1819bbb49 100644 --- a/metagpt/const.py +++ b/metagpt/const.py @@ -102,3 +102,5 @@ CODE_SUMMARIES_FILE_REPO = "docs/code_summaries" CODE_SUMMARIES_PDF_FILE_REPO = "resources/code_summaries" YAPI_URL = "http://yapi.deepwisdomai.com/" + +LLM_API_TIMEOUT = 300 diff --git a/metagpt/provider/__init__.py b/metagpt/provider/__init__.py index a9f46eb03..42626a551 100644 --- a/metagpt/provider/__init__.py +++ b/metagpt/provider/__init__.py @@ -8,8 +8,9 @@ from metagpt.provider.fireworks_api import FireWorksGPTAPI from metagpt.provider.google_gemini_api import GeminiGPTAPI +from metagpt.provider.ollama_api import OllamaGPTAPI from metagpt.provider.open_llm_api import OpenLLMGPTAPI from metagpt.provider.openai_api import OpenAIGPTAPI from metagpt.provider.zhipuai_api import ZhiPuAIGPTAPI -__all__ = ["FireWorksGPTAPI", "GeminiGPTAPI", "OpenLLMGPTAPI", "OpenAIGPTAPI", "ZhiPuAIGPTAPI"] +__all__ = ["FireWorksGPTAPI", "GeminiGPTAPI", "OpenLLMGPTAPI", "OpenAIGPTAPI", "ZhiPuAIGPTAPI", "OllamaGPTAPI"] diff --git a/metagpt/provider/general_api_base.py b/metagpt/provider/general_api_base.py index da16e942d..015e34aeb 100644 --- a/metagpt/provider/general_api_base.py +++ b/metagpt/provider/general_api_base.py @@ -1,3 +1,7 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# @Desc : refs to openai 0.x sdk + import asyncio import json import os @@ -43,8 +47,8 @@ MAX_CONNECTION_RETRIES = 2 # Has one attribute per thread, 'session'. _thread_context = threading.local() -OPENAI_LOG = os.environ.get("OPENAI_LOG") -OPENAI_LOG = "debug" +LLM_LOG = os.environ.get("LLM_LOG") +LLM_LOG = "debug" class ApiType(Enum): @@ -74,8 +78,8 @@ api_key_to_header = ( def _console_log_level(): - if OPENAI_LOG in ["debug", "info"]: - return OPENAI_LOG + if LLM_LOG in ["debug", "info"]: + return LLM_LOG else: return None @@ -140,7 +144,7 @@ class OpenAIResponse: @property def organization(self) -> Optional[str]: - return self._headers.get("OpenAI-Organization") + return self._headers.get("LLM-Organization") @property def response_ms(self) -> Optional[int]: @@ -478,7 +482,7 @@ class APIRequestor: error_data["message"] += "\n\n" + error_data["internal_message"] log_info( - "OpenAI API error received", + "LLM API error received", error_code=error_data.get("code"), error_type=error_data.get("type"), error_message=error_data.get("message"), @@ -516,7 +520,7 @@ class APIRequestor: ) def request_headers(self, method: str, extra, request_id: Optional[str]) -> Dict[str, str]: - user_agent = "OpenAI/v1 PythonBindings/%s" % (version.VERSION,) + user_agent = "LLM/v1 PythonBindings/%s" % (version.VERSION,) uname_without_node = " ".join(v for k, v in platform.uname()._asdict().items() if k != "node") ua = { @@ -530,17 +534,17 @@ class APIRequestor: } headers = { - "X-OpenAI-Client-User-Agent": json.dumps(ua), + "X-LLM-Client-User-Agent": json.dumps(ua), "User-Agent": user_agent, } headers.update(api_key_to_header(self.api_type, self.api_key)) if self.organization: - headers["OpenAI-Organization"] = self.organization + headers["LLM-Organization"] = self.organization if self.api_version is not None and self.api_type == ApiType.OPEN_AI: - headers["OpenAI-Version"] = self.api_version + headers["LLM-Version"] = self.api_version if request_id is not None: headers["X-Request-Id"] = request_id headers.update(extra) @@ -592,15 +596,14 @@ class APIRequestor: headers["Content-Type"] = "application/json" else: raise openai.APIConnectionError( - "Unrecognized HTTP method %r. This may indicate a bug in the " - "OpenAI bindings. Please contact us through our help center at help.openai.com for " - "assistance." % (method,) + message=f"Unrecognized HTTP method {method}. This may indicate a bug in the LLM bindings.", + request=None, ) headers = self.request_headers(method, headers, request_id) - log_debug("Request to OpenAI API", method=method, path=abs_url) - log_debug("Post details", data=data, api_version=self.api_version) + # log_debug("Request to LLM API", method=method, path=abs_url) + # log_debug("Post details", data=data, api_version=self.api_version) return abs_url, headers, data @@ -639,14 +642,14 @@ class APIRequestor: except requests.exceptions.Timeout as e: raise openai.APITimeoutError("Request timed out: {}".format(e)) from e except requests.exceptions.RequestException as e: - raise openai.APIConnectionError("Error communicating with OpenAI: {}".format(e)) from e - log_debug( - "OpenAI API response", - path=abs_url, - response_code=result.status_code, - processing_ms=result.headers.get("OpenAI-Processing-Ms"), - request_id=result.headers.get("X-Request-Id"), - ) + raise openai.APIConnectionError(message="Error communicating with LLM: {}".format(e), request=None) from e + # log_debug( + # "LLM API response", + # path=abs_url, + # response_code=result.status_code, + # processing_ms=result.headers.get("LLM-Processing-Ms"), + # request_id=result.headers.get("X-Request-Id"), + # ) return result async def arequest_raw( @@ -685,18 +688,18 @@ class APIRequestor: } try: result = await session.request(**request_kwargs) - log_info( - "OpenAI API response", - path=abs_url, - response_code=result.status, - processing_ms=result.headers.get("OpenAI-Processing-Ms"), - request_id=result.headers.get("X-Request-Id"), - ) + # log_info( + # "LLM API response", + # path=abs_url, + # response_code=result.status, + # processing_ms=result.headers.get("LLM-Processing-Ms"), + # request_id=result.headers.get("X-Request-Id"), + # ) return result except (aiohttp.ServerTimeoutError, asyncio.TimeoutError) as e: raise openai.APITimeoutError("Request timed out") from e except aiohttp.ClientError as e: - raise openai.APIConnectionError("Error communicating with OpenAI") from e + raise openai.APIConnectionError(message="Error communicating with LLM", request=None) from e def _interpret_response( self, result: requests.Response, stream: bool diff --git a/metagpt/provider/general_api_requestor.py b/metagpt/provider/general_api_requestor.py index f8321cc6b..8b06b9388 100644 --- a/metagpt/provider/general_api_requestor.py +++ b/metagpt/provider/general_api_requestor.py @@ -3,14 +3,38 @@ # @Desc : General Async API for http-based LLM model import asyncio -from typing import AsyncGenerator, Tuple, Union +from typing import AsyncGenerator, Generator, Iterator, Optional, Tuple, Union import aiohttp +import requests from metagpt.logs import logger from metagpt.provider.general_api_base import APIRequestor +def parse_stream_helper(line: bytes) -> Optional[str]: + if line and line.startswith(b"data:"): + if line.startswith(b"data: "): + # SSE event may be valid when it contain whitespace + line = line[len(b"data: ") :] + else: + line = line[len(b"data:") :] + if line.strip() == b"[DONE]": + # return here will cause GeneratorExit exception in urllib3 + # and it will close http connection with TCP Reset + return None + else: + return line.decode("utf-8") + return None + + +def parse_stream(rbody: Iterator[bytes]) -> Iterator[str]: + for line in rbody: + _line = parse_stream_helper(line) + if _line is not None: + yield _line + + class GeneralAPIRequestor(APIRequestor): """ usage @@ -32,10 +56,34 @@ class GeneralAPIRequestor(APIRequestor): return rbody + def _interpret_response( + self, result: requests.Response, stream: bool + ) -> Tuple[Union[str, Iterator[Generator]], bool]: + """Returns the response(s) and a bool indicating whether it is a stream.""" + if stream and "text/event-stream" in result.headers.get("Content-Type", ""): + return ( + self._interpret_response_line(line, result.status_code, result.headers, stream=True) + for line in parse_stream(result.iter_lines()) + ), True + else: + return ( + self._interpret_response_line( + result.content, # let the caller to decode the msg + result.status_code, + result.headers, + stream=False, + ), + False, + ) + async def _interpret_async_response( self, result: aiohttp.ClientResponse, stream: bool ) -> Tuple[Union[str, AsyncGenerator[str, None]], bool]: - if stream and "text/event-stream" in result.headers.get("Content-Type", ""): + if stream and ( + "text/event-stream" in result.headers.get("Content-Type", "") + or "application/x-ndjson" in result.headers.get("Content-Type", "") + ): + # the `Content-Type` of ollama stream resp is "application/x-ndjson" return ( self._interpret_response_line(line, result.status, result.headers, stream=True) async for line in result.content diff --git a/metagpt/provider/ollama_api.py b/metagpt/provider/ollama_api.py new file mode 100644 index 000000000..a15c46458 --- /dev/null +++ b/metagpt/provider/ollama_api.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# @Desc : self-host open llm model with ollama which isn't openai-api-compatible + +import json + +from requests import ConnectionError +from tenacity import ( + after_log, + retry, + retry_if_exception_type, + stop_after_attempt, + wait_random_exponential, +) + +from metagpt.config import CONFIG, LLMProviderEnum +from metagpt.const import LLM_API_TIMEOUT +from metagpt.logs import logger +from metagpt.provider.base_gpt_api import BaseGPTAPI +from metagpt.provider.general_api_requestor import GeneralAPIRequestor +from metagpt.provider.llm_provider_registry import register_provider +from metagpt.provider.openai_api import CostManager, log_and_reraise + + +class OllamaCostManager(CostManager): + def update_cost(self, prompt_tokens, completion_tokens, model): + """ + Update the total cost, prompt tokens, and completion tokens. + """ + self.total_prompt_tokens += prompt_tokens + self.total_completion_tokens += completion_tokens + + logger.info( + f"Max budget: ${CONFIG.max_budget:.3f} | " + f"prompt_tokens: {prompt_tokens}, completion_tokens: {completion_tokens}" + ) + CONFIG.total_cost = self.total_cost + + +@register_provider(LLMProviderEnum.OLLAMA) +class OllamaGPTAPI(BaseGPTAPI): + """ + Refs to `https://github.com/jmorganca/ollama/blob/main/docs/api.md#generate-a-chat-completion` + """ + + def __init__(self): + self.__init_ollama(CONFIG) + self.client = GeneralAPIRequestor(base_url=CONFIG.ollama_api_base) + self.suffix_url = "/chat" + self.http_method = "post" + self.use_system_prompt = False + self._cost_manager = OllamaCostManager() + + def __init_ollama(self, config: CONFIG): + assert config.ollama_api_base + + self.model = config.ollama_api_model + + def _const_kwargs(self, messages: list[dict], stream: bool = False) -> dict: + kwargs = {"model": self.model, "messages": messages, "options": {"temperature": 0.3}, "stream": stream} + return kwargs + + def _update_costs(self, usage: dict): + """update each request's token cost""" + if CONFIG.calc_usage: + try: + prompt_tokens = int(usage.get("prompt_tokens", 0)) + completion_tokens = int(usage.get("completion_tokens", 0)) + self._cost_manager.update_cost(prompt_tokens, completion_tokens, self.model) + except Exception as e: + logger.error(f"ollama updats costs failed! exp: {e}") + + def get_choice_text(self, resp: dict) -> str: + """get the resp content from llm response""" + assist_msg = resp.get("message", {}) + assert assist_msg.get("role", None) == "assistant" + return assist_msg.get("content") + + def get_usage(self, resp: dict) -> dict: + return {"prompt_tokens": resp.get("prompt_eval_count", 0), "completion_tokens": resp.get("eval_count", 0)} + + def _decode_and_load(self, chunk: bytes, encoding: str = "utf-8") -> dict: + chunk = chunk.decode(encoding) + return json.loads(chunk) + + def completion(self, messages: list[dict]) -> dict: + resp, _, _ = self.client.request( + method=self.http_method, + url=self.suffix_url, + params=self._const_kwargs(messages), + request_timeout=LLM_API_TIMEOUT, + ) + resp = self._decode_and_load(resp) + usage = self.get_usage(resp) + self._update_costs(usage) + return resp + + async def _achat_completion(self, messages: list[dict]) -> dict: + resp, _, _ = await self.client.arequest( + method=self.http_method, + url=self.suffix_url, + params=self._const_kwargs(messages), + request_timeout=LLM_API_TIMEOUT, + ) + resp = self._decode_and_load(resp) + usage = self.get_usage(resp) + self._update_costs(usage) + return resp + + async def acompletion(self, messages: list[dict]) -> dict: + return await self._achat_completion(messages) + + async def _achat_completion_stream(self, messages: list[dict]) -> str: + stream_resp, _, _ = await self.client.arequest( + method=self.http_method, + url=self.suffix_url, + stream=True, + params=self._const_kwargs(messages, stream=True), + request_timeout=LLM_API_TIMEOUT, + ) + + collected_content = [] + usage = {} + async for raw_chunk in stream_resp: + chunk = self._decode_and_load(raw_chunk) + + if not chunk.get("done", False): + content = self.get_choice_text(chunk) + collected_content.append(content) + print(content, end="") + else: + # stream finished + usage = self.get_usage(chunk) + + self._update_costs(usage) + full_content = "".join(collected_content) + return full_content + + @retry( + stop=stop_after_attempt(3), + wait=wait_random_exponential(min=1, max=60), + after=after_log(logger, logger.level("WARNING").name), + retry=retry_if_exception_type(ConnectionError), + retry_error_callback=log_and_reraise, + ) + async def acompletion_text(self, messages: list[dict], stream=False) -> str: + """response in async with stream or non-stream mode""" + if stream: + return await self._achat_completion_stream(messages) + resp = await self._achat_completion(messages) + return self.get_choice_text(resp) diff --git a/metagpt/utils/repair_llm_raw_output.py b/metagpt/utils/repair_llm_raw_output.py index 67ad4e963..87fd0efd0 100644 --- a/metagpt/utils/repair_llm_raw_output.py +++ b/metagpt/utils/repair_llm_raw_output.py @@ -196,6 +196,8 @@ def repair_invalid_json(output: str, error: str) -> str: new_line = f'"{line}' elif '",' in line: new_line = line[:-2] + "'," + else: + new_line = line arr[line_no] = new_line output = "\n".join(arr) diff --git a/tests/metagpt/provider/test_google_gemini_api.py b/tests/metagpt/provider/test_google_gemini_api.py index 229d9b9a7..9c8cf46c0 100644 --- a/tests/metagpt/provider/test_google_gemini_api.py +++ b/tests/metagpt/provider/test_google_gemini_api.py @@ -9,7 +9,7 @@ import pytest from metagpt.provider.google_gemini_api import GeminiGPTAPI -messages = [{"role": "user", "content": "who are you"}] +messages = [{"role": "user", "parts": "who are you"}] @dataclass diff --git a/tests/metagpt/provider/test_ollama_api.py b/tests/metagpt/provider/test_ollama_api.py new file mode 100644 index 000000000..2798f5cc3 --- /dev/null +++ b/tests/metagpt/provider/test_ollama_api.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# @Desc : the unittest of ollama api + +import pytest + +from metagpt.provider.ollama_api import OllamaGPTAPI + +messages = [{"role": "user", "content": "who are you"}] + + +default_resp = {"message": {"role": "assisant", "content": "I'm ollama"}} + + +def mock_llm_ask(self, messages: list[dict]) -> dict: + return default_resp + + +def test_gemini_completion(mocker): + mocker.patch("metagpt.provider.ollama_api.OllamaGPTAPI.completion", mock_llm_ask) + resp = OllamaGPTAPI().completion(messages) + assert resp["message"]["content"] == default_resp["message"]["content"] + + +async def mock_llm_aask(self, messgaes: list[dict]) -> dict: + return default_resp + + +@pytest.mark.asyncio +async def test_gemini_acompletion(mocker): + mocker.patch("metagpt.provider.ollama_api.OllamaGPTAPI.acompletion", mock_llm_aask) + resp = await OllamaGPTAPI().acompletion(messages) + assert resp["message"]["content"] == default_resp["message"]["content"] From 40d3cc5f81f4a3d566844c0ee3e3ad01354f3461 Mon Sep 17 00:00:00 2001 From: better629 Date: Fri, 22 Dec 2023 09:51:26 +0800 Subject: [PATCH 03/28] format general_api_requestor params type --- metagpt/provider/general_api_requestor.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/metagpt/provider/general_api_requestor.py b/metagpt/provider/general_api_requestor.py index 8b06b9388..cf31fd629 100644 --- a/metagpt/provider/general_api_requestor.py +++ b/metagpt/provider/general_api_requestor.py @@ -3,7 +3,7 @@ # @Desc : General Async API for http-based LLM model import asyncio -from typing import AsyncGenerator, Generator, Iterator, Optional, Tuple, Union +from typing import AsyncGenerator, Generator, Iterator, Tuple, Union import aiohttp import requests @@ -12,7 +12,7 @@ from metagpt.logs import logger from metagpt.provider.general_api_base import APIRequestor -def parse_stream_helper(line: bytes) -> Optional[str]: +def parse_stream_helper(line: bytes) -> Union[bytes, None]: if line and line.startswith(b"data:"): if line.startswith(b"data: "): # SSE event may be valid when it contain whitespace @@ -24,11 +24,11 @@ def parse_stream_helper(line: bytes) -> Optional[str]: # and it will close http connection with TCP Reset return None else: - return line.decode("utf-8") + return line return None -def parse_stream(rbody: Iterator[bytes]) -> Iterator[str]: +def parse_stream(rbody: Iterator[bytes]) -> Iterator[bytes]: for line in rbody: _line = parse_stream_helper(line) if _line is not None: @@ -50,7 +50,7 @@ class GeneralAPIRequestor(APIRequestor): ) """ - def _interpret_response_line(self, rbody: str, rcode: int, rheaders, stream: bool) -> str: + def _interpret_response_line(self, rbody: bytes, rcode: int, rheaders, stream: bool) -> bytes: # just do nothing to meet the APIRequestor process and return the raw data # due to the openai sdk will convert the data into OpenAIResponse which we don't need in general cases. @@ -58,7 +58,7 @@ class GeneralAPIRequestor(APIRequestor): def _interpret_response( self, result: requests.Response, stream: bool - ) -> Tuple[Union[str, Iterator[Generator]], bool]: + ) -> Tuple[Union[bytes, Iterator[Generator]], bytes]: """Returns the response(s) and a bool indicating whether it is a stream.""" if stream and "text/event-stream" in result.headers.get("Content-Type", ""): return ( @@ -78,7 +78,7 @@ class GeneralAPIRequestor(APIRequestor): async def _interpret_async_response( self, result: aiohttp.ClientResponse, stream: bool - ) -> Tuple[Union[str, AsyncGenerator[str, None]], bool]: + ) -> Tuple[Union[bytes, AsyncGenerator[bytes, None]], bool]: if stream and ( "text/event-stream" in result.headers.get("Content-Type", "") or "application/x-ndjson" in result.headers.get("Content-Type", "") From 2a0922ba26317607ea3e53f05958144ef5445560 Mon Sep 17 00:00:00 2001 From: better629 Date: Fri, 22 Dec 2023 13:47:44 +0800 Subject: [PATCH 04/28] add non-software role/action BaseModel --- metagpt/actions/clone_function.py | 9 ++++- metagpt/actions/design_api_review.py | 12 +++++- metagpt/actions/execute_task.py | 10 ++++- metagpt/actions/generate_questions.py | 2 + metagpt/actions/invoice_ocr.py | 22 +++++++---- metagpt/actions/prepare_interview.py | 2 + metagpt/actions/research.py | 55 +++++++++++++------------- metagpt/actions/write_docstring.py | 12 ++++-- metagpt/actions/write_review.py | 7 ++++ metagpt/actions/write_tutorial.py | 18 +++++---- metagpt/roles/customer_service.py | 4 -- metagpt/roles/invoice_ocr_assistant.py | 31 +++++++-------- metagpt/roles/researcher.py | 34 ++++++++-------- metagpt/roles/sales.py | 1 - metagpt/roles/sk_agent.py | 34 +++++++++------- metagpt/roles/tutorial_assistant.py | 31 +++++++-------- 16 files changed, 162 insertions(+), 122 deletions(-) diff --git a/metagpt/actions/clone_function.py b/metagpt/actions/clone_function.py index 1447e8dbf..24d584515 100644 --- a/metagpt/actions/clone_function.py +++ b/metagpt/actions/clone_function.py @@ -1,8 +1,12 @@ import traceback from pathlib import Path +from pydantic import Field + from metagpt.actions.write_code import WriteCode +from metagpt.llm import LLM from metagpt.logs import logger +from metagpt.provider.base_gpt_api import BaseGPTAPI from metagpt.schema import Message from metagpt.utils.highlight import highlight @@ -27,8 +31,9 @@ def run(*args) -> pd.DataFrame: class CloneFunction(WriteCode): - def __init__(self, name="CloneFunction", context: list[Message] = None, llm=None): - super().__init__(name, context, llm) + name: str = "CloneFunction" + context: list[Message] = [] + llm: BaseGPTAPI = Field(default_factory=LLM) def _save(self, code_path, code): if isinstance(code_path, str): diff --git a/metagpt/actions/design_api_review.py b/metagpt/actions/design_api_review.py index 7f25bb9a3..0ff522fe8 100644 --- a/metagpt/actions/design_api_review.py +++ b/metagpt/actions/design_api_review.py @@ -5,12 +5,20 @@ @Author : alexanderwu @File : design_api_review.py """ + +from typing import Optional + +from pydantic import Field + from metagpt.actions.action import Action +from metagpt.llm import LLM +from metagpt.provider.base_gpt_api import BaseGPTAPI class DesignReview(Action): - def __init__(self, name, context=None, llm=None): - super().__init__(name, context, llm) + name: str = "DesignReview" + context: Optional[str] = None + llm: BaseGPTAPI = Field(default_factory=LLM) async def run(self, prd, api_design): prompt = ( diff --git a/metagpt/actions/execute_task.py b/metagpt/actions/execute_task.py index afdeda323..8d4e569b4 100644 --- a/metagpt/actions/execute_task.py +++ b/metagpt/actions/execute_task.py @@ -5,13 +5,19 @@ @Author : femto Zheng @File : execute_task.py """ + +from pydantic import Field + from metagpt.actions import Action +from metagpt.llm import LLM +from metagpt.provider.base_gpt_api import BaseGPTAPI from metagpt.schema import Message class ExecuteTask(Action): - def __init__(self, name="ExecuteTask", context: list[Message] = None, llm=None): - super().__init__(name, context, llm) + name: str = "ExecuteTask" + context: list[Message] = [] + llm: BaseGPTAPI = Field(default_factory=LLM) def run(self, *args, **kwargs): pass diff --git a/metagpt/actions/generate_questions.py b/metagpt/actions/generate_questions.py index c38c463bc..8573708f2 100644 --- a/metagpt/actions/generate_questions.py +++ b/metagpt/actions/generate_questions.py @@ -21,5 +21,7 @@ class GenerateQuestions(Action): """This class allows LLM to further mine noteworthy details based on specific "##TOPIC"(discussion topic) and "##RECORD" (discussion records), thereby deepening the discussion.""" + name: str = "GenerateQuestions" + async def run(self, context): return await QUESTIONS.fill(context=context, llm=self.llm) diff --git a/metagpt/actions/invoice_ocr.py b/metagpt/actions/invoice_ocr.py index dcf537a58..11b4febc0 100644 --- a/metagpt/actions/invoice_ocr.py +++ b/metagpt/actions/invoice_ocr.py @@ -12,17 +12,21 @@ import os import zipfile from datetime import datetime from pathlib import Path +from typing import Optional import pandas as pd from paddleocr import PaddleOCR +from pydantic import Field from metagpt.actions import Action from metagpt.const import INVOICE_OCR_TABLE_PATH +from metagpt.llm import LLM from metagpt.logs import logger from metagpt.prompts.invoice_ocr import ( EXTRACT_OCR_MAIN_INFO_PROMPT, REPLY_OCR_QUESTION_PROMPT, ) +from metagpt.provider.base_gpt_api import BaseGPTAPI from metagpt.utils.common import OutputParser from metagpt.utils.file import File @@ -36,8 +40,9 @@ class InvoiceOCR(Action): """ - def __init__(self, name: str = "", *args, **kwargs): - super().__init__(name, *args, **kwargs) + name: str = "InvoiceOCR" + context: Optional[str] = None + llm: BaseGPTAPI = Field(default_factory=LLM) @staticmethod async def _check_file_type(file_path: Path) -> str: @@ -125,9 +130,9 @@ class GenerateTable(Action): """ - def __init__(self, name: str = "", language: str = "ch", *args, **kwargs): - super().__init__(name, *args, **kwargs) - self.language = language + name: str = "GenerateTable" + context: Optional[str] = None + llm: BaseGPTAPI = Field(default_factory=LLM) async def run(self, ocr_results: list, filename: str, *args, **kwargs) -> dict[str, str]: """Processes OCR results, extracts invoice information, generates a table, and saves it as an Excel file. @@ -169,9 +174,10 @@ class ReplyQuestion(Action): """ - def __init__(self, name: str = "", language: str = "ch", *args, **kwargs): - super().__init__(name, *args, **kwargs) - self.language = language + name: str = "ReplyQuestion" + context: Optional[str] = None + llm: BaseGPTAPI = Field(default_factory=LLM) + language: str = "ch" async def run(self, query: str, ocr_result: list, *args, **kwargs) -> str: """Reply to questions based on ocr results. diff --git a/metagpt/actions/prepare_interview.py b/metagpt/actions/prepare_interview.py index 7ed42d590..04cc954d2 100644 --- a/metagpt/actions/prepare_interview.py +++ b/metagpt/actions/prepare_interview.py @@ -19,5 +19,7 @@ Attention: Provide as markdown block as the format above, at least 10 questions. class PrepareInterview(Action): + name: str = "PrepareInterview" + async def run(self, context): return await QUESTIONS.fill(context=context, llm=self.llm) diff --git a/metagpt/actions/research.py b/metagpt/actions/research.py index a70038c51..6670b3784 100644 --- a/metagpt/actions/research.py +++ b/metagpt/actions/research.py @@ -3,13 +3,15 @@ from __future__ import annotations import asyncio -from typing import Callable +from typing import Callable, Optional, Union -from pydantic import parse_obj_as +from pydantic import Field, parse_obj_as from metagpt.actions import Action from metagpt.config import CONFIG +from metagpt.llm import LLM from metagpt.logs import logger +from metagpt.provider.base_gpt_api import BaseGPTAPI from metagpt.tools.search_engine import SearchEngine from metagpt.tools.web_browser_engine import WebBrowserEngine, WebBrowserEngineType from metagpt.utils.common import OutputParser @@ -78,17 +80,12 @@ above. The report must meet the following requirements: class CollectLinks(Action): """Action class to collect links from a search engine.""" - def __init__( - self, - name: str = "", - *args, - rank_func: Callable[[list[str]], None] | None = None, - **kwargs, - ): - super().__init__(name, *args, **kwargs) - self.desc = "Collect links from a search engine." - self.search_engine = SearchEngine() - self.rank_func = rank_func + name: str = "CollectLinks" + context: Optional[str] = None + llm: BaseGPTAPI = Field(default_factory=LLM) + desc: str = "Collect links from a search engine." + search_engine: SearchEngine = Field(default_factory=SearchEngine) + rank_func: Union[Callable[[list[str]], None], None] = None async def run( self, @@ -178,20 +175,20 @@ class CollectLinks(Action): class WebBrowseAndSummarize(Action): """Action class to explore the web and provide summaries of articles and webpages.""" - def __init__( - self, - *args, - browse_func: Callable[[list[str]], None] | None = None, - **kwargs, - ): - super().__init__(*args, **kwargs) + name: str = "WebBrowseAndSummarize" + context: Optional[str] = None + llm: BaseGPTAPI = Field(default_factory=LLM) + desc = "Explore the web and provide summaries of articles and webpages." + browse_func = Union[Callable[[list[str]], None], None] = None + web_browser_engine: WebBrowserEngine = WebBrowserEngine( + engine=WebBrowserEngineType.CUSTOM if browse_func else None, + run_func=browse_func, + ) + + def __init__(self, **kwargs): + super().__init__(**kwargs) if CONFIG.model_for_researcher_summary: self.llm.model = CONFIG.model_for_researcher_summary - self.web_browser_engine = WebBrowserEngine( - engine=WebBrowserEngineType.CUSTOM if browse_func else None, - run_func=browse_func, - ) - self.desc = "Explore the web and provide summaries of articles and webpages." async def run( self, @@ -247,8 +244,12 @@ class WebBrowseAndSummarize(Action): class ConductResearch(Action): """Action class to conduct research and generate a research report.""" - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + name: str = "ConductResearch" + context: Optional[str] = None + llm: BaseGPTAPI = Field(default_factory=LLM) + + def __init__(self, **kwargs): + super().__init__(**kwargs) if CONFIG.model_for_researcher_report: self.llm.model = CONFIG.model_for_researcher_report diff --git a/metagpt/actions/write_docstring.py b/metagpt/actions/write_docstring.py index 0ad134157..1c27a9433 100644 --- a/metagpt/actions/write_docstring.py +++ b/metagpt/actions/write_docstring.py @@ -22,9 +22,13 @@ This script uses the 'fire' library to create a command-line interface. It gener the specified docstring style and adds them to the code. """ import ast -from typing import Literal +from typing import Literal, Optional + +from pydantic import Field from metagpt.actions.action import Action +from metagpt.llm import LLM +from metagpt.provider.base_gpt_api import BaseGPTAPI from metagpt.utils.common import OutputParser from metagpt.utils.pycst import merge_docstring @@ -157,9 +161,9 @@ class WriteDocstring(Action): desc: A string describing the action. """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.desc = "Write docstring for code." + desc: str = "Write docstring for code." + context: Optional[str] = None + llm: BaseGPTAPI = Field(default_factory=LLM) async def run( self, diff --git a/metagpt/actions/write_review.py b/metagpt/actions/write_review.py index 8a4856317..646f44aeb 100644 --- a/metagpt/actions/write_review.py +++ b/metagpt/actions/write_review.py @@ -6,8 +6,12 @@ """ from typing import List +from pydantic import Field + from metagpt.actions import Action from metagpt.actions.action_node import ActionNode +from metagpt.llm import LLM +from metagpt.provider.base_gpt_api import BaseGPTAPI REVIEW = ActionNode( key="Review", @@ -33,5 +37,8 @@ WRITE_REVIEW_NODE = ActionNode.from_children("WRITE_REVIEW_NODE", [REVIEW, LGTM] class WriteReview(Action): """Write a review for the given context.""" + name: str = "WriteReview" + llm: BaseGPTAPI = Field(default_factory=LLM) + async def run(self, context): return await WRITE_REVIEW_NODE.fill(context=context, llm=self.llm, schema="json") diff --git a/metagpt/actions/write_tutorial.py b/metagpt/actions/write_tutorial.py index d41915de3..742b6742b 100644 --- a/metagpt/actions/write_tutorial.py +++ b/metagpt/actions/write_tutorial.py @@ -9,8 +9,12 @@ from typing import Dict +from pydantic import Field + from metagpt.actions import Action +from metagpt.llm import LLM from metagpt.prompts.tutorial_assistant import CONTENT_PROMPT, DIRECTORY_PROMPT +from metagpt.provider.base_gpt_api import BaseGPTAPI from metagpt.utils.common import OutputParser @@ -22,9 +26,9 @@ class WriteDirectory(Action): language: The language to output, default is "Chinese". """ - def __init__(self, name: str = "", language: str = "Chinese", *args, **kwargs): - super().__init__(name, *args, **kwargs) - self.language = language + name: str = "WriteDirectory" + llm: BaseGPTAPI = Field(default_factory=LLM) + language: str = "Chinese" async def run(self, topic: str, *args, **kwargs) -> Dict: """Execute the action to generate a tutorial directory according to the topic. @@ -49,10 +53,10 @@ class WriteContent(Action): language: The language to output, default is "Chinese". """ - def __init__(self, name: str = "", directory: str = "", language: str = "Chinese", *args, **kwargs): - super().__init__(name, *args, **kwargs) - self.language = language - self.directory = directory + name: str = "WriteContent" + llm: BaseGPTAPI = Field(default_factory=LLM) + directory: str = "" + language: str = "Chinese" async def run(self, topic: str, *args, **kwargs) -> str: """Execute the action to write document content according to the directory and topic. diff --git a/metagpt/roles/customer_service.py b/metagpt/roles/customer_service.py index 777f62731..c7baa697d 100644 --- a/metagpt/roles/customer_service.py +++ b/metagpt/roles/customer_service.py @@ -29,8 +29,4 @@ class CustomerService(Sales): name: str = "Xiaomei" profile: str = "Human customer service" desc: str = DESC - store: Optional[str] = None - - def __init__(self, **kwargs): - super().__init__(**kwargs) diff --git a/metagpt/roles/invoice_ocr_assistant.py b/metagpt/roles/invoice_ocr_assistant.py index bf8fc454e..17086d42a 100644 --- a/metagpt/roles/invoice_ocr_assistant.py +++ b/metagpt/roles/invoice_ocr_assistant.py @@ -7,11 +7,13 @@ @File : invoice_ocr_assistant.py """ +from typing import Optional + import pandas as pd from metagpt.actions.invoice_ocr import GenerateTable, InvoiceOCR, ReplyQuestion from metagpt.prompts.invoice_ocr import INVOICE_OCR_SUCCESS -from metagpt.roles import Role +from metagpt.roles.role import Role, RoleReactMode from metagpt.schema import Message @@ -28,21 +30,18 @@ class InvoiceOCRAssistant(Role): language: The language in which the invoice table will be generated. """ - def __init__( - self, - name: str = "Stitch", - profile: str = "Invoice OCR Assistant", - goal: str = "OCR identifies invoice files and generates invoice main information table", - constraints: str = "", - language: str = "ch", - ): - super().__init__(name, profile, goal, constraints) - self._init_actions([InvoiceOCR]) - self.language = language - self.filename = "" - self.origin_query = "" - self.orc_data = None - self._set_react_mode(react_mode="by_order") + name: str = "Stitch" + profile: str = "Invoice OCR Assistant" + goal: str = "OCR identifies invoice files and generates invoice main information table" + constraints: str = "" + language: str = "ch" + filename: str = "" + origin_query: str = "" + orc_data: Optional[list] = None + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._set_react_mode(react_mode=RoleReactMode.BY_ORDER.value) async def _act(self) -> Message: """Perform an action as determined by the role. diff --git a/metagpt/roles/researcher.py b/metagpt/roles/researcher.py index 162d72b9b..29c879233 100644 --- a/metagpt/roles/researcher.py +++ b/metagpt/roles/researcher.py @@ -4,7 +4,6 @@ the `cause_by` value in the `Message` to a string to support the new message distribution feature. """ - import asyncio from pydantic import BaseModel @@ -13,7 +12,7 @@ from metagpt.actions import Action, CollectLinks, ConductResearch, WebBrowseAndS from metagpt.actions.research import get_research_system_text from metagpt.const import RESEARCH_PATH from metagpt.logs import logger -from metagpt.roles import Role +from metagpt.roles.role import Role, RoleReactMode from metagpt.schema import Message @@ -25,21 +24,20 @@ class Report(BaseModel): class Researcher(Role): - def __init__( - self, - name: str = "David", - profile: str = "Researcher", - goal: str = "Gather information and conduct research", - constraints: str = "Ensure accuracy and relevance of information", - language: str = "en-us", - **kwargs, - ): - super().__init__(name, profile, goal, constraints, **kwargs) - self._init_actions([CollectLinks(name), WebBrowseAndSummarize(name), ConductResearch(name)]) - self._set_react_mode(react_mode="by_order") - self.language = language - if language not in ("en-us", "zh-cn"): - logger.warning(f"The language `{language}` has not been tested, it may not work.") + name: str = "David" + profile: str = "Researcher" + goal: str = "Gather information and conduct research" + constraints: str = "Ensure accuracy and relevance of information" + language: str = "en-us" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._init_actions( + [CollectLinks(name=self.name), WebBrowseAndSummarize(name=self.name), ConductResearch(name=self.name)] + ) + self._set_react_mode(react_mode=RoleReactMode.BY_ORDER.value) + if self.language not in ("en-us", "zh-cn"): + logger.warning(f"The language `{self.language}` has not been tested, it may not work.") async def _act(self) -> Message: logger.info(f"{self._setting}: ready to {self._rc.todo}") @@ -107,7 +105,7 @@ if __name__ == "__main__": import fire async def main(topic: str, language="en-us"): - role = Researcher(topic, language=language) + role = Researcher(language=language) await role.run(topic) fire.Fire(main) diff --git a/metagpt/roles/sales.py b/metagpt/roles/sales.py index 76abf10f3..f8dccf2af 100644 --- a/metagpt/roles/sales.py +++ b/metagpt/roles/sales.py @@ -22,7 +22,6 @@ class Sales(Role): " 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: Optional[str] = None def __init__(self, **kwargs): diff --git a/metagpt/roles/sk_agent.py b/metagpt/roles/sk_agent.py index 56482ef26..2fce739e2 100644 --- a/metagpt/roles/sk_agent.py +++ b/metagpt/roles/sk_agent.py @@ -7,13 +7,16 @@ @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 filtering. """ + +from pydantic import Field from semantic_kernel.planning import SequentialPlanner from semantic_kernel.planning.action_planner.action_planner import ActionPlanner from semantic_kernel.planning.basic_planner import BasicPlanner from metagpt.actions import UserRequirement from metagpt.actions.execute_task import ExecuteTask -from metagpt.logs import logger +from metagpt.llm import LLM +from metagpt.provider.base_gpt_api import BaseGPTAPI from metagpt.roles import Role from metagpt.schema import Message from metagpt.utils.make_sk_kernel import make_sk_kernel @@ -30,27 +33,28 @@ class SkAgent(Role): constraints (str): Constraints for the SkAgent. """ - def __init__( - self, - name: str = "Sunshine", - profile: str = "sk_agent", - goal: str = "Execute task based on passed in task description", - constraints: str = "", - planner_cls=BasicPlanner, - ) -> None: + name: str = "Sunshine" + profile: str = "sk_agent" + goal: str = "Execute task based on passed in task description" + constraints: str = "" + planner_cls: BasicPlanner = BasicPlanner + planner: BasicPlanner = Field(default_factory=BasicPlanner) + llm: BaseGPTAPI = Field(default_factory=LLM) + + def __init__(self, **kwargs) -> None: """Initializes the Engineer role with given attributes.""" - super().__init__(name, profile, goal, constraints) + super().__init__(**kwargs) self._init_actions([ExecuteTask()]) self._watch([UserRequirement]) self.kernel = make_sk_kernel() # how funny the interface is inconsistent - if planner_cls == BasicPlanner: - self.planner = planner_cls() - elif planner_cls in [SequentialPlanner, ActionPlanner]: - self.planner = planner_cls(self.kernel) + if self.planner_cls == BasicPlanner: + self.planner = self.planner_cls() + elif self.planner_cls in [SequentialPlanner, ActionPlanner]: + self.planner = self.planner_cls(self.kernel) else: - raise f"Unsupported planner of type {planner_cls}" + raise Exception(f"Unsupported planner of type {self.planner_cls}") self.import_semantic_skill_from_directory = self.kernel.import_semantic_skill_from_directory self.import_skill = self.kernel.import_skill diff --git a/metagpt/roles/tutorial_assistant.py b/metagpt/roles/tutorial_assistant.py index e0be4de61..5d1323371 100644 --- a/metagpt/roles/tutorial_assistant.py +++ b/metagpt/roles/tutorial_assistant.py @@ -12,7 +12,7 @@ from typing import Dict from metagpt.actions.write_tutorial import WriteContent, WriteDirectory from metagpt.const import TUTORIAL_PATH from metagpt.logs import logger -from metagpt.roles import Role +from metagpt.roles.role import Role, RoleReactMode from metagpt.schema import Message from metagpt.utils.file import File @@ -28,21 +28,20 @@ class TutorialAssistant(Role): language: The language in which the tutorial documents will be generated. """ - def __init__( - self, - name: str = "Stitch", - profile: str = "Tutorial Assistant", - goal: str = "Generate tutorial documents", - constraints: str = "Strictly follow Markdown's syntax, with neat and standardized layout", - language: str = "Chinese", - ): - super().__init__(name, profile, goal, constraints) - self._init_actions([WriteDirectory(language=language)]) - self.topic = "" - self.main_title = "" - self.total_content = "" - self.language = language - self._set_react_mode(react_mode="by_order") + name: str = "Stitch" + profile: str = "Tutorial Assistant" + goal: str = "Generate tutorial documents" + constraints: str = "Strictly follow Markdown's syntax, with neat and standardized layout" + language: str = "Chinese" + + topic = "" + main_title = "" + total_content = "" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._init_actions([WriteDirectory(language=self.language)]) + self._set_react_mode(react_mode=RoleReactMode.BY_ORDER.value) async def _handle_directory(self, titles: Dict) -> Message: """Handle the directories for the tutorial document. From 19c16bf9f19c0492ed220bc5721d3d34732ab1c3 Mon Sep 17 00:00:00 2001 From: better629 Date: Fri, 22 Dec 2023 13:55:23 +0800 Subject: [PATCH 05/28] fix --- metagpt/actions/research.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/metagpt/actions/research.py b/metagpt/actions/research.py index 6670b3784..074cdee0a 100644 --- a/metagpt/actions/research.py +++ b/metagpt/actions/research.py @@ -178,8 +178,8 @@ class WebBrowseAndSummarize(Action): name: str = "WebBrowseAndSummarize" context: Optional[str] = None llm: BaseGPTAPI = Field(default_factory=LLM) - desc = "Explore the web and provide summaries of articles and webpages." - browse_func = Union[Callable[[list[str]], None], None] = None + desc: str = "Explore the web and provide summaries of articles and webpages." + browse_func: Union[Callable[[list[str]], None], None] = None web_browser_engine: WebBrowserEngine = WebBrowserEngine( engine=WebBrowserEngineType.CUSTOM if browse_func else None, run_func=browse_func, From 322ac4aa4064f37e59f28df62f842ac3161d26b5 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Fri, 22 Dec 2023 15:17:59 +0800 Subject: [PATCH 06/28] upgrade langchain and simplify faiss load/save --- examples/search_kb.py | 20 +++++++++------- metagpt/actions/search_and_summarize.py | 4 ++-- metagpt/document_store/base_store.py | 8 +++---- metagpt/document_store/faiss_store.py | 31 ++++++++++--------------- metagpt/roles/sales.py | 7 +++--- requirements.txt | 2 +- 6 files changed, 34 insertions(+), 38 deletions(-) diff --git a/examples/search_kb.py b/examples/search_kb.py index 5d61bbe02..c70cad2fd 100644 --- a/examples/search_kb.py +++ b/examples/search_kb.py @@ -5,12 +5,13 @@ """ import asyncio -from metagpt.actions import Action +from langchain.embeddings import OpenAIEmbeddings + +from metagpt.config import CONFIG 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. [ @@ -26,14 +27,15 @@ from metagpt.schema import Message """ +def get_store(): + embedding = OpenAIEmbeddings(openai_api_key=CONFIG.openai_api_key, openai_api_base=CONFIG.openai_base_url) + return FaissStore(DATA_PATH / "example.json", embedding=embedding) + + async def search(): - store = FaissStore(DATA_PATH / "example.json") - role = Sales(profile="Sales", store=store) - role._watch({Action}) - queries = [ - Message(content="Which facial cleanser is good for oily skin?", cause_by=Action), - Message(content="Is L'Oreal good to use?", cause_by=Action), - ] + role = Sales(profile="Sales", store=get_store()) + queries = ["Which facial cleanser is good for oily skin?", "Is L'Oreal good to use?"] + for query in queries: logger.info(f"User: {query}") result = await role.run(query) diff --git a/metagpt/actions/search_and_summarize.py b/metagpt/actions/search_and_summarize.py index bc1319291..25af21795 100644 --- a/metagpt/actions/search_and_summarize.py +++ b/metagpt/actions/search_and_summarize.py @@ -5,7 +5,7 @@ @Author : alexanderwu @File : search_google.py """ -from typing import Optional +from typing import Any, Optional import pydantic from pydantic import Field, root_validator @@ -111,7 +111,7 @@ class SearchAndSummarize(Action): llm: BaseGPTAPI = Field(default_factory=LLM) config: None = Field(default_factory=Config) engine: Optional[SearchEngineType] = CONFIG.search_engine - search_func: Optional[str] = None + search_func: Optional[Any] = None search_engine: SearchEngine = None result = "" diff --git a/metagpt/document_store/base_store.py b/metagpt/document_store/base_store.py index 5de377d21..af69b10de 100644 --- a/metagpt/document_store/base_store.py +++ b/metagpt/document_store/base_store.py @@ -33,6 +33,7 @@ class LocalStore(BaseStore, ABC): raise FileNotFoundError self.config = Config() self.raw_data_path = raw_data_path + self.fname = self.raw_data_path.name.split(".")[0] if not cache_dir: cache_dir = raw_data_path.parent self.cache_dir = cache_dir @@ -40,10 +41,9 @@ class LocalStore(BaseStore, ABC): if not self.store: self.store = self.write() - def _get_index_and_store_fname(self): - fname = self.raw_data_path.name.split(".")[0] - index_file = self.cache_dir / f"{fname}.index" - store_file = self.cache_dir / f"{fname}.pkl" + def _get_index_and_store_fname(self, index_ext=".index", pkl_ext=".pkl"): + index_file = self.cache_dir / f"{self.fname}{index_ext}" + store_file = self.cache_dir / f"{self.fname}{pkl_ext}" return index_file, store_file @abstractmethod diff --git a/metagpt/document_store/faiss_store.py b/metagpt/document_store/faiss_store.py index b1faa3538..320e7518f 100644 --- a/metagpt/document_store/faiss_store.py +++ b/metagpt/document_store/faiss_store.py @@ -6,13 +6,12 @@ @File : faiss_store.py """ import asyncio -import pickle from pathlib import Path from typing import Optional -import faiss from langchain.embeddings import OpenAIEmbeddings from langchain.vectorstores import FAISS +from langchain_core.embeddings import Embeddings from metagpt.const import DATA_PATH from metagpt.document import IndexableDocument @@ -21,35 +20,29 @@ from metagpt.logs import logger class FaissStore(LocalStore): - def __init__(self, raw_data_path: 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", embedding: Embeddings = None + ): self.meta_col = meta_col self.content_col = content_col - super().__init__(raw_data_path, cache_dir) + self.embedding = embedding or OpenAIEmbeddings() + super().__init__(raw_data, cache_dir) def _load(self) -> Optional["FaissStore"]: - index_file, store_file = self._get_index_and_store_fname() + index_file, store_file = self._get_index_and_store_fname(index_ext=".faiss") # langchain FAISS using .faiss + if not (index_file.exists() and store_file.exists()): logger.info("Missing at least one of index_file/store_file, load failed and return None") return None - index = faiss.read_index(str(index_file)) - with open(str(store_file), "rb") as f: - store = pickle.load(f) - store.index = index - return store + + return FAISS.load_local(self.raw_data_path.parent, self.embedding, self.fname) def _write(self, docs, metadatas): - store = FAISS.from_texts(docs, OpenAIEmbeddings(openai_api_version="2020-11-07"), metadatas=metadatas) + store = FAISS.from_texts(docs, self.embedding, metadatas=metadatas) return store def persist(self): - index_file, store_file = self._get_index_and_store_fname() - store = self.store - index = self.store.index - faiss.write_index(store.index, str(index_file)) - store.index = None - with open(store_file, "wb") as f: - pickle.dump(store, f) - store.index = index + self.store.save_local(self.raw_data_path.parent, self.fname) def search(self, query, expand_cols=False, sep="\n", *args, k=5, **kwargs): rsp = self.store.similarity_search(query, k=k, **kwargs) diff --git a/metagpt/roles/sales.py b/metagpt/roles/sales.py index 76abf10f3..af6badfb5 100644 --- a/metagpt/roles/sales.py +++ b/metagpt/roles/sales.py @@ -6,9 +6,9 @@ @File : sales.py """ -from typing import Optional +from typing import Any, Optional -from metagpt.actions import SearchAndSummarize +from metagpt.actions import SearchAndSummarize, UserRequirement from metagpt.roles import Role from metagpt.tools import SearchEngineType @@ -23,7 +23,7 @@ class Sales(Role): "but pretend to be what I know. Note that each of my replies will be replied in the tone of a " "professional guide" - store: Optional[str] = None + store: Optional[Any] = None def __init__(self, **kwargs): super().__init__(**kwargs) @@ -35,3 +35,4 @@ class Sales(Role): else: action = SearchAndSummarize() self._init_actions([action]) + self._watch([UserRequirement]) diff --git a/requirements.txt b/requirements.txt index eaff5c4b2..9954a9941 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ typer # godot==0.1.1 # google_api_python_client==2.93.0 lancedb==0.1.16 -langchain==0.0.231 +langchain==0.0.352 loguru==0.6.0 meilisearch==0.21.0 numpy==1.24.3 From 3b066b36ccebe3e0238b707a71b1a50d5f606017 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Fri, 22 Dec 2023 15:57:55 +0800 Subject: [PATCH 07/28] upgrade langchain and simplify faiss load/save --- metagpt/document_store/base_store.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/document_store/base_store.py b/metagpt/document_store/base_store.py index af69b10de..b719d1083 100644 --- a/metagpt/document_store/base_store.py +++ b/metagpt/document_store/base_store.py @@ -33,7 +33,7 @@ class LocalStore(BaseStore, ABC): raise FileNotFoundError self.config = Config() self.raw_data_path = raw_data_path - self.fname = self.raw_data_path.name.split(".")[0] + self.fname = self.raw_data_path.stem if not cache_dir: cache_dir = raw_data_path.parent self.cache_dir = cache_dir From 78164884452c7525c3dbefb6852cd60d08e7ce35 Mon Sep 17 00:00:00 2001 From: better629 Date: Fri, 22 Dec 2023 16:25:06 +0800 Subject: [PATCH 08/28] update examples --- examples/agent_creator.py | 19 +++------ examples/build_customized_agent.py | 35 +++++++--------- examples/build_customized_multi_agents.py | 49 +++++++++-------------- metagpt/actions/write_tutorial.py | 2 +- metagpt/roles/role.py | 2 +- metagpt/roles/sk_agent.py | 10 ++++- metagpt/utils/make_sk_kernel.py | 6 ++- 7 files changed, 54 insertions(+), 69 deletions(-) diff --git a/examples/agent_creator.py b/examples/agent_creator.py index 26af8a287..0b85b33a6 100644 --- a/examples/agent_creator.py +++ b/examples/agent_creator.py @@ -55,16 +55,13 @@ class CreateAgent(Action): class AgentCreator(Role): - def __init__( - self, - name: str = "Matrix", - profile: str = "AgentCreator", - agent_template: str = MULTI_ACTION_AGENT_CODE_EXAMPLE, - **kwargs, - ): - super().__init__(name, profile, **kwargs) + name: str = "Matrix" + profile: str = "AgentCreator" + agent_template: str = MULTI_ACTION_AGENT_CODE_EXAMPLE + + def __init__(self, **kwargs): + super().__init__(**kwargs) self._init_actions([CreateAgent]) - self.agent_template = agent_template async def _act(self) -> Message: logger.info(f"{self._setting}: ready to {self._rc.todo}") @@ -86,10 +83,6 @@ if __name__ == "__main__": creator = AgentCreator(agent_template=agent_template) - # msg = """Write an agent called SimpleTester that will take any code snippet (str) - # and return a testing code (str) for testing - # the given code snippet. Use pytest as the testing framework.""" - msg = """ Write an agent called SimpleTester that will take any code snippet (str) and do the following: 1. write a testing code (str) for testing the given code snippet, save the testing code as a .py file in the current working directory; diff --git a/examples/build_customized_agent.py b/examples/build_customized_agent.py index 6805fd460..679aee948 100644 --- a/examples/build_customized_agent.py +++ b/examples/build_customized_agent.py @@ -10,9 +10,8 @@ import subprocess import fire from metagpt.actions import Action -from metagpt.llm import LLM from metagpt.logs import logger -from metagpt.roles import Role +from metagpt.roles.role import Role, RoleReactMode from metagpt.schema import Message @@ -23,8 +22,7 @@ class SimpleWriteCode(Action): your code: """ - def __init__(self, name: str = "SimpleWriteCode", context=None, llm: LLM = None): - super().__init__(name, context, llm) + name: str = "SimpleWriteCode" async def run(self, instruction: str): prompt = self.PROMPT_TEMPLATE.format(instruction=instruction) @@ -44,8 +42,7 @@ class SimpleWriteCode(Action): class SimpleRunCode(Action): - def __init__(self, name: str = "SimpleRunCode", context=None, llm: LLM = None): - super().__init__(name, context, llm) + name: str = "SimpleRunCode" async def run(self, code_text: str): result = subprocess.run(["python3", "-c", code_text], capture_output=True, text=True) @@ -55,13 +52,11 @@ class SimpleRunCode(Action): class SimpleCoder(Role): - def __init__( - self, - name: str = "Alice", - profile: str = "SimpleCoder", - **kwargs, - ): - super().__init__(name, profile, **kwargs) + name: str = "Alice" + profile: str = "SimpleCoder" + + def __init__(self, **kwargs): + super().__init__(**kwargs) self._init_actions([SimpleWriteCode]) async def _act(self) -> Message: @@ -76,15 +71,13 @@ class SimpleCoder(Role): class RunnableCoder(Role): - def __init__( - self, - name: str = "Alice", - profile: str = "RunnableCoder", - **kwargs, - ): - super().__init__(name, profile, **kwargs) + name: str = "Alice" + profile: str = "RunnableCoder" + + def __init__(self, **kwargs): + super().__init__(**kwargs) self._init_actions([SimpleWriteCode, SimpleRunCode]) - self._set_react_mode(react_mode="by_order") + self._set_react_mode(react_mode=RoleReactMode.BY_ORDER.value) async def _act(self) -> Message: logger.info(f"{self._setting}: ready to {self._rc.todo}") diff --git a/examples/build_customized_multi_agents.py b/examples/build_customized_multi_agents.py index 030a4b339..518aa6324 100644 --- a/examples/build_customized_multi_agents.py +++ b/examples/build_customized_multi_agents.py @@ -8,7 +8,6 @@ import re import fire from metagpt.actions import Action, UserRequirement -from metagpt.llm import LLM from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message @@ -28,9 +27,7 @@ class SimpleWriteCode(Action): Return ```python your_code_here ``` with NO other texts, your code: """ - - def __init__(self, name: str = "SimpleWriteCode", context=None, llm: LLM = None): - super().__init__(name, context, llm) + name: str = "SimpleWriteCode" async def run(self, instruction: str): prompt = self.PROMPT_TEMPLATE.format(instruction=instruction) @@ -43,13 +40,11 @@ class SimpleWriteCode(Action): class SimpleCoder(Role): - def __init__( - self, - name: str = "Alice", - profile: str = "SimpleCoder", - **kwargs, - ): - super().__init__(name, profile, **kwargs) + name: str = "Alice" + profile: str = "SimpleCoder" + + def __init__(self, **kwargs): + super().__init__(**kwargs) self._watch([UserRequirement]) self._init_actions([SimpleWriteCode]) @@ -62,8 +57,7 @@ class SimpleWriteTest(Action): your code: """ - def __init__(self, name: str = "SimpleWriteTest", context=None, llm: LLM = None): - super().__init__(name, context, llm) + name: str = "SimpleWriteTest" async def run(self, context: str, k: int = 3): prompt = self.PROMPT_TEMPLATE.format(context=context, k=k) @@ -76,13 +70,11 @@ class SimpleWriteTest(Action): class SimpleTester(Role): - def __init__( - self, - name: str = "Bob", - profile: str = "SimpleTester", - **kwargs, - ): - super().__init__(name, profile, **kwargs) + name: str = "Bob" + profile: str = "SimpleTester" + + def __init__(self, **kwargs): + super().__init__(**kwargs) self._init_actions([SimpleWriteTest]) # self._watch([SimpleWriteCode]) self._watch([SimpleWriteCode, SimpleWriteReview]) # feel free to try this too @@ -106,8 +98,7 @@ class SimpleWriteReview(Action): Review the test cases and provide one critical comments: """ - def __init__(self, name: str = "SimpleWriteReview", context=None, llm: LLM = None): - super().__init__(name, context, llm) + name: str = "SimpleWriteReview" async def run(self, context: str): prompt = self.PROMPT_TEMPLATE.format(context=context) @@ -118,13 +109,11 @@ class SimpleWriteReview(Action): class SimpleReviewer(Role): - def __init__( - self, - name: str = "Charlie", - profile: str = "SimpleReviewer", - **kwargs, - ): - super().__init__(name, profile, **kwargs) + name: str = "Charlie" + profile: str = "SimpleReviewer" + + def __init__(self, **kwargs): + super().__init__(**kwargs) self._init_actions([SimpleWriteReview]) self._watch([SimpleWriteTest]) @@ -147,7 +136,7 @@ async def main( ) team.invest(investment=investment) - team.start_project(idea) + team.run_project(idea) await team.run(n_round=n_round) diff --git a/metagpt/actions/write_tutorial.py b/metagpt/actions/write_tutorial.py index 742b6742b..f33a6b114 100644 --- a/metagpt/actions/write_tutorial.py +++ b/metagpt/actions/write_tutorial.py @@ -55,7 +55,7 @@ class WriteContent(Action): name: str = "WriteContent" llm: BaseGPTAPI = Field(default_factory=LLM) - directory: str = "" + directory: dict = dict() language: str = "Chinese" async def run(self, topic: str, *args, **kwargs) -> str: diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index b9fde7d05..8edbdfca1 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -400,7 +400,7 @@ class Role(BaseModel): 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 and new not in existed_pure: + if (new["cause_by"] in self._rc.watch and new not in existed_pure) or (not self._rc.watch): news.append(observed[idx]) return news diff --git a/metagpt/roles/sk_agent.py b/metagpt/roles/sk_agent.py index 2fce739e2..791dff5e2 100644 --- a/metagpt/roles/sk_agent.py +++ b/metagpt/roles/sk_agent.py @@ -9,13 +9,16 @@ """ from pydantic import Field +from semantic_kernel import Kernel +from semantic_kernel.orchestration.sk_function_base import SKFunctionBase from semantic_kernel.planning import SequentialPlanner from semantic_kernel.planning.action_planner.action_planner import ActionPlanner -from semantic_kernel.planning.basic_planner import BasicPlanner +from semantic_kernel.planning.basic_planner import BasicPlanner, Plan from metagpt.actions import UserRequirement from metagpt.actions.execute_task import ExecuteTask from metagpt.llm import LLM +from metagpt.logs import logger from metagpt.provider.base_gpt_api import BaseGPTAPI from metagpt.roles import Role from metagpt.schema import Message @@ -37,9 +40,14 @@ class SkAgent(Role): profile: str = "sk_agent" goal: str = "Execute task based on passed in task description" constraints: str = "" + + plan: Plan = None planner_cls: BasicPlanner = BasicPlanner planner: BasicPlanner = Field(default_factory=BasicPlanner) llm: BaseGPTAPI = Field(default_factory=LLM) + kernel: Kernel = Field(default_factory=Kernel) + import_semantic_skill_from_directory: str = "" + import_skill: dict[str, SKFunctionBase] = dict() def __init__(self, **kwargs) -> None: """Initializes the Engineer role with given attributes.""" diff --git a/metagpt/utils/make_sk_kernel.py b/metagpt/utils/make_sk_kernel.py index 83b4005ec..5edddd618 100644 --- a/metagpt/utils/make_sk_kernel.py +++ b/metagpt/utils/make_sk_kernel.py @@ -21,12 +21,14 @@ def make_sk_kernel(): if CONFIG.openai_api_type == "azure": kernel.add_chat_service( "chat_completion", - AzureChatCompletion(CONFIG.deployment_name, CONFIG.openai_base_url, CONFIG.openai_api_key), + AzureChatCompletion( + deployment_name=CONFIG.deployment_name, base_url=CONFIG.openai_base_url, api_key=CONFIG.openai_api_key + ), ) else: kernel.add_chat_service( "chat_completion", - OpenAIChatCompletion(CONFIG.openai_api_model, CONFIG.openai_api_key), + OpenAIChatCompletion(model_id=CONFIG.openai_api_model, api_key=CONFIG.openai_api_key), ) return kernel From a6346c7bce2d78286ab944d3d6ec98e6eca1e2f7 Mon Sep 17 00:00:00 2001 From: better629 Date: Fri, 22 Dec 2023 16:35:59 +0800 Subject: [PATCH 09/28] update --- metagpt/roles/role.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index 8edbdfca1..b9fde7d05 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -400,7 +400,7 @@ class Role(BaseModel): 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 and new not in existed_pure) or (not self._rc.watch): + if new["cause_by"] in self._rc.watch and new not in existed_pure: news.append(observed[idx]) return news From bf4ef46a767d4920ca11beca51ed86a3cf20cc79 Mon Sep 17 00:00:00 2001 From: seehi <6580@pm.me> Date: Fri, 22 Dec 2023 16:52:30 +0800 Subject: [PATCH 10/28] typing of store --- metagpt/roles/sales.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/metagpt/roles/sales.py b/metagpt/roles/sales.py index af6badfb5..1ef93f6f3 100644 --- a/metagpt/roles/sales.py +++ b/metagpt/roles/sales.py @@ -6,9 +6,10 @@ @File : sales.py """ -from typing import Any, Optional +from typing import Optional from metagpt.actions import SearchAndSummarize, UserRequirement +from metagpt.document_store.base_store import BaseStore from metagpt.roles import Role from metagpt.tools import SearchEngineType @@ -23,7 +24,7 @@ class Sales(Role): "but pretend to be what I know. Note that each of my replies will be replied in the tone of a " "professional guide" - store: Optional[Any] = None + store: Optional[BaseStore] = None def __init__(self, **kwargs): super().__init__(**kwargs) From 058252c636bbe6779f15503a00fcb13f685a0191 Mon Sep 17 00:00:00 2001 From: geekan Date: Fri, 22 Dec 2023 17:15:36 +0800 Subject: [PATCH 11/28] fix bugs and make it perform better --- examples/agent_creator.py | 2 +- examples/build_customized_agent.py | 4 +-- examples/build_customized_multi_agents.py | 2 +- examples/debate.py | 2 +- examples/debate_simple.py | 14 ++++++---- metagpt/actions/action.py | 5 ++-- metagpt/actions/action_node.py | 34 +++++++++++++++++------ metagpt/environment.py | 11 +++++--- metagpt/roles/researcher.py | 2 +- metagpt/roles/role.py | 13 ++++++--- metagpt/roles/searcher.py | 2 +- 11 files changed, 59 insertions(+), 32 deletions(-) diff --git a/examples/agent_creator.py b/examples/agent_creator.py index 26af8a287..961d12968 100644 --- a/examples/agent_creator.py +++ b/examples/agent_creator.py @@ -67,7 +67,7 @@ class AgentCreator(Role): self.agent_template = agent_template async def _act(self) -> Message: - logger.info(f"{self._setting}: ready to {self._rc.todo}") + logger.info(f"{self._setting}: to do {self._rc.todo}") todo = self._rc.todo msg = self._rc.memory.get()[-1] diff --git a/examples/build_customized_agent.py b/examples/build_customized_agent.py index 6805fd460..eb92d9a9c 100644 --- a/examples/build_customized_agent.py +++ b/examples/build_customized_agent.py @@ -65,7 +65,7 @@ class SimpleCoder(Role): self._init_actions([SimpleWriteCode]) async def _act(self) -> Message: - logger.info(f"{self._setting}: ready to {self._rc.todo}") + logger.info(f"{self._setting}: to do {self._rc.todo}") todo = self._rc.todo # todo will be SimpleWriteCode() msg = self.get_memories(k=1)[0] # find the most recent messages @@ -87,7 +87,7 @@ class RunnableCoder(Role): self._set_react_mode(react_mode="by_order") async def _act(self) -> Message: - logger.info(f"{self._setting}: ready to {self._rc.todo}") + logger.info(f"{self._setting}: to do {self._rc.todo}") # By choosing the Action by order under the hood # todo will be first SimpleWriteCode() then SimpleRunCode() todo = self._rc.todo diff --git a/examples/build_customized_multi_agents.py b/examples/build_customized_multi_agents.py index 030a4b339..865c09c37 100644 --- a/examples/build_customized_multi_agents.py +++ b/examples/build_customized_multi_agents.py @@ -88,7 +88,7 @@ class SimpleTester(Role): self._watch([SimpleWriteCode, SimpleWriteReview]) # feel free to try this too async def _act(self) -> Message: - logger.info(f"{self._setting}: ready to {self._rc.todo}") + logger.info(f"{self._setting}: to do {self._rc.todo}") todo = self._rc.todo # context = self.get_memories(k=1)[0].content # use the most recent memory as context diff --git a/examples/debate.py b/examples/debate.py index 52f49e00e..ba15abda8 100644 --- a/examples/debate.py +++ b/examples/debate.py @@ -63,7 +63,7 @@ class Debator(Role): return len(self._rc.news) async def _act(self) -> Message: - logger.info(f"{self._setting}: ready to {self._rc.todo}") + logger.info(f"{self._setting}: to do {self._rc.todo}") todo = self._rc.todo # An instance of SpeakAloud memories = self.get_memories() diff --git a/examples/debate_simple.py b/examples/debate_simple.py index 0a86c4131..b90af4f82 100644 --- a/examples/debate_simple.py +++ b/examples/debate_simple.py @@ -8,13 +8,15 @@ import asyncio from metagpt.actions import Action, UserRequirement +from metagpt.environment import Environment from metagpt.roles import Role from metagpt.team import Team -action1 = Action(name="BidenSay", instruction="Use diverse words to attack your opponent, strong and emotional.") -action2 = Action(name="TrumpSay", instruction="Use diverse words to attack your opponent, strong and emotional.") -biden = Role(name="Biden", profile="democrat", goal="win election", actions=[action1], watch=[action2, UserRequirement]) -trump = Role(name="Trump", profile="republican", goal="win election", actions=[action2], watch=[action1]) -team = Team(investment=10.0, env_desc="US election live broadcast", roles=[biden, trump]) +action1 = Action(name="BidenSay", instruction="发表政见,充满激情的与对手辩论") +action2 = Action(name="TrumpSay", instruction="发表政见,充满激情的与对手辩论,MAGA!") +biden = Role(name="拜登", profile="民主党", goal="大选获胜", actions=[action1], watch=[action2, UserRequirement]) +trump = Role(name="特朗普", profile="共和党", goal="大选获胜", actions=[action2], watch=[action1]) +env = Environment(desc="US election live broadcast") +team = Team(investment=10.0, env=env, roles=[biden, trump]) -asyncio.run(team.run(idea="Topic: climate change", n_round=5)) +asyncio.run(team.run(idea="主题:气候变化,用中文辩论", n_round=5)) diff --git a/metagpt/actions/action.py b/metagpt/actions/action.py index f0470640d..24237c6f1 100644 --- a/metagpt/actions/action.py +++ b/metagpt/actions/action.py @@ -41,7 +41,7 @@ class Action(BaseModel): def __init_with_instruction(self, instruction: str): """Initialize action with instruction""" - self.node = ActionNode(key=self.name, expected_type=str, instruction=instruction, example="") + self.node = ActionNode(key=self.name, expected_type=str, instruction=instruction, example="", schema="raw") return self def __init__(self, **kwargs: Any): @@ -85,7 +85,8 @@ class Action(BaseModel): async def _run_action_node(self, *args, **kwargs): """Run action node""" msgs = args[0] - context = "\n".join([f"Msg {idx}: {i}" for idx, i in enumerate(reversed(msgs))]) + context = "## History Messages\n" + context += "\n".join([f"{idx}: {i}" for idx, i in enumerate(reversed(msgs))]) return await self.node.fill(context=context, llm=self.llm) async def run(self, *args, **kwargs): diff --git a/metagpt/actions/action_node.py b/metagpt/actions/action_node.py index 795634a17..7445e5000 100644 --- a/metagpt/actions/action_node.py +++ b/metagpt/actions/action_node.py @@ -21,7 +21,7 @@ from metagpt.utils.common import OutputParser, general_after_log TAG = "CONTENT" -LANGUAGE_CONSTRAINT = "Language: Please use the same language as the user input." +LANGUAGE_CONSTRAINT = "Language: Please use the same language as Human INPUT." FORMAT_CONSTRAINT = f"Format: output wrapped inside [{TAG}][/{TAG}] like format example, nothing else." @@ -55,7 +55,7 @@ def dict_to_markdown(d, prefix="- ", kv_sep="\n", postfix="\n"): class ActionNode: """ActionNode is a tree of nodes.""" - mode: str + schema: str # raw/json/markdown, default: "" # Action Context context: str # all the context, including all necessary info @@ -81,6 +81,7 @@ class ActionNode: example: Any, content: str = "", children: dict[str, "ActionNode"] = None, + schema: str = "", ): self.key = key self.expected_type = expected_type @@ -88,6 +89,7 @@ class ActionNode: self.example = example self.content = content self.children = children if children is not None else {} + self.schema = schema def __str__(self): return ( @@ -222,7 +224,13 @@ class ActionNode: mode="children": 编译所有子节点为一个统一模板,包括instruction与example mode="all": NotImplemented mode="root": NotImplemented + schmea: raw/json/markdown + schema="raw": 不编译,context, lang_constaint, instruction + schema="json":编译context, example(json), instruction(markdown), constraint, action + schema="markdown": 编译context, example(markdown), instruction(markdown), constraint, action """ + if schema == "raw": + return context + "\n\n## Actions\n" + LANGUAGE_CONSTRAINT + "\n" + self.instruction # FIXME: json instruction会带来格式问题,如:"Project name": "web_2048 # 项目名称使用下划线", # compile example暂时不支持markdown @@ -283,12 +291,17 @@ class ActionNode: async def simple_fill(self, schema, mode): prompt = self.compile(context=self.context, schema=schema, mode=mode) - mapping = self.get_mapping(mode) - class_name = f"{self.key}_AN" - content, scontent = await self._aask_v1(prompt, class_name, mapping, schema=schema) - self.content = content - self.instruct_content = scontent + if schema != "raw": + mapping = self.get_mapping(mode) + class_name = f"{self.key}_AN" + content, scontent = await self._aask_v1(prompt, class_name, mapping, schema=schema) + self.content = content + self.instruct_content = scontent + else: + self.content = await self.llm.aask(prompt) + self.instruct_content = None + return self async def fill(self, context, llm, schema="json", mode="auto", strgy="simple"): @@ -297,6 +310,7 @@ class ActionNode: :param context: Everything we should know when filling node. :param llm: Large Language Model with pre-defined system message. :param schema: json/markdown, determine example and output format. + - raw: free form text - json: it's easy to open source LLM with json format - markdown: when generating code, markdown is always better :param mode: auto/children/root @@ -310,14 +324,16 @@ class ActionNode: """ self.set_llm(llm) self.set_context(context) + if self.schema: + schema = self.schema if strgy == "simple": - return await self.simple_fill(schema, mode) + return await self.simple_fill(schema=schema, mode=mode) elif strgy == "complex": # 这里隐式假设了拥有children tmp = {} for _, i in self.children.items(): - child = await i.simple_fill(schema, mode) + child = await i.simple_fill(schema=schema, mode=mode) tmp.update(child.instruct_content.dict()) cls = self.create_children_class() self.instruct_content = cls(**tmp) diff --git a/metagpt/environment.py b/metagpt/environment.py index e0fb741c0..4f2fc9c5e 100644 --- a/metagpt/environment.py +++ b/metagpt/environment.py @@ -95,15 +95,18 @@ class Environment(BaseModel): """增加一个在当前环境的角色 Add a role in the current environment """ - role.set_env(self) self.roles[role.profile] = role + role.set_env(self) def add_roles(self, roles: Iterable[Role]): """增加一批在当前环境的角色 Add a batch of characters in the current environment """ for role in roles: - self.add_role(role) + self.roles[role.profile] = role + + for role in roles: # setup system message with roles + role.set_env(self) def publish_message(self, message: Message) -> bool: """ @@ -152,8 +155,8 @@ class Environment(BaseModel): """ return self.roles.get(name, None) - def role_names(self) -> str: - return ", ".join([f"{i.name}" for i in self.roles.values()]) + def role_names(self) -> list[str]: + return [i.name for i in self.roles.values()] @property def is_idle(self): diff --git a/metagpt/roles/researcher.py b/metagpt/roles/researcher.py index 162d72b9b..456e8baba 100644 --- a/metagpt/roles/researcher.py +++ b/metagpt/roles/researcher.py @@ -42,7 +42,7 @@ class Researcher(Role): logger.warning(f"The language `{language}` has not been tested, it may not work.") async def _act(self) -> Message: - logger.info(f"{self._setting}: ready to {self._rc.todo}") + logger.info(f"{self._setting}: to do {self._rc.todo}({self._rc.todo.name})") todo = self._rc.todo msg = self._rc.memory.get(k=1)[0] if isinstance(msg.instruct_content, Report): diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index 9d0898b71..91e56bade 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -139,7 +139,7 @@ class Role(BaseModel): desc: str = "" is_human: bool = False - _llm: BaseGPTAPI = Field(default_factory=LLM) + _llm: BaseGPTAPI = Field(default_factory=LLM) # Each role has its own LLM, use different system message _role_id: str = "" _states: list[str] = [] _actions: list[Action] = [] @@ -258,6 +258,9 @@ class Role(BaseModel): def _init_action_system_message(self, action: Action): action.set_prefix(self._get_prefix()) + def refresh_system_message(self): + self._llm.system_prompt = self._get_prefix() + def set_recovered(self, recovered: bool = False): self.recovered = recovered @@ -336,6 +339,7 @@ class Role(BaseModel): self._rc.env = env if env: env.set_subscription(self, self._subscription) + self.refresh_system_message() # add env message to system message @property def subscription(self) -> Set: @@ -353,7 +357,8 @@ class Role(BaseModel): prefix += CONSTRAINT_TEMPLATE.format(**{"constraints": self.constraints}) if self._rc.env and self._rc.env.desc: - env_desc = f"You are in {self._rc.env.desc} with roles({self._rc.env.role_names()})." + other_role_names = ", ".join(self._rc.env.role_names()) + env_desc = f"You are in {self._rc.env.desc} with roles({other_role_names})." prefix += env_desc return prefix @@ -390,13 +395,13 @@ class Role(BaseModel): self._set_state(next_state) async def _act(self) -> Message: - logger.info(f"{self._setting}: ready to {self._rc.todo}") + logger.info(f"{self._setting}: to do {self._rc.todo}") response = await self._rc.todo.run(self._rc.important_memory) if isinstance(response, (ActionOutput, ActionNode)): msg = Message( content=response.content, instruct_content=response.instruct_content, - role=self.profile, + role=self._setting, cause_by=self._rc.todo, sent_from=self, ) diff --git a/metagpt/roles/searcher.py b/metagpt/roles/searcher.py index e4a672176..da844b4dc 100644 --- a/metagpt/roles/searcher.py +++ b/metagpt/roles/searcher.py @@ -57,7 +57,7 @@ class Searcher(Role): async def _act_sp(self) -> Message: """Performs the search action in a single process.""" - logger.info(f"{self._setting}: ready to {self._rc.todo}") + logger.info(f"{self._setting}: to do {self._rc.todo}") response = await self._rc.todo.run(self._rc.memory.get(k=0)) if isinstance(response, (ActionOutput, ActionNode)): From b6eb776190c05ace0b43c03020797939d1cf2eaf Mon Sep 17 00:00:00 2001 From: better629 Date: Fri, 22 Dec 2023 17:43:10 +0800 Subject: [PATCH 12/28] update sk AzureChatCompletion from base_url to endpoint --- examples/search_with_specific_engine.py | 4 ++-- metagpt/roles/role.py | 2 ++ metagpt/utils/make_sk_kernel.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/search_with_specific_engine.py b/examples/search_with_specific_engine.py index 923f538ed..1a217fdf2 100644 --- a/examples/search_with_specific_engine.py +++ b/examples/search_with_specific_engine.py @@ -9,9 +9,9 @@ async def main(): # Serper API # await Searcher(engine=SearchEngineType.SERPER_GOOGLE).run(question) # SerpAPI - # await Searcher(engine=SearchEngineType.SERPAPI_GOOGLE).run(question) + await Searcher(engine=SearchEngineType.SERPAPI_GOOGLE).run(question) # Google API - await Searcher(engine=SearchEngineType.DIRECT_GOOGLE).run(question) + # await Searcher(engine=SearchEngineType.DIRECT_GOOGLE).run(question) if __name__ == "__main__": diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index b9fde7d05..e2560128b 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -204,6 +204,8 @@ class Role(BaseModel): object.__setattr__(self, "builtin_class_name", self.__class__.__name__) self.__fields__["builtin_class_name"].default = self.__class__.__name__ + self._watch(kwargs.get("watch") or [UserRequirement]) + def __init_subclass__(cls, **kwargs: Any) -> None: super().__init_subclass__(**kwargs) role_subclass_registry[cls.__name__] = cls diff --git a/metagpt/utils/make_sk_kernel.py b/metagpt/utils/make_sk_kernel.py index 5edddd618..de84e3630 100644 --- a/metagpt/utils/make_sk_kernel.py +++ b/metagpt/utils/make_sk_kernel.py @@ -22,7 +22,7 @@ def make_sk_kernel(): kernel.add_chat_service( "chat_completion", AzureChatCompletion( - deployment_name=CONFIG.deployment_name, base_url=CONFIG.openai_base_url, api_key=CONFIG.openai_api_key + deployment_name=CONFIG.deployment_name, endpoint=CONFIG.openai_base_url, api_key=CONFIG.openai_api_key ), ) else: From 6cd083a2b71037d88372d5645bce174514a392ae Mon Sep 17 00:00:00 2001 From: geekan Date: Fri, 22 Dec 2023 17:48:54 +0800 Subject: [PATCH 13/28] tuning performance --- examples/agent_creator.py | 2 +- examples/build_customized_agent.py | 4 ++-- examples/build_customized_multi_agents.py | 2 +- examples/debate.py | 2 +- examples/debate_simple.py | 8 ++++---- metagpt/actions/write_code_review.py | 2 +- metagpt/roles/role.py | 2 +- metagpt/roles/searcher.py | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/examples/agent_creator.py b/examples/agent_creator.py index 961d12968..dbb6f735a 100644 --- a/examples/agent_creator.py +++ b/examples/agent_creator.py @@ -67,7 +67,7 @@ class AgentCreator(Role): self.agent_template = agent_template async def _act(self) -> Message: - logger.info(f"{self._setting}: to do {self._rc.todo}") + logger.info(f"{self._setting}: to do {self._rc.todo}({self._rc.todo.name})") todo = self._rc.todo msg = self._rc.memory.get()[-1] diff --git a/examples/build_customized_agent.py b/examples/build_customized_agent.py index eb92d9a9c..ccdf7748a 100644 --- a/examples/build_customized_agent.py +++ b/examples/build_customized_agent.py @@ -65,7 +65,7 @@ class SimpleCoder(Role): self._init_actions([SimpleWriteCode]) async def _act(self) -> Message: - logger.info(f"{self._setting}: to do {self._rc.todo}") + logger.info(f"{self._setting}: to do {self._rc.todo}({self._rc.todo.name})") todo = self._rc.todo # todo will be SimpleWriteCode() msg = self.get_memories(k=1)[0] # find the most recent messages @@ -87,7 +87,7 @@ class RunnableCoder(Role): self._set_react_mode(react_mode="by_order") async def _act(self) -> Message: - logger.info(f"{self._setting}: to do {self._rc.todo}") + logger.info(f"{self._setting}: to do {self._rc.todo}({self._rc.todo.name})") # By choosing the Action by order under the hood # todo will be first SimpleWriteCode() then SimpleRunCode() todo = self._rc.todo diff --git a/examples/build_customized_multi_agents.py b/examples/build_customized_multi_agents.py index 865c09c37..16c36e08d 100644 --- a/examples/build_customized_multi_agents.py +++ b/examples/build_customized_multi_agents.py @@ -88,7 +88,7 @@ class SimpleTester(Role): self._watch([SimpleWriteCode, SimpleWriteReview]) # feel free to try this too async def _act(self) -> Message: - logger.info(f"{self._setting}: to do {self._rc.todo}") + logger.info(f"{self._setting}: to do {self._rc.todo}({self._rc.todo.name})") todo = self._rc.todo # context = self.get_memories(k=1)[0].content # use the most recent memory as context diff --git a/examples/debate.py b/examples/debate.py index ba15abda8..b3d287079 100644 --- a/examples/debate.py +++ b/examples/debate.py @@ -63,7 +63,7 @@ class Debator(Role): return len(self._rc.news) async def _act(self) -> Message: - logger.info(f"{self._setting}: to do {self._rc.todo}") + logger.info(f"{self._setting}: to do {self._rc.todo}({self._rc.todo.name})") todo = self._rc.todo # An instance of SpeakAloud memories = self.get_memories() diff --git a/examples/debate_simple.py b/examples/debate_simple.py index b90af4f82..524449771 100644 --- a/examples/debate_simple.py +++ b/examples/debate_simple.py @@ -12,10 +12,10 @@ from metagpt.environment import Environment from metagpt.roles import Role from metagpt.team import Team -action1 = Action(name="BidenSay", instruction="发表政见,充满激情的与对手辩论") -action2 = Action(name="TrumpSay", instruction="发表政见,充满激情的与对手辩论,MAGA!") -biden = Role(name="拜登", profile="民主党", goal="大选获胜", actions=[action1], watch=[action2, UserRequirement]) -trump = Role(name="特朗普", profile="共和党", goal="大选获胜", actions=[action2], watch=[action1]) +action1 = Action(name="BidenSay", instruction="发表政见,充满激情的反驳特朗普最新消息,尽最大努力获得选票") +action2 = Action(name="TrumpSay", instruction="发表政见,充满激情的反驳拜登最新消息,尽最大努力获得选票,MAGA!") +biden = Role(name="拜登", profile="民主党候选人", goal="大选获胜", actions=[action1], watch=[action2, UserRequirement]) +trump = Role(name="特朗普", profile="共和党候选人", goal="大选获胜", actions=[action2], watch=[action1]) env = Environment(desc="US election live broadcast") team = Team(investment=10.0, env=env, roles=[biden, trump]) diff --git a/metagpt/actions/write_code_review.py b/metagpt/actions/write_code_review.py index 1eba672a5..b0e7904e3 100644 --- a/metagpt/actions/write_code_review.py +++ b/metagpt/actions/write_code_review.py @@ -40,7 +40,7 @@ EXAMPLE_AND_INSTRUCTION = """ {format_example} -# Instruction: Based on the actual code situation, follow one of the "Format example". +# Instruction: Based on the actual code situation, follow one of the "Format example". Return only 1 file under review. ## Code Review: Ordered List. Based on the "Code to be Reviewed", provide key, clear, concise, and specific answer. If any answer is no, explain how to fix it step by step. 1. Is the code implemented as per the requirements? If not, how to achieve it? Analyse it step by step. diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index 91e56bade..d868e820f 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -395,7 +395,7 @@ class Role(BaseModel): self._set_state(next_state) async def _act(self) -> Message: - logger.info(f"{self._setting}: to do {self._rc.todo}") + logger.info(f"{self._setting}: to do {self._rc.todo}({self._rc.todo.name})") response = await self._rc.todo.run(self._rc.important_memory) if isinstance(response, (ActionOutput, ActionNode)): msg = Message( diff --git a/metagpt/roles/searcher.py b/metagpt/roles/searcher.py index da844b4dc..6e2bd8bc9 100644 --- a/metagpt/roles/searcher.py +++ b/metagpt/roles/searcher.py @@ -57,7 +57,7 @@ class Searcher(Role): async def _act_sp(self) -> Message: """Performs the search action in a single process.""" - logger.info(f"{self._setting}: to do {self._rc.todo}") + logger.info(f"{self._setting}: to do {self._rc.todo}({self._rc.todo.name})") response = await self._rc.todo.run(self._rc.memory.get(k=0)) if isinstance(response, (ActionOutput, ActionNode)): From da1e0b87920dd84f71c4ab3b26e64c7d49a89898 Mon Sep 17 00:00:00 2001 From: better629 Date: Fri, 22 Dec 2023 17:59:06 +0800 Subject: [PATCH 14/28] fix invoice_ocr_assistant --- metagpt/roles/invoice_ocr_assistant.py | 1 + 1 file changed, 1 insertion(+) diff --git a/metagpt/roles/invoice_ocr_assistant.py b/metagpt/roles/invoice_ocr_assistant.py index 17086d42a..1e28bc078 100644 --- a/metagpt/roles/invoice_ocr_assistant.py +++ b/metagpt/roles/invoice_ocr_assistant.py @@ -41,6 +41,7 @@ class InvoiceOCRAssistant(Role): def __init__(self, **kwargs): super().__init__(**kwargs) + self._init_actions([InvoiceOCR]) self._set_react_mode(react_mode=RoleReactMode.BY_ORDER.value) async def _act(self) -> Message: From a1e1eb830736fd6f0fd976fd7bbe9d391b0d7b8c Mon Sep 17 00:00:00 2001 From: better629 Date: Fri, 22 Dec 2023 20:25:19 +0800 Subject: [PATCH 15/28] fix invoice_ocr --- examples/invoice_ocr.py | 4 +-- metagpt/roles/invoice_ocr_assistant.py | 28 +++++++++++++++++-- metagpt/roles/role.py | 1 + .../roles/test_invoice_ocr_assistant.py | 4 +-- 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/examples/invoice_ocr.py b/examples/invoice_ocr.py index a6e565772..d9a2e8a6d 100644 --- a/examples/invoice_ocr.py +++ b/examples/invoice_ocr.py @@ -10,7 +10,7 @@ import asyncio from pathlib import Path -from metagpt.roles.invoice_ocr_assistant import InvoiceOCRAssistant +from metagpt.roles.invoice_ocr_assistant import InvoiceOCRAssistant, InvoicePath from metagpt.schema import Message @@ -26,7 +26,7 @@ async def main(): for path in absolute_file_paths: role = InvoiceOCRAssistant() - await role.run(Message(content="Invoicing date", instruct_content={"file_path": path})) + await role.run(Message(content="Invoicing date", instruct_content=InvoicePath(file_path=path))) if __name__ == "__main__": diff --git a/metagpt/roles/invoice_ocr_assistant.py b/metagpt/roles/invoice_ocr_assistant.py index 1e28bc078..56d729fa9 100644 --- a/metagpt/roles/invoice_ocr_assistant.py +++ b/metagpt/roles/invoice_ocr_assistant.py @@ -7,9 +7,11 @@ @File : invoice_ocr_assistant.py """ +from pathlib import Path from typing import Optional import pandas as pd +from pydantic import BaseModel from metagpt.actions.invoice_ocr import GenerateTable, InvoiceOCR, ReplyQuestion from metagpt.prompts.invoice_ocr import INVOICE_OCR_SUCCESS @@ -17,6 +19,22 @@ from metagpt.roles.role import Role, RoleReactMode from metagpt.schema import Message +class InvoicePath(BaseModel): + file_path: Path = "" + + +class OCRResults(BaseModel): + ocr_results: list[dict] = [] + + +class InvoiceData(BaseModel): + invoice_data: list[dict] = [] + + +class ReplyData(BaseModel): + content: str = "" + + class InvoiceOCRAssistant(Role): """Invoice OCR assistant, support OCR text recognition of invoice PDF, png, jpg, and zip files, generate a table for the payee, city, total amount, and invoicing date of the invoice, @@ -54,7 +72,8 @@ class InvoiceOCRAssistant(Role): todo = self._rc.todo if isinstance(todo, InvoiceOCR): self.origin_query = msg.content - file_path = msg.instruct_content.get("file_path") + invoice_path: InvoicePath = msg.instruct_content + file_path = invoice_path.file_path self.filename = file_path.name if not file_path: raise Exception("Invoice file not uploaded") @@ -69,17 +88,20 @@ class InvoiceOCRAssistant(Role): self._rc.todo = None content = INVOICE_OCR_SUCCESS + resp = OCRResults(ocr_results=resp) elif isinstance(todo, GenerateTable): - ocr_results = msg.instruct_content - resp = await todo.run(ocr_results, self.filename) + ocr_results: OCRResults = msg.instruct_content + resp = await todo.run(ocr_results.ocr_results, self.filename) # Convert list to Markdown format string df = pd.DataFrame(resp) markdown_table = df.to_markdown(index=False) content = f"{markdown_table}\n\n\n" + resp = InvoiceData(invoice_data=resp) else: resp = await todo.run(self.origin_query, self.orc_data) content = resp + resp = ReplyData(content=resp) msg = Message(content=content, instruct_content=resp) self._rc.memory.add(msg) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index e2560128b..98d835281 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -465,6 +465,7 @@ class Role(BaseModel): async def _act_by_order(self) -> Message: """switch action each time by order defined in _init_actions, i.e. _act (Action1) -> _act (Action2) -> ...""" start_idx = self._rc.state if self._rc.state >= 0 else 0 # action to run from recovered state + rsp = Message(content="No actions taken yet") # return default message if _actions=[] for i in range(start_idx, len(self._states)): self._set_state(i) rsp = await self._act() diff --git a/tests/metagpt/roles/test_invoice_ocr_assistant.py b/tests/metagpt/roles/test_invoice_ocr_assistant.py index c9aad93a7..e5a570f53 100644 --- a/tests/metagpt/roles/test_invoice_ocr_assistant.py +++ b/tests/metagpt/roles/test_invoice_ocr_assistant.py @@ -12,7 +12,7 @@ from pathlib import Path import pandas as pd import pytest -from metagpt.roles.invoice_ocr_assistant import InvoiceOCRAssistant +from metagpt.roles.invoice_ocr_assistant import InvoiceOCRAssistant, InvoicePath from metagpt.schema import Message @@ -55,7 +55,7 @@ async def test_invoice_ocr_assistant( ): invoice_path = Path.cwd() / invoice_path role = InvoiceOCRAssistant() - await role.run(Message(content=query, instruct_content={"file_path": invoice_path})) + await role.run(Message(content=query, instruct_content=InvoicePath(file_path=invoice_path))) invoice_table_path = Path.cwd() / invoice_table_path df = pd.read_excel(invoice_table_path) dict_result = df.to_dict(orient="records") From 3e74b5890978468b824f76d15a568d357948bd92 Mon Sep 17 00:00:00 2001 From: better629 Date: Fri, 22 Dec 2023 20:44:40 +0800 Subject: [PATCH 16/28] fix invoice_ocr --- metagpt/roles/invoice_ocr_assistant.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/roles/invoice_ocr_assistant.py b/metagpt/roles/invoice_ocr_assistant.py index 56d729fa9..bd60c43c8 100644 --- a/metagpt/roles/invoice_ocr_assistant.py +++ b/metagpt/roles/invoice_ocr_assistant.py @@ -24,7 +24,7 @@ class InvoicePath(BaseModel): class OCRResults(BaseModel): - ocr_results: list[dict] = [] + ocr_results: list = [] class InvoiceData(BaseModel): From 67a325b05dc8e1887acc00490cf88af1e23b5411 Mon Sep 17 00:00:00 2001 From: better629 Date: Fri, 22 Dec 2023 21:06:44 +0800 Subject: [PATCH 17/28] fix invoice_ocr --- metagpt/roles/invoice_ocr_assistant.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/metagpt/roles/invoice_ocr_assistant.py b/metagpt/roles/invoice_ocr_assistant.py index bd60c43c8..84e354c0e 100644 --- a/metagpt/roles/invoice_ocr_assistant.py +++ b/metagpt/roles/invoice_ocr_assistant.py @@ -7,6 +7,7 @@ @File : invoice_ocr_assistant.py """ +import json from pathlib import Path from typing import Optional @@ -24,7 +25,7 @@ class InvoicePath(BaseModel): class OCRResults(BaseModel): - ocr_results: list = [] + ocr_result: str = "[]" class InvoiceData(BaseModel): @@ -88,10 +89,10 @@ class InvoiceOCRAssistant(Role): self._rc.todo = None content = INVOICE_OCR_SUCCESS - resp = OCRResults(ocr_results=resp) + resp = OCRResults(ocr_result=json.dumps(resp)) elif isinstance(todo, GenerateTable): ocr_results: OCRResults = msg.instruct_content - resp = await todo.run(ocr_results.ocr_results, self.filename) + resp = await todo.run(json.loads(ocr_results.ocr_result), self.filename) # Convert list to Markdown format string df = pd.DataFrame(resp) From 571063069eedff877c9ac755b95a84351b78f9f2 Mon Sep 17 00:00:00 2001 From: better629 Date: Fri, 22 Dec 2023 22:22:01 +0800 Subject: [PATCH 18/28] fix --- metagpt/actions/invoice_ocr.py | 1 + metagpt/roles/invoice_ocr_assistant.py | 3 +++ tests/metagpt/roles/test_invoice_ocr_assistant.py | 3 ++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/metagpt/actions/invoice_ocr.py b/metagpt/actions/invoice_ocr.py index 11b4febc0..87f81371e 100644 --- a/metagpt/actions/invoice_ocr.py +++ b/metagpt/actions/invoice_ocr.py @@ -133,6 +133,7 @@ class GenerateTable(Action): name: str = "GenerateTable" context: Optional[str] = None llm: BaseGPTAPI = Field(default_factory=LLM) + language: str = "ch" async def run(self, ocr_results: list, filename: str, *args, **kwargs) -> dict[str, str]: """Processes OCR results, extracts invoice information, generates a table, and saves it as an Excel file. diff --git a/metagpt/roles/invoice_ocr_assistant.py b/metagpt/roles/invoice_ocr_assistant.py index 84e354c0e..3349a498f 100644 --- a/metagpt/roles/invoice_ocr_assistant.py +++ b/metagpt/roles/invoice_ocr_assistant.py @@ -90,6 +90,9 @@ class InvoiceOCRAssistant(Role): self._rc.todo = None content = INVOICE_OCR_SUCCESS resp = OCRResults(ocr_result=json.dumps(resp)) + msg = Message(content=content, instruct_content=resp) + self._rc.memory.add(msg) + return await super().react() elif isinstance(todo, GenerateTable): ocr_results: OCRResults = msg.instruct_content resp = await todo.run(json.loads(ocr_results.ocr_result), self.filename) diff --git a/tests/metagpt/roles/test_invoice_ocr_assistant.py b/tests/metagpt/roles/test_invoice_ocr_assistant.py index e5a570f53..ab3092004 100644 --- a/tests/metagpt/roles/test_invoice_ocr_assistant.py +++ b/tests/metagpt/roles/test_invoice_ocr_assistant.py @@ -7,6 +7,7 @@ @File : test_invoice_ocr_assistant.py """ +import json from pathlib import Path import pandas as pd @@ -59,4 +60,4 @@ async def test_invoice_ocr_assistant( invoice_table_path = Path.cwd() / invoice_table_path df = pd.read_excel(invoice_table_path) dict_result = df.to_dict(orient="records") - assert dict_result == expected_result + assert json.dumps(dict_result) == json.dumps(expected_result) From 3feee734929c9b7383fed79fcd3b7ad6a95960e7 Mon Sep 17 00:00:00 2001 From: geekan Date: Fri, 22 Dec 2023 18:53:44 +0800 Subject: [PATCH 19/28] refine debate example --- examples/debate_simple.py | 12 ++++++------ metagpt/roles/role.py | 17 ++++++----------- metagpt/team.py | 4 ++-- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/examples/debate_simple.py b/examples/debate_simple.py index 524449771..fe04a7d1a 100644 --- a/examples/debate_simple.py +++ b/examples/debate_simple.py @@ -7,16 +7,16 @@ """ import asyncio -from metagpt.actions import Action, UserRequirement +from metagpt.actions import Action from metagpt.environment import Environment from metagpt.roles import Role from metagpt.team import Team -action1 = Action(name="BidenSay", instruction="发表政见,充满激情的反驳特朗普最新消息,尽最大努力获得选票") -action2 = Action(name="TrumpSay", instruction="发表政见,充满激情的反驳拜登最新消息,尽最大努力获得选票,MAGA!") -biden = Role(name="拜登", profile="民主党候选人", goal="大选获胜", actions=[action1], watch=[action2, UserRequirement]) -trump = Role(name="特朗普", profile="共和党候选人", goal="大选获胜", actions=[action2], watch=[action1]) +action1 = Action(name="BidenSay", instruction="Passionately refute Trump's latest news, and strive to gain votes") +action2 = Action(name="TrumpSay", instruction="Passionately refute Biden's latest news, 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]) env = Environment(desc="US election live broadcast") team = Team(investment=10.0, env=env, roles=[biden, trump]) -asyncio.run(team.run(idea="主题:气候变化,用中文辩论", n_round=5)) +asyncio.run(team.run(idea="Topic: Climate Change", send_to="Biden", n_round=5)) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index b59f51929..e5142fbec 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -144,7 +144,7 @@ class Role(BaseModel): _states: list[str] = [] _actions: list[Action] = [] _rc: RoleContext = Field(default_factory=RoleContext) - _subscription: tuple[str] = set() + subscription: set[str] = set() # builtin variables recovered: bool = False # to tag if a recovered role @@ -183,7 +183,7 @@ class Role(BaseModel): # 关于私有变量的初始化 https://github.com/pydantic/pydantic/issues/655 self._private_attributes["_llm"] = LLM() if not self.is_human else HumanProvider() self._private_attributes["_role_id"] = str(self._setting) - self._private_attributes["_subscription"] = {any_to_str(self), self.name} if self.name else {any_to_str(self)} + self.subscription = {any_to_str(self), self.name} if self.name else {any_to_str(self)} for key in self._private_attributes.keys(): if key in kwargs: @@ -322,9 +322,9 @@ class Role(BaseModel): buffer to be further processed in _observe. By default, a Role subscribes Messages with a tag of its own name or profile. """ - self._subscription = tags + self.subscription = tags if self._rc.env: # According to the routing feature plan in Chapter 2.2.3.2 of RFC 113 - self._rc.env.set_subscription(self, self._subscription) + self._rc.env.set_subscription(self, self.subscription) def _set_state(self, state: int): """Update the current state.""" @@ -337,14 +337,9 @@ class Role(BaseModel): messages by observing.""" self._rc.env = env if env: - env.set_subscription(self, self._subscription) + env.set_subscription(self, self.subscription) self.refresh_system_message() # add env message to system message - @property - def subscription(self) -> Set: - """The labels for messages to be consumed by the Role object.""" - return self._subscription - def _get_prefix(self): """Get the role prefix""" if self.desc: @@ -418,7 +413,7 @@ class Role(BaseModel): 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 and new not in existed_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 diff --git a/metagpt/team.py b/metagpt/team.py index 0b9f042df..879da0aca 100644 --- a/metagpt/team.py +++ b/metagpt/team.py @@ -120,10 +120,10 @@ class Team(BaseModel): logger.info(self.json(ensure_ascii=False)) @serialize_decorator - async def run(self, n_round=3, idea=""): + async def run(self, n_round=3, idea="", send_to=""): """Run company until target round or no money""" if idea: - self.run_project(idea=idea) + self.run_project(idea=idea, send_to=send_to) while n_round > 0: # self._save() From 336350eba9db48c33d1049218eb9bfc088e6958e Mon Sep 17 00:00:00 2001 From: geekan Date: Fri, 22 Dec 2023 22:49:13 +0800 Subject: [PATCH 20/28] refine code --- examples/debate_simple.py | 6 +++--- metagpt/roles/role.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/debate_simple.py b/examples/debate_simple.py index fe04a7d1a..1a80bf8f4 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="Passionately refute Trump's latest news, and strive to gain votes") -action2 = Action(name="TrumpSay", instruction="Passionately refute Biden's latest news, and strive to gain votes") +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]) env = Environment(desc="US election live broadcast") team = Team(investment=10.0, env=env, roles=[biden, trump]) -asyncio.run(team.run(idea="Topic: Climate Change", send_to="Biden", n_round=5)) +asyncio.run(team.run(idea="Topic: climate change. Under 80 words per message.", send_to="Biden", n_round=5)) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index e5142fbec..6a7cc32ec 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -390,7 +390,7 @@ class Role(BaseModel): async def _act(self) -> Message: logger.info(f"{self._setting}: to do {self._rc.todo}({self._rc.todo.name})") - response = await self._rc.todo.run(self._rc.important_memory) + response = await self._rc.todo.run(self._rc.history) if isinstance(response, (ActionOutput, ActionNode)): msg = Message( content=response.content, From 6624819febf5cdbd336f147425608713b668c185 Mon Sep 17 00:00:00 2001 From: geekan Date: Sat, 23 Dec 2023 17:38:47 +0800 Subject: [PATCH 21/28] add test case for action node --- metagpt/actions/action_node.py | 16 ++-- metagpt/roles/role.py | 2 +- tests/metagpt/actions/test_action_node.py | 76 +++++++++++++++++++ ...l_mining.py => test_generate_questions.py} | 4 +- .../metagpt/actions/test_prepare_interview.py | 2 +- 5 files changed, 88 insertions(+), 12 deletions(-) create mode 100644 tests/metagpt/actions/test_action_node.py rename tests/metagpt/actions/{test_detail_mining.py => test_generate_questions.py} (86%) diff --git a/metagpt/actions/action_node.py b/metagpt/actions/action_node.py index 7445e5000..3529942c3 100644 --- a/metagpt/actions/action_node.py +++ b/metagpt/actions/action_node.py @@ -340,15 +340,15 @@ class ActionNode: return self -def action_node_from_tuple_example(): - # 示例:列表中包含元组 - list_of_tuples = [("key1", str, "Instruction 1", "Example 1")] +def action_node_example(): + node = ActionNode(key="key-0", expected_type=str, instruction="instruction-a", example="example-b") - # 从列表中创建 ActionNode 实例 - nodes = [ActionNode(*data) for data in list_of_tuples] - for i in nodes: - logger.info(i) + logger.info(node.compile(context="123", schema="raw", mode="auto")) + logger.info(node.compile(context="123", schema="json", mode="auto")) + logger.info(node.compile(context="123", schema="markdown", mode="auto")) + logger.info(node.to_dict()) + logger.info(node) if __name__ == "__main__": - action_node_from_tuple_example() + action_node_example() diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index 6a7cc32ec..c25cd947c 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -520,7 +520,7 @@ class Role(BaseModel): return self._rc.memory.get(k=k) @role_raise_decorator - async def run(self, with_message=None): + async def run(self, with_message=None) -> Message | None: """Observe, and think and act based on the results of the observation""" if with_message: msg = None diff --git a/tests/metagpt/actions/test_action_node.py b/tests/metagpt/actions/test_action_node.py new file mode 100644 index 000000000..24b48f2f6 --- /dev/null +++ b/tests/metagpt/actions/test_action_node.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@Time : 2023/12/23 15:49 +@Author : alexanderwu +@File : test_action_node.py +""" +import pytest + +from metagpt.actions import Action +from metagpt.actions.action_node import ActionNode +from metagpt.environment import Environment +from metagpt.roles import Role +from metagpt.schema import Message +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") + 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] + ) + 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 "BidenSay" 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]) + 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 "Debate" 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]) + msg: Message = await biden.run("Topic: climate change. Under 80 words per message.") + + assert len(msg.content) > 10 + assert msg.sent_from == "metagpt.roles.role.Role" + + +@pytest.mark.asyncio +async def test_action_node(): + node = ActionNode(key="key-a", expected_type=str, instruction="instruction-b", example="example-c") + + raw_template = node.compile(context="123", schema="raw", mode="auto") + json_template = node.compile(context="123", schema="json", mode="auto") + markdown_template = node.compile(context="123", schema="markdown", mode="auto") + node_dict = node.to_dict() + + assert "123" in raw_template + assert "instruction" in raw_template + + assert "123" in json_template + assert "format example" in json_template + assert "constraint" in json_template + assert "action" in json_template + assert "[/" in json_template + + assert "123" in markdown_template + assert "key-a" in markdown_template + + assert node_dict["key-a"] == "instruction-b" diff --git a/tests/metagpt/actions/test_detail_mining.py b/tests/metagpt/actions/test_generate_questions.py similarity index 86% rename from tests/metagpt/actions/test_detail_mining.py rename to tests/metagpt/actions/test_generate_questions.py index a178ec840..b7c9d3984 100644 --- a/tests/metagpt/actions/test_detail_mining.py +++ b/tests/metagpt/actions/test_generate_questions.py @@ -21,8 +21,8 @@ context = """ @pytest.mark.asyncio async def test_generate_questions(): - detail_mining = GenerateQuestions() - rsp = await detail_mining.run(context) + action = GenerateQuestions() + rsp = await action.run(context) logger.info(f"{rsp.content=}") assert "Questions" in rsp.content diff --git a/tests/metagpt/actions/test_prepare_interview.py b/tests/metagpt/actions/test_prepare_interview.py index 7c32882e0..cd0c850ed 100644 --- a/tests/metagpt/actions/test_prepare_interview.py +++ b/tests/metagpt/actions/test_prepare_interview.py @@ -3,7 +3,7 @@ """ @Time : 2023/9/13 00:26 @Author : fisherdeng -@File : test_detail_mining.py +@File : test_generate_questions.py """ import pytest From c7f47e80add05eb7bb4ea8c303ab7641caec92e7 Mon Sep 17 00:00:00 2001 From: geekan Date: Sat, 23 Dec 2023 19:35:07 +0800 Subject: [PATCH 22/28] add test --- metagpt/actions/action.py | 2 +- tests/metagpt/actions/test_action_node.py | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/metagpt/actions/action.py b/metagpt/actions/action.py index 24237c6f1..c8c901eb0 100644 --- a/metagpt/actions/action.py +++ b/metagpt/actions/action.py @@ -59,7 +59,7 @@ class Action(BaseModel): action_subclass_registry[cls.__name__] = cls def dict(self, *args, **kwargs) -> "DictStrAny": - obj_dict = super(Action, self).dict(*args, **kwargs) + obj_dict = super().dict(*args, **kwargs) if "llm" in obj_dict: obj_dict.pop("llm") return obj_dict diff --git a/tests/metagpt/actions/test_action_node.py b/tests/metagpt/actions/test_action_node.py index 24b48f2f6..5bafe2bf2 100644 --- a/tests/metagpt/actions/test_action_node.py +++ b/tests/metagpt/actions/test_action_node.py @@ -53,7 +53,7 @@ async def test_debate_one_role(): @pytest.mark.asyncio -async def test_action_node(): +async def test_action_node_one_layer(): node = ActionNode(key="key-a", expected_type=str, instruction="instruction-b", example="example-c") raw_template = node.compile(context="123", schema="raw", mode="auto") @@ -74,3 +74,15 @@ async def test_action_node(): assert "key-a" in markdown_template assert node_dict["key-a"] == "instruction-b" + + +@pytest.mark.asyncio +async def test_action_node_two_layer(): + node_a = ActionNode(key="key-a", expected_type=str, instruction="i-a", example="e-a") + node_b = ActionNode(key="key-b", expected_type=str, instruction="i-b", example="e-b") + + root = ActionNode.from_children(key="", nodes=[node_a, node_b]) + assert "key-a" in root.children + assert node_b in root.children.values() + json_template = root.compile(context="123", schema="json", mode="auto") + assert "i-a" in json_template From 4b120a932f7821088021f71c21c4e32e6b3fca08 Mon Sep 17 00:00:00 2001 From: shenchucheng Date: Sat, 23 Dec 2023 21:56:19 +0800 Subject: [PATCH 23/28] add options to disable llm provider check --- config/config.yaml | 4 +++- metagpt/config.py | 4 +++- metagpt/llm.py | 7 ++++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/config/config.yaml b/config/config.yaml index a9c764c56..6a1fd597f 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -117,4 +117,6 @@ RPM: 10 ### repair operation on the content extracted from LLM's raw output. Warning, it improves the result but not fix all cases. # REPAIR_LLM_OUTPUT: false -# PROMPT_FORMAT: json #json or markdown \ No newline at end of file +# PROMPT_FORMAT: json #json or markdown + +# DISABLE_LLM_PROVIDER_CHECK: false diff --git a/metagpt/config.py b/metagpt/config.py index 208b4fd7b..16df19a4c 100644 --- a/metagpt/config.py +++ b/metagpt/config.py @@ -107,7 +107,9 @@ class Config(metaclass=Singleton): self.gemini_api_key = self._get("GEMINI_API_KEY") self.ollama_api_base = self._get("OLLAMA_API_BASE") self.ollama_api_model = self._get("OLLAMA_API_MODEL") - _ = self.get_default_llm_provider_enum() + + if not self._get("DISABLE_LLM_PROVIDER_CHECK"): + _ = self.get_default_llm_provider_enum() self.openai_base_url = self._get("OPENAI_BASE_URL") self.openai_proxy = self._get("OPENAI_PROXY") or self.global_proxy diff --git a/metagpt/llm.py b/metagpt/llm.py index 8763642f0..f1cb98dae 100644 --- a/metagpt/llm.py +++ b/metagpt/llm.py @@ -6,6 +6,8 @@ @File : llm.py """ +from typing import Optional + from metagpt.config import CONFIG, LLMProviderEnum from metagpt.provider.base_gpt_api import BaseGPTAPI from metagpt.provider.human_provider import HumanProvider @@ -14,6 +16,9 @@ from metagpt.provider.llm_provider_registry import LLM_REGISTRY _ = HumanProvider() # Avoid pre-commit error -def LLM(provider: LLMProviderEnum = CONFIG.get_default_llm_provider_enum()) -> BaseGPTAPI: +def LLM(provider: Optional[LLMProviderEnum] = None) -> BaseGPTAPI: """get the default llm provider""" + if provider is None: + provider = CONFIG.get_default_llm_provider_enum() + return LLM_REGISTRY.get_provider(provider) From 011ae46c09dd81f10e49060530f73985ec13c488 Mon Sep 17 00:00:00 2001 From: shenchucheng Date: Sat, 23 Dec 2023 21:58:54 +0800 Subject: [PATCH 24/28] Lazy Loading WEB_BROWSER_ENGINE --- metagpt/actions/research.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/metagpt/actions/research.py b/metagpt/actions/research.py index 074cdee0a..c47a77bdd 100644 --- a/metagpt/actions/research.py +++ b/metagpt/actions/research.py @@ -180,9 +180,11 @@ class WebBrowseAndSummarize(Action): llm: BaseGPTAPI = Field(default_factory=LLM) desc: str = "Explore the web and provide summaries of articles and webpages." browse_func: Union[Callable[[list[str]], None], None] = None - web_browser_engine: WebBrowserEngine = WebBrowserEngine( - engine=WebBrowserEngineType.CUSTOM if browse_func else None, - run_func=browse_func, + web_browser_engine: WebBrowserEngine = Field( + default_factory=lambda: WebBrowserEngine( + engine=WebBrowserEngineType.CUSTOM if WebBrowseAndSummarize.browse_func else None, + run_func=WebBrowseAndSummarize.browse_func, + ) ) def __init__(self, **kwargs): From b4552938e68de47fb9cc8af987b79403e5a146c6 Mon Sep 17 00:00:00 2001 From: shenchucheng Date: Sat, 23 Dec 2023 22:45:20 +0800 Subject: [PATCH 25/28] add llm stream log --- metagpt/logs.py | 13 +++++++++++++ metagpt/provider/google_gemini_api.py | 4 ++-- metagpt/provider/ollama_api.py | 4 ++-- metagpt/provider/openai_api.py | 4 ++-- metagpt/provider/zhipuai_api.py | 4 ++-- 5 files changed, 21 insertions(+), 8 deletions(-) diff --git a/metagpt/logs.py b/metagpt/logs.py index ab1bc4e94..fb0fdd553 100644 --- a/metagpt/logs.py +++ b/metagpt/logs.py @@ -8,6 +8,7 @@ import sys from datetime import datetime +from functools import partial from loguru import logger as _logger @@ -26,3 +27,15 @@ def define_log_level(print_level="INFO", logfile_level="DEBUG"): logger = define_log_level() + + +def log_llm_stream(msg): + _llm_stream_log(msg) + + +def set_llm_stream_logfunc(func): + global _llm_stream_log + _llm_stream_log = func + + +_llm_stream_log = partial(print, end="") diff --git a/metagpt/provider/google_gemini_api.py b/metagpt/provider/google_gemini_api.py index 682f7b507..63a2ff687 100644 --- a/metagpt/provider/google_gemini_api.py +++ b/metagpt/provider/google_gemini_api.py @@ -20,7 +20,7 @@ from tenacity import ( ) from metagpt.config import CONFIG, LLMProviderEnum -from metagpt.logs import logger +from metagpt.logs import log_llm_stream, logger from metagpt.provider.base_gpt_api import BaseGPTAPI from metagpt.provider.llm_provider_registry import register_provider from metagpt.provider.openai_api import CostManager, log_and_reraise @@ -119,7 +119,7 @@ class GeminiGPTAPI(BaseGPTAPI): collected_content = [] async for chunk in resp: content = chunk.text - print(content, end="") + log_llm_stream(content, end="") collected_content.append(content) full_content = "".join(collected_content) diff --git a/metagpt/provider/ollama_api.py b/metagpt/provider/ollama_api.py index a15c46458..d668d3af1 100644 --- a/metagpt/provider/ollama_api.py +++ b/metagpt/provider/ollama_api.py @@ -15,7 +15,7 @@ from tenacity import ( from metagpt.config import CONFIG, LLMProviderEnum from metagpt.const import LLM_API_TIMEOUT -from metagpt.logs import logger +from metagpt.logs import log_llm_stream, logger from metagpt.provider.base_gpt_api import BaseGPTAPI from metagpt.provider.general_api_requestor import GeneralAPIRequestor from metagpt.provider.llm_provider_registry import register_provider @@ -127,7 +127,7 @@ class OllamaGPTAPI(BaseGPTAPI): if not chunk.get("done", False): content = self.get_choice_text(chunk) collected_content.append(content) - print(content, end="") + log_llm_stream(content, end="") else: # stream finished usage = self.get_usage(chunk) diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py index 8fd8959b2..0b6fdd869 100644 --- a/metagpt/provider/openai_api.py +++ b/metagpt/provider/openai_api.py @@ -29,7 +29,7 @@ from tenacity import ( ) from metagpt.config import CONFIG, Config, LLMProviderEnum -from metagpt.logs import logger +from metagpt.logs import log_llm_stream, logger from metagpt.provider.base_gpt_api import BaseGPTAPI from metagpt.provider.constant import GENERAL_FUNCTION_SCHEMA, GENERAL_TOOL_CHOICE from metagpt.provider.llm_provider_registry import register_provider @@ -222,7 +222,7 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter): chunk_message = chunk.choices[0].delta # extract the message collected_messages.append(chunk_message) # save the message if chunk_message.content: - print(chunk_message.content, end="") + log_llm_stream(chunk_message.content) print() full_reply_content = "".join([m.content for m in collected_messages if m.content]) diff --git a/metagpt/provider/zhipuai_api.py b/metagpt/provider/zhipuai_api.py index 60d9a0777..650720d6f 100644 --- a/metagpt/provider/zhipuai_api.py +++ b/metagpt/provider/zhipuai_api.py @@ -17,7 +17,7 @@ from tenacity import ( ) from metagpt.config import CONFIG, LLMProviderEnum -from metagpt.logs import logger +from metagpt.logs import log_llm_stream, logger from metagpt.provider.base_gpt_api import BaseGPTAPI from metagpt.provider.llm_provider_registry import register_provider from metagpt.provider.openai_api import CostManager, log_and_reraise @@ -94,7 +94,7 @@ class ZhiPuAIGPTAPI(BaseGPTAPI): if event.event == ZhiPuEvent.ADD.value: content = event.data collected_content.append(content) - print(content, end="") + log_llm_stream(content) elif event.event == ZhiPuEvent.ERROR.value or event.event == ZhiPuEvent.INTERRUPTED.value: content = event.data logger.error(f"event error: {content}", end="") From ddc0d3faa427ab94b1d10e94e609411731fbbbe3 Mon Sep 17 00:00:00 2001 From: shenchucheng Date: Sun, 24 Dec 2023 03:52:29 +0800 Subject: [PATCH 26/28] not call LLM in global --- metagpt/roles/role.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index c25cd947c..fe61b9878 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -152,7 +152,7 @@ class Role(BaseModel): builtin_class_name: str = "" _private_attributes = { - "_llm": LLM() if not is_human else HumanProvider(), + "_llm": None, "_role_id": _role_id, "_states": [], "_actions": [], From b113aa246f704326b87e1437dc8e2a41ef0d1ec7 Mon Sep 17 00:00:00 2001 From: shenchucheng Date: Mon, 25 Dec 2023 17:22:30 +0800 Subject: [PATCH 27/28] update log_llm_stream in log_llm_stream.py/ollama_api.py --- metagpt/provider/google_gemini_api.py | 2 +- metagpt/provider/ollama_api.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/metagpt/provider/google_gemini_api.py b/metagpt/provider/google_gemini_api.py index 63a2ff687..825b0bfe3 100644 --- a/metagpt/provider/google_gemini_api.py +++ b/metagpt/provider/google_gemini_api.py @@ -119,7 +119,7 @@ class GeminiGPTAPI(BaseGPTAPI): collected_content = [] async for chunk in resp: content = chunk.text - log_llm_stream(content, end="") + log_llm_stream(content) collected_content.append(content) full_content = "".join(collected_content) diff --git a/metagpt/provider/ollama_api.py b/metagpt/provider/ollama_api.py index d668d3af1..e913f3d0d 100644 --- a/metagpt/provider/ollama_api.py +++ b/metagpt/provider/ollama_api.py @@ -127,7 +127,7 @@ class OllamaGPTAPI(BaseGPTAPI): if not chunk.get("done", False): content = self.get_choice_text(chunk) collected_content.append(content) - log_llm_stream(content, end="") + log_llm_stream(content) else: # stream finished usage = self.get_usage(chunk) From 90bbf72ae8c8acb180b053c191fe7e531e448aff Mon Sep 17 00:00:00 2001 From: femto Date: Mon, 25 Dec 2023 17:35:13 +0800 Subject: [PATCH 28/28] fix sk agent --- metagpt/roles/sk_agent.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/metagpt/roles/sk_agent.py b/metagpt/roles/sk_agent.py index 791dff5e2..6063205bd 100644 --- a/metagpt/roles/sk_agent.py +++ b/metagpt/roles/sk_agent.py @@ -7,13 +7,13 @@ @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 filtering. """ +from typing import Any, Type from pydantic import Field from semantic_kernel import Kernel -from semantic_kernel.orchestration.sk_function_base import SKFunctionBase from semantic_kernel.planning import SequentialPlanner from semantic_kernel.planning.action_planner.action_planner import ActionPlanner -from semantic_kernel.planning.basic_planner import BasicPlanner, Plan +from semantic_kernel.planning.basic_planner import BasicPlanner from metagpt.actions import UserRequirement from metagpt.actions.execute_task import ExecuteTask @@ -41,13 +41,13 @@ class SkAgent(Role): goal: str = "Execute task based on passed in task description" constraints: str = "" - plan: Plan = None - planner_cls: BasicPlanner = BasicPlanner - planner: BasicPlanner = Field(default_factory=BasicPlanner) + plan: Any = None + planner_cls: Any = None + planner: Any = None llm: BaseGPTAPI = Field(default_factory=LLM) kernel: Kernel = Field(default_factory=Kernel) - import_semantic_skill_from_directory: str = "" - import_skill: dict[str, SKFunctionBase] = dict() + import_semantic_skill_from_directory: Type[Kernel.import_semantic_skill_from_directory] = None + import_skill: Type[Kernel.import_skill] = None def __init__(self, **kwargs) -> None: """Initializes the Engineer role with given attributes.""" @@ -57,8 +57,8 @@ class SkAgent(Role): self.kernel = make_sk_kernel() # how funny the interface is inconsistent - if self.planner_cls == BasicPlanner: - self.planner = self.planner_cls() + if self.planner_cls == BasicPlanner or self.planner_cls is None: + self.planner = BasicPlanner() elif self.planner_cls in [SequentialPlanner, ActionPlanner]: self.planner = self.planner_cls(self.kernel) else: @@ -78,6 +78,7 @@ class SkAgent(Role): async def _act(self) -> Message: # how funny the interface is inconsistent + result = None if isinstance(self.planner, BasicPlanner): result = await self.planner.execute_plan_async(self.plan, self.kernel) elif any(isinstance(self.planner, cls) for cls in [SequentialPlanner, ActionPlanner]):