Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding a mechanism for self-improvement #353

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions metagpt/actions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -45,6 +46,7 @@ class ActionType(Enum):
COLLECT_LINKS = CollectLinks
WEB_BROWSE_AND_SUMMARIZE = WebBrowseAndSummarize
CONDUCT_RESEARCH = ConductResearch
INTERNAL_FEEDBACK = Feedback


__all__ = [
Expand Down
1 change: 1 addition & 0 deletions metagpt/actions/design_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down
80 changes: 80 additions & 0 deletions metagpt/actions/internal_feedback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Time : 2023/9/15 15:00
@Author : mczhuge
@File : handover_eval.py
mczhuge marked this conversation as resolved.
Show resolved Hide resolved
"""
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"):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

place it in utils


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']}")



Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PEP8

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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import it in file header

#prev_role = handover_msg[0].to_dict()["role"]
#prev_msg = handover_msg[0].to_dict()["content"]
if isinstance(handover_msg, list):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dup space

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(.*?)```"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the relevant output has been changed to json in order to be compatible with llama, the functions to obtain the relevant output need to be unified.

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
2 changes: 2 additions & 0 deletions metagpt/actions/project_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion metagpt/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = {}
Expand Down
2 changes: 2 additions & 0 deletions metagpt/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
9 changes: 7 additions & 2 deletions metagpt/learn/__init__.py
Original file line number Diff line number Diff line change
@@ -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"
]
65 changes: 65 additions & 0 deletions metagpt/learn/reflect.py
Original file line number Diff line number Diff line change
@@ -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"):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove useless comments


# 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():
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

class Reflect instead of class Reflect()

def from_feedback(role, constraints):

chat = OpenAIGPTAPI()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use LLM instead

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
27 changes: 26 additions & 1 deletion metagpt/memory/longterm_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -68,4 +70,27 @@ def delete(self, message: Message):
def clear(self):
super(LongTermMemory, self).clear()
self.memory_storage.clean()


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)
12 changes: 9 additions & 3 deletions metagpt/provider/openai_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if "usage" in usage.keys():
    usage = usage["usage"]

except Exception as e:
logger.error("updating costs failed!", e)

Expand Down
55 changes: 49 additions & 6 deletions metagpt/roles/architect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If no modification is involved, you do not need to copy these codes


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))


return ret