From e6c33f199c315518a6e46496dbc177275a97df78 Mon Sep 17 00:00:00 2001
From: AK
Date: Sun, 6 Aug 2023 22:02:15 +0800
Subject: [PATCH 01/76] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index a2f95437d..72b8cc92c 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,7 @@ ## Installation
### Traditional Installation
```bash
-# Step 1: Ensure that NPM is installed on your system. Then install mermaid-js.
+# Step 1: Ensure that NPM is installed on your system.(If you don't have npm in your computer, please go to the offical website to install Node.js https://nodejs.org/ and then you will have npm tool in your computer.) Then install mermaid-js.
npm --version
sudo npm install -g @mermaid-js/mermaid-cli
From e10bbf78b05c1a22b90beb19d8f737088615812d Mon Sep 17 00:00:00 2001
From: AK
Date: Sun, 6 Aug 2023 22:02:41 +0800
Subject: [PATCH 02/76] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 72b8cc92c..143bdf0f9 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,7 @@ ## Installation
### Traditional Installation
```bash
-# Step 1: Ensure that NPM is installed on your system.(If you don't have npm in your computer, please go to the offical website to install Node.js https://nodejs.org/ and then you will have npm tool in your computer.) Then install mermaid-js.
+# Step 1: Ensure that NPM is installed on your system.(If you don't have npm in your computer, please go to the Node.js offical website to install Node.js https://nodejs.org/ and then you will have npm tool in your computer.) Then install mermaid-js.
npm --version
sudo npm install -g @mermaid-js/mermaid-cli
From 102ad7d807492bd8e86fd3fe89cdfcce66ca887a Mon Sep 17 00:00:00 2001
From: AK
Date: Sun, 6 Aug 2023 23:07:54 +0800
Subject: [PATCH 03/76] Update README.md
(If you don't have npm in your computer, please go to the Node.js offical website to install Node.js https://nodejs.org/ and then you will have npm tool in your computer.)
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 143bdf0f9..b6b1f862b 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,7 @@ ## Installation
### Traditional Installation
```bash
-# Step 1: Ensure that NPM is installed on your system.(If you don't have npm in your computer, please go to the Node.js offical website to install Node.js https://nodejs.org/ and then you will have npm tool in your computer.) Then install mermaid-js.
+# 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 offical 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
From 949a5074ce6240fb34b1f2bf00771ab7375fc9c6 Mon Sep 17 00:00:00 2001
From: rainyrfeng
Date: Thu, 31 Aug 2023 11:05:59 +0800
Subject: [PATCH 04/76] fix feature
---
config/config.yaml | 3 ++-
metagpt/config.py | 1 +
metagpt/provider/openai_api.py | 11 +++++++----
3 files changed, 10 insertions(+), 5 deletions(-)
diff --git a/config/config.yaml b/config/config.yaml
index 274cdf469..428f8dae4 100644
--- a/config/config.yaml
+++ b/config/config.yaml
@@ -16,11 +16,12 @@ RPM: 10
#Anthropic_API_KEY: "YOUR_API_KEY"
#### if AZURE, check https://github.com/openai/openai-cookbook/blob/main/examples/azure/chat.ipynb
-
+#### You can use ENGINE or DEPLOYMENT mode
#OPENAI_API_TYPE: "azure"
#OPENAI_API_BASE: "YOUR_AZURE_ENDPOINT"
#OPENAI_API_KEY: "YOUR_AZURE_API_KEY"
#OPENAI_API_VERSION: "YOUR_AZURE_API_VERSION"
+#OPENAI_API_ENGINE: "YOUR_OPENAI_API_ENGINE"
#DEPLOYMENT_ID: "YOUR_DEPLOYMENT_ID"
#### for Search
diff --git a/metagpt/config.py b/metagpt/config.py
index 2c1096877..16c4117cd 100644
--- a/metagpt/config.py
+++ b/metagpt/config.py
@@ -56,6 +56,7 @@ class Config(metaclass=Singleton):
openai.api_base = self.openai_api_base
self.openai_api_type = self._get("OPENAI_API_TYPE")
self.openai_api_version = self._get("OPENAI_API_VERSION")
+ self.openai_api_engine = self._get('OPENAI_API_ENGINE')
self.openai_api_rpm = self._get("RPM", 3)
self.openai_api_model = self._get("OPENAI_API_MODEL", "gpt-4")
self.max_tokens_rsp = self._get("MAX_TOKENS", 2048)
diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py
index 79121c8de..7ef694a98 100644
--- a/metagpt/provider/openai_api.py
+++ b/metagpt/provider/openai_api.py
@@ -156,10 +156,12 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter):
# iterate through the stream of events
async for chunk in response:
collected_chunks.append(chunk) # save the event response
- chunk_message = chunk["choices"][0]["delta"] # extract the message
- collected_messages.append(chunk_message) # save the message
- if "content" in chunk_message:
- print(chunk_message["content"], end="")
+ choices = chunk["choices"]
+ if len(choices) > 0:
+ chunk_message = chunk["choices"][0].get("delta", {}) # extract the message
+ collected_messages.append(chunk_message) # save the message
+ if "content" in chunk_message:
+ print(chunk_message["content"], end="")
print()
full_reply_content = "".join([m.get("content", "") for m in collected_messages])
@@ -170,6 +172,7 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter):
def _cons_kwargs(self, messages: list[dict]) -> dict:
if CONFIG.openai_api_type == "azure":
kwargs = {
+ "engine": CONFIG.openai_api_engine,
"deployment_id": CONFIG.deployment_id,
"messages": messages,
"max_tokens": self.get_max_tokens(messages),
From 689aa935eb87861fb9c2d647e180ddd2ee00618c Mon Sep 17 00:00:00 2001
From: stellaHSR <34952977+stellaHSR@users.noreply.github.com>
Date: Mon, 4 Sep 2023 19:19:37 +0800
Subject: [PATCH 05/76] Update startup.py
set default params
---
startup.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/startup.py b/startup.py
index 611317fd4..e6d5fc4e9 100644
--- a/startup.py
+++ b/startup.py
@@ -44,9 +44,9 @@ def main(
idea: str,
investment: float = 3.0,
n_round: int = 5,
- code_review: bool = False,
+ code_review: bool = True,
run_tests: bool = False,
- implement: bool = False
+ implement: bool = True
):
"""
We are a software startup comprised of AI. By investing in us,
@@ -66,4 +66,4 @@ def main(
if __name__ == '__main__':
fire.Fire(main)
-
\ No newline at end of file
+
From 26fbbfc83c83f9f5c5515bcc3dd1782688de3d90 Mon Sep 17 00:00:00 2001
From: stellahsr
Date: Mon, 4 Sep 2023 19:51:28 +0800
Subject: [PATCH 06/76] rm useless files
---
Message | 0
None | 0
int | 0
3 files changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 Message
delete mode 100644 None
delete mode 100644 int
diff --git a/Message b/Message
deleted file mode 100644
index e69de29bb..000000000
diff --git a/None b/None
deleted file mode 100644
index e69de29bb..000000000
diff --git a/int b/int
deleted file mode 100644
index e69de29bb..000000000
From a386c7e974cc35e9bfd80d9fe680a1c4c313d43b Mon Sep 17 00:00:00 2001
From: "hy.li"
Date: Mon, 4 Sep 2023 23:30:27 +0800
Subject: [PATCH 07/76] playwright version mmdc
---
README.md | 24 +
config/config.yaml | 4 +
metagpt/config.py | 1 +
metagpt/utils/common.py | 2 +-
metagpt/utils/index.html | 2212 +++++++++++++++++++++++++++
metagpt/utils/mermaid.py | 3 +
metagpt/utils/mermaid_playwright.py | 199 +++
7 files changed, 2444 insertions(+), 1 deletion(-)
create mode 100644 metagpt/utils/index.html
create mode 100644 metagpt/utils/mermaid_playwright.py
diff --git a/README.md b/README.md
index 84dafa46b..adc9d8cea 100644
--- a/README.md
+++ b/README.md
@@ -82,6 +82,30 @@ # Step 3: Clone the repository to your local machine, and install it.
- if `python setup.py install` fails with error `[Errno 13] Permission denied: '/usr/local/lib/python3.11/dist-packages/test-easy-install-13129.write-test'`, try instead running `python setup.py install --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 for this task.
+
+- **Install Playwright**
+
+```bash
+pip install playwright
+```
+
+- **Install the Required Browsers**
+
+to support PDF conversion, had better 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
+```
+
### Installation by Docker
```bash
diff --git a/config/config.yaml b/config/config.yaml
index 274cdf469..ec89a9932 100644
--- a/config/config.yaml
+++ b/config/config.yaml
@@ -75,3 +75,7 @@ SD_T2I_API: "/sdapi/v1/txt2img"
### for Research
MODEL_FOR_RESEARCHER_SUMMARY: gpt-3.5-turbo
MODEL_FOR_RESEARCHER_REPORT: gpt-3.5-turbo-16k
+
+### choose the engine for mermaid conversion,
+# default is nodejs, you can change it to playwright
+# MERMAID_ENGINE: nodejs
\ No newline at end of file
diff --git a/metagpt/config.py b/metagpt/config.py
index 76c6563cb..b51c81862 100644
--- a/metagpt/config.py
+++ b/metagpt/config.py
@@ -82,6 +82,7 @@ class Config(metaclass=Singleton):
self.calc_usage = self._get("CALC_USAGE", True)
self.model_for_researcher_summary = self._get("MODEL_FOR_RESEARCHER_SUMMARY")
self.model_for_researcher_report = self._get("MODEL_FOR_RESEARCHER_REPORT")
+ self.mermaid_engine = self._get("MERMAID_ENGINE", 'nodejs')
def _init_with_config_files_and_env(self, configs: dict, yaml_file):
"""Load from config/key.yaml, config/config.yaml, and env in decreasing order of priority"""
diff --git a/metagpt/utils/common.py b/metagpt/utils/common.py
index 7f090cf63..2e214685c 100644
--- a/metagpt/utils/common.py
+++ b/metagpt/utils/common.py
@@ -187,7 +187,7 @@ class CodeParser:
else:
logger.error(f"{pattern} not match following text:")
logger.error(text)
- raise Exception
+ # raise Exception
return code
@classmethod
diff --git a/metagpt/utils/index.html b/metagpt/utils/index.html
new file mode 100644
index 000000000..0ac6d9a74
--- /dev/null
+++ b/metagpt/utils/index.html
@@ -0,0 +1,2212 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/metagpt/utils/mermaid.py b/metagpt/utils/mermaid.py
index 24aabe8ae..f395b43b2 100644
--- a/metagpt/utils/mermaid.py
+++ b/metagpt/utils/mermaid.py
@@ -14,6 +14,7 @@ from metagpt.logs import logger
from metagpt.utils.common import check_cmd_exists
+
def mermaid_to_file(mermaid_code, output_file_without_suffix, width=2048, height=2048) -> int:
"""suffix: png/svg/pdf
@@ -56,6 +57,8 @@ def mermaid_to_file(mermaid_code, output_file_without_suffix, width=2048, height
subprocess.run([CONFIG.mmdc, "-i", str(tmp), "-o", output_file, "-w", str(width), "-H", str(height)])
return 0
+if CONFIG.mermaid_engine.lower() == "playwright":
+ from metagpt.utils.mermaid_playwright import mermaid_to_file
MMC1 = """classDiagram
class Main {
diff --git a/metagpt/utils/mermaid_playwright.py b/metagpt/utils/mermaid_playwright.py
new file mode 100644
index 000000000..aa04e70eb
--- /dev/null
+++ b/metagpt/utils/mermaid_playwright.py
@@ -0,0 +1,199 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+@Time : 2023/9/4 16:12
+@Author : Steven Lee
+@File : mermaid_playwright.py
+"""
+import os
+import asyncio
+from metagpt.config import CONFIG
+from metagpt.const import PROJECT_ROOT
+from metagpt.logs import logger
+
+from urllib.parse import urljoin
+from playwright.async_api import async_playwright
+import nest_asyncio
+
+__dirname = os.path.dirname(os.path.abspath(__file__))
+
+
+def mermaid_to_file(mermaid_code, output_file_without_suffix, width=2048, height=2048, output_formats=['png', 'svg', 'pdf']) -> int:
+ """
+ Converts the given Mermaid code to various output formats and saves them to files.
+
+ Args:
+ mermaid_code (str): The Mermaid code to convert.
+ output_file_without_suffix (str): The output file name without the file extension.
+ width (int, optional): The width of the output image in pixels. Defaults to 2048.
+ height (int, optional): The height of the output image in pixels. Defaults to 2048.
+ output_formats (list[str], optional): The list of output formats to generate. Defaults to ['png', 'svg', 'pdf'].
+
+ Returns:
+ int: Returns 1 if the conversion and saving were successful, -1 otherwise.
+ """
+
+ async def mermaid_to_file0(mermaid_code, output_file_without_suffix, width=2048, height=2048, output_formats=['png', 'svg', 'pdf'])-> int:
+
+ async with async_playwright() as p:
+ browser = await p.chromium.launch()
+ device_scale_factor = 1.0
+
+ context = await browser.new_context(
+ viewport={'width': width, 'height': height},
+ device_scale_factor=device_scale_factor,
+ )
+ page = await context.new_page()
+
+ async def console_message(msg):
+ print(msg.text)
+ page.on('console', console_message)
+
+ try:
+ await page.set_viewport_size({'width': width, 'height': height})
+
+ mermaid_html_path = os.path.abspath(
+ os.path.join(__dirname, 'index.html'))
+ mermaid_html_url = urljoin('file:', mermaid_html_path)
+ await page.goto(mermaid_html_url)
+ await page.wait_for_load_state("networkidle")
+
+ await page.wait_for_selector("div#container", state="attached")
+ mermaid_config = {}
+ background_color = "#ffffff"
+ my_css = ""
+ await page.evaluate(f'document.body.style.background = "{background_color}";')
+
+ metadata = await page.evaluate('''async ([definition, mermaidConfig, myCSS, backgroundColor]) => {
+ const { mermaid, zenuml } = globalThis;
+ await mermaid.registerExternalDiagrams([zenuml]);
+ mermaid.initialize({ startOnLoad: false, ...mermaidConfig });
+ const { svg } = await mermaid.render('my-svg', definition, document.getElementById('container'));
+ document.getElementById('container').innerHTML = svg;
+ const svgElement = document.querySelector('svg');
+ svgElement.style.backgroundColor = backgroundColor;
+
+ if (myCSS) {
+ const style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
+ style.appendChild(document.createTextNode(myCSS));
+ svgElement.appendChild(style);
+ }
+
+ let title = null;
+ let desc = null;
+
+ if (svgElement.firstChild instanceof SVGTitleElement) {
+ title = svgElement.firstChild.textContent;
+ }
+
+ for (const svgNode of svgElement.children) {
+ if (svgNode instanceof SVGDescElement) {
+ desc = svgNode.textContent;
+ break;
+ }
+ }
+
+ return {
+ title,
+ desc
+ };
+ }''', [mermaid_code, mermaid_config, my_css, background_color])
+
+ if 'svg' in output_formats :
+ svg_xml = await page.evaluate('''() => {
+ const svg = document.querySelector('svg');
+ const xmlSerializer = new XMLSerializer();
+ return xmlSerializer.serializeToString(svg);
+ }''')
+ # result[f'{output_file_without_suffix}.svg'] = svg_xml
+ with open(f'{output_file_without_suffix}.svg', 'wb') as f:
+ f.write(svg_xml.encode('utf-8'))
+
+ if 'png' in output_formats:
+ clip = await page.evaluate('''() => {
+ const svg = document.querySelector('svg');
+ const rect = svg.getBoundingClientRect();
+ return {
+ x: Math.floor(rect.left),
+ y: Math.floor(rect.top),
+ width: Math.ceil(rect.width),
+ height: Math.ceil(rect.height)
+ };
+ }''')
+ await page.set_viewport_size({'width': clip['x'] + clip['width'], 'height': clip['y'] + clip['height']})
+ screenshot = await page.screenshot(clip=clip, omit_background=True, scale='device')
+ with open(f'{output_file_without_suffix}.png', 'wb') as f:
+ f.write(screenshot)
+ if 'pdf' in output_formats:
+ pdf_data = await page.pdf(scale=device_scale_factor)
+ with open(f'{output_file_without_suffix}.pdf', 'wb') as f:
+ f.write(pdf_data)
+ return 1
+ except Exception as e:
+ logger.error(e)
+ return -1
+ finally:
+ await browser.close()
+ with open(f"{output_file_without_suffix}.mmd", "w", encoding="utf-8") as f:
+ f.write(mermaid_code)
+ nest_asyncio.apply()
+ loop = asyncio.new_event_loop()
+ result = loop.run_until_complete(mermaid_to_file0(mermaid_code, output_file_without_suffix, width, height, output_formats))
+ loop.close()
+ return result
+
+MMC1 = """classDiagram
+ class Main {
+ -SearchEngine search_engine
+ +main() str
+ }
+ class SearchEngine {
+ -Index index
+ -Ranking ranking
+ -Summary summary
+ +search(query: str) str
+ }
+ class Index {
+ -KnowledgeBase knowledge_base
+ +create_index(data: dict)
+ +query_index(query: str) list
+ }
+ class Ranking {
+ +rank_results(results: list) list
+ }
+ class Summary {
+ +summarize_results(results: list) str
+ }
+ class KnowledgeBase {
+ +update(data: dict)
+ +fetch_data(query: str) dict
+ }
+ Main --> SearchEngine
+ SearchEngine --> Index
+ SearchEngine --> Ranking
+ SearchEngine --> Summary
+ Index --> KnowledgeBase"""
+
+MMC2 = """sequenceDiagram
+ participant M as Main
+ participant SE as SearchEngine
+ participant I as Index
+ participant R as Ranking
+ participant S as Summary
+ participant KB as KnowledgeBase
+ M->>SE: search(query)
+ SE->>I: query_index(query)
+ I->>KB: fetch_data(query)
+ KB-->>I: return data
+ I-->>SE: return results
+ SE->>R: rank_results(results)
+ R-->>SE: return ranked_results
+ SE->>S: summarize_results(ranked_results)
+ S-->>SE: return summary
+ SE-->>M: return summary"""
+
+
+if __name__ == "__main__":
+ # logger.info(print_members(print_members))
+ mermaid_to_file(MMC1, PROJECT_ROOT / "MMC1")
+ mermaid_to_file(MMC2, PROJECT_ROOT / "MMC2")
From a0be280879024fb7e422ca7dafa95491211721df Mon Sep 17 00:00:00 2001
From: rainyrfeng
Date: Tue, 5 Sep 2023 11:26:16 +0800
Subject: [PATCH 08/76] fix feature
---
metagpt/provider/openai_api.py | 32 +++++++++++++++-----------------
1 file changed, 15 insertions(+), 17 deletions(-)
diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py
index 752fbb4dd..11e35b114 100644
--- a/metagpt/provider/openai_api.py
+++ b/metagpt/provider/openai_api.py
@@ -176,25 +176,23 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter):
return full_reply_content
def _cons_kwargs(self, messages: list[dict]) -> dict:
+ kwargs = {
+ "messages": messages,
+ "max_tokens": self.get_max_tokens(messages),
+ "n": 1,
+ "stop": None,
+ "temperature": 0.3,
+ }
if CONFIG.openai_api_type == "azure":
- kwargs = {
- "engine": CONFIG.openai_api_engine,
- "deployment_id": CONFIG.deployment_id,
- "messages": messages,
- "max_tokens": self.get_max_tokens(messages),
- "n": 1,
- "stop": None,
- "temperature": 0.3,
- }
+ if CONFIG.openai_api_engine and CONFIG.deployment_id:
+ raise ValueError("You can only use one of the `deployment_id` or `engine` model")
+ elif not CONFIG.openai_api_engine and not CONFIG.deployment_id:
+ raise ValueError("You must specify `OPENAI_API_ENGINE` or `DEPLOYMENT_ID` parameter")
+ kwargs_mode = {"engine": CONFIG.openai_api_engine} if CONFIG.openai_api_engine \
+ else {"deployment_id": CONFIG.deployment_id}
else:
- kwargs = {
- "model": self.model,
- "messages": messages,
- "max_tokens": self.get_max_tokens(messages),
- "n": 1,
- "stop": None,
- "temperature": 0.3,
- }
+ kwargs_mode = {"model": self.model}
+ kwargs.update(kwargs_mode)
kwargs["timeout"] = 3
return kwargs
From 864a66c7024d89994fcfe677703173f3db8a567d Mon Sep 17 00:00:00 2001
From: rainyrfeng
Date: Tue, 5 Sep 2023 20:36:32 +0800
Subject: [PATCH 09/76] fix feature
---
config/config.yaml | 2 +-
metagpt/config.py | 2 +-
metagpt/provider/openai_api.py | 12 ++++++------
3 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/config/config.yaml b/config/config.yaml
index 428f8dae4..4519288d3 100644
--- a/config/config.yaml
+++ b/config/config.yaml
@@ -21,7 +21,7 @@ RPM: 10
#OPENAI_API_BASE: "YOUR_AZURE_ENDPOINT"
#OPENAI_API_KEY: "YOUR_AZURE_API_KEY"
#OPENAI_API_VERSION: "YOUR_AZURE_API_VERSION"
-#OPENAI_API_ENGINE: "YOUR_OPENAI_API_ENGINE"
+#DEPLOYMENT_NAME: "YOUR_DEPLOYMENT_NAME"
#DEPLOYMENT_ID: "YOUR_DEPLOYMENT_ID"
#### for Search
diff --git a/metagpt/config.py b/metagpt/config.py
index 7ac32f4d0..96f402b38 100644
--- a/metagpt/config.py
+++ b/metagpt/config.py
@@ -56,10 +56,10 @@ class Config(metaclass=Singleton):
openai.api_base = self.openai_api_base
self.openai_api_type = self._get("OPENAI_API_TYPE")
self.openai_api_version = self._get("OPENAI_API_VERSION")
- self.openai_api_engine = self._get('OPENAI_API_ENGINE')
self.openai_api_rpm = self._get("RPM", 3)
self.openai_api_model = self._get("OPENAI_API_MODEL", "gpt-4")
self.max_tokens_rsp = self._get("MAX_TOKENS", 2048)
+ self.deployment_name = self._get('DEPLOYMENT_NAME')
self.deployment_id = self._get("DEPLOYMENT_ID")
self.claude_api_key = self._get("Anthropic_API_KEY")
diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py
index 11e35b114..ad9df0396 100644
--- a/metagpt/provider/openai_api.py
+++ b/metagpt/provider/openai_api.py
@@ -182,18 +182,18 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter):
"n": 1,
"stop": None,
"temperature": 0.3,
+ "timeout": 3
}
if CONFIG.openai_api_type == "azure":
- if CONFIG.openai_api_engine and CONFIG.deployment_id:
- raise ValueError("You can only use one of the `deployment_id` or `engine` model")
- elif not CONFIG.openai_api_engine and not CONFIG.deployment_id:
- raise ValueError("You must specify `OPENAI_API_ENGINE` or `DEPLOYMENT_ID` parameter")
- kwargs_mode = {"engine": CONFIG.openai_api_engine} if CONFIG.openai_api_engine \
+ if CONFIG.deployment_name and CONFIG.deployment_id:
+ raise ValueError("You can only use one of the `deployment_id` or `deployment_name` model")
+ elif not CONFIG.deployment_name and not CONFIG.deployment_id:
+ raise ValueError("You must specify `DEPLOYMENT_NAME` or `DEPLOYMENT_ID` parameter")
+ kwargs_mode = {"engine": CONFIG.deployment_name} if CONFIG.deployment_name \
else {"deployment_id": CONFIG.deployment_id}
else:
kwargs_mode = {"model": self.model}
kwargs.update(kwargs_mode)
- kwargs["timeout"] = 3
return kwargs
async def _achat_completion(self, messages: list[dict]) -> dict:
From 414dea3ea695c174c64d9c0778f31ca840ba94fa Mon Sep 17 00:00:00 2001
From: Stitch-z <284618289@qq.com>
Date: Wed, 6 Sep 2023 14:43:24 +0800
Subject: [PATCH 10/76] feature: add a new role tutorial assistant
---
examples/write_tutorial.py | 20 +++++
metagpt/actions/write_tutorial.py | 102 +++++++++++++++++++++++
metagpt/const.py | 1 +
metagpt/prompts/tutorial_assistant.py | 39 +++++++++
metagpt/roles/tutorial_assistant.py | 113 ++++++++++++++++++++++++++
5 files changed, 275 insertions(+)
create mode 100644 examples/write_tutorial.py
create mode 100644 metagpt/actions/write_tutorial.py
create mode 100644 metagpt/prompts/tutorial_assistant.py
create mode 100644 metagpt/roles/tutorial_assistant.py
diff --git a/examples/write_tutorial.py b/examples/write_tutorial.py
new file mode 100644
index 000000000..167f3eb7c
--- /dev/null
+++ b/examples/write_tutorial.py
@@ -0,0 +1,20 @@
+#!/usr/bin/env python3
+# _*_ coding: utf-8 _*_
+"""
+@Time : 2023/9/4 21:40:57
+@Author : Stitch-z
+@File : tutorial_assistant.py
+"""
+import asyncio
+
+from metagpt.roles.tutorial_assistant import TutorialAssistant
+
+
+async def main():
+ topic = "Write a tutorial about MySQL"
+ role = TutorialAssistant(language="Chinese")
+ await role.run(topic)
+
+
+if __name__ == '__main__':
+ asyncio.run(main())
\ No newline at end of file
diff --git a/metagpt/actions/write_tutorial.py b/metagpt/actions/write_tutorial.py
new file mode 100644
index 000000000..38a45c4c3
--- /dev/null
+++ b/metagpt/actions/write_tutorial.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python3
+# _*_ coding: utf-8 _*_
+"""
+@Time : 2023/9/4 15:40:40
+@Author : Stitch-z
+@File : tutorial_assistant.py
+@Describe : Actions of the tutorial assistant, including writing directories and document content.
+"""
+import json
+from datetime import datetime
+from typing import Dict
+
+import aiofiles
+
+from metagpt.actions import Action
+from metagpt.const import TUTORIAL_PATH
+from metagpt.logs import logger
+from metagpt.prompts.tutorial_assistant import DIRECTORY_PROMPT, CONTENT_PROMPT
+
+
+class WriteDirectory(Action):
+ """Action class for writing tutorial directories.
+
+ Args:
+ name: The name of the 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
+
+ async def run(self, topic: str, *args, **kwargs) -> Dict:
+ """Execute the action to generate a tutorial directory according to the topic.
+
+ Args:
+ topic: The tutorial topic.
+
+ Returns:
+ the tutorial directory information, such as {"title": "xxx", "directory": [{"dir 1": ["sub dir 1", "sub dir 2"]}]}
+ """
+ prompt = DIRECTORY_PROMPT.format(topic=topic, language=self.language)
+ directory = await self._aask(prompt=prompt)
+ return json.loads(directory)
+
+
+class WriteContent(Action):
+ """Action class for writing tutorial content.
+
+ Args:
+ name: The name of the action.
+ directory: The content to write.
+ 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
+
+ async def run(self, topic: str, *args, **kwargs) -> str:
+ """Execute the action to write document content according to the directory and topic.
+
+ Args:
+ topic: The tutorial topic.
+
+ Returns:
+ The written tutorial content.
+ """
+ prompt = CONTENT_PROMPT.format(topic=topic, language=self.language, directory=self.directory)
+ return await self._aask(prompt=prompt)
+
+
+class SaveDocx(Action):
+ """Action class for saving tutorial docx.
+
+ Args:
+ name: The name of the action.
+ """
+
+ def __init__(self, name: str = "", *args, **kwargs):
+ super().__init__(name, *args, **kwargs)
+
+ async def run(self, title: str, content: str, *args, **kwargs) -> str:
+ """Execute the action to save the generated tutorial document to a Markdown file.
+
+ Args:
+ title: The title of tutorial.
+ content: The total content of tutorial.
+
+ Returns:
+ The full filename of tutorial content.
+
+ """
+ current_time = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
+ pathname = TUTORIAL_PATH / current_time
+ pathname.mkdir(parents=True, exist_ok=True)
+ filename = f"{pathname}/{title}.md"
+ async with aiofiles.open(filename, mode="w", encoding="utf-8") as writer:
+ await writer.write(content)
+ logger.info(f"Successfully write docx: {filename}")
+ return filename
\ No newline at end of file
diff --git a/metagpt/const.py b/metagpt/const.py
index 16f652186..35b4c9fa7 100644
--- a/metagpt/const.py
+++ b/metagpt/const.py
@@ -33,5 +33,6 @@ API_QUESTIONS_PATH = UT_PATH / "files/question/"
YAPI_URL = "http://yapi.deepwisdomai.com/"
TMP = PROJECT_ROOT / 'tmp'
RESEARCH_PATH = DATA_PATH / "research"
+TUTORIAL_PATH = DATA_PATH / "tutorial_docx"
MEM_TTL = 24 * 30 * 3600
diff --git a/metagpt/prompts/tutorial_assistant.py b/metagpt/prompts/tutorial_assistant.py
new file mode 100644
index 000000000..aaf9ca215
--- /dev/null
+++ b/metagpt/prompts/tutorial_assistant.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+# _*_ coding: utf-8 _*_
+"""
+@Time : 2023/9/4 15:40:40
+@Author : Stitch-z
+@File : tutorial_assistant.py
+@Describe : Tutorial Assistant's prompt templates.
+"""
+
+
+DIRECTORY_PROMPT = """
+You are now a seasoned technical professional in the field of the internet.
+We need you to write a technical tutorial with the topic "{topic}".
+Please provide the specific table of contents for this tutorial, strictly following the following requirements:
+1. The output must be strictly in the specified language, {language}.
+2. Answer in the dictionary format like {{"title": "xxx", "directory": [{{"dir 1": ["sub dir 1", "sub dir 2"]}}]}}.
+3. The directory should be as specific and sufficient as possible, with a primary and secondary directory.The secondary directory is in the array.
+4. Do not have extra spaces or line breaks.
+5. Each directory title has practical significance.
+"""
+
+CONTENT_PROMPT = """
+You are now a seasoned technical professional in the field of the internet.
+We need you to write a technical tutorial with the topic "{topic}".
+Now I will give you the module directory titles for the topic.
+Please output the detailed principle content of this title in detail.
+If there are code examples, please provide them according to standard code specifications.
+Without a code example, it is not necessary.
+
+The module directory titles for the topic is as follows:
+{directory}
+
+Strictly limit output according to the following requirements:
+1. Follow the Markdown syntax format for layout.
+2. If there are code examples, they must follow standard syntax specifications, have document annotations, and be displayed in code blocks.
+3. The output must be strictly in the specified language, {language}.
+4. Do not have redundant output, including concluding remarks.
+5. Don't return the topic "{topic}".
+"""
\ No newline at end of file
diff --git a/metagpt/roles/tutorial_assistant.py b/metagpt/roles/tutorial_assistant.py
new file mode 100644
index 000000000..daf4daf40
--- /dev/null
+++ b/metagpt/roles/tutorial_assistant.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python3
+# _*_ coding: utf-8 _*_
+"""
+@Time : 2023/9/4 15:40:40
+@Author : Stitch-z
+@File : tutorial_assistant.py
+"""
+
+from typing import Dict
+
+from metagpt.actions.write_tutorial import WriteDirectory, WriteContent, SaveDocx
+from metagpt.logs import logger
+from metagpt.roles import Role
+from metagpt.schema import Message
+
+
+class TutorialAssistant(Role):
+ """Tutorial assistant, input one sentence to generate a tutorial document in markup format.
+
+ Args:
+ name: The name of the role.
+ profile: The role profile description.
+ goal: The goal of the role.
+ constraints: Constraints or requirements for the 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
+
+ async def _think(self) -> None:
+ """Determine the next action to be taken by the role."""
+ if self._rc.todo is None:
+ self._set_state(0)
+ return
+
+ if self._rc.state + 1 < len(self._states):
+ self._set_state(self._rc.state + 1)
+ else:
+ self._rc.todo = None
+
+ async def _handle_directory(self, titles: Dict) -> Message:
+ """Handle the directories for the tutorial document.
+
+ Args:
+ titles: A dictionary containing the titles and directory structure,
+ such as {"title": "xxx", "directory": [{"dir 1": ["sub dir 1", "sub dir 2"]}]}
+
+ Returns:
+ A message containing information about the directory.
+ """
+ self.main_title = titles.get("title")
+ directory = f"{self.main_title}\n"
+ self.total_content += f"# {self.main_title}"
+ actions = list()
+ for first_dir in titles.get("directory"):
+ actions.append(WriteContent(language=self.language, directory=first_dir))
+ key = list(first_dir.keys())[0]
+ directory += f"- {key}\n"
+ for second_dir in first_dir[key]:
+ directory += f" - {second_dir}\n"
+ actions.append(SaveDocx())
+ self._init_actions(actions)
+ self._rc.todo = None
+ return Message(content=directory)
+
+ async def _act(self) -> Message:
+ """Perform an action as determined by the role.
+
+ Returns:
+ A message containing the result of the action.
+ """
+ todo = self._rc.todo
+ if type(todo) is WriteDirectory:
+ msg = self._rc.memory.get(k=1)[0]
+ self.topic = msg.content
+ resp = await todo.run(topic=self.topic)
+ logger.info(resp)
+ return await self._handle_directory(resp)
+ elif type(todo) is SaveDocx:
+ filename = await todo.run(title=self.main_title, content=self.total_content)
+ return Message(content=filename, role=self.profile)
+ resp = await todo.run(topic=self.topic)
+ logger.info(resp)
+ if self.total_content != "":
+ self.total_content += "\n\n\n"
+ self.total_content += resp
+ return Message(content=resp, role=self.profile)
+
+ async def _react(self) -> Message:
+ """Execute the assistant's think and actions.
+
+ Returns:
+ A message containing the final result of the assistant's actions.
+ """
+ while True:
+ await self._think()
+ if self._rc.todo is None:
+ break
+ msg = await self._act()
+ return msg
From c0aab24b291eb7e2f5e7dd71e3ef8aa01c044706 Mon Sep 17 00:00:00 2001
From: Chen Zhang
Date: Wed, 6 Sep 2023 17:47:35 +0800
Subject: [PATCH 11/76] fix: windows compatibility
---
metagpt/utils/common.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/metagpt/utils/common.py b/metagpt/utils/common.py
index 7f090cf63..a0dabd77e 100644
--- a/metagpt/utils/common.py
+++ b/metagpt/utils/common.py
@@ -9,6 +9,7 @@ import ast
import contextlib
import inspect
import os
+import platform
import re
from typing import List, Tuple
@@ -20,7 +21,10 @@ def check_cmd_exists(command) -> int:
:param command: 待检查的命令
:return: 如果命令存在,返回0,如果不存在,返回非0
"""
- check_command = 'command -v ' + command + ' >/dev/null 2>&1 || { echo >&2 "no mermaid"; exit 1; }'
+ if platform.system().lower() == 'windows':
+ check_command = 'where ' + command
+ else:
+ check_command = 'command -v ' + command + ' >/dev/null 2>&1 || { echo >&2 "no mermaid"; exit 1; }'
result = os.system(check_command)
return result
From bc332a5f56d8e4f3e4697f8f30c73b56d504abeb Mon Sep 17 00:00:00 2001
From: Stitch-z <284618289@qq.com>
Date: Wed, 6 Sep 2023 18:15:07 +0800
Subject: [PATCH 12/76] update: optimize prompts
---
metagpt/prompts/tutorial_assistant.py | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/metagpt/prompts/tutorial_assistant.py b/metagpt/prompts/tutorial_assistant.py
index aaf9ca215..c9039fd41 100644
--- a/metagpt/prompts/tutorial_assistant.py
+++ b/metagpt/prompts/tutorial_assistant.py
@@ -7,10 +7,12 @@
@Describe : Tutorial Assistant's prompt templates.
"""
-
-DIRECTORY_PROMPT = """
+COMMON_PROMPT = """
You are now a seasoned technical professional in the field of the internet.
-We need you to write a technical tutorial with the topic "{topic}".
+We need you to write a technical tutorial with the topic "{topic}".
+"""
+
+DIRECTORY_PROMPT = COMMON_PROMPT + """
Please provide the specific table of contents for this tutorial, strictly following the following requirements:
1. The output must be strictly in the specified language, {language}.
2. Answer in the dictionary format like {{"title": "xxx", "directory": [{{"dir 1": ["sub dir 1", "sub dir 2"]}}]}}.
@@ -19,9 +21,7 @@ Please provide the specific table of contents for this tutorial, strictly follow
5. Each directory title has practical significance.
"""
-CONTENT_PROMPT = """
-You are now a seasoned technical professional in the field of the internet.
-We need you to write a technical tutorial with the topic "{topic}".
+CONTENT_PROMPT = COMMON_PROMPT + """
Now I will give you the module directory titles for the topic.
Please output the detailed principle content of this title in detail.
If there are code examples, please provide them according to standard code specifications.
From 23686efe031af165be9ea550700ec4b5b4369357 Mon Sep 17 00:00:00 2001
From: gallonyin
Date: Wed, 6 Sep 2023 20:19:23 +0800
Subject: [PATCH 13/76] =?UTF-8?q?Fix=20README=5FCN.md=20=E2=80=9C=E6=96=87?=
=?UTF-8?q?=E6=9C=AC=E9=94=99=E8=AF=AF=E2=80=9D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/README_CN.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/README_CN.md b/docs/README_CN.md
index 2180eb518..4a1f0c44e 100644
--- a/docs/README_CN.md
+++ b/docs/README_CN.md
@@ -115,7 +115,7 @@ ## 示例:启动一个创业公司
```shell
python startup.py "写一个命令行贪吃蛇"
-# 开启code review模式会会花费更多的money, 但是会提升代码质量和成功率
+# 开启code review模式会花费更多的money, 但是会提升代码质量和成功率
python startup.py "写一个命令行贪吃蛇" --code_review True
```
From 9f5ff2fb7d802adc0c29bf40119f943d732ceabb Mon Sep 17 00:00:00 2001
From: stellaHSR <34952977+stellaHSR@users.noreply.github.com>
Date: Wed, 6 Sep 2023 21:36:44 +0800
Subject: [PATCH 14/76] Update README.md
---
README.md | 1 -
1 file changed, 1 deletion(-)
diff --git a/README.md b/README.md
index 84dafa46b..5998adbb1 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,6 @@ # MetaGPT: The Multi-Agent Framework
-
From c1afa18882202bb720a1f48ff09edcef1496837c Mon Sep 17 00:00:00 2001
From: stellaHSR <34952977+stellaHSR@users.noreply.github.com>
Date: Wed, 6 Sep 2023 21:41:47 +0800
Subject: [PATCH 15/76] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 5998adbb1..b35291e56 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ # MetaGPT: The Multi-Agent Framework
-
+
From 813366f3e35e635b25e1d579cbcbe668322b6e52 Mon Sep 17 00:00:00 2001
From: stellaHSR <34952977+stellaHSR@users.noreply.github.com>
Date: Wed, 6 Sep 2023 21:42:06 +0800
Subject: [PATCH 16/76] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index b35291e56..5998adbb1 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ # MetaGPT: The Multi-Agent Framework
-
+
From 68b7118aed372879e44c304a29ddbe8b22202077 Mon Sep 17 00:00:00 2001
From: stellaHSR <34952977+stellaHSR@users.noreply.github.com>
Date: Wed, 6 Sep 2023 23:07:22 +0800
Subject: [PATCH 17/76] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 5998adbb1..dfb04cedf 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ # MetaGPT: The Multi-Agent Framework
-
+
From 3cc5503ab46241eba57a346861ffebb87c1a949c Mon Sep 17 00:00:00 2001
From: stellaHSR <34952977+stellaHSR@users.noreply.github.com>
Date: Wed, 6 Sep 2023 23:27:10 +0800
Subject: [PATCH 18/76] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index dfb04cedf..7623fde3f 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ # MetaGPT: The Multi-Agent Framework
-
+
From c5903eff8a77af362f18f8f3d723fb6045d7e871 Mon Sep 17 00:00:00 2001
From: stellaHSR <34952977+stellaHSR@users.noreply.github.com>
Date: Wed, 6 Sep 2023 23:28:46 +0800
Subject: [PATCH 19/76] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 7623fde3f..a3f669d7b 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ # MetaGPT: The Multi-Agent Framework
-
+
From 8e0b9308a8b5c6a528911609ea3f24b5875b385d Mon Sep 17 00:00:00 2001
From: stellaHSR <34952977+stellaHSR@users.noreply.github.com>
Date: Wed, 6 Sep 2023 23:30:59 +0800
Subject: [PATCH 20/76] Update README.md
---
README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/README.md b/README.md
index a3f669d7b..2c0c1757a 100644
--- a/README.md
+++ b/README.md
@@ -19,6 +19,7 @@ # MetaGPT: The Multi-Agent Framework
+ 
From 5158ede41f618d8ef44d0b6c10722cc86b8888d7 Mon Sep 17 00:00:00 2001
From: stellaHSR <34952977+stellaHSR@users.noreply.github.com>
Date: Wed, 6 Sep 2023 23:31:49 +0800
Subject: [PATCH 21/76] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 2c0c1757a..8934f48f8 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ # MetaGPT: The Multi-Agent Framework
- 
+
From 034a133817a9a4d23cd9a119665588ac469f0d49 Mon Sep 17 00:00:00 2001
From: stellaHSR <34952977+stellaHSR@users.noreply.github.com>
Date: Wed, 6 Sep 2023 23:34:44 +0800
Subject: [PATCH 22/76] Update README.md
---
README.md | 1 -
1 file changed, 1 deletion(-)
diff --git a/README.md b/README.md
index 8934f48f8..a3f669d7b 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,6 @@ # MetaGPT: The Multi-Agent Framework
-
From b90e16e74565906699e08b8dac2be1e5db20b5e4 Mon Sep 17 00:00:00 2001
From: stellaHSR <34952977+stellaHSR@users.noreply.github.com>
Date: Wed, 6 Sep 2023 23:34:57 +0800
Subject: [PATCH 23/76] Update README.md
---
README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/README.md b/README.md
index a3f669d7b..8934f48f8 100644
--- a/README.md
+++ b/README.md
@@ -19,6 +19,7 @@ # MetaGPT: The Multi-Agent Framework
+
From 76fe10ca0a2c047e38f84cb0729aaf379e1cbaab Mon Sep 17 00:00:00 2001
From: stellaHSR <34952977+stellaHSR@users.noreply.github.com>
Date: Wed, 6 Sep 2023 23:37:37 +0800
Subject: [PATCH 24/76] Update README.md
---
README.md | 1 -
1 file changed, 1 deletion(-)
diff --git a/README.md b/README.md
index 8934f48f8..a3f669d7b 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,6 @@ # MetaGPT: The Multi-Agent Framework
-
From 473baf193c4dd8f689e3d9295b0ca8731cd2df3a Mon Sep 17 00:00:00 2001
From: Stitch-z <284618289@qq.com>
Date: Wed, 6 Sep 2023 23:38:46 +0800
Subject: [PATCH 25/76] update: add unit test for the role tutorial assistant
---
tests/metagpt/actions/test_write_tutorial.py | 53 +++++++++++++++++++
.../metagpt/roles/test_tutorial_assistant.py | 27 ++++++++++
2 files changed, 80 insertions(+)
create mode 100644 tests/metagpt/actions/test_write_tutorial.py
create mode 100644 tests/metagpt/roles/test_tutorial_assistant.py
diff --git a/tests/metagpt/actions/test_write_tutorial.py b/tests/metagpt/actions/test_write_tutorial.py
new file mode 100644
index 000000000..6460aa08b
--- /dev/null
+++ b/tests/metagpt/actions/test_write_tutorial.py
@@ -0,0 +1,53 @@
+#!/usr/bin/env python3
+# _*_ coding: utf-8 _*_
+"""
+@Time : 2023/9/6 21:41:34
+@Author : Stitch-z
+@File : test_write_tutorial.py
+"""
+from typing import Dict
+
+import aiofiles
+import pytest
+
+from metagpt.actions.write_tutorial import WriteDirectory, WriteContent, SaveDocx
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ ("language", "topic"),
+ [("English", "Write a tutorial about Python")]
+)
+async def test_write_directory(language: str, topic: str):
+ ret = await WriteDirectory(language=language).run(topic=topic)
+ assert isinstance(ret, dict)
+ assert "title" in ret
+ assert "directory" in ret
+ assert isinstance(ret["directory"], list)
+ assert len(ret["directory"])
+ assert isinstance(ret["directory"][0], dict)
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ ("language", "topic", "directory"),
+ [("English", "Write a tutorial about Python", {"Introduction": ["What is Python?", "Why learn Python?"]})]
+)
+async def test_write_content(language: str, topic: str, directory: Dict):
+ ret = await WriteContent(language=language, directory=directory).run(topic=topic)
+ assert isinstance(ret, str)
+ assert list(directory.keys())[0] in ret
+ for value in list(directory.values())[0]:
+ assert value in ret
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ ("title", "content"),
+ [("Python", "Write a tutorial about Python")]
+)
+async def test_save_docx(title: str, content: str):
+ ret = await SaveDocx().run(title=title, content=content)
+ assert isinstance(ret, str)
+ assert title in ret
+ async with aiofiles.open(ret, mode="r") as reader:
+ body = await reader.read()
+ assert body == content
\ No newline at end of file
diff --git a/tests/metagpt/roles/test_tutorial_assistant.py b/tests/metagpt/roles/test_tutorial_assistant.py
new file mode 100644
index 000000000..945620cfc
--- /dev/null
+++ b/tests/metagpt/roles/test_tutorial_assistant.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python3
+# _*_ coding: utf-8 _*_
+"""
+@Time : 2023/9/6 23:11:27
+@Author : Stitch-z
+@File : test_tutorial_assistant.py
+"""
+import aiofiles
+import pytest
+
+from metagpt.roles.tutorial_assistant import TutorialAssistant
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ ("language", "topic"),
+ [("Chinese", "Write a tutorial about Python")]
+)
+async def test_tutorial_assistant(language: str, topic: str):
+ topic = "Write a tutorial about MySQL"
+ role = TutorialAssistant(language=language)
+ msg = await role.run(topic)
+ filename = msg.content
+ title = filename.split("/")[-1].split(".")[0]
+ async with aiofiles.open(filename, mode="r") as reader:
+ content = await reader.read()
+ assert content.startswith(f"# {title}")
\ No newline at end of file
From eb7a8f5287992b0d15bdf2d76a58a91e5fd59e63 Mon Sep 17 00:00:00 2001
From: ushio0107
Date: Wed, 6 Sep 2023 23:57:46 +0800
Subject: [PATCH 26/76] Update README_CN.md
---
docs/README_CN.md | 21 ++++++++++++++++++++-
1 file changed, 20 insertions(+), 1 deletion(-)
diff --git a/docs/README_CN.md b/docs/README_CN.md
index 2180eb518..dda5f15eb 100644
--- a/docs/README_CN.md
+++ b/docs/README_CN.md
@@ -53,6 +53,25 @@ # 第 3 步:克隆仓库到您的本地机器,并进行安装。
python setup.py install
```
+**注意:**
+
+- 如果已经安装了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"
+ ```
+
+- 如果`python setup.py install`失败并显示错误`[Errno 13] Permission denied: '/usr/local/lib/python3.11/dist-packages/test-easy-install-13129.write-test'`,请尝试使用`python setup.py install --user`运行。
+
### Docker安装
```bash
@@ -115,7 +134,7 @@ ## 示例:启动一个创业公司
```shell
python startup.py "写一个命令行贪吃蛇"
-# 开启code review模式会会花费更多的money, 但是会提升代码质量和成功率
+# 开启code review模式会会花费更多的金钱, 但是会提升代码质量和成功率
python startup.py "写一个命令行贪吃蛇" --code_review True
```
From 207485e24a9d068fc301741558c55a5661861406 Mon Sep 17 00:00:00 2001
From: ushio0107
Date: Thu, 7 Sep 2023 00:09:40 +0800
Subject: [PATCH 27/76] Update README_CN.md
---
docs/README_CN.md | 1 -
1 file changed, 1 deletion(-)
diff --git a/docs/README_CN.md b/docs/README_CN.md
index dda5f15eb..8e994e664 100644
--- a/docs/README_CN.md
+++ b/docs/README_CN.md
@@ -142,7 +142,6 @@ # 开启code review模式会会花费更多的金钱, 但是会提升代码质
### 平台或工具的倾向性
可以在阐述需求时说明想要使用的平台或工具。
例如:
-
```shell
python startup.py "写一个基于pygame的命令行贪吃蛇"
```
From 4973364556da056476c56874f7cfea0e9f190a4e Mon Sep 17 00:00:00 2001
From: ushio0107
Date: Thu, 7 Sep 2023 13:55:07 +0800
Subject: [PATCH 28/76] Update README_CN.md
---
docs/README_CN.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/README_CN.md b/docs/README_CN.md
index 8e994e664..ae5d954e4 100644
--- a/docs/README_CN.md
+++ b/docs/README_CN.md
@@ -134,7 +134,7 @@ ## 示例:启动一个创业公司
```shell
python startup.py "写一个命令行贪吃蛇"
-# 开启code review模式会会花费更多的金钱, 但是会提升代码质量和成功率
+# 开启code review模式会花费更多的金钱, 但是会提升代码质量和成功率
python startup.py "写一个命令行贪吃蛇" --code_review True
```
From d83d1e105c28b558f9da5e56967834de6b8fe3c6 Mon Sep 17 00:00:00 2001
From: Stitch-z <284618289@qq.com>
Date: Thu, 7 Sep 2023 19:55:50 +0800
Subject: [PATCH 29/76] update: format directory structure and extract
universal file operation class
---
metagpt/actions/write_tutorial.py | 67 +++++++++-----------
metagpt/prompts/tutorial_assistant.py | 4 +-
metagpt/roles/tutorial_assistant.py | 11 ++--
metagpt/utils/file.py | 35 ++++++++++
tests/metagpt/actions/test_write_tutorial.py | 17 +----
tests/metagpt/utils/test_file.py | 27 ++++++++
6 files changed, 102 insertions(+), 59 deletions(-)
create mode 100644 metagpt/utils/file.py
create mode 100644 tests/metagpt/utils/test_file.py
diff --git a/metagpt/actions/write_tutorial.py b/metagpt/actions/write_tutorial.py
index 38a45c4c3..b23fc2ad4 100644
--- a/metagpt/actions/write_tutorial.py
+++ b/metagpt/actions/write_tutorial.py
@@ -7,13 +7,9 @@
@Describe : Actions of the tutorial assistant, including writing directories and document content.
"""
import json
-from datetime import datetime
from typing import Dict
-import aiofiles
-
from metagpt.actions import Action
-from metagpt.const import TUTORIAL_PATH
from metagpt.logs import logger
from metagpt.prompts.tutorial_assistant import DIRECTORY_PROMPT, CONTENT_PROMPT
@@ -30,6 +26,33 @@ class WriteDirectory(Action):
super().__init__(name, *args, **kwargs)
self.language = language
+ @staticmethod
+ async def _handle_resp(resp: str) -> Dict:
+ """Process string results and convert them to JSON format.
+
+ Args:
+ resp: The directory results returned by gpt.
+
+ Returns:
+ The parsed dictionary, such as {"title": "xxx", "directory": [{"dir 1": ["sub dir 1", "sub dir 2"]}]}.
+
+ Raises:
+ Exception: If no matching dictionary section is found.
+ json.JSONDecodeError: If the dictionary part cannot be parsed as JSON.
+ """
+ start = resp.find('{')
+ end = resp.rfind('}')
+ if start != -1 and end != -1 and end > start:
+ directory_str = resp[start:end + 1]
+ logger.info(f"Successfully parsed json: {str(directory_str)}")
+ try:
+ return json.loads(directory_str)
+ except json.JSONDecodeError as e:
+ logger.error(f"Json parsing error: {e}")
+ raise e
+ else:
+ raise Exception("No matching dictionary section found.")
+
async def run(self, topic: str, *args, **kwargs) -> Dict:
"""Execute the action to generate a tutorial directory according to the topic.
@@ -37,11 +60,11 @@ class WriteDirectory(Action):
topic: The tutorial topic.
Returns:
- the tutorial directory information, such as {"title": "xxx", "directory": [{"dir 1": ["sub dir 1", "sub dir 2"]}]}
+ the tutorial directory information, including {"title": "xxx", "directory": [{"dir 1": ["sub dir 1", "sub dir 2"]}]}.
"""
prompt = DIRECTORY_PROMPT.format(topic=topic, language=self.language)
- directory = await self._aask(prompt=prompt)
- return json.loads(directory)
+ resp = await self._aask(prompt=prompt)
+ return await self._handle_resp(resp)
class WriteContent(Action):
@@ -70,33 +93,3 @@ class WriteContent(Action):
prompt = CONTENT_PROMPT.format(topic=topic, language=self.language, directory=self.directory)
return await self._aask(prompt=prompt)
-
-class SaveDocx(Action):
- """Action class for saving tutorial docx.
-
- Args:
- name: The name of the action.
- """
-
- def __init__(self, name: str = "", *args, **kwargs):
- super().__init__(name, *args, **kwargs)
-
- async def run(self, title: str, content: str, *args, **kwargs) -> str:
- """Execute the action to save the generated tutorial document to a Markdown file.
-
- Args:
- title: The title of tutorial.
- content: The total content of tutorial.
-
- Returns:
- The full filename of tutorial content.
-
- """
- current_time = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
- pathname = TUTORIAL_PATH / current_time
- pathname.mkdir(parents=True, exist_ok=True)
- filename = f"{pathname}/{title}.md"
- async with aiofiles.open(filename, mode="w", encoding="utf-8") as writer:
- await writer.write(content)
- logger.info(f"Successfully write docx: {filename}")
- return filename
\ No newline at end of file
diff --git a/metagpt/prompts/tutorial_assistant.py b/metagpt/prompts/tutorial_assistant.py
index c9039fd41..d690aad83 100644
--- a/metagpt/prompts/tutorial_assistant.py
+++ b/metagpt/prompts/tutorial_assistant.py
@@ -15,7 +15,7 @@ We need you to write a technical tutorial with the topic "{topic}".
DIRECTORY_PROMPT = COMMON_PROMPT + """
Please provide the specific table of contents for this tutorial, strictly following the following requirements:
1. The output must be strictly in the specified language, {language}.
-2. Answer in the dictionary format like {{"title": "xxx", "directory": [{{"dir 1": ["sub dir 1", "sub dir 2"]}}]}}.
+2. Answer strictly in the dictionary format like {{"title": "xxx", "directory": [{{"dir 1": ["sub dir 1", "sub dir 2"]}}, {{"dir 2": ["sub dir 3", "sub dir 4"]}}]}}.
3. The directory should be as specific and sufficient as possible, with a primary and secondary directory.The secondary directory is in the array.
4. Do not have extra spaces or line breaks.
5. Each directory title has practical significance.
@@ -35,5 +35,5 @@ Strictly limit output according to the following requirements:
2. If there are code examples, they must follow standard syntax specifications, have document annotations, and be displayed in code blocks.
3. The output must be strictly in the specified language, {language}.
4. Do not have redundant output, including concluding remarks.
-5. Don't return the topic "{topic}".
+5. Strict requirement not to output the topic "{topic}".
"""
\ No newline at end of file
diff --git a/metagpt/roles/tutorial_assistant.py b/metagpt/roles/tutorial_assistant.py
index daf4daf40..9a7df4f4d 100644
--- a/metagpt/roles/tutorial_assistant.py
+++ b/metagpt/roles/tutorial_assistant.py
@@ -6,12 +6,15 @@
@File : tutorial_assistant.py
"""
+from datetime import datetime
from typing import Dict
-from metagpt.actions.write_tutorial import WriteDirectory, WriteContent, SaveDocx
+from metagpt.actions.write_tutorial import WriteDirectory, WriteContent
+from metagpt.const import TUTORIAL_PATH
from metagpt.logs import logger
from metagpt.roles import Role
from metagpt.schema import Message
+from metagpt.utils.file import File
class TutorialAssistant(Role):
@@ -71,7 +74,6 @@ class TutorialAssistant(Role):
directory += f"- {key}\n"
for second_dir in first_dir[key]:
directory += f" - {second_dir}\n"
- actions.append(SaveDocx())
self._init_actions(actions)
self._rc.todo = None
return Message(content=directory)
@@ -89,9 +91,6 @@ class TutorialAssistant(Role):
resp = await todo.run(topic=self.topic)
logger.info(resp)
return await self._handle_directory(resp)
- elif type(todo) is SaveDocx:
- filename = await todo.run(title=self.main_title, content=self.total_content)
- return Message(content=filename, role=self.profile)
resp = await todo.run(topic=self.topic)
logger.info(resp)
if self.total_content != "":
@@ -110,4 +109,6 @@ class TutorialAssistant(Role):
if self._rc.todo is None:
break
msg = await self._act()
+ root_path = TUTORIAL_PATH / datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
+ await File.write(root_path, f"{self.main_title}.md", self.total_content.encode('utf-8'))
return msg
diff --git a/metagpt/utils/file.py b/metagpt/utils/file.py
new file mode 100644
index 000000000..93e1ad6c7
--- /dev/null
+++ b/metagpt/utils/file.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+# _*_ coding: utf-8 _*_
+"""
+@Time : 2023/9/4 15:40:40
+@Author : Stitch-z
+@File : file.py
+@Describe : General file operations.
+"""
+import aiofiles
+from pathlib import Path
+
+from metagpt.logs import logger
+
+
+class File:
+ """A general util for file operations."""
+
+ @classmethod
+ async def write(cls, root_path: Path, filename: str, content: bytes) -> Path:
+ """Write the file content to the local specified path.
+
+ Args:
+ root_path: The root path of file, such as "/data".
+ filename: The name of file, such as "test.txt".
+ content: The binary content of file.
+
+ Returns:
+ The full filename of file, such as "/data/test.txt".
+ """
+ root_path.mkdir(parents=True, exist_ok=True)
+ full_path = root_path / filename
+ async with aiofiles.open(full_path, mode="wb") as writer:
+ await writer.write(content)
+ logger.info(f"Successfully write docx: {full_path}")
+ return full_path
\ No newline at end of file
diff --git a/tests/metagpt/actions/test_write_tutorial.py b/tests/metagpt/actions/test_write_tutorial.py
index 6460aa08b..683fee082 100644
--- a/tests/metagpt/actions/test_write_tutorial.py
+++ b/tests/metagpt/actions/test_write_tutorial.py
@@ -7,10 +7,9 @@
"""
from typing import Dict
-import aiofiles
import pytest
-from metagpt.actions.write_tutorial import WriteDirectory, WriteContent, SaveDocx
+from metagpt.actions.write_tutorial import WriteDirectory, WriteContent
@pytest.mark.asyncio
@@ -27,6 +26,7 @@ async def test_write_directory(language: str, topic: str):
assert len(ret["directory"])
assert isinstance(ret["directory"][0], dict)
+
@pytest.mark.asyncio
@pytest.mark.parametrize(
("language", "topic", "directory"),
@@ -38,16 +38,3 @@ async def test_write_content(language: str, topic: str, directory: Dict):
assert list(directory.keys())[0] in ret
for value in list(directory.values())[0]:
assert value in ret
-
-@pytest.mark.asyncio
-@pytest.mark.parametrize(
- ("title", "content"),
- [("Python", "Write a tutorial about Python")]
-)
-async def test_save_docx(title: str, content: str):
- ret = await SaveDocx().run(title=title, content=content)
- assert isinstance(ret, str)
- assert title in ret
- async with aiofiles.open(ret, mode="r") as reader:
- body = await reader.read()
- assert body == content
\ No newline at end of file
diff --git a/tests/metagpt/utils/test_file.py b/tests/metagpt/utils/test_file.py
new file mode 100644
index 000000000..a9f1a353d
--- /dev/null
+++ b/tests/metagpt/utils/test_file.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python3
+# _*_ coding: utf-8 _*_
+"""
+@Time : 2023/9/4 15:40:40
+@Author : Stitch-z
+@File : test_file.py
+"""
+from pathlib import Path
+
+import aiofiles
+import pytest
+
+from metagpt.utils.file import File
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ ("root_path", "filename", "content"),
+ [(Path("/code/MetaGPT/data/tutorial_docx/2023-09-07_17-05-20"), "test.md", "Hello World!")]
+)
+async def test_write_file(root_path: Path, filename: str, content: bytes):
+ full_file_name = await File.write(root_path=root_path, filename=filename, content=content.encode('utf-8'))
+ assert isinstance(full_file_name, Path)
+ assert root_path / filename == full_file_name
+ async with aiofiles.open(full_file_name, mode="r") as reader:
+ body = await reader.read()
+ assert body == content
\ No newline at end of file
From b2b227391a8a30bc70ae6f40214d6e697ccb51cd Mon Sep 17 00:00:00 2001
From: Stitch-z <284618289@qq.com>
Date: Thu, 7 Sep 2023 20:08:02 +0800
Subject: [PATCH 30/76] update: Add exception handling for write file
operation.
---
metagpt/utils/file.py | 16 ++++++++++------
1 file changed, 10 insertions(+), 6 deletions(-)
diff --git a/metagpt/utils/file.py b/metagpt/utils/file.py
index 93e1ad6c7..3e2adcf7b 100644
--- a/metagpt/utils/file.py
+++ b/metagpt/utils/file.py
@@ -27,9 +27,13 @@ class File:
Returns:
The full filename of file, such as "/data/test.txt".
"""
- root_path.mkdir(parents=True, exist_ok=True)
- full_path = root_path / filename
- async with aiofiles.open(full_path, mode="wb") as writer:
- await writer.write(content)
- logger.info(f"Successfully write docx: {full_path}")
- return full_path
\ No newline at end of file
+ try:
+ root_path.mkdir(parents=True, exist_ok=True)
+ full_path = root_path / filename
+ async with aiofiles.open(full_path, mode="wb") as writer:
+ await writer.write(content)
+ logger.info(f"Successfully write docx: {full_path}")
+ return full_path
+ except Exception as e:
+ logger.error(f"Error writing file: {e}")
+ raise e
\ No newline at end of file
From 5405d9cf32e79f3408a25d481940fe39e8c6316e Mon Sep 17 00:00:00 2001
From: Stitch-z <284618289@qq.com>
Date: Thu, 7 Sep 2023 20:09:04 +0800
Subject: [PATCH 31/76] update: Add exception handling for write file
operation.
---
metagpt/utils/file.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/metagpt/utils/file.py b/metagpt/utils/file.py
index 3e2adcf7b..84b2f8aeb 100644
--- a/metagpt/utils/file.py
+++ b/metagpt/utils/file.py
@@ -26,6 +26,9 @@ class File:
Returns:
The full filename of file, such as "/data/test.txt".
+
+ Raises:
+ Exception: If an unexpected error occurs during the file writing process.
"""
try:
root_path.mkdir(parents=True, exist_ok=True)
From a90c4309a0dba269e72192521c111c8d316adea6 Mon Sep 17 00:00:00 2001
From: stellaHSR <34952977+stellaHSR@users.noreply.github.com>
Date: Sun, 10 Sep 2023 23:17:58 +0800
Subject: [PATCH 32/76] Update prompt.py
fixquotes error
---
metagpt/roles/prompt.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/metagpt/roles/prompt.py b/metagpt/roles/prompt.py
index d13551203..c22e0226b 100644
--- a/metagpt/roles/prompt.py
+++ b/metagpt/roles/prompt.py
@@ -32,7 +32,7 @@ class PromptString(Enum):
RECENT_ACTIVITY = "Based on the following memory, produce a brief summary of what {full_name} has been up to recently. Do not invent details not explicitly stated in the memory. For any conversation, be sure to mention whether the conversation has concluded or is still ongoing.\n\nMemory: {memory_descriptions}"
- MAKE_PLANS = 'You are a plan-generating AI. Your job is to assist the character in formulating new plans based on new information. Given the character's information (profile, objectives, recent activities, current plans, and location context) and their current thought process, produce a new set of plans for them. The final plan should comprise at least {time_window} of activities and no more than 5 individual plans. List the plans in the order they should be executed, with each plan detailing its description, location, start time, stop criteria, and maximum duration.\n\nSample plan: \'{{"index": 1, "description": "Cook dinner", "location_id": "0a3bc22b-36aa-48ab-adb0-18616004caed","start_time": "2022-12-12T20:00:00+00:00","max_duration_hrs": 1.5, "stop_condition": "Dinner is fully prepared"}}\'\n\nFor each plan, choose the most appropriate location name from this list: {allowed_location_descriptions}\n\n{format_instructions}\n\nAlways prioritize completing any unfinished conversations.\n\nLet's begin!\n\nName: {full_name}\nProfile: {private_bio}\nObjectives: {directives}\nLocation Context: {location_context}\nCurrent Plans: {current_plans}\nRecent Activities: {recent_activity}\nThought Process: {thought_process}\nIt's essential to encourage the character to collaborate with other characters in their plans.\n\n'
+ MAKE_PLANS = "You are a plan-generating AI. Your job is to assist the character in formulating new plans based on new information. Given the character's information (profile, objectives, recent activities, current plans, and location context) and their current thought process, produce a new set of plans for them. The final plan should comprise at least {time_window} of activities and no more than 5 individual plans. List the plans in the order they should be executed, with each plan detailing its description, location, start time, stop criteria, and maximum duration.\n\nSample plan: {{\"index\": 1, \"description\": \"Cook dinner\", \"location_id\": \"0a3bc22b-36aa-48ab-adb0-18616004caed\",\"start_time\": \"2022-12-12T20:00:00+00:00\",\"max_duration_hrs\": 1.5, \"stop_condition\": \"Dinner is fully prepared\"}}\'\n\nFor each plan, choose the most appropriate location name from this list: {allowed_location_descriptions}\n\n{format_instructions}\n\nAlways prioritize completing any unfinished conversations.\n\nLet's begin!\n\nName: {full_name}\nProfile: {private_bio}\nObjectives: {directives}\nLocation Context: {location_context}\nCurrent Plans: {current_plans}\nRecent Activities: {recent_activity}\nThought Process: {thought_process}\nIt's essential to encourage the character to collaborate with other characters in their plans.\n\n"
EXECUTE_PLAN = "You are a role-playing AI, playing the role of {your_name}, in front of a live audience. Every word you say can be observed by the audience, so make sure you speak often and make it interesting. You cannot interact directly with the audience.\n\nBased on the following context and tools, proceed as if you are {your_name}. Your primary task is to complete the task given below, however, if you are currently in a conversation with another character, you should always complete the conversation first and then proceed with the task. Do not start working while you are engaged in an unfinished conversation. Use your best judgment to determine whether a conversation involves you and whether it is unfinished. You don't need to respond to every message you receive.\n\nThis is a personal profile of your character:\n{your_private_bio}\n\nThis is a description of your location and other nearby characters you can converse with:\n\n{location_context}\n\nThese are some relevant memories:\n```\n{relevant_memories}\n```\n\nThis is some relevant conversation history:\n```\n{conversation_history}\n```\n\nThese are the tools you can use:\n{tools}\n\nYour response should follow the format below:\n\nTask: The task you must complete\nThoughts: What you should think about doing\nAction: The action to take, must be one of these words: [{tool_names}]\nAction Input: Input for the action\nObservation: The result of the action\n... (This Thoughts/Action/Action Input/Observation can be repeated N times)\nThoughts: 'I have completed the task'\nFinal Response: The final response to the task\n\nIf you are not ready with a final response, then you must take an action.\n\nIf you are certain that you cannot complete the task with the tools provided, return 'Final Response: Need Help', however, if you are in conversation with another character, responses like 'I don't know' are valid. During conversations, you should never break character or admit you are an AI.\nIf the task is completed and no specific response is required, return 'Final Response: Done'\nLet's begin!\n\nTask: {input}\n\n{agent_scratchpad}"
From 90b045b11a87c1f6801c012f9c78a1f4b3277a13 Mon Sep 17 00:00:00 2001
From: Stitch-z <284618289@qq.com>
Date: Mon, 11 Sep 2023 10:20:05 +0800
Subject: [PATCH 33/76] update file tool log
---
metagpt/utils/file.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/metagpt/utils/file.py b/metagpt/utils/file.py
index 84b2f8aeb..5aca2a0e5 100644
--- a/metagpt/utils/file.py
+++ b/metagpt/utils/file.py
@@ -35,7 +35,7 @@ class File:
full_path = root_path / filename
async with aiofiles.open(full_path, mode="wb") as writer:
await writer.write(content)
- logger.info(f"Successfully write docx: {full_path}")
+ logger.info(f"Successfully write file: {full_path}")
return full_path
except Exception as e:
logger.error(f"Error writing file: {e}")
From 3b97adcad1b0badd5731826af1fa4d13eb6e5314 Mon Sep 17 00:00:00 2001
From: stellaHSR <34952977+stellaHSR@users.noreply.github.com>
Date: Mon, 11 Sep 2023 14:40:02 +0800
Subject: [PATCH 34/76] Update README.md
add HF space link
---
README.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/README.md b/README.md
index a3f669d7b..db4d394ea 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,7 @@ # MetaGPT: The Multi-Agent Framework
+
@@ -213,6 +214,9 @@ ## QuickStart
- [MetaGPT quickstart](https://deepwisdom.feishu.cn/wiki/CyY9wdJc4iNqArku3Lncl4v8n2b)
+Try it on Huggingface Space
+- https://huggingface.co/spaces/deepwisdom/MetaGPT
+
## Citation
For now, cite the [Arxiv paper](https://arxiv.org/abs/2308.00352):
From 222a3236b5e9e6b794a62f6a99ff82059c0c5454 Mon Sep 17 00:00:00 2001
From: stellaHSR <34952977+stellaHSR@users.noreply.github.com>
Date: Mon, 11 Sep 2023 14:42:30 +0800
Subject: [PATCH 35/76] Update README_CN.md
add HF space link
---
docs/README_CN.md | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/docs/README_CN.md b/docs/README_CN.md
index ae5d954e4..c3a0e92df 100644
--- a/docs/README_CN.md
+++ b/docs/README_CN.md
@@ -13,6 +13,7 @@ # MetaGPT: 多智能体框架
+
@@ -198,6 +199,10 @@ ## 快速体验
- [MetaGPT快速体验](https://deepwisdom.feishu.cn/wiki/Q8ycw6J9tiNXdHk66MRcIN8Pnlg)
+可直接在Huggingface Space体验
+
+- https://huggingface.co/spaces/deepwisdom/MetaGPT
+
## 联系信息
如果您对这个项目有任何问题或反馈,欢迎联系我们。我们非常欢迎您的建议!
From 8d74c9954c16cf76ab0844002a0e16cbefa4daaf Mon Sep 17 00:00:00 2001
From: stellaHSR <34952977+stellaHSR@users.noreply.github.com>
Date: Mon, 11 Sep 2023 14:45:27 +0800
Subject: [PATCH 36/76] Update README_JA.md
add HF space link
---
docs/README_JA.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/docs/README_JA.md b/docs/README_JA.md
index 4be862b6f..9bda95556 100644
--- a/docs/README_JA.md
+++ b/docs/README_JA.md
@@ -13,6 +13,7 @@ # MetaGPT: マルチエージェントフレームワーク
+
@@ -198,6 +199,9 @@ ## クイックスタート
- [MetaGPT クイックスタート](https://deepwisdom.feishu.cn/wiki/CyY9wdJc4iNqArku3Lncl4v8n2b)
+試着する Huggingface Space
+- https://huggingface.co/spaces/deepwisdom/MetaGPT
+
## 引用
現時点では、[Arxiv 論文](https://arxiv.org/abs/2308.00352)を引用してください:
From c043ba8e05b2f19d6bb96fd0c158d049a55bffbb Mon Sep 17 00:00:00 2001
From: stellaHSR <34952977+stellaHSR@users.noreply.github.com>
Date: Mon, 11 Sep 2023 15:39:06 +0800
Subject: [PATCH 37/76] Update README.md
update logo
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index db4d394ea..241d12ebf 100644
--- a/README.md
+++ b/README.md
@@ -12,8 +12,8 @@ # MetaGPT: The Multi-Agent Framework
-
-
+
+
From 12117f49bd38a583154a2a9849a577a5f30d06ac Mon Sep 17 00:00:00 2001
From: stellaHSR <34952977+stellaHSR@users.noreply.github.com>
Date: Mon, 11 Sep 2023 15:42:53 +0800
Subject: [PATCH 38/76] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 241d12ebf..b2a4f18c1 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,6 @@ # MetaGPT: The Multi-Agent Framework
-
@@ -22,6 +21,7 @@ # MetaGPT: The Multi-Agent Framework
+
1. MetaGPT takes a **one line requirement** as input and outputs **user stories / competitive analysis / requirements / data structures / APIs / documents, etc.**
From 7e9eb2b1017a428c8221e854083e19f6b2cc5cd5 Mon Sep 17 00:00:00 2001
From: stellaHSR <34952977+stellaHSR@users.noreply.github.com>
Date: Mon, 11 Sep 2023 15:46:18 +0800
Subject: [PATCH 39/76] Update README_CN.md
update logo
---
docs/README_CN.md | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/docs/README_CN.md b/docs/README_CN.md
index c3a0e92df..a5e4c6879 100644
--- a/docs/README_CN.md
+++ b/docs/README_CN.md
@@ -12,13 +12,17 @@ # MetaGPT: 多智能体框架
-
-
+
+
+
+
+
+
1. MetaGPT输入**一句话的老板需求**,输出**用户故事 / 竞品分析 / 需求 / 数据结构 / APIs / 文件等**
2. MetaGPT内部包括**产品经理 / 架构师 / 项目经理 / 工程师**,它提供了一个**软件公司**的全过程与精心调配的SOP
From 4b4a6613d0c8b0d696ca45bce3b1967ca0c72f7f Mon Sep 17 00:00:00 2001
From: stellaHSR <34952977+stellaHSR@users.noreply.github.com>
Date: Mon, 11 Sep 2023 15:47:28 +0800
Subject: [PATCH 40/76] Update README_JA.md
update logo
---
docs/README_JA.md | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/docs/README_JA.md b/docs/README_JA.md
index 9bda95556..f930c0cc2 100644
--- a/docs/README_JA.md
+++ b/docs/README_JA.md
@@ -12,13 +12,17 @@ # MetaGPT: マルチエージェントフレームワーク
-
-
+
+
+
+
+
+
1. MetaGPT は、**1 行の要件** を入力とし、**ユーザーストーリー / 競合分析 / 要件 / データ構造 / API / 文書など** を出力します。
2. MetaGPT には、**プロダクト マネージャー、アーキテクト、プロジェクト マネージャー、エンジニア** が含まれています。MetaGPT は、**ソフトウェア会社のプロセス全体を、慎重に調整された SOP とともに提供します。**
From 10ce3ed70245462f50fcb23eeee7708b637d65ad Mon Sep 17 00:00:00 2001
From: "hy.li"
Date: Mon, 11 Sep 2023 16:59:41 +0800
Subject: [PATCH 41/76] more options to convert mermaid
---
README.md | 69 ++++++++--
config/config.yaml | 2 +-
metagpt/utils/mermaid.py | 73 ++++++----
metagpt/utils/mmdc_ink.py | 51 +++++++
...rmaid_playwright.py => mmdc_playwright.py} | 119 ++++------------
metagpt/utils/mmdc_pyppeteer.py | 129 ++++++++++++++++++
6 files changed, 312 insertions(+), 131 deletions(-)
create mode 100644 metagpt/utils/mmdc_ink.py
rename metagpt/utils/{mermaid_playwright.py => mmdc_playwright.py} (63%)
create mode 100644 metagpt/utils/mmdc_pyppeteer.py
diff --git a/README.md b/README.md
index 864d56c53..a380ce1a8 100644
--- a/README.md
+++ b/README.md
@@ -81,29 +81,68 @@ # Step 3: Clone the repository to your local machine, and install it.
- if `python setup.py install` fails with error `[Errno 13] Permission denied: '/usr/local/lib/python3.11/dist-packages/test-easy-install-13129.write-test'`, try instead running `python setup.py install --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 for this task.
+- 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.
-- **Install Playwright**
+ - Playwright
+ - **Install Playwright**
-```bash
-pip install playwright
-```
+ ```bash
+ pip install playwright
+ ```
-- **Install the Required Browsers**
+ - **Install the Required Browsers**
-to support PDF conversion, had better install Chrominum.
+ to support PDF conversion, had better install Chrominum.
-```bash
-playwright install --with-deps chromium
-```
+ ```bash
+ playwright install --with-deps chromium
+ ```
-- **modify `config.yaml`**
+ - **modify `config.yaml`**
-uncomment MERMAID_ENGINE from config.yaml and change it to `playwright`
+ uncomment MERMAID_ENGINE from config.yaml and change it to `playwright`
-```yaml
-MERMAID_ENGINE: playwright
-```
+ ```yaml
+ MERMAID_ENGINE: playwright
+ ```
+
+ - pyppeteer
+ - **Install pyppeteer**
+
+ ```bash
+ pip install pyppeteer
+ ```
+
+ - **Install the Required Browsers**
+
+ ```bash
+ pyppeteer-install
+ ```
+
+ pyppeteer alow you use already installed browsers, if you do not want to run the above command, please set the following envirment
+
+ ```bash
+ export PUPPETEER_EXECUTABLE_PATH = /path/to/your/chromium or edge or chrome
+ ```
+
+ - **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.
### Installation by Docker
diff --git a/config/config.yaml b/config/config.yaml
index 40d37451a..179985a6f 100644
--- a/config/config.yaml
+++ b/config/config.yaml
@@ -78,5 +78,5 @@ MODEL_FOR_RESEARCHER_SUMMARY: gpt-3.5-turbo
MODEL_FOR_RESEARCHER_REPORT: gpt-3.5-turbo-16k
### choose the engine for mermaid conversion,
-# default is nodejs, you can change it to playwright
+# default is nodejs, you can change it to playwright,pyppeteer or ink
# MERMAID_ENGINE: nodejs
\ No newline at end of file
diff --git a/metagpt/utils/mermaid.py b/metagpt/utils/mermaid.py
index f395b43b2..713f49601 100644
--- a/metagpt/utils/mermaid.py
+++ b/metagpt/utils/mermaid.py
@@ -12,6 +12,7 @@ from metagpt.config import CONFIG
from metagpt.const import PROJECT_ROOT
from metagpt.logs import logger
from metagpt.utils.common import check_cmd_exists
+import os
@@ -31,34 +32,58 @@ def mermaid_to_file(mermaid_code, output_file_without_suffix, width=2048, height
if check_cmd_exists("mmdc") != 0:
logger.warning("RUN `npm install -g @mermaid-js/mermaid-cli` to install mmdc")
return -1
+ engine = CONFIG.mermaid_engine.lower()
- for suffix in ["pdf", "svg", "png"]:
- output_file = f"{output_file_without_suffix}.{suffix}"
- # Call the `mmdc` command to convert the Mermaid code to a PNG
- logger.info(f"Generating {output_file}..")
+ if engine == "nodejs":
+ for suffix in ["pdf", "svg", "png"]:
+ output_file = f"{output_file_without_suffix}.{suffix}"
+ # Call the `mmdc` command to convert the Mermaid code to a PNG
+ logger.info(f"Generating {output_file}..")
- if CONFIG.puppeteer_config:
- subprocess.run(
- [
- CONFIG.mmdc,
- "-p",
- CONFIG.puppeteer_config,
- "-i",
- str(tmp),
- "-o",
- output_file,
- "-w",
- str(width),
- "-H",
- str(height),
- ]
- )
- else:
- subprocess.run([CONFIG.mmdc, "-i", str(tmp), "-o", output_file, "-w", str(width), "-H", str(height)])
+ if CONFIG.puppeteer_config:
+ subprocess.run(
+ [
+ CONFIG.mmdc,
+ "-p",
+ CONFIG.puppeteer_config,
+ "-i",
+ str(tmp),
+ "-o",
+ output_file,
+ "-w",
+ str(width),
+ "-H",
+ str(height),
+ ]
+ )
+ else:
+ subprocess.run([CONFIG.mmdc, "-i", str(tmp), "-o", output_file, "-w", str(width), "-H", str(height)])
+ else:
+ if engine not in ['playwright', 'puppeteer', 'ink']:
+ logger.warning(f"Unsupported mermaid engine: {engine}")
+ return -1
+ __dirname = os.path.dirname(os.path.abspath(__file__))
+ module_path = os.path.join(__dirname, f'mmdc_{engine}.py')
+ import sys
+ # 构建命令行参数
+ command = [
+ sys.executable,
+ module_path,
+ "-i",mermaid_code,
+ "-o",output_file_without_suffix
+ ]
+
+ # 执行命令
+ try:
+ result = subprocess.run(command, text=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ logger.info(result.stdout)
+ if result.stderr:
+ logger.error(result.stderr)
+ except subprocess.CalledProcessError as e:
+ logger.error(f"Command execution failed with return code {e.returncode}")
+ logger.error(e.output)
return 0
-if CONFIG.mermaid_engine.lower() == "playwright":
- from metagpt.utils.mermaid_playwright import mermaid_to_file
MMC1 = """classDiagram
class Main {
diff --git a/metagpt/utils/mmdc_ink.py b/metagpt/utils/mmdc_ink.py
new file mode 100644
index 000000000..ce50b11cd
--- /dev/null
+++ b/metagpt/utils/mmdc_ink.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+@Time : 2023/7/4 10:53
+@Author : alexanderwu, imjohndoe
+@File : mermaid.py
+"""
+
+import requests
+import base64
+import os
+
+import click
+@click.command()
+@click.version_option()
+@click.option("-i","--mermaid_code", type=str, help="mermaid code")
+@click.option("-o","--output_file_without_suffix", type=str, help="output filename without suffix")
+def mermaid_to_file(mermaid_code, output_file_without_suffix):
+ """suffix: png/svg
+ :param mermaid_code: mermaid code
+ :param output_file_without_suffix: output filename without suffix
+ :return: 0 if succeed, -1 if failed
+ """
+ print('Starting mermaid_to_file command of mermaid.ink...')
+
+ encoded_string = base64.b64encode(mermaid_code.encode()).decode()
+
+ dir_name = os.path.dirname(output_file_without_suffix)
+ if dir_name and not os.path.exists(dir_name):
+ os.makedirs(dir_name)
+ with open(f"{output_file_without_suffix}.mmd", "w", encoding="utf-8") as f:
+ f.write(mermaid_code)
+
+ for suffix in ["svg", "png"]:
+ output_file = f"{output_file_without_suffix}.{suffix}"
+ path_type = "svg" if suffix == "svg" else "img"
+ url = f"https://mermaid.ink/{path_type}/{encoded_string}"
+ response = requests.get(url)
+
+ if response.status_code == 200:
+ with open(output_file, 'wb') as f:
+ f.write(response.content)
+ print(f"Generating {output_file}..")
+ else:
+ print(f"Failed to retrieve {suffix}")
+ return -1
+
+ return 0
+
+if __name__ == "__main__":
+ mermaid_to_file()
\ No newline at end of file
diff --git a/metagpt/utils/mermaid_playwright.py b/metagpt/utils/mmdc_playwright.py
similarity index 63%
rename from metagpt/utils/mermaid_playwright.py
rename to metagpt/utils/mmdc_playwright.py
index aa04e70eb..d5d6b898e 100644
--- a/metagpt/utils/mermaid_playwright.py
+++ b/metagpt/utils/mmdc_playwright.py
@@ -3,22 +3,24 @@
"""
@Time : 2023/9/4 16:12
@Author : Steven Lee
-@File : mermaid_playwright.py
+@File : mmdc_playwright.py
"""
import os
import asyncio
-from metagpt.config import CONFIG
-from metagpt.const import PROJECT_ROOT
-from metagpt.logs import logger
-
+import click
from urllib.parse import urljoin
+
from playwright.async_api import async_playwright
-import nest_asyncio
-__dirname = os.path.dirname(os.path.abspath(__file__))
+@click.command()
+@click.version_option()
+@click.option("-i","--mermaid_code", type=str, help="mermaid code")
+@click.option("-o","--output_file_without_suffix", type=str, help="output filename without suffix")
+@click.option("--width",type=int,help="width",default=2048)
+@click.option("--height",type=int,help="height",default=2048)
+def mermaid_to_file(mermaid_code, output_file_without_suffix, width, height):
-
-def mermaid_to_file(mermaid_code, output_file_without_suffix, width=2048, height=2048, output_formats=['png', 'svg', 'pdf']) -> int:
+
"""
Converts the given Mermaid code to various output formats and saves them to files.
@@ -27,18 +29,18 @@ def mermaid_to_file(mermaid_code, output_file_without_suffix, width=2048, height
output_file_without_suffix (str): The output file name without the file extension.
width (int, optional): The width of the output image in pixels. Defaults to 2048.
height (int, optional): The height of the output image in pixels. Defaults to 2048.
- output_formats (list[str], optional): The list of output formats to generate. Defaults to ['png', 'svg', 'pdf'].
Returns:
int: Returns 1 if the conversion and saving were successful, -1 otherwise.
"""
- async def mermaid_to_file0(mermaid_code, output_file_without_suffix, width=2048, height=2048, output_formats=['png', 'svg', 'pdf'])-> int:
+ __dirname = os.path.dirname(os.path.abspath(__file__))
+
+ async def mermaid_to_file0(mermaid_code, output_file_without_suffix, width=2048, height=2048, suffixes=['png', 'svg', 'pdf'])-> int:
async with async_playwright() as p:
browser = await p.chromium.launch()
device_scale_factor = 1.0
-
context = await browser.new_context(
viewport={'width': width, 'height': height},
device_scale_factor=device_scale_factor,
@@ -79,37 +81,19 @@ def mermaid_to_file(mermaid_code, output_file_without_suffix, width=2048, height
svgElement.appendChild(style);
}
- let title = null;
- let desc = null;
-
- if (svgElement.firstChild instanceof SVGTitleElement) {
- title = svgElement.firstChild.textContent;
- }
-
- for (const svgNode of svgElement.children) {
- if (svgNode instanceof SVGDescElement) {
- desc = svgNode.textContent;
- break;
- }
- }
-
- return {
- title,
- desc
- };
}''', [mermaid_code, mermaid_config, my_css, background_color])
- if 'svg' in output_formats :
+ if 'svg' in suffixes :
svg_xml = await page.evaluate('''() => {
const svg = document.querySelector('svg');
const xmlSerializer = new XMLSerializer();
return xmlSerializer.serializeToString(svg);
}''')
- # result[f'{output_file_without_suffix}.svg'] = svg_xml
+ print(f"Generating {output_file_without_suffix}.svg..")
with open(f'{output_file_without_suffix}.svg', 'wb') as f:
f.write(svg_xml.encode('utf-8'))
- if 'png' in output_formats:
+ if 'png' in suffixes:
clip = await page.evaluate('''() => {
const svg = document.querySelector('svg');
const rect = svg.getBoundingClientRect();
@@ -122,78 +106,31 @@ def mermaid_to_file(mermaid_code, output_file_without_suffix, width=2048, height
}''')
await page.set_viewport_size({'width': clip['x'] + clip['width'], 'height': clip['y'] + clip['height']})
screenshot = await page.screenshot(clip=clip, omit_background=True, scale='device')
+ print(f"Generating {output_file_without_suffix}.png..")
with open(f'{output_file_without_suffix}.png', 'wb') as f:
f.write(screenshot)
- if 'pdf' in output_formats:
+ if 'pdf' in suffixes:
pdf_data = await page.pdf(scale=device_scale_factor)
+ print(f"Generating {output_file_without_suffix}.pdf..")
with open(f'{output_file_without_suffix}.pdf', 'wb') as f:
f.write(pdf_data)
return 1
except Exception as e:
- logger.error(e)
+ print(e)
return -1
finally:
await browser.close()
+ dir_name = os.path.dirname(output_file_without_suffix)
+ if dir_name and not os.path.exists(dir_name):
+ os.makedirs(dir_name)
with open(f"{output_file_without_suffix}.mmd", "w", encoding="utf-8") as f:
f.write(mermaid_code)
- nest_asyncio.apply()
+ suffixes = ['png', 'svg', 'pdf']
loop = asyncio.new_event_loop()
- result = loop.run_until_complete(mermaid_to_file0(mermaid_code, output_file_without_suffix, width, height, output_formats))
+ result = loop.run_until_complete(mermaid_to_file0(mermaid_code, output_file_without_suffix, width, height, suffixes))
loop.close()
return result
-MMC1 = """classDiagram
- class Main {
- -SearchEngine search_engine
- +main() str
- }
- class SearchEngine {
- -Index index
- -Ranking ranking
- -Summary summary
- +search(query: str) str
- }
- class Index {
- -KnowledgeBase knowledge_base
- +create_index(data: dict)
- +query_index(query: str) list
- }
- class Ranking {
- +rank_results(results: list) list
- }
- class Summary {
- +summarize_results(results: list) str
- }
- class KnowledgeBase {
- +update(data: dict)
- +fetch_data(query: str) dict
- }
- Main --> SearchEngine
- SearchEngine --> Index
- SearchEngine --> Ranking
- SearchEngine --> Summary
- Index --> KnowledgeBase"""
-
-MMC2 = """sequenceDiagram
- participant M as Main
- participant SE as SearchEngine
- participant I as Index
- participant R as Ranking
- participant S as Summary
- participant KB as KnowledgeBase
- M->>SE: search(query)
- SE->>I: query_index(query)
- I->>KB: fetch_data(query)
- KB-->>I: return data
- I-->>SE: return results
- SE->>R: rank_results(results)
- R-->>SE: return ranked_results
- SE->>S: summarize_results(ranked_results)
- S-->>SE: return summary
- SE-->>M: return summary"""
-
-
if __name__ == "__main__":
- # logger.info(print_members(print_members))
- mermaid_to_file(MMC1, PROJECT_ROOT / "MMC1")
- mermaid_to_file(MMC2, PROJECT_ROOT / "MMC2")
+ mermaid_to_file()
+
diff --git a/metagpt/utils/mmdc_pyppeteer.py b/metagpt/utils/mmdc_pyppeteer.py
new file mode 100644
index 000000000..e6986bc76
--- /dev/null
+++ b/metagpt/utils/mmdc_pyppeteer.py
@@ -0,0 +1,129 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+@Time : 2023/9/4 16:12
+@Author : Steven Lee
+@File : mmdc_pyppeteer.py
+"""
+import asyncio
+import click
+import os
+from urllib.parse import urljoin
+import sys
+from pyppeteer import launch
+
+@click.command()
+@click.version_option()
+@click.option("-i","--mermaid_code", type=str, help="mermaid code")
+@click.option("-o","--output_file_without_suffix", type=str, help="output filename without suffix")
+@click.option("--width",type=int,help="width",default=2048)
+@click.option("--height",type=int,help="height",default=2048)
+def mermaid_to_file(mermaid_code, output_file_without_suffix, width, height):
+ """
+ Converts the given Mermaid code to various output formats and saves them to files.
+
+ Args:
+ mermaid_code (str): The Mermaid code to convert.
+ output_file_without_suffix (str): The output file name without the file extension.
+ width (int, optional): The width of the output image in pixels. Defaults to 2048.
+ height (int, optional): The height of the output image in pixels. Defaults to 2048.
+
+ Returns:
+ int: Returns 1 if the conversion and saving were successful, -1 otherwise.
+ """
+
+ async def mermaid_to_file0(mermaid_code, output_file_without_suffix, width=2048, height=2048, suffixes=['png', 'svg', 'pdf'])-> int:
+ __dirname = os.path.dirname(os.path.abspath(__file__))
+ browser = await launch(headless=True,
+ executablePath=os.getenv('PUPPETEER_EXECUTABLE_PATH',"/opt/homebrew/bin/chromium"),
+ args=['--disable-extensions',"--no-sandbox"]
+ )
+ page = await browser.newPage()
+ device_scale_factor = 1.0
+
+ async def console_message(msg):
+ print(msg.text)
+ page.on('console', console_message)
+
+ try:
+ await page.setViewport(viewport={'width': width, 'height': height, 'deviceScaleFactor': device_scale_factor})
+
+ mermaid_html_path = os.path.abspath(
+ os.path.join(__dirname, 'index.html'))
+ mermaid_html_url = urljoin('file:', mermaid_html_path)
+ await page.goto(mermaid_html_url)
+
+ await page.querySelector("div#container")
+ mermaid_config = {}
+ background_color = "#ffffff"
+ my_css = ""
+ await page.evaluate(f'document.body.style.background = "{background_color}";')
+
+ metadata = await page.evaluate('''async ([definition, mermaidConfig, myCSS, backgroundColor]) => {
+ const { mermaid, zenuml } = globalThis;
+ await mermaid.registerExternalDiagrams([zenuml]);
+ mermaid.initialize({ startOnLoad: false, ...mermaidConfig });
+ const { svg } = await mermaid.render('my-svg', definition, document.getElementById('container'));
+ document.getElementById('container').innerHTML = svg;
+ const svgElement = document.querySelector('svg');
+ svgElement.style.backgroundColor = backgroundColor;
+
+ if (myCSS) {
+ const style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
+ style.appendChild(document.createTextNode(myCSS));
+ svgElement.appendChild(style);
+ }
+ }''', [mermaid_code, mermaid_config, my_css, background_color])
+
+ if 'svg' in suffixes :
+ svg_xml = await page.evaluate('''() => {
+ const svg = document.querySelector('svg');
+ const xmlSerializer = new XMLSerializer();
+ return xmlSerializer.serializeToString(svg);
+ }''')
+ print(f"Generating {output_file_without_suffix}.svg..")
+ with open(f'{output_file_without_suffix}.svg', 'wb') as f:
+ f.write(svg_xml.encode('utf-8'))
+
+ if 'png' in suffixes:
+ clip = await page.evaluate('''() => {
+ const svg = document.querySelector('svg');
+ const rect = svg.getBoundingClientRect();
+ return {
+ x: Math.floor(rect.left),
+ y: Math.floor(rect.top),
+ width: Math.ceil(rect.width),
+ height: Math.ceil(rect.height)
+ };
+ }''')
+ await page.setViewport({'width': clip['x'] + clip['width'], 'height': clip['y'] + clip['height'], 'deviceScaleFactor': device_scale_factor})
+ screenshot = await page.screenshot(clip=clip, omit_background=True, scale='device')
+ print(f"Generating {output_file_without_suffix}.png..")
+ with open(f'{output_file_without_suffix}.png', 'wb') as f:
+ f.write(screenshot)
+ if 'pdf' in suffixes:
+ pdf_data = await page.pdf(scale=device_scale_factor)
+ print(f"Generating {output_file_without_suffix}.pdf..")
+ with open(f'{output_file_without_suffix}.pdf', 'wb') as f:
+ f.write(pdf_data)
+ return 1
+ except Exception as e:
+ print(e)
+ return -1
+ finally:
+ await browser.close()
+
+
+ suffixes = ['png', 'svg', 'pdf']
+ dir_name = os.path.dirname(output_file_without_suffix)
+ if dir_name and not os.path.exists(dir_name):
+ os.makedirs(dir_name)
+ with open(f"{output_file_without_suffix}.mmd", "w", encoding="utf-8") as f:
+ f.write(mermaid_code)
+ loop = asyncio.new_event_loop()
+ result = loop.run_until_complete(mermaid_to_file0(mermaid_code, output_file_without_suffix, width, height,suffixes))
+ loop.close()
+ return result
+
+if __name__ == "__main__":
+ mermaid_to_file()
From b2a6cacd1e295caab71d535381e3be9bb182a48c Mon Sep 17 00:00:00 2001
From: stellaHSR <34952977+stellaHSR@users.noreply.github.com>
Date: Mon, 11 Sep 2023 19:28:52 +0800
Subject: [PATCH 42/76] Delete docs/resources/20230811-214014.jpg
rm wechat img
---
docs/resources/20230811-214014.jpg | Bin 59081 -> 0 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 docs/resources/20230811-214014.jpg
diff --git a/docs/resources/20230811-214014.jpg b/docs/resources/20230811-214014.jpg
deleted file mode 100644
index 2006f2646d960305e5b41ec53404ac5d235d411c..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 59081
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