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 01/10] 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 02/10] 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 03/10] 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 04/10] 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 05/10] 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 06/10] 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 07/10] 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 a725a2398d1d77d3fa03e0a6141ca63239167ba8 Mon Sep 17 00:00:00 2001
From: stellaHSR <34952977+stellaHSR@users.noreply.github.com>
Date: Tue, 12 Sep 2023 18:07:56 +0800
Subject: [PATCH 08/10] Update README.md
update info and agentstore waitlist
---
README.md | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index b2a4f18c1..b4a272ef0 100644
--- a/README.md
+++ b/README.md
@@ -12,16 +12,17 @@ # MetaGPT: The Multi-Agent Framework
-
-
+
+