Merge branch 'feature/rfc243' into 'mgx_ops'

feat: rfc243

See merge request pub/MetaGPT!170
This commit is contained in:
王金淋 2024-06-28 06:15:52 +00:00
commit ce485dc436
42 changed files with 2458 additions and 33 deletions

View file

@ -0,0 +1,125 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Time : 2024/6/13
@Author : mashenquan
@File : write_project_framework.py
@Desc : The implementation of RFC243. https://deepwisdom.feishu.cn/wiki/QobGwPkImijoyukBUKHcrYetnBb
"""
import asyncio
import json
import uuid
from json import JSONDecodeError
from pathlib import Path
from typing import Dict, List
import typer
from pydantic import BaseModel
from metagpt.config2 import Config
from metagpt.const import DEFAULT_WORKSPACE_ROOT
from metagpt.context import Context
from metagpt.environment import Environment
from metagpt.environment.mgx.mgx_env import MGXEnv
from metagpt.logs import logger
from metagpt.roles import Architect
from metagpt.roles.di.team_leader import TeamLeader
from metagpt.schema import AIMessage, UserMessage
from metagpt.strategy.experience_retriever import TRDToolExpRetriever
from metagpt.utils.common import aread
app = typer.Typer(add_completion=False)
class EnvBuilder(BaseModel):
context: Context
user_requirements: List[str]
actors: Dict[str, str]
technical_constraint: str
output_dir: Path
def build(self) -> Environment:
env = MGXEnv(context=self.context)
team_leader = TeamLeader()
architect = Architect(experience_retriever=TRDToolExpRetriever())
# Prepare context
use_case_actors = "".join([f"- {v}: {k}\n" for k, v in self.actors.items()])
msg = """
The content of "Actor, System, External System" provides an explanation of actors and systems that appear in UML Use Case diagram.
## Actor, System, External System
{use_case_actors}
"""
architect.rc.memory.add(AIMessage(content=msg.format(use_case_actors=use_case_actors)))
# Prepare technical requirements
msg = """
"Additional Technical Requirements" specifies the additional technical requirements that the generated software framework code must meet.
## Additional Technical Requirements
{technical_requirements}
"""
architect.rc.memory.add(AIMessage(content=msg.format(technical_requirements=self.technical_constraint)))
env.add_roles([team_leader, architect])
return env
async def develop(
context: Context,
user_requirement_filename: str,
actors_filename: str,
constraint_filename: str,
output_dir: str,
):
output_dir = Path(output_dir) if output_dir else DEFAULT_WORKSPACE_ROOT / uuid.uuid4().hex
v = await aread(filename=user_requirement_filename)
try:
user_requirements = json.loads(v)
except JSONDecodeError:
user_requirements = [v]
v = await aread(filename=actors_filename)
actors = json.loads(v)
technical_constraint = await aread(filename=constraint_filename)
env_builder = EnvBuilder(
context=context,
user_requirements=user_requirements,
actors=actors,
technical_constraint=technical_constraint,
output_dir=output_dir,
)
env = env_builder.build()
msg = """
Given the user requirement of "User Requirements", write out the software framework.
## User Requirements
{user_requirements}
"""
env.publish_message(
UserMessage(content=msg.format(user_requirements="\n".join(user_requirements)), send_to="Bob"),
user_defined_recipient="Bob",
)
while not env.is_idle:
await env.run()
@app.command()
def startup(
user_requirement_filename: str = typer.Argument(..., help="The filename of the user requirements."),
actors_filename: str = typer.Argument(..., help="The filename of UML use case actors description."),
llm_config: str = typer.Option(default="", help="Low-cost LLM config"),
constraint_filename: str = typer.Option(default="", help="What technical dependency constraints are."),
output_dir: str = typer.Option(default="", help="Output directory."),
):
if llm_config and Path(llm_config).exists():
config = Config.from_yaml_file(Path(llm_config))
else:
logger.info("GPT 4 turbo is recommended")
config = Config.default()
ctx = Context(config=config)
asyncio.run(develop(ctx, user_requirement_filename, actors_filename, constraint_filename, output_dir))
if __name__ == "__main__":
app()

View file

@ -0,0 +1,194 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Time : 2024/6/13
@Author : mashenquan
@File : write_project_framework.py
@Desc : The implementation of RFC243. https://deepwisdom.feishu.cn/wiki/QobGwPkImijoyukBUKHcrYetnBb
"""
import asyncio
import json
import uuid
from pathlib import Path
from typing import Dict, List
import typer
from metagpt.actions.requirement_analysis.framework import (
EvaluateFramework,
WriteFramework,
save_framework,
)
from metagpt.actions.requirement_analysis.trd import (
CompressExternalInterfaces,
DetectInteraction,
EvaluateTRD,
WriteTRD,
)
from metagpt.config2 import Config
from metagpt.const import DEFAULT_WORKSPACE_ROOT
from metagpt.context import Context
from metagpt.logs import logger
from metagpt.utils.common import aread
app = typer.Typer(add_completion=False)
async def _write_trd(
context: Context, actors: Dict[str, str], user_requirements: List[str], available_external_interfaces: str
) -> (str, str):
detect_interaction = DetectInteraction(context=context)
write_trd = WriteTRD(context=context)
evaluate_trd = EvaluateTRD(context=context)
use_case_actors = "".join([f"- {v}: {k}\n" for k, v in actors.items()])
legacy_user_requirements = []
legacy_user_requirements_interaction_events = []
legacy_user_requirements_trd = ""
for ix, r in enumerate(user_requirements):
is_pass = False
evaluation_conclusion = ""
interaction_events = ""
trd = ""
while not is_pass and (context.cost_manager.total_cost < context.cost_manager.max_budget):
interaction_events = await detect_interaction.run(
user_requirements=r,
use_case_actors=use_case_actors,
legacy_interaction_events=interaction_events,
evaluation_conclusion=evaluation_conclusion,
)
if ix == 0:
trd = await write_trd.run(
user_requirements=r,
use_case_actors=use_case_actors,
available_external_interfaces=available_external_interfaces,
evaluation_conclusion=evaluation_conclusion,
interaction_events=interaction_events,
previous_version_trd=trd,
)
else:
trd = await write_trd.run(
user_requirements=r,
use_case_actors=use_case_actors,
available_external_interfaces=available_external_interfaces,
evaluation_conclusion=evaluation_conclusion,
interaction_events=interaction_events,
previous_version_trd=trd,
legacy_user_requirements="\n".join(legacy_user_requirements),
legacy_user_requirements_trd=legacy_user_requirements_trd,
legacy_user_requirements_interaction_events="\n".join(legacy_user_requirements_interaction_events),
)
evaluation = await evaluate_trd.run(
user_requirements=r,
use_case_actors=use_case_actors,
trd=trd,
interaction_events=interaction_events,
legacy_user_requirements_interaction_events="\n".join(legacy_user_requirements_interaction_events),
)
is_pass = evaluation.is_pass
evaluation_conclusion = evaluation.conclusion
legacy_user_requirements.append(r)
legacy_user_requirements_interaction_events.append(interaction_events)
legacy_user_requirements_trd = trd
return use_case_actors, legacy_user_requirements_trd
async def _write_framework(context: Context, use_case_actors: str, trd: str, acknowledge: str, constraint: str) -> str:
write_framework = WriteFramework(context=context)
evaluate_framework = EvaluateFramework(context=context)
is_pass = False
framework = ""
evaluation_conclusion = ""
while not is_pass and (context.cost_manager.total_cost < context.cost_manager.max_budget):
try:
framework = await write_framework.run(
use_case_actors=use_case_actors,
trd=trd,
acknowledge=acknowledge,
legacy_output=framework,
evaluation_conclusion=evaluation_conclusion,
additional_technical_requirements=constraint,
)
except Exception as e:
logger.info(f"{e}")
break
evaluation = await evaluate_framework.run(
use_case_actors=use_case_actors,
trd=trd,
acknowledge=acknowledge,
legacy_output=framework,
additional_technical_requirements=constraint,
)
is_pass = evaluation.is_pass
evaluation_conclusion = evaluation.conclusion
return framework
async def develop(
context: Context,
user_requirement_filename: str,
actors_filename: str,
acknowledge_filename: str,
constraint_filename: str,
output_dir: str,
):
output_dir = Path(output_dir) if output_dir else DEFAULT_WORKSPACE_ROOT / uuid.uuid4().hex
v = await aread(filename=user_requirement_filename)
user_requirements = json.loads(v)
v = await aread(filename=actors_filename)
actors = json.loads(v)
acknowledge = await aread(filename=acknowledge_filename)
technical_constraint = await aread(filename=constraint_filename)
# Compress acknowledge
compress_acknowledge = CompressExternalInterfaces(context=context)
available_external_interfaces = await compress_acknowledge.run(acknowledge=acknowledge)
# Write TRD
use_case_actors, trd = await _write_trd(
context=context,
actors=actors,
user_requirements=user_requirements,
available_external_interfaces=available_external_interfaces,
)
# Write framework
framework = await _write_framework(
context=context,
use_case_actors=use_case_actors,
trd=trd,
acknowledge=acknowledge,
constraint=technical_constraint,
)
# Save
file_list = await save_framework(dir_data=framework, trd=trd, output_dir=output_dir)
logger.info(f"Output:\n{file_list}")
@app.command()
def startup(
user_requirement_filename: str = typer.Argument(..., help="The filename of the user requirements."),
actors_filename: str = typer.Argument(..., help="The filename of UML use case actors description."),
acknowledge_filename: str = typer.Argument(..., help="External interfaces declarations."),
llm_config: str = typer.Option(default="", help="Low-cost LLM config"),
constraint_filename: str = typer.Option(default="", help="What technical dependency constraints are."),
output_dir: str = typer.Option(default="", help="Output directory."),
investment: float = typer.Option(default=15.0, help="Dollar amount to invest in the AI company."),
):
if llm_config and Path(llm_config).exists():
config = Config.from_yaml_file(Path(llm_config))
else:
logger.info("GPT 4 turbo is recommended")
config = Config.default()
ctx = Context(config=config)
ctx.cost_manager.max_budget = investment
asyncio.run(
develop(ctx, user_requirement_filename, actors_filename, acknowledge_filename, constraint_filename, output_dir)
)
if __name__ == "__main__":
app()