From c2ad5a503693640026deedb004ceb4fdeec54364 Mon Sep 17 00:00:00 2001 From: femto Date: Thu, 7 Sep 2023 16:16:11 +0800 Subject: [PATCH 01/22] write_prd_json --- .aider.chat.history.md | 1634 +++++++++++++++++++++++++++++ .aider.input.history | 112 ++ .gitignore | 1 + .python-version | 1 + metagpt/actions/__init__.py | 2 + metagpt/actions/action.py | 25 + metagpt/actions/write_prd_json.py | 133 +++ metagpt/roles/product_manager.py | 4 +- requirements.txt | 3 +- 9 files changed, 1912 insertions(+), 3 deletions(-) create mode 100644 .aider.chat.history.md create mode 100644 .aider.input.history create mode 100644 .python-version create mode 100644 metagpt/actions/write_prd_json.py diff --git a/.aider.chat.history.md b/.aider.chat.history.md new file mode 100644 index 000000000..9c7f94321 --- /dev/null +++ b/.aider.chat.history.md @@ -0,0 +1,1634 @@ + +# aider chat started at 2023-08-01 17:17:19 + +> Aider v0.10.1 +> Add .aider* to .gitignore (recommended)? y +> Added .aider* to .gitignore +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider --no-auto-commit +> No OpenAI API key provided. Use --openai-api-key or export OPENAI_API_KEY. + +# aider chat started at 2023-08-01 17:17:41 + +> Aider v0.10.1 +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider --no-auto-commit +> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k +> Model: gpt-3.5-turbo-16k +> Git repo: ../../.git +> Repo-map: disabled +> Git repo has uncommitted changes. +> Commit before the chat proceeds [y/n/commit message]? Added .aider* to .gitignore.y + +# aider chat started at 2023-08-01 17:18:15 + +> Aider v0.10.1 +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider --no-auto-commit --no-git +> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k +> Model: gpt-3.5-turbo-16k +> Git repo: none +> Repo-map: disabled +> Use /help to see in-chat commands, run with --help to see cmd line args + +#### /add pingpong_game/*.py +> Added pingpong_game/ball.py to the chat +> Added pingpong_game/settings.py to the chat +> Added pingpong_game/main.py to the chat +> Added pingpong_game/menu.py to the chat +> Added pingpong_game/game.py to the chat +> Added pingpong_game/paddle.py to the chat +> Added pingpong_game/leaderboard.py to the chat + +#### Traceback (most recent call last): +#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 36, in +#### main() +#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 28, in main +#### game.update() +#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/game.py", line 39, in update +#### self.player_paddle.update() +#### AttributeError: 'Paddle' object has no attribute 'update' , could you help me with that? + +Based on the error message, it seems that the `Paddle` class does not have an `update` method. To fix this issue, you need to add an `update` method to the `Paddle` class. + +Here's the updated code for the `Paddle` class: + +pingpong_game/paddle.py +``` +import pygame + +class Paddle: + def __init__(self): + self.x = 0 + self.y = 0 + self.width = 0 + self.height = 0 + self.speed = 0 + + def move_up(self): + """ + Move the paddle up. + """ + self.y -= self.speed + + def move_down(self): + """ + Move the paddle down. + """ + self.y += self.speed + + def update(self): + """ + Update the paddle position. + """ + pass + + def draw(self, screen): + """ + Draw the paddle on the screen. + """ + pygame.draw.rect(screen, (255, 255, 255), (self.x, self.y, self.width, self.height)) +``` + +Now the `Paddle` class has an empty `update` method, which can be called without causing an `AttributeError`. + +> Applied edit to pingpong_game/paddle.py + +#### but update is empty, could you give a meaningful implementation? + +Certainly! Here's an updated implementation for the `update` method in the `Paddle` class: + +pingpong_game/paddle.py +``` +import pygame + +class Paddle: + def __init__(self): + self.x = 0 + self.y = 0 + self.width = 0 + self.height = 0 + self.speed = 0 + + def move_up(self): + """ + Move the paddle up. + """ + self.y -= self.speed + + def move_down(self): + """ + Move the paddle down. + """ + self.y += self.speed + + def update(self): + """ + Update the paddle position. + """ + keys = pygame.key.get_pressed() + if keys[pygame.K_UP]: + self.move_up() + if keys[pygame.K_DOWN]: + self.move_down() + + def draw(self, screen): + """ + Draw the paddle on the screen. + """ + pygame.draw.rect(screen, (255, 255, 255), (self.x, self.y, self.width, self.height)) +``` + +In this updated implementation, the `update` method checks for key presses and moves the paddle accordingly. It uses the `pygame.key.get_pressed()` function to get the state of all keyboard keys, and if the up or down arrow key is pressed, it calls the `move_up` or `move_down` method respectively. + +Please replace the content of the `pingpong_game/paddle.py` file with the updated code above. + +> Applied edit to pingpong_game/paddle.py + +#### Traceback (most recent call last): +#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 36, in +#### main() +#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 29, in main +#### game.draw(screen) +#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/game.py", line 58, in draw +#### self.menu.draw(screen, self.score, self.level) +#### AttributeError: 'Menu' object has no attribute 'draw' + +The error is occurring because the `Menu` class does not have a `draw` method. To fix this, you can add a `draw` method to the `Menu` class that takes the `screen`, `score`, and `level` as parameters and draws the menu on the screen. + +Here's the updated code for the `Menu` class: + +```python +import pygame + +class Menu: + def __init__(self): + self.font = pygame.font.Font(None, 36) + self.start_button = Button("Start Game", (400, 200)) + self.settings_button = Button("Settings", (400, 300)) + self.leaderboard_button = Button("Leaderboard", (400, 400)) + self.quit_button = Button("Quit", (400, 500)) + + def show_menu(self, screen): + """ + Show the main menu screen. + """ + screen.fill((0, 0, 0)) + self.start_button.draw(screen) + self.settings_button.draw(screen) + self.leaderboard_button.draw(screen) + self.quit_button.draw(screen) + + pygame.display.update() + + def handle_event(self, event): + """ + Handle menu events. + """ + if event.type == pygame.MOUSEBUTTONDOWN: + if self.start_button.is_clicked(event.pos): + self.start_game() + elif self.settings_button.is_clicked(event.pos): + self.show_settings() + elif self.leaderboard_button.is_clicked(event.pos): + self.show_leaderboard() + elif self.quit_button.is_clicked(event.pos): + self.quit_game() + + def start_game(self): + """ + Start a new game. + """ + game.start_game() + + def show_settings(self): + """ + Show the settings screen. + """ + settings.show_settings() + + def show_leaderboard(self): + """ + Show the leaderboard screen. + """ + leaderboard.show_leaderboard() + + def quit_game(self): + """ + Quit the game. + """ + pygame.quit() + +class Button: + def __init__(self, text, position): + self.text = text + self.position = position + self.rect = pygame.Rect(position[0], position[1], 200, 50) + + def draw(self, screen): + """ + Draw the button on the screen. + """ + pygame.draw.rect(screen, (255, 255, 255), self.rect) + text_surface = self.font.render(self.text, True, (0, 0, 0)) + text_rect = text_surface.get_rect(center=self.rect.center) + screen.blit(text_surface, text_rect) + + def is_clicked(self, pos): + """ + Check if the button is clicked. + """ + return self.rect.collidepoint(pos) +``` + +Please replace the content of the `pingpong_game/menu.py` file with the updated code above. + +> Malformed response #1, retrying... +> No filename provided before ``` in file listing + +Apologies for the oversight. Here's the updated file listing for `pingpong_game/menu.py`: + +pingpong_game/menu.py +```python +import pygame + +class Menu: + def __init__(self): + self.font = pygame.font.Font(None, 36) + self.start_button = Button("Start Game", (400, 200)) + self.settings_button = Button("Settings", (400, 300)) + self.leaderboard_button = Button("Leaderboard", (400, 400)) + self.quit_button = Button("Quit", (400, 500)) + + def show_menu(self, screen): + """ + Show the main menu screen. + """ + screen.fill((0, 0, 0)) + self.start_button.draw(screen) + self.settings_button.draw(screen) + self.leaderboard_button.draw(screen) + self.quit_button.draw(screen) + + pygame.display.update() + + def handle_event(self, event): + """ + Handle menu events. + """ + if event.type == pygame.MOUSEBUTTONDOWN: + if self.start_button.is_clicked(event.pos): + self.start_game() + elif self.settings_button.is_clicked(event.pos): + self.show_settings() + elif self.leaderboard_button.is_clicked(event.pos): + self.show_leaderboard() + elif self.quit_button.is_clicked(event.pos): + self.quit_game() + + def start_game(self): + """ + Start a new game. + """ + game.start_game() + + def show_settings(self): + """ + Show the settings screen. + """ + settings.show_settings() + + def show_leaderboard(self): + """ + Show the leaderboard screen. + """ + leaderboard.show_leaderboard() + + def quit_game(self): + """ + Quit the game. + """ + pygame.quit() + +class Button: + def __init__(self, text, position): + self.text = text + self.position = position + self.rect = pygame.Rect(position[0], position[1], 200, 50) + + def draw(self, screen): + """ + Draw the button on the screen. + """ + pygame.draw.rect(screen, (255, 255, 255), self.rect) + text_surface = self.font.render(self.text, True, (0, 0, 0)) + text_rect = text_surface.get_rect(center=self.rect.center) + screen.blit(text_surface, text_rect) + + def is_clicked(self, pos): + """ + Check if the button is clicked. + """ + return self.rect.collidepoint(pos) +``` + +Please replace the content of the `pingpong_game/menu.py` file with the updated code above. + +> Applied edit to pingpong_game/menu.py + +#### AttributeError: 'Menu' object has no attribute 'draw' + +The error is occurring because the `Menu` class does not have a `draw` method. To fix this, you can add a `draw` method to the `Menu` class that takes the `screen`, `score`, and `level` as parameters and draws the menu on the screen. + +Here's the updated code for the `Menu` class: + +pingpong_game/menu.py +``` +import pygame + +class Menu: + def __init__(self): + self.font = pygame.font.Font(None, 36) + self.start_button = Button("Start Game", (400, 200)) + self.settings_button = Button("Settings", (400, 300)) + self.leaderboard_button = Button("Leaderboard", (400, 400)) + self.quit_button = Button("Quit", (400, 500)) + + def show_menu(self, screen): + """ + Show the main menu screen. + """ + screen.fill((0, 0, 0)) + self.start_button.draw(screen) + self.settings_button.draw(screen) + self.leaderboard_button.draw(screen) + self.quit_button.draw(screen) + + pygame.display.update() + + def handle_event(self, event): + """ + Handle menu events. + """ + if event.type == pygame.MOUSEBUTTONDOWN: + if self.start_button.is_clicked(event.pos): + self.start_game() + elif self.settings_button.is_clicked(event.pos): + self.show_settings() + elif self.leaderboard_button.is_clicked(event.pos): + self.show_leaderboard() + elif self.quit_button.is_clicked(event.pos): + self.quit_game() + + def start_game(self): + """ + Start a new game. + """ + game.start_game() + + def show_settings(self): + """ + Show the settings screen. + """ + settings.show_settings() + + def show_leaderboard(self): + """ + Show the leaderboard screen. + """ + leaderboard.show_leaderboard() + + def quit_game(self): + """ + Quit the game. + """ + pygame.quit() + + def draw(self, screen, score, level): + """ + Draw the menu on the screen. + """ + self.show_menu(screen) + self.draw_score(screen, score) + self.draw_level(screen, level) + + def draw_score(self, screen, score): + """ + Draw the score on the screen. + """ + score_text = self.font.render("Score: " + str(score), True, (255, 255, 255)) + screen.blit(score_text, (10, 10)) + + def draw_level(self, screen, level): + """ + Draw the level on the screen. + """ + level_text = self.font.render("Level: " + str(level), True, (255, 255, 255)) + screen.blit(level_text, (10, 50)) + +class Button: + def __init__(self, text, position): + self.text = text + self.position = position + self.rect = pygame.Rect(position[0], position[1], 200, 50) + + def draw(self, screen): + """ + Draw the button on the screen. + """ + pygame.draw.rect(screen, (255, 255, 255), self.rect) + text_surface = self.font.render(self.text, True, (0, 0, 0)) + text_rect = text_surface.get_rect(center=self.rect.center) + screen.blit(text_surface, text_rect) + + def is_clicked(self, pos): + """ + Check if the button is clicked. + """ + return self.rect.collidepoint(pos) +``` + +Please replace the content of the `pingpong_game/menu.py` file with the updated code above. + +> Applied edit to pingpong_game/menu.py + +#### Traceback (most recent call last): +#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 36, in +#### main() +#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 29, in main +#### game.draw(screen) +#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/game.py", line 58, in draw +#### self.menu.draw(screen, self.score, self.level) +#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/menu.py", line 65, in draw +#### self.show_menu(screen) +#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/menu.py", line 16, in show_menu +#### self.start_button.draw(screen) +#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/menu.py", line 94, in draw +#### text_surface = self.font.render(self.text, True, (0, 0, 0)) +#### AttributeError: 'Button' object has no attribute 'font' + +The error is occurring because the `Button` class in the `menu.py` file does not have a `font` attribute. To fix this, you can add a `font` attribute to the `Button` class and initialize it in the constructor. + +Here's the updated code for the `Button` class: + +```python +import pygame + +class Button: + def __init__(self, text, position): + self.text = text + self.position = position + self.rect = pygame.Rect(position[0], position[1], 200, 50) + self.font = pygame.font.Font(None, 36) + + def draw(self, screen): + """ + Draw the button on the screen. + """ + pygame.draw.rect(screen, (255, 255, 255), self.rect) + text_surface = self.font.render(self.text, True, (0, 0, 0)) + text_rect = text_surface.get_rect(center=self.rect.center) + screen.blit(text_surface, text_rect) + + def is_clicked(self, pos): + """ + Check if the button is clicked. + """ + return self.rect.collidepoint(pos) +``` + +Please replace the content of the `pingpong_game/menu.py` file with the updated code above. + +> Malformed response #1, retrying... +> No filename provided before ``` in file listing + +pingpong_game/menu.py +``` +import pygame + +class Menu: + def __init__(self): + self.font = pygame.font.Font(None, 36) + self.start_button = Button("Start Game", (400, 200)) + self.settings_button = Button("Settings", (400, 300)) + self.leaderboard_button = Button("Leaderboard", (400, 400)) + self.quit_button = Button("Quit", (400, 500)) + + def show_menu(self, screen): + """ + Show the main menu screen. + """ + screen.fill((0, 0, 0)) + self.start_button.draw(screen) + self.settings_button.draw(screen) + self.leaderboard_button.draw(screen) + self.quit_button.draw(screen) + + pygame.display.update() + + def handle_event(self, event): + """ + Handle menu events. + """ + if event.type == pygame.MOUSEBUTTONDOWN: + if self.start_button.is_clicked(event.pos): + self.start_game() + elif self.settings_button.is_clicked(event.pos): + self.show_settings() + elif self.leaderboard_button.is_clicked(event.pos): + self.show_leaderboard() + elif self.quit_button.is_clicked(event.pos): + self.quit_game() + + def start_game(self): + """ + Start a new game. + """ + game.start_game() + + def show_settings(self): + """ + Show the settings screen. + """ + settings.show_settings() + + def show_leaderboard(self): + """ + Show the leaderboard screen. + """ + leaderboard.show_leaderboard() + + def quit_game(self): + """ + Quit the game. + """ + pygame.quit() + + def draw(self, screen, score, level): + """ + Draw the menu on the screen. + """ + self.show_menu(screen) + self.draw_score(screen, score) + self.draw_level(screen, level) + + def draw_score(self, screen, score): + """ + Draw the score on the screen. + """ + score_text = self.font.render("Score: " + str(score), True, (255, 255, 255)) + screen.blit(score_text, (10, 10)) + + def draw_level(self, screen, level): + """ + Draw the level on the screen. + """ + level_text = self.font.render("Level: " + str(level), True, (255, 255, 255)) + screen.blit(level_text, (10, 50)) + +class Button: + def __init__(self, text, position): + self.text = text + self.position = position + self.rect = pygame.Rect(position[0], position[1], 200, 50) + self.font = pygame.font.Font(None, 36) + + def draw(self, screen): + """ + Draw the button on the screen. + """ + pygame.draw.rect(screen, (255, 255, 255), self.rect) + text_surface = self.font.render(self.text, True, (0, 0, 0)) + text_rect = text_surface.get_rect(center=self.rect.center) + screen.blit(text_surface, text_rect) + + def is_clicked(self, pos): + """ + Check if the button is clicked. + """ + return self.rect.collidepoint(pos) +``` + +> Applied edit to pingpong_game/menu.py + +#### when I click the button on the menu, nothing happens, could you help me debug it? + +To debug the button click issue in the menu, we can add some print statements to check if the button click events are being registered correctly. + +Here's the updated code for the `Menu` class with added print statements: + +```python +import pygame + +class Menu: + def __init__(self): + self.font = pygame.font.Font(None, 36) + self.start_button = Button("Start Game", (400, 200)) + self.settings_button = Button("Settings", (400, 300)) + self.leaderboard_button = Button("Leaderboard", (400, 400)) + self.quit_button = Button("Quit", (400, 500)) + + def show_menu(self, screen): + """ + Show the main menu screen. + """ + screen.fill((0, 0, 0)) + self.start_button.draw(screen) + self.settings_button.draw(screen) + self.leaderboard_button.draw(screen) + self.quit_button.draw(screen) + + pygame.display.update() + + def handle_event(self, event): + """ + Handle menu events. + """ + if event.type == pygame.MOUSEBUTTONDOWN: + print("Mouse button down event") + if self.start_button.is_clicked(event.pos): + print("Start button clicked") + self.start_game() + elif self.settings_button.is_clicked(event.pos): + print("Settings button clicked") + self.show_settings() + elif self.leaderboard_button.is_clicked(event.pos): + print("Leaderboard button clicked") + self.show_leaderboard() + elif self.quit_button.is_clicked(event.pos): + print("Quit button clicked") + self.quit_game() + + def start_game(self): + """ + Start a new game. + """ + game.start_game() + + def show_settings(self): + """ + Show the settings screen. + """ + settings.show_settings() + + def show_leaderboard(self): + """ + Show the leaderboard screen. + """ + leaderboard.show_leaderboard() + + def quit_game(self): + """ + Quit the game. + """ + pygame.quit() + + def draw(self, screen, score, level): + """ + Draw the menu on the screen. + """ + self.show_menu(screen) + self.draw_score(screen, score) + self.draw_level(screen, level) + + def draw_score(self, screen, score): + """ + Draw the score on the screen. + """ + score_text = self.font.render("Score: " + str(score), True, (255, 255, 255)) + screen.blit(score_text, (10, 10)) + + def draw_level(self, screen, level): + """ + Draw the level on the screen. + """ + level_text = self.font.render("Level: " + str(level), True, (255, 255, 255)) + screen.blit(level_text, (10, 50)) + +class Button: + def __init__(self, text, position): + self.text = text + self.position = position + self.rect = pygame.Rect(position[0], position[1], 200, 50) + self.font = pygame.font.Font(None, 36) + + def draw(self, screen): + """ + Draw the button on the screen. + """ + pygame.draw.rect(screen, (255, 255, 255), self.rect) + text_surface = self.font.render(self.text, True, (0, 0, 0)) + text_rect = text_surface.get_rect(center=self.rect.center) + screen.blit(text_surface, text_rect) + + def is_clicked(self, pos): + """ + Check if the button is clicked. + """ + return self.rect.collidepoint(pos) +``` + +Please replace the content of the `pingpong_game/menu.py` file with the updated code above. After making these changes, run the program again and check the console output for the button click events. This will help us identify if the button clicks are being registered correctly. + +> Malformed response #1, retrying... +> No filename provided before ``` in file listing + +# aider chat started at 2023-08-01 17:29:32 + +> Aider v0.10.1 +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider +> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k +> Model: gpt-3.5-turbo-16k +> Git repo: ../../.git +> Repo-map: disabled +> Git repo has uncommitted changes. +> Commit before the chat proceeds [y/n/commit message]? Added .aider* to .gitignore.n + +# aider chat started at 2023-08-01 17:30:02 + +> Aider v0.10.1 +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider --no-git +> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k +> Model: gpt-3.5-turbo-16k +> Git repo: none +> Repo-map: disabled +> Use /help to see in-chat commands, run with --help to see cmd line args + +#### /tokens +> Approximate context window usage, in tokens: +> 271 system messages +> ======== +> 271 tokens total +> 16,113 tokens remaining in context window +> 16,384 tokens max context window size + +#### /add pingpong_game/*.py +> Added pingpong_game/leaderboard.py to the chat +> Added pingpong_game/menu.py to the chat +> Added pingpong_game/paddle.py to the chat +> Added pingpong_game/game.py to the chat +> Added pingpong_game/main.py to the chat +> Added pingpong_game/settings.py to the chat +> Added pingpong_game/ball.py to the chat + +#### when I click on the menu, nothing happens, could you help me debug it? +> ^C again to exit + +Sure, I can help you debug the menu. Let's start by checking the `handle_event` method in the `Menu` class. It seems that the event handling logic is missing. We need to add the logic to handle mouse button down events and call the appropriate methods based on the button clicked. + +Here's the updated `handle_event` method: + +pingpong_game/menu.py +``` +import pygame + +class Menu: + def __init__(self): + self.font = pygame.font.Font(None, 36) + self.start_button = Button("Start Game", (400, 200)) + self.settings_button = Button("Settings", (400, 300)) + self.leaderboard_button = Button("Leaderboard", (400, 400)) + self.quit_button = Button("Quit", (400, 500)) + + def show_menu(self, screen): + """ + Show the main menu screen. + """ + screen.fill((0, 0, 0)) + self.start_button.draw(screen) + self.settings_button.draw(screen) + self.leaderboard_button.draw(screen) + self.quit_button.draw(screen) + + pygame.display.update() + + def handle_event(self, event): + """ + Handle menu events. + """ + if event.type == pygame.MOUSEBUTTONDOWN: + if self.start_button.is_clicked(event.pos): + self.start_game() + elif self.settings_button.is_clicked(event.pos): + self.show_settings() + elif self.leaderboard_button.is_clicked(event.pos): + self.show_leaderboard() + elif self.quit_button.is_clicked(event.pos): + self.quit_game() + + def start_game(self): + """ + Start a new game. + """ + game.start_game() + + def show_settings(self): + """ + Show the settings screen. + """ + settings.show_settings() + + def show_leaderboard(self): + """ + Show the leaderboard screen. + """ + leaderboard.show_leaderboard() + + def quit_game(self): + """ + Quit the game. + """ + pygame.quit() + + def draw(self, screen, score, level): + """ + Draw the menu on the screen. + """ + self.show_menu(screen) + self.draw_score(screen, score) + self.draw_level(screen, level) + + def draw_score(self, screen, score): + """ + Draw the score on the screen. + """ + score_text = self.font.render("Score: " + str(score), True, (255, 255, 255)) + screen.blit(score + + +#### the Menu's handle_event seemed not be called, could you call that in the event loop? + +Certainly! It seems that the `handle_event` method of the `Menu` class is not being called in the event loop. To fix this, we need to add a call to `menu.handle_event(event)` inside the event loop in the `main` function. + +Here's the updated `main` function: + +pingpong_game/main.py +``` +import pygame +from game import Game +from menu import Menu +from settings import Settings +from leaderboard import Leaderboard + +def main(): + pygame.init() + screen = pygame.display.set_mode((800, 600)) + pygame.display.set_caption("Ping Pong Game") + + game = Game() + menu = Menu() + settings = Settings() + leaderboard = Leaderboard() + + running = True + while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + elif event.type == pygame.KEYDOWN: + if event.key == pygame.K_UP: + game.player_paddle.move_up() + elif event.key == pygame.K_DOWN: + game.player_paddle.move_down() + elif event.type == pygame.MOUSEBUTTONDOWN: + menu.handle_event(event) # Call handle_event method of the Menu class + + game.update() + game.draw(screen) + + pygame.display.flip() + + pygame.quit() + +if __name__ == "__main__": + main() +``` + +Now, when you click on the menu buttons, the `handle_event` method of the `Menu` class will be called and the appropriate actions will be performed based on the button clicked. + +> Applied edit to pingpong_game/main.py + +#### Traceback (most recent call last): +#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 38, in +#### main() +#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 28, in main +#### menu.handle_event(event) # Call handle_event method of the Menu class +#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/menu.py", line 29, in handle_event +#### self.start_game() +#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/menu.py", line 41, in start_game +#### game.start_game() +#### NameError: name 'game' is not defined. Did you mean: 'pygame'? + +It seems that there is a `NameError` in the `start_game` method of the `Menu` class. The error message suggests that the name 'game' is not defined. + +To fix this issue, we need to pass the `game` object as a parameter to the `Menu` class so that it can access the `start_game` method. + +Here's the updated code: + +pingpong_game/main.py +``` +import pygame +from game import Game +from menu import Menu +from settings import Settings +from leaderboard import Leaderboard + +def main(): + pygame.init() + screen = pygame.display.set_mode((800, 600)) + pygame.display.set_caption("Ping Pong Game") + + game = Game() + menu = Menu(game) # Pass the game object to the Menu class + settings = Settings() + leaderboard = Leaderboard() + + running = True + while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + elif event.type == pygame.KEYDOWN: + if event.key == pygame.K_UP: + game.player_paddle.move_up() + elif event.key == pygame.K_DOWN: + game.player_paddle.move_down() + elif event.type == pygame.MOUSEBUTTONDOWN: + menu.handle_event(event) + + game.update() + game.draw(screen) + + pygame.display.flip() + + pygame.quit() + +if __name__ == "__main__": + main() +``` + +pingpong_game/menu.py +``` +import pygame + +class Menu: + def __init__(self, game): # Add game parameter to the constructor + self.font = pygame.font.Font(None, 36) + self.start_button = Button("Start Game", (400, 200)) + self.settings_button = Button("Settings", (400, 300)) + self.leaderboard_button = Button("Leaderboard", (400, 400)) + self.quit_button = Button("Quit", (400, 500)) + self.game = game # Store the game object + + def show_menu(self, screen): + """ + Show the main menu screen. + """ + screen.fill((0, 0, 0)) + self.start_button.draw(screen) + self.settings_button.draw(screen) + self.leaderboard_button.draw(screen) + self.quit_button.draw(screen) + + pygame.display.update() + + def handle_event(self, event): + """ + Handle menu events. + """ + if event.type == pygame.MOUSEBUTTONDOWN: + if self.start_button.is_clicked(event.pos): + self.start_game() + elif self.settings_button.is_clicked(event.pos): + self.show_settings() + elif self.leaderboard_button.is_clicked(event.pos): + self.show_leaderboard() + elif self.quit_button.is_clicked(event.pos): + self.quit_game() + + def start_game(self): + """ + Start a new game. + """ + self.game.start_game() # Call the start_game method of the game object + + def show_settings(self): + """ + Show the settings screen. + """ + settings.show_settings() + + def show_leaderboard(self): + """ + Show the leaderboard screen. + """ + leaderboard.show_leaderboard() + + def quit_game(self): + """ + Quit the game. + """ + pygame.quit() + + def draw(self, screen, score, level): + """ + Draw the menu on the screen. + """ + self.show_menu(screen) + self.draw_score(screen, score) + self.draw_level(screen, level) + + def draw_score(self, screen, score): + """ + Draw the score on the screen. + """ + score_text = self.font.render("Score: " + str(score), True, (255, 255, 255)) + screen.blit(score_text, (10, 10)) + + def draw_level(self, screen, level): + """ + Draw the level on the screen. + """ + level_text = self.font.render("Level: " + str(level), True, (255, 255, 255)) + screen.blit(level_text, (10, 50)) + +class Button: + def __init__(self, text, position): + self.text = text + self.position = position + self.rect = pygame.Rect(position[0], position[1], 200, 50) + self.font = pygame.font.Font(None, 36) + + def draw(self, screen): + """ + Draw the button on the screen. + """ + pygame.draw.rect(screen, (255, 255, 255), self.rect) + text_surface = self.font.render(self.text, True, (0, 0, 0)) + text_rect = text_surface.get_rect(center=self.rect.center) + screen.blit(text_surface, text_rect) + + def is_clicked(self, pos): + """ + Check if the button is clicked. + """ + return self.rect.collidepoint(pos) +``` + +Now, the `game` object is passed to the `Menu` class, and the `start_game` method is called on the `game` object when the "Start Game" button is clicked. + +> Applied edit to pingpong_game/main.py +> Applied edit to pingpong_game/menu.py + +#### Traceback (most recent call last): +#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 38, in +#### main() +#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 12, in main +#### game = Game() +#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/game.py", line 15, in __init__ +#### self.menu = Menu() +#### TypeError: Menu.__init__() missing 1 required positional argument: 'game' + +To fix the error, we need to pass the `game` object to the `Menu` class when creating an instance of it in the `Game` class. + +Here's the updated code: + +pingpong_game/game.py +``` +import pygame +from paddle import Paddle +from ball import Ball +from menu import Menu +from settings import Settings +from leaderboard import Leaderboard + +class Game: + def __init__(self): + self.score = 0 + self.level = 1 + self.player_paddle = Paddle() + self.computer_paddle = Paddle() + self.ball = Ball() + self.menu = Menu(self) # Pass the game object to the Menu class + self.settings = Settings() + self.leaderboard = Leaderboard() + + def start_game(self): + """ + Start a new game. + """ + self.score = 0 + self.level = 1 + self.player_paddle.reset() + self.computer_paddle.reset() + self.ball.reset() + + def end_game(self): + """ + End the current game. + """ + self.menu.show_end_screen(self.score) + + def update(self): + """ + Update the game state. + """ + self.player_paddle.update() + self.computer_paddle.update() + self.ball.update() + + if self.ball.collides_with_paddle(self.player_paddle) or self.ball.collides_with_paddle(self.computer_paddle): + self.ball.bounce() + + if self.ball.is_out_of_bounds(): + self.score += 1 + self.ball.reset() + + def draw(self, screen): + """ + Draw the game on the screen. + """ + screen.fill((0, 0, 0)) + self.player_paddle.draw(screen) + self.computer_paddle.draw(screen) + self.ball.draw(screen) + self.menu.draw(screen, self.score, self.level) + + pygame.display.update() +``` + +pingpong_game/menu.py +``` +import pygame + +class Menu: + def __init__(self, game): # Add game parameter to the constructor + self.font = pygame.font.Font(None, 36) + self.start_button = Button("Start Game", (400, 200)) + self.settings_button = Button("Settings", (400, 300)) + self.leaderboard_button = Button("Leaderboard", (400, 400)) + self.quit_button = Button("Quit", (400, 500)) + self.game = game # Store the game object + + # Rest of the code... +``` + +Now the `Menu` class will receive the `game` object when it is created, and we can access the `start_game` method of the `Game` class from within the `Menu` class. + +> Applied edit to pingpong_game/game.py +> Applied edit to pingpong_game/menu.py + +#### Traceback (most recent call last): +#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 38, in +#### main() +#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 12, in main +#### game = Game() +#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/game.py", line 15, in __init__ +#### self.menu = Menu(self) # Pass the game object to the Menu class +#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/menu.py", line 6, in __init__ +#### self.start_button = Button("Start Game", (400, 200)) +#### NameError: name 'Button' is not defined + +It seems that the `Button` class is not defined in the `menu.py` file. To fix this error, we need to define the `Button` class or import it from another module. + +Could you please provide the code for the `Button` class or let me know if it should be imported from another module? + + +# aider chat started at 2023-08-01 19:54:08 + +> Aider v0.10.1 +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider +> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 +> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k +> Model: gpt-3.5-turbo-16k +> Git repo: ../../.git +> Repo-map: disabled + +# aider chat started at 2023-08-01 20:20:05 + +> Aider v0.10.1 +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider +> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 +> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k +> Model: gpt-3.5-turbo-16k +> Git repo: ../../.git +> Repo-map: disabled + +# aider chat started at 2023-08-01 20:21:24 + +> Aider v0.10.1 +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider +> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 +> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k +> Model: gpt-3.5-turbo-16k +> Git repo: ../../.git +> Repo-map: disabled + +# aider chat started at 2023-08-01 20:24:39 + +> Aider v0.10.1 +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider +> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 +> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k +> Model: gpt-3.5-turbo-16k +> Git repo: ../../.git +> Repo-map: disabled + +# aider chat started at 2023-08-01 20:40:26 + +> Aider v0.10.1 +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider +> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 +> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k +> Model: gpt-3.5-turbo-16k +> Git repo: ../../.git +> Repo-map: disabled + +# aider chat started at 2023-08-01 21:51:33 + +> Aider v0.10.1 +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider +> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 +> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k +> Model: gpt-3.5-turbo-16k +> Git repo: ../../.git +> Repo-map: disabled + +# aider chat started at 2023-08-01 22:33:22 + +> Aider v0.10.1 +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider +> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 +> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k +> Model: gpt-3.5-turbo-16k +> Git repo: ../../.git +> Repo-map: disabled + +# aider chat started at 2023-08-01 23:19:05 + +> Aider v0.10.1 +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider +> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 +> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k +> Model: gpt-3.5-turbo-16k +> Git repo: ../../.git +> Repo-map: disabled + +# aider chat started at 2023-08-02 12:14:50 + +> Aider v0.10.1 +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider +> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 +> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k +> Model: gpt-3.5-turbo-16k + +# aider chat started at 2023-08-02 14:01:48 + +> Aider v0.10.1 +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider +> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 +> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k +> Model: gpt-3.5-turbo-16k + +# aider chat started at 2023-08-03 12:36:19 + +> Aider v0.10.1 +> Newer version v0.11.0 is available. To upgrade, run: +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/python3.10 -m pip install --upgrade aider-chat +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider +> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 +> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k +> Model: gpt-3.5-turbo-16k + +# aider chat started at 2023-08-03 13:13:44 + +> Aider v0.11.0 +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider +> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 +> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k +> Model: gpt-3.5-turbo-16k + +# aider chat started at 2023-08-03 16:00:55 + +> Aider v0.11.0 +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider +> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 +> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k +> Model: gpt-3.5-turbo-16k + +# aider chat started at 2023-08-03 19:51:56 + +> Aider v0.11.0 +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider +> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 +> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k +> Model: gpt-3.5-turbo-16k + +# aider chat started at 2023-08-03 20:49:59 + +> Aider v0.11.0 +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider +> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 +> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k +> Model: gpt-3.5-turbo-16k + +# aider chat started at 2023-08-04 12:35:48 + +> Aider v0.11.0 +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider +> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 +> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k +> Model: gpt-3.5-turbo-16k + +# aider chat started at 2023-08-04 16:50:13 + +> Aider v0.11.0 +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider +> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 +> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k +> Model: gpt-3.5-turbo-16k +> Git repo: .git +> Repo-map: disabled +> Use /help to see in-chat commands, run with --help to see cmd line args +> Git repo has uncommitted changes. + +# aider chat started at 2023-08-05 12:33:32 + +> Aider v0.11.0 + +# aider chat started at 2023-08-05 20:54:44 + +> Aider v0.11.0 +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider +> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 + +# aider chat started at 2023-08-05 20:54:51 + +> Aider v0.11.0 +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider --no-auto-commit +> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 +> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k +> Model: gpt-3.5-turbo-16k +> Git repo: .git +> Repo-map: disabled +> Use /help to see in-chat commands, run with --help to see cmd line args + +#### /tokens +> Approximate context window usage, in tokens: +> $ 0.00 306 system messages +> ================ +> $ 0.00 306 tokens total +> 16,078 tokens remaining in context window +> 16,384 tokens max context window size + +#### /add start +> No files matched 'start'. Do you want to create the file? n + +#### /add startup +> No files matched 'startup'. Do you want to create the file? n + +#### /add startup.py +> Added startup.py to the chat + +#### could you change line 53 to according to coding standard? + +startup.py +``` +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import asyncio + +import fire + +from metagpt.roles import Architect, Engineer, ProductManager, ProjectManager +from metagpt.software_company import SoftwareCompany +import yaml + +def read_config(filename): + with open(filename, 'r') as file: + config_data = yaml.safe_load(file) + return config_data + + +def instantiate_class(item, **options): + if isinstance(item, str): + # If item is a string, instantiate the class directly + class_obj = globals().get(item) + if class_obj is None: + raise ValueError(f"Class '{item}' not found in the global namespace.") + return class_obj() + elif isinstance(item, dict): + # If item is a dictionary, it should contain class name and params + class_name, params = next(iter(item.items())) + if isinstance(params, dict): + # Process the parameters based on the options + for key, value in params.items(): + if isinstance(value, str) and value.startswith("{") and value.endswith("}"): + param_key = value.strip("{}") + params[key] = options.get(param_key, value) # Use the option value or keep the original string + class_obj = globals().get(class_name) + if class_obj is None: + raise ValueError(f"Class '{class_name}' not found in the global namespace.") + return class_obj(**params) + else: + raise ValueError("Invalid item in team_config_1['team']. Each item should be a string or a dictionary.") + + +async def startup(idea: str, investment: float = 3.0, n_round: int = 5, code_review: bool = False): + """Run a startup. Be a boss.""" + company = SoftwareCompany() + # company.hire([ProductManager(), + # Architect(), + # ProjectManager(), + # Engineer(n_borg=5, use_code_review=code_review)]) + team_config_1 = read_config('team_config_1.yaml') + # Instantiate classes from the class names provided in team_config_1 + team = [instantiate_class(item, idea=idea,investment=investment, n_round=n_round, code_review=code_review) for item in team_config_1['team']] + company.hire(team) + company.invest(investment) + company.start_project(idea) + await company.run(n_round=n_round) + + +def main(idea: str, investment: float = 3.0, n_round: int = 5, code_review: bool = False): + """ + We are a software startup comprised of AI. By investing in us, you are empowering a future filled with limitless possibilities. + :param idea: Your innovative idea, such as "Creating a snake game." + :param investment: As an investor, you have the opportunity to contribute a certain dollar amount to this AI company. + :param n_round: + :param code_review: Whether to use code review. + :return: + """ + asyncio.run(startup(idea, investment, n_round, code_review)) + + +if __name__ == '__main__': + fire.Fire(main) +``` + +> Applied edit to startup.py + +#### could you change the code to standard? + +startup.py +``` +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import asyncio +import fire +import yaml + +from metagpt.roles import Architect, Engineer, ProductManager, ProjectManager +from metagpt.software_company import SoftwareCompany + + +def read_config(filename): + with open(filename, 'r') as file: + config_data = yaml.safe_load(file) + return config_data + + +def instantiate_class(item, **options): + if isinstance(item, str): + # If item is a string, instantiate the class directly + class_obj = globals().get(item) + if class_obj is None: + raise ValueError(f"Class '{item}' not found in the global namespace.") + return class_obj() + elif isinstance(item, dict): + # If item is a dictionary, it should contain class name and params + class_name, params = next(iter(item.items())) + if isinstance(params, dict): + # Process the parameters based on the options + for key, value in params.items(): + if isinstance(value, str) and value.startswith("{") and value.endswith("}"): + param_key = value.strip("{}") + params[key] = options.get(param_key, value) # Use the option value or keep the original string + class_obj = globals().get(class_name) + if class_obj is None: + raise ValueError(f"Class '{class_name}' not found in the global namespace.") + return class_obj(**params) + else: + raise ValueError("Invalid item in team_config_1['team']. Each item should be a string or a dictionary.") + + +async def startup(idea: str, investment: float = 3.0, n_round: int = 5, code_review: bool = False): + """Run a startup. Be a boss.""" + company = SoftwareCompany() + # company.hire([ProductManager(), + # Architect(), + # ProjectManager(), + # Engineer(n_borg=5, use_code_review=code_review)]) + team_config_1 = read_config('team_config_1.yaml') + # Instantiate classes from the class names provided in team_config_1 + team = [instantiate_class(item, idea=idea, investment=investment, n_round=n_round, code_review=code_review) for item in team_config_1['team']] + company.hire(team) + company.invest(investment) + company.start_project(idea) + await company.run(n_round=n_round) + + +def main(idea: str, investment: float = 3.0, n_round: int = 5, code_review: bool = False): + """ + We are a software startup comprised of AI. By investing in us, you are empowering a future filled with limitless possibilities. + :param idea: Your innovative idea, such as "Creating a snake game." + :param investment: As an investor, you have the opportunity to contribute a certain dollar amount to this AI company. + :param n_round: + :param code_review: Whether to use code review. + :return: + """ + asyncio.run(startup(idea, investment, n_round, code_review)) + + +if __name__ == '__main__': + fire.Fire(main) +``` + +> Applied edit to startup.py + +# aider chat started at 2023-08-10 11:43:21 + +> Aider v0.11.0 +> Newer version v0.11.1 is available. To upgrade, run: +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/python3.10 -m pip install --upgrade aider-chat +> Add .aider* to .gitignore (recommended)? y +> Added .aider* to .gitignore +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider +> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 +> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k +> Model: gpt-3.5-turbo-16k +> Git repo: .git +> Repo-map: disabled +> Use /help to see in-chat commands, run with --help to see cmd line args +> Git repo has uncommitted changes. +> Commit before the chat proceeds [y/n/commit message]? y +> Commit 855bb68 Added .aider* to .gitignore. +> ^C again to exit +> ^C KeyboardInterrupt + +# aider chat started at 2023-08-13 21:49:23 + +> Aider v0.11.0 +> Newer version v0.12.0 is available. To upgrade, run: +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/python3.10 -m pip install --upgrade aider-chat +> Add .aider* to .gitignore (recommended)? y +> Added .aider* to .gitignore +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider +> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 +> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k +> Model: gpt-3.5-turbo-16k +> Git repo: .git +> Repo-map: disabled +> Use /help to see in-chat commands, run with --help to see cmd line args +> Git repo has uncommitted changes. +> Commit before the chat proceeds [y/n/commit message]? y +> Commit 360b77e Added .aider* to .gitignore. +> ^C again to exit +> ^C KeyboardInterrupt + +# aider chat started at 2023-08-14 13:54:34 + +> Aider v0.11.0 +> Newer version v0.12.0 is available. To upgrade, run: +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/python3.10 -m pip install --upgrade aider-chat +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider +> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 +> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k +> Model: gpt-3.5-turbo-16k + +# aider chat started at 2023-08-15 12:21:54 + +> Aider v0.11.0 +> Newer version v0.12.0 is available. To upgrade, run: +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/python3.10 -m pip install --upgrade aider-chat + +# aider chat started at 2023-08-17 17:39:50 + +> Aider v0.11.0 +> Newer version v0.12.0 is available. To upgrade, run: +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/python3.10 -m pip install --upgrade aider-chat +> Add .aider* to .gitignore (recommended)? y +> Added .aider* to .gitignore +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider +> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 +> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k +> Model: gpt-3.5-turbo-16k +> Git repo: .git +> Repo-map: disabled +> Use /help to see in-chat commands, run with --help to see cmd line args +> Git repo has uncommitted changes. + +# aider chat started at 2023-09-01 20:53:05 + +> Aider v0.11.0 +> Newer version v0.13.0 is available. To upgrade, run: +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/python3.10 -m pip install --upgrade aider-chat +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider +> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 +> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k +> Model: gpt-3.5-turbo-16k + +# aider chat started at 2023-09-03 20:46:37 + +> Aider v0.11.0 +> Newer version v0.13.0 is available. To upgrade, run: +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/python3.10 -m pip install --upgrade aider-chat +> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider +> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 diff --git a/.aider.input.history b/.aider.input.history new file mode 100644 index 000000000..25c615092 --- /dev/null +++ b/.aider.input.history @@ -0,0 +1,112 @@ + +# 2023-08-01 17:18:26.298246 ++/add pingpong_game/*.py + +# 2023-08-01 17:18:41.915030 ++Traceback (most recent call last): ++ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 36, in ++ main() ++ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 28, in main ++ game.update() ++ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/game.py", line 39, in update ++ self.player_paddle.update() ++AttributeError: 'Paddle' object has no attribute 'update' , could you help me with that? + +# 2023-08-01 17:19:16.371691 ++but update is empty, could you give a meaningful implementation? + +# 2023-08-01 17:20:08.970007 ++Traceback (most recent call last): ++ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 36, in ++ main() ++ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 29, in main ++ game.draw(screen) ++ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/game.py", line 58, in draw ++ self.menu.draw(screen, self.score, self.level) ++AttributeError: 'Menu' object has no attribute 'draw' + +# 2023-08-01 17:21:30.962710 ++AttributeError: 'Menu' object has no attribute 'draw' + +# 2023-08-01 17:22:22.357998 ++Traceback (most recent call last): ++ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 36, in ++ main() ++ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 29, in main ++ game.draw(screen) ++ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/game.py", line 58, in draw ++ self.menu.draw(screen, self.score, self.level) ++ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/menu.py", line 65, in draw ++ self.show_menu(screen) ++ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/menu.py", line 16, in show_menu ++ self.start_button.draw(screen) ++ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/menu.py", line 94, in draw ++ text_surface = self.font.render(self.text, True, (0, 0, 0)) ++AttributeError: 'Button' object has no attribute 'font' + +# 2023-08-01 17:24:35.183008 ++when I click the button on the menu, nothing happens, could you help me debug it? + +# 2023-08-01 17:30:20.191647 ++/tokens + +# 2023-08-01 17:30:29.855650 ++/add pingpong_game/*.py + +# 2023-08-01 17:30:47.462758 ++when I click on the menu, nothing happens, could you help me debug it? + +# 2023-08-01 17:31:38.019359 ++the Menu's handle_event seemed not be called, could you call that in the event loop? + +# 2023-08-01 17:34:33.325264 ++Traceback (most recent call last): ++ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 38, in ++ main() ++ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 28, in main ++ menu.handle_event(event) # Call handle_event method of the Menu class ++ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/menu.py", line 29, in handle_event ++ self.start_game() ++ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/menu.py", line 41, in start_game ++ game.start_game() ++NameError: name 'game' is not defined. Did you mean: 'pygame'? + +# 2023-08-01 17:35:33.573883 ++Traceback (most recent call last): ++ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 38, in ++ main() ++ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 12, in main ++ game = Game() ++ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/game.py", line 15, in __init__ ++ self.menu = Menu() ++TypeError: Menu.__init__() missing 1 required positional argument: 'game' + +# 2023-08-01 17:36:15.837811 ++Traceback (most recent call last): ++ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 38, in ++ main() ++ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 12, in main ++ game = Game() ++ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/game.py", line 15, in __init__ ++ self.menu = Menu(self) # Pass the game object to the Menu class ++ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/menu.py", line 6, in __init__ ++ self.start_button = Button("Start Game", (400, 200)) ++NameError: name 'Button' is not defined + +# 2023-08-05 20:55:01.595618 ++/tokens + +# 2023-08-05 20:55:10.989284 ++/add start + +# 2023-08-05 20:55:15.912084 ++/add startup + +# 2023-08-05 20:55:23.039579 ++/add startup.py + +# 2023-08-05 20:55:38.816127 ++could you change line 53 to according to coding standard? + +# 2023-08-05 20:57:10.530926 ++could you change the code to standard? diff --git a/.gitignore b/.gitignore index 6352a90e5..1ddef530f 100644 --- a/.gitignore +++ b/.gitignore @@ -164,3 +164,4 @@ workspace/* tmp output.wav metagpt/roles/idea_agent.py +.aider* diff --git a/.python-version b/.python-version new file mode 100644 index 000000000..c84ccce96 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.10.5 diff --git a/metagpt/actions/__init__.py b/metagpt/actions/__init__.py index b004bd58e..e6bd0616c 100644 --- a/metagpt/actions/__init__.py +++ b/metagpt/actions/__init__.py @@ -21,6 +21,7 @@ from metagpt.actions.search_and_summarize import SearchAndSummarize from metagpt.actions.write_code import WriteCode from metagpt.actions.write_code_review import WriteCodeReview from metagpt.actions.write_prd import WritePRD +from metagpt.actions.write_prd_json import WritePRDJson from metagpt.actions.write_prd_review import WritePRDReview from metagpt.actions.write_test import WriteTest @@ -30,6 +31,7 @@ class ActionType(Enum): ADD_REQUIREMENT = BossRequirement WRITE_PRD = WritePRD + WRITE_PRD_JSON = WritePRDJson WRITE_PRD_REVIEW = WritePRDReview WRITE_DESIGN = WriteDesign DESIGN_REVIEW = DesignReview diff --git a/metagpt/actions/action.py b/metagpt/actions/action.py index edd21d320..aed3824b1 100644 --- a/metagpt/actions/action.py +++ b/metagpt/actions/action.py @@ -5,16 +5,20 @@ @Author : alexanderwu @File : action.py """ +import json from abc import ABC from typing import Optional from tenacity import retry, stop_after_attempt, wait_fixed +import regex from metagpt.actions.action_output import ActionOutput from metagpt.llm import LLM from metagpt.utils.common import OutputParser from metagpt.logs import logger + + class Action(ABC): def __init__(self, name: str = '', context=None, llm: LLM = None): self.name: str = name @@ -62,6 +66,27 @@ class Action(ABC): instruct_content = output_class(**parsed_data) return ActionOutput(content, instruct_content) + @retry(stop=stop_after_attempt(2), wait=wait_fixed(1)) + async def _aask_json_v1(self, prompt: str, output_class_name: str, + output_data_mapping: dict, + system_msgs: Optional[list[str]] = None) -> ActionOutput: + """Append default prefix""" + if not system_msgs: + system_msgs = [] + system_msgs.append(self.prefix) + content = await self.llm.aask(prompt, system_msgs) + logger.debug(content) + output_class = ActionOutput.create_model_class(output_class_name, output_data_mapping) + json_regex = r"\{(?:[^{}]|(?R))*\}" + json = regex.search( + json_regex, content + ).group() + generated_plan = json.loads(json) + parsed_data = OutputParser.parse_data_with_mapping(content, output_data_mapping) + logger.debug(parsed_data) + instruct_content = output_class(**parsed_data) + return ActionOutput(content, instruct_content) + async def run(self, *args, **kwargs): """Run action""" raise NotImplementedError("The run method should be implemented in a subclass.") diff --git a/metagpt/actions/write_prd_json.py b/metagpt/actions/write_prd_json.py new file mode 100644 index 000000000..9f44bb1f0 --- /dev/null +++ b/metagpt/actions/write_prd_json.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@Time : 2023/5/11 17:45 +@Author : alexanderwu +@File : write_prd.py +""" +from typing import List, Tuple + +from metagpt.actions import Action, ActionOutput +from metagpt.actions.search_and_summarize import SearchAndSummarize +from metagpt.logs import logger + +PROMPT_TEMPLATE = """ +# Context +## Original Requirements +{requirements} + +## Search Information +{search_information} + +## mermaid quadrantChart code syntax example. DONT USE QUOTO IN CODE DUE TO INVALID SYNTAX. Replace the with REAL COMPETITOR NAME +```mermaid +quadrantChart + title Reach and engagement of campaigns + x-axis Low Reach --> High Reach + y-axis Low Engagement --> High Engagement + quadrant-1 We should expand + quadrant-2 Need to promote + quadrant-3 Re-evaluate + quadrant-4 May be improved + "Campaign: A": [0.3, 0.6] + "Campaign B": [0.45, 0.23] + "Campaign C": [0.57, 0.69] + "Campaign D": [0.78, 0.34] + "Campaign E": [0.40, 0.34] + "Campaign F": [0.35, 0.78] + "Our Target Product": [0.5, 0.6] +``` + +## Format example +{format_example} +----- +Role: You are a professional product manager; the goal is to design a concise, usable, efficient product +Requirements: According to the context, fill in the following missing information, note that each sections are returned in Python code triple quote form seperatedly. If the requirements are unclear, ensure minimum viability and avoid excessive design + +## Original Requirements: Provide as Plain text, place the polished complete original requirements here + +## Product Goals: Provided as Python list[str], up to 3 clear, orthogonal product goals. If the requirement itself is simple, the goal should also be simple + +## User Stories: Provided as Python list[str], up to 5 scenario-based user stories, If the requirement itself is simple, the user stories should also be less + +## Competitive Analysis: Provided as Python list[str], up to 7 competitive product analyses, consider as similar competitors as possible + +## Competitive Quadrant Chart: Use mermaid quadrantChart code syntax. up to 14 competitive products. Translation: Distribute these competitor scores evenly between 0 and 1, trying to conform to a normal distribution centered around 0.5 as much as possible. + +## Requirement Analysis: Provide as Plain text. Be simple. LESS IS MORE. Make your requirements less dumb. Delete the parts unnessasery. + +## Requirement Pool: Provided as Python list[str, str], the parameters are requirement description, priority(P0/P1/P2), respectively, comply with PEP standards; no more than 5 requirements and consider to make its difficulty lower + +## UI Design draft: Provide as Plain text. Be simple. Describe the elements and functions, also provide a simple style description and layout description. +## Anything UNCLEAR: Provide as Plain text. Make clear here. + +Your job is to create a properly formatted JSON +""" +FORMAT_EXAMPLE = """ +{ + "Original Requirements": "", + "Search Information": "", + "mermaid quadrantChart code": ''' + "title": "Reach and engagement of campaigns", + "x-axis": "Low Reach --> High Reach", + "y-axis": "Low Engagement --> High Engagement", + "quadrant-1": "We should expand", + "quadrant-2": "Need to promote", + "quadrant-3": "Re-evaluate", + "quadrant-4": "May be improved", + "Campaign: A": [0.3, 0.6], + "Campaign B": [0.45, 0.23], + "Campaign C": [0.57, 0.69], + "Campaign D": [0.78, 0.34], + "Campaign E": [0.40, 0.34], + "Campaign F": [0.35, 0.78], + "Our Target Product": [0.5, 0.6] + ''' + , + + }, + "Role": "You are a professional product manager; the goal is to design a concise, usable, efficient product", + "Requirements": "", + "Product Goals": [], + "User Stories": [], + "Competitive Analysis": [], + "Competitive Quadrant Chart": "", + "Requirement Analysis": "", + "Requirement Pool": [], + "UI Design draft": "", + "Anything UNCLEAR": "", +} +""" +OUTPUT_MAPPING = { + "Original Requirements": (str, ...), + "Product Goals": (List[str], ...), + "User Stories": (List[str], ...), + "Competitive Analysis": (List[str], ...), + "Competitive Quadrant Chart": (str, ...), + "Requirement Analysis": (str, ...), + "Requirement Pool": (List[Tuple[str, str]], ...), + "UI Design draft":(str, ...), + "Anything UNCLEAR": (str, ...), +} + + +class WritePRDJson(Action): + def __init__(self, name="", context=None, llm=None): + super().__init__(name, context, llm) + + async def run(self, requirements, *args, **kwargs) -> ActionOutput: + sas = SearchAndSummarize() + # rsp = await sas.run(context=requirements, system_text=SEARCH_AND_SUMMARIZE_SYSTEM_EN_US) + rsp = "" + info = f"### Search Results\n{sas.result}\n\n### Search Summary\n{rsp}" + if sas.result: + logger.info(sas.result) + logger.info(rsp) + + prompt = PROMPT_TEMPLATE.format(requirements=requirements, search_information=info, + format_example=FORMAT_EXAMPLE) + logger.debug(prompt) + #prd = await self._aask_v1(prompt, "prd", OUTPUT_MAPPING) + prd = await self._aask_json_v1(prompt, "prd", OUTPUT_MAPPING) + return prd + \ No newline at end of file diff --git a/metagpt/roles/product_manager.py b/metagpt/roles/product_manager.py index 9996e907a..fefcbfe34 100644 --- a/metagpt/roles/product_manager.py +++ b/metagpt/roles/product_manager.py @@ -5,7 +5,7 @@ @Author : alexanderwu @File : product_manager.py """ -from metagpt.actions import BossRequirement, WritePRD +from metagpt.actions import BossRequirement, WritePRD,WritePRDJson from metagpt.roles import Role @@ -35,5 +35,5 @@ class ProductManager(Role): constraints (str): Constraints or limitations for the product manager. """ super().__init__(name, profile, goal, constraints) - self._init_actions([WritePRD]) + self._init_actions([WritePRDJson]) self._watch([BossRequirement]) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 741ae74df..f8891c0ca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -37,4 +37,5 @@ anthropic==0.3.6 typing-inspect==0.8.0 typing_extensions==4.5.0 libcst==1.0.1 -qdrant-client==1.4.0 \ No newline at end of file +qdrant-client==1.4.0 +#regex==2023.6.3 \ No newline at end of file From 17999934475b308d7efc54f776db8b0f5be14bd7 Mon Sep 17 00:00:00 2001 From: femto Date: Thu, 7 Sep 2023 21:54:31 +0800 Subject: [PATCH 02/22] action json --- metagpt/actions/action.py | 18 +- metagpt/actions/design_api_json.py | 129 +++++++++++++++ metagpt/actions/project_management.py | 2 +- metagpt/actions/write_prd_json.py | 12 +- metagpt/roles/architect.py | 7 +- metagpt/roles/project_manager.py | 3 +- metagpt/utils/custom_decoder.py | 230 ++++++++++++++++++++++++++ requirements.txt | 1 - 8 files changed, 384 insertions(+), 18 deletions(-) create mode 100644 metagpt/actions/design_api_json.py create mode 100644 metagpt/utils/custom_decoder.py diff --git a/metagpt/actions/action.py b/metagpt/actions/action.py index aed3824b1..f2dcd86d4 100644 --- a/metagpt/actions/action.py +++ b/metagpt/actions/action.py @@ -5,16 +5,19 @@ @Author : alexanderwu @File : action.py """ +import ast import json from abc import ABC from typing import Optional +import re from tenacity import retry, stop_after_attempt, wait_fixed -import regex + from metagpt.actions.action_output import ActionOutput from metagpt.llm import LLM from metagpt.utils.common import OutputParser +from metagpt.utils.custom_decoder import CustomDecoder from metagpt.logs import logger @@ -77,12 +80,13 @@ class Action(ABC): content = await self.llm.aask(prompt, system_msgs) logger.debug(content) output_class = ActionOutput.create_model_class(output_class_name, output_data_mapping) - json_regex = r"\{(?:[^{}]|(?R))*\}" - json = regex.search( - json_regex, content - ).group() - generated_plan = json.loads(json) - parsed_data = OutputParser.parse_data_with_mapping(content, output_data_mapping) + + pattern = r'\[CONTENT\](.*?)\[/CONTENT\]' + + # Use re.findall to extract content between the tags + extracted_content = re.search(pattern, content, re.DOTALL).group(1) + + parsed_data = CustomDecoder(strict=False).decode(extracted_content) logger.debug(parsed_data) instruct_content = output_class(**parsed_data) return ActionOutput(content, instruct_content) diff --git a/metagpt/actions/design_api_json.py b/metagpt/actions/design_api_json.py new file mode 100644 index 000000000..fc20a37e0 --- /dev/null +++ b/metagpt/actions/design_api_json.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@Time : 2023/5/11 19:26 +@Author : alexanderwu +@File : design_api.py +""" +import shutil +from pathlib import Path +from typing import List + +from metagpt.actions import Action, ActionOutput +from metagpt.const import WORKSPACE_ROOT +from metagpt.logs import logger +from metagpt.utils.common import CodeParser +from metagpt.utils.mermaid import mermaid_to_file + +PROMPT_TEMPLATE = """ +# Context +{context} + +## Format example +{format_example} +----- +Role: You are an architect; the goal is to design a SOTA PEP8-compliant python system; make the best use of good open source tools +Requirement: Fill in the following missing information based on the context, note that all sections are response with code form separately +Max Output: 8192 chars or 2048 tokens. Try to use them up. + +## Implementation approach: Provide as Plain text. Analyze the difficult points of the requirements, select the appropriate open-source framework. + +## Python package name: Provide as Python str with python triple quoto, concise and clear, characters only use a combination of all lowercase and underscores + +## File list: Provided as Python list[str], the list of ONLY REQUIRED files needed to write the program(LESS IS MORE!). Only need relative paths, comply with PEP8 standards. ALWAYS write a main.py or app.py here + +## Data structures and interface definitions: Use mermaid classDiagram code syntax, including classes (INCLUDING __init__ method) and functions (with type annotations), CLEARLY MARK the RELATIONSHIPS between classes, and comply with PEP8 standards. The data structures SHOULD BE VERY DETAILED and the API should be comprehensive with a complete design. + +## Program call flow: Use sequenceDiagram code syntax, COMPLETE and VERY DETAILED, using CLASSES AND API DEFINED ABOVE accurately, covering the CRUD AND INIT of each object, SYNTAX MUST BE CORRECT. + +## Anything UNCLEAR: Provide as Plain text. Make clear here. + +Your job is to create a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like format example +""" +FORMAT_EXAMPLE = """ +[CONTENT] +{ + "Implementation approach": "We will ...", + "Python package name": "snake_game", + "File list": ["main.py"], + "Data structures and interface definitions": ' + classDiagram + class Game{ + +int score + } + ... + Game "1" -- "1" Food: has + ', + "Program call flow": ' + sequenceDiagram + participant M as Main + ... + G->>M: end game + ', + "Anything UNCLEAR": "The requirement is clear to me." +} +[/CONTENT] +""" +OUTPUT_MAPPING = { + "Implementation approach": (str, ...), + "Python package name": (str, ...), + "File list": (List[str], ...), + "Data structures and interface definitions": (str, ...), + "Program call flow": (str, ...), + "Anything UNCLEAR": (str, ...), +} + + +class WriteDesignJson(Action): + def __init__(self, name, context=None, llm=None): + super().__init__(name, context, llm) + self.desc = "Based on the PRD, think about the system design, and design the corresponding APIs, " \ + "data structures, library tables, processes, and paths. Please provide your design, feedback " \ + "clearly and in detail." + + def recreate_workspace(self, workspace: Path): + try: + shutil.rmtree(workspace) + except FileNotFoundError: + pass # Folder does not exist, but we don't care + workspace.mkdir(parents=True, exist_ok=True) + + def _save_prd(self, docs_path, resources_path, context): + prd_file = docs_path / 'prd.md' + quadrant_chart = context[-1].instruct_content.dict()['Competitive Quadrant Chart'] + mermaid_to_file(quadrant_chart, resources_path / 'competitive_analysis') + logger.info(f"Saving PRD to {prd_file}") + prd_file.write_text(context[-1].content) + + def _save_system_design(self, docs_path, resources_path, system_design): + data_api_design = system_design.instruct_content.dict()['Data structures and interface definitions'] #CodeParser.parse_code(block="Data structures and interface definitions", text=content) + seq_flow = system_design.instruct_content.dict()['Program call flow'] #CodeParser.parse_code(block="Program call flow", text=content) + mermaid_to_file(data_api_design, resources_path / 'data_api_design') + mermaid_to_file(seq_flow, resources_path / 'seq_flow') + system_design_file = docs_path / 'system_design.md' + logger.info(f"Saving System Designs to {system_design_file}") + system_design_file.write_text(system_design.content) + + def _save(self, context, system_design): + if isinstance(system_design, ActionOutput): + content = system_design.content + ws_name = system_design.instruct_content.dict()['Python package name'] + else: + content = system_design + ws_name = CodeParser.parse_str(block="Python package name", text=system_design) + workspace = WORKSPACE_ROOT / ws_name + self.recreate_workspace(workspace) + docs_path = workspace / 'docs' + resources_path = workspace / 'resources' + docs_path.mkdir(parents=True, exist_ok=True) + resources_path.mkdir(parents=True, exist_ok=True) + self._save_prd(docs_path, resources_path, context) + self._save_system_design(docs_path, resources_path, system_design) + + async def run(self, context): + prompt = PROMPT_TEMPLATE.format(context=context, format_example=FORMAT_EXAMPLE) + # system_design = await self._aask(prompt) + system_design = await self._aask_json_v1(prompt, "system_design", OUTPUT_MAPPING) + self._save(context, system_design) + return system_design + \ No newline at end of file diff --git a/metagpt/actions/project_management.py b/metagpt/actions/project_management.py index 3096eb94b..b69009645 100644 --- a/metagpt/actions/project_management.py +++ b/metagpt/actions/project_management.py @@ -107,7 +107,7 @@ class WriteTasks(Action): super().__init__(name, context, llm) def _save(self, context, rsp): - ws_name = CodeParser.parse_str(block="Python package name", text=context[-1].content) + ws_name = context[-1].instruct_content.dict()["Python package name"]#CodeParser.parse_str(block="Python package name", text=context[-1].content) file_path = WORKSPACE_ROOT / ws_name / 'docs/api_spec_and_tasks.md' file_path.write_text(rsp.content) diff --git a/metagpt/actions/write_prd_json.py b/metagpt/actions/write_prd_json.py index 9f44bb1f0..650f8334f 100644 --- a/metagpt/actions/write_prd_json.py +++ b/metagpt/actions/write_prd_json.py @@ -61,13 +61,14 @@ Requirements: According to the context, fill in the following missing informatio ## UI Design draft: Provide as Plain text. Be simple. Describe the elements and functions, also provide a simple style description and layout description. ## Anything UNCLEAR: Provide as Plain text. Make clear here. -Your job is to create a properly formatted JSON +Your job is to create a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like format example """ FORMAT_EXAMPLE = """ +[CONTENT] { "Original Requirements": "", "Search Information": "", - "mermaid quadrantChart code": ''' + "mermaid quadrantChart code": ' "title": "Reach and engagement of campaigns", "x-axis": "Low Reach --> High Reach", "y-axis": "Low Engagement --> High Engagement", @@ -82,7 +83,7 @@ FORMAT_EXAMPLE = """ "Campaign E": [0.40, 0.34], "Campaign F": [0.35, 0.78], "Our Target Product": [0.5, 0.6] - ''' + ' , }, @@ -93,10 +94,11 @@ FORMAT_EXAMPLE = """ "Competitive Analysis": [], "Competitive Quadrant Chart": "", "Requirement Analysis": "", - "Requirement Pool": [], + "Requirement Pool": [["P0","P0 requirement"],["P1","P1 requirement"]], "UI Design draft": "", "Anything UNCLEAR": "", } +[/CONTENT] """ OUTPUT_MAPPING = { "Original Requirements": (str, ...), @@ -105,7 +107,7 @@ OUTPUT_MAPPING = { "Competitive Analysis": (List[str], ...), "Competitive Quadrant Chart": (str, ...), "Requirement Analysis": (str, ...), - "Requirement Pool": (List[Tuple[str, str]], ...), + "Requirement Pool": (List[List[str]], ...), "UI Design draft":(str, ...), "Anything UNCLEAR": (str, ...), } diff --git a/metagpt/roles/architect.py b/metagpt/roles/architect.py index d0756672e..bd52665dc 100644 --- a/metagpt/roles/architect.py +++ b/metagpt/roles/architect.py @@ -6,7 +6,8 @@ @File : architect.py """ -from metagpt.actions import WriteDesign, WritePRD +from metagpt.actions import WriteDesign, WritePRD, WritePRDJson +from metagpt.actions.design_api_json import WriteDesignJson from metagpt.roles import Role @@ -30,9 +31,9 @@ class Architect(Role): super().__init__(name, profile, goal, constraints) # Initialize actions specific to the Architect role - self._init_actions([WriteDesign]) + self._init_actions([WriteDesignJson]) # Set events or actions the Architect should watch or be aware of - self._watch({WritePRD}) + self._watch({WritePRDJson}) \ No newline at end of file diff --git a/metagpt/roles/project_manager.py b/metagpt/roles/project_manager.py index dd4ba42ae..78d2f8d52 100644 --- a/metagpt/roles/project_manager.py +++ b/metagpt/roles/project_manager.py @@ -6,6 +6,7 @@ @File : project_manager.py """ from metagpt.actions import WriteDesign, WriteTasks +from metagpt.actions.design_api_json import WriteDesignJson from metagpt.roles import Role @@ -36,4 +37,4 @@ class ProjectManager(Role): """ super().__init__(name, profile, goal, constraints) self._init_actions([WriteTasks]) - self._watch([WriteDesign]) \ No newline at end of file + self._watch([WriteDesignJson]) \ No newline at end of file diff --git a/metagpt/utils/custom_decoder.py b/metagpt/utils/custom_decoder.py new file mode 100644 index 000000000..4c4aaa587 --- /dev/null +++ b/metagpt/utils/custom_decoder.py @@ -0,0 +1,230 @@ +import json +import re +from json import scanner, JSONDecodeError +from json.decoder import _decode_uXXXX +NUMBER_RE = re.compile( + r'(-?(?:0|[1-9]\d*))(\.\d+)?([eE][-+]?\d+)?', + (re.VERBOSE | re.MULTILINE | re.DOTALL)) +def py_make_scanner(context): + parse_object = context.parse_object + parse_array = context.parse_array + parse_string = context.parse_string + match_number = NUMBER_RE.match + strict = context.strict + parse_float = context.parse_float + parse_int = context.parse_int + parse_constant = context.parse_constant + object_hook = context.object_hook + object_pairs_hook = context.object_pairs_hook + memo = context.memo + + def _scan_once(string, idx): + try: + nextchar = string[idx] + except IndexError: + raise StopIteration(idx) from None + + if nextchar == '"' or nextchar == "'": + return parse_string(string, idx + 1, strict, delimiter=nextchar) + elif nextchar == '{': + return parse_object((string, idx + 1), strict, + _scan_once, object_hook, object_pairs_hook, memo) + elif nextchar == '[': + return parse_array((string, idx + 1), _scan_once) + elif nextchar == 'n' and string[idx:idx + 4] == 'null': + return None, idx + 4 + elif nextchar == 't' and string[idx:idx + 4] == 'true': + return True, idx + 4 + elif nextchar == 'f' and string[idx:idx + 5] == 'false': + return False, idx + 5 + + m = match_number(string, idx) + if m is not None: + integer, frac, exp = m.groups() + if frac or exp: + res = parse_float(integer + (frac or '') + (exp or '')) + else: + res = parse_int(integer) + return res, m.end() + elif nextchar == 'N' and string[idx:idx + 3] == 'NaN': + return parse_constant('NaN'), idx + 3 + elif nextchar == 'I' and string[idx:idx + 8] == 'Infinity': + return parse_constant('Infinity'), idx + 8 + elif nextchar == '-' and string[idx:idx + 9] == '-Infinity': + return parse_constant('-Infinity'), idx + 9 + else: + raise StopIteration(idx) + + def scan_once(string, idx): + try: + return _scan_once(string, idx) + finally: + memo.clear() + + return scan_once +FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL +STRINGCHUNK = re.compile(r'(.*?)(["\\\x00-\x1f])', FLAGS) +STRINGCHUNK_SINGLEQUOTE = re.compile(r'(.*?)([\'\\\x00-\x1f])', FLAGS) +BACKSLASH = { + '"': '"', '\\': '\\', '/': '/', + 'b': '\b', 'f': '\f', 'n': '\n', 'r': '\r', 't': '\t', +} +WHITESPACE = re.compile(r'[ \t\n\r]*', FLAGS) +WHITESPACE_STR = ' \t\n\r' +def JSONObject(s_and_end, strict, scan_once, object_hook, object_pairs_hook, + memo=None, _w=WHITESPACE.match, _ws=WHITESPACE_STR): + s, end = s_and_end + pairs = [] + pairs_append = pairs.append + # Backwards compatibility + if memo is None: + memo = {} + memo_get = memo.setdefault + # Use a slice to prevent IndexError from being raised, the following + # check will raise a more specific ValueError if the string is empty + nextchar = s[end:end + 1] + # Normally we expect nextchar == '"' + if nextchar != '"' and nextchar != "'": + if nextchar in _ws: + end = _w(s, end).end() + nextchar = s[end:end + 1] + # Trivial empty object + if nextchar == '}': + if object_pairs_hook is not None: + result = object_pairs_hook(pairs) + return result, end + 1 + pairs = {} + if object_hook is not None: + pairs = object_hook(pairs) + return pairs, end + 1 + elif nextchar != '"': + raise JSONDecodeError( + "Expecting property name enclosed in double quotes", s, end) + end += 1 + while True: + key, end = scanstring(s, end, strict, delimiter=nextchar) + key = memo_get(key, key) + # To skip some function call overhead we optimize the fast paths where + # the JSON key separator is ": " or just ":". + if s[end:end + 1] != ':': + end = _w(s, end).end() + if s[end:end + 1] != ':': + raise JSONDecodeError("Expecting ':' delimiter", s, end) + end += 1 + + try: + if s[end] in _ws: + end += 1 + if s[end] in _ws: + end = _w(s, end + 1).end() + except IndexError: + pass + + try: + value, end = scan_once(s, end) + except StopIteration as err: + raise JSONDecodeError("Expecting value", s, err.value) from None + pairs_append((key, value)) + try: + nextchar = s[end] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end] + except IndexError: + nextchar = '' + end += 1 + + if nextchar == '}': + break + elif nextchar != ',': + raise JSONDecodeError("Expecting ',' delimiter", s, end - 1) + end = _w(s, end).end() + nextchar = s[end:end + 1] + end += 1 + if nextchar != '"': + raise JSONDecodeError( + "Expecting property name enclosed in double quotes", s, end - 1) + if object_pairs_hook is not None: + result = object_pairs_hook(pairs) + return result, end + pairs = dict(pairs) + if object_hook is not None: + pairs = object_hook(pairs) + return pairs, end +def py_scanstring(s, end, strict=True, + _b=BACKSLASH, _m=STRINGCHUNK.match,delimiter='"'): + """Scan the string s for a JSON string. End is the index of the + character in s after the quote that started the JSON string. + Unescapes all valid JSON string escape sequences and raises ValueError + on attempt to decode an invalid string. If strict is False then literal + control characters are allowed in the string. + + Returns a tuple of the decoded string and the index of the character in s + after the end quote.""" + chunks = [] + _append = chunks.append + begin = end - 1 + if delimiter == '"': + _m = STRINGCHUNK.match + else: + _m = STRINGCHUNK_SINGLEQUOTE.match + while 1: + chunk = _m(s, end) + if chunk is None: + raise JSONDecodeError("Unterminated string starting at", s, begin) + end = chunk.end() + content, terminator = chunk.groups() + # Content is contains zero or more unescaped string characters + if content: + _append(content) + # Terminator is the end of string, a literal control character, + # or a backslash denoting that an escape sequence follows + if terminator == delimiter: + break + elif terminator != '\\': + if strict: + #msg = "Invalid control character %r at" % (terminator,) + msg = "Invalid control character {0!r} at".format(terminator) + raise JSONDecodeError(msg, s, end) + else: + _append(terminator) + continue + try: + esc = s[end] + except IndexError: + raise JSONDecodeError("Unterminated string starting at", + s, begin) from None + # If not a unicode escape sequence, must be in the lookup table + if esc != 'u': + try: + char = _b[esc] + except KeyError: + msg = "Invalid \\escape: {0!r}".format(esc) + raise JSONDecodeError(msg, s, end) + end += 1 + else: + uni = _decode_uXXXX(s, end) + end += 5 + if 0xd800 <= uni <= 0xdbff and s[end:end + 2] == '\\u': + uni2 = _decode_uXXXX(s, end + 1) + if 0xdc00 <= uni2 <= 0xdfff: + uni = 0x10000 + (((uni - 0xd800) << 10) | (uni2 - 0xdc00)) + end += 6 + char = chr(uni) + _append(char) + return ''.join(chunks), end +scanstring = py_scanstring +class CustomDecoder(json.JSONDecoder): + def __init__(self, *, object_hook=None, parse_float=None, + parse_int=None, parse_constant=None, strict=True, + object_pairs_hook=None): + super().__init__(object_hook=object_hook, parse_float=parse_float, parse_int=parse_int, + parse_constant=parse_constant, strict=strict,object_pairs_hook=object_pairs_hook) + self.parse_object = JSONObject + self.parse_string = py_scanstring + self.scan_once = py_make_scanner(self) + + def decode(self, s, _w=json.decoder.WHITESPACE.match): + + return super().decode(s) + diff --git a/requirements.txt b/requirements.txt index f8891c0ca..7f29c3343 100644 --- a/requirements.txt +++ b/requirements.txt @@ -38,4 +38,3 @@ typing-inspect==0.8.0 typing_extensions==4.5.0 libcst==1.0.1 qdrant-client==1.4.0 -#regex==2023.6.3 \ No newline at end of file From 58140f4598fc8059f1b26745de272b9097990be4 Mon Sep 17 00:00:00 2001 From: femto Date: Thu, 7 Sep 2023 21:59:11 +0800 Subject: [PATCH 03/22] aider --- .aider.chat.history.md | 1634 ---------------------------------------- .aider.input.history | 112 --- .python-version | 1 - 3 files changed, 1747 deletions(-) delete mode 100644 .aider.chat.history.md delete mode 100644 .aider.input.history delete mode 100644 .python-version diff --git a/.aider.chat.history.md b/.aider.chat.history.md deleted file mode 100644 index 9c7f94321..000000000 --- a/.aider.chat.history.md +++ /dev/null @@ -1,1634 +0,0 @@ - -# aider chat started at 2023-08-01 17:17:19 - -> Aider v0.10.1 -> Add .aider* to .gitignore (recommended)? y -> Added .aider* to .gitignore -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider --no-auto-commit -> No OpenAI API key provided. Use --openai-api-key or export OPENAI_API_KEY. - -# aider chat started at 2023-08-01 17:17:41 - -> Aider v0.10.1 -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider --no-auto-commit -> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k -> Model: gpt-3.5-turbo-16k -> Git repo: ../../.git -> Repo-map: disabled -> Git repo has uncommitted changes. -> Commit before the chat proceeds [y/n/commit message]? Added .aider* to .gitignore.y - -# aider chat started at 2023-08-01 17:18:15 - -> Aider v0.10.1 -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider --no-auto-commit --no-git -> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k -> Model: gpt-3.5-turbo-16k -> Git repo: none -> Repo-map: disabled -> Use /help to see in-chat commands, run with --help to see cmd line args - -#### /add pingpong_game/*.py -> Added pingpong_game/ball.py to the chat -> Added pingpong_game/settings.py to the chat -> Added pingpong_game/main.py to the chat -> Added pingpong_game/menu.py to the chat -> Added pingpong_game/game.py to the chat -> Added pingpong_game/paddle.py to the chat -> Added pingpong_game/leaderboard.py to the chat - -#### Traceback (most recent call last): -#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 36, in -#### main() -#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 28, in main -#### game.update() -#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/game.py", line 39, in update -#### self.player_paddle.update() -#### AttributeError: 'Paddle' object has no attribute 'update' , could you help me with that? - -Based on the error message, it seems that the `Paddle` class does not have an `update` method. To fix this issue, you need to add an `update` method to the `Paddle` class. - -Here's the updated code for the `Paddle` class: - -pingpong_game/paddle.py -``` -import pygame - -class Paddle: - def __init__(self): - self.x = 0 - self.y = 0 - self.width = 0 - self.height = 0 - self.speed = 0 - - def move_up(self): - """ - Move the paddle up. - """ - self.y -= self.speed - - def move_down(self): - """ - Move the paddle down. - """ - self.y += self.speed - - def update(self): - """ - Update the paddle position. - """ - pass - - def draw(self, screen): - """ - Draw the paddle on the screen. - """ - pygame.draw.rect(screen, (255, 255, 255), (self.x, self.y, self.width, self.height)) -``` - -Now the `Paddle` class has an empty `update` method, which can be called without causing an `AttributeError`. - -> Applied edit to pingpong_game/paddle.py - -#### but update is empty, could you give a meaningful implementation? - -Certainly! Here's an updated implementation for the `update` method in the `Paddle` class: - -pingpong_game/paddle.py -``` -import pygame - -class Paddle: - def __init__(self): - self.x = 0 - self.y = 0 - self.width = 0 - self.height = 0 - self.speed = 0 - - def move_up(self): - """ - Move the paddle up. - """ - self.y -= self.speed - - def move_down(self): - """ - Move the paddle down. - """ - self.y += self.speed - - def update(self): - """ - Update the paddle position. - """ - keys = pygame.key.get_pressed() - if keys[pygame.K_UP]: - self.move_up() - if keys[pygame.K_DOWN]: - self.move_down() - - def draw(self, screen): - """ - Draw the paddle on the screen. - """ - pygame.draw.rect(screen, (255, 255, 255), (self.x, self.y, self.width, self.height)) -``` - -In this updated implementation, the `update` method checks for key presses and moves the paddle accordingly. It uses the `pygame.key.get_pressed()` function to get the state of all keyboard keys, and if the up or down arrow key is pressed, it calls the `move_up` or `move_down` method respectively. - -Please replace the content of the `pingpong_game/paddle.py` file with the updated code above. - -> Applied edit to pingpong_game/paddle.py - -#### Traceback (most recent call last): -#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 36, in -#### main() -#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 29, in main -#### game.draw(screen) -#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/game.py", line 58, in draw -#### self.menu.draw(screen, self.score, self.level) -#### AttributeError: 'Menu' object has no attribute 'draw' - -The error is occurring because the `Menu` class does not have a `draw` method. To fix this, you can add a `draw` method to the `Menu` class that takes the `screen`, `score`, and `level` as parameters and draws the menu on the screen. - -Here's the updated code for the `Menu` class: - -```python -import pygame - -class Menu: - def __init__(self): - self.font = pygame.font.Font(None, 36) - self.start_button = Button("Start Game", (400, 200)) - self.settings_button = Button("Settings", (400, 300)) - self.leaderboard_button = Button("Leaderboard", (400, 400)) - self.quit_button = Button("Quit", (400, 500)) - - def show_menu(self, screen): - """ - Show the main menu screen. - """ - screen.fill((0, 0, 0)) - self.start_button.draw(screen) - self.settings_button.draw(screen) - self.leaderboard_button.draw(screen) - self.quit_button.draw(screen) - - pygame.display.update() - - def handle_event(self, event): - """ - Handle menu events. - """ - if event.type == pygame.MOUSEBUTTONDOWN: - if self.start_button.is_clicked(event.pos): - self.start_game() - elif self.settings_button.is_clicked(event.pos): - self.show_settings() - elif self.leaderboard_button.is_clicked(event.pos): - self.show_leaderboard() - elif self.quit_button.is_clicked(event.pos): - self.quit_game() - - def start_game(self): - """ - Start a new game. - """ - game.start_game() - - def show_settings(self): - """ - Show the settings screen. - """ - settings.show_settings() - - def show_leaderboard(self): - """ - Show the leaderboard screen. - """ - leaderboard.show_leaderboard() - - def quit_game(self): - """ - Quit the game. - """ - pygame.quit() - -class Button: - def __init__(self, text, position): - self.text = text - self.position = position - self.rect = pygame.Rect(position[0], position[1], 200, 50) - - def draw(self, screen): - """ - Draw the button on the screen. - """ - pygame.draw.rect(screen, (255, 255, 255), self.rect) - text_surface = self.font.render(self.text, True, (0, 0, 0)) - text_rect = text_surface.get_rect(center=self.rect.center) - screen.blit(text_surface, text_rect) - - def is_clicked(self, pos): - """ - Check if the button is clicked. - """ - return self.rect.collidepoint(pos) -``` - -Please replace the content of the `pingpong_game/menu.py` file with the updated code above. - -> Malformed response #1, retrying... -> No filename provided before ``` in file listing - -Apologies for the oversight. Here's the updated file listing for `pingpong_game/menu.py`: - -pingpong_game/menu.py -```python -import pygame - -class Menu: - def __init__(self): - self.font = pygame.font.Font(None, 36) - self.start_button = Button("Start Game", (400, 200)) - self.settings_button = Button("Settings", (400, 300)) - self.leaderboard_button = Button("Leaderboard", (400, 400)) - self.quit_button = Button("Quit", (400, 500)) - - def show_menu(self, screen): - """ - Show the main menu screen. - """ - screen.fill((0, 0, 0)) - self.start_button.draw(screen) - self.settings_button.draw(screen) - self.leaderboard_button.draw(screen) - self.quit_button.draw(screen) - - pygame.display.update() - - def handle_event(self, event): - """ - Handle menu events. - """ - if event.type == pygame.MOUSEBUTTONDOWN: - if self.start_button.is_clicked(event.pos): - self.start_game() - elif self.settings_button.is_clicked(event.pos): - self.show_settings() - elif self.leaderboard_button.is_clicked(event.pos): - self.show_leaderboard() - elif self.quit_button.is_clicked(event.pos): - self.quit_game() - - def start_game(self): - """ - Start a new game. - """ - game.start_game() - - def show_settings(self): - """ - Show the settings screen. - """ - settings.show_settings() - - def show_leaderboard(self): - """ - Show the leaderboard screen. - """ - leaderboard.show_leaderboard() - - def quit_game(self): - """ - Quit the game. - """ - pygame.quit() - -class Button: - def __init__(self, text, position): - self.text = text - self.position = position - self.rect = pygame.Rect(position[0], position[1], 200, 50) - - def draw(self, screen): - """ - Draw the button on the screen. - """ - pygame.draw.rect(screen, (255, 255, 255), self.rect) - text_surface = self.font.render(self.text, True, (0, 0, 0)) - text_rect = text_surface.get_rect(center=self.rect.center) - screen.blit(text_surface, text_rect) - - def is_clicked(self, pos): - """ - Check if the button is clicked. - """ - return self.rect.collidepoint(pos) -``` - -Please replace the content of the `pingpong_game/menu.py` file with the updated code above. - -> Applied edit to pingpong_game/menu.py - -#### AttributeError: 'Menu' object has no attribute 'draw' - -The error is occurring because the `Menu` class does not have a `draw` method. To fix this, you can add a `draw` method to the `Menu` class that takes the `screen`, `score`, and `level` as parameters and draws the menu on the screen. - -Here's the updated code for the `Menu` class: - -pingpong_game/menu.py -``` -import pygame - -class Menu: - def __init__(self): - self.font = pygame.font.Font(None, 36) - self.start_button = Button("Start Game", (400, 200)) - self.settings_button = Button("Settings", (400, 300)) - self.leaderboard_button = Button("Leaderboard", (400, 400)) - self.quit_button = Button("Quit", (400, 500)) - - def show_menu(self, screen): - """ - Show the main menu screen. - """ - screen.fill((0, 0, 0)) - self.start_button.draw(screen) - self.settings_button.draw(screen) - self.leaderboard_button.draw(screen) - self.quit_button.draw(screen) - - pygame.display.update() - - def handle_event(self, event): - """ - Handle menu events. - """ - if event.type == pygame.MOUSEBUTTONDOWN: - if self.start_button.is_clicked(event.pos): - self.start_game() - elif self.settings_button.is_clicked(event.pos): - self.show_settings() - elif self.leaderboard_button.is_clicked(event.pos): - self.show_leaderboard() - elif self.quit_button.is_clicked(event.pos): - self.quit_game() - - def start_game(self): - """ - Start a new game. - """ - game.start_game() - - def show_settings(self): - """ - Show the settings screen. - """ - settings.show_settings() - - def show_leaderboard(self): - """ - Show the leaderboard screen. - """ - leaderboard.show_leaderboard() - - def quit_game(self): - """ - Quit the game. - """ - pygame.quit() - - def draw(self, screen, score, level): - """ - Draw the menu on the screen. - """ - self.show_menu(screen) - self.draw_score(screen, score) - self.draw_level(screen, level) - - def draw_score(self, screen, score): - """ - Draw the score on the screen. - """ - score_text = self.font.render("Score: " + str(score), True, (255, 255, 255)) - screen.blit(score_text, (10, 10)) - - def draw_level(self, screen, level): - """ - Draw the level on the screen. - """ - level_text = self.font.render("Level: " + str(level), True, (255, 255, 255)) - screen.blit(level_text, (10, 50)) - -class Button: - def __init__(self, text, position): - self.text = text - self.position = position - self.rect = pygame.Rect(position[0], position[1], 200, 50) - - def draw(self, screen): - """ - Draw the button on the screen. - """ - pygame.draw.rect(screen, (255, 255, 255), self.rect) - text_surface = self.font.render(self.text, True, (0, 0, 0)) - text_rect = text_surface.get_rect(center=self.rect.center) - screen.blit(text_surface, text_rect) - - def is_clicked(self, pos): - """ - Check if the button is clicked. - """ - return self.rect.collidepoint(pos) -``` - -Please replace the content of the `pingpong_game/menu.py` file with the updated code above. - -> Applied edit to pingpong_game/menu.py - -#### Traceback (most recent call last): -#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 36, in -#### main() -#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 29, in main -#### game.draw(screen) -#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/game.py", line 58, in draw -#### self.menu.draw(screen, self.score, self.level) -#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/menu.py", line 65, in draw -#### self.show_menu(screen) -#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/menu.py", line 16, in show_menu -#### self.start_button.draw(screen) -#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/menu.py", line 94, in draw -#### text_surface = self.font.render(self.text, True, (0, 0, 0)) -#### AttributeError: 'Button' object has no attribute 'font' - -The error is occurring because the `Button` class in the `menu.py` file does not have a `font` attribute. To fix this, you can add a `font` attribute to the `Button` class and initialize it in the constructor. - -Here's the updated code for the `Button` class: - -```python -import pygame - -class Button: - def __init__(self, text, position): - self.text = text - self.position = position - self.rect = pygame.Rect(position[0], position[1], 200, 50) - self.font = pygame.font.Font(None, 36) - - def draw(self, screen): - """ - Draw the button on the screen. - """ - pygame.draw.rect(screen, (255, 255, 255), self.rect) - text_surface = self.font.render(self.text, True, (0, 0, 0)) - text_rect = text_surface.get_rect(center=self.rect.center) - screen.blit(text_surface, text_rect) - - def is_clicked(self, pos): - """ - Check if the button is clicked. - """ - return self.rect.collidepoint(pos) -``` - -Please replace the content of the `pingpong_game/menu.py` file with the updated code above. - -> Malformed response #1, retrying... -> No filename provided before ``` in file listing - -pingpong_game/menu.py -``` -import pygame - -class Menu: - def __init__(self): - self.font = pygame.font.Font(None, 36) - self.start_button = Button("Start Game", (400, 200)) - self.settings_button = Button("Settings", (400, 300)) - self.leaderboard_button = Button("Leaderboard", (400, 400)) - self.quit_button = Button("Quit", (400, 500)) - - def show_menu(self, screen): - """ - Show the main menu screen. - """ - screen.fill((0, 0, 0)) - self.start_button.draw(screen) - self.settings_button.draw(screen) - self.leaderboard_button.draw(screen) - self.quit_button.draw(screen) - - pygame.display.update() - - def handle_event(self, event): - """ - Handle menu events. - """ - if event.type == pygame.MOUSEBUTTONDOWN: - if self.start_button.is_clicked(event.pos): - self.start_game() - elif self.settings_button.is_clicked(event.pos): - self.show_settings() - elif self.leaderboard_button.is_clicked(event.pos): - self.show_leaderboard() - elif self.quit_button.is_clicked(event.pos): - self.quit_game() - - def start_game(self): - """ - Start a new game. - """ - game.start_game() - - def show_settings(self): - """ - Show the settings screen. - """ - settings.show_settings() - - def show_leaderboard(self): - """ - Show the leaderboard screen. - """ - leaderboard.show_leaderboard() - - def quit_game(self): - """ - Quit the game. - """ - pygame.quit() - - def draw(self, screen, score, level): - """ - Draw the menu on the screen. - """ - self.show_menu(screen) - self.draw_score(screen, score) - self.draw_level(screen, level) - - def draw_score(self, screen, score): - """ - Draw the score on the screen. - """ - score_text = self.font.render("Score: " + str(score), True, (255, 255, 255)) - screen.blit(score_text, (10, 10)) - - def draw_level(self, screen, level): - """ - Draw the level on the screen. - """ - level_text = self.font.render("Level: " + str(level), True, (255, 255, 255)) - screen.blit(level_text, (10, 50)) - -class Button: - def __init__(self, text, position): - self.text = text - self.position = position - self.rect = pygame.Rect(position[0], position[1], 200, 50) - self.font = pygame.font.Font(None, 36) - - def draw(self, screen): - """ - Draw the button on the screen. - """ - pygame.draw.rect(screen, (255, 255, 255), self.rect) - text_surface = self.font.render(self.text, True, (0, 0, 0)) - text_rect = text_surface.get_rect(center=self.rect.center) - screen.blit(text_surface, text_rect) - - def is_clicked(self, pos): - """ - Check if the button is clicked. - """ - return self.rect.collidepoint(pos) -``` - -> Applied edit to pingpong_game/menu.py - -#### when I click the button on the menu, nothing happens, could you help me debug it? - -To debug the button click issue in the menu, we can add some print statements to check if the button click events are being registered correctly. - -Here's the updated code for the `Menu` class with added print statements: - -```python -import pygame - -class Menu: - def __init__(self): - self.font = pygame.font.Font(None, 36) - self.start_button = Button("Start Game", (400, 200)) - self.settings_button = Button("Settings", (400, 300)) - self.leaderboard_button = Button("Leaderboard", (400, 400)) - self.quit_button = Button("Quit", (400, 500)) - - def show_menu(self, screen): - """ - Show the main menu screen. - """ - screen.fill((0, 0, 0)) - self.start_button.draw(screen) - self.settings_button.draw(screen) - self.leaderboard_button.draw(screen) - self.quit_button.draw(screen) - - pygame.display.update() - - def handle_event(self, event): - """ - Handle menu events. - """ - if event.type == pygame.MOUSEBUTTONDOWN: - print("Mouse button down event") - if self.start_button.is_clicked(event.pos): - print("Start button clicked") - self.start_game() - elif self.settings_button.is_clicked(event.pos): - print("Settings button clicked") - self.show_settings() - elif self.leaderboard_button.is_clicked(event.pos): - print("Leaderboard button clicked") - self.show_leaderboard() - elif self.quit_button.is_clicked(event.pos): - print("Quit button clicked") - self.quit_game() - - def start_game(self): - """ - Start a new game. - """ - game.start_game() - - def show_settings(self): - """ - Show the settings screen. - """ - settings.show_settings() - - def show_leaderboard(self): - """ - Show the leaderboard screen. - """ - leaderboard.show_leaderboard() - - def quit_game(self): - """ - Quit the game. - """ - pygame.quit() - - def draw(self, screen, score, level): - """ - Draw the menu on the screen. - """ - self.show_menu(screen) - self.draw_score(screen, score) - self.draw_level(screen, level) - - def draw_score(self, screen, score): - """ - Draw the score on the screen. - """ - score_text = self.font.render("Score: " + str(score), True, (255, 255, 255)) - screen.blit(score_text, (10, 10)) - - def draw_level(self, screen, level): - """ - Draw the level on the screen. - """ - level_text = self.font.render("Level: " + str(level), True, (255, 255, 255)) - screen.blit(level_text, (10, 50)) - -class Button: - def __init__(self, text, position): - self.text = text - self.position = position - self.rect = pygame.Rect(position[0], position[1], 200, 50) - self.font = pygame.font.Font(None, 36) - - def draw(self, screen): - """ - Draw the button on the screen. - """ - pygame.draw.rect(screen, (255, 255, 255), self.rect) - text_surface = self.font.render(self.text, True, (0, 0, 0)) - text_rect = text_surface.get_rect(center=self.rect.center) - screen.blit(text_surface, text_rect) - - def is_clicked(self, pos): - """ - Check if the button is clicked. - """ - return self.rect.collidepoint(pos) -``` - -Please replace the content of the `pingpong_game/menu.py` file with the updated code above. After making these changes, run the program again and check the console output for the button click events. This will help us identify if the button clicks are being registered correctly. - -> Malformed response #1, retrying... -> No filename provided before ``` in file listing - -# aider chat started at 2023-08-01 17:29:32 - -> Aider v0.10.1 -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider -> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k -> Model: gpt-3.5-turbo-16k -> Git repo: ../../.git -> Repo-map: disabled -> Git repo has uncommitted changes. -> Commit before the chat proceeds [y/n/commit message]? Added .aider* to .gitignore.n - -# aider chat started at 2023-08-01 17:30:02 - -> Aider v0.10.1 -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider --no-git -> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k -> Model: gpt-3.5-turbo-16k -> Git repo: none -> Repo-map: disabled -> Use /help to see in-chat commands, run with --help to see cmd line args - -#### /tokens -> Approximate context window usage, in tokens: -> 271 system messages -> ======== -> 271 tokens total -> 16,113 tokens remaining in context window -> 16,384 tokens max context window size - -#### /add pingpong_game/*.py -> Added pingpong_game/leaderboard.py to the chat -> Added pingpong_game/menu.py to the chat -> Added pingpong_game/paddle.py to the chat -> Added pingpong_game/game.py to the chat -> Added pingpong_game/main.py to the chat -> Added pingpong_game/settings.py to the chat -> Added pingpong_game/ball.py to the chat - -#### when I click on the menu, nothing happens, could you help me debug it? -> ^C again to exit - -Sure, I can help you debug the menu. Let's start by checking the `handle_event` method in the `Menu` class. It seems that the event handling logic is missing. We need to add the logic to handle mouse button down events and call the appropriate methods based on the button clicked. - -Here's the updated `handle_event` method: - -pingpong_game/menu.py -``` -import pygame - -class Menu: - def __init__(self): - self.font = pygame.font.Font(None, 36) - self.start_button = Button("Start Game", (400, 200)) - self.settings_button = Button("Settings", (400, 300)) - self.leaderboard_button = Button("Leaderboard", (400, 400)) - self.quit_button = Button("Quit", (400, 500)) - - def show_menu(self, screen): - """ - Show the main menu screen. - """ - screen.fill((0, 0, 0)) - self.start_button.draw(screen) - self.settings_button.draw(screen) - self.leaderboard_button.draw(screen) - self.quit_button.draw(screen) - - pygame.display.update() - - def handle_event(self, event): - """ - Handle menu events. - """ - if event.type == pygame.MOUSEBUTTONDOWN: - if self.start_button.is_clicked(event.pos): - self.start_game() - elif self.settings_button.is_clicked(event.pos): - self.show_settings() - elif self.leaderboard_button.is_clicked(event.pos): - self.show_leaderboard() - elif self.quit_button.is_clicked(event.pos): - self.quit_game() - - def start_game(self): - """ - Start a new game. - """ - game.start_game() - - def show_settings(self): - """ - Show the settings screen. - """ - settings.show_settings() - - def show_leaderboard(self): - """ - Show the leaderboard screen. - """ - leaderboard.show_leaderboard() - - def quit_game(self): - """ - Quit the game. - """ - pygame.quit() - - def draw(self, screen, score, level): - """ - Draw the menu on the screen. - """ - self.show_menu(screen) - self.draw_score(screen, score) - self.draw_level(screen, level) - - def draw_score(self, screen, score): - """ - Draw the score on the screen. - """ - score_text = self.font.render("Score: " + str(score), True, (255, 255, 255)) - screen.blit(score - - -#### the Menu's handle_event seemed not be called, could you call that in the event loop? - -Certainly! It seems that the `handle_event` method of the `Menu` class is not being called in the event loop. To fix this, we need to add a call to `menu.handle_event(event)` inside the event loop in the `main` function. - -Here's the updated `main` function: - -pingpong_game/main.py -``` -import pygame -from game import Game -from menu import Menu -from settings import Settings -from leaderboard import Leaderboard - -def main(): - pygame.init() - screen = pygame.display.set_mode((800, 600)) - pygame.display.set_caption("Ping Pong Game") - - game = Game() - menu = Menu() - settings = Settings() - leaderboard = Leaderboard() - - running = True - while running: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - running = False - elif event.type == pygame.KEYDOWN: - if event.key == pygame.K_UP: - game.player_paddle.move_up() - elif event.key == pygame.K_DOWN: - game.player_paddle.move_down() - elif event.type == pygame.MOUSEBUTTONDOWN: - menu.handle_event(event) # Call handle_event method of the Menu class - - game.update() - game.draw(screen) - - pygame.display.flip() - - pygame.quit() - -if __name__ == "__main__": - main() -``` - -Now, when you click on the menu buttons, the `handle_event` method of the `Menu` class will be called and the appropriate actions will be performed based on the button clicked. - -> Applied edit to pingpong_game/main.py - -#### Traceback (most recent call last): -#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 38, in -#### main() -#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 28, in main -#### menu.handle_event(event) # Call handle_event method of the Menu class -#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/menu.py", line 29, in handle_event -#### self.start_game() -#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/menu.py", line 41, in start_game -#### game.start_game() -#### NameError: name 'game' is not defined. Did you mean: 'pygame'? - -It seems that there is a `NameError` in the `start_game` method of the `Menu` class. The error message suggests that the name 'game' is not defined. - -To fix this issue, we need to pass the `game` object as a parameter to the `Menu` class so that it can access the `start_game` method. - -Here's the updated code: - -pingpong_game/main.py -``` -import pygame -from game import Game -from menu import Menu -from settings import Settings -from leaderboard import Leaderboard - -def main(): - pygame.init() - screen = pygame.display.set_mode((800, 600)) - pygame.display.set_caption("Ping Pong Game") - - game = Game() - menu = Menu(game) # Pass the game object to the Menu class - settings = Settings() - leaderboard = Leaderboard() - - running = True - while running: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - running = False - elif event.type == pygame.KEYDOWN: - if event.key == pygame.K_UP: - game.player_paddle.move_up() - elif event.key == pygame.K_DOWN: - game.player_paddle.move_down() - elif event.type == pygame.MOUSEBUTTONDOWN: - menu.handle_event(event) - - game.update() - game.draw(screen) - - pygame.display.flip() - - pygame.quit() - -if __name__ == "__main__": - main() -``` - -pingpong_game/menu.py -``` -import pygame - -class Menu: - def __init__(self, game): # Add game parameter to the constructor - self.font = pygame.font.Font(None, 36) - self.start_button = Button("Start Game", (400, 200)) - self.settings_button = Button("Settings", (400, 300)) - self.leaderboard_button = Button("Leaderboard", (400, 400)) - self.quit_button = Button("Quit", (400, 500)) - self.game = game # Store the game object - - def show_menu(self, screen): - """ - Show the main menu screen. - """ - screen.fill((0, 0, 0)) - self.start_button.draw(screen) - self.settings_button.draw(screen) - self.leaderboard_button.draw(screen) - self.quit_button.draw(screen) - - pygame.display.update() - - def handle_event(self, event): - """ - Handle menu events. - """ - if event.type == pygame.MOUSEBUTTONDOWN: - if self.start_button.is_clicked(event.pos): - self.start_game() - elif self.settings_button.is_clicked(event.pos): - self.show_settings() - elif self.leaderboard_button.is_clicked(event.pos): - self.show_leaderboard() - elif self.quit_button.is_clicked(event.pos): - self.quit_game() - - def start_game(self): - """ - Start a new game. - """ - self.game.start_game() # Call the start_game method of the game object - - def show_settings(self): - """ - Show the settings screen. - """ - settings.show_settings() - - def show_leaderboard(self): - """ - Show the leaderboard screen. - """ - leaderboard.show_leaderboard() - - def quit_game(self): - """ - Quit the game. - """ - pygame.quit() - - def draw(self, screen, score, level): - """ - Draw the menu on the screen. - """ - self.show_menu(screen) - self.draw_score(screen, score) - self.draw_level(screen, level) - - def draw_score(self, screen, score): - """ - Draw the score on the screen. - """ - score_text = self.font.render("Score: " + str(score), True, (255, 255, 255)) - screen.blit(score_text, (10, 10)) - - def draw_level(self, screen, level): - """ - Draw the level on the screen. - """ - level_text = self.font.render("Level: " + str(level), True, (255, 255, 255)) - screen.blit(level_text, (10, 50)) - -class Button: - def __init__(self, text, position): - self.text = text - self.position = position - self.rect = pygame.Rect(position[0], position[1], 200, 50) - self.font = pygame.font.Font(None, 36) - - def draw(self, screen): - """ - Draw the button on the screen. - """ - pygame.draw.rect(screen, (255, 255, 255), self.rect) - text_surface = self.font.render(self.text, True, (0, 0, 0)) - text_rect = text_surface.get_rect(center=self.rect.center) - screen.blit(text_surface, text_rect) - - def is_clicked(self, pos): - """ - Check if the button is clicked. - """ - return self.rect.collidepoint(pos) -``` - -Now, the `game` object is passed to the `Menu` class, and the `start_game` method is called on the `game` object when the "Start Game" button is clicked. - -> Applied edit to pingpong_game/main.py -> Applied edit to pingpong_game/menu.py - -#### Traceback (most recent call last): -#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 38, in -#### main() -#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 12, in main -#### game = Game() -#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/game.py", line 15, in __init__ -#### self.menu = Menu() -#### TypeError: Menu.__init__() missing 1 required positional argument: 'game' - -To fix the error, we need to pass the `game` object to the `Menu` class when creating an instance of it in the `Game` class. - -Here's the updated code: - -pingpong_game/game.py -``` -import pygame -from paddle import Paddle -from ball import Ball -from menu import Menu -from settings import Settings -from leaderboard import Leaderboard - -class Game: - def __init__(self): - self.score = 0 - self.level = 1 - self.player_paddle = Paddle() - self.computer_paddle = Paddle() - self.ball = Ball() - self.menu = Menu(self) # Pass the game object to the Menu class - self.settings = Settings() - self.leaderboard = Leaderboard() - - def start_game(self): - """ - Start a new game. - """ - self.score = 0 - self.level = 1 - self.player_paddle.reset() - self.computer_paddle.reset() - self.ball.reset() - - def end_game(self): - """ - End the current game. - """ - self.menu.show_end_screen(self.score) - - def update(self): - """ - Update the game state. - """ - self.player_paddle.update() - self.computer_paddle.update() - self.ball.update() - - if self.ball.collides_with_paddle(self.player_paddle) or self.ball.collides_with_paddle(self.computer_paddle): - self.ball.bounce() - - if self.ball.is_out_of_bounds(): - self.score += 1 - self.ball.reset() - - def draw(self, screen): - """ - Draw the game on the screen. - """ - screen.fill((0, 0, 0)) - self.player_paddle.draw(screen) - self.computer_paddle.draw(screen) - self.ball.draw(screen) - self.menu.draw(screen, self.score, self.level) - - pygame.display.update() -``` - -pingpong_game/menu.py -``` -import pygame - -class Menu: - def __init__(self, game): # Add game parameter to the constructor - self.font = pygame.font.Font(None, 36) - self.start_button = Button("Start Game", (400, 200)) - self.settings_button = Button("Settings", (400, 300)) - self.leaderboard_button = Button("Leaderboard", (400, 400)) - self.quit_button = Button("Quit", (400, 500)) - self.game = game # Store the game object - - # Rest of the code... -``` - -Now the `Menu` class will receive the `game` object when it is created, and we can access the `start_game` method of the `Game` class from within the `Menu` class. - -> Applied edit to pingpong_game/game.py -> Applied edit to pingpong_game/menu.py - -#### Traceback (most recent call last): -#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 38, in -#### main() -#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 12, in main -#### game = Game() -#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/game.py", line 15, in __init__ -#### self.menu = Menu(self) # Pass the game object to the Menu class -#### File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/menu.py", line 6, in __init__ -#### self.start_button = Button("Start Game", (400, 200)) -#### NameError: name 'Button' is not defined - -It seems that the `Button` class is not defined in the `menu.py` file. To fix this error, we need to define the `Button` class or import it from another module. - -Could you please provide the code for the `Button` class or let me know if it should be imported from another module? - - -# aider chat started at 2023-08-01 19:54:08 - -> Aider v0.10.1 -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider -> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 -> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k -> Model: gpt-3.5-turbo-16k -> Git repo: ../../.git -> Repo-map: disabled - -# aider chat started at 2023-08-01 20:20:05 - -> Aider v0.10.1 -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider -> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 -> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k -> Model: gpt-3.5-turbo-16k -> Git repo: ../../.git -> Repo-map: disabled - -# aider chat started at 2023-08-01 20:21:24 - -> Aider v0.10.1 -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider -> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 -> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k -> Model: gpt-3.5-turbo-16k -> Git repo: ../../.git -> Repo-map: disabled - -# aider chat started at 2023-08-01 20:24:39 - -> Aider v0.10.1 -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider -> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 -> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k -> Model: gpt-3.5-turbo-16k -> Git repo: ../../.git -> Repo-map: disabled - -# aider chat started at 2023-08-01 20:40:26 - -> Aider v0.10.1 -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider -> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 -> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k -> Model: gpt-3.5-turbo-16k -> Git repo: ../../.git -> Repo-map: disabled - -# aider chat started at 2023-08-01 21:51:33 - -> Aider v0.10.1 -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider -> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 -> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k -> Model: gpt-3.5-turbo-16k -> Git repo: ../../.git -> Repo-map: disabled - -# aider chat started at 2023-08-01 22:33:22 - -> Aider v0.10.1 -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider -> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 -> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k -> Model: gpt-3.5-turbo-16k -> Git repo: ../../.git -> Repo-map: disabled - -# aider chat started at 2023-08-01 23:19:05 - -> Aider v0.10.1 -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider -> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 -> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k -> Model: gpt-3.5-turbo-16k -> Git repo: ../../.git -> Repo-map: disabled - -# aider chat started at 2023-08-02 12:14:50 - -> Aider v0.10.1 -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider -> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 -> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k -> Model: gpt-3.5-turbo-16k - -# aider chat started at 2023-08-02 14:01:48 - -> Aider v0.10.1 -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider -> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 -> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k -> Model: gpt-3.5-turbo-16k - -# aider chat started at 2023-08-03 12:36:19 - -> Aider v0.10.1 -> Newer version v0.11.0 is available. To upgrade, run: -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/python3.10 -m pip install --upgrade aider-chat -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider -> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 -> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k -> Model: gpt-3.5-turbo-16k - -# aider chat started at 2023-08-03 13:13:44 - -> Aider v0.11.0 -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider -> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 -> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k -> Model: gpt-3.5-turbo-16k - -# aider chat started at 2023-08-03 16:00:55 - -> Aider v0.11.0 -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider -> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 -> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k -> Model: gpt-3.5-turbo-16k - -# aider chat started at 2023-08-03 19:51:56 - -> Aider v0.11.0 -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider -> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 -> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k -> Model: gpt-3.5-turbo-16k - -# aider chat started at 2023-08-03 20:49:59 - -> Aider v0.11.0 -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider -> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 -> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k -> Model: gpt-3.5-turbo-16k - -# aider chat started at 2023-08-04 12:35:48 - -> Aider v0.11.0 -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider -> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 -> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k -> Model: gpt-3.5-turbo-16k - -# aider chat started at 2023-08-04 16:50:13 - -> Aider v0.11.0 -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider -> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 -> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k -> Model: gpt-3.5-turbo-16k -> Git repo: .git -> Repo-map: disabled -> Use /help to see in-chat commands, run with --help to see cmd line args -> Git repo has uncommitted changes. - -# aider chat started at 2023-08-05 12:33:32 - -> Aider v0.11.0 - -# aider chat started at 2023-08-05 20:54:44 - -> Aider v0.11.0 -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider -> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 - -# aider chat started at 2023-08-05 20:54:51 - -> Aider v0.11.0 -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider --no-auto-commit -> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 -> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k -> Model: gpt-3.5-turbo-16k -> Git repo: .git -> Repo-map: disabled -> Use /help to see in-chat commands, run with --help to see cmd line args - -#### /tokens -> Approximate context window usage, in tokens: -> $ 0.00 306 system messages -> ================ -> $ 0.00 306 tokens total -> 16,078 tokens remaining in context window -> 16,384 tokens max context window size - -#### /add start -> No files matched 'start'. Do you want to create the file? n - -#### /add startup -> No files matched 'startup'. Do you want to create the file? n - -#### /add startup.py -> Added startup.py to the chat - -#### could you change line 53 to according to coding standard? - -startup.py -``` -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import asyncio - -import fire - -from metagpt.roles import Architect, Engineer, ProductManager, ProjectManager -from metagpt.software_company import SoftwareCompany -import yaml - -def read_config(filename): - with open(filename, 'r') as file: - config_data = yaml.safe_load(file) - return config_data - - -def instantiate_class(item, **options): - if isinstance(item, str): - # If item is a string, instantiate the class directly - class_obj = globals().get(item) - if class_obj is None: - raise ValueError(f"Class '{item}' not found in the global namespace.") - return class_obj() - elif isinstance(item, dict): - # If item is a dictionary, it should contain class name and params - class_name, params = next(iter(item.items())) - if isinstance(params, dict): - # Process the parameters based on the options - for key, value in params.items(): - if isinstance(value, str) and value.startswith("{") and value.endswith("}"): - param_key = value.strip("{}") - params[key] = options.get(param_key, value) # Use the option value or keep the original string - class_obj = globals().get(class_name) - if class_obj is None: - raise ValueError(f"Class '{class_name}' not found in the global namespace.") - return class_obj(**params) - else: - raise ValueError("Invalid item in team_config_1['team']. Each item should be a string or a dictionary.") - - -async def startup(idea: str, investment: float = 3.0, n_round: int = 5, code_review: bool = False): - """Run a startup. Be a boss.""" - company = SoftwareCompany() - # company.hire([ProductManager(), - # Architect(), - # ProjectManager(), - # Engineer(n_borg=5, use_code_review=code_review)]) - team_config_1 = read_config('team_config_1.yaml') - # Instantiate classes from the class names provided in team_config_1 - team = [instantiate_class(item, idea=idea,investment=investment, n_round=n_round, code_review=code_review) for item in team_config_1['team']] - company.hire(team) - company.invest(investment) - company.start_project(idea) - await company.run(n_round=n_round) - - -def main(idea: str, investment: float = 3.0, n_round: int = 5, code_review: bool = False): - """ - We are a software startup comprised of AI. By investing in us, you are empowering a future filled with limitless possibilities. - :param idea: Your innovative idea, such as "Creating a snake game." - :param investment: As an investor, you have the opportunity to contribute a certain dollar amount to this AI company. - :param n_round: - :param code_review: Whether to use code review. - :return: - """ - asyncio.run(startup(idea, investment, n_round, code_review)) - - -if __name__ == '__main__': - fire.Fire(main) -``` - -> Applied edit to startup.py - -#### could you change the code to standard? - -startup.py -``` -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import asyncio -import fire -import yaml - -from metagpt.roles import Architect, Engineer, ProductManager, ProjectManager -from metagpt.software_company import SoftwareCompany - - -def read_config(filename): - with open(filename, 'r') as file: - config_data = yaml.safe_load(file) - return config_data - - -def instantiate_class(item, **options): - if isinstance(item, str): - # If item is a string, instantiate the class directly - class_obj = globals().get(item) - if class_obj is None: - raise ValueError(f"Class '{item}' not found in the global namespace.") - return class_obj() - elif isinstance(item, dict): - # If item is a dictionary, it should contain class name and params - class_name, params = next(iter(item.items())) - if isinstance(params, dict): - # Process the parameters based on the options - for key, value in params.items(): - if isinstance(value, str) and value.startswith("{") and value.endswith("}"): - param_key = value.strip("{}") - params[key] = options.get(param_key, value) # Use the option value or keep the original string - class_obj = globals().get(class_name) - if class_obj is None: - raise ValueError(f"Class '{class_name}' not found in the global namespace.") - return class_obj(**params) - else: - raise ValueError("Invalid item in team_config_1['team']. Each item should be a string or a dictionary.") - - -async def startup(idea: str, investment: float = 3.0, n_round: int = 5, code_review: bool = False): - """Run a startup. Be a boss.""" - company = SoftwareCompany() - # company.hire([ProductManager(), - # Architect(), - # ProjectManager(), - # Engineer(n_borg=5, use_code_review=code_review)]) - team_config_1 = read_config('team_config_1.yaml') - # Instantiate classes from the class names provided in team_config_1 - team = [instantiate_class(item, idea=idea, investment=investment, n_round=n_round, code_review=code_review) for item in team_config_1['team']] - company.hire(team) - company.invest(investment) - company.start_project(idea) - await company.run(n_round=n_round) - - -def main(idea: str, investment: float = 3.0, n_round: int = 5, code_review: bool = False): - """ - We are a software startup comprised of AI. By investing in us, you are empowering a future filled with limitless possibilities. - :param idea: Your innovative idea, such as "Creating a snake game." - :param investment: As an investor, you have the opportunity to contribute a certain dollar amount to this AI company. - :param n_round: - :param code_review: Whether to use code review. - :return: - """ - asyncio.run(startup(idea, investment, n_round, code_review)) - - -if __name__ == '__main__': - fire.Fire(main) -``` - -> Applied edit to startup.py - -# aider chat started at 2023-08-10 11:43:21 - -> Aider v0.11.0 -> Newer version v0.11.1 is available. To upgrade, run: -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/python3.10 -m pip install --upgrade aider-chat -> Add .aider* to .gitignore (recommended)? y -> Added .aider* to .gitignore -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider -> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 -> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k -> Model: gpt-3.5-turbo-16k -> Git repo: .git -> Repo-map: disabled -> Use /help to see in-chat commands, run with --help to see cmd line args -> Git repo has uncommitted changes. -> Commit before the chat proceeds [y/n/commit message]? y -> Commit 855bb68 Added .aider* to .gitignore. -> ^C again to exit -> ^C KeyboardInterrupt - -# aider chat started at 2023-08-13 21:49:23 - -> Aider v0.11.0 -> Newer version v0.12.0 is available. To upgrade, run: -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/python3.10 -m pip install --upgrade aider-chat -> Add .aider* to .gitignore (recommended)? y -> Added .aider* to .gitignore -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider -> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 -> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k -> Model: gpt-3.5-turbo-16k -> Git repo: .git -> Repo-map: disabled -> Use /help to see in-chat commands, run with --help to see cmd line args -> Git repo has uncommitted changes. -> Commit before the chat proceeds [y/n/commit message]? y -> Commit 360b77e Added .aider* to .gitignore. -> ^C again to exit -> ^C KeyboardInterrupt - -# aider chat started at 2023-08-14 13:54:34 - -> Aider v0.11.0 -> Newer version v0.12.0 is available. To upgrade, run: -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/python3.10 -m pip install --upgrade aider-chat -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider -> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 -> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k -> Model: gpt-3.5-turbo-16k - -# aider chat started at 2023-08-15 12:21:54 - -> Aider v0.11.0 -> Newer version v0.12.0 is available. To upgrade, run: -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/python3.10 -m pip install --upgrade aider-chat - -# aider chat started at 2023-08-17 17:39:50 - -> Aider v0.11.0 -> Newer version v0.12.0 is available. To upgrade, run: -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/python3.10 -m pip install --upgrade aider-chat -> Add .aider* to .gitignore (recommended)? y -> Added .aider* to .gitignore -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider -> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 -> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k -> Model: gpt-3.5-turbo-16k -> Git repo: .git -> Repo-map: disabled -> Use /help to see in-chat commands, run with --help to see cmd line args -> Git repo has uncommitted changes. - -# aider chat started at 2023-09-01 20:53:05 - -> Aider v0.11.0 -> Newer version v0.13.0 is available. To upgrade, run: -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/python3.10 -m pip install --upgrade aider-chat -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider -> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 -> API key does not support gpt-4, falling back to gpt-3.5-turbo-16k -> Model: gpt-3.5-turbo-16k - -# aider chat started at 2023-09-03 20:46:37 - -> Aider v0.11.0 -> Newer version v0.13.0 is available. To upgrade, run: -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/python3.10 -m pip install --upgrade aider-chat -> /Users/femtozheng/.pyenv/versions/3.10.5/bin/aider -> Setting openai.api_base=https://openai-forward-0caq.onrender.com/v1 diff --git a/.aider.input.history b/.aider.input.history deleted file mode 100644 index 25c615092..000000000 --- a/.aider.input.history +++ /dev/null @@ -1,112 +0,0 @@ - -# 2023-08-01 17:18:26.298246 -+/add pingpong_game/*.py - -# 2023-08-01 17:18:41.915030 -+Traceback (most recent call last): -+ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 36, in -+ main() -+ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 28, in main -+ game.update() -+ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/game.py", line 39, in update -+ self.player_paddle.update() -+AttributeError: 'Paddle' object has no attribute 'update' , could you help me with that? - -# 2023-08-01 17:19:16.371691 -+but update is empty, could you give a meaningful implementation? - -# 2023-08-01 17:20:08.970007 -+Traceback (most recent call last): -+ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 36, in -+ main() -+ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 29, in main -+ game.draw(screen) -+ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/game.py", line 58, in draw -+ self.menu.draw(screen, self.score, self.level) -+AttributeError: 'Menu' object has no attribute 'draw' - -# 2023-08-01 17:21:30.962710 -+AttributeError: 'Menu' object has no attribute 'draw' - -# 2023-08-01 17:22:22.357998 -+Traceback (most recent call last): -+ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 36, in -+ main() -+ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 29, in main -+ game.draw(screen) -+ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/game.py", line 58, in draw -+ self.menu.draw(screen, self.score, self.level) -+ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/menu.py", line 65, in draw -+ self.show_menu(screen) -+ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/menu.py", line 16, in show_menu -+ self.start_button.draw(screen) -+ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/menu.py", line 94, in draw -+ text_surface = self.font.render(self.text, True, (0, 0, 0)) -+AttributeError: 'Button' object has no attribute 'font' - -# 2023-08-01 17:24:35.183008 -+when I click the button on the menu, nothing happens, could you help me debug it? - -# 2023-08-01 17:30:20.191647 -+/tokens - -# 2023-08-01 17:30:29.855650 -+/add pingpong_game/*.py - -# 2023-08-01 17:30:47.462758 -+when I click on the menu, nothing happens, could you help me debug it? - -# 2023-08-01 17:31:38.019359 -+the Menu's handle_event seemed not be called, could you call that in the event loop? - -# 2023-08-01 17:34:33.325264 -+Traceback (most recent call last): -+ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 38, in -+ main() -+ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 28, in main -+ menu.handle_event(event) # Call handle_event method of the Menu class -+ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/menu.py", line 29, in handle_event -+ self.start_game() -+ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/menu.py", line 41, in start_game -+ game.start_game() -+NameError: name 'game' is not defined. Did you mean: 'pygame'? - -# 2023-08-01 17:35:33.573883 -+Traceback (most recent call last): -+ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 38, in -+ main() -+ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 12, in main -+ game = Game() -+ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/game.py", line 15, in __init__ -+ self.menu = Menu() -+TypeError: Menu.__init__() missing 1 required positional argument: 'game' - -# 2023-08-01 17:36:15.837811 -+Traceback (most recent call last): -+ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 38, in -+ main() -+ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/main.py", line 12, in main -+ game = Game() -+ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/game.py", line 15, in __init__ -+ self.menu = Menu(self) # Pass the game object to the Menu class -+ File "/Users/femtozheng/python-project/MetaGPT/workspace/pingpong_game/pingpong_game/menu.py", line 6, in __init__ -+ self.start_button = Button("Start Game", (400, 200)) -+NameError: name 'Button' is not defined - -# 2023-08-05 20:55:01.595618 -+/tokens - -# 2023-08-05 20:55:10.989284 -+/add start - -# 2023-08-05 20:55:15.912084 -+/add startup - -# 2023-08-05 20:55:23.039579 -+/add startup.py - -# 2023-08-05 20:55:38.816127 -+could you change line 53 to according to coding standard? - -# 2023-08-05 20:57:10.530926 -+could you change the code to standard? diff --git a/.python-version b/.python-version deleted file mode 100644 index c84ccce96..000000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.10.5 From 4f51a5986f24be70981d1d0ac06e4287326fe1bc Mon Sep 17 00:00:00 2001 From: femto Date: Thu, 7 Sep 2023 22:33:05 +0800 Subject: [PATCH 04/22] proceed to engineer --- metagpt/actions/write_prd_json.py | 30 +++++++++++++++--------------- metagpt/roles/engineer.py | 3 ++- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/metagpt/actions/write_prd_json.py b/metagpt/actions/write_prd_json.py index 650f8334f..adfac6f8a 100644 --- a/metagpt/actions/write_prd_json.py +++ b/metagpt/actions/write_prd_json.py @@ -61,7 +61,7 @@ Requirements: According to the context, fill in the following missing informatio ## UI Design draft: Provide as Plain text. Be simple. Describe the elements and functions, also provide a simple style description and layout description. ## Anything UNCLEAR: Provide as Plain text. Make clear here. -Your job is to create a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like format example +Your job is to create a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like format example,output CONTENT json directly """ FORMAT_EXAMPLE = """ [CONTENT] @@ -69,20 +69,20 @@ FORMAT_EXAMPLE = """ "Original Requirements": "", "Search Information": "", "mermaid quadrantChart code": ' - "title": "Reach and engagement of campaigns", - "x-axis": "Low Reach --> High Reach", - "y-axis": "Low Engagement --> High Engagement", - "quadrant-1": "We should expand", - "quadrant-2": "Need to promote", - "quadrant-3": "Re-evaluate", - "quadrant-4": "May be improved", - "Campaign: A": [0.3, 0.6], - "Campaign B": [0.45, 0.23], - "Campaign C": [0.57, 0.69], - "Campaign D": [0.78, 0.34], - "Campaign E": [0.40, 0.34], - "Campaign F": [0.35, 0.78], - "Our Target Product": [0.5, 0.6] + quadrantChart + title Reach and engagement of campaigns + x-axis Low Reach --> High Reach + y-axis Low Engagement --> High Engagement + quadrant-1 We should expand + quadrant-2 Need to promote + quadrant-3 Re-evaluate + quadrant-4 May be improved + Campaign A: [0.3, 0.6] + Campaign B: [0.45, 0.23] + Campaign C: [0.57, 0.69] + Campaign D: [0.78, 0.34] + Campaign E: [0.40, 0.34] + Campaign F: [0.35, 0.78] ' , diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index d6218d05b..c00b084ee 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -10,6 +10,7 @@ import shutil from collections import OrderedDict from pathlib import Path +from metagpt.actions.design_api_json import WriteDesignJson from metagpt.const import WORKSPACE_ROOT from metagpt.logs import logger from metagpt.roles import Role @@ -94,7 +95,7 @@ class Engineer(Role): return CodeParser.parse_str(block="Python package name", text=system_design_msg.content) def get_workspace(self) -> Path: - msg = self._rc.memory.get_by_action(WriteDesign)[-1] + msg = self._rc.memory.get_by_action(WriteDesignJson)[-1] if not msg: return WORKSPACE_ROOT / 'src' workspace = self.parse_workspace(msg) From e6e72c6e2336b9047ccc4f4f4ea15dac5e7ffbcb Mon Sep 17 00:00:00 2001 From: femto Date: Fri, 8 Sep 2023 11:51:46 +0800 Subject: [PATCH 05/22] test --- tests/metagpt/utils/test_custom_decoder.py | 40 ++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tests/metagpt/utils/test_custom_decoder.py diff --git a/tests/metagpt/utils/test_custom_decoder.py b/tests/metagpt/utils/test_custom_decoder.py new file mode 100644 index 000000000..754375549 --- /dev/null +++ b/tests/metagpt/utils/test_custom_decoder.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@Time : 2023/9/8 11:38 +@Author : femto Zheng +@File : test_custom_decoder.py +""" +import pytest + +import json + +from metagpt.utils.custom_decoder import CustomDecoder + +def test_parse_single_quote(): + # Create a custom JSON decoder + decoder = CustomDecoder(strict=False) + # Your provided input with single-quoted strings and line breaks + input_data = '''{'a" + b':'"title": "Reach and engagement of campaigns", + "x-axis": "Low Reach --> High Reach", + "y-axis": "Low Engagement --> High Engagement", + "quadrant-1": "We should expand", + "quadrant-2": "Need to promote", + "quadrant-3": "Re-evaluate", + "quadrant-4": "May be improved", + "Campaign: A": [0.3, 0.6], + "Campaign B": [0.45, 0.23], + "Campaign C": [0.57, 0.69], + "Campaign D": [0.78, 0.34], + "Campaign E": [0.40, 0.34], + "Campaign F": [0.35, 0.78], + "Our Target Product": [0.5, 0.6] + ' + } + ''' + # Parse the JSON using the custom decoder + + parsed_data = decoder.decode(input_data) + assert 'a"\n b' in parsed_data + From 6755de0ee83b9bbd0de37e2027042fd78f354507 Mon Sep 17 00:00:00 2001 From: femto Date: Fri, 8 Sep 2023 12:12:33 +0800 Subject: [PATCH 06/22] ruff fix --- metagpt/actions/action.py | 25 ++-- metagpt/actions/design_api.py | 112 ++++++++--------- metagpt/actions/design_api_json.py | 129 ------------------- metagpt/actions/project_management.py | 19 +-- metagpt/actions/write_prd.py | 100 +++++++-------- metagpt/actions/write_prd_json.py | 135 -------------------- metagpt/document_store/lancedb_store.py | 45 ++++--- metagpt/roles/architect.py | 32 ++--- metagpt/roles/engineer.py | 74 +++++------ metagpt/roles/product_manager.py | 24 ++-- metagpt/roles/project_manager.py | 24 ++-- metagpt/utils/custom_decoder.py | 136 ++++++++++++--------- tests/metagpt/utils/test_custom_decoder.py | 8 +- 13 files changed, 290 insertions(+), 573 deletions(-) delete mode 100644 metagpt/actions/design_api_json.py delete mode 100644 metagpt/actions/write_prd_json.py diff --git a/metagpt/actions/action.py b/metagpt/actions/action.py index f2dcd86d4..709e24bfc 100644 --- a/metagpt/actions/action.py +++ b/metagpt/actions/action.py @@ -5,25 +5,21 @@ @Author : alexanderwu @File : action.py """ -import ast -import json +import re from abc import ABC from typing import Optional -import re from tenacity import retry, stop_after_attempt, wait_fixed - from metagpt.actions.action_output import ActionOutput from metagpt.llm import LLM +from metagpt.logs import logger from metagpt.utils.common import OutputParser from metagpt.utils.custom_decoder import CustomDecoder -from metagpt.logs import logger - class Action(ABC): - def __init__(self, name: str = '', context=None, llm: LLM = None): + def __init__(self, name: str = "", context=None, llm: LLM = None): self.name: str = name if llm is None: llm = LLM() @@ -54,9 +50,9 @@ class Action(ABC): return await self.llm.aask(prompt, system_msgs) @retry(stop=stop_after_attempt(2), wait=wait_fixed(1)) - async def _aask_v1(self, prompt: str, output_class_name: str, - output_data_mapping: dict, - system_msgs: Optional[list[str]] = None) -> ActionOutput: + async def _aask_v1( + self, prompt: str, output_class_name: str, output_data_mapping: dict, system_msgs: Optional[list[str]] = None + ) -> ActionOutput: """Append default prefix""" if not system_msgs: system_msgs = [] @@ -70,9 +66,9 @@ class Action(ABC): return ActionOutput(content, instruct_content) @retry(stop=stop_after_attempt(2), wait=wait_fixed(1)) - async def _aask_json_v1(self, prompt: str, output_class_name: str, - output_data_mapping: dict, - system_msgs: Optional[list[str]] = None) -> ActionOutput: + async def _aask_json_v1( + self, prompt: str, output_class_name: str, output_data_mapping: dict, system_msgs: Optional[list[str]] = None + ) -> ActionOutput: """Append default prefix""" if not system_msgs: system_msgs = [] @@ -81,7 +77,7 @@ class Action(ABC): logger.debug(content) output_class = ActionOutput.create_model_class(output_class_name, output_data_mapping) - pattern = r'\[CONTENT\](.*?)\[/CONTENT\]' + pattern = r"\[CONTENT\](.*?)\[/CONTENT\]" # Use re.findall to extract content between the tags extracted_content = re.search(pattern, content, re.DOTALL).group(1) @@ -94,4 +90,3 @@ class Action(ABC): async def run(self, *args, **kwargs): """Run action""" raise NotImplementedError("The run method should be implemented in a subclass.") - \ No newline at end of file diff --git a/metagpt/actions/design_api.py b/metagpt/actions/design_api.py index abd1f9d4c..8e2ca3306 100644 --- a/metagpt/actions/design_api.py +++ b/metagpt/actions/design_api.py @@ -25,7 +25,6 @@ PROMPT_TEMPLATE = """ Role: You are an architect; the goal is to design a SOTA PEP8-compliant python system; make the best use of good open source tools Requirement: Fill in the following missing information based on the context, note that all sections are response with code form separately Max Output: 8192 chars or 2048 tokens. Try to use them up. -Attention: Use '##' to split sections, not '#', and '## ' SHOULD WRITE BEFORE the code and triple quote. ## Implementation approach: Provide as Plain text. Analyze the difficult points of the requirements, select the appropriate open-source framework. @@ -39,45 +38,31 @@ Attention: Use '##' to split sections, not '#', and '## ' SHOULD W ## Anything UNCLEAR: Provide as Plain text. Make clear here. +Your job is to create a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like format example """ FORMAT_EXAMPLE = """ ---- -## Implementation approach -We will ... - -## Python package name -```python -"snake_game" -``` - -## File list -```python -[ - "main.py", -] -``` - -## Data structures and interface definitions -```mermaid -classDiagram - class Game{ - +int score - } - ... - Game "1" -- "1" Food: has -``` - -## Program call flow -```mermaid -sequenceDiagram - participant M as Main - ... - G->>M: end game -``` - -## Anything UNCLEAR -The requirement is clear to me. ---- +[CONTENT] +{ + "Implementation approach": "We will ...", + "Python package name": "snake_game", + "File list": ["main.py"], + "Data structures and interface definitions": ' + classDiagram + class Game{ + +int score + } + ... + Game "1" -- "1" Food: has + ', + "Program call flow": ' + sequenceDiagram + participant M as Main + ... + G->>M: end game + ', + "Anything UNCLEAR": "The requirement is clear to me." +} +[/CONTENT] """ OUTPUT_MAPPING = { "Implementation approach": (str, ...), @@ -92,9 +77,11 @@ OUTPUT_MAPPING = { class WriteDesign(Action): def __init__(self, name, context=None, llm=None): super().__init__(name, context, llm) - self.desc = "Based on the PRD, think about the system design, and design the corresponding APIs, " \ - "data structures, library tables, processes, and paths. Please provide your design, feedback " \ - "clearly and in detail." + self.desc = ( + "Based on the PRD, think about the system design, and design the corresponding APIs, " + "data structures, library tables, processes, and paths. Please provide your design, feedback " + "clearly and in detail." + ) def recreate_workspace(self, workspace: Path): try: @@ -103,42 +90,43 @@ class WriteDesign(Action): pass # Folder does not exist, but we don't care workspace.mkdir(parents=True, exist_ok=True) - def _save_prd(self, docs_path, resources_path, prd): - prd_file = docs_path / 'prd.md' - quadrant_chart = CodeParser.parse_code(block="Competitive Quadrant Chart", text=prd) - mermaid_to_file(quadrant_chart, resources_path / 'competitive_analysis') + def _save_prd(self, docs_path, resources_path, context): + prd_file = docs_path / "prd.md" + quadrant_chart = context[-1].instruct_content.dict()["Competitive Quadrant Chart"] + mermaid_to_file(quadrant_chart, resources_path / "competitive_analysis") logger.info(f"Saving PRD to {prd_file}") - prd_file.write_text(prd) + prd_file.write_text(context[-1].content) - def _save_system_design(self, docs_path, resources_path, content): - data_api_design = CodeParser.parse_code(block="Data structures and interface definitions", text=content) - seq_flow = CodeParser.parse_code(block="Program call flow", text=content) - mermaid_to_file(data_api_design, resources_path / 'data_api_design') - mermaid_to_file(seq_flow, resources_path / 'seq_flow') - system_design_file = docs_path / 'system_design.md' + def _save_system_design(self, docs_path, resources_path, system_design): + data_api_design = system_design.instruct_content.dict()[ + "Data structures and interface definitions" + ] # CodeParser.parse_code(block="Data structures and interface definitions", text=content) + seq_flow = system_design.instruct_content.dict()[ + "Program call flow" + ] # CodeParser.parse_code(block="Program call flow", text=content) + mermaid_to_file(data_api_design, resources_path / "data_api_design") + mermaid_to_file(seq_flow, resources_path / "seq_flow") + system_design_file = docs_path / "system_design.md" logger.info(f"Saving System Designs to {system_design_file}") - system_design_file.write_text(content) + system_design_file.write_text(system_design.content) def _save(self, context, system_design): if isinstance(system_design, ActionOutput): - content = system_design.content - ws_name = CodeParser.parse_str(block="Python package name", text=content) + ws_name = system_design.instruct_content.dict()["Python package name"] else: - content = system_design ws_name = CodeParser.parse_str(block="Python package name", text=system_design) workspace = WORKSPACE_ROOT / ws_name self.recreate_workspace(workspace) - docs_path = workspace / 'docs' - resources_path = workspace / 'resources' + docs_path = workspace / "docs" + resources_path = workspace / "resources" docs_path.mkdir(parents=True, exist_ok=True) resources_path.mkdir(parents=True, exist_ok=True) - self._save_prd(docs_path, resources_path, context[-1].content) - self._save_system_design(docs_path, resources_path, content) + self._save_prd(docs_path, resources_path, context) + self._save_system_design(docs_path, resources_path, system_design) async def run(self, context): prompt = PROMPT_TEMPLATE.format(context=context, format_example=FORMAT_EXAMPLE) # system_design = await self._aask(prompt) - system_design = await self._aask_v1(prompt, "system_design", OUTPUT_MAPPING) + system_design = await self._aask_json_v1(prompt, "system_design", OUTPUT_MAPPING) self._save(context, system_design) return system_design - \ No newline at end of file diff --git a/metagpt/actions/design_api_json.py b/metagpt/actions/design_api_json.py deleted file mode 100644 index fc20a37e0..000000000 --- a/metagpt/actions/design_api_json.py +++ /dev/null @@ -1,129 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -@Time : 2023/5/11 19:26 -@Author : alexanderwu -@File : design_api.py -""" -import shutil -from pathlib import Path -from typing import List - -from metagpt.actions import Action, ActionOutput -from metagpt.const import WORKSPACE_ROOT -from metagpt.logs import logger -from metagpt.utils.common import CodeParser -from metagpt.utils.mermaid import mermaid_to_file - -PROMPT_TEMPLATE = """ -# Context -{context} - -## Format example -{format_example} ------ -Role: You are an architect; the goal is to design a SOTA PEP8-compliant python system; make the best use of good open source tools -Requirement: Fill in the following missing information based on the context, note that all sections are response with code form separately -Max Output: 8192 chars or 2048 tokens. Try to use them up. - -## Implementation approach: Provide as Plain text. Analyze the difficult points of the requirements, select the appropriate open-source framework. - -## Python package name: Provide as Python str with python triple quoto, concise and clear, characters only use a combination of all lowercase and underscores - -## File list: Provided as Python list[str], the list of ONLY REQUIRED files needed to write the program(LESS IS MORE!). Only need relative paths, comply with PEP8 standards. ALWAYS write a main.py or app.py here - -## Data structures and interface definitions: Use mermaid classDiagram code syntax, including classes (INCLUDING __init__ method) and functions (with type annotations), CLEARLY MARK the RELATIONSHIPS between classes, and comply with PEP8 standards. The data structures SHOULD BE VERY DETAILED and the API should be comprehensive with a complete design. - -## Program call flow: Use sequenceDiagram code syntax, COMPLETE and VERY DETAILED, using CLASSES AND API DEFINED ABOVE accurately, covering the CRUD AND INIT of each object, SYNTAX MUST BE CORRECT. - -## Anything UNCLEAR: Provide as Plain text. Make clear here. - -Your job is to create a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like format example -""" -FORMAT_EXAMPLE = """ -[CONTENT] -{ - "Implementation approach": "We will ...", - "Python package name": "snake_game", - "File list": ["main.py"], - "Data structures and interface definitions": ' - classDiagram - class Game{ - +int score - } - ... - Game "1" -- "1" Food: has - ', - "Program call flow": ' - sequenceDiagram - participant M as Main - ... - G->>M: end game - ', - "Anything UNCLEAR": "The requirement is clear to me." -} -[/CONTENT] -""" -OUTPUT_MAPPING = { - "Implementation approach": (str, ...), - "Python package name": (str, ...), - "File list": (List[str], ...), - "Data structures and interface definitions": (str, ...), - "Program call flow": (str, ...), - "Anything UNCLEAR": (str, ...), -} - - -class WriteDesignJson(Action): - def __init__(self, name, context=None, llm=None): - super().__init__(name, context, llm) - self.desc = "Based on the PRD, think about the system design, and design the corresponding APIs, " \ - "data structures, library tables, processes, and paths. Please provide your design, feedback " \ - "clearly and in detail." - - def recreate_workspace(self, workspace: Path): - try: - shutil.rmtree(workspace) - except FileNotFoundError: - pass # Folder does not exist, but we don't care - workspace.mkdir(parents=True, exist_ok=True) - - def _save_prd(self, docs_path, resources_path, context): - prd_file = docs_path / 'prd.md' - quadrant_chart = context[-1].instruct_content.dict()['Competitive Quadrant Chart'] - mermaid_to_file(quadrant_chart, resources_path / 'competitive_analysis') - logger.info(f"Saving PRD to {prd_file}") - prd_file.write_text(context[-1].content) - - def _save_system_design(self, docs_path, resources_path, system_design): - data_api_design = system_design.instruct_content.dict()['Data structures and interface definitions'] #CodeParser.parse_code(block="Data structures and interface definitions", text=content) - seq_flow = system_design.instruct_content.dict()['Program call flow'] #CodeParser.parse_code(block="Program call flow", text=content) - mermaid_to_file(data_api_design, resources_path / 'data_api_design') - mermaid_to_file(seq_flow, resources_path / 'seq_flow') - system_design_file = docs_path / 'system_design.md' - logger.info(f"Saving System Designs to {system_design_file}") - system_design_file.write_text(system_design.content) - - def _save(self, context, system_design): - if isinstance(system_design, ActionOutput): - content = system_design.content - ws_name = system_design.instruct_content.dict()['Python package name'] - else: - content = system_design - ws_name = CodeParser.parse_str(block="Python package name", text=system_design) - workspace = WORKSPACE_ROOT / ws_name - self.recreate_workspace(workspace) - docs_path = workspace / 'docs' - resources_path = workspace / 'resources' - docs_path.mkdir(parents=True, exist_ok=True) - resources_path.mkdir(parents=True, exist_ok=True) - self._save_prd(docs_path, resources_path, context) - self._save_system_design(docs_path, resources_path, system_design) - - async def run(self, context): - prompt = PROMPT_TEMPLATE.format(context=context, format_example=FORMAT_EXAMPLE) - # system_design = await self._aask(prompt) - system_design = await self._aask_json_v1(prompt, "system_design", OUTPUT_MAPPING) - self._save(context, system_design) - return system_design - \ No newline at end of file diff --git a/metagpt/actions/project_management.py b/metagpt/actions/project_management.py index b69009645..7b531b4d3 100644 --- a/metagpt/actions/project_management.py +++ b/metagpt/actions/project_management.py @@ -9,9 +9,8 @@ from typing import List, Tuple from metagpt.actions.action import Action from metagpt.const import WORKSPACE_ROOT -from metagpt.utils.common import CodeParser -PROMPT_TEMPLATE = ''' +PROMPT_TEMPLATE = """ # Context {context} @@ -36,7 +35,7 @@ Attention: Use '##' to split sections, not '#', and '## ' SHOULD W ## Anything UNCLEAR: Provide as Plain text. Make clear here. For example, don't forget a main entry. don't forget to init 3rd party libs. -''' +""" FORMAT_EXAMPLE = ''' --- @@ -102,18 +101,21 @@ OUTPUT_MAPPING = { class WriteTasks(Action): - def __init__(self, name="CreateTasks", context=None, llm=None): super().__init__(name, context, llm) def _save(self, context, rsp): - ws_name = context[-1].instruct_content.dict()["Python package name"]#CodeParser.parse_str(block="Python package name", text=context[-1].content) - file_path = WORKSPACE_ROOT / ws_name / 'docs/api_spec_and_tasks.md' + ws_name = context[-1].instruct_content.dict()[ + "Python package name" + ] # CodeParser.parse_str(block="Python package name", text=context[-1].content) + file_path = WORKSPACE_ROOT / ws_name / "docs/api_spec_and_tasks.md" file_path.write_text(rsp.content) # Write requirements.txt - requirements_path = WORKSPACE_ROOT / ws_name / 'requirements.txt' - requirements_path.write_text(rsp.instruct_content.dict().get("Required Python third-party packages").strip('"\n')) + requirements_path = WORKSPACE_ROOT / ws_name / "requirements.txt" + requirements_path.write_text( + rsp.instruct_content.dict().get("Required Python third-party packages").strip('"\n') + ) async def run(self, context): prompt = PROMPT_TEMPLATE.format(context=context, format_example=FORMAT_EXAMPLE) @@ -126,4 +128,3 @@ class AssignTasks(Action): async def run(self, *args, **kwargs): # Here you should implement the actual action pass - \ No newline at end of file diff --git a/metagpt/actions/write_prd.py b/metagpt/actions/write_prd.py index 2b96f867c..d2bb11eb8 100644 --- a/metagpt/actions/write_prd.py +++ b/metagpt/actions/write_prd.py @@ -5,7 +5,7 @@ @Author : alexanderwu @File : write_prd.py """ -from typing import List, Tuple +from typing import List from metagpt.actions import Action, ActionOutput from metagpt.actions.search_and_summarize import SearchAndSummarize @@ -43,7 +43,6 @@ quadrantChart ----- Role: You are a professional product manager; the goal is to design a concise, usable, efficient product Requirements: According to the context, fill in the following missing information, note that each sections are returned in Python code triple quote form seperatedly. If the requirements are unclear, ensure minimum viability and avoid excessive design -ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. AND '## ' SHOULD WRITE BEFORE the code and triple quote. Output carefully referenced "Format example" in format. ## Original Requirements: Provide as Plain text, place the polished complete original requirements here @@ -61,57 +60,45 @@ ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. AND '## ' SHOULD W ## UI Design draft: Provide as Plain text. Be simple. Describe the elements and functions, also provide a simple style description and layout description. ## Anything UNCLEAR: Provide as Plain text. Make clear here. + +Your job is to create a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like format example,output CONTENT json directly """ FORMAT_EXAMPLE = """ ---- -## Original Requirements -The boss ... +[CONTENT] +{ + "Original Requirements": "", + "Search Information": "", + "mermaid quadrantChart code": ' + quadrantChart + title Reach and engagement of campaigns + x-axis Low Reach --> High Reach + y-axis Low Engagement --> High Engagement + quadrant-1 We should expand + quadrant-2 Need to promote + quadrant-3 Re-evaluate + quadrant-4 May be improved + Campaign A: [0.3, 0.6] + Campaign B: [0.45, 0.23] + Campaign C: [0.57, 0.69] + Campaign D: [0.78, 0.34] + Campaign E: [0.40, 0.34] + Campaign F: [0.35, 0.78] + ' + , -## Product Goals -```python -[ - "Create a ...", -] -``` - -## User Stories -```python -[ - "As a user, ...", -] -``` - -## Competitive Analysis -```python -[ - "Python Snake Game: ...", -] -``` - -## Competitive Quadrant Chart -```mermaid -quadrantChart - title Reach and engagement of campaigns - ... - "Our Target Product": [0.6, 0.7] -``` - -## Requirement Analysis -The product should be a ... - -## Requirement Pool -```python -[ - ("End game ...", "P0") -] -``` - -## UI Design draft -Give a basic function description, and a draft - -## Anything UNCLEAR -There are no unclear points. ---- + }, + "Role": "You are a professional product manager; the goal is to design a concise, usable, efficient product", + "Requirements": "", + "Product Goals": [], + "User Stories": [], + "Competitive Analysis": [], + "Competitive Quadrant Chart": "", + "Requirement Analysis": "", + "Requirement Pool": [["P0","P0 requirement"],["P1","P1 requirement"]], + "UI Design draft": "", + "Anything UNCLEAR": "", +} +[/CONTENT] """ OUTPUT_MAPPING = { "Original Requirements": (str, ...), @@ -120,8 +107,8 @@ OUTPUT_MAPPING = { "Competitive Analysis": (List[str], ...), "Competitive Quadrant Chart": (str, ...), "Requirement Analysis": (str, ...), - "Requirement Pool": (List[Tuple[str, str]], ...), - "UI Design draft":(str, ...), + "Requirement Pool": (List[List[str]], ...), + "UI Design draft": (str, ...), "Anything UNCLEAR": (str, ...), } @@ -139,9 +126,10 @@ class WritePRD(Action): logger.info(sas.result) logger.info(rsp) - prompt = PROMPT_TEMPLATE.format(requirements=requirements, search_information=info, - format_example=FORMAT_EXAMPLE) + prompt = PROMPT_TEMPLATE.format( + requirements=requirements, search_information=info, format_example=FORMAT_EXAMPLE + ) logger.debug(prompt) - prd = await self._aask_v1(prompt, "prd", OUTPUT_MAPPING) + # prd = await self._aask_v1(prompt, "prd", OUTPUT_MAPPING) + prd = await self._aask_json_v1(prompt, "prd", OUTPUT_MAPPING) return prd - \ No newline at end of file diff --git a/metagpt/actions/write_prd_json.py b/metagpt/actions/write_prd_json.py deleted file mode 100644 index adfac6f8a..000000000 --- a/metagpt/actions/write_prd_json.py +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -@Time : 2023/5/11 17:45 -@Author : alexanderwu -@File : write_prd.py -""" -from typing import List, Tuple - -from metagpt.actions import Action, ActionOutput -from metagpt.actions.search_and_summarize import SearchAndSummarize -from metagpt.logs import logger - -PROMPT_TEMPLATE = """ -# Context -## Original Requirements -{requirements} - -## Search Information -{search_information} - -## mermaid quadrantChart code syntax example. DONT USE QUOTO IN CODE DUE TO INVALID SYNTAX. Replace the with REAL COMPETITOR NAME -```mermaid -quadrantChart - title Reach and engagement of campaigns - x-axis Low Reach --> High Reach - y-axis Low Engagement --> High Engagement - quadrant-1 We should expand - quadrant-2 Need to promote - quadrant-3 Re-evaluate - quadrant-4 May be improved - "Campaign: A": [0.3, 0.6] - "Campaign B": [0.45, 0.23] - "Campaign C": [0.57, 0.69] - "Campaign D": [0.78, 0.34] - "Campaign E": [0.40, 0.34] - "Campaign F": [0.35, 0.78] - "Our Target Product": [0.5, 0.6] -``` - -## Format example -{format_example} ------ -Role: You are a professional product manager; the goal is to design a concise, usable, efficient product -Requirements: According to the context, fill in the following missing information, note that each sections are returned in Python code triple quote form seperatedly. If the requirements are unclear, ensure minimum viability and avoid excessive design - -## Original Requirements: Provide as Plain text, place the polished complete original requirements here - -## Product Goals: Provided as Python list[str], up to 3 clear, orthogonal product goals. If the requirement itself is simple, the goal should also be simple - -## User Stories: Provided as Python list[str], up to 5 scenario-based user stories, If the requirement itself is simple, the user stories should also be less - -## Competitive Analysis: Provided as Python list[str], up to 7 competitive product analyses, consider as similar competitors as possible - -## Competitive Quadrant Chart: Use mermaid quadrantChart code syntax. up to 14 competitive products. Translation: Distribute these competitor scores evenly between 0 and 1, trying to conform to a normal distribution centered around 0.5 as much as possible. - -## Requirement Analysis: Provide as Plain text. Be simple. LESS IS MORE. Make your requirements less dumb. Delete the parts unnessasery. - -## Requirement Pool: Provided as Python list[str, str], the parameters are requirement description, priority(P0/P1/P2), respectively, comply with PEP standards; no more than 5 requirements and consider to make its difficulty lower - -## UI Design draft: Provide as Plain text. Be simple. Describe the elements and functions, also provide a simple style description and layout description. -## Anything UNCLEAR: Provide as Plain text. Make clear here. - -Your job is to create a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like format example,output CONTENT json directly -""" -FORMAT_EXAMPLE = """ -[CONTENT] -{ - "Original Requirements": "", - "Search Information": "", - "mermaid quadrantChart code": ' - quadrantChart - title Reach and engagement of campaigns - x-axis Low Reach --> High Reach - y-axis Low Engagement --> High Engagement - quadrant-1 We should expand - quadrant-2 Need to promote - quadrant-3 Re-evaluate - quadrant-4 May be improved - Campaign A: [0.3, 0.6] - Campaign B: [0.45, 0.23] - Campaign C: [0.57, 0.69] - Campaign D: [0.78, 0.34] - Campaign E: [0.40, 0.34] - Campaign F: [0.35, 0.78] - ' - , - - }, - "Role": "You are a professional product manager; the goal is to design a concise, usable, efficient product", - "Requirements": "", - "Product Goals": [], - "User Stories": [], - "Competitive Analysis": [], - "Competitive Quadrant Chart": "", - "Requirement Analysis": "", - "Requirement Pool": [["P0","P0 requirement"],["P1","P1 requirement"]], - "UI Design draft": "", - "Anything UNCLEAR": "", -} -[/CONTENT] -""" -OUTPUT_MAPPING = { - "Original Requirements": (str, ...), - "Product Goals": (List[str], ...), - "User Stories": (List[str], ...), - "Competitive Analysis": (List[str], ...), - "Competitive Quadrant Chart": (str, ...), - "Requirement Analysis": (str, ...), - "Requirement Pool": (List[List[str]], ...), - "UI Design draft":(str, ...), - "Anything UNCLEAR": (str, ...), -} - - -class WritePRDJson(Action): - def __init__(self, name="", context=None, llm=None): - super().__init__(name, context, llm) - - async def run(self, requirements, *args, **kwargs) -> ActionOutput: - sas = SearchAndSummarize() - # rsp = await sas.run(context=requirements, system_text=SEARCH_AND_SUMMARIZE_SYSTEM_EN_US) - rsp = "" - info = f"### Search Results\n{sas.result}\n\n### Search Summary\n{rsp}" - if sas.result: - logger.info(sas.result) - logger.info(rsp) - - prompt = PROMPT_TEMPLATE.format(requirements=requirements, search_information=info, - format_example=FORMAT_EXAMPLE) - logger.debug(prompt) - #prd = await self._aask_v1(prompt, "prd", OUTPUT_MAPPING) - prd = await self._aask_json_v1(prompt, "prd", OUTPUT_MAPPING) - return prd - \ No newline at end of file diff --git a/metagpt/document_store/lancedb_store.py b/metagpt/document_store/lancedb_store.py index b366fa650..99c4575a6 100644 --- a/metagpt/document_store/lancedb_store.py +++ b/metagpt/document_store/lancedb_store.py @@ -5,13 +5,15 @@ @Author : unkn-wn (Leon Yee) @File : lancedb_store.py """ +import os +import shutil + import lancedb -import shutil, os class LanceStore: def __init__(self, name): - db = lancedb.connect('./data/lancedb') + db = lancedb.connect("./data/lancedb") self.db = db self.name = name self.table = None @@ -23,16 +25,18 @@ class LanceStore: # .where - SQL syntax filtering for metadata (e.g. where("price > 100")) # .metric - specifies the distance metric to use # .nprobes - values will yield better recall (more likely to find vectors if they exist) at the expense of latency. - if self.table == None: raise Exception("Table not created yet, please add data first.") + if self.table is None: + raise Exception("Table not created yet, please add data first.") - results = self.table \ - .search(query) \ - .limit(n_results) \ - .select(kwargs.get('select')) \ - .where(kwargs.get('where')) \ - .metric(metric) \ - .nprobes(nprobes) \ + results = ( + self.table.search(query) + .limit(n_results) + .select(kwargs.get("select")) + .where(kwargs.get("where")) + .metric(metric) + .nprobes(nprobes) .to_df() + ) return results def persist(self): @@ -45,14 +49,11 @@ class LanceStore: documents = [] for i in range(len(data)): - row = { - 'vector': data[i], - 'id': ids[i] - } + row = {"vector": data[i], "id": ids[i]} row.update(metadatas[i]) documents.append(row) - if self.table != None: + if self.table is not None: self.table.add(documents) else: self.table = self.db.create_table(self.name, documents) @@ -61,13 +62,10 @@ class LanceStore: # This function is for adding individual documents # It assumes you're passing in a single vector embedding, metadata, and id - row = { - 'vector': data, - 'id': _id - } + row = {"vector": data, "id": _id} row.update(metadata) - if self.table != None: + if self.table is not None: self.table.add([row]) else: self.table = self.db.create_table(self.name, [row]) @@ -75,7 +73,8 @@ class LanceStore: def delete(self, _id): # This function deletes a row by id. # LanceDB delete syntax uses SQL syntax, so you can use "in" or "=" - if self.table == None: raise Exception("Table not created yet, please add data first") + if self.table is None: + raise Exception("Table not created yet, please add data first") if isinstance(_id, str): return self.table.delete(f"id = '{_id}'") @@ -85,6 +84,6 @@ class LanceStore: def drop(self, name): # This function drops a table, if it exists. - path = os.path.join(self.db.uri, name + '.lance') + path = os.path.join(self.db.uri, name + ".lance") if os.path.exists(path): - shutil.rmtree(path) \ No newline at end of file + shutil.rmtree(path) diff --git a/metagpt/roles/architect.py b/metagpt/roles/architect.py index bd52665dc..15d5fe5b1 100644 --- a/metagpt/roles/architect.py +++ b/metagpt/roles/architect.py @@ -6,34 +6,34 @@ @File : architect.py """ -from metagpt.actions import WriteDesign, WritePRD, WritePRDJson -from metagpt.actions.design_api_json import WriteDesignJson +from metagpt.actions import WritePRD +from metagpt.actions.design_api import WriteDesign from metagpt.roles import Role class Architect(Role): """ Represents an Architect role in a software development process. - + Attributes: name (str): Name of the architect. profile (str): Role profile, default is 'Architect'. goal (str): Primary goal or responsibility of the architect. constraints (str): Constraints or guidelines for the architect. """ - - def __init__(self, - name: str = "Bob", - profile: str = "Architect", - goal: str = "Design a concise, usable, complete python system", - constraints: str = "Try to specify good open source tools as much as possible") -> None: + + def __init__( + self, + name: str = "Bob", + profile: str = "Architect", + goal: str = "Design a concise, usable, complete python system", + constraints: str = "Try to specify good open source tools as much as possible", + ) -> None: """Initializes the Architect with given attributes.""" super().__init__(name, profile, goal, constraints) - - # Initialize actions specific to the Architect role - self._init_actions([WriteDesignJson]) - - # Set events or actions the Architect should watch or be aware of - self._watch({WritePRDJson}) - \ No newline at end of file + # Initialize actions specific to the Architect role + self._init_actions([WriteDesign]) + + # Set events or actions the Architect should watch or be aware of + self._watch({WritePRD}) diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index c00b084ee..6d65575a8 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -10,14 +10,13 @@ import shutil from collections import OrderedDict from pathlib import Path -from metagpt.actions.design_api_json import WriteDesignJson +from metagpt.actions import WriteCode, WriteCodeReview, WriteDesign, WriteTasks from metagpt.const import WORKSPACE_ROOT from metagpt.logs import logger from metagpt.roles import Role -from metagpt.actions import WriteCode, WriteCodeReview, WriteTasks, WriteDesign from metagpt.schema import Message from metagpt.utils.common import CodeParser -from metagpt.utils.special_tokens import MSG_SEP, FILENAME_CODE_SEP +from metagpt.utils.special_tokens import FILENAME_CODE_SEP, MSG_SEP async def gather_ordered_k(coros, k) -> list: @@ -50,7 +49,7 @@ async def gather_ordered_k(coros, k) -> list: class Engineer(Role): """ Represents an Engineer role responsible for writing and possibly reviewing code. - + Attributes: name (str): Name of the engineer. profile (str): Role profile, default is 'Engineer'. @@ -60,14 +59,16 @@ class Engineer(Role): use_code_review (bool): Whether to use code review. todos (list): List of tasks. """ - - def __init__(self, - name: str = "Alex", - profile: str = "Engineer", - goal: str = "Write elegant, readable, extensible, efficient code", - constraints: str = "The code should conform to standards like PEP8 and be modular and maintainable", - n_borg: int = 1, - use_code_review: bool = False) -> None: + + def __init__( + self, + name: str = "Alex", + profile: str = "Engineer", + goal: str = "Write elegant, readable, extensible, efficient code", + constraints: str = "The code should conform to standards like PEP8 and be modular and maintainable", + n_borg: int = 1, + use_code_review: bool = False, + ) -> None: """Initializes the Engineer role with given attributes.""" super().__init__(name, profile, goal, constraints) self._init_actions([WriteCode]) @@ -91,13 +92,13 @@ class Engineer(Role): @classmethod def parse_workspace(cls, system_design_msg: Message) -> str: if system_design_msg.instruct_content: - return system_design_msg.instruct_content.dict().get("Python package name").strip().strip("'").strip("\"") + return system_design_msg.instruct_content.dict().get("Python package name").strip().strip("'").strip('"') return CodeParser.parse_str(block="Python package name", text=system_design_msg.content) def get_workspace(self) -> Path: - msg = self._rc.memory.get_by_action(WriteDesignJson)[-1] + msg = self._rc.memory.get_by_action(WriteDesign)[-1] if not msg: - return WORKSPACE_ROOT / 'src' + return WORKSPACE_ROOT / "src" workspace = self.parse_workspace(msg) # Codes are written in workspace/{package_name}/{package_name} return WORKSPACE_ROOT / workspace / workspace @@ -112,7 +113,7 @@ class Engineer(Role): def write_file(self, filename: str, code: str): workspace = self.get_workspace() - filename = filename.replace('"', '').replace('\n', '') + filename = filename.replace('"', "").replace("\n", "") file = workspace / filename file.parent.mkdir(parents=True, exist_ok=True) file.write_text(code) @@ -128,8 +129,7 @@ class Engineer(Role): todo_coros = [] for todo in self.todos: todo_coro = WriteCode().run( - context=self._rc.memory.get_by_actions([WriteTasks, WriteDesign]), - filename=todo + context=self._rc.memory.get_by_actions([WriteTasks, WriteDesign]), filename=todo ) todo_coros.append(todo_coro) @@ -143,17 +143,14 @@ class Engineer(Role): self._rc.memory.add(msg) del self.todos[0] - logger.info(f'Done {self.get_workspace()} generating.') + logger.info(f"Done {self.get_workspace()} generating.") msg = Message(content="all done.", role=self.profile, cause_by=type(self._rc.todo)) return msg async def _act_sp(self) -> Message: - code_msg_all = [] # gather all code info, will pass to qa_engineer for tests later + code_msg_all = [] # gather all code info, will pass to qa_engineer for tests later for todo in self.todos: - code = await WriteCode().run( - context=self._rc.history, - filename=todo - ) + code = await WriteCode().run(context=self._rc.history, filename=todo) # logger.info(todo) # logger.info(code_rsp) # code = self.parse_code(code_rsp) @@ -164,17 +161,14 @@ class Engineer(Role): code_msg = todo + FILENAME_CODE_SEP + str(file_path) code_msg_all.append(code_msg) - logger.info(f'Done {self.get_workspace()} generating.') + logger.info(f"Done {self.get_workspace()} generating.") msg = Message( - content=MSG_SEP.join(code_msg_all), - role=self.profile, - cause_by=type(self._rc.todo), - send_to="QaEngineer" + content=MSG_SEP.join(code_msg_all), role=self.profile, cause_by=type(self._rc.todo), send_to="QaEngineer" ) return msg async def _act_sp_precision(self) -> Message: - code_msg_all = [] # gather all code info, will pass to qa_engineer for tests later + code_msg_all = [] # gather all code info, will pass to qa_engineer for tests later for todo in self.todos: """ # Select essential information from the historical data to reduce the length of the prompt (summarized from human experience): @@ -189,18 +183,11 @@ class Engineer(Role): context.append(m.content) context_str = "\n".join(context) # Write code - code = await WriteCode().run( - context=context_str, - filename=todo - ) + code = await WriteCode().run(context=context_str, filename=todo) # Code review if self.use_code_review: try: - rewrite_code = await WriteCodeReview().run( - context=context_str, - code=code, - filename=todo - ) + rewrite_code = await WriteCodeReview().run(context=context_str, code=code, filename=todo) code = rewrite_code except Exception as e: logger.error("code review failed!", e) @@ -212,12 +199,9 @@ class Engineer(Role): code_msg = todo + FILENAME_CODE_SEP + str(file_path) code_msg_all.append(code_msg) - logger.info(f'Done {self.get_workspace()} generating.') + logger.info(f"Done {self.get_workspace()} generating.") msg = Message( - content=MSG_SEP.join(code_msg_all), - role=self.profile, - cause_by=type(self._rc.todo), - send_to="QaEngineer" + content=MSG_SEP.join(code_msg_all), role=self.profile, cause_by=type(self._rc.todo), send_to="QaEngineer" ) return msg @@ -225,4 +209,4 @@ class Engineer(Role): """Determines the mode of action based on whether code review is used.""" if self.use_code_review: return await self._act_sp_precision() - return await self._act_sp() \ No newline at end of file + return await self._act_sp() diff --git a/metagpt/roles/product_manager.py b/metagpt/roles/product_manager.py index fefcbfe34..a58ea5385 100644 --- a/metagpt/roles/product_manager.py +++ b/metagpt/roles/product_manager.py @@ -5,29 +5,31 @@ @Author : alexanderwu @File : product_manager.py """ -from metagpt.actions import BossRequirement, WritePRD,WritePRDJson +from metagpt.actions import BossRequirement, WritePRD from metagpt.roles import Role class ProductManager(Role): """ Represents a Product Manager role responsible for product development and management. - + Attributes: name (str): Name of the product manager. profile (str): Role profile, default is 'Product Manager'. goal (str): Goal of the product manager. constraints (str): Constraints or limitations for the product manager. """ - - def __init__(self, - name: str = "Alice", - profile: str = "Product Manager", - goal: str = "Efficiently create a successful product", - constraints: str = "") -> None: + + def __init__( + self, + name: str = "Alice", + profile: str = "Product Manager", + goal: str = "Efficiently create a successful product", + constraints: str = "", + ) -> None: """ Initializes the ProductManager role with given attributes. - + Args: name (str): Name of the product manager. profile (str): Role profile. @@ -35,5 +37,5 @@ class ProductManager(Role): constraints (str): Constraints or limitations for the product manager. """ super().__init__(name, profile, goal, constraints) - self._init_actions([WritePRDJson]) - self._watch([BossRequirement]) \ No newline at end of file + self._init_actions([WritePRD]) + self._watch([BossRequirement]) diff --git a/metagpt/roles/project_manager.py b/metagpt/roles/project_manager.py index 78d2f8d52..7e7c5699d 100644 --- a/metagpt/roles/project_manager.py +++ b/metagpt/roles/project_manager.py @@ -5,30 +5,32 @@ @Author : alexanderwu @File : project_manager.py """ -from metagpt.actions import WriteDesign, WriteTasks -from metagpt.actions.design_api_json import WriteDesignJson +from metagpt.actions import WriteTasks +from metagpt.actions.design_api import WriteDesign from metagpt.roles import Role class ProjectManager(Role): """ Represents a Project Manager role responsible for overseeing project execution and team efficiency. - + Attributes: name (str): Name of the project manager. profile (str): Role profile, default is 'Project Manager'. goal (str): Goal of the project manager. constraints (str): Constraints or limitations for the project manager. """ - - def __init__(self, - name: str = "Eve", - profile: str = "Project Manager", - goal: str = "Improve team efficiency and deliver with quality and quantity", - constraints: str = "") -> None: + + def __init__( + self, + name: str = "Eve", + profile: str = "Project Manager", + goal: str = "Improve team efficiency and deliver with quality and quantity", + constraints: str = "", + ) -> None: """ Initializes the ProjectManager role with given attributes. - + Args: name (str): Name of the project manager. profile (str): Role profile. @@ -37,4 +39,4 @@ class ProjectManager(Role): """ super().__init__(name, profile, goal, constraints) self._init_actions([WriteTasks]) - self._watch([WriteDesignJson]) \ No newline at end of file + self._watch([WriteDesign]) diff --git a/metagpt/utils/custom_decoder.py b/metagpt/utils/custom_decoder.py index 4c4aaa587..02a8ab41c 100644 --- a/metagpt/utils/custom_decoder.py +++ b/metagpt/utils/custom_decoder.py @@ -1,10 +1,11 @@ import json import re -from json import scanner, JSONDecodeError +from json import JSONDecodeError from json.decoder import _decode_uXXXX -NUMBER_RE = re.compile( - r'(-?(?:0|[1-9]\d*))(\.\d+)?([eE][-+]?\d+)?', - (re.VERBOSE | re.MULTILINE | re.DOTALL)) + +NUMBER_RE = re.compile(r"(-?(?:0|[1-9]\d*))(\.\d+)?([eE][-+]?\d+)?", (re.VERBOSE | re.MULTILINE | re.DOTALL)) + + def py_make_scanner(context): parse_object = context.parse_object parse_array = context.parse_array @@ -26,32 +27,31 @@ def py_make_scanner(context): if nextchar == '"' or nextchar == "'": return parse_string(string, idx + 1, strict, delimiter=nextchar) - elif nextchar == '{': - return parse_object((string, idx + 1), strict, - _scan_once, object_hook, object_pairs_hook, memo) - elif nextchar == '[': + elif nextchar == "{": + return parse_object((string, idx + 1), strict, _scan_once, object_hook, object_pairs_hook, memo) + elif nextchar == "[": return parse_array((string, idx + 1), _scan_once) - elif nextchar == 'n' and string[idx:idx + 4] == 'null': + elif nextchar == "n" and string[idx : idx + 4] == "null": return None, idx + 4 - elif nextchar == 't' and string[idx:idx + 4] == 'true': + elif nextchar == "t" and string[idx : idx + 4] == "true": return True, idx + 4 - elif nextchar == 'f' and string[idx:idx + 5] == 'false': + elif nextchar == "f" and string[idx : idx + 5] == "false": return False, idx + 5 m = match_number(string, idx) if m is not None: integer, frac, exp = m.groups() if frac or exp: - res = parse_float(integer + (frac or '') + (exp or '')) + res = parse_float(integer + (frac or "") + (exp or "")) else: res = parse_int(integer) return res, m.end() - elif nextchar == 'N' and string[idx:idx + 3] == 'NaN': - return parse_constant('NaN'), idx + 3 - elif nextchar == 'I' and string[idx:idx + 8] == 'Infinity': - return parse_constant('Infinity'), idx + 8 - elif nextchar == '-' and string[idx:idx + 9] == '-Infinity': - return parse_constant('-Infinity'), idx + 9 + elif nextchar == "N" and string[idx : idx + 3] == "NaN": + return parse_constant("NaN"), idx + 3 + elif nextchar == "I" and string[idx : idx + 8] == "Infinity": + return parse_constant("Infinity"), idx + 8 + elif nextchar == "-" and string[idx : idx + 9] == "-Infinity": + return parse_constant("-Infinity"), idx + 9 else: raise StopIteration(idx) @@ -62,17 +62,28 @@ def py_make_scanner(context): memo.clear() return scan_once + + FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL STRINGCHUNK = re.compile(r'(.*?)(["\\\x00-\x1f])', FLAGS) -STRINGCHUNK_SINGLEQUOTE = re.compile(r'(.*?)([\'\\\x00-\x1f])', FLAGS) +STRINGCHUNK_SINGLEQUOTE = re.compile(r"(.*?)([\'\\\x00-\x1f])", FLAGS) BACKSLASH = { - '"': '"', '\\': '\\', '/': '/', - 'b': '\b', 'f': '\f', 'n': '\n', 'r': '\r', 't': '\t', + '"': '"', + "\\": "\\", + "/": "/", + "b": "\b", + "f": "\f", + "n": "\n", + "r": "\r", + "t": "\t", } -WHITESPACE = re.compile(r'[ \t\n\r]*', FLAGS) -WHITESPACE_STR = ' \t\n\r' -def JSONObject(s_and_end, strict, scan_once, object_hook, object_pairs_hook, - memo=None, _w=WHITESPACE.match, _ws=WHITESPACE_STR): +WHITESPACE = re.compile(r"[ \t\n\r]*", FLAGS) +WHITESPACE_STR = " \t\n\r" + + +def JSONObject( + s_and_end, strict, scan_once, object_hook, object_pairs_hook, memo=None, _w=WHITESPACE.match, _ws=WHITESPACE_STR +): s, end = s_and_end pairs = [] pairs_append = pairs.append @@ -82,14 +93,14 @@ def JSONObject(s_and_end, strict, scan_once, object_hook, object_pairs_hook, memo_get = memo.setdefault # Use a slice to prevent IndexError from being raised, the following # check will raise a more specific ValueError if the string is empty - nextchar = s[end:end + 1] + nextchar = s[end : end + 1] # Normally we expect nextchar == '"' if nextchar != '"' and nextchar != "'": if nextchar in _ws: end = _w(s, end).end() - nextchar = s[end:end + 1] + nextchar = s[end : end + 1] # Trivial empty object - if nextchar == '}': + if nextchar == "}": if object_pairs_hook is not None: result = object_pairs_hook(pairs) return result, end + 1 @@ -98,17 +109,16 @@ def JSONObject(s_and_end, strict, scan_once, object_hook, object_pairs_hook, pairs = object_hook(pairs) return pairs, end + 1 elif nextchar != '"': - raise JSONDecodeError( - "Expecting property name enclosed in double quotes", s, end) + raise JSONDecodeError("Expecting property name enclosed in double quotes", s, end) end += 1 while True: key, end = scanstring(s, end, strict, delimiter=nextchar) key = memo_get(key, key) # To skip some function call overhead we optimize the fast paths where # the JSON key separator is ": " or just ":". - if s[end:end + 1] != ':': + if s[end : end + 1] != ":": end = _w(s, end).end() - if s[end:end + 1] != ':': + if s[end : end + 1] != ":": raise JSONDecodeError("Expecting ':' delimiter", s, end) end += 1 @@ -131,19 +141,18 @@ def JSONObject(s_and_end, strict, scan_once, object_hook, object_pairs_hook, end = _w(s, end + 1).end() nextchar = s[end] except IndexError: - nextchar = '' + nextchar = "" end += 1 - if nextchar == '}': + if nextchar == "}": break - elif nextchar != ',': + elif nextchar != ",": raise JSONDecodeError("Expecting ',' delimiter", s, end - 1) end = _w(s, end).end() - nextchar = s[end:end + 1] + nextchar = s[end : end + 1] end += 1 if nextchar != '"': - raise JSONDecodeError( - "Expecting property name enclosed in double quotes", s, end - 1) + raise JSONDecodeError("Expecting property name enclosed in double quotes", s, end - 1) if object_pairs_hook is not None: result = object_pairs_hook(pairs) return result, end @@ -151,8 +160,9 @@ def JSONObject(s_and_end, strict, scan_once, object_hook, object_pairs_hook, if object_hook is not None: pairs = object_hook(pairs) return pairs, end -def py_scanstring(s, end, strict=True, - _b=BACKSLASH, _m=STRINGCHUNK.match,delimiter='"'): + + +def py_scanstring(s, end, strict=True, _b=BACKSLASH, _m=STRINGCHUNK.match, delimiter='"'): """Scan the string s for a JSON string. End is the index of the character in s after the quote that started the JSON string. Unescapes all valid JSON string escape sequences and raises ValueError @@ -181,9 +191,9 @@ def py_scanstring(s, end, strict=True, # or a backslash denoting that an escape sequence follows if terminator == delimiter: break - elif terminator != '\\': + elif terminator != "\\": if strict: - #msg = "Invalid control character %r at" % (terminator,) + # msg = "Invalid control character %r at" % (terminator,) msg = "Invalid control character {0!r} at".format(terminator) raise JSONDecodeError(msg, s, end) else: @@ -192,10 +202,9 @@ def py_scanstring(s, end, strict=True, try: esc = s[end] except IndexError: - raise JSONDecodeError("Unterminated string starting at", - s, begin) from None + raise JSONDecodeError("Unterminated string starting at", s, begin) from None # If not a unicode escape sequence, must be in the lookup table - if esc != 'u': + if esc != "u": try: char = _b[esc] except KeyError: @@ -205,26 +214,41 @@ def py_scanstring(s, end, strict=True, else: uni = _decode_uXXXX(s, end) end += 5 - if 0xd800 <= uni <= 0xdbff and s[end:end + 2] == '\\u': + if 0xD800 <= uni <= 0xDBFF and s[end : end + 2] == "\\u": uni2 = _decode_uXXXX(s, end + 1) - if 0xdc00 <= uni2 <= 0xdfff: - uni = 0x10000 + (((uni - 0xd800) << 10) | (uni2 - 0xdc00)) + if 0xDC00 <= uni2 <= 0xDFFF: + uni = 0x10000 + (((uni - 0xD800) << 10) | (uni2 - 0xDC00)) end += 6 char = chr(uni) _append(char) - return ''.join(chunks), end + return "".join(chunks), end + + scanstring = py_scanstring + + class CustomDecoder(json.JSONDecoder): - def __init__(self, *, object_hook=None, parse_float=None, - parse_int=None, parse_constant=None, strict=True, - object_pairs_hook=None): - super().__init__(object_hook=object_hook, parse_float=parse_float, parse_int=parse_int, - parse_constant=parse_constant, strict=strict,object_pairs_hook=object_pairs_hook) + def __init__( + self, + *, + object_hook=None, + parse_float=None, + parse_int=None, + parse_constant=None, + strict=True, + object_pairs_hook=None + ): + super().__init__( + object_hook=object_hook, + parse_float=parse_float, + parse_int=parse_int, + parse_constant=parse_constant, + strict=strict, + object_pairs_hook=object_pairs_hook, + ) self.parse_object = JSONObject self.parse_string = py_scanstring self.scan_once = py_make_scanner(self) def decode(self, s, _w=json.decoder.WHITESPACE.match): - return super().decode(s) - diff --git a/tests/metagpt/utils/test_custom_decoder.py b/tests/metagpt/utils/test_custom_decoder.py index 754375549..99ca3fc6a 100644 --- a/tests/metagpt/utils/test_custom_decoder.py +++ b/tests/metagpt/utils/test_custom_decoder.py @@ -5,17 +5,16 @@ @Author : femto Zheng @File : test_custom_decoder.py """ -import pytest -import json from metagpt.utils.custom_decoder import CustomDecoder + def test_parse_single_quote(): # Create a custom JSON decoder decoder = CustomDecoder(strict=False) # Your provided input with single-quoted strings and line breaks - input_data = '''{'a" + input_data = """{'a" b':'"title": "Reach and engagement of campaigns", "x-axis": "Low Reach --> High Reach", "y-axis": "Low Engagement --> High Engagement", @@ -32,9 +31,8 @@ def test_parse_single_quote(): "Our Target Product": [0.5, 0.6] ' } - ''' + """ # Parse the JSON using the custom decoder parsed_data = decoder.decode(input_data) assert 'a"\n b' in parsed_data - From d17b84797e45bce3b42d6c4f2ba525a7aa35a193 Mon Sep 17 00:00:00 2001 From: femto Date: Fri, 8 Sep 2023 12:13:47 +0800 Subject: [PATCH 07/22] fix --- metagpt/actions/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/metagpt/actions/__init__.py b/metagpt/actions/__init__.py index e6bd0616c..b004bd58e 100644 --- a/metagpt/actions/__init__.py +++ b/metagpt/actions/__init__.py @@ -21,7 +21,6 @@ from metagpt.actions.search_and_summarize import SearchAndSummarize from metagpt.actions.write_code import WriteCode from metagpt.actions.write_code_review import WriteCodeReview from metagpt.actions.write_prd import WritePRD -from metagpt.actions.write_prd_json import WritePRDJson from metagpt.actions.write_prd_review import WritePRDReview from metagpt.actions.write_test import WriteTest @@ -31,7 +30,6 @@ class ActionType(Enum): ADD_REQUIREMENT = BossRequirement WRITE_PRD = WritePRD - WRITE_PRD_JSON = WritePRDJson WRITE_PRD_REVIEW = WritePRDReview WRITE_DESIGN = WriteDesign DESIGN_REVIEW = DesignReview From 749c60e9eb1fb75842fd62e28a4de2c00c93441b Mon Sep 17 00:00:00 2001 From: femto Date: Fri, 8 Sep 2023 12:44:36 +0800 Subject: [PATCH 08/22] fix --- Message | 0 None | 0 int | 0 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 Message delete mode 100644 None delete mode 100644 int diff --git a/Message b/Message deleted file mode 100644 index e69de29bb..000000000 diff --git a/None b/None deleted file mode 100644 index e69de29bb..000000000 diff --git a/int b/int deleted file mode 100644 index e69de29bb..000000000 From dd8be8a382d753c7d5a9299169a3e39b0c1485ff Mon Sep 17 00:00:00 2001 From: femto Date: Fri, 8 Sep 2023 16:32:10 +0800 Subject: [PATCH 09/22] parse_triple_quote --- metagpt/actions/action.py | 2 +- metagpt/actions/write_prd.py | 21 +++++-------- metagpt/utils/custom_decoder.py | 22 ++++++++++++-- tests/metagpt/utils/test_custom_decoder.py | 34 ++++++++++++++++++++++ 4 files changed, 62 insertions(+), 17 deletions(-) diff --git a/metagpt/actions/action.py b/metagpt/actions/action.py index 709e24bfc..bf991233c 100644 --- a/metagpt/actions/action.py +++ b/metagpt/actions/action.py @@ -77,7 +77,7 @@ class Action(ABC): logger.debug(content) output_class = ActionOutput.create_model_class(output_class_name, output_data_mapping) - pattern = r"\[CONTENT\](.*?)\[/CONTENT\]" + pattern = r"\[CONTENT\](.+?)\[/CONTENT\]" # Use re.findall to extract content between the tags extracted_content = re.search(pattern, content, re.DOTALL).group(1) diff --git a/metagpt/actions/write_prd.py b/metagpt/actions/write_prd.py index d2bb11eb8..f259e786f 100644 --- a/metagpt/actions/write_prd.py +++ b/metagpt/actions/write_prd.py @@ -68,8 +68,13 @@ FORMAT_EXAMPLE = """ { "Original Requirements": "", "Search Information": "", - "mermaid quadrantChart code": ' - quadrantChart + + "Role": "You are a professional product manager; the goal is to design a concise, usable, efficient product", + "Requirements": "", + "Product Goals": [], + "User Stories": [], + "Competitive Analysis": [], + "Competitive Quadrant Chart": "quadrantChart title Reach and engagement of campaigns x-axis Low Reach --> High Reach y-axis Low Engagement --> High Engagement @@ -82,17 +87,7 @@ FORMAT_EXAMPLE = """ Campaign C: [0.57, 0.69] Campaign D: [0.78, 0.34] Campaign E: [0.40, 0.34] - Campaign F: [0.35, 0.78] - ' - , - - }, - "Role": "You are a professional product manager; the goal is to design a concise, usable, efficient product", - "Requirements": "", - "Product Goals": [], - "User Stories": [], - "Competitive Analysis": [], - "Competitive Quadrant Chart": "", + Campaign F: [0.35, 0.78]", "Requirement Analysis": "", "Requirement Pool": [["P0","P0 requirement"],["P1","P1 requirement"]], "UI Design draft": "", diff --git a/metagpt/utils/custom_decoder.py b/metagpt/utils/custom_decoder.py index 02a8ab41c..fcac85963 100644 --- a/metagpt/utils/custom_decoder.py +++ b/metagpt/utils/custom_decoder.py @@ -26,7 +26,12 @@ def py_make_scanner(context): raise StopIteration(idx) from None if nextchar == '"' or nextchar == "'": - return parse_string(string, idx + 1, strict, delimiter=nextchar) + if idx + 2 < len(string) and string[idx + 1] == nextchar and string[idx + 2] == nextchar: + # Handle the case where the next two characters are the same as nextchar + return parse_string(string, idx + 3, strict, delimiter=nextchar * 3) # triple quote + else: + # Handle the case where the next two characters are not the same as nextchar + return parse_string(string, idx + 1, strict, delimiter=nextchar) elif nextchar == "{": return parse_object((string, idx + 1), strict, _scan_once, object_hook, object_pairs_hook, memo) elif nextchar == "[": @@ -67,6 +72,8 @@ def py_make_scanner(context): FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL STRINGCHUNK = re.compile(r'(.*?)(["\\\x00-\x1f])', FLAGS) STRINGCHUNK_SINGLEQUOTE = re.compile(r"(.*?)([\'\\\x00-\x1f])", FLAGS) +STRINGCHUNK_TRIPLE_DOUBLE_QUOTE = re.compile(r"(.*?)(\"\"\"|[\\\x00-\x1f])", FLAGS) +STRINGCHUNK_TRIPLE_SINGLEQUOTE = re.compile(r"(.*?)('''|[\\\x00-\x1f])", FLAGS) BACKSLASH = { '"': '"', "\\": "\\", @@ -112,7 +119,12 @@ def JSONObject( raise JSONDecodeError("Expecting property name enclosed in double quotes", s, end) end += 1 while True: - key, end = scanstring(s, end, strict, delimiter=nextchar) + if end + 1 < len(s) and s[end] == nextchar and s[end + 1] == nextchar: + # Handle the case where the next two characters are the same as nextchar + key, end = scanstring(s, end + 2, strict, delimiter=nextchar * 3) + else: + # Handle the case where the next two characters are not the same as nextchar + key, end = scanstring(s, end, strict, delimiter=nextchar) key = memo_get(key, key) # To skip some function call overhead we optimize the fast paths where # the JSON key separator is ": " or just ":". @@ -176,8 +188,12 @@ def py_scanstring(s, end, strict=True, _b=BACKSLASH, _m=STRINGCHUNK.match, delim begin = end - 1 if delimiter == '"': _m = STRINGCHUNK.match - else: + elif delimiter == "'": _m = STRINGCHUNK_SINGLEQUOTE.match + elif delimiter == '"""': + _m = STRINGCHUNK_TRIPLE_DOUBLE_QUOTE.match + else: + _m = STRINGCHUNK_TRIPLE_SINGLEQUOTE.match while 1: chunk = _m(s, end) if chunk is None: diff --git a/tests/metagpt/utils/test_custom_decoder.py b/tests/metagpt/utils/test_custom_decoder.py index 99ca3fc6a..c7b14ad59 100644 --- a/tests/metagpt/utils/test_custom_decoder.py +++ b/tests/metagpt/utils/test_custom_decoder.py @@ -36,3 +36,37 @@ def test_parse_single_quote(): parsed_data = decoder.decode(input_data) assert 'a"\n b' in parsed_data + + +def test_parse_triple_double_quote(): + # Create a custom JSON decoder + decoder = CustomDecoder(strict=False) + # Your provided input with single-quoted strings and line breaks + input_data = '{"""a""":"b"}' + # Parse the JSON using the custom decoder + + parsed_data = decoder.decode(input_data) + assert "a" in parsed_data + + input_data = '{"""a""":"""b"""}' + # Parse the JSON using the custom decoder + + parsed_data = decoder.decode(input_data) + assert parsed_data["a"] == "b" + + +def test_parse_triple_single_quote(): + # Create a custom JSON decoder + decoder = CustomDecoder(strict=False) + # Your provided input with single-quoted strings and line breaks + input_data = "{'''a''':'b'}" + # Parse the JSON using the custom decoder + + parsed_data = decoder.decode(input_data) + assert "a" in parsed_data + + input_data = "{'''a''':'''b'''}" + # Parse the JSON using the custom decoder + + parsed_data = decoder.decode(input_data) + assert parsed_data["a"] == "b" From 2846a462a06e9f5536973c2c3e1151047c98d9ed Mon Sep 17 00:00:00 2001 From: femto Date: Fri, 8 Sep 2023 17:21:42 +0800 Subject: [PATCH 10/22] fix test --- metagpt/actions/action.py | 9 ++++++--- metagpt/actions/design_api.py | 14 ++++++++++---- metagpt/actions/write_prd.py | 3 ++- tests/metagpt/actions/test_design_api.py | 10 ++++++---- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/metagpt/actions/action.py b/metagpt/actions/action.py index bf991233c..f563c3804 100644 --- a/metagpt/actions/action.py +++ b/metagpt/actions/action.py @@ -77,10 +77,13 @@ class Action(ABC): logger.debug(content) output_class = ActionOutput.create_model_class(output_class_name, output_data_mapping) - pattern = r"\[CONTENT\](.+?)\[/CONTENT\]" + pattern = r"\[CONTENT\](\s*\{.*?\}\s*)\[/CONTENT\]" + matches = re.findall(pattern, content, re.DOTALL) - # Use re.findall to extract content between the tags - extracted_content = re.search(pattern, content, re.DOTALL).group(1) + for match in matches: + if match: + extracted_content = match + break parsed_data = CustomDecoder(strict=False).decode(extracted_content) logger.debug(parsed_data) diff --git a/metagpt/actions/design_api.py b/metagpt/actions/design_api.py index 8e2ca3306..cb94b0426 100644 --- a/metagpt/actions/design_api.py +++ b/metagpt/actions/design_api.py @@ -38,7 +38,8 @@ Max Output: 8192 chars or 2048 tokens. Try to use them up. ## Anything UNCLEAR: Provide as Plain text. Make clear here. -Your job is to create a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like format example +output a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like format example, +and only output the json inside this tag, nothing else """ FORMAT_EXAMPLE = """ [CONTENT] @@ -92,8 +93,10 @@ class WriteDesign(Action): def _save_prd(self, docs_path, resources_path, context): prd_file = docs_path / "prd.md" - quadrant_chart = context[-1].instruct_content.dict()["Competitive Quadrant Chart"] - mermaid_to_file(quadrant_chart, resources_path / "competitive_analysis") + if context[-1].instruct_content and context[-1].instruct_content.dict()["Competitive Quadrant Chart"]: + quadrant_chart = context[-1].instruct_content.dict()["Competitive Quadrant Chart"] + mermaid_to_file(quadrant_chart, resources_path / "competitive_analysis") + logger.info(f"Saving PRD to {prd_file}") prd_file.write_text(context[-1].content) @@ -125,7 +128,10 @@ class WriteDesign(Action): self._save_system_design(docs_path, resources_path, system_design) async def run(self, context): - prompt = PROMPT_TEMPLATE.format(context=context, format_example=FORMAT_EXAMPLE) + if isinstance(context, ActionOutput): + prompt = PROMPT_TEMPLATE.format(context=context.content, format_example=FORMAT_EXAMPLE) + else: # context is a string + prompt = PROMPT_TEMPLATE.format(context=context, format_example=FORMAT_EXAMPLE) # system_design = await self._aask(prompt) system_design = await self._aask_json_v1(prompt, "system_design", OUTPUT_MAPPING) self._save(context, system_design) diff --git a/metagpt/actions/write_prd.py b/metagpt/actions/write_prd.py index f259e786f..9a0df85d6 100644 --- a/metagpt/actions/write_prd.py +++ b/metagpt/actions/write_prd.py @@ -61,7 +61,8 @@ Requirements: According to the context, fill in the following missing informatio ## UI Design draft: Provide as Plain text. Be simple. Describe the elements and functions, also provide a simple style description and layout description. ## Anything UNCLEAR: Provide as Plain text. Make clear here. -Your job is to create a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like format example,output CONTENT json directly +output a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like format example, +and only output the json inside this tag, nothing else """ FORMAT_EXAMPLE = """ [CONTENT] diff --git a/tests/metagpt/actions/test_design_api.py b/tests/metagpt/actions/test_design_api.py index e6a396ad0..46ef59c29 100644 --- a/tests/metagpt/actions/test_design_api.py +++ b/tests/metagpt/actions/test_design_api.py @@ -7,6 +7,7 @@ """ import pytest +from metagpt.actions import ActionOutput from metagpt.actions.design_api import WriteDesign from metagpt.logs import logger from tests.metagpt.actions.mock import PRD_SAMPLE @@ -18,9 +19,10 @@ async def test_design_api(): design_api = WriteDesign("design_api") - result = await design_api.run(prd) + result = await design_api.run([ActionOutput(content=prd, instruct_content=None)]) logger.info(result) - assert len(result) > 0 + + assert result @pytest.mark.asyncio @@ -28,7 +30,7 @@ async def test_design_api_calculator(): prd = PRD_SAMPLE design_api = WriteDesign("design_api") - result = await design_api.run(prd) + result = await design_api.run([ActionOutput(content=prd, instruct_content=None)]) logger.info(result) - assert len(result) > 10 + assert result From 6df217e4d9c6a74e8eea38bebd5e62d0d8d24fdf Mon Sep 17 00:00:00 2001 From: femto Date: Fri, 8 Sep 2023 20:44:34 +0800 Subject: [PATCH 11/22] fix test --- metagpt/actions/action.py | 1 + metagpt/actions/project_management.py | 89 ++++++++------------ tests/metagpt/utils/test_custom_decoder.py | 96 ++++++++++++++++++++++ 3 files changed, 129 insertions(+), 57 deletions(-) diff --git a/metagpt/actions/action.py b/metagpt/actions/action.py index f563c3804..3f46b1e31 100644 --- a/metagpt/actions/action.py +++ b/metagpt/actions/action.py @@ -80,6 +80,7 @@ class Action(ABC): pattern = r"\[CONTENT\](\s*\{.*?\}\s*)\[/CONTENT\]" matches = re.findall(pattern, content, re.DOTALL) + extracted_content = None for match in matches: if match: extracted_content = match diff --git a/metagpt/actions/project_management.py b/metagpt/actions/project_management.py index 7b531b4d3..526915b30 100644 --- a/metagpt/actions/project_management.py +++ b/metagpt/actions/project_management.py @@ -5,7 +5,7 @@ @Author : alexanderwu @File : project_management.py """ -from typing import List, Tuple +from typing import List from metagpt.actions.action import Action from metagpt.const import WORKSPACE_ROOT @@ -35,65 +35,42 @@ Attention: Use '##' to split sections, not '#', and '## ' SHOULD W ## Anything UNCLEAR: Provide as Plain text. Make clear here. For example, don't forget a main entry. don't forget to init 3rd party libs. +output a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like format example, +and only output the json inside this tag, nothing else """ FORMAT_EXAMPLE = ''' ---- -## Required Python third-party packages -```python -""" -flask==1.1.2 -bcrypt==3.2.0 -""" -``` - -## Required Other language third-party packages -```python -""" -No third-party ... -""" -``` - -## Full API spec -```python -""" -openapi: 3.0.0 -... -description: A JSON object ... -""" -``` - -## Logic Analysis -```python -[ - ("game.py", "Contains ..."), -] -``` - -## Task list -```python -[ - "game.py", -] -``` - -## Shared Knowledge -```python -""" -'game.py' contains ... -""" -``` - -## Anything UNCLEAR -We need ... how to start. ---- +{ + "Required Python third-party packages": [ + "flask==1.1.2", + "bcrypt==3.2.0" + ], + "Required Other language third-party packages": [ + "No third-party ..." + ], + "Full API spec": """ + openapi: 3.0.0 + ... + description: A JSON object ... + """, + "Logic Analysis": [ + ["game.py","Contains..."] + ], + "Task list": [ + "game.py" + ], + "Shared Knowledge": """ + 'game.py' contains ... + """, + "Anything UNCLEAR": "We need ... how to start." +} ''' OUTPUT_MAPPING = { - "Required Python third-party packages": (str, ...), - "Required Other language third-party packages": (str, ...), + "Required Python third-party packages": (List[str], ...), + "Required Other language third-party packages": (List[str], ...), "Full API spec": (str, ...), - "Logic Analysis": (List[Tuple[str, str]], ...), + "Logic Analysis": (List[List[str]], ...), "Task list": (List[str], ...), "Shared Knowledge": (str, ...), "Anything UNCLEAR": (str, ...), @@ -113,13 +90,11 @@ class WriteTasks(Action): # Write requirements.txt requirements_path = WORKSPACE_ROOT / ws_name / "requirements.txt" - requirements_path.write_text( - rsp.instruct_content.dict().get("Required Python third-party packages").strip('"\n') - ) + requirements_path.write_text("\n".join(rsp.instruct_content.dict().get("Required Python third-party packages"))) async def run(self, context): prompt = PROMPT_TEMPLATE.format(context=context, format_example=FORMAT_EXAMPLE) - rsp = await self._aask_v1(prompt, "task", OUTPUT_MAPPING) + rsp = await self._aask_json_v1(prompt, "task", OUTPUT_MAPPING) self._save(context, rsp) return rsp diff --git a/tests/metagpt/utils/test_custom_decoder.py b/tests/metagpt/utils/test_custom_decoder.py index c7b14ad59..072d96152 100644 --- a/tests/metagpt/utils/test_custom_decoder.py +++ b/tests/metagpt/utils/test_custom_decoder.py @@ -70,3 +70,99 @@ def test_parse_triple_single_quote(): parsed_data = decoder.decode(input_data) assert parsed_data["a"] == "b" + + +# def test_parse_complex(): +# # Create a custom JSON decoder +# decoder = CustomDecoder(strict=False) +# # Your provided input with single-quoted strings and line breaks +# input_data = '''{ +# "Required Python third-party packages": [ +# "flask==1.1.2", +# "bcrypt==3.2.0" +# ], +# "Required Other language third-party packages": [ +# "No third-party packages are required." +# ], +# "Full API spec": """ +# openapi: 3.0.0 +# +# description: A JSON object representing the game state. +# +# paths: +# /game: +# get: +# summary: Get the current game state. +# responses: +# '200': +# description: The current game state. +# +# /game/{direction}: +# post: +# summary: Move the snake in the specified direction. +# parameters: +# - name: direction +# in: path +# description: The direction to move the snake (one of UP, DOWN, LEFT, or RIGHT). +# responses: +# '200': +# description: The updated game state. +# +# /game/{food}: +# get: +# summary: Get the current food position. +# responses: +# '200': +# description: The current food position. +# +# /game/{snake}: +# get: +# summary: Get the current snake position. +# responses: +# '200': +# description: The current snake position. +# +# /game/{snake}/{direction}: +# post: +# summary: Set the snake direction. +# parameters: +# - name: direction +# in: path +# description: The direction to move the snake (one of UP, DOWN, LEFT, or RIGHT). +# responses: +# '200': +# description: The updated game state. +# +# /game/end: +# post: +# summary: End the game. +# responses: +# '200': +# description: The final game state. +# " +# ], +# "Logic Analysis": [ +# ["game.py","Contains the game logic and snake movement."], +# ["snake.py","Contains the snake class and methods."], +# ["game_objects.py","Contains the game objects and their properties."], +# ["game_logic.py","Contains the game logic and rules."], +# ["ui.py","Contains the user interface and display logic."] +# ], +# "Task list": [ +# "game.py", +# "snake.py", +# "game_objects.py", +# "game_logic.py", +# "ui.py" +# ], +# "Shared Knowledge": """ +# The game state is represented by a JSON object. +# The snake movement is based on the direction parameter. +# The game ends when the snake collides with the wall or eats the food. +# """, +# "Anything UNCLEAR": "The requirement is clear to me." +# } +# ''' +# # Parse the JSON using the custom decoder +# +# parsed_data = decoder.decode(input_data) From 3be5980214b783a8e2df6bd9862517bfd8d2d22e Mon Sep 17 00:00:00 2001 From: femto Date: Sat, 9 Sep 2023 20:25:24 +0800 Subject: [PATCH 12/22] ActionOutput use extracted_content --- metagpt/actions/action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/actions/action.py b/metagpt/actions/action.py index 3f46b1e31..90c7d59ff 100644 --- a/metagpt/actions/action.py +++ b/metagpt/actions/action.py @@ -89,7 +89,7 @@ class Action(ABC): parsed_data = CustomDecoder(strict=False).decode(extracted_content) logger.debug(parsed_data) instruct_content = output_class(**parsed_data) - return ActionOutput(content, instruct_content) + return ActionOutput(extracted_content, instruct_content) async def run(self, *args, **kwargs): """Run action""" From 0086e0c25c77fb95ecd3573725dd237b28f78ca4 Mon Sep 17 00:00:00 2001 From: femto Date: Sun, 10 Sep 2023 12:58:50 +0800 Subject: [PATCH 13/22] rm incorrect comment --- metagpt/actions/design_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/actions/design_api.py b/metagpt/actions/design_api.py index cb94b0426..4692abfea 100644 --- a/metagpt/actions/design_api.py +++ b/metagpt/actions/design_api.py @@ -130,7 +130,7 @@ class WriteDesign(Action): async def run(self, context): if isinstance(context, ActionOutput): prompt = PROMPT_TEMPLATE.format(context=context.content, format_example=FORMAT_EXAMPLE) - else: # context is a string + else: prompt = PROMPT_TEMPLATE.format(context=context, format_example=FORMAT_EXAMPLE) # system_design = await self._aask(prompt) system_design = await self._aask_json_v1(prompt, "system_design", OUTPUT_MAPPING) From 6b0daa03126c294bfffe467d0e233ea38fae6deb Mon Sep 17 00:00:00 2001 From: femto Date: Mon, 11 Sep 2023 11:31:43 +0800 Subject: [PATCH 14/22] write prd --- metagpt/actions/write_prd.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/metagpt/actions/write_prd.py b/metagpt/actions/write_prd.py index 9a0df85d6..99f057da4 100644 --- a/metagpt/actions/write_prd.py +++ b/metagpt/actions/write_prd.py @@ -67,10 +67,8 @@ and only output the json inside this tag, nothing else FORMAT_EXAMPLE = """ [CONTENT] { - "Original Requirements": "", - "Search Information": "", - - "Role": "You are a professional product manager; the goal is to design a concise, usable, efficient product", + "Original Requirements": "", + "Search Information": "", "Requirements": "", "Product Goals": [], "User Stories": [], From d18a419d4b3f1c04872e9bc7905d6c3da4950498 Mon Sep 17 00:00:00 2001 From: femto Date: Mon, 11 Sep 2023 12:48:44 +0800 Subject: [PATCH 15/22] json to markdown --- metagpt/actions/design_api.py | 13 ++--- metagpt/actions/project_management.py | 3 +- metagpt/utils/json_to_markdown.py | 42 ++++++++++++++ tests/metagpt/actions/test_design_api.py | 6 +- tests/metagpt/utils/test_json_to_markdown.py | 60 ++++++++++++++++++++ 5 files changed, 113 insertions(+), 11 deletions(-) create mode 100644 metagpt/utils/json_to_markdown.py create mode 100644 tests/metagpt/utils/test_json_to_markdown.py diff --git a/metagpt/actions/design_api.py b/metagpt/actions/design_api.py index 4692abfea..a2192c4dc 100644 --- a/metagpt/actions/design_api.py +++ b/metagpt/actions/design_api.py @@ -13,6 +13,7 @@ from metagpt.actions import Action, ActionOutput from metagpt.const import WORKSPACE_ROOT from metagpt.logs import logger from metagpt.utils.common import CodeParser +from metagpt.utils.json_to_markdown import json_to_markdown from metagpt.utils.mermaid import mermaid_to_file PROMPT_TEMPLATE = """ @@ -97,8 +98,9 @@ class WriteDesign(Action): quadrant_chart = context[-1].instruct_content.dict()["Competitive Quadrant Chart"] mermaid_to_file(quadrant_chart, resources_path / "competitive_analysis") - logger.info(f"Saving PRD to {prd_file}") - prd_file.write_text(context[-1].content) + if context[-1].instruct_content: + logger.info(f"Saving PRD to {prd_file}") + prd_file.write_text(json_to_markdown(context[-1].instruct_content.dict())) def _save_system_design(self, docs_path, resources_path, system_design): data_api_design = system_design.instruct_content.dict()[ @@ -111,7 +113,7 @@ class WriteDesign(Action): mermaid_to_file(seq_flow, resources_path / "seq_flow") system_design_file = docs_path / "system_design.md" logger.info(f"Saving System Designs to {system_design_file}") - system_design_file.write_text(system_design.content) + system_design_file.write_text((json_to_markdown(system_design.instruct_content.dict()))) def _save(self, context, system_design): if isinstance(system_design, ActionOutput): @@ -128,10 +130,7 @@ class WriteDesign(Action): self._save_system_design(docs_path, resources_path, system_design) async def run(self, context): - if isinstance(context, ActionOutput): - prompt = PROMPT_TEMPLATE.format(context=context.content, format_example=FORMAT_EXAMPLE) - else: - prompt = PROMPT_TEMPLATE.format(context=context, format_example=FORMAT_EXAMPLE) + prompt = PROMPT_TEMPLATE.format(context=context, format_example=FORMAT_EXAMPLE) # system_design = await self._aask(prompt) system_design = await self._aask_json_v1(prompt, "system_design", OUTPUT_MAPPING) self._save(context, system_design) diff --git a/metagpt/actions/project_management.py b/metagpt/actions/project_management.py index 526915b30..cc62425c0 100644 --- a/metagpt/actions/project_management.py +++ b/metagpt/actions/project_management.py @@ -9,6 +9,7 @@ from typing import List from metagpt.actions.action import Action from metagpt.const import WORKSPACE_ROOT +from metagpt.utils.json_to_markdown import json_to_markdown PROMPT_TEMPLATE = """ # Context @@ -86,7 +87,7 @@ class WriteTasks(Action): "Python package name" ] # CodeParser.parse_str(block="Python package name", text=context[-1].content) file_path = WORKSPACE_ROOT / ws_name / "docs/api_spec_and_tasks.md" - file_path.write_text(rsp.content) + file_path.write_text(json_to_markdown(rsp.instruct_content.dict())) # Write requirements.txt requirements_path = WORKSPACE_ROOT / ws_name / "requirements.txt" diff --git a/metagpt/utils/json_to_markdown.py b/metagpt/utils/json_to_markdown.py new file mode 100644 index 000000000..d9b40c6f6 --- /dev/null +++ b/metagpt/utils/json_to_markdown.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@Time : 2023/9/11 11:50 +@Author : femto Zheng +@File : json_to_markdown.py +""" + + +# since we original write docs/*.md in markdown format, so I convert json back to markdown +def json_to_markdown(data, depth=2): + """ + Convert a JSON object to Markdown with headings for keys and lists for arrays, supporting nested objects. + + Args: + data: JSON object (dictionary) or value. + depth (int): Current depth level for Markdown headings. + + Returns: + str: Markdown representation of the JSON data. + """ + markdown = "" + + if isinstance(data, dict): + for key, value in data.items(): + if isinstance(value, list): + # Handle JSON arrays + markdown += "#" * depth + f" {key}\n\n" + items = [str(item) for item in value] + markdown += "- " + "\n- ".join(items) + "\n\n" + elif isinstance(value, dict): + # Handle nested JSON objects + markdown += "#" * depth + f" {key}\n\n" + markdown += json_to_markdown(value, depth + 1) + else: + # Handle other values + markdown += "#" * depth + f" {key}\n\n{value}\n\n" + else: + # Handle non-dictionary JSON data + markdown = str(data) + + return markdown diff --git a/tests/metagpt/actions/test_design_api.py b/tests/metagpt/actions/test_design_api.py index 46ef59c29..0add8fb74 100644 --- a/tests/metagpt/actions/test_design_api.py +++ b/tests/metagpt/actions/test_design_api.py @@ -7,9 +7,9 @@ """ import pytest -from metagpt.actions import ActionOutput from metagpt.actions.design_api import WriteDesign from metagpt.logs import logger +from metagpt.schema import Message from tests.metagpt.actions.mock import PRD_SAMPLE @@ -19,7 +19,7 @@ async def test_design_api(): design_api = WriteDesign("design_api") - result = await design_api.run([ActionOutput(content=prd, instruct_content=None)]) + result = await design_api.run([Message(content=prd, instruct_content=None)]) logger.info(result) assert result @@ -30,7 +30,7 @@ async def test_design_api_calculator(): prd = PRD_SAMPLE design_api = WriteDesign("design_api") - result = await design_api.run([ActionOutput(content=prd, instruct_content=None)]) + result = await design_api.run([Message(content=prd, instruct_content=None)]) logger.info(result) assert result diff --git a/tests/metagpt/utils/test_json_to_markdown.py b/tests/metagpt/utils/test_json_to_markdown.py new file mode 100644 index 000000000..53e410398 --- /dev/null +++ b/tests/metagpt/utils/test_json_to_markdown.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@Time : 2023/9/11 11:53 +@Author : femto Zheng +@File : test_json_to_markdown.py +""" + +from metagpt.utils.json_to_markdown import json_to_markdown + + +def test_json_to_markdown(): + # Example nested JSON data + json_data = { + "title": "Sample JSON to Markdown Conversion", + "description": "Convert JSON to Markdown with headings and lists.", + "tags": ["json", "markdown", "conversion"], + "content": { + "section1": {"subsection1": "This is a subsection.", "subsection2": "Another subsection."}, + "section2": "This is the second section content.", + }, + } + + # Convert JSON to Markdown with nested sections + markdown_output = json_to_markdown(json_data) + + expected = """## title + +Sample JSON to Markdown Conversion + +## description + +Convert JSON to Markdown with headings and lists. + +## tags + +- json +- markdown +- conversion + +## content + +### section1 + +#### subsection1 + +This is a subsection. + +#### subsection2 + +Another subsection. + +### section2 + +This is the second section content. + +""" + # Print or use the generated Markdown + # print(markdown_output) + assert expected == markdown_output From f17b00ed1c16efc81e9e560b1a8a1049a2f45047 Mon Sep 17 00:00:00 2001 From: femto Date: Mon, 11 Sep 2023 13:19:05 +0800 Subject: [PATCH 16/22] project management --- metagpt/actions/project_management.py | 8 +++++--- metagpt/roles/qa_engineer.py | 2 +- tests/metagpt/roles/test_product_manager.py | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/metagpt/actions/project_management.py b/metagpt/actions/project_management.py index cc62425c0..40b3204ab 100644 --- a/metagpt/actions/project_management.py +++ b/metagpt/actions/project_management.py @@ -9,6 +9,7 @@ from typing import List from metagpt.actions.action import Action from metagpt.const import WORKSPACE_ROOT +from metagpt.utils.common import CodeParser from metagpt.utils.json_to_markdown import json_to_markdown PROMPT_TEMPLATE = """ @@ -83,9 +84,10 @@ class WriteTasks(Action): super().__init__(name, context, llm) def _save(self, context, rsp): - ws_name = context[-1].instruct_content.dict()[ - "Python package name" - ] # CodeParser.parse_str(block="Python package name", text=context[-1].content) + if context[-1].instruct_content: + ws_name = context[-1].instruct_content.dict()["Python package name"] + else: + ws_name = CodeParser.parse_str(block="Python package name", text=context[-1].content) file_path = WORKSPACE_ROOT / ws_name / "docs/api_spec_and_tasks.md" file_path.write_text(json_to_markdown(rsp.instruct_content.dict())) diff --git a/metagpt/roles/qa_engineer.py b/metagpt/roles/qa_engineer.py index 65bf2cc5b..543912f3c 100644 --- a/metagpt/roles/qa_engineer.py +++ b/metagpt/roles/qa_engineer.py @@ -36,7 +36,7 @@ class QaEngineer(Role): @classmethod def parse_workspace(cls, system_design_msg: Message) -> str: - if not system_design_msg.instruct_content: + if system_design_msg.instruct_content: return system_design_msg.instruct_content.dict().get("Python package name") return CodeParser.parse_str(block="Python package name", text=system_design_msg.content) diff --git a/tests/metagpt/roles/test_product_manager.py b/tests/metagpt/roles/test_product_manager.py index 34c70efbc..21def787f 100644 --- a/tests/metagpt/roles/test_product_manager.py +++ b/tests/metagpt/roles/test_product_manager.py @@ -18,4 +18,4 @@ async def test_product_manager(): rsp = await product_manager.handle(MockMessages.req) logger.info(rsp) assert len(rsp.content) > 0 - assert "产品目标" in rsp.content + assert "Product Goals" in rsp.content From fce3d55f7fa34f9a3a0da23ffa414dc1bcc7adda Mon Sep 17 00:00:00 2001 From: femto Date: Mon, 11 Sep 2023 16:59:23 +0800 Subject: [PATCH 17/22] change prompt a bit --- metagpt/actions/design_api.py | 2 +- metagpt/actions/project_management.py | 2 +- metagpt/actions/write_prd.py | 2 +- metagpt/actions/write_test.py | 10 +++++++++- metagpt/roles/qa_engineer.py | 13 ++++++++++--- 5 files changed, 22 insertions(+), 7 deletions(-) diff --git a/metagpt/actions/design_api.py b/metagpt/actions/design_api.py index a2192c4dc..cde366533 100644 --- a/metagpt/actions/design_api.py +++ b/metagpt/actions/design_api.py @@ -24,7 +24,7 @@ PROMPT_TEMPLATE = """ {format_example} ----- Role: You are an architect; the goal is to design a SOTA PEP8-compliant python system; make the best use of good open source tools -Requirement: Fill in the following missing information based on the context, note that all sections are response with code form separately +Requirement: Fill in the following missing information based on the context, each section name is a key in json Max Output: 8192 chars or 2048 tokens. Try to use them up. ## Implementation approach: Provide as Plain text. Analyze the difficult points of the requirements, select the appropriate open-source framework. diff --git a/metagpt/actions/project_management.py b/metagpt/actions/project_management.py index 40b3204ab..5ae3a728c 100644 --- a/metagpt/actions/project_management.py +++ b/metagpt/actions/project_management.py @@ -20,7 +20,7 @@ PROMPT_TEMPLATE = """ {format_example} ----- Role: You are a project manager; the goal is to break down tasks according to PRD/technical design, give a task list, and analyze task dependencies to start with the prerequisite modules -Requirements: Based on the context, fill in the following missing information, note that all sections are returned in Python code triple quote form seperatedly. Here the granularity of the task is a file, if there are any missing files, you can supplement them +Requirements: Based on the context, fill in the following missing information, each section name is a key in json. Here the granularity of the task is a file, if there are any missing files, you can supplement them Attention: Use '##' to split sections, not '#', and '## ' SHOULD WRITE BEFORE the code and triple quote. ## Required Python third-party packages: Provided in requirements.txt format diff --git a/metagpt/actions/write_prd.py b/metagpt/actions/write_prd.py index 99f057da4..99032c2de 100644 --- a/metagpt/actions/write_prd.py +++ b/metagpt/actions/write_prd.py @@ -42,7 +42,7 @@ quadrantChart {format_example} ----- Role: You are a professional product manager; the goal is to design a concise, usable, efficient product -Requirements: According to the context, fill in the following missing information, note that each sections are returned in Python code triple quote form seperatedly. If the requirements are unclear, ensure minimum viability and avoid excessive design +Requirements: According to the context, fill in the following missing information, each section name is a key in json ,If the requirements are unclear, ensure minimum viability and avoid excessive design ## Original Requirements: Provide as Plain text, place the polished complete original requirements here diff --git a/metagpt/actions/write_test.py b/metagpt/actions/write_test.py index ddf65c373..0429cb973 100644 --- a/metagpt/actions/write_test.py +++ b/metagpt/actions/write_test.py @@ -35,7 +35,15 @@ class WriteTest(Action): async def write_code(self, prompt): code_rsp = await self._aask(prompt) - code = CodeParser.parse_code(block="", text=code_rsp) + + try: + code = CodeParser.parse_code(block="", text=code_rsp) + except Exception as e: + # Handle the exception if needed + print(f"An exception occurred: {str(e)}") + + # Return code_rsp in case of an exception, assuming llm just returns code as it is and doesn't wrap it inside ``` + code = code_rsp return code async def run(self, code_to_test, test_file_name, source_file_path, workspace): diff --git a/metagpt/roles/qa_engineer.py b/metagpt/roles/qa_engineer.py index 543912f3c..a763c2ce8 100644 --- a/metagpt/roles/qa_engineer.py +++ b/metagpt/roles/qa_engineer.py @@ -8,7 +8,14 @@ import os from pathlib import Path -from metagpt.actions import DebugError, RunCode, WriteCode, WriteDesign, WriteTest +from metagpt.actions import ( + DebugError, + RunCode, + WriteCode, + WriteCodeReview, + WriteDesign, + WriteTest, +) from metagpt.const import WORKSPACE_ROOT from metagpt.logs import logger from metagpt.roles import Role @@ -30,7 +37,7 @@ class QaEngineer(Role): self._init_actions( [WriteTest] ) # FIXME: a bit hack here, only init one action to circumvent _think() logic, will overwrite _think() in future updates - self._watch([WriteCode, WriteTest, RunCode, DebugError]) + self._watch([WriteCode, WriteCodeReview, WriteTest, RunCode, DebugError]) self.test_round = 0 self.test_round_allowed = test_round_allowed @@ -159,7 +166,7 @@ class QaEngineer(Role): for msg in self._rc.news: # Decide what to do based on observed msg type, currently defined by human, # might potentially be moved to _think, that is, let the agent decides for itself - if msg.cause_by == WriteCode: + if msg.cause_by in [WriteCode, WriteCodeReview]: # engineer wrote a code, time to write a test for it await self._write_test(msg) elif msg.cause_by in [WriteTest, DebugError]: From 04f4a353ad715f36cd4976b45bcbb3e0d680521a Mon Sep 17 00:00:00 2001 From: femto Date: Mon, 11 Sep 2023 17:14:53 +0800 Subject: [PATCH 18/22] change stop_after_attempt to 3 for _aask --- metagpt/actions/action.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/metagpt/actions/action.py b/metagpt/actions/action.py index 90c7d59ff..99d1b024a 100644 --- a/metagpt/actions/action.py +++ b/metagpt/actions/action.py @@ -49,7 +49,7 @@ class Action(ABC): system_msgs.append(self.prefix) return await self.llm.aask(prompt, system_msgs) - @retry(stop=stop_after_attempt(2), wait=wait_fixed(1)) + @retry(stop=stop_after_attempt(3), wait=wait_fixed(1)) async def _aask_v1( self, prompt: str, output_class_name: str, output_data_mapping: dict, system_msgs: Optional[list[str]] = None ) -> ActionOutput: @@ -65,7 +65,7 @@ class Action(ABC): instruct_content = output_class(**parsed_data) return ActionOutput(content, instruct_content) - @retry(stop=stop_after_attempt(2), wait=wait_fixed(1)) + @retry(stop=stop_after_attempt(3), wait=wait_fixed(1)) async def _aask_json_v1( self, prompt: str, output_class_name: str, output_data_mapping: dict, system_msgs: Optional[list[str]] = None ) -> ActionOutput: From 74217a1df35bb7073503bc13f29d91f9240e01e2 Mon Sep 17 00:00:00 2001 From: femto Date: Tue, 19 Sep 2023 12:23:22 +0800 Subject: [PATCH 19/22] modified --- metagpt/actions/write_test.py | 5 +- metagpt/utils/custom_decoder.py | 41 +++++++-- requirements.txt | 1 + tests/metagpt/actions/test_write_test.py | 17 +++- tests/metagpt/utils/test_custom_decoder.py | 96 ---------------------- 5 files changed, 54 insertions(+), 106 deletions(-) diff --git a/metagpt/actions/write_test.py b/metagpt/actions/write_test.py index 0429cb973..35ff36dc2 100644 --- a/metagpt/actions/write_test.py +++ b/metagpt/actions/write_test.py @@ -6,6 +6,7 @@ @File : environment.py """ from metagpt.actions.action import Action +from metagpt.logs import logger from metagpt.utils.common import CodeParser PROMPT_TEMPLATE = """ @@ -38,9 +39,9 @@ class WriteTest(Action): try: code = CodeParser.parse_code(block="", text=code_rsp) - except Exception as e: + except Exception: # Handle the exception if needed - print(f"An exception occurred: {str(e)}") + logger.error(f"Can't parse the code: {code_rsp}") # Return code_rsp in case of an exception, assuming llm just returns code as it is and doesn't wrap it inside ``` code = code_rsp diff --git a/metagpt/utils/custom_decoder.py b/metagpt/utils/custom_decoder.py index fcac85963..373d16356 100644 --- a/metagpt/utils/custom_decoder.py +++ b/metagpt/utils/custom_decoder.py @@ -91,6 +91,24 @@ WHITESPACE_STR = " \t\n\r" def JSONObject( s_and_end, strict, scan_once, object_hook, object_pairs_hook, memo=None, _w=WHITESPACE.match, _ws=WHITESPACE_STR ): + """Parse a JSON object from a string and return the parsed object. + + Args: + s_and_end (tuple): A tuple containing the input string to parse and the current index within the string. + strict (bool): If `True`, enforces strict JSON string decoding rules. + If `False`, allows literal control characters in the string. Defaults to `True`. + scan_once (callable): A function to scan and parse JSON values from the input string. + object_hook (callable): A function that, if specified, will be called with the parsed object as a dictionary. + object_pairs_hook (callable): A function that, if specified, will be called with the parsed object as a list of pairs. + memo (dict, optional): A dictionary used to memoize string keys for optimization. Defaults to None. + _w (function): A regular expression matching function for whitespace. Defaults to WHITESPACE.match. + _ws (str): A string containing whitespace characters. Defaults to WHITESPACE_STR. + + Returns: + tuple or dict: A tuple containing the parsed object and the index of the character in the input string + after the end of the object. + """ + s, end = s_and_end pairs = [] pairs_append = pairs.append @@ -175,14 +193,23 @@ def JSONObject( def py_scanstring(s, end, strict=True, _b=BACKSLASH, _m=STRINGCHUNK.match, delimiter='"'): - """Scan the string s for a JSON string. End is the index of the - character in s after the quote that started the JSON string. - Unescapes all valid JSON string escape sequences and raises ValueError - on attempt to decode an invalid string. If strict is False then literal - control characters are allowed in the string. + """Scan the string s for a JSON string. + + Args: + s (str): The input string to be scanned for a JSON string. + end (int): The index of the character in `s` after the quote that started the JSON string. + strict (bool): If `True`, enforces strict JSON string decoding rules. + If `False`, allows literal control characters in the string. Defaults to `True`. + _b (dict): A dictionary containing escape sequence mappings. + _m (function): A regular expression matching function for string chunks. + delimiter (str): The string delimiter used to define the start and end of the JSON string. + Can be one of: '"', "'", '\"""', or "'''". Defaults to '"'. + + Returns: + tuple: A tuple containing the decoded string and the index of the character in `s` + after the end quote. + """ - Returns a tuple of the decoded string and the index of the character in s - after the end quote.""" chunks = [] _append = chunks.append begin = end - 1 diff --git a/requirements.txt b/requirements.txt index 7f29c3343..24f539a98 100644 --- a/requirements.txt +++ b/requirements.txt @@ -38,3 +38,4 @@ typing-inspect==0.8.0 typing_extensions==4.5.0 libcst==1.0.1 qdrant-client==1.4.0 +pytest-mock==3.11.1 \ No newline at end of file diff --git a/tests/metagpt/actions/test_write_test.py b/tests/metagpt/actions/test_write_test.py index 87a22b139..e5acdff44 100644 --- a/tests/metagpt/actions/test_write_test.py +++ b/tests/metagpt/actions/test_write_test.py @@ -31,7 +31,7 @@ async def test_write_test(): code_to_test=code, test_file_name="test_food.py", source_file_path="/some/dummy/path/cli_snake_game/cli_snake_game/food.py", - workspace="/some/dummy/path/cli_snake_game" + workspace="/some/dummy/path/cli_snake_game", ) logger.info(test_code) @@ -40,3 +40,18 @@ async def test_write_test(): assert "from cli_snake_game.food import Food" in test_code assert "class TestFood(unittest.TestCase)" in test_code assert "def test_generate" in test_code + + +@pytest.mark.asyncio +async def test_write_code_invalid_code(mocker): + # Mock the _aask method to return an invalid code string + mocker.patch.object(WriteTest, "_aask", return_value="Invalid Code String") + + # Create an instance of WriteTest + write_test = WriteTest() + + # Call the write_code method + code = await write_test.write_code("Some prompt:") + + # Assert that the returned code is the same as the invalid code string + assert code == "Invalid Code String" diff --git a/tests/metagpt/utils/test_custom_decoder.py b/tests/metagpt/utils/test_custom_decoder.py index 072d96152..c7b14ad59 100644 --- a/tests/metagpt/utils/test_custom_decoder.py +++ b/tests/metagpt/utils/test_custom_decoder.py @@ -70,99 +70,3 @@ def test_parse_triple_single_quote(): parsed_data = decoder.decode(input_data) assert parsed_data["a"] == "b" - - -# def test_parse_complex(): -# # Create a custom JSON decoder -# decoder = CustomDecoder(strict=False) -# # Your provided input with single-quoted strings and line breaks -# input_data = '''{ -# "Required Python third-party packages": [ -# "flask==1.1.2", -# "bcrypt==3.2.0" -# ], -# "Required Other language third-party packages": [ -# "No third-party packages are required." -# ], -# "Full API spec": """ -# openapi: 3.0.0 -# -# description: A JSON object representing the game state. -# -# paths: -# /game: -# get: -# summary: Get the current game state. -# responses: -# '200': -# description: The current game state. -# -# /game/{direction}: -# post: -# summary: Move the snake in the specified direction. -# parameters: -# - name: direction -# in: path -# description: The direction to move the snake (one of UP, DOWN, LEFT, or RIGHT). -# responses: -# '200': -# description: The updated game state. -# -# /game/{food}: -# get: -# summary: Get the current food position. -# responses: -# '200': -# description: The current food position. -# -# /game/{snake}: -# get: -# summary: Get the current snake position. -# responses: -# '200': -# description: The current snake position. -# -# /game/{snake}/{direction}: -# post: -# summary: Set the snake direction. -# parameters: -# - name: direction -# in: path -# description: The direction to move the snake (one of UP, DOWN, LEFT, or RIGHT). -# responses: -# '200': -# description: The updated game state. -# -# /game/end: -# post: -# summary: End the game. -# responses: -# '200': -# description: The final game state. -# " -# ], -# "Logic Analysis": [ -# ["game.py","Contains the game logic and snake movement."], -# ["snake.py","Contains the snake class and methods."], -# ["game_objects.py","Contains the game objects and their properties."], -# ["game_logic.py","Contains the game logic and rules."], -# ["ui.py","Contains the user interface and display logic."] -# ], -# "Task list": [ -# "game.py", -# "snake.py", -# "game_objects.py", -# "game_logic.py", -# "ui.py" -# ], -# "Shared Knowledge": """ -# The game state is represented by a JSON object. -# The snake movement is based on the direction parameter. -# The game ends when the snake collides with the wall or eats the food. -# """, -# "Anything UNCLEAR": "The requirement is clear to me." -# } -# ''' -# # Parse the JSON using the custom decoder -# -# parsed_data = decoder.decode(input_data) From 7ec77e0ad701d261c0655390c434536c434590ab Mon Sep 17 00:00:00 2001 From: femto Date: Tue, 19 Sep 2023 21:31:27 +0800 Subject: [PATCH 20/22] json/markdown format --- config/config.yaml | 4 +- metagpt/actions/action.py | 50 +++++----- metagpt/actions/design_api.py | 85 +++++++++++++++-- metagpt/actions/project_management.py | 99 ++++++++++++++++++-- metagpt/actions/write_prd.py | 127 ++++++++++++++++++++++++-- metagpt/config.py | 10 +- metagpt/utils/common.py | 42 ++++----- metagpt/utils/get_template.py | 20 ++++ 8 files changed, 360 insertions(+), 77 deletions(-) create mode 100644 metagpt/utils/get_template.py diff --git a/config/config.yaml b/config/config.yaml index 93301fcf2..444f55efd 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -82,4 +82,6 @@ MODEL_FOR_RESEARCHER_REPORT: gpt-3.5-turbo-16k # MERMAID_ENGINE: nodejs ### browser path for pyppeteer engine, support Chrome, Chromium,MS Edge -#PYPPETEER_EXECUTABLE_PATH: "/usr/bin/google-chrome-stable" \ No newline at end of file +#PYPPETEER_EXECUTABLE_PATH: "/usr/bin/google-chrome-stable" + +PROMPT_FORMAT: json #json or markdown \ No newline at end of file diff --git a/metagpt/actions/action.py b/metagpt/actions/action.py index 99d1b024a..c688f6661 100644 --- a/metagpt/actions/action.py +++ b/metagpt/actions/action.py @@ -12,6 +12,7 @@ from typing import Optional from tenacity import retry, stop_after_attempt, wait_fixed from metagpt.actions.action_output import ActionOutput +from metagpt.config import CONFIG from metagpt.llm import LLM from metagpt.logs import logger from metagpt.utils.common import OutputParser @@ -51,7 +52,12 @@ class Action(ABC): @retry(stop=stop_after_attempt(3), wait=wait_fixed(1)) async def _aask_v1( - self, prompt: str, output_class_name: str, output_data_mapping: dict, system_msgs: Optional[list[str]] = None + self, + prompt: str, + output_class_name: str, + output_data_mapping: dict, + system_msgs: Optional[list[str]] = None, + format=CONFIG.prompt_format, ) -> ActionOutput: """Append default prefix""" if not system_msgs: @@ -60,37 +66,25 @@ class Action(ABC): content = await self.llm.aask(prompt, system_msgs) logger.debug(content) output_class = ActionOutput.create_model_class(output_class_name, output_data_mapping) - parsed_data = OutputParser.parse_data_with_mapping(content, output_data_mapping) + + if format == "json": + pattern = r"\[CONTENT\](\s*\{.*?\}\s*)\[/CONTENT\]" + matches = re.findall(pattern, content, re.DOTALL) + + for match in matches: + if match: + content = match + break + + parsed_data = CustomDecoder(strict=False).decode(content) + + else: # using markdown parser + parsed_data = OutputParser.parse_data_with_mapping(content, output_data_mapping) + logger.debug(parsed_data) instruct_content = output_class(**parsed_data) return ActionOutput(content, instruct_content) - @retry(stop=stop_after_attempt(3), wait=wait_fixed(1)) - async def _aask_json_v1( - self, prompt: str, output_class_name: str, output_data_mapping: dict, system_msgs: Optional[list[str]] = None - ) -> ActionOutput: - """Append default prefix""" - if not system_msgs: - system_msgs = [] - system_msgs.append(self.prefix) - content = await self.llm.aask(prompt, system_msgs) - logger.debug(content) - output_class = ActionOutput.create_model_class(output_class_name, output_data_mapping) - - pattern = r"\[CONTENT\](\s*\{.*?\}\s*)\[/CONTENT\]" - matches = re.findall(pattern, content, re.DOTALL) - - extracted_content = None - for match in matches: - if match: - extracted_content = match - break - - parsed_data = CustomDecoder(strict=False).decode(extracted_content) - logger.debug(parsed_data) - instruct_content = output_class(**parsed_data) - return ActionOutput(extracted_content, instruct_content) - async def run(self, *args, **kwargs): """Run action""" raise NotImplementedError("The run method should be implemented in a subclass.") diff --git a/metagpt/actions/design_api.py b/metagpt/actions/design_api.py index 89e2ad93d..af0ace497 100644 --- a/metagpt/actions/design_api.py +++ b/metagpt/actions/design_api.py @@ -13,10 +13,13 @@ from metagpt.actions import Action, ActionOutput from metagpt.const import WORKSPACE_ROOT from metagpt.logs import logger from metagpt.utils.common import CodeParser +from metagpt.utils.get_template import get_template from metagpt.utils.json_to_markdown import json_to_markdown from metagpt.utils.mermaid import mermaid_to_file -PROMPT_TEMPLATE = """ +templates = { + "json": { + "PROMPT_TEMPLATE": """ # Context {context} @@ -41,8 +44,8 @@ Max Output: 8192 chars or 2048 tokens. Try to use them up. output a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like format example, and only output the json inside this tag, nothing else -""" -FORMAT_EXAMPLE = """ +""", + "FORMAT_EXAMPLE": """ [CONTENT] { "Implementation approach": "We will ...", @@ -65,7 +68,76 @@ FORMAT_EXAMPLE = """ "Anything UNCLEAR": "The requirement is clear to me." } [/CONTENT] -""" +""", + }, + "markdown": { + "PROMPT_TEMPLATE": """ +# Context +{context} + +## Format example +{format_example} +----- +Role: You are an architect; the goal is to design a SOTA PEP8-compliant python system; make the best use of good open source tools +Requirement: Fill in the following missing information based on the context, note that all sections are response with code form separately +Max Output: 8192 chars or 2048 tokens. Try to use them up. +Attention: Use '##' to split sections, not '#', and '## ' SHOULD WRITE BEFORE the code and triple quote. + +## Implementation approach: Provide as Plain text. Analyze the difficult points of the requirements, select the appropriate open-source framework. + +## Python package name: Provide as Python str with python triple quoto, concise and clear, characters only use a combination of all lowercase and underscores + +## File list: Provided as Python list[str], the list of ONLY REQUIRED files needed to write the program(LESS IS MORE!). Only need relative paths, comply with PEP8 standards. ALWAYS write a main.py or app.py here + +## Data structures and interface definitions: Use mermaid classDiagram code syntax, including classes (INCLUDING __init__ method) and functions (with type annotations), CLEARLY MARK the RELATIONSHIPS between classes, and comply with PEP8 standards. The data structures SHOULD BE VERY DETAILED and the API should be comprehensive with a complete design. + +## Program call flow: Use sequenceDiagram code syntax, COMPLETE and VERY DETAILED, using CLASSES AND API DEFINED ABOVE accurately, covering the CRUD AND INIT of each object, SYNTAX MUST BE CORRECT. + +## Anything UNCLEAR: Provide as Plain text. Make clear here. + +""", + "FORMAT_EXAMPLE": """ +--- +## Implementation approach +We will ... + +## Python package name +```python +"snake_game" +``` + +## File list +```python +[ + "main.py", +] +``` + +## Data structures and interface definitions +```mermaid +classDiagram + class Game{ + +int score + } + ... + Game "1" -- "1" Food: has +``` + +## Program call flow +```mermaid +sequenceDiagram + participant M as Main + ... + G->>M: end game +``` + +## Anything UNCLEAR +The requirement is clear to me. +--- +""", + }, +} + OUTPUT_MAPPING = { "Implementation approach": (str, ...), "Python package name": (str, ...), @@ -130,8 +202,9 @@ class WriteDesign(Action): await self._save_system_design(docs_path, resources_path, system_design) async def run(self, context): - prompt = PROMPT_TEMPLATE.format(context=context, format_example=FORMAT_EXAMPLE) + prompt_template, format_example = get_template(templates) + prompt = prompt_template.format(context=context, format_example=format_example) # system_design = await self._aask(prompt) - system_design = await self._aask_json_v1(prompt, "system_design", OUTPUT_MAPPING) + system_design = await self._aask_v1(prompt, "system_design", OUTPUT_MAPPING) await self._save(context, system_design) return system_design diff --git a/metagpt/actions/project_management.py b/metagpt/actions/project_management.py index 5ae3a728c..bcaf85941 100644 --- a/metagpt/actions/project_management.py +++ b/metagpt/actions/project_management.py @@ -10,9 +10,12 @@ from typing import List from metagpt.actions.action import Action from metagpt.const import WORKSPACE_ROOT from metagpt.utils.common import CodeParser +from metagpt.utils.get_template import get_template from metagpt.utils.json_to_markdown import json_to_markdown -PROMPT_TEMPLATE = """ +templates = { + "json": { + "PROMPT_TEMPLATE": """ # Context {context} @@ -29,7 +32,7 @@ Attention: Use '##' to split sections, not '#', and '## ' SHOULD W ## Full API spec: Use OpenAPI 3.0. Describe all APIs that may be used by both frontend and backend. -## Logic Analysis: Provided as a Python list[str, str]. the first is filename, the second is class/method/function should be implemented in this file. Analyze the dependencies between the files, which work should be done first +## Logic Analysis: Provided as a Python list[list[str]. the first is filename, the second is class/method/function should be implemented in this file. Analyze the dependencies between the files, which work should be done first ## Task list: Provided as Python list[str]. Each str is a filename, the more at the beginning, the more it is a prerequisite dependency, should be done first @@ -39,9 +42,8 @@ Attention: Use '##' to split sections, not '#', and '## ' SHOULD W output a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like format example, and only output the json inside this tag, nothing else -""" - -FORMAT_EXAMPLE = ''' +""", + "FORMAT_EXAMPLE": ''' { "Required Python third-party packages": [ "flask==1.1.2", @@ -66,8 +68,88 @@ FORMAT_EXAMPLE = ''' """, "Anything UNCLEAR": "We need ... how to start." } -''' +''', + }, + "markdown": { + "PROMPT_TEMPLATE": """ +# Context +{context} +## Format example +{format_example} +----- +Role: You are a project manager; the goal is to break down tasks according to PRD/technical design, give a task list, and analyze task dependencies to start with the prerequisite modules +Requirements: Based on the context, fill in the following missing information, note that all sections are returned in Python code triple quote form seperatedly. Here the granularity of the task is a file, if there are any missing files, you can supplement them +Attention: Use '##' to split sections, not '#', and '## ' SHOULD WRITE BEFORE the code and triple quote. + +## Required Python third-party packages: Provided in requirements.txt format + +## Required Other language third-party packages: Provided in requirements.txt format + +## Full API spec: Use OpenAPI 3.0. Describe all APIs that may be used by both frontend and backend. + +## Logic Analysis: Provided as a Python list[list[str]. the first is filename, the second is class/method/function should be implemented in this file. Analyze the dependencies between the files, which work should be done first + +## Task list: Provided as Python list[str]. Each str is a filename, the more at the beginning, the more it is a prerequisite dependency, should be done first + +## Shared Knowledge: Anything that should be public like utils' functions, config's variables details that should make clear first. + +## Anything UNCLEAR: Provide as Plain text. Make clear here. For example, don't forget a main entry. don't forget to init 3rd party libs. + +""", + "FORMAT_EXAMPLE": ''' +--- +## Required Python third-party packages +```python +""" +flask==1.1.2 +bcrypt==3.2.0 +""" +``` + +## Required Other language third-party packages +```python +""" +No third-party ... +""" +``` + +## Full API spec +```python +""" +openapi: 3.0.0 +... +description: A JSON object ... +""" +``` + +## Logic Analysis +```python +[ + ["game.py", "Contains ..."], +] +``` + +## Task list +```python +[ + "game.py", +] +``` + +## Shared Knowledge +```python +""" +'game.py' contains ... +""" +``` + +## Anything UNCLEAR +We need ... how to start. +--- +''', + }, +} OUTPUT_MAPPING = { "Required Python third-party packages": (List[str], ...), "Required Other language third-party packages": (List[str], ...), @@ -96,8 +178,9 @@ class WriteTasks(Action): requirements_path.write_text("\n".join(rsp.instruct_content.dict().get("Required Python third-party packages"))) async def run(self, context): - prompt = PROMPT_TEMPLATE.format(context=context, format_example=FORMAT_EXAMPLE) - rsp = await self._aask_json_v1(prompt, "task", OUTPUT_MAPPING) + prompt_template, format_example = get_template(templates) + prompt = prompt_template.format(context=context, format_example=format_example) + rsp = await self._aask_v1(prompt, "task", OUTPUT_MAPPING) self._save(context, rsp) return rsp diff --git a/metagpt/actions/write_prd.py b/metagpt/actions/write_prd.py index 99032c2de..42c9dd9d1 100644 --- a/metagpt/actions/write_prd.py +++ b/metagpt/actions/write_prd.py @@ -10,8 +10,11 @@ from typing import List from metagpt.actions import Action, ActionOutput from metagpt.actions.search_and_summarize import SearchAndSummarize from metagpt.logs import logger +from metagpt.utils.get_template import get_template -PROMPT_TEMPLATE = """ +templates = { + "json": { + "PROMPT_TEMPLATE": """ # Context ## Original Requirements {requirements} @@ -56,15 +59,15 @@ Requirements: According to the context, fill in the following missing informatio ## Requirement Analysis: Provide as Plain text. Be simple. LESS IS MORE. Make your requirements less dumb. Delete the parts unnessasery. -## Requirement Pool: Provided as Python list[str, str], the parameters are requirement description, priority(P0/P1/P2), respectively, comply with PEP standards; no more than 5 requirements and consider to make its difficulty lower +## Requirement Pool: Provided as Python list[list[str], the parameters are requirement description, priority(P0/P1/P2), respectively, comply with PEP standards; no more than 5 requirements and consider to make its difficulty lower ## UI Design draft: Provide as Plain text. Be simple. Describe the elements and functions, also provide a simple style description and layout description. ## Anything UNCLEAR: Provide as Plain text. Make clear here. output a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like format example, and only output the json inside this tag, nothing else -""" -FORMAT_EXAMPLE = """ +""", + "FORMAT_EXAMPLE": """ [CONTENT] { "Original Requirements": "", @@ -93,7 +96,114 @@ FORMAT_EXAMPLE = """ "Anything UNCLEAR": "", } [/CONTENT] -""" +""", + }, + "markdown": { + "PROMPT_TEMPLATE": """ +# Context +## Original Requirements +{requirements} + +## Search Information +{search_information} + +## mermaid quadrantChart code syntax example. DONT USE QUOTO IN CODE DUE TO INVALID SYNTAX. Replace the with REAL COMPETITOR NAME +```mermaid +quadrantChart + title Reach and engagement of campaigns + x-axis Low Reach --> High Reach + y-axis Low Engagement --> High Engagement + quadrant-1 We should expand + quadrant-2 Need to promote + quadrant-3 Re-evaluate + quadrant-4 May be improved + "Campaign: A": [0.3, 0.6] + "Campaign B": [0.45, 0.23] + "Campaign C": [0.57, 0.69] + "Campaign D": [0.78, 0.34] + "Campaign E": [0.40, 0.34] + "Campaign F": [0.35, 0.78] + "Our Target Product": [0.5, 0.6] +``` + +## Format example +{format_example} +----- +Role: You are a professional product manager; the goal is to design a concise, usable, efficient product +Requirements: According to the context, fill in the following missing information, note that each sections are returned in Python code triple quote form seperatedly. If the requirements are unclear, ensure minimum viability and avoid excessive design +ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. AND '## ' SHOULD WRITE BEFORE the code and triple quote. Output carefully referenced "Format example" in format. + +## Original Requirements: Provide as Plain text, place the polished complete original requirements here + +## Product Goals: Provided as Python list[str], up to 3 clear, orthogonal product goals. If the requirement itself is simple, the goal should also be simple + +## User Stories: Provided as Python list[str], up to 5 scenario-based user stories, If the requirement itself is simple, the user stories should also be less + +## Competitive Analysis: Provided as Python list[str], up to 7 competitive product analyses, consider as similar competitors as possible + +## Competitive Quadrant Chart: Use mermaid quadrantChart code syntax. up to 14 competitive products. Translation: Distribute these competitor scores evenly between 0 and 1, trying to conform to a normal distribution centered around 0.5 as much as possible. + +## Requirement Analysis: Provide as Plain text. Be simple. LESS IS MORE. Make your requirements less dumb. Delete the parts unnessasery. + +## Requirement Pool: Provided as Python list[list[str], the parameters are requirement description, priority(P0/P1/P2), respectively, comply with PEP standards; no more than 5 requirements and consider to make its difficulty lower + +## UI Design draft: Provide as Plain text. Be simple. Describe the elements and functions, also provide a simple style description and layout description. +## Anything UNCLEAR: Provide as Plain text. Make clear here. +""", + "FORMAT_EXAMPLE": """ +--- +## Original Requirements +The boss ... + +## Product Goals +```python +[ + "Create a ...", +] +``` + +## User Stories +```python +[ + "As a user, ...", +] +``` + +## Competitive Analysis +```python +[ + "Python Snake Game: ...", +] +``` + +## Competitive Quadrant Chart +```mermaid +quadrantChart + title Reach and engagement of campaigns + ... + "Our Target Product": [0.6, 0.7] +``` + +## Requirement Analysis +The product should be a ... + +## Requirement Pool +```python +[ + ["End game ...", "P0"] +] +``` + +## UI Design draft +Give a basic function description, and a draft + +## Anything UNCLEAR +There are no unclear points. +--- +""", + }, +} + OUTPUT_MAPPING = { "Original Requirements": (str, ...), "Product Goals": (List[str], ...), @@ -120,10 +230,11 @@ class WritePRD(Action): logger.info(sas.result) logger.info(rsp) - prompt = PROMPT_TEMPLATE.format( - requirements=requirements, search_information=info, format_example=FORMAT_EXAMPLE + prompt_template, format_example = get_template(templates) + prompt = prompt_template.format( + requirements=requirements, search_information=info, format_example=format_example ) logger.debug(prompt) # prd = await self._aask_v1(prompt, "prd", OUTPUT_MAPPING) - prd = await self._aask_json_v1(prompt, "prd", OUTPUT_MAPPING) + prd = await self._aask_v1(prompt, "prd", OUTPUT_MAPPING) return prd diff --git a/metagpt/config.py b/metagpt/config.py index b4e0fe7fa..2140334a6 100644 --- a/metagpt/config.py +++ b/metagpt/config.py @@ -59,7 +59,7 @@ class Config(metaclass=Singleton): self.openai_api_rpm = self._get("RPM", 3) self.openai_api_model = self._get("OPENAI_API_MODEL", "gpt-4") self.max_tokens_rsp = self._get("MAX_TOKENS", 2048) - self.deployment_name = self._get('DEPLOYMENT_NAME') + self.deployment_name = self._get("DEPLOYMENT_NAME") self.deployment_id = self._get("DEPLOYMENT_ID") self.claude_api_key = self._get("Anthropic_API_KEY") @@ -83,8 +83,10 @@ 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') - self.pyppeteer_executable_path = self._get("PYPPETEER_EXECUTABLE_PATH", '') + self.mermaid_engine = self._get("MERMAID_ENGINE", "nodejs") + self.pyppeteer_executable_path = self._get("PYPPETEER_EXECUTABLE_PATH", "") + + self.prompt_format = self._get("PROMPT_FORMAT", "json") 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""" @@ -113,4 +115,4 @@ class Config(metaclass=Singleton): return value -CONFIG = Config() \ No newline at end of file +CONFIG = Config() diff --git a/metagpt/utils/common.py b/metagpt/utils/common.py index 5f94de066..9add0db8f 100644 --- a/metagpt/utils/common.py +++ b/metagpt/utils/common.py @@ -17,20 +17,19 @@ from metagpt.logs import logger def check_cmd_exists(command) -> int: - """ 检查命令是否存在 + """检查命令是否存在 :param command: 待检查的命令 :return: 如果命令存在,返回0,如果不存在,返回非0 """ - if platform.system().lower() == 'windows': - check_command = 'where ' + command + if platform.system().lower() == "windows": + check_command = "where " + command else: - check_command = 'command -v ' + command + ' >/dev/null 2>&1 || { echo >&2 "no mermaid"; exit 1; }' + check_command = "command -v " + command + ' >/dev/null 2>&1 || { echo >&2 "no mermaid"; exit 1; }' result = os.system(check_command) return result class OutputParser: - @classmethod def parse_blocks(cls, text: str): # 首先根据"##"将文本分割成不同的block @@ -54,7 +53,7 @@ class OutputParser: @classmethod def parse_code(cls, text: str, lang: str = "") -> str: - pattern = rf'```{lang}.*?\s+(.*?)```' + pattern = rf"```{lang}.*?\s+(.*?)```" match = re.search(pattern, text, re.DOTALL) if match: code = match.group(1) @@ -65,13 +64,13 @@ class OutputParser: @classmethod def parse_str(cls, text: str): text = text.split("=")[-1] - text = text.strip().strip("'").strip("\"") + text = text.strip().strip("'").strip('"') return text @classmethod def parse_file_list(cls, text: str) -> list[str]: # Regular expression pattern to find the tasks list. - pattern = r'\s*(.*=.*)?(\[.*\])' + pattern = r"\s*(.*=.*)?(\[.*\])" # Extract tasks list string using regex. match = re.search(pattern, text, re.DOTALL) @@ -83,12 +82,12 @@ class OutputParser: else: tasks = text.split("\n") return tasks - + @staticmethod def parse_python_code(text: str) -> str: for pattern in ( - r'(.*?```python.*?\s+)?(?P.*)(```.*?)', - r'(.*?```python.*?\s+)?(?P.*)', + r"(.*?```python.*?\s+)?(?P.*)(```.*?)", + r"(.*?```python.*?\s+)?(?P.*)", ): match = re.search(pattern, text, re.DOTALL) if not match: @@ -135,7 +134,7 @@ class OutputParser: typing = typing_define[0] else: typing = typing_define - if typing == List[str] or typing == List[Tuple[str, str]]: + if typing == List[str] or typing == List[Tuple[str, str]] or typing == List[List[str]]: # 尝试解析list try: content = cls.parse_file_list(text=content) @@ -153,7 +152,6 @@ class OutputParser: class CodeParser: - @classmethod def parse_block(cls, block: str, text: str) -> str: blocks = cls.parse_blocks(text) @@ -184,7 +182,7 @@ class CodeParser: def parse_code(cls, block: str, text: str, lang: str = "") -> str: if block: text = cls.parse_block(block, text) - pattern = rf'```{lang}.*?\s+(.*?)```' + pattern = rf"```{lang}.*?\s+(.*?)```" match = re.search(pattern, text, re.DOTALL) if match: code = match.group(1) @@ -199,7 +197,7 @@ class CodeParser: def parse_str(cls, block: str, text: str, lang: str = ""): code = cls.parse_code(block, text, lang) code = code.split("=")[-1] - code = code.strip().strip("'").strip("\"") + code = code.strip().strip("'").strip('"') return code @classmethod @@ -207,7 +205,7 @@ class CodeParser: # Regular expression pattern to find the tasks list. code = cls.parse_code(block, text, lang) # print(code) - pattern = r'\s*(.*=.*)?(\[.*\])' + pattern = r"\s*(.*=.*)?(\[.*\])" # Extract tasks list string using regex. match = re.search(pattern, code, re.DOTALL) @@ -230,7 +228,7 @@ class NoMoneyException(Exception): super().__init__(self.message) def __str__(self): - return f'{self.message} -> Amount required: {self.amount}' + return f"{self.message} -> Amount required: {self.amount}" def print_members(module, indent=0): @@ -240,19 +238,19 @@ def print_members(module, indent=0): :param indent: :return: """ - prefix = ' ' * indent + prefix = " " * indent for name, obj in inspect.getmembers(module): print(name, obj) if inspect.isclass(obj): - print(f'{prefix}Class: {name}') + print(f"{prefix}Class: {name}") # print the methods within the class - if name in ['__class__', '__base__']: + if name in ["__class__", "__base__"]: continue print_members(obj, indent + 2) elif inspect.isfunction(obj): - print(f'{prefix}Function: {name}') + print(f"{prefix}Function: {name}") elif inspect.ismethod(obj): - print(f'{prefix}Method: {name}') + print(f"{prefix}Method: {name}") def parse_recipient(text): diff --git a/metagpt/utils/get_template.py b/metagpt/utils/get_template.py new file mode 100644 index 000000000..e374188e0 --- /dev/null +++ b/metagpt/utils/get_template.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@Time : 2023/9/19 20:39 +@Author : femto Zheng +@File : get_template.py +""" +from metagpt.config import CONFIG + + +def get_template(templates): + selected_templates = templates.get(CONFIG.prompt_format) + if selected_templates is None: + raise ValueError(f"Can't find {CONFIG.prompt_format} in passed in templates") + + # Extract the selected templates + prompt_template = selected_templates["PROMPT_TEMPLATE"] + format_example = selected_templates["FORMAT_EXAMPLE"] + + return prompt_template, format_example From 0d9bbcb863a1bd667817c1975fb32e8fbcb754fb Mon Sep 17 00:00:00 2001 From: femto Date: Wed, 20 Sep 2023 12:09:10 +0800 Subject: [PATCH 21/22] json/markdown format --- config/config.yaml | 2 +- metagpt/actions/design_api.py | 7 ++++--- metagpt/actions/project_management.py | 7 ++++--- metagpt/actions/write_prd.py | 7 ++++--- metagpt/config.py | 2 +- metagpt/utils/get_template.py | 6 +++--- 6 files changed, 17 insertions(+), 14 deletions(-) diff --git a/config/config.yaml b/config/config.yaml index 444f55efd..17339eda5 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -84,4 +84,4 @@ MODEL_FOR_RESEARCHER_REPORT: gpt-3.5-turbo-16k ### browser path for pyppeteer engine, support Chrome, Chromium,MS Edge #PYPPETEER_EXECUTABLE_PATH: "/usr/bin/google-chrome-stable" -PROMPT_FORMAT: json #json or markdown \ No newline at end of file +PROMPT_FORMAT: markdown #json or markdown \ No newline at end of file diff --git a/metagpt/actions/design_api.py b/metagpt/actions/design_api.py index af0ace497..f19fcbeaa 100644 --- a/metagpt/actions/design_api.py +++ b/metagpt/actions/design_api.py @@ -10,6 +10,7 @@ from pathlib import Path from typing import List from metagpt.actions import Action, ActionOutput +from metagpt.config import CONFIG from metagpt.const import WORKSPACE_ROOT from metagpt.logs import logger from metagpt.utils.common import CodeParser @@ -201,10 +202,10 @@ class WriteDesign(Action): await self._save_prd(docs_path, resources_path, context) await self._save_system_design(docs_path, resources_path, system_design) - async def run(self, context): - prompt_template, format_example = get_template(templates) + async def run(self, context, format=CONFIG.prompt_format): + prompt_template, format_example = get_template(templates, format) prompt = prompt_template.format(context=context, format_example=format_example) # system_design = await self._aask(prompt) - system_design = await self._aask_v1(prompt, "system_design", OUTPUT_MAPPING) + system_design = await self._aask_v1(prompt, "system_design", OUTPUT_MAPPING, format=format) await self._save(context, system_design) return system_design diff --git a/metagpt/actions/project_management.py b/metagpt/actions/project_management.py index bcaf85941..b395fa64e 100644 --- a/metagpt/actions/project_management.py +++ b/metagpt/actions/project_management.py @@ -8,6 +8,7 @@ from typing import List from metagpt.actions.action import Action +from metagpt.config import CONFIG from metagpt.const import WORKSPACE_ROOT from metagpt.utils.common import CodeParser from metagpt.utils.get_template import get_template @@ -177,10 +178,10 @@ class WriteTasks(Action): requirements_path = WORKSPACE_ROOT / ws_name / "requirements.txt" requirements_path.write_text("\n".join(rsp.instruct_content.dict().get("Required Python third-party packages"))) - async def run(self, context): - prompt_template, format_example = get_template(templates) + async def run(self, context, format=CONFIG.prompt_format): + prompt_template, format_example = get_template(templates, format) prompt = prompt_template.format(context=context, format_example=format_example) - rsp = await self._aask_v1(prompt, "task", OUTPUT_MAPPING) + rsp = await self._aask_v1(prompt, "task", OUTPUT_MAPPING, format=format) self._save(context, rsp) return rsp diff --git a/metagpt/actions/write_prd.py b/metagpt/actions/write_prd.py index 42c9dd9d1..bd04ca79e 100644 --- a/metagpt/actions/write_prd.py +++ b/metagpt/actions/write_prd.py @@ -9,6 +9,7 @@ from typing import List from metagpt.actions import Action, ActionOutput from metagpt.actions.search_and_summarize import SearchAndSummarize +from metagpt.config import CONFIG from metagpt.logs import logger from metagpt.utils.get_template import get_template @@ -221,7 +222,7 @@ class WritePRD(Action): def __init__(self, name="", context=None, llm=None): super().__init__(name, context, llm) - async def run(self, requirements, *args, **kwargs) -> ActionOutput: + async def run(self, requirements, format=CONFIG.prompt_format, *args, **kwargs) -> ActionOutput: sas = SearchAndSummarize() # rsp = await sas.run(context=requirements, system_text=SEARCH_AND_SUMMARIZE_SYSTEM_EN_US) rsp = "" @@ -230,11 +231,11 @@ class WritePRD(Action): logger.info(sas.result) logger.info(rsp) - prompt_template, format_example = get_template(templates) + prompt_template, format_example = get_template(templates, format) prompt = prompt_template.format( requirements=requirements, search_information=info, format_example=format_example ) logger.debug(prompt) # prd = await self._aask_v1(prompt, "prd", OUTPUT_MAPPING) - prd = await self._aask_v1(prompt, "prd", OUTPUT_MAPPING) + prd = await self._aask_v1(prompt, "prd", OUTPUT_MAPPING, format=format) return prd diff --git a/metagpt/config.py b/metagpt/config.py index 2140334a6..53271133b 100644 --- a/metagpt/config.py +++ b/metagpt/config.py @@ -86,7 +86,7 @@ class Config(metaclass=Singleton): self.mermaid_engine = self._get("MERMAID_ENGINE", "nodejs") self.pyppeteer_executable_path = self._get("PYPPETEER_EXECUTABLE_PATH", "") - self.prompt_format = self._get("PROMPT_FORMAT", "json") + self.prompt_format = self._get("PROMPT_FORMAT", "markdown") 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/get_template.py b/metagpt/utils/get_template.py index e374188e0..86c1915f7 100644 --- a/metagpt/utils/get_template.py +++ b/metagpt/utils/get_template.py @@ -8,10 +8,10 @@ from metagpt.config import CONFIG -def get_template(templates): - selected_templates = templates.get(CONFIG.prompt_format) +def get_template(templates, format=CONFIG.prompt_format): + selected_templates = templates.get(format) if selected_templates is None: - raise ValueError(f"Can't find {CONFIG.prompt_format} in passed in templates") + raise ValueError(f"Can't find {format} in passed in templates") # Extract the selected templates prompt_template = selected_templates["PROMPT_TEMPLATE"] From 961e73e94c59c7a64ffbef4da3fb6aebfc7832a8 Mon Sep 17 00:00:00 2001 From: femto Date: Wed, 20 Sep 2023 12:18:39 +0800 Subject: [PATCH 22/22] json/markdown format --- config/config.yaml | 2 +- metagpt/actions/action.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/config/config.yaml b/config/config.yaml index 17339eda5..444f55efd 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -84,4 +84,4 @@ MODEL_FOR_RESEARCHER_REPORT: gpt-3.5-turbo-16k ### browser path for pyppeteer engine, support Chrome, Chromium,MS Edge #PYPPETEER_EXECUTABLE_PATH: "/usr/bin/google-chrome-stable" -PROMPT_FORMAT: markdown #json or markdown \ No newline at end of file +PROMPT_FORMAT: json #json or markdown \ No newline at end of file diff --git a/metagpt/actions/action.py b/metagpt/actions/action.py index c688f6661..790295d55 100644 --- a/metagpt/actions/action.py +++ b/metagpt/actions/action.py @@ -12,7 +12,6 @@ from typing import Optional from tenacity import retry, stop_after_attempt, wait_fixed from metagpt.actions.action_output import ActionOutput -from metagpt.config import CONFIG from metagpt.llm import LLM from metagpt.logs import logger from metagpt.utils.common import OutputParser @@ -57,7 +56,7 @@ class Action(ABC): output_class_name: str, output_data_mapping: dict, system_msgs: Optional[list[str]] = None, - format=CONFIG.prompt_format, + format="markdown", # compatible to original format ) -> ActionOutput: """Append default prefix""" if not system_msgs: