diff --git a/docs/install/cli_install.md b/docs/install/cli_install.md new file mode 100644 index 000000000..80deda771 --- /dev/null +++ b/docs/install/cli_install.md @@ -0,0 +1,109 @@ +## Traditional Command Line Installation + +### Support System and version +| System Version | Python Version | Supported | +| ---- | ---- | ----- | +| macOS 13.x | python 3.9 | Yes | +| Windows 11 | python 3.9 | Yes | +| Ubuntu 22.04 | python 3.9 | Yes | + +### Detail Installation +```bash +# Step 1: Ensure that NPM is installed on your system. Then install mermaid-js. (If you don't have npm in your computer, please go to the Node.js official website to install Node.js https://nodejs.org/ and then you will have npm tool in your computer.) +npm --version +sudo npm install -g @mermaid-js/mermaid-cli + +# Step 2: Ensure that Python 3.9+ is installed on your system. You can check this by using: +python3 --version + +# Step 3: Clone the repository to your local machine, and install it. +git clone https://github.com/geekan/MetaGPT.git +cd MetaGPT +pip install -e. +``` + +**Note:** + +- If already have Chrome, Chromium, or MS Edge installed, you can skip downloading Chromium by setting the environment variable + `PUPPETEER_SKIP_CHROMIUM_DOWNLOAD` to `true`. + +- Some people are [having issues](https://github.com/mermaidjs/mermaid.cli/issues/15) installing this tool globally. Installing it locally is an alternative solution, + + ```bash + npm install @mermaid-js/mermaid-cli + ``` + +- don't forget to the configuration for mmdc in config.yml + + ```yml + PUPPETEER_CONFIG: "./config/puppeteer-config.json" + MMDC: "./node_modules/.bin/mmdc" + ``` + +- if `pip install -e.` fails with error `[Errno 13] Permission denied: '/usr/local/lib/python3.11/dist-packages/test-easy-install-13129.write-test'`, try instead running `pip install -e. --user` + +- To convert Mermaid charts to SVG, PNG, and PDF formats. In addition to the Node.js version of Mermaid-CLI, you now have the option to use Python version Playwright, pyppeteer or mermaid.ink for this task. + + - Playwright + - **Install Playwright** + + ```bash + pip install playwright + ``` + + - **Install the Required Browsers** + + to support PDF conversion, please install Chrominum. + + ```bash + playwright install --with-deps chromium + ``` + + - **modify `config.yaml`** + + uncomment MERMAID_ENGINE from config.yaml and change it to `playwright` + + ```yaml + MERMAID_ENGINE: playwright + ``` + + - pyppeteer + - **Install pyppeteer** + + ```bash + pip install pyppeteer + ``` + + - **Use your own Browsers** + + pyppeteer allows you use installed browsers, please set the following envirment + + ```bash + export PUPPETEER_EXECUTABLE_PATH = /path/to/your/chromium or edge or chrome + ``` + + please do not use this command to install browser, it is too old + + ```bash + pyppeteer-install + ``` + + - **modify `config.yaml`** + + uncomment MERMAID_ENGINE from config.yaml and change it to `pyppeteer` + + ```yaml + MERMAID_ENGINE: pyppeteer + ``` + + - mermaid.ink + - **modify `config.yaml`** + + uncomment MERMAID_ENGINE from config.yaml and change it to `ink` + + ```yaml + MERMAID_ENGINE: ink + ``` + + Note: this method does not support pdf export. + \ No newline at end of file diff --git a/docs/install/cli_install_cn.md b/docs/install/cli_install_cn.md new file mode 100644 index 000000000..f351090ed --- /dev/null +++ b/docs/install/cli_install_cn.md @@ -0,0 +1,43 @@ +## 命令行安装 + +### 支持的系统和版本 +| 系统版本 | Python 版本 | 是否支持 | +| ---- | ---- | ----- | +| macOS 13.x | python 3.9 | 是 | +| Windows 11 | python 3.9 | 是 | +| Ubuntu 22.04 | python 3.9 | 是 | + +### 详细安装 + +```bash +# 第 1 步:确保您的系统上安装了 NPM。并使用npm安装mermaid-js +npm --version +sudo npm install -g @mermaid-js/mermaid-cli + +# 第 2 步:确保您的系统上安装了 Python 3.9+。您可以使用以下命令进行检查: +python --version + +# 第 3 步:克隆仓库到您的本地机器,并进行安装。 +git clone https://github.com/geekan/MetaGPT.git +cd MetaGPT +pip install -e. +``` + +**注意:** + +- 如果已经安装了Chrome、Chromium或MS Edge,可以通过将环境变量`PUPPETEER_SKIP_CHROMIUM_DOWNLOAD`设置为`true`来跳过下载Chromium。 + +- 一些人在全局安装此工具时遇到问题。在本地安装是替代解决方案, + + ```bash + npm install @mermaid-js/mermaid-cli + ``` + +- 不要忘记在config.yml中为mmdc配置配置, + + ```yml + PUPPETEER_CONFIG: "./config/puppeteer-config.json" + MMDC: "./node_modules/.bin/mmdc" + ``` + +- 如果`pip install -e.`失败并显示错误`[Errno 13] Permission denied: '/usr/local/lib/python3.11/dist-packages/test-easy-install-13129.write-test'`,请尝试使用`pip install -e. --user`运行。 diff --git a/docs/install/docker_install.md b/docs/install/docker_install.md new file mode 100644 index 000000000..b803a5dae --- /dev/null +++ b/docs/install/docker_install.md @@ -0,0 +1,44 @@ +## Docker Installation + +### Use default MetaGPT image + +```bash +# Step 1: Download metagpt official image and prepare config.yaml +docker pull metagpt/metagpt:latest +mkdir -p /opt/metagpt/{config,workspace} +docker run --rm metagpt/metagpt:latest cat /app/metagpt/config/config.yaml > /opt/metagpt/config/key.yaml +vim /opt/metagpt/config/key.yaml # Change the config + +# Step 2: Run metagpt demo with container +docker run --rm \ + --privileged \ + -v /opt/metagpt/config/key.yaml:/app/metagpt/config/key.yaml \ + -v /opt/metagpt/workspace:/app/metagpt/workspace \ + metagpt/metagpt:latest \ + python3 startup.py "Write a cli snake game" + +# You can also start a container and execute commands in it +docker run --name metagpt -d \ + --privileged \ + -v /opt/metagpt/config/key.yaml:/app/metagpt/config/key.yaml \ + -v /opt/metagpt/workspace:/app/metagpt/workspace \ + metagpt/metagpt:latest + +docker exec -it metagpt /bin/bash +$ python3 startup.py "Write a cli snake game" +``` + +The command `docker run ...` do the following things: + +- Run in privileged mode to have permission to run the browser +- Map host configure file `/opt/metagpt/config/key.yaml` to container `/app/metagpt/config/key.yaml` +- Map host directory `/opt/metagpt/workspace` to container `/app/metagpt/workspace` +- Execute the demo command `python3 startup.py "Write a cli snake game"` + +### Build image by yourself + +```bash +# You can also build metagpt image by yourself. +git clone https://github.com/geekan/MetaGPT.git +cd MetaGPT && docker build -t metagpt:custom . +``` diff --git a/docs/install/docker_install_cn.md b/docs/install/docker_install_cn.md new file mode 100644 index 000000000..347fae10c --- /dev/null +++ b/docs/install/docker_install_cn.md @@ -0,0 +1,44 @@ +## Docker安装 + +### 使用MetaGPT镜像 + +```bash +# 步骤1: 下载metagpt官方镜像并准备好config.yaml +docker pull metagpt/metagpt:latest +mkdir -p /opt/metagpt/{config,workspace} +docker run --rm metagpt/metagpt:latest cat /app/metagpt/config/config.yaml > /opt/metagpt/config/key.yaml +vim /opt/metagpt/config/key.yaml # 修改配置文件 + +# 步骤2: 使用容器运行metagpt演示 +docker run --rm \ + --privileged \ + -v /opt/metagpt/config/key.yaml:/app/metagpt/config/key.yaml \ + -v /opt/metagpt/workspace:/app/metagpt/workspace \ + metagpt/metagpt:latest \ + python startup.py "Write a cli snake game" + +# 您也可以启动一个容器并在其中执行命令 +docker run --name metagpt -d \ + --privileged \ + -v /opt/metagpt/config/key.yaml:/app/metagpt/config/key.yaml \ + -v /opt/metagpt/workspace:/app/metagpt/workspace \ + metagpt/metagpt:latest + +docker exec -it metagpt /bin/bash +$ python startup.py "Write a cli snake game" +``` + +`docker run ...`做了以下事情: + +- 以特权模式运行,有权限运行浏览器 +- 将主机文件 `/opt/metagpt/config/key.yaml` 映射到容器文件 `/app/metagpt/config/key.yaml` +- 将主机目录 `/opt/metagpt/workspace` 映射到容器目录 `/app/metagpt/workspace` +- 执行示例命令 `python startup.py "Write a cli snake game"` + +### 自己构建镜像 + +```bash +# 您也可以自己构建metagpt镜像 +git clone https://github.com/geekan/MetaGPT.git +cd MetaGPT && docker build -t metagpt:custom . +``` diff --git a/docs/tutorial/usage.md b/docs/tutorial/usage.md new file mode 100644 index 000000000..ee87b65c9 --- /dev/null +++ b/docs/tutorial/usage.md @@ -0,0 +1,67 @@ +## MetaGPT Usage + +### Configuration + +- Configure your `OPENAI_API_KEY` in any of `config/key.yaml / config/config.yaml / env` +- Priority order: `config/key.yaml > config/config.yaml > env` + +```bash +# Copy the configuration file and make the necessary modifications. +cp config/config.yaml config/key.yaml +``` + +| Variable Name | config/key.yaml | env | +| ------------------------------------------ | ----------------------------------------- | ----------------------------------------------- | +| OPENAI_API_KEY # Replace with your own key | OPENAI_API_KEY: "sk-..." | export OPENAI_API_KEY="sk-..." | +| OPENAI_API_BASE # Optional | OPENAI_API_BASE: "https:///v1" | export OPENAI_API_BASE="https:///v1" | + +### Initiating a startup + +```shell +# Run the script +python startup.py "Write a cli snake game" +# Do not hire an engineer to implement the project +python startup.py "Write a cli snake game" --implement False +# Hire an engineer and perform code reviews +python startup.py "Write a cli snake game" --code_review True +``` + +After running the script, you can find your new project in the `workspace/` directory. + +### Preference of Platform or Tool + +You can tell which platform or tool you want to use when stating your requirements. + +```shell +python startup.py "Write a cli snake game based on pygame" +``` + +### Usage + +``` +NAME + startup.py - We are a software startup comprised of AI. By investing in us, you are empowering a future filled with limitless possibilities. + +SYNOPSIS + startup.py IDEA + +DESCRIPTION + We are a software startup comprised of AI. By investing in us, you are empowering a future filled with limitless possibilities. + +POSITIONAL ARGUMENTS + IDEA + Type: str + Your innovative idea, such as "Creating a snake game." + +FLAGS + --investment=INVESTMENT + Type: float + Default: 3.0 + As an investor, you have the opportunity to contribute a certain dollar amount to this AI company. + --n_round=N_ROUND + Type: int + Default: 5 + +NOTES + You can also use flags syntax for POSITIONAL ARGUMENTS +``` \ No newline at end of file diff --git a/docs/tutorial/usage_cn.md b/docs/tutorial/usage_cn.md new file mode 100644 index 000000000..4b3bdd2c3 --- /dev/null +++ b/docs/tutorial/usage_cn.md @@ -0,0 +1,63 @@ +## MetaGPT 使用 + +### 配置 + +- 在 `config/key.yaml / config/config.yaml / env` 中配置您的 `OPENAI_API_KEY` +- 优先级顺序:`config/key.yaml > config/config.yaml > env` + +```bash +# 复制配置文件并进行必要的修改 +cp config/config.yaml config/key.yaml +``` + +| 变量名 | config/key.yaml | env | +| ----------------------------------- | ----------------------------------------- | ----------------------------------------------- | +| OPENAI_API_KEY # 用您自己的密钥替换 | OPENAI_API_KEY: "sk-..." | export OPENAI_API_KEY="sk-..." | +| OPENAI_API_BASE # 可选 | OPENAI_API_BASE: "https:///v1" | export OPENAI_API_BASE="https:///v1" | + +### 示例:启动一个创业公司 + +```shell +python startup.py "写一个命令行贪吃蛇" +# 开启code review模式会花费更多的金钱, 但是会提升代码质量和成功率 +python startup.py "写一个命令行贪吃蛇" --code_review True +``` + +运行脚本后,您可以在 `workspace/` 目录中找到您的新项目。 + +### 平台或工具的倾向性 +可以在阐述需求时说明想要使用的平台或工具。 +例如: +```shell +python startup.py "写一个基于pygame的命令行贪吃蛇" +``` + +### 使用 + +``` +名称 + startup.py - 我们是一家AI软件创业公司。通过投资我们,您将赋能一个充满无限可能的未来。 + +概要 + startup.py IDEA + +描述 + 我们是一家AI软件创业公司。通过投资我们,您将赋能一个充满无限可能的未来。 + +位置参数 + IDEA + 类型: str + 您的创新想法,例如"写一个命令行贪吃蛇。" + +标志 + --investment=INVESTMENT + 类型: float + 默认值: 3.0 + 作为投资者,您有机会向这家AI公司投入一定的美元金额。 + --n_round=N_ROUND + 类型: int + 默认值: 5 + +备注 + 您也可以用`标志`的语法,来处理`位置参数` +``` diff --git a/examples/build_customized_multi_agents.py b/examples/build_customized_multi_agents.py new file mode 100644 index 000000000..0df927e32 --- /dev/null +++ b/examples/build_customized_multi_agents.py @@ -0,0 +1,158 @@ +''' +Filename: MetaGPT/examples/build_customized_multi_agents.py +Created Date: Wednesday, November 15th 2023, 7:12:39 pm +Author: garylin2099 +''' +import re +import asyncio +import fire + +from metagpt.llm import LLM +from metagpt.actions import Action, BossRequirement +from metagpt.roles import Role +from metagpt.team import Team +from metagpt.schema import Message +from metagpt.logs import logger + +def parse_code(rsp): + pattern = r'```python(.*)```' + match = re.search(pattern, rsp, re.DOTALL) + code_text = match.group(1) if match else rsp + return code_text + +class SimpleWriteCode(Action): + + PROMPT_TEMPLATE = """ + Write a python function that can {instruction}. + 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) + + async def run(self, instruction: str): + + prompt = self.PROMPT_TEMPLATE.format(instruction=instruction) + + rsp = await self._aask(prompt) + + code_text = parse_code(rsp) + + return code_text + + +class SimpleCoder(Role): + def __init__( + self, + name: str = "Alice", + profile: str = "SimpleCoder", + **kwargs, + ): + super().__init__(name, profile, **kwargs) + self._watch([BossRequirement]) + self._init_actions([SimpleWriteCode]) + + +class SimpleWriteTest(Action): + + PROMPT_TEMPLATE = """ + Context: {context} + Write {k} unit tests using pytest for the given function, assuming you have imported it. + Return ```python your_code_here ``` with NO other texts, + your code: + """ + + def __init__(self, name: str = "SimpleWriteTest", context=None, llm: LLM = None): + super().__init__(name, context, llm) + + async def run(self, context: str, k: int = 3): + + prompt = self.PROMPT_TEMPLATE.format(context=context, k=k) + + rsp = await self._aask(prompt) + + code_text = parse_code(rsp) + + return code_text + + +class SimpleTester(Role): + def __init__( + self, + name: str = "Bob", + profile: str = "SimpleTester", + **kwargs, + ): + super().__init__(name, profile, **kwargs) + self._init_actions([SimpleWriteTest]) + # self._watch([SimpleWriteCode]) + 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}") + todo = self._rc.todo + + # context = self.get_memories(k=1)[0].content # use the most recent memory as context + context = self.get_memories() # use all memories as context + + code_text = await todo.run(context, k=5) # specify arguments + msg = Message(content=code_text, role=self.profile, cause_by=type(todo)) + + return msg + + +class SimpleWriteReview(Action): + + PROMPT_TEMPLATE = """ + Context: {context} + 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) + + async def run(self, context: str): + + prompt = self.PROMPT_TEMPLATE.format(context=context) + + rsp = await self._aask(prompt) + + return rsp + + +class SimpleReviewer(Role): + def __init__( + self, + name: str = "Charlie", + profile: str = "SimpleReviewer", + **kwargs, + ): + super().__init__(name, profile, **kwargs) + self._init_actions([SimpleWriteReview]) + self._watch([SimpleWriteTest]) + + +async def main( + idea: str = "write a function that calculates the product of a list", + investment: float = 3.0, + n_round: int = 5, + add_human: bool = False, +): + logger.info(idea) + + team = Team() + team.hire( + [ + SimpleCoder(), + SimpleTester(), + SimpleReviewer(is_human=add_human), + ] + ) + + team.invest(investment=investment) + team.start_project(idea) + await team.run(n_round=n_round) + +if __name__ == '__main__': + fire.Fire(main) diff --git a/metagpt/provider/constant.py b/metagpt/provider/constant.py new file mode 100644 index 000000000..db67847a8 --- /dev/null +++ b/metagpt/provider/constant.py @@ -0,0 +1,30 @@ +# function in tools, https://platform.openai.com/docs/api-reference/chat/create#chat-create-tools +# Reference: https://github.com/KillianLucas/open-interpreter/blob/v0.1.14/interpreter/llm/setup_openai_coding_llm.py +GENERAL_FUNCTION_SCHEMA = { + "name": "execute", + "description": "Executes code on the user's machine, **in the users local environment**, and returns the output", + "parameters": { + "type": "object", + "properties": { + "language": { + "type": "string", + "description": "The programming language (required parameter to the `execute` function)", + "enum": [ + "python", + "R", + "shell", + "applescript", + "javascript", + "html", + "powershell", + ], + }, + "code": {"type": "string", "description": "The code to execute (required)"}, + }, + "required": ["language", "code"], + }, +} + +# tool_choice value for general_function_schema +# https://platform.openai.com/docs/api-reference/chat/create#chat-create-tool_choice +GENERAL_TOOL_CHOICE = {"type": "function", "function": {"name": "execute"}} diff --git a/metagpt/provider/general_api_requestor.py b/metagpt/provider/general_api_requestor.py new file mode 100644 index 000000000..150f2f1e0 --- /dev/null +++ b/metagpt/provider/general_api_requestor.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# @Desc : General Async API for http-based LLM model + +from typing import AsyncGenerator, Tuple, Union, Optional, Literal +import aiohttp +import asyncio + +from openai.api_requestor import APIRequestor + +from metagpt.logs import logger + + +class GeneralAPIRequestor(APIRequestor): + """ + usage + # full_url = "{api_base}{url}" + requester = GeneralAPIRequestor(api_base=api_base) + result, _, api_key = await requester.arequest( + method=method, + url=url, + headers=headers, + stream=stream, + params=kwargs, + request_timeout=120 + ) + """ + + def _interpret_response_line( + self, rbody: str, rcode: int, rheaders, stream: bool + ) -> str: + # 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. + + return rbody + + 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", ""): + return ( + self._interpret_response_line( + line, result.status, result.headers, stream=True + ) + async for line in result.content + ), True + else: + try: + await result.read() + except (aiohttp.ServerTimeoutError, asyncio.TimeoutError) as e: + raise TimeoutError("Request timed out") from e + except aiohttp.ClientError as exp: + logger.warning(f"response: {result.content}, exp: {exp}") + return ( + self._interpret_response_line( + await result.read(), # let the caller to decode the msg + result.status, + result.headers, + stream=False, + ), + False, + ) diff --git a/metagpt/provider/human_provider.py b/metagpt/provider/human_provider.py new file mode 100644 index 000000000..1d12f972f --- /dev/null +++ b/metagpt/provider/human_provider.py @@ -0,0 +1,35 @@ +''' +Filename: MetaGPT/metagpt/provider/human_provider.py +Created Date: Wednesday, November 8th 2023, 11:55:46 pm +Author: garylin2099 +''' +from typing import Optional +from metagpt.provider.base_gpt_api import BaseGPTAPI +from metagpt.logs import logger + +class HumanProvider(BaseGPTAPI): + """Humans provide themselves as a 'model', which actually takes in human input as its response. + This enables replacing LLM anywhere in the framework with a human, thus introducing human interaction + """ + + def ask(self, msg: str) -> str: + logger.info("It's your turn, please type in your response. You may also refer to the context below") + rsp = input(msg) + if rsp in ["exit", "quit"]: + exit() + return rsp + + async def aask(self, msg: str, system_msgs: Optional[list[str]] = None) -> str: + return self.ask(msg) + + def completion(self, messages: list[dict]): + """dummy implementation of abstract method in base""" + return [] + + async def acompletion(self, messages: list[dict]): + """dummy implementation of abstract method in base""" + return [] + + async def acompletion_text(self, messages: list[dict], stream=False) -> str: + """dummy implementation of abstract method in base""" + return [] diff --git a/metagpt/provider/zhipuai/__init__.py b/metagpt/provider/zhipuai/__init__.py new file mode 100644 index 000000000..2bcf8efd0 --- /dev/null +++ b/metagpt/provider/zhipuai/__init__.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# @Desc : diff --git a/metagpt/provider/zhipuai/async_sse_client.py b/metagpt/provider/zhipuai/async_sse_client.py new file mode 100644 index 000000000..b819fdc63 --- /dev/null +++ b/metagpt/provider/zhipuai/async_sse_client.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# @Desc : async_sse_client to make keep the use of Event to access response +# refs to `https://github.com/zhipuai/zhipuai-sdk-python/blob/main/zhipuai/utils/sse_client.py` + +from zhipuai.utils.sse_client import SSEClient, Event, _FIELD_SEPARATOR + + +class AsyncSSEClient(SSEClient): + + async def _aread(self): + data = b"" + async for chunk in self._event_source: + for line in chunk.splitlines(True): + data += line + if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")): + yield data + data = b"" + if data: + yield data + + async def async_events(self): + async for chunk in self._aread(): + event = Event() + # Split before decoding so splitlines() only uses \r and \n + for line in chunk.splitlines(): + # Decode the line. + line = line.decode(self._char_enc) + + # Lines starting with a separator are comments and are to be + # ignored. + if not line.strip() or line.startswith(_FIELD_SEPARATOR): + continue + + data = line.split(_FIELD_SEPARATOR, 1) + field = data[0] + + # Ignore unknown fields. + if field not in event.__dict__: + self._logger.debug( + "Saw invalid field %s while parsing " "Server Side Event", field + ) + continue + + if len(data) > 1: + # From the spec: + # "If value starts with a single U+0020 SPACE character, + # remove it from value." + if data[1].startswith(" "): + value = data[1][1:] + else: + value = data[1] + else: + # If no value is present after the separator, + # assume an empty value. + value = "" + + # The data field may come over multiple lines and their values + # are concatenated with each other. + if field == "data": + event.__dict__[field] += value + "\n" + else: + event.__dict__[field] = value + + # Events with no data are not dispatched. + if not event.data: + continue + + # If the data field ends with a newline, remove it. + if event.data.endswith("\n"): + event.data = event.data[0:-1] + + # Empty event names default to 'message' + event.event = event.event or "message" + + # Dispatch the event + self._logger.debug("Dispatching %s...", event) + yield event diff --git a/metagpt/provider/zhipuai/zhipu_model_api.py b/metagpt/provider/zhipuai/zhipu_model_api.py new file mode 100644 index 000000000..618b2e865 --- /dev/null +++ b/metagpt/provider/zhipuai/zhipu_model_api.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# @Desc : zhipu model api to support sync & async for invoke & sse_invoke + +import zhipuai +from zhipuai.model_api.api import ModelAPI, InvokeType +from zhipuai.utils.http_client import headers as zhipuai_default_headers + +from metagpt.provider.zhipuai.async_sse_client import AsyncSSEClient +from metagpt.provider.general_api_requestor import GeneralAPIRequestor + + +class ZhiPuModelAPI(ModelAPI): + + @classmethod + def get_header(cls) -> dict: + token = cls._generate_token() + zhipuai_default_headers.update({"Authorization": token}) + return zhipuai_default_headers + + @classmethod + def get_sse_header(cls) -> dict: + token = cls._generate_token() + headers = { + "Authorization": token + } + return headers + + @classmethod + def split_zhipu_api_url(cls, invoke_type: InvokeType, kwargs): + # use this method to prevent zhipu api upgrading to different version. + # and follow the GeneralAPIRequestor implemented based on openai sdk + zhipu_api_url = cls._build_api_url(kwargs, invoke_type) + """ + example: + zhipu_api_url: https://open.bigmodel.cn/api/paas/v3/model-api/{model}/{invoke_method} + """ + arr = zhipu_api_url.split("/api/") + # ("https://open.bigmodel.cn/api/" , "/paas/v3/model-api/chatglm_turbo/invoke") + return f"{arr[0]}/api", f"/{arr[1]}" + + @classmethod + async def arequest(cls, invoke_type: InvokeType, stream: bool, method: str, headers: dict, kwargs): + # TODO to make the async request to be more generic for models in http mode. + assert method in ["post", "get"] + + api_base, url = cls.split_zhipu_api_url(invoke_type, kwargs) + requester = GeneralAPIRequestor(api_base=api_base) + result, _, api_key = await requester.arequest( + method=method, + url=url, + headers=headers, + stream=stream, + params=kwargs, + request_timeout=zhipuai.api_timeout_seconds + ) + + return result + + @classmethod + async def ainvoke(cls, **kwargs) -> dict: + """ async invoke different from raw method `async_invoke` which get the final result by task_id""" + headers = cls.get_header() + resp = await cls.arequest(invoke_type=InvokeType.SYNC, + stream=False, + method="post", + headers=headers, + kwargs=kwargs) + return resp + + @classmethod + async def asse_invoke(cls, **kwargs) -> AsyncSSEClient: + """ async sse_invoke """ + headers = cls.get_sse_header() + return AsyncSSEClient(await cls.arequest(invoke_type=InvokeType.SSE, + stream=True, + method="post", + headers=headers, + kwargs=kwargs)) diff --git a/metagpt/provider/zhipuai_api.py b/metagpt/provider/zhipuai_api.py new file mode 100644 index 000000000..3161c0e88 --- /dev/null +++ b/metagpt/provider/zhipuai_api.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# @Desc : zhipuai LLM from https://open.bigmodel.cn/dev/api#sdk + +from enum import Enum +import json +from tenacity import ( + after_log, + retry, + retry_if_exception_type, + stop_after_attempt, + wait_fixed, +) +from requests import ConnectionError + +import openai +import zhipuai + +from metagpt.config import CONFIG +from metagpt.logs import logger +from metagpt.provider.base_gpt_api import BaseGPTAPI +from metagpt.provider.openai_api import CostManager, log_and_reraise +from metagpt.provider.zhipuai.zhipu_model_api import ZhiPuModelAPI + + +class ZhiPuEvent(Enum): + ADD = "add" + ERROR = "error" + INTERRUPTED = "interrupted" + FINISH = "finish" + + +class ZhiPuAIGPTAPI(BaseGPTAPI): + """ + Refs to `https://open.bigmodel.cn/dev/api#chatglm_turbo` + From now, there is only one model named `chatglm_turbo` + """ + + use_system_prompt: bool = False # zhipuai has no system prompt when use api + + def __init__(self): + self.__init_zhipuai(CONFIG) + self.llm = ZhiPuModelAPI + self.model = "chatglm_turbo" # so far only one model, just use it + self._cost_manager = CostManager() + + def __init_zhipuai(self, config: CONFIG): + assert config.zhipuai_api_key + zhipuai.api_key = config.zhipuai_api_key + openai.api_key = zhipuai.api_key # due to use openai sdk, set the api_key but it will't be used. + + def _const_kwargs(self, messages: list[dict]) -> dict: + kwargs = { + "model": self.model, + "prompt": messages, + "temperature": 0.3 + } + 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("zhipuai updats costs failed!", e) + + def get_choice_text(self, resp: dict) -> str: + """ get the first text of choice from llm response """ + assist_msg = resp.get("data", {}).get("choices", [{"role": "error"}])[-1] + assert assist_msg["role"] == "assistant" + return assist_msg.get("content") + + def completion(self, messages: list[dict]) -> dict: + resp = self.llm.invoke(**self._const_kwargs(messages)) + usage = resp.get("data").get("usage") + self._update_costs(usage) + return resp + + async def _achat_completion(self, messages: list[dict]) -> dict: + resp = await self.llm.ainvoke(**self._const_kwargs(messages)) + usage = resp.get("data").get("usage") + 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: + response = await self.llm.asse_invoke(**self._const_kwargs(messages)) + collected_content = [] + usage = {} + async for event in response.async_events(): + if event.event == ZhiPuEvent.ADD.value: + content = event.data + collected_content.append(content) + print(content, end="") + elif event.event == ZhiPuEvent.ERROR.value or event.event == ZhiPuEvent.INTERRUPTED.value: + content = event.data + logger.error(f"event error: {content}", end="") + collected_content.append([content]) + elif event.event == ZhiPuEvent.FINISH.value: + """ + event.meta + { + "task_status":"SUCCESS", + "usage":{ + "completion_tokens":351, + "prompt_tokens":595, + "total_tokens":946 + }, + "task_id":"xx", + "request_id":"xxx" + } + """ + meta = json.loads(event.meta) + usage = meta.get("usage") + else: + print(f"zhipuapi else event: {event.data}", end="") + + self._update_costs(usage) + full_content = "".join(collected_content) + return full_content + + @retry( + stop=stop_after_attempt(3), + wait=wait_fixed(1), + 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/team.py b/metagpt/team.py new file mode 100644 index 000000000..67d3ecec8 --- /dev/null +++ b/metagpt/team.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@Time : 2023/5/12 00:30 +@Author : alexanderwu +@File : software_company.py +""" +from pydantic import BaseModel, Field + +from metagpt.actions import BossRequirement +from metagpt.config import CONFIG +from metagpt.environment import Environment +from metagpt.logs import logger +from metagpt.roles import Role +from metagpt.schema import Message +from metagpt.utils.common import NoMoneyException + + +class Team(BaseModel): + """ + Team: Possesses one or more roles (agents), SOP (Standard Operating Procedures), and a platform for instant messaging, + dedicated to perform any multi-agent activity, such as collaboratively writing executable code. + """ + environment: Environment = Field(default_factory=Environment) + investment: float = Field(default=10.0) + idea: str = Field(default="") + + class Config: + arbitrary_types_allowed = True + + def hire(self, roles: list[Role]): + """Hire roles to cooperate""" + self.environment.add_roles(roles) + + def invest(self, investment: float): + """Invest company. raise NoMoneyException when exceed max_budget.""" + self.investment = investment + CONFIG.max_budget = investment + logger.info(f'Investment: ${investment}.') + + def _check_balance(self): + if CONFIG.total_cost > CONFIG.max_budget: + raise NoMoneyException(CONFIG.total_cost, f'Insufficient funds: {CONFIG.max_budget}') + + def start_project(self, idea, send_to: str = ""): + """Start a project from publishing boss requirement.""" + self.idea = idea + self.environment.publish_message(Message(role="Human", content=idea, cause_by=BossRequirement, send_to=send_to)) + + def _save(self): + logger.info(self.json()) + + async def run(self, n_round=3): + """Run company until target round or no money""" + while n_round > 0: + # self._save() + n_round -= 1 + logger.debug(f"{n_round=}") + self._check_balance() + await self.environment.run() + return self.environment.history + \ No newline at end of file diff --git a/tests/metagpt/provider/test_openai.py b/tests/metagpt/provider/test_openai.py new file mode 100644 index 000000000..2b0af37b5 --- /dev/null +++ b/tests/metagpt/provider/test_openai.py @@ -0,0 +1,80 @@ +import pytest + +from metagpt.provider.openai_api import OpenAIGPTAPI +from metagpt.schema import UserMessage + + +@pytest.mark.asyncio +async def test_aask_code(): + llm = OpenAIGPTAPI() + msg = [{"role": "user", "content": "Write a python hello world code."}] + rsp = await llm.aask_code(msg) # -> {'language': 'python', 'code': "print('Hello, World!')"} + assert "language" in rsp + assert "code" in rsp + assert len(rsp["code"]) > 0 + + +@pytest.mark.asyncio +async def test_aask_code_str(): + llm = OpenAIGPTAPI() + msg = "Write a python hello world code." + rsp = await llm.aask_code(msg) # -> {'language': 'python', 'code': "print('Hello, World!')"} + assert "language" in rsp + assert "code" in rsp + assert len(rsp["code"]) > 0 + + +@pytest.mark.asyncio +async def test_aask_code_Message(): + llm = OpenAIGPTAPI() + msg = UserMessage("Write a python hello world code.") + rsp = await llm.aask_code(msg) # -> {'language': 'python', 'code': "print('Hello, World!')"} + assert "language" in rsp + assert "code" in rsp + assert len(rsp["code"]) > 0 + + +def test_ask_code(): + llm = OpenAIGPTAPI() + msg = [{"role": "user", "content": "Write a python hello world code."}] + rsp = llm.ask_code(msg) # -> {'language': 'python', 'code': "print('Hello, World!')"} + assert "language" in rsp + assert "code" in rsp + assert len(rsp["code"]) > 0 + + +def test_ask_code_str(): + llm = OpenAIGPTAPI() + msg = "Write a python hello world code." + rsp = llm.ask_code(msg) # -> {'language': 'python', 'code': "print('Hello, World!')"} + assert "language" in rsp + assert "code" in rsp + assert len(rsp["code"]) > 0 + + +def test_ask_code_Message(): + llm = OpenAIGPTAPI() + msg = UserMessage("Write a python hello world code.") + rsp = llm.ask_code(msg) # -> {'language': 'python', 'code': "print('Hello, World!')"} + assert "language" in rsp + assert "code" in rsp + assert len(rsp["code"]) > 0 + + +def test_ask_code_list_Message(): + llm = OpenAIGPTAPI() + msg = [UserMessage("a=[1,2,5,10,-10]"), UserMessage("写出求a中最大值的代码python")] + rsp = llm.ask_code(msg) # -> {'language': 'python', 'code': 'max_value = max(a)\nmax_value'} + assert "language" in rsp + assert "code" in rsp + assert len(rsp["code"]) > 0 + + +def test_ask_code_list_str(): + llm = OpenAIGPTAPI() + msg = ["a=[1,2,5,10,-10]", "写出求a中最大值的代码python"] + rsp = llm.ask_code(msg) # -> {'language': 'python', 'code': 'max_value = max(a)\nmax_value'} + print(rsp) + assert "language" in rsp + assert "code" in rsp + assert len(rsp["code"]) > 0 diff --git a/tests/metagpt/provider/test_zhipuai_api.py b/tests/metagpt/provider/test_zhipuai_api.py new file mode 100644 index 000000000..6a0c70de5 --- /dev/null +++ b/tests/metagpt/provider/test_zhipuai_api.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# @Desc : the unittest of ZhiPuAIGPTAPI + +import pytest + +from metagpt.provider.zhipuai_api import ZhiPuAIGPTAPI + + +default_resp = { + "code": 200, + "data": { + "choices": [ + {"role": "assistant", "content": "I'm chatglm-turbo"} + ] + } +} + +messages = [ + {"role": "user", "content": "who are you"} +] + + +def mock_llm_ask(self, messages: list[dict]) -> dict: + return default_resp + + +def test_zhipuai_completion(mocker): + mocker.patch("metagpt.provider.zhipuai_api.ZhiPuAIGPTAPI.completion", mock_llm_ask) + + resp = ZhiPuAIGPTAPI().completion(messages) + assert resp["code"] == 200 + assert "chatglm-turbo" in resp["data"]["choices"][0]["content"] + + +async def mock_llm_aask(self, messgaes: list[dict], stream: bool = False) -> dict: + return default_resp + + +@pytest.mark.asyncio +async def test_zhipuai_acompletion(mocker): + mocker.patch("metagpt.provider.zhipuai_api.ZhiPuAIGPTAPI.acompletion_text", mock_llm_aask) + + resp = await ZhiPuAIGPTAPI().acompletion_text(messages, stream=False) + + assert resp["code"] == 200 + assert "chatglm-turbo" in resp["data"]["choices"][0]["content"]