diff --git a/.gitignore b/.gitignore index 05158cca2..1613a638d 100644 --- a/.gitignore +++ b/.gitignore @@ -160,6 +160,7 @@ tmp metagpt/roles/idea_agent.py .aider* *.bak +*.bk # output folder output @@ -168,3 +169,5 @@ tmp.png tests/metagpt/utils/file_repo_git *.tmp *.png +htmlcov +htmlcov.* diff --git a/metagpt/actions/design_api.py b/metagpt/actions/design_api.py index 03f3d7704..2574550e4 100644 --- a/metagpt/actions/design_api.py +++ b/metagpt/actions/design_api.py @@ -47,10 +47,10 @@ class WriteDesign(Action): ) async def run(self, with_messages: Message, schema: str = CONFIG.prompt_schema): - # Use `git diff` to identify which PRD documents have been modified in the `docs/prds` directory. + # Use `git status` to identify which PRD documents have been modified in the `docs/prds` directory. prds_file_repo = CONFIG.git_repo.new_file_repository(PRDS_FILE_REPO) changed_prds = prds_file_repo.changed_files - # Use `git diff` to identify which design documents in the `docs/system_designs` directory have undergone + # Use `git status` to identify which design documents in the `docs/system_designs` directory have undergone # changes. system_design_file_repo = CONFIG.git_repo.new_file_repository(SYSTEM_DESIGN_FILE_REPO) changed_system_designs = system_design_file_repo.changed_files diff --git a/metagpt/memory/brain_memory.py b/metagpt/memory/brain_memory.py index fe6bf991d..ff29eaddb 100644 --- a/metagpt/memory/brain_memory.py +++ b/metagpt/memory/brain_memory.py @@ -157,7 +157,7 @@ class BrainMemory(BaseModel): if left == 0: break m.content = m.content[0:left] - msgs.append(m.model_dump()) + msgs.append(m) break msgs.append(m) total_length += delta @@ -171,8 +171,8 @@ class BrainMemory(BaseModel): @staticmethod def to_metagpt_history_format(history) -> str: - mmsg = [SimpleMessage(role=m.role, content=m.content) for m in history] - return json.dumps(mmsg) + mmsg = [SimpleMessage(role=m.role, content=m.content).model_dump() for m in history] + return json.dumps(mmsg, ensure_ascii=False) async def get_title(self, llm, max_words=5, **kwargs) -> str: """Generate text title""" diff --git a/metagpt/roles/assistant.py b/metagpt/roles/assistant.py index 89965f3bd..227578a63 100644 --- a/metagpt/roles/assistant.py +++ b/metagpt/roles/assistant.py @@ -132,8 +132,8 @@ class Assistant(Role): def get_memory(self) -> str: return self.memory.model_dump_json() - def load_memory(self, jsn): + def load_memory(self, m): try: - self.memory = BrainMemory(**jsn) + self.memory = BrainMemory(**m) except Exception as e: logger.exception(f"load error:{e}, data:{jsn}") diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index b8866e055..e05e69cbb 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -235,7 +235,9 @@ class Engineer(Role): task_doc = await task_file_repo.get(i.name) elif str(i.parent) == SYSTEM_DESIGN_FILE_REPO: design_doc = await design_file_repo.get(i.name) - # FIXME: design doc没有加载进来,是None + if not task_doc or not design_doc: + logger.error(f'Detected source code "{filename}" from an unknown origin.') + raise ValueError(f'Detected source code "{filename}" from an unknown origin.') context = CodingContext(filename=filename, design_doc=design_doc, task_doc=task_doc, code_doc=old_code_doc) return context diff --git a/metagpt/schema.py b/metagpt/schema.py index 5dde0ee46..91158ffeb 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -343,16 +343,21 @@ class MessageQueue(BaseModel): return "[]" lst = [] + msgs = [] try: while True: item = await wait_for(self._queue.get(), timeout=1.0) if item is None: break - lst.append(item.dict(exclude_none=True)) + msgs.append(item) + lst.append(item.dump()) self._queue.task_done() except asyncio.TimeoutError: logger.debug("Queue is empty, exiting...") - return json.dumps(lst) + finally: + for m in msgs: + self._queue.put_nowait(m) + return json.dumps(lst, ensure_ascii=False) @staticmethod def load(data) -> "MessageQueue": @@ -361,7 +366,7 @@ class MessageQueue(BaseModel): try: lst = json.loads(data) for i in lst: - msg = Message(**i) + msg = Message.load(i) queue.push(msg) except JSONDecodeError as e: logger.warning(f"JSON load failed: {data}, error:{e}") diff --git a/metagpt/utils/common.py b/metagpt/utils/common.py index 30c318fd5..5999b2e11 100644 --- a/metagpt/utils/common.py +++ b/metagpt/utils/common.py @@ -528,18 +528,18 @@ def role_raise_decorator(func): @handle_exception -async def aread(file_path: str) -> str: +async def aread(filename: str | Path, encoding=None) -> str: """Read file asynchronously.""" - async with aiofiles.open(str(file_path), mode="r") as reader: + async with aiofiles.open(str(filename), mode="r", encoding=encoding) as reader: content = await reader.read() return content -async def awrite(filename: str | Path, data: str): +async def awrite(filename: str | Path, data: str, encoding=None): """Write file asynchronously.""" pathname = Path(filename) pathname.parent.mkdir(parents=True, exist_ok=True) - async with aiofiles.open(str(pathname), mode="w", encoding="utf-8") as writer: + async with aiofiles.open(str(pathname), mode="w", encoding=encoding) as writer: await writer.write(data) diff --git a/metagpt/utils/pycst.py b/metagpt/utils/pycst.py index 1edfed81c..a26ba70ff 100644 --- a/metagpt/utils/pycst.py +++ b/metagpt/utils/pycst.py @@ -49,6 +49,14 @@ def get_docstring_statement(body: DocstringNode) -> cst.SimpleStatementLine: return statement +def has_decorator(node: DocstringNode, name: str) -> bool: + return hasattr(node, "decorators") and any( + (hasattr(i.decorator, "value") and i.decorator.value == name) + or (hasattr(i.decorator, "func") and hasattr(i.decorator.func, "value") and i.decorator.func.value == name) + for i in node.decorators + ) + + class DocstringCollector(cst.CSTVisitor): """A visitor class for collecting docstrings from a CST. @@ -82,7 +90,7 @@ class DocstringCollector(cst.CSTVisitor): def _leave(self, node: DocstringNode) -> None: key = tuple(self.stack) self.stack.pop() - if hasattr(node, "decorators") and any(i.decorator.value == "overload" for i in node.decorators): + if has_decorator(node, "overload"): return statement = get_docstring_statement(node) @@ -127,9 +135,7 @@ class DocstringTransformer(cst.CSTTransformer): key = tuple(self.stack) self.stack.pop() - if hasattr(updated_node, "decorators") and any( - (i.decorator.value == "overload") for i in updated_node.decorators - ): + if has_decorator(updated_node, "overload"): return updated_node statement = self.docstrings.get(key) diff --git a/tests/data/code/js/1.js b/tests/data/code/js/1.js new file mode 100644 index 000000000..042f922b3 --- /dev/null +++ b/tests/data/code/js/1.js @@ -0,0 +1,6 @@ +WRMCB=function(e){var c=console;if(c&&c.log&&c.error){c.log('Error running batched script.');c.error(e);}} +; +try { +/* module-key = 'jira.webresources:bigpipe-js', location = '/includes/jira/common/bigpipe.js' */ +define("jira/bigpipe/element",["jquery","wrm/data","jira/skate","jira/util/logger"],function(e,r,t,n){return t("big-pipe",{attached:function(i){function a(){var e=new CustomEvent("success");i.dispatchEvent(e)}function o(e,r){var t=new CustomEvent("error");t.data={event:e,signature:r},i.dispatchEvent(t)}function d(e,r){p("error"),o(e,r)}function p(e){"performance"in window&&performance.mark&&performance.mark(c+e)}var s=i.getAttribute("data-id");if(null===s)return n.error("No data-id attribute provided for tag for element:",i),void d({name:"NoPipeIdError",message:"Unable to render element. Element does not contain a pipe id.",element:i},"no.pipe.id");var c="bigPipe."+s+".";p("start");var u=r.claim(s);u?function(r){try{var o=e(r);e(i).replaceWith(o).each(function(){t.init(this)}),p("end"),a()}catch(e){n.error("Error while parsing html: "+e),d(e,"parsing")}}(u):d({name:"NoDataError",message:"BigPipe response is empty."},"no.data")},detached:function(){},type:t.type.ELEMENT,resolvedAttribute:"resolved",unresolvedAttribute:"unresolved"})}); +}catch(e){WRMCB(e)}; \ No newline at end of file diff --git a/tests/data/code/python/1.py b/tests/data/code/python/1.py new file mode 100644 index 000000000..e9aeaeeee --- /dev/null +++ b/tests/data/code/python/1.py @@ -0,0 +1,83 @@ +""" +=============== +Degree Analysis +=============== + +This example shows several ways to visualize the distribution of the degree of +nodes with two common techniques: a *degree-rank plot* and a +*degree histogram*. + +In this example, a random Graph is generated with 100 nodes. The degree of +each node is determined, and a figure is generated showing three things: +1. The subgraph of connected components +2. The degree-rank plot for the Graph, and +3. The degree histogram +""" +import matplotlib.pyplot as plt +import networkx as nx +import numpy as np + +G = nx.gnp_random_graph(100, 0.02, seed=10374196) + +degree_sequence = sorted((d for n, d in G.degree()), reverse=True) +dmax = max(degree_sequence) + +fig = plt.figure("Degree of a random graph", figsize=(8, 8)) +# Create a gridspec for adding subplots of different sizes +axgrid = fig.add_gridspec(5, 4) + +ax0 = fig.add_subplot(axgrid[0:3, :]) +Gcc = G.subgraph(sorted(nx.connected_components(G), key=len, reverse=True)[0]) +pos = nx.spring_layout(Gcc, seed=10396953) +nx.draw_networkx_nodes(Gcc, pos, ax=ax0, node_size=20) +nx.draw_networkx_edges(Gcc, pos, ax=ax0, alpha=0.4) +ax0.set_title("Connected components of G") +ax0.set_axis_off() + +print("aa") + +ax1 = fig.add_subplot(axgrid[3:, :2]) +ax1.plot(degree_sequence, "b-", marker="o") +ax1.set_title("Degree Rank Plot") +ax1.set_ylabel("Degree") +ax1.set_xlabel("Rank") + +ax2 = fig.add_subplot(axgrid[3:, 2:]) +ax2.bar(*np.unique(degree_sequence, return_counts=True)) +ax2.set_title("Degree histogram") +ax2.set_xlabel("Degree") +ax2.set_ylabel("# of Nodes") + +fig.tight_layout() +plt.show() + + +class Game: + def __init__(self): + self.snake = Snake(400, 300, 5, 0) + self.enemy = Enemy(100, 100, 3, 1) + self.power_up = PowerUp(200, 200) + + def handle_events(self): + for event in pygame.event.get(): + if event.type == pygame.QUIT: + return False + elif event.type == pygame.KEYDOWN: + if event.key == pygame.K_UP: + self.snake.change_direction(0) + elif event.key == pygame.K_DOWN: + self.snake.change_direction(1) + elif event.key == pygame.K_LEFT: + self.snake.change_direction(2) + elif event.key == pygame.K_RIGHT: + self.snake.change_direction(3) + return True + + def update(self): + self.snake.move() + self.enemy.move() + + def draw(self, screen): + self.snake.draw(screen) + self.enemy.draw(screen) + self.power_up.draw(screen) diff --git a/tests/data/demo_project/dependencies.json b/tests/data/demo_project/dependencies.json new file mode 100644 index 000000000..cfcf6c165 --- /dev/null +++ b/tests/data/demo_project/dependencies.json @@ -0,0 +1 @@ +{"docs/system_design/20231221155954.json": ["docs/prds/20231221155954.json"], "docs/tasks/20231221155954.json": ["docs/system_design/20231221155954.json"], "game_2048/game.py": ["docs/tasks/20231221155954.json", "docs/system_design/20231221155954.json"], "game_2048/main.py": ["docs/tasks/20231221155954.json", "docs/system_design/20231221155954.json"], "resources/code_summaries/20231221155954.md": ["docs/tasks/20231221155954.json", "game_2048/game.py", "docs/system_design/20231221155954.json", "game_2048/main.py"], "docs/code_summaries/20231221155954.json": ["docs/tasks/20231221155954.json", "game_2048/game.py", "docs/system_design/20231221155954.json", "game_2048/main.py"], "tests/test_main.py": ["game_2048/main.py"], "tests/test_game.py": ["game_2048/game.py"], "test_outputs/test_main.py.json": ["game_2048/main.py", "tests/test_main.py"], "test_outputs/test_game.py.json": ["game_2048/game.py", "tests/test_game.py"]} \ No newline at end of file diff --git a/tests/metagpt/actions/test_ui_design.py b/tests/metagpt/actions/test_ui_design.py deleted file mode 100644 index 83590ec7d..000000000 --- a/tests/metagpt/actions/test_ui_design.py +++ /dev/null @@ -1,189 +0,0 @@ -# -*- coding: utf-8 -*- -# @Date : 2023/7/22 02:40 -# @Author : stellahong (stellahong@deepwisdom.ai) -# -from tests.metagpt.roles.ui_role import UIDesign - -llm_resp = """ - # UI Design Description -```The user interface for the snake game will be designed in a way that is simple, clean, and intuitive. The main elements of the game such as the game grid, snake, food, score, and game over message will be clearly defined and easy to understand. The game grid will be centered on the screen with the score displayed at the top. The game controls will be intuitive and easy to use. The design will be modern and minimalist with a pleasing color scheme.``` - -## Selected Elements - -Game Grid: The game grid will be a rectangular area in the center of the screen where the game will take place. It will be defined by a border and will have a darker background color. - -Snake: The snake will be represented by a series of connected blocks that move across the grid. The color of the snake will be different from the background color to make it stand out. - -Food: The food will be represented by small objects that are a different color from the snake and the background. The food will be randomly placed on the grid. - -Score: The score will be displayed at the top of the screen. The score will increase each time the snake eats a piece of food. - -Game Over: When the game is over, a message will be displayed in the center of the screen. The player will be given the option to restart the game. - -## HTML Layout -```html - - - - - - Snake Game - - - -
Score: 0
-
- -
-
Game Over
- - -``` - -## CSS Styles (styles.css) -```css -body { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - height: 100vh; - margin: 0; - background-color: #f0f0f0; -} - -.score { - font-size: 2em; - margin-bottom: 1em; -} - -.game-grid { - width: 400px; - height: 400px; - display: grid; - grid-template-columns: repeat(20, 1fr); - grid-template-rows: repeat(20, 1fr); - gap: 1px; - background-color: #222; - border: 1px solid #555; -} - -.snake-segment { - background-color: #00cc66; -} - -.food { - background-color: #cc3300; -} - -.control-panel { - display: flex; - justify-content: space-around; - width: 400px; - margin-top: 1em; -} - -.control-button { - padding: 1em; - font-size: 1em; - border: none; - background-color: #555; - color: #fff; - cursor: pointer; -} - -.game-over { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - font-size: 3em; - """ - - -def test_ui_design_parse_css(): - ui_design_work = UIDesign(name="UI design action") - - css = """ - body { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - height: 100vh; - margin: 0; - background-color: #f0f0f0; -} - -.score { - font-size: 2em; - margin-bottom: 1em; -} - -.game-grid { - width: 400px; - height: 400px; - display: grid; - grid-template-columns: repeat(20, 1fr); - grid-template-rows: repeat(20, 1fr); - gap: 1px; - background-color: #222; - border: 1px solid #555; -} - -.snake-segment { - background-color: #00cc66; -} - -.food { - background-color: #cc3300; -} - -.control-panel { - display: flex; - justify-content: space-around; - width: 400px; - margin-top: 1em; -} - -.control-button { - padding: 1em; - font-size: 1em; - border: none; - background-color: #555; - color: #fff; - cursor: pointer; -} - -.game-over { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - font-size: 3em; - """ - assert ui_design_work.parse_css_code(context=llm_resp) == css - - -def test_ui_design_parse_html(): - ui_design_work = UIDesign(name="UI design action") - - html = """ - - - - - - Snake Game - - - -
Score: 0
-
- -
-
Game Over
- - - """ - assert ui_design_work.parse_css_code(context=llm_resp) == html diff --git a/tests/metagpt/learn/test_text_to_embedding.py b/tests/metagpt/learn/test_text_to_embedding.py index f9ad20ee7..cbd1bbbbc 100644 --- a/tests/metagpt/learn/test_text_to_embedding.py +++ b/tests/metagpt/learn/test_text_to_embedding.py @@ -7,30 +7,20 @@ @Desc : Unit tests. """ -import asyncio - -from pydantic import BaseModel +import pytest +from metagpt.config import CONFIG from metagpt.learn.text_to_embedding import text_to_embedding -async def mock_text_to_embedding(): - class Input(BaseModel): - input: str +@pytest.mark.asyncio +async def test_text_to_embedding(): + # Prerequisites + assert CONFIG.OPENAI_API_KEY - inputs = [{"input": "Panda emoji"}] - - for i in inputs: - seed = Input(**i) - v = await text_to_embedding(seed.input) - assert len(v.data) > 0 - - -def test_suite(): - loop = asyncio.get_event_loop() - task = loop.create_task(mock_text_to_embedding()) - loop.run_until_complete(task) + v = await text_to_embedding(text="Panda emoji") + assert len(v.data) > 0 if __name__ == "__main__": - test_suite() + pytest.main([__file__, "-s"]) diff --git a/tests/metagpt/learn/test_text_to_image.py b/tests/metagpt/learn/test_text_to_image.py index 626945218..0afe8534d 100644 --- a/tests/metagpt/learn/test_text_to_image.py +++ b/tests/metagpt/learn/test_text_to_image.py @@ -24,9 +24,11 @@ async def test(): assert "base64" in data or "http" in data key = CONFIG.METAGPT_TEXT_TO_IMAGE_MODEL_URL CONFIG.METAGPT_TEXT_TO_IMAGE_MODEL_URL = None - data = await text_to_image("Panda emoji", size_type="512x512") - assert "base64" in data or "http" in data - CONFIG.METAGPT_TEXT_TO_IMAGE_MODEL_URL = key + try: + data = await text_to_image("Panda emoji", size_type="512x512") + assert "base64" in data or "http" in data + finally: + CONFIG.METAGPT_TEXT_TO_IMAGE_MODEL_URL = key if __name__ == "__main__": diff --git a/tests/metagpt/learn/test_text_to_speech.py b/tests/metagpt/learn/test_text_to_speech.py index 2e2f223dc..02faecdde 100644 --- a/tests/metagpt/learn/test_text_to_speech.py +++ b/tests/metagpt/learn/test_text_to_speech.py @@ -29,9 +29,11 @@ async def test_text_to_speech(): # test iflytek key = CONFIG.AZURE_TTS_SUBSCRIPTION_KEY CONFIG.AZURE_TTS_SUBSCRIPTION_KEY = "" - data = await text_to_speech("panda emoji") - assert "base64" in data or "http" in data - CONFIG.AZURE_TTS_SUBSCRIPTION_KEY = key + try: + data = await text_to_speech("panda emoji") + assert "base64" in data or "http" in data + finally: + CONFIG.AZURE_TTS_SUBSCRIPTION_KEY = key if __name__ == "__main__": diff --git a/tests/metagpt/memory/test_brain_memory.py b/tests/metagpt/memory/test_brain_memory.py index d52372814..32dcd672a 100644 --- a/tests/metagpt/memory/test_brain_memory.py +++ b/tests/metagpt/memory/test_brain_memory.py @@ -58,6 +58,9 @@ async def test_memory_llm(llm): res = await memory.rewrite(sentence="apple Lily eating", context="", llm=llm) assert "Lily" in res + res = await memory.summarize(llm=llm) + assert res + res = await memory.get_title(llm=llm) assert res assert "Lily" in res diff --git a/tests/metagpt/memory/test_longterm_memory.py b/tests/metagpt/memory/test_longterm_memory.py index c915a6610..0f7a4fac4 100644 --- a/tests/metagpt/memory/test_longterm_memory.py +++ b/tests/metagpt/memory/test_longterm_memory.py @@ -7,6 +7,8 @@ import os +import pytest + from metagpt.actions import UserRequirement from metagpt.config import CONFIG from metagpt.memory.longterm_memory import LongTermMemory @@ -63,3 +65,7 @@ def test_ltm_search(): assert len(news) == 1 ltm_new.clear() + + +if __name__ == "__main__": + pytest.main([__file__, "-s"]) diff --git a/tests/metagpt/roles/mock.py b/tests/metagpt/roles/mock.py index 2ea036bb7..f72ac484e 100644 --- a/tests/metagpt/roles/mock.py +++ b/tests/metagpt/roles/mock.py @@ -5,6 +5,8 @@ @Author : alexanderwu @File : mock_markdown.py """ +import json + from metagpt.actions import UserRequirement, WriteDesign, WritePRD, WriteTasks from metagpt.schema import Message @@ -151,6 +153,32 @@ sequenceDiagram ``` """ +JSON_TASKS = { + "Logic Analysis": """ + 在这个项目中,所有的模块都依赖于“SearchEngine”类,这是主入口,其他的模块(Index、Ranking和Summary)都通过它交互。另外,"Index"类又依赖于"KnowledgeBase"类,因为它需要从知识库中获取数据。 + +- "main.py"包含"Main"类,是程序的入口点,它调用"SearchEngine"进行搜索操作,所以在其他任何模块之前,"SearchEngine"必须首先被定义。 +- "search.py"定义了"SearchEngine"类,它依赖于"Index"、"Ranking"和"Summary",因此,这些模块需要在"search.py"之前定义。 +- "index.py"定义了"Index"类,它从"knowledge_base.py"获取数据来创建索引,所以"knowledge_base.py"需要在"index.py"之前定义。 +- "ranking.py"和"summary.py"相对独立,只需确保在"search.py"之前定义。 +- "knowledge_base.py"是独立的模块,可以优先开发。 +- "interface.py"、"user_feedback.py"、"security.py"、"testing.py"和"monitoring.py"看起来像是功能辅助模块,可以在主要功能模块开发完成后并行开发。 + """, + "Task list": [ + "smart_search_engine/knowledge_base.py", + "smart_search_engine/index.py", + "smart_search_engine/ranking.py", + "smart_search_engine/summary.py", + "smart_search_engine/search.py", + "smart_search_engine/main.py", + "smart_search_engine/interface.py", + "smart_search_engine/user_feedback.py", + "smart_search_engine/security.py", + "smart_search_engine/testing.py", + "smart_search_engine/monitoring.py", + ], +} + TASKS = """## Logic Analysis @@ -256,3 +284,4 @@ class MockMessages: prd = Message(role="Product Manager", content=PRD, cause_by=WritePRD) system_design = Message(role="Architect", content=SYSTEM_DESIGN, cause_by=WriteDesign) tasks = Message(role="Project Manager", content=TASKS, cause_by=WriteTasks) + json_tasks = Message(role="Project Manager", content=json.dumps(JSON_TASKS), cause_by=WriteTasks) diff --git a/tests/metagpt/roles/test_assistant.py b/tests/metagpt/roles/test_assistant.py index 4d426ff45..b516fd211 100644 --- a/tests/metagpt/roles/test_assistant.py +++ b/tests/metagpt/roles/test_assistant.py @@ -6,6 +6,7 @@ @File : test_asssistant.py @Desc : Used by AgentStore. """ + import pytest from pydantic import BaseModel @@ -90,10 +91,42 @@ async def test_run(): assert msg assert msg.cause_by == seed.cause_by assert msg.content - # # Retrieve user terminal input. - # logger.info("Enter prompt") - # talk = input("You: ") - # await role.talk(talk) + + +@pytest.mark.parametrize( + "memory", + [ + { + "history": [ + { + "content": "can you draw me an picture?", + "role": "user", + "id": "1", + }, + {"content": "Yes, of course. What do you want me to draw", "role": "assistant"}, + ], + "knowledge": [{"content": "tulin is a scientist."}], + "last_talk": "Draw me an apple.", + } + ], +) +@pytest.mark.asyncio +async def test_memory(memory): + role = Assistant() + role.load_memory(memory) + + val = role.get_memory() + assert val + + await role.talk("draw apple") + + agent_skills = CONFIG.agent_skills + CONFIG.agent_skills = [] + try: + await role.think() + finally: + CONFIG.agent_skills = agent_skills + assert isinstance(role.rc.todo, TalkAction) if __name__ == "__main__": diff --git a/tests/metagpt/roles/test_engineer.py b/tests/metagpt/roles/test_engineer.py index 6e7bc49ea..d03aea0a6 100644 --- a/tests/metagpt/roles/test_engineer.py +++ b/tests/metagpt/roles/test_engineer.py @@ -7,30 +7,51 @@ @Modified By: mashenquan, 2023-11-1. In accordance with Chapter 2.2.1 and 2.2.2 of RFC 116, utilize the new message distribution feature for message handling. """ +import json +from pathlib import Path + import pytest +from metagpt.actions import WriteCode, WriteTasks +from metagpt.config import CONFIG +from metagpt.const import ( + PRDS_FILE_REPO, + REQUIREMENT_FILENAME, + SYSTEM_DESIGN_FILE_REPO, + TASK_FILE_REPO, +) from metagpt.logs import logger from metagpt.roles.engineer import Engineer -from metagpt.utils.common import CodeParser +from metagpt.schema import CodingContext, Message +from metagpt.utils.common import CodeParser, any_to_name, any_to_str, aread, awrite +from metagpt.utils.file_repository import FileRepository +from metagpt.utils.git_repository import ChangeType from tests.metagpt.roles.mock import STRS_FOR_PARSING, TASKS, MockMessages @pytest.mark.asyncio async def test_engineer(): - engineer = Engineer() + # Prerequisites + rqno = "20231221155954.json" + await FileRepository.save_file(REQUIREMENT_FILENAME, content=MockMessages.req.content) + await FileRepository.save_file(rqno, relative_path=PRDS_FILE_REPO, content=MockMessages.prd.content) + await FileRepository.save_file( + rqno, relative_path=SYSTEM_DESIGN_FILE_REPO, content=MockMessages.system_design.content + ) + await FileRepository.save_file(rqno, relative_path=TASK_FILE_REPO, content=MockMessages.json_tasks.content) - engineer.put_message(MockMessages.req) - engineer.put_message(MockMessages.prd) - engineer.put_message(MockMessages.system_design) - rsp = await engineer.run(MockMessages.tasks) + engineer = Engineer() + rsp = await engineer.run(Message(content="", cause_by=WriteTasks)) logger.info(rsp) - assert "all done." == rsp.content + assert rsp.cause_by == any_to_str(WriteCode) + src_file_repo = CONFIG.git_repo.new_file_repository(CONFIG.src_workspace) + assert src_file_repo.changed_files def test_parse_str(): for idx, i in enumerate(STRS_FOR_PARSING): - text = CodeParser.parse_str(f"{idx+1}", i) + text = CodeParser.parse_str(f"{idx + 1}", i) # logger.info(text) assert text == "a" @@ -84,3 +105,59 @@ def test_parse_code(): logger.info(code) assert isinstance(code, str) assert target_code == code + + +def test_todo(): + role = Engineer() + assert role.todo == any_to_name(WriteCode) + + +@pytest.mark.asyncio +async def test_new_coding_context(): + # Prerequisites + demo_path = Path(__file__).parent / "../../data/demo_project" + deps = json.loads(await aread(demo_path / "dependencies.json")) + dependency = await CONFIG.git_repo.get_dependency() + for k, v in deps.items(): + await dependency.update(k, set(v)) + data = await aread(demo_path / "system_design.json") + rqno = "20231221155954.json" + await awrite(CONFIG.git_repo.workdir / SYSTEM_DESIGN_FILE_REPO / rqno, data) + data = await aread(demo_path / "tasks.json") + await awrite(CONFIG.git_repo.workdir / TASK_FILE_REPO / rqno, data) + + CONFIG.src_workspace = Path(CONFIG.git_repo.workdir) / "game_2048" + src_file_repo = CONFIG.git_repo.new_file_repository(relative_path=CONFIG.src_workspace) + task_file_repo = CONFIG.git_repo.new_file_repository(relative_path=TASK_FILE_REPO) + design_file_repo = CONFIG.git_repo.new_file_repository(relative_path=SYSTEM_DESIGN_FILE_REPO) + + filename = "game.py" + ctx_doc = await Engineer._new_coding_doc( + filename=filename, + src_file_repo=src_file_repo, + task_file_repo=task_file_repo, + design_file_repo=design_file_repo, + dependency=dependency, + ) + assert ctx_doc + assert ctx_doc.filename == filename + assert ctx_doc.content + ctx = CodingContext.model_validate_json(ctx_doc.content) + assert ctx.filename == filename + assert ctx.design_doc + assert ctx.design_doc.content + assert ctx.task_doc + assert ctx.task_doc.content + assert ctx.code_doc + + CONFIG.git_repo.add_change({f"{TASK_FILE_REPO}/{rqno}": ChangeType.UNTRACTED}) + CONFIG.git_repo.commit("mock env") + await src_file_repo.save(filename=filename, content="content") + role = Engineer() + assert not role.code_todos + await role._new_code_actions() + assert role.code_todos + + +if __name__ == "__main__": + pytest.main([__file__, "-s"]) diff --git a/tests/metagpt/roles/test_researcher.py b/tests/metagpt/roles/test_researcher.py index a1d731d0c..891befa38 100644 --- a/tests/metagpt/roles/test_researcher.py +++ b/tests/metagpt/roles/test_researcher.py @@ -48,3 +48,7 @@ def test_write_report(mocker): content = "# Research Report" researcher.Researcher().write_report(topic, content) assert (researcher.RESEARCH_PATH / f"{i+1}. metagpt.md").read_text().startswith("# Research Report") + + +if __name__ == "__main__": + pytest.main([__file__, "-s"]) diff --git a/tests/metagpt/roles/test_role.py b/tests/metagpt/roles/test_role.py index d45b6bd8d..b3b54455e 100644 --- a/tests/metagpt/roles/test_role.py +++ b/tests/metagpt/roles/test_role.py @@ -1,6 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Desc : unittest of Role +import pytest from metagpt.roles.role import Role @@ -9,3 +10,7 @@ def test_role_desc(): role = Role(profile="Sales", desc="Best Seller") assert role.profile == "Sales" assert role.desc == "Best Seller" + + +if __name__ == "__main__": + pytest.main([__file__, "-s"]) diff --git a/tests/metagpt/roles/test_ui.py b/tests/metagpt/roles/test_ui.py deleted file mode 100644 index 2038a1aee..000000000 --- a/tests/metagpt/roles/test_ui.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -# @Date : 2023/7/22 02:40 -# @Author : stellahong (stellahong@deepwisdom.ai) -# -from metagpt.roles import ProductManager -from metagpt.team import Team -from tests.metagpt.roles.ui_role import UI - - -def test_add_ui(): - ui = UI() - assert ui.profile == "UI Design" - - -async def test_ui_role(idea: str, investment: float = 3.0, n_round: int = 5): - """Run a startup. Be a boss.""" - company = Team() - company.hire([ProductManager(), UI()]) - company.invest(investment) - company.run_project(idea) - await company.run(n_round=n_round) diff --git a/tests/metagpt/roles/ui_role.py b/tests/metagpt/roles/ui_role.py deleted file mode 100644 index 51b346821..000000000 --- a/tests/metagpt/roles/ui_role.py +++ /dev/null @@ -1,280 +0,0 @@ -# -*- coding: utf-8 -*- -# @Date : 2023/7/15 16:40 -# @Author : stellahong (stellahong@deepwisdom.ai) -# @Desc : -import os -import re -from functools import wraps -from importlib import import_module - -from metagpt.actions import Action, ActionOutput, WritePRD -from metagpt.actions.action_node import ActionNode -from metagpt.config import CONFIG -from metagpt.logs import logger -from metagpt.roles import Role -from metagpt.schema import Message -from metagpt.tools.sd_engine import SDEngine - -PROMPT_TEMPLATE = """ -{context} - -## Role -You are a UserInterface Designer; the goal is to finish a UI design according to PRD, give a design description, and select specified elements and UI style. -""" - -UI_DESIGN_DESC = ActionNode( - key="UI Design Desc", - expected_type=str, - instruction="place the design objective here", - example="Snake games are classic and addictive games with simple yet engaging elements. Here are the main elements" - " commonly found in snake games", -) - -SELECTED_ELEMENTS = ActionNode( - key="Selected Elements", - expected_type=list[str], - instruction="up to 5 specified elements, clear and simple", - example=[ - "Game Grid: The game grid is a rectangular...", - "Snake: The player controls a snake that moves across the grid...", - "Food: Food items (often represented as small objects or differently colored blocks)", - "Score: The player's score increases each time the snake eats a piece of food. The longer the snake becomes, the higher the score.", - "Game Over: The game ends when the snake collides with itself or an obstacle. At this point, the player's final score is displayed, and they are given the option to restart the game.", - ], -) - -HTML_LAYOUT = ActionNode( - key="HTML Layout", - expected_type=str, - instruction="use standard HTML code", - example=""" - - - - - Snake Game - - - -
- -
-
- -
- - -""", -) - -CSS_STYLES = ActionNode( - key="CSS Styles", - expected_type=str, - instruction="use standard css code", - example="""body { - display: flex; - justify-content: center; - align-items: center; - height: 100vh; - margin: 0; - background-color: #f0f0f0; -} - -.game-grid { - width: 400px; - height: 400px; - display: grid; - grid-template-columns: repeat(20, 1fr); /* Adjust to the desired grid size */ - grid-template-rows: repeat(20, 1fr); - gap: 1px; - background-color: #222; - border: 1px solid #555; -} - -.game-grid div { - width: 100%; - height: 100%; - background-color: #444; -} - -.snake-segment { - background-color: #00cc66; /* Snake color */ -} - -.food { - width: 100%; - height: 100%; - background-color: #cc3300; /* Food color */ - position: absolute; -} - -/* Optional styles for a simple game over message */ -.game-over { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - font-size: 24px; - font-weight: bold; - color: #ff0000; - display: none; -} -""", -) - -ANYTHING_UNCLEAR = ActionNode( - key="Anything UNCLEAR", - expected_type=str, - instruction="Mention any aspects of the project that are unclear and try to clarify them.", - example="...", -) - -NODES = [ - UI_DESIGN_DESC, - SELECTED_ELEMENTS, - HTML_LAYOUT, - CSS_STYLES, - ANYTHING_UNCLEAR, -] - -UI_DESIGN_NODE = ActionNode.from_children("UI_DESIGN", NODES) - - -def load_engine(func): - """Decorator to load an engine by file name and engine name.""" - - @wraps(func) - def wrapper(*args, **kwargs): - file_name, engine_name = func(*args, **kwargs) - engine_file = import_module(file_name, package="metagpt") - ip_module_cls = getattr(engine_file, engine_name) - try: - engine = ip_module_cls() - except: - engine = None - - return engine - - return wrapper - - -def parse(func): - """Decorator to parse information using regex pattern.""" - - @wraps(func) - def wrapper(*args, **kwargs): - context, pattern = func(*args, **kwargs) - match = re.search(pattern, context, re.DOTALL) - if match: - text_info = match.group(1) - logger.info(text_info) - else: - text_info = context - logger.info("未找到匹配的内容") - - return text_info - - return wrapper - - -class UIDesign(Action): - """Class representing the UI Design action.""" - - def __init__(self, name, context=None, llm=None): - super().__init__(name, context, llm) # 需要调用LLM进一步丰富UI设计的prompt - - @parse - def parse_requirement(self, context: str): - """Parse UI Design draft from the context using regex.""" - pattern = r"## UI Design draft.*?\n(.*?)## Anything UNCLEAR" - return context, pattern - - @parse - def parse_ui_elements(self, context: str): - """Parse Selected Elements from the context using regex.""" - pattern = r"## Selected Elements.*?\n(.*?)## HTML Layout" - return context, pattern - - @parse - def parse_css_code(self, context: str): - pattern = r"```css.*?\n(.*?)## Anything UNCLEAR" - return context, pattern - - @parse - def parse_html_code(self, context: str): - pattern = r"```html.*?\n(.*?)```" - return context, pattern - - async def draw_icons(self, context, *args, **kwargs): - """Draw icons using SDEngine.""" - engine = SDEngine() - icon_prompts = self.parse_ui_elements(context) - icons = icon_prompts.split("\n") - icons = [s for s in icons if len(s.strip()) > 0] - prompts_batch = [] - for icon_prompt in icons: - # fixme: 添加icon lora - prompt = engine.construct_payload(icon_prompt + ".") - prompts_batch.append(prompt) - await engine.run_t2i(prompts_batch) - logger.info("Finish icon design using StableDiffusion API") - - async def _save(self, css_content, html_content): - save_dir = CONFIG.workspace_path / "resources" / "codes" - if not os.path.exists(save_dir): - os.makedirs(save_dir, exist_ok=True) - # Save CSS and HTML content to files - css_file_path = save_dir / "ui_design.css" - html_file_path = save_dir / "ui_design.html" - - css_file_path.write_text(css_content) - html_file_path.write_text(html_content) - - async def run(self, requirements: list[Message], *args, **kwargs) -> ActionOutput: - """Run the UI Design action.""" - # fixme: update prompt (根据需求细化prompt) - context = requirements[-1].content - ui_design_draft = self.parse_requirement(context=context) - # todo: parse requirements str - prompt = PROMPT_TEMPLATE.format(context=ui_design_draft) - logger.info(prompt) - ui_describe = await UI_DESIGN_NODE.fill(prompt) - logger.info(ui_describe.content) - logger.info(ui_describe.instruct_content) - css = self.parse_css_code(context=ui_describe.content) - html = self.parse_html_code(context=ui_describe.content) - await self._save(css_content=css, html_content=html) - await self.draw_icons(ui_describe.content) - return ui_describe - - -class UI(Role): - """Class representing the UI Role.""" - - def __init__( - self, - name="Catherine", - profile="UI Design", - goal="Finish a workable and good User Interface design based on a product design", - constraints="Give clear layout description and use standard icons to finish the design", - skills=["SD"], - ): - super().__init__(name, profile, goal, constraints) - self.load_skills(skills) - self._init_actions([UIDesign]) - self._watch([WritePRD]) - - @load_engine - def load_sd_engine(self): - """Load the SDEngine.""" - file_name = ".tools.sd_engine" - engine_name = "SDEngine" - return file_name, engine_name - - def load_skills(self, skills): - """Load skills for the UI Role.""" - # todo: 添加其他出图engine - for skill in skills: - if skill == "SD": - self.sd_engine = self.load_sd_engine() - logger.info(f"load skill engine {self.sd_engine}") diff --git a/tests/metagpt/test_schema.py b/tests/metagpt/test_schema.py index a6316733a..1bf0d4c4c 100644 --- a/tests/metagpt/test_schema.py +++ b/tests/metagpt/test_schema.py @@ -10,10 +10,20 @@ import json +import pytest + from metagpt.actions import Action from metagpt.actions.action_node import ActionNode from metagpt.actions.write_code import WriteCode -from metagpt.schema import AIMessage, Message, SystemMessage, UserMessage +from metagpt.config import CONFIG +from metagpt.schema import ( + AIMessage, + Document, + Message, + MessageQueue, + SystemMessage, + UserMessage, +) from metagpt.utils.common import any_to_str @@ -95,3 +105,32 @@ def test_message_serdeser(): new_message = Message(**message_dict) assert new_message.instruct_content is None assert new_message.cause_by == "metagpt.actions.add_requirement.UserRequirement" + assert not Message.load("{") + + +def test_document(): + doc = Document(root_path="a", filename="b", content="c") + meta_doc = doc.get_meta() + assert doc.root_path == meta_doc.root_path + assert doc.filename == meta_doc.filename + assert meta_doc.content == "" + + assert doc.full_path == str(CONFIG.git_repo.workdir / doc.root_path / doc.filename) + + +@pytest.mark.asyncio +async def test_message_queue(): + mq = MessageQueue() + mq.push(Message(content="1")) + mq.push(Message(content="2中文测试aaa")) + msg = mq.pop() + assert msg.content == "1" + + val = await mq.dump() + assert val + new_mq = MessageQueue.load(val) + assert new_mq.pop_all() == mq.pop_all() + + +if __name__ == "__main__": + pytest.main([__file__, "-s"]) diff --git a/tests/metagpt/utils/test_dependency_file.py b/tests/metagpt/utils/test_dependency_file.py index 0ff5e97b0..c863f29b5 100644 --- a/tests/metagpt/utils/test_dependency_file.py +++ b/tests/metagpt/utils/test_dependency_file.py @@ -53,7 +53,8 @@ async def test_dependency_file(): file1 = DependencyFile(workdir=Path(__file__).parent) assert file1.exists - assert await file1.get("a/b.txt") == set() + assert await file1.get("a/b.txt", persist=False) == set() + assert await file1.get("a/b.txt") == {"c/e.txt", "d.txt"} await file1.load() assert await file1.get("a/b.txt") == {"c/e.txt", "d.txt"} file1.delete_file() diff --git a/tests/metagpt/utils/test_file.py b/tests/metagpt/utils/test_file.py index 4a8c743cf..4cd89e03c 100644 --- a/tests/metagpt/utils/test_file.py +++ b/tests/metagpt/utils/test_file.py @@ -15,7 +15,13 @@ 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!")], + [ + ( + Path(__file__).parent / "../../../workspace/unittest/data/tutorial_docx/2023-09-07_17-05-20", + "test.md", + "Hello World!", + ) + ], ) async def test_write_and_read_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")) diff --git a/tests/metagpt/utils/test_s3.py b/tests/metagpt/utils/test_s3.py index 0a654f2da..edf198028 100644 --- a/tests/metagpt/utils/test_s3.py +++ b/tests/metagpt/utils/test_s3.py @@ -47,9 +47,11 @@ async def test_s3_no_error(): conn = S3() key = conn.auth_config["aws_secret_access_key"] conn.auth_config["aws_secret_access_key"] = "" - res = await conn.cache("ABC", ".bak", "script") - assert not res - conn.auth_config["aws_secret_access_key"] = key + try: + res = await conn.cache("ABC", ".bak", "script") + assert not res + finally: + conn.auth_config["aws_secret_access_key"] = key if __name__ == "__main__":