diff --git a/metagpt/actions/__init__.py b/metagpt/actions/__init__.py index b004bd58e..64a02f49d 100644 --- a/metagpt/actions/__init__.py +++ b/metagpt/actions/__init__.py @@ -14,6 +14,7 @@ from metagpt.actions.design_api import WriteDesign from metagpt.actions.design_api_review import DesignReview from metagpt.actions.design_filenames import DesignFilenames +from metagpt.actions.internal_feedback import Feedback from metagpt.actions.project_management import AssignTasks, WriteTasks from metagpt.actions.research import CollectLinks, WebBrowseAndSummarize, ConductResearch from metagpt.actions.run_code import RunCode @@ -45,6 +46,7 @@ class ActionType(Enum): COLLECT_LINKS = CollectLinks WEB_BROWSE_AND_SUMMARIZE = WebBrowseAndSummarize CONDUCT_RESEARCH = ConductResearch + INTERNAL_FEEDBACK = Feedback __all__ = [ diff --git a/metagpt/actions/design_api.py b/metagpt/actions/design_api.py index 4d17e4f5e..8f7eda01f 100644 --- a/metagpt/actions/design_api.py +++ b/metagpt/actions/design_api.py @@ -105,6 +105,7 @@ def recreate_workspace(self, workspace: Path): async def _save_prd(self, docs_path, resources_path, prd): prd_file = docs_path / 'prd.md' + prd = prd.content if not isinstance(prd, str) else prd quadrant_chart = CodeParser.parse_code(block="Competitive Quadrant Chart", text=prd) await mermaid_to_file(quadrant_chart, resources_path / 'competitive_analysis') logger.info(f"Saving PRD to {prd_file}") diff --git a/metagpt/actions/internal_feedback.py b/metagpt/actions/internal_feedback.py new file mode 100644 index 000000000..7c22f9418 --- /dev/null +++ b/metagpt/actions/internal_feedback.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@Time : 2023/9/15 15:00 +@Author : mczhuge +@File : handover_eval.py +""" +import os +import json +from typing import List, Tuple +from metagpt.actions import Action, ActionOutput +from metagpt.actions.search_and_summarize import SearchAndSummarize +from metagpt.logs import logger +from metagpt.config import CONFIG + +ROSTER = { + "BOSS": {"name": "BOSS", "next": "ProductManager", "prev": None}, + "ProductManager": {"name": "Alice", "next": "Architect", "prev": "BOSS"}, + "Architect": {"name": "Bob", "next": "ProjectManager", "prev": "ProductManager"}, + "ProjectManager": {"name": "Eve", "next": "Engineer", "prev": "Architect"}, + "Engineer": {"name": "Alex", "next": "QaEngineer", "prev": "ProjectManager"}, + "QaEngineer": {"name": "Edward", "next": None, "prev": "Engineer"}, +} + +def print_with_color(text, color="red"): + + color_codes = { + 'reset': '\033[0m', + 'red': '\033[91m', + 'green': '\033[92m', + 'yellow': '\033[93m', + 'blue': '\033[94m', + } + print(f"{color_codes[color]} {text} {color_codes['reset']}") + + + +class Feedback(Action): + def __init__(self, name="", context=None, llm=None): + super().__init__(name, context, llm) + + # Noted: Using 'They' or 'Their' is not a grammatical issue; https://www.quora.com/What-pronoun-to-use-when-you-dont-know-the-gender + self.PROMPT_TEMPLATE = """You are {your_name}, in the role of {your_role}. + You recently worked together with {prev_name} on a project, where they held the position of {prev_role}. + They shared their work details with you: {prev_msg}. + Your current task involves incorporating their input into your {your_role} duties. + Critically evaluate this handover process and, drawing on {prev_role}, suggest potential ways for improvement (avoid mentioning the details of the current project). + Please provide your assessment concisely in 20 words or less: + """ + + async def run(self, handover_msg, *args, **kwargs) -> ActionOutput: + import re + #prev_role = handover_msg[0].to_dict()["role"] + #prev_msg = handover_msg[0].to_dict()["content"] + if isinstance(handover_msg, list): + handover_msg = handover_msg[0] + prev_role = handover_msg.to_dict()["role"].replace(" ", "") + prev_msg = handover_msg.to_dict()["content"] + + prev_name = ROSTER[prev_role]["name"] + your_role = ROSTER[prev_role]["next"] + your_name = ROSTER[your_role]["name"] + + prompt = self.PROMPT_TEMPLATE.format(your_name=your_name, + your_role=your_role, + prev_name=prev_name, + prev_role=prev_role, + prev_msg=prev_msg) + logger.debug(prompt) + feedback = await self._aask(prompt) + + pattern = r"## Python package name\n```python(.*?)```" + match = re.search(pattern, prev_msg, re.DOTALL) + + if match: + package_name = match.group(1).replace("\n", "").replace("\"","") + else: + package_name = None + + return feedback, prev_role, package_name diff --git a/metagpt/actions/project_management.py b/metagpt/actions/project_management.py index 3096eb94b..ef493906d 100644 --- a/metagpt/actions/project_management.py +++ b/metagpt/actions/project_management.py @@ -10,6 +10,7 @@ from metagpt.actions.action import Action from metagpt.const import WORKSPACE_ROOT from metagpt.utils.common import CodeParser +from metagpt.schema import Message PROMPT_TEMPLATE = ''' # Context @@ -107,6 +108,7 @@ def __init__(self, name="CreateTasks", context=None, llm=None): super().__init__(name, context, llm) def _save(self, context, rsp): + #ws_input = context[-1].content.content if isinstance(context[-1].content, Message) else ws_input 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(rsp.content) diff --git a/metagpt/config.py b/metagpt/config.py index b4e0fe7fa..e3a051ec0 100644 --- a/metagpt/config.py +++ b/metagpt/config.py @@ -8,7 +8,7 @@ import openai import yaml -from metagpt.const import PROJECT_ROOT +from metagpt.const import PROJECT_ROOT, HANDOVER_FILE from metagpt.logs import logger from metagpt.tools import SearchEngineType, WebBrowserEngineType from metagpt.utils.singleton import Singleton @@ -37,6 +37,7 @@ class Config(metaclass=Singleton): _instance = None key_yaml_file = PROJECT_ROOT / "config/key.yaml" default_yaml_file = PROJECT_ROOT / "config/config.yaml" + handover_file = HANDOVER_FILE def __init__(self, yaml_file=default_yaml_file): self._configs = {} diff --git a/metagpt/const.py b/metagpt/const.py index b8b08628e..618235148 100644 --- a/metagpt/const.py +++ b/metagpt/const.py @@ -38,5 +38,7 @@ def get_project_root(): TUTORIAL_PATH = DATA_PATH / "tutorial_docx" SKILL_DIRECTORY = PROJECT_ROOT / "metagpt/skills" +LONG_TERM_MEMORY_PATH = WORKSPACE_ROOT / "long_term_memory" +HANDOVER_FILE = LONG_TERM_MEMORY_PATH / "handover.json" MEM_TTL = 24 * 30 * 3600 diff --git a/metagpt/learn/__init__.py b/metagpt/learn/__init__.py index 28b8739c3..8dc82c93d 100644 --- a/metagpt/learn/__init__.py +++ b/metagpt/learn/__init__.py @@ -1,7 +1,12 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- """ -@Time : 2023/4/30 20:57 -@Author : alexanderwu +@Time : 2023/9/20 20:57 +@Author : mczhuge @File : __init__.py """ +from metagpt.learn.reflect import Reflect + +__all__ = [ + "Reflect" +] \ No newline at end of file diff --git a/metagpt/learn/reflect.py b/metagpt/learn/reflect.py new file mode 100644 index 000000000..4a3e5fc3b --- /dev/null +++ b/metagpt/learn/reflect.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@Time : 2023/9/20 12:15 +@Author : mczhuge +@File : reflect.py +""" + +import json +from metagpt.actions import Action +from metagpt.provider import OpenAIGPTAPI +from metagpt.config import CONFIG + +FEEDBACK_EXAMPLE = """ +When you work on the "{proj_name}" project, others provide the following suggestion: {handover_feedback} +""" + +PROMPT_TEMPLATE = """ +You work as a {role} at a software company. + +You've previously followed certain constraints: + +### Constraints +{constraints} + +After collaborating with your colleagues, they've provided you with important feedback: + +### Feedback +{whole_feedback} + +You can choose to accept suggestions that align with your role. Don't forget the original constraints. + +Now, rewrite your "{role}" constraints in 30 words: +""" + +# def print_with_color(text, color="red"): + +# color_codes = { +# 'reset': '\033[0m', +# 'red': '\033[91m', +# 'green': '\033[92m', +# 'yellow': '\033[93m', +# 'blue': '\033[94m', +# } +# print(f"{color_codes[color]} {text} {color_codes['reset']}") + +class Reflect(): + def from_feedback(role, constraints): + + chat = OpenAIGPTAPI() + with open(CONFIG.handover_file, "r") as file: + data = json.load(file) + + feedback_for_role = [] + for key, feedback_data in data.items(): + if 'Project Name' in feedback_data: + feedback_for_role.append(FEEDBACK_EXAMPLE.format(proj_name=feedback_data['Project Name'], + handover_feedback=feedback_data[role.replace(" ", "")])) + + whole_feedback = "\n".join(feedback_for_role) + new_constraints = chat.ask(msg = PROMPT_TEMPLATE.format(role=role, + constraints=constraints, + whole_feedback=whole_feedback)) + + return new_constraints diff --git a/metagpt/memory/longterm_memory.py b/metagpt/memory/longterm_memory.py index 1f4698704..78c9bfe5c 100644 --- a/metagpt/memory/longterm_memory.py +++ b/metagpt/memory/longterm_memory.py @@ -2,10 +2,12 @@ # -*- coding: utf-8 -*- # @Desc : the implement of Long-term memory +import json from metagpt.logs import logger from metagpt.memory import Memory from metagpt.memory.memory_storage import MemoryStorage from metagpt.schema import Message +from metagpt.config import CONFIG class LongTermMemory(Memory): @@ -68,4 +70,27 @@ def delete(self, message: Message): def clear(self): super(LongTermMemory, self).clear() self.memory_storage.clean() - \ No newline at end of file + + def save_feedback(self, message: Message, init=False, project_name=None): + + with open(CONFIG.handover_file, "r") as file: + data = json.load(file) + + if not data: + current_key = str(1) + elif init: + current_key = str(max(map(int, data.keys())) + 1) + else: + current_key = str(max(map(int, data.keys()))) + + if current_key not in data.keys(): + data[current_key] = {} + + data[current_key][message.send_to] = message.content + if project_name != None: + item = data[current_key] + item = {"Project Name": project_name, **item} + data[current_key] = item + + with open(CONFIG.handover_file, "w") as file: + json.dump(data, file, indent=4) diff --git a/metagpt/provider/openai_api.py b/metagpt/provider/openai_api.py index 7e865f288..c48b11fbc 100644 --- a/metagpt/provider/openai_api.py +++ b/metagpt/provider/openai_api.py @@ -280,11 +280,17 @@ async def acompletion_batch_text(self, batch: list[list[dict]]) -> list[str]: return results def _update_costs(self, usage: dict): + if CONFIG.calc_usage: try: - prompt_tokens = int(usage["prompt_tokens"]) - completion_tokens = int(usage["completion_tokens"]) - self._cost_manager.update_cost(prompt_tokens, completion_tokens, self.model) + if "usage" in usage.keys(): + prompt_tokens = int(usage["usage"]["prompt_tokens"]) + completion_tokens = int(usage["usage"]["completion_tokens"]) + self._cost_manager.update_cost(prompt_tokens, completion_tokens, self.model) + else: + prompt_tokens = int(usage["prompt_tokens"]) + completion_tokens = int(usage["completion_tokens"]) + self._cost_manager.update_cost(prompt_tokens, completion_tokens, self.model) except Exception as e: logger.error("updating costs failed!", e) diff --git a/metagpt/roles/architect.py b/metagpt/roles/architect.py index d0756672e..856c373a6 100644 --- a/metagpt/roles/architect.py +++ b/metagpt/roles/architect.py @@ -6,8 +6,10 @@ @File : architect.py """ -from metagpt.actions import WriteDesign, WritePRD +from metagpt.actions import WriteDesign, WritePRD, Feedback from metagpt.roles import Role +from metagpt.logs import logger +from metagpt.schema import Message class Architect(Role): @@ -25,14 +27,55 @@ 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: + constraints: str = "Try to specify good open source tools as much as possible", + feedback: str = True) -> None: """Initializes the Architect with given attributes.""" - super().__init__(name, profile, goal, constraints) + super().__init__(name, profile, goal, constraints, feedback) # Initialize actions specific to the Architect role self._init_actions([WriteDesign]) - + if feedback: + self._add_action_at_head(Feedback) # Set events or actions the Architect should watch or be aware of - self._watch({WritePRD}) + self._watch([WritePRD]) + + async def _think(self) -> None: + + if self._rc.todo is None: + self._set_state(0) + return + + if self._rc.state + 1 < len(self._states): + self._set_state(self._rc.state + 1) + else: + self._rc.todo = None + + async def _act(self) -> Message: + logger.info(f"{self._setting}: ready to {self._rc.todo}") + todo = self._rc.todo + + if isinstance(todo, Feedback): + msg = self._rc.memory.get_by_action(WritePRD)[0] + feedback, prev_role, _ = await todo.run(msg) + ret = Message(feedback, role=self.profile, cause_by=type(todo), send_to=prev_role) + self._rc.long_term_memory.save_feedback(ret, init=False) + elif isinstance(todo, WriteDesign): + msg = self._rc.memory.get_by_action(WritePRD) + design = await todo.run(msg) + ret = Message(design.content, role=self.profile, cause_by=WriteDesign) + else: + raise NotImplementedError + + self._rc.memory.add(ret) + return ret + + async def _react(self) -> Message: + while True: + await self._think() + if self._rc.todo is None: + break + msg = await self._act() + todo = self._rc.todo + ret = Message(msg.content, role=self.profile, cause_by=type(todo)) - \ No newline at end of file + return ret \ No newline at end of file diff --git a/metagpt/roles/engineer.py b/metagpt/roles/engineer.py index d6218d05b..de6cee476 100644 --- a/metagpt/roles/engineer.py +++ b/metagpt/roles/engineer.py @@ -13,7 +13,7 @@ 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.actions import WriteCode, WriteCodeReview, WriteTasks, WriteDesign, Feedback from metagpt.schema import Message from metagpt.utils.common import CodeParser from metagpt.utils.special_tokens import MSG_SEP, FILENAME_CODE_SEP @@ -66,13 +66,18 @@ def __init__(self, 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: + use_code_review: bool = False, + feedback: bool = True) -> None: """Initializes the Engineer role with given attributes.""" - super().__init__(name, profile, goal, constraints) + super().__init__(name, profile, goal, constraints, feedback) self._init_actions([WriteCode]) self.use_code_review = use_code_review + self.feedback = feedback + if self.feedback: + self._add_action_at_head(Feedback) if self.use_code_review: - self._init_actions([WriteCode, WriteCodeReview]) + self._add_action_at_tail(WriteCodeReview) + self._watch([WriteTasks]) self.todos = [] self.n_borg = n_borg @@ -121,6 +126,8 @@ def recv(self, message: Message) -> None: self._rc.memory.add(message) if message in self._rc.important_memory: self.todos = self.parse_tasks(message) + if self.feedback: + self.todos.insert(0, Feedback()) async def _act_mp(self) -> Message: # self.recreate_workspace() @@ -149,19 +156,25 @@ async def _act_mp(self) -> Message: async def _act_sp(self) -> Message: 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 - ) - # logger.info(todo) - # logger.info(code_rsp) - # code = self.parse_code(code_rsp) - file_path = self.write_file(todo, code) - msg = Message(content=code, role=self.profile, cause_by=type(self._rc.todo)) - self._rc.memory.add(msg) - - code_msg = todo + FILENAME_CODE_SEP + str(file_path) - code_msg_all.append(code_msg) + if isinstance(todo, Feedback): + msg = self._rc.memory.get_by_action(WriteTasks) + feedback, prev_role = await todo.run(msg) + ret = Message(feedback, role=self.profile, cause_by=type(todo), send_to=prev_role) + self._rc.long_term_memory.save_feedback(ret, init=False) + else: + code = await WriteCode().run( + context=self._rc.history, + filename=todo + ) + # logger.info(todo) + # logger.info(code_rsp) + # code = self.parse_code(code_rsp) + file_path = self.write_file(todo, code) + msg = Message(content=code, role=self.profile, cause_by=type(self._rc.todo)) + self._rc.memory.add(msg) + + code_msg = todo + FILENAME_CODE_SEP + str(file_path) + code_msg_all.append(code_msg) logger.info(f'Done {self.get_workspace()} generating.') msg = Message( @@ -175,41 +188,48 @@ async def _act_sp(self) -> Message: async def _act_sp_precision(self) -> Message: 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): - 1. All from Architect - 2. All from ProjectManager - 3. Do we need other codes (currently needed)? - TODO: The goal is not to need it. After clear task decomposition, based on the design idea, you should be able to write a single file without needing other codes. If you can't, it means you need a clearer definition. This is the key to writing longer code. - """ - context = [] - msg = self._rc.memory.get_by_actions([WriteDesign, WriteTasks, WriteCode]) - for m in msg: - context.append(m.content) - context_str = "\n".join(context) - # Write code - 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 - ) - code = rewrite_code - except Exception as e: - logger.error("code review failed!", e) - pass - file_path = self.write_file(todo, code) - msg = Message(content=code, role=self.profile, cause_by=WriteCode) - self._rc.memory.add(msg) - code_msg = todo + FILENAME_CODE_SEP + str(file_path) - code_msg_all.append(code_msg) + if isinstance(todo, Feedback): + msg = self._rc.memory.get_by_action(WriteTasks) + feedback, prev_role, _ = await todo.run(msg) + ret = Message(feedback, role=self.profile, cause_by=type(todo), send_to=prev_role) + self._rc.long_term_memory.save_feedback(ret, init=False) + else: + """ + # Select essential information from the historical data to reduce the length of the prompt (summarized from human experience): + 1. All from Architect + 2. All from ProjectManager + 3. Do we need other codes (currently needed)? + TODO: The goal is not to need it. After clear task decomposition, based on the design idea, you should be able to write a single file without needing other codes. If you can't, it means you need a clearer definition. This is the key to writing longer code. + """ + context = [] + msg = self._rc.memory.get_by_actions([WriteDesign, WriteTasks, WriteCode]) + for m in msg: + context.append(m.content) + context_str = "\n".join(context) + # Write code + 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 + ) + code = rewrite_code + except Exception as e: + logger.error("code review failed!", e) + pass + file_path = self.write_file(todo, code) + msg = Message(content=code, role=self.profile, cause_by=WriteCode) + self._rc.memory.add(msg) + + code_msg = todo + FILENAME_CODE_SEP + str(file_path) + code_msg_all.append(code_msg) logger.info(f'Done {self.get_workspace()} generating.') msg = Message( diff --git a/metagpt/roles/product_manager.py b/metagpt/roles/product_manager.py index 9996e907a..6553fb0b4 100644 --- a/metagpt/roles/product_manager.py +++ b/metagpt/roles/product_manager.py @@ -5,8 +5,11 @@ @Author : alexanderwu @File : product_manager.py """ -from metagpt.actions import BossRequirement, WritePRD +from metagpt.actions import BossRequirement, WritePRD, Feedback from metagpt.roles import Role +from metagpt.logs import logger +from metagpt.schema import Message +from metagpt.learn import Reflect class ProductManager(Role): @@ -24,7 +27,8 @@ def __init__(self, name: str = "Alice", profile: str = "Product Manager", goal: str = "Efficiently create a successful product", - constraints: str = "") -> None: + constraints: str = "", + feedback: bool = True) -> None: """ Initializes the ProductManager role with given attributes. @@ -34,6 +38,58 @@ def __init__(self, goal (str): Goal of the product manager. constraints (str): Constraints or limitations for the product manager. """ - super().__init__(name, profile, goal, constraints) + super().__init__(name, profile, goal, constraints, feedback) + + self.constraints = constraints + self._init_actions([WritePRD]) - self._watch([BossRequirement]) \ No newline at end of file + + print("-0-", constraints) + + if feedback: + # Adopt suggestion from later role + self.constraints = Reflect.from_feedback(role=profile, constraints=self.constraints) + # Give feedback to previous role + self._add_action_at_head(Feedback) + + print("-1-", self.constraints) + self._watch([BossRequirement]) + + async def _think(self) -> None: + + if self._rc.todo is None: + self._set_state(0) + return + + if self._rc.state + 1 < len(self._states): + self._set_state(self._rc.state + 1) + else: + self._rc.todo = None + + async def _act(self) -> Message: + logger.info(f"{self._setting}: ready to {self._rc.todo}") + todo = self._rc.todo + msg = self._rc.memory.get_by_action(BossRequirement) + if isinstance(todo, Feedback): + feedback, prev_role, _ = await todo.run(msg) + ret = Message(feedback, role=self.profile, cause_by=type(todo), send_to=prev_role) + self._rc.long_term_memory.save_feedback(ret, init=True) + elif isinstance(todo, WritePRD): + prd = await todo.run(msg) + ret = Message(prd.content, role=self.profile, cause_by=WritePRD) + else: + raise NotImplementedError + + self._rc.memory.add(ret) + return ret + + async def _react(self) -> Message: + while True: + await self._think() + if self._rc.todo is None: + break + msg = await self._act() + todo = self._rc.todo + ret = Message(msg.content, role=self.profile, cause_by=type(todo)) + + return ret \ No newline at end of file diff --git a/metagpt/roles/project_manager.py b/metagpt/roles/project_manager.py index dd4ba42ae..5e2787377 100644 --- a/metagpt/roles/project_manager.py +++ b/metagpt/roles/project_manager.py @@ -5,9 +5,10 @@ @Author : alexanderwu @File : project_manager.py """ -from metagpt.actions import WriteDesign, WriteTasks +from metagpt.actions import WriteDesign, WriteTasks, Feedback from metagpt.roles import Role - +from metagpt.logs import logger +from metagpt.schema import Message class ProjectManager(Role): """ @@ -24,7 +25,8 @@ def __init__(self, name: str = "Eve", profile: str = "Project Manager", goal: str = "Improve team efficiency and deliver with quality and quantity", - constraints: str = "") -> None: + constraints: str = "", + feedback: bool = True) -> None: """ Initializes the ProjectManager role with given attributes. @@ -34,6 +36,51 @@ def __init__(self, goal (str): Goal of the project manager. constraints (str): Constraints or limitations for the project manager. """ - super().__init__(name, profile, goal, constraints) + super().__init__(name, profile, goal, constraints, feedback) + # Initialize actions specific to the ProjectManager role self._init_actions([WriteTasks]) - self._watch([WriteDesign]) \ No newline at end of file + if feedback: + self._add_action_at_head(Feedback) + # Set events or actions the ProjectManager should watch or be aware of + self._watch([WriteDesign]) + + async def _think(self) -> None: + + if self._rc.todo is None: + self._set_state(0) + return + + if self._rc.state + 1 < len(self._states): + self._set_state(self._rc.state + 1) + else: + self._rc.todo = None + + async def _act(self) -> Message: + logger.info(f"{self._setting}: ready to {self._rc.todo}") + todo = self._rc.todo + + if isinstance(todo, Feedback): + msg = self._rc.memory.get_by_action(WriteDesign)[0] + feedback, prev_role, proj_name = await todo.run(msg) + ret = Message(feedback, role=self.profile, cause_by=type(todo), send_to=prev_role) + self._rc.long_term_memory.save_feedback(ret, init=False, project_name=proj_name) + elif isinstance(todo, WriteTasks): + msg = self._rc.memory.get_by_action(WriteDesign) + tasks = await todo.run(msg) + ret = Message(tasks.content, role=self.profile, cause_by=WriteTasks) + else: + raise NotImplementedError + + self._rc.memory.add(ret) + return ret + + async def _react(self) -> Message: + while True: + await self._think() + if self._rc.todo is None: + break + msg = await self._act() + todo = self._rc.todo + ret = Message(msg.content, role=self.profile, cause_by=type(todo)) #.content? + + return ret \ No newline at end of file diff --git a/metagpt/roles/qa_engineer.py b/metagpt/roles/qa_engineer.py index 65bf2cc5b..91a360130 100644 --- a/metagpt/roles/qa_engineer.py +++ b/metagpt/roles/qa_engineer.py @@ -176,4 +176,4 @@ async def _act(self) -> Message: sent_from=self.profile, send_to="", ) - return result_msg + return result_msg \ No newline at end of file diff --git a/metagpt/roles/role.py b/metagpt/roles/role.py index b1ae51cf5..805a4fc04 100644 --- a/metagpt/roles/role.py +++ b/metagpt/roles/role.py @@ -116,6 +116,24 @@ def _init_actions(self, actions): self._actions.append(i) self._states.append(f"{idx}. {action}") + def _add_action_at_head(self, action): + if not isinstance(action, Action): + i = action("") + else: + i = action + i.set_prefix(self._get_prefix(), self.profile) + self._actions.insert(0, i) + self._states.insert(0, f"{0}. {action}") + + def _add_action_at_tail(self, action): + if not isinstance(action, Action): + i = action("") + else: + i = action + i.set_prefix(self._get_prefix(), self.profile) + self._actions.append(i) + self._states.append(f"{len(self._actions) - 1}. {action}") + def _watch(self, actions: Iterable[Type[Action]]): """Listen to the corresponding behaviors""" self._rc.watch.update(actions) @@ -190,7 +208,7 @@ async def _observe(self) -> int: for i in env_msgs: self.recv(i) - news_text = [f"{i.role}: {i.content[:20]}..." for i in self._rc.news] + news_text = [f"{i.role}: {str(i.content)[:20]}..." for i in self._rc.news] if news_text: logger.debug(f'{self._setting} observed: {news_text}') return len(self._rc.news) diff --git a/metagpt/software_company.py b/metagpt/software_company.py index b2bd18c58..d9eb1f491 100644 --- a/metagpt/software_company.py +++ b/metagpt/software_company.py @@ -7,7 +7,7 @@ """ from pydantic import BaseModel, Field -from metagpt.actions import BossRequirement +from metagpt.actions import BossRequirement, Feedback from metagpt.config import CONFIG from metagpt.environment import Environment from metagpt.logs import logger @@ -38,6 +38,22 @@ def invest(self, investment: float): CONFIG.max_budget = investment logger.info(f'Investment: ${investment}.') + def improvement(self, initial=False, roles=None): + handover_file = CONFIG.handover_file + if initial: + import os + import json + os.makedirs(os.path.dirname(handover_file), exist_ok=True) + if not os.path.exists(handover_file): + with open(handover_file, "w") as file: + json.dump({}, file) + else: + msgs = self.environment.memory.get_by_action(Feedback) + if isinstance(msgs, list): + for msg in msgs: + logger.info(f"{msg.role}'s feedback: {msg.content}") + + def _check_balance(self): if CONFIG.total_cost > CONFIG.max_budget: raise NoMoneyException(CONFIG.total_cost, f'Insufficient funds: {CONFIG.max_budget}') diff --git a/startup.py b/startup.py index e2a903c9b..279535a7d 100644 --- a/startup.py +++ b/startup.py @@ -21,31 +21,39 @@ async def startup( code_review: bool = False, run_tests: bool = False, implement: bool = True, + self_improvement: bool = False, ): """Run a startup. Be a boss.""" company = SoftwareCompany() + company.invest(investment) + company.hire( [ - ProductManager(), - Architect(), - ProjectManager(), + ProductManager(feedback = self_improvement), + Architect(feedback = self_improvement), + ProjectManager(feedback = self_improvement), ] ) # if implement or code_review if implement or code_review: # developing features: implement the idea - company.hire([Engineer(n_borg=5, use_code_review=code_review)]) + company.hire([Engineer(n_borg=5, use_code_review=code_review, feedback = self_improvement)]) if run_tests: # developing features: run tests on the spot and identify bugs # (bug fixing capability comes soon!) company.hire([QaEngineer()]) + + if self_improvement: + company.improvement(initial=True) - company.invest(investment) company.start_project(idea) await company.run(n_round=n_round) + if self_improvement: + company.improvement(roles = company.environment.get_roles()) + def main( idea: str,