1. RunCode -> DebugError loop done; 2. modify default k for memory search

This commit is contained in:
yzlin 2023-08-01 12:16:38 +08:00
parent 6bf527d31e
commit 8abdca3057
10 changed files with 140 additions and 70 deletions

View file

@ -141,7 +141,8 @@ class Engineer(Role):
msg = Message(content=code, role=self.profile, cause_by=type(self._rc.todo))
self._rc.memory.add(msg)
code_msg_all.append(FILENAME_CODE_SEP.join([todo, str(file_path), code]))
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(
@ -188,7 +189,8 @@ class Engineer(Role):
msg = Message(content=code, role=self.profile, cause_by=WriteCode)
self._rc.memory.add(msg)
code_msg_all.append(FILENAME_CODE_SEP.join([todo, str(file_path), code]))
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(

View file

@ -15,7 +15,7 @@ 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.common import CodeParser, parse_recipient
from metagpt.utils.special_tokens import MSG_SEP, FILENAME_CODE_SEP
class QaEngineer(Role):
@ -24,7 +24,9 @@ class QaEngineer(Role):
constraints="The test code you write should conform to code standard like PEP8, be modular, easy to read and maintain"):
super().__init__(name, profile, goal, constraints)
self._init_actions([WriteTest])
self._watch([WriteCode])
self._watch([WriteCode, WriteTest, RunCode, DebugError])
self.test_round = 0
self.test_round_allowed = 5 # hard code for 1 WriteTest round + 2 x (RunCode -> DebugError loop)
@classmethod
def parse_workspace(cls, system_design_msg: Message) -> str:
@ -38,12 +40,11 @@ class QaEngineer(Role):
return WORKSPACE_ROOT / 'src'
workspace = self.parse_workspace(msg)
# project directory: workspace/{package_name}, which contains package source code folder, tests folder, resources folder, etc.
# source codes directory: workspace/{package_name}/{package_name}
if return_proj_dir:
return WORKSPACE_ROOT / workspace
# development codes directory: workspace/{package_name}/{package_name}
return WORKSPACE_ROOT / workspace / workspace
def write_file(self, filename: str, code: str):
workspace = self.get_workspace() / 'tests'
file = workspace / filename
@ -60,8 +61,12 @@ class QaEngineer(Role):
for code_msg in code_msgs:
# write tests
file_name, file_path, code_to_test = code_msg.split(FILENAME_CODE_SEP)
file_name, file_path = code_msg.split(FILENAME_CODE_SEP)
code_to_test = open(file_path, "r").read()
if "test" in file_name:
continue # Engineer might write some test files, skip testing a test file
test_file_name = "test_" + file_name
test_file_path = self.get_workspace() / "tests" / test_file_name
logger.info(f'Writing {test_file_name}..')
test_code = await WriteTest().run(
code_to_test=code_to_test,
@ -72,48 +77,86 @@ class QaEngineer(Role):
)
self.write_file(test_file_name, test_code)
# run tests
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
# prepare context for run tests in next round
command = ['python', f'tests/{test_file_name}']
file_info = {
"file_name": file_name, "file_path": str(file_path),
"test_file_name": test_file_name, "test_file_path": str(test_file_path),
"command": command
}
msg = Message(
content=str(file_info), role=self.profile, cause_by=WriteTest,
sent_from="QaEngineer", send_to="QaEngineer"
)
result_msg_all.append(result_msg)
# RunCode().run(
# mode="script",
# working_directory=self.get_workspace(),
# additional_python_paths=[self.get_workspace(return_proj_dir=False)],
# command=['python', '-m', 'unittest', 'discover', '-s', 'tests']
# )
self._publish_message(msg)
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 _run_code(self, msg):
file_info = eval(msg.content)
code_to_test = open(file_info["file_path"], "r").read()
test_code = open(file_info["test_file_path"], "r").read()
proj_dir = self.get_workspace()
development_code_dir = self.get_workspace(return_proj_dir=False)
result_msg = await RunCode().run(
mode="script",
code=code_to_test,
code_file_name=file_info["file_name"],
test_code=test_code,
test_file_name=file_info["test_file_name"],
command=file_info["command"],
working_directory=proj_dir, # workspace/package_name, will run tests/test_xxx.py here
additional_python_paths=[development_code_dir], # workspace/package_name/package_name,
# import statement inside package code needs this
)
recipient = parse_recipient(result_msg) # the recipient might be Engineer or myself
content = str(file_info) + FILENAME_CODE_SEP + result_msg
msg = Message(
content=content, role=self.profile, cause_by=RunCode,
sent_from="QaEngineer", send_to=recipient
)
self._publish_message(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
file_info, context = msg.content.split(FILENAME_CODE_SEP)
file_name, code = await DebugError().run(context)
if file_name:
self.write_file(file_name, code)
recipient = msg.sent_from # send back to the one who ran the code for another run, might be one's self
msg = Message(content=file_info, role=self.profile, cause_by=DebugError, sent_from="QaEngineer", send_to=recipient)
self._publish_message(msg)
async def _observe(self) -> int:
await super()._observe()
self._rc.news = [msg for msg in self._rc.news \
if msg.send_to == "QaEngineer"] # only relevant msgs count as observed news
return len(self._rc.news)
async def _act(self) -> Message:
if self.test_round > self.test_round_allowed:
result_msg = Message(
content=f"Exceeding {self.test_round_allowed} rounds of tests, skip (writing code counts as a round, too)",
role=self.profile, cause_by=WriteTest, sent_from="QaEngineer", send_to=""
)
return result_msg
for msg in self._rc.news:
if msg.send_to != "QaEngineer":
continue
# 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:
# engineer wrote a code, write a test for it
# engineer wrote a code, time to write a test for it
result_msg = await self._write_test(msg)
elif msg.cause_by in [WriteTest, DebugError]:
# I wrote or debugged my test code, time to run it
result_msg = await self._run_code(msg)
elif msg.cause_by == RunCode:
# I wrote and ran my test code, fix bugs, if any
# I ran my test code, time to fix bugs, if any
result_msg = await self._debug_error(msg)
self.test_round += 1
result_msg = Message(
content=f"Round {self.test_round} of tests done",
role=self.profile, cause_by=WriteTest, sent_from="QaEngineer", send_to=""
)
return result_msg