From e6c33f199c315518a6e46496dbc177275a97df78 Mon Sep 17 00:00:00 2001
From: AK
Date: Sun, 6 Aug 2023 22:02:15 +0800
Subject: [PATCH 01/68] 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/68] 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/68] 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 a386c7e974cc35e9bfd80d9fe680a1c4c313d43b Mon Sep 17 00:00:00 2001
From: "hy.li"
Date: Mon, 4 Sep 2023 23:30:27 +0800
Subject: [PATCH 04/68] 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 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 05/68] 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 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 06/68] 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 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 07/68] 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 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 08/68] 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 09/68] 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 10/68] 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 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 11/68] 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 12/68] 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 13/68] 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 14/68] 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 15/68] 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 16/68] 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 17/68] 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 18/68] 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 19/68] 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 20/68] 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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