1. record news in rc; 2. add msg sent to; 3. qa_engineer restructure

This commit is contained in:
yzlin 2023-07-31 02:12:34 +08:00
parent 9ba18672c6
commit 6bf527d31e
9 changed files with 330 additions and 63 deletions

View file

@ -7,13 +7,32 @@
"""
from metagpt.actions.action import Action
PROMPT_TEMPLATE = """
NOTICE
1. Role: You are a Development Engineer or QA engineer;
2. Task: You received this message from another Development Engineer or QA engineer who run or test your code.
Based on the message, first, figure out your own role, i.e. Engineer or QaEngineer,
then rewrite the development code or the test code based on your role, the error, and the summary, such that all bugs are fixed and the code performs well.
Attention: Use '##' to split sections, not '#', and '## <SECTION_NAME>' SHOULD WRITE BEFORE the test case or script and triple quote.
The message is as follows:
{context}
---
Now you should start rewriting the code:
## file name of the code to rewrite: Write code with triple quoto. Do your best to implement THIS ONLY ONE FILE.
"""
class DebugError(Action):
def __init__(self, name, context=None, llm=None):
super().__init__(name, context, llm)
async def run(self, code, error):
prompt = f"Here is a piece of Python code:\n\n{code}\n\nThe following error occurred during execution:" \
f"\n\n{error}\n\nPlease try to fix the error in this code."
fixed_code = await self._aask(prompt)
return fixed_code
# async def run(self, code, error):
# prompt = f"Here is a piece of Python code:\n\n{code}\n\nThe following error occurred during execution:" \
# f"\n\n{error}\n\nPlease try to fix the error in this code."
# fixed_code = await self._aask(prompt)
# return fixed_code
async def run(self, context):
prompt = PROMPT_TEMPLATE.format(context=context)
rsp = await self._aask(prompt)
return rsp

View file

@ -12,6 +12,42 @@ import subprocess
from metagpt.logs import logger
from metagpt.actions.action import Action
PROMPT_TEMPLATE = """
Role: You are a senior development and qa engineer, your role is summarize the code running result.
If the running result does not include an error, you should explicitly approve the result.
On the other hand, if the running result indicates some error, you should point out which part, the development code or the test code, produces the error,
and give specific instructions on fixing the errors.
---
## Development Code File Name
{code_file_name}
## Development Code
```python
{code}
```
## Test File Name
{test_file_name}
## Test Code
```python
{test_code}
```
## Running Command
{command}
## Running Output
standard output: {outs};
standard errors: {errs};
## instruction:
Please summarize the cause of the errors and give correction instruction
## File To Rewrite
Determine the ONE file to rewrite in order to fix the error, for example, xyz.py, or test_xyz.py
## Status:
Determine if all of the code works fine, if so write PASS, else FAIL,
WRITE ONLY ONE WORD, PASS OR FAIL, IN THI SECTION
## Send To:
Please write Engineer if the errors are due to problematic development codes, and QaEngineer to problematic test codes, and NoOne if there are no errors,
WRITE ONLY ONE WORD, Engineer OR QaEngineer OR NoOne, IN THIS SECTION.
---
You should fill in necessary summary, status, send to, and finally return all content between the --- segment line.
"""
class RunCode(Action):
def __init__(self, name="RunCode", context=None, llm=None):
@ -52,11 +88,24 @@ class RunCode(Action):
process.kill() # Kill the process if it times out
stdout, stderr = process.communicate()
return stdout.decode('utf-8'), stderr.decode('utf-8')
async def run(self, context="", mode="script", **kwargs):
async def run(
self, code, mode="script", code_file_name="", test_code="", test_file_name="", command=[], **kwargs
):
if mode == "script":
outs, errs = await self.run_script(**kwargs)
outs, errs = await self.run_script(command=command, **kwargs)
elif mode == "text":
outs, errs = await self.run_text(**kwargs)
outs, errs = await self.run_text(code=code)
return outs, errs
logger.info(outs)
logger.info(errs)
prompt = PROMPT_TEMPLATE.format(
code=code, code_file_name=code_file_name,
test_code=test_code, test_file_name=test_file_name,
command=" ".join(command),
outs=outs, errs=errs
)
rsp = await self._aask(prompt)
return rsp

View file

@ -16,7 +16,7 @@ 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 WRITECODE_MSG_SEP, FILENAME_CODE_SEP
from metagpt.utils.special_tokens import MSG_SEP, FILENAME_CODE_SEP
async def gather_ordered_k(coros, k) -> list:
@ -144,7 +144,12 @@ class Engineer(Role):
code_msg_all.append(FILENAME_CODE_SEP.join([todo, str(file_path), code]))
logger.info(f'Done {self.get_workspace()} generating.')
msg = Message(content=WRITECODE_MSG_SEP.join(code_msg_all), role=self.profile, cause_by=type(self._rc.todo))
msg = Message(
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:
@ -186,7 +191,12 @@ class Engineer(Role):
code_msg_all.append(FILENAME_CODE_SEP.join([todo, str(file_path), code]))
logger.info(f'Done {self.get_workspace()} generating.')
msg = Message(content=WRITECODE_MSG_SEP.join(code_msg_all), role=self.profile, cause_by=type(self._rc.todo))
msg = Message(
content=MSG_SEP.join(code_msg_all),
role=self.profile,
cause_by=type(self._rc.todo),
send_to="QaEngineer"
)
return msg
async def _act(self) -> Message:

View file

@ -7,15 +7,16 @@
"""
import re
from pathlib import Path
from typing import Type
from metagpt.actions import WriteTest, WriteCode, WriteDesign, RunCode
from metagpt.actions import WriteTest, WriteCode, WriteDesign, RunCode, DebugError
from metagpt.const import WORKSPACE_ROOT
from metagpt.logs import logger
from metagpt.roles import Role
from metagpt.schema import Message
from metagpt.roles.engineer import Engineer
from metagpt.utils.common import CodeParser
from metagpt.utils.special_tokens import WRITECODE_MSG_SEP, FILENAME_CODE_SEP
from metagpt.utils.special_tokens import MSG_SEP, FILENAME_CODE_SEP
class QaEngineer(Role):
def __init__(self, name="Edward", profile="QA Engineer",
@ -51,39 +52,40 @@ class QaEngineer(Role):
def recv(self, message: Message) -> None:
self._rc.memory.add(message)
async def _act(self) -> Message:
code_action_watched = self._rc.important_memory[-1]
code_msgs = code_action_watched.content.split(WRITECODE_MSG_SEP)
async def _write_test(self, message: Message) -> None:
code_msgs = message.content.split(MSG_SEP)
result_msg_all = []
for code_msg in code_msgs:
# write tests
file_name, file_path, code_to_test = code_msg.split(FILENAME_CODE_SEP)
test_file_name = "test_" + file_name
logger.info(f'Writing {test_file_name}..')
code = await WriteTest().run(
test_code = await WriteTest().run(
code_to_test=code_to_test,
test_file_name=test_file_name,
# source_file_name=file_name,
source_file_path=file_path,
workspace=self.get_workspace()
)
self.write_file(test_file_name, code)
# add to memory
msg = Message(content=code, role=self.profile, cause_by=WriteTest)
self._rc.memory.add(msg)
self.write_file(test_file_name, test_code)
# run tests
stdout, stderr = await RunCode().run(
result_msg = await RunCode().run(
mode="script",
code=code_to_test,
code_file_name=file_name,
test_code=test_code,
test_file_name=test_file_name,
command=['python', f'tests/{test_file_name}'],
working_directory=self.get_workspace(), # workspace/package_name, will run tests/test_xxx.py here
additional_python_paths=[self.get_workspace(return_proj_dir=False)], # workspace/package_name/package_name,
# import statement inside package code needs this
command=['python', f'tests/{test_file_name}']
)
logger.info(stdout)
logger.info(stderr)
result_msg_all.append(result_msg)
# RunCode().run(
# mode="script",
@ -91,7 +93,27 @@ class QaEngineer(Role):
# additional_python_paths=[self.get_workspace(return_proj_dir=False)],
# command=['python', '-m', 'unittest', 'discover', '-s', 'tests']
# )
logger.info(f'Done {self.get_workspace()} generating.')
msg = Message(content="all done.", role=self.profile, cause_by=WriteTest)
logger.info(f'Done {self.get_workspace()}/tests generating.')
msg_content = MSG_SEP.join(result_msg_all)
msg = Message(content=msg_content, role=self.profile, cause_by=RunCode, send_to=QaEngineer)
return msg
async def _debug_error(self, msg):
# process the msg, if the code works fine, no need to debug
# else: debug and rewrite the code
pass
async def _act(self) -> Message:
for msg in self._rc.news:
if msg.send_to != "QaEngineer":
continue
if msg.cause_by == WriteCode:
# engineer wrote a code, write a test for it
result_msg = await self._write_test(msg)
elif msg.cause_by == RunCode:
# I wrote and ran my test code, fix bugs, if any
result_msg = await self._debug_error(msg)
return result_msg

View file

@ -70,6 +70,7 @@ class RoleContext(BaseModel):
state: int = Field(default=0)
todo: Action = Field(default=None)
watch: set[Type[Action]] = Field(default_factory=set)
news: list[Type[Message]] = Field(default=[])
class Config:
arbitrary_types_allowed = True
@ -184,15 +185,15 @@ class Role:
observed = self._rc.env.memory.get_by_actions(self._rc.watch)
news = self._rc.memory.remember(observed) # remember recent exact or similar memories
self._rc.news = self._rc.memory.remember(observed) # remember recent exact or similar memories
for i in env_msgs:
self.recv(i)
news_text = [f"{i.role}: {i.content[:20]}..." for i in news]
news_text = [f"{i.role}: {i.content[:20]}..." for i in self._rc.news]
if news_text:
logger.debug(f'{self._setting} observed: {news_text}')
return len(news)
return len(self._rc.news)
def _publish_message(self, msg):
"""如果role归属于env那么role的消息会向env广播"""

View file

@ -27,6 +27,7 @@ class Message:
instruct_content: BaseModel = field(default=None)
role: str = field(default='user') # system / user / assistant
cause_by: Type["Action"] = field(default="")
send_to: str = field(default="")
def __str__(self):
# prefix = '-'.join([self.role, str(self.cause_by)])

View file

@ -1,4 +1,4 @@
# token to separate different code messages in a WriteCode Message content
WRITECODE_MSG_SEP = "#*000*#"
MSG_SEP = "#*000*#"
# token to seperate file name and the actual code text in a code message
FILENAME_CODE_SEP = "#*001*#"
FILENAME_CODE_SEP = "#*001*#"