mirror of
https://github.com/FoundationAgents/MetaGPT.git
synced 2026-07-14 16:32:16 +02:00
Merge branch 'main' into code_interpreter
This commit is contained in:
commit
38f21137ec
146 changed files with 4466 additions and 1375 deletions
|
|
@ -22,9 +22,9 @@ from metagpt.actions.write_code_review import WriteCodeReview
|
|||
from metagpt.actions.write_prd import WritePRD
|
||||
from metagpt.actions.write_prd_review import WritePRDReview
|
||||
from metagpt.actions.write_test import WriteTest
|
||||
from metagpt.actions.mi.execute_nb_code import ExecuteNbCode
|
||||
from metagpt.actions.mi.write_analysis_code import WriteCodeWithTools
|
||||
from metagpt.actions.mi.write_plan import WritePlan
|
||||
from metagpt.actions.di.execute_nb_code import ExecuteNbCode
|
||||
from metagpt.actions.di.write_analysis_code import WriteCodeWithTools
|
||||
from metagpt.actions.di.write_plan import WritePlan
|
||||
|
||||
|
||||
class ActionType(Enum):
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
from typing import List
|
||||
|
||||
from metagpt.actions.action_node import ActionNode
|
||||
from metagpt.logs import logger
|
||||
from metagpt.utils.mermaid import MMC1, MMC2
|
||||
|
||||
IMPLEMENTATION_APPROACH = ActionNode(
|
||||
|
|
@ -109,14 +108,3 @@ REFINED_NODES = [
|
|||
|
||||
DESIGN_API_NODE = ActionNode.from_children("DesignAPI", NODES)
|
||||
REFINED_DESIGN_NODE = ActionNode.from_children("RefinedDesignAPI", REFINED_NODES)
|
||||
|
||||
|
||||
def main():
|
||||
prompt = DESIGN_API_NODE.compile(context="")
|
||||
logger.info(prompt)
|
||||
prompt = REFINED_DESIGN_NODE.compile(context="")
|
||||
logger.info(prompt)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from __future__ import annotations
|
|||
import json
|
||||
|
||||
from metagpt.actions import Action
|
||||
from metagpt.prompts.mi.write_analysis_code import (
|
||||
from metagpt.prompts.di.write_analysis_code import (
|
||||
CHECK_DATA_PROMPT,
|
||||
DEBUG_REFLECTION_EXAMPLE,
|
||||
INTERPRETER_SYSTEM_MSG,
|
||||
|
|
@ -8,7 +8,6 @@
|
|||
from typing import List
|
||||
|
||||
from metagpt.actions.action_node import ActionNode
|
||||
from metagpt.logs import logger
|
||||
|
||||
REQUIRED_PYTHON_PACKAGES = ActionNode(
|
||||
key="Required Python packages",
|
||||
|
|
@ -119,14 +118,3 @@ REFINED_NODES = [
|
|||
|
||||
PM_NODE = ActionNode.from_children("PM_NODE", NODES)
|
||||
REFINED_PM_NODE = ActionNode.from_children("REFINED_PM_NODE", REFINED_NODES)
|
||||
|
||||
|
||||
def main():
|
||||
prompt = PM_NODE.compile(context="")
|
||||
logger.info(prompt)
|
||||
prompt = REFINED_PM_NODE.compile(context="")
|
||||
logger.info(prompt)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@
|
|||
@Time : 2023/12/19
|
||||
@Author : mashenquan
|
||||
@File : rebuild_class_view.py
|
||||
@Desc : Rebuild class view info
|
||||
@Desc : Reconstructs class diagram from a source code project.
|
||||
Implement RFC197, https://deepwisdom.feishu.cn/wiki/VyK0wfq56ivuvjklMKJcmHQknGt
|
||||
"""
|
||||
import re
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional, Set, Tuple
|
||||
|
||||
import aiofiles
|
||||
|
||||
|
|
@ -21,86 +23,144 @@ from metagpt.const import (
|
|||
GRAPH_REPO_FILE_REPO,
|
||||
)
|
||||
from metagpt.logs import logger
|
||||
from metagpt.repo_parser import RepoParser
|
||||
from metagpt.schema import ClassAttribute, ClassMethod, ClassView
|
||||
from metagpt.utils.common import split_namespace
|
||||
from metagpt.repo_parser import DotClassInfo, RepoParser
|
||||
from metagpt.schema import UMLClassView
|
||||
from metagpt.utils.common import concat_namespace, split_namespace
|
||||
from metagpt.utils.di_graph_repository import DiGraphRepository
|
||||
from metagpt.utils.graph_repository import GraphKeyword, GraphRepository
|
||||
|
||||
|
||||
class RebuildClassView(Action):
|
||||
"""
|
||||
Reconstructs a graph repository about class diagram from a source code project.
|
||||
|
||||
Attributes:
|
||||
graph_db (Optional[GraphRepository]): The optional graph repository.
|
||||
"""
|
||||
|
||||
graph_db: Optional[GraphRepository] = None
|
||||
|
||||
async def run(self, with_messages=None, format=config.prompt_schema):
|
||||
"""
|
||||
Implementation of `Action`'s `run` method.
|
||||
|
||||
Args:
|
||||
with_messages (Optional[Type]): An optional argument specifying messages to react to.
|
||||
format (str): The format for the prompt schema.
|
||||
"""
|
||||
graph_repo_pathname = self.context.git_repo.workdir / GRAPH_REPO_FILE_REPO / self.context.git_repo.workdir.name
|
||||
graph_db = await DiGraphRepository.load_from(str(graph_repo_pathname.with_suffix(".json")))
|
||||
self.graph_db = await DiGraphRepository.load_from(str(graph_repo_pathname.with_suffix(".json")))
|
||||
repo_parser = RepoParser(base_directory=Path(self.i_context))
|
||||
# use pylint
|
||||
class_views, relationship_views, package_root = await repo_parser.rebuild_class_views(path=Path(self.i_context))
|
||||
await GraphRepository.update_graph_db_with_class_views(graph_db, class_views)
|
||||
await GraphRepository.update_graph_db_with_class_relationship_views(graph_db, relationship_views)
|
||||
await GraphRepository.update_graph_db_with_class_views(self.graph_db, class_views)
|
||||
await GraphRepository.update_graph_db_with_class_relationship_views(self.graph_db, relationship_views)
|
||||
await GraphRepository.rebuild_composition_relationship(self.graph_db)
|
||||
# use ast
|
||||
direction, diff_path = self._diff_path(path_root=Path(self.i_context).resolve(), package_root=package_root)
|
||||
symbols = repo_parser.generate_symbols()
|
||||
for file_info in symbols:
|
||||
# Align to the same root directory in accordance with `class_views`.
|
||||
file_info.file = self._align_root(file_info.file, direction, diff_path)
|
||||
await GraphRepository.update_graph_db_with_file_info(graph_db, file_info)
|
||||
await self._create_mermaid_class_views(graph_db=graph_db)
|
||||
await graph_db.save()
|
||||
await GraphRepository.update_graph_db_with_file_info(self.graph_db, file_info)
|
||||
await self._create_mermaid_class_views()
|
||||
await self.graph_db.save()
|
||||
|
||||
async def _create_mermaid_class_views(self, graph_db):
|
||||
path = Path(self.context.git_repo.workdir) / DATA_API_DESIGN_FILE_REPO
|
||||
async def _create_mermaid_class_views(self) -> str:
|
||||
"""Creates a Mermaid class diagram using data from the `graph_db` graph repository.
|
||||
|
||||
This method utilizes information stored in the graph repository to generate a Mermaid class diagram.
|
||||
Returns:
|
||||
mermaid class diagram file name.
|
||||
"""
|
||||
path = self.context.git_repo.workdir / DATA_API_DESIGN_FILE_REPO
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
pathname = path / self.context.git_repo.workdir.name
|
||||
async with aiofiles.open(str(pathname.with_suffix(".mmd")), mode="w", encoding="utf-8") as writer:
|
||||
filename = str(pathname.with_suffix(".class_diagram.mmd"))
|
||||
async with aiofiles.open(filename, mode="w", encoding="utf-8") as writer:
|
||||
content = "classDiagram\n"
|
||||
logger.debug(content)
|
||||
await writer.write(content)
|
||||
# class names
|
||||
rows = await graph_db.select(predicate=GraphKeyword.IS, object_=GraphKeyword.CLASS)
|
||||
rows = await self.graph_db.select(predicate=GraphKeyword.IS, object_=GraphKeyword.CLASS)
|
||||
class_distinct = set()
|
||||
relationship_distinct = set()
|
||||
for r in rows:
|
||||
await RebuildClassView._create_mermaid_class(r.subject, graph_db, writer, class_distinct)
|
||||
content = await self._create_mermaid_class(r.subject)
|
||||
if content:
|
||||
await writer.write(content)
|
||||
class_distinct.add(r.subject)
|
||||
for r in rows:
|
||||
await RebuildClassView._create_mermaid_relationship(r.subject, graph_db, writer, relationship_distinct)
|
||||
content, distinct = await self._create_mermaid_relationship(r.subject)
|
||||
if content:
|
||||
logger.debug(content)
|
||||
await writer.write(content)
|
||||
relationship_distinct.update(distinct)
|
||||
logger.info(f"classes: {len(class_distinct)}, relationship: {len(relationship_distinct)}")
|
||||
|
||||
@staticmethod
|
||||
async def _create_mermaid_class(ns_class_name, graph_db, file_writer, distinct):
|
||||
if self.i_context:
|
||||
r_filename = Path(filename).relative_to(self.context.git_repo.workdir)
|
||||
await self.graph_db.insert(
|
||||
subject=self.i_context, predicate="hasMermaidClassDiagramFile", object_=str(r_filename)
|
||||
)
|
||||
logger.info(f"{self.i_context} hasMermaidClassDiagramFile {filename}")
|
||||
return filename
|
||||
|
||||
async def _create_mermaid_class(self, ns_class_name) -> str:
|
||||
"""Generates a Mermaid class diagram for a specific class using data from the `graph_db` graph repository.
|
||||
|
||||
Args:
|
||||
ns_class_name (str): The namespace-prefixed name of the class for which the Mermaid class diagram is to be created.
|
||||
|
||||
Returns:
|
||||
str: A Mermaid code block object in markdown representing the class diagram.
|
||||
"""
|
||||
fields = split_namespace(ns_class_name)
|
||||
if len(fields) > 2:
|
||||
# Ignore sub-class
|
||||
return
|
||||
return ""
|
||||
|
||||
class_view = ClassView(name=fields[1])
|
||||
rows = await graph_db.select(subject=ns_class_name)
|
||||
for r in rows:
|
||||
name = split_namespace(r.object_)[-1]
|
||||
name, visibility, abstraction = RebuildClassView._parse_name(name=name, language="python")
|
||||
if r.predicate == GraphKeyword.HAS_CLASS_PROPERTY:
|
||||
var_type = await RebuildClassView._parse_variable_type(r.object_, graph_db)
|
||||
attribute = ClassAttribute(
|
||||
name=name, visibility=visibility, abstraction=bool(abstraction), value_type=var_type
|
||||
)
|
||||
class_view.attributes.append(attribute)
|
||||
elif r.predicate == GraphKeyword.HAS_CLASS_FUNCTION:
|
||||
method = ClassMethod(name=name, visibility=visibility, abstraction=bool(abstraction))
|
||||
await RebuildClassView._parse_function_args(method, r.object_, graph_db)
|
||||
class_view.methods.append(method)
|
||||
rows = await self.graph_db.select(subject=ns_class_name, predicate=GraphKeyword.HAS_DETAIL)
|
||||
if not rows:
|
||||
return ""
|
||||
dot_class_info = DotClassInfo.model_validate_json(rows[0].object_)
|
||||
class_view = UMLClassView.load_dot_class_info(dot_class_info)
|
||||
|
||||
# update graph db
|
||||
await graph_db.insert(ns_class_name, GraphKeyword.HAS_CLASS_VIEW, class_view.model_dump_json())
|
||||
# update uml view
|
||||
await self.graph_db.insert(ns_class_name, GraphKeyword.HAS_CLASS_VIEW, class_view.model_dump_json())
|
||||
# update uml isCompositeOf
|
||||
for c in dot_class_info.compositions:
|
||||
await self.graph_db.insert(
|
||||
subject=ns_class_name,
|
||||
predicate=GraphKeyword.IS + COMPOSITION + GraphKeyword.OF,
|
||||
object_=concat_namespace("?", c),
|
||||
)
|
||||
|
||||
# update uml isAggregateOf
|
||||
for a in dot_class_info.aggregations:
|
||||
await self.graph_db.insert(
|
||||
subject=ns_class_name,
|
||||
predicate=GraphKeyword.IS + AGGREGATION + GraphKeyword.OF,
|
||||
object_=concat_namespace("?", a),
|
||||
)
|
||||
|
||||
content = class_view.get_mermaid(align=1)
|
||||
logger.debug(content)
|
||||
await file_writer.write(content)
|
||||
distinct.add(ns_class_name)
|
||||
return content
|
||||
|
||||
@staticmethod
|
||||
async def _create_mermaid_relationship(ns_class_name, graph_db, file_writer, distinct):
|
||||
async def _create_mermaid_relationship(self, ns_class_name: str) -> Tuple[Optional[str], Optional[Set]]:
|
||||
"""Generates a Mermaid class relationship diagram for a specific class using data from the `graph_db` graph repository.
|
||||
|
||||
Args:
|
||||
ns_class_name (str): The namespace-prefixed class name for which the Mermaid relationship diagram is to be created.
|
||||
|
||||
Returns:
|
||||
Tuple[str, Set]: A tuple containing the relationship diagram as a string and a set of deduplication.
|
||||
"""
|
||||
s_fields = split_namespace(ns_class_name)
|
||||
if len(s_fields) > 2:
|
||||
# Ignore sub-class
|
||||
return
|
||||
return None, None
|
||||
|
||||
predicates = {GraphKeyword.IS + v + GraphKeyword.OF: v for v in [GENERALIZATION, COMPOSITION, AGGREGATION]}
|
||||
mappings = {
|
||||
|
|
@ -109,8 +169,9 @@ class RebuildClassView(Action):
|
|||
AGGREGATION: " o-- ",
|
||||
}
|
||||
content = ""
|
||||
distinct = set()
|
||||
for p, v in predicates.items():
|
||||
rows = await graph_db.select(subject=ns_class_name, predicate=p)
|
||||
rows = await self.graph_db.select(subject=ns_class_name, predicate=p)
|
||||
for r in rows:
|
||||
o_fields = split_namespace(r.object_)
|
||||
if len(o_fields) > 2:
|
||||
|
|
@ -121,86 +182,26 @@ class RebuildClassView(Action):
|
|||
distinct.add(link)
|
||||
content += f"\t{link}\n"
|
||||
|
||||
if content:
|
||||
logger.debug(content)
|
||||
await file_writer.write(content)
|
||||
|
||||
@staticmethod
|
||||
def _parse_name(name: str, language="python"):
|
||||
pattern = re.compile(r"<I>(.*?)<\/I>")
|
||||
result = re.search(pattern, name)
|
||||
|
||||
abstraction = ""
|
||||
if result:
|
||||
name = result.group(1)
|
||||
abstraction = "*"
|
||||
if name.startswith("__"):
|
||||
visibility = "-"
|
||||
elif name.startswith("_"):
|
||||
visibility = "#"
|
||||
else:
|
||||
visibility = "+"
|
||||
return name, visibility, abstraction
|
||||
|
||||
@staticmethod
|
||||
async def _parse_variable_type(ns_name, graph_db) -> str:
|
||||
rows = await graph_db.select(subject=ns_name, predicate=GraphKeyword.HAS_TYPE_DESC)
|
||||
if not rows:
|
||||
return ""
|
||||
vals = rows[0].object_.replace("'", "").split(":")
|
||||
if len(vals) == 1:
|
||||
return ""
|
||||
val = vals[-1].strip()
|
||||
return "" if val == "NoneType" else val + " "
|
||||
|
||||
@staticmethod
|
||||
async def _parse_function_args(method: ClassMethod, ns_name: str, graph_db: GraphRepository):
|
||||
rows = await graph_db.select(subject=ns_name, predicate=GraphKeyword.HAS_ARGS_DESC)
|
||||
if not rows:
|
||||
return
|
||||
info = rows[0].object_.replace("'", "")
|
||||
|
||||
fs_tag = "("
|
||||
ix = info.find(fs_tag)
|
||||
fe_tag = "):"
|
||||
eix = info.rfind(fe_tag)
|
||||
if eix < 0:
|
||||
fe_tag = ")"
|
||||
eix = info.rfind(fe_tag)
|
||||
args_info = info[ix + len(fs_tag) : eix].strip()
|
||||
method.return_type = info[eix + len(fe_tag) :].strip()
|
||||
if method.return_type == "None":
|
||||
method.return_type = ""
|
||||
if "(" in method.return_type:
|
||||
method.return_type = method.return_type.replace("(", "Tuple[").replace(")", "]")
|
||||
|
||||
# parse args
|
||||
if not args_info:
|
||||
return
|
||||
splitter_ixs = []
|
||||
cost = 0
|
||||
for i in range(len(args_info)):
|
||||
if args_info[i] == "[":
|
||||
cost += 1
|
||||
elif args_info[i] == "]":
|
||||
cost -= 1
|
||||
if args_info[i] == "," and cost == 0:
|
||||
splitter_ixs.append(i)
|
||||
splitter_ixs.append(len(args_info))
|
||||
args = []
|
||||
ix = 0
|
||||
for eix in splitter_ixs:
|
||||
args.append(args_info[ix:eix])
|
||||
ix = eix + 1
|
||||
for arg in args:
|
||||
parts = arg.strip().split(":")
|
||||
if len(parts) == 1:
|
||||
method.args.append(ClassAttribute(name=parts[0].strip()))
|
||||
continue
|
||||
method.args.append(ClassAttribute(name=parts[0].strip(), value_type=parts[-1].strip()))
|
||||
return content, distinct
|
||||
|
||||
@staticmethod
|
||||
def _diff_path(path_root: Path, package_root: Path) -> (str, str):
|
||||
"""Returns the difference between the root path and the path information represented in the package name.
|
||||
|
||||
Args:
|
||||
path_root (Path): The root path.
|
||||
package_root (Path): The package root path.
|
||||
|
||||
Returns:
|
||||
Tuple[str, str]: A tuple containing the representation of the difference ("+", "-", "=") and the path detail of the differing part.
|
||||
|
||||
Example:
|
||||
>>> _diff_path(path_root=Path("/Users/x/github/MetaGPT"), package_root=Path("/Users/x/github/MetaGPT/metagpt"))
|
||||
"-", "metagpt"
|
||||
|
||||
>>> _diff_path(path_root=Path("/Users/x/github/MetaGPT/metagpt"), package_root=Path("/Users/x/github/MetaGPT/metagpt"))
|
||||
"=", "."
|
||||
"""
|
||||
if len(str(path_root)) > len(str(package_root)):
|
||||
return "+", str(path_root.relative_to(package_root))
|
||||
if len(str(path_root)) < len(str(package_root)):
|
||||
|
|
@ -208,7 +209,24 @@ class RebuildClassView(Action):
|
|||
return "=", "."
|
||||
|
||||
@staticmethod
|
||||
def _align_root(path: str, direction: str, diff_path: str):
|
||||
def _align_root(path: str, direction: str, diff_path: str) -> str:
|
||||
"""Aligns the path to the same root represented by `diff_path`.
|
||||
|
||||
Args:
|
||||
path (str): The path to be aligned.
|
||||
direction (str): The direction of alignment ('+', '-', '=').
|
||||
diff_path (str): The path representing the difference.
|
||||
|
||||
Returns:
|
||||
str: The aligned path.
|
||||
|
||||
Example:
|
||||
>>> _align_root(path="metagpt/software_company.py", direction="+", diff_path="MetaGPT")
|
||||
"MetaGPT/metagpt/software_company.py"
|
||||
|
||||
>>> _align_root(path="metagpt/software_company.py", direction="-", diff_path="metagpt")
|
||||
"software_company.py"
|
||||
"""
|
||||
if direction == "=":
|
||||
return path
|
||||
if direction == "+":
|
||||
|
|
|
|||
|
|
@ -4,34 +4,214 @@
|
|||
@Time : 2024/1/4
|
||||
@Author : mashenquan
|
||||
@File : rebuild_sequence_view.py
|
||||
@Desc : Rebuild sequence view info
|
||||
@Desc : Reconstruct sequence view information through reverse engineering.
|
||||
Implement RFC197, https://deepwisdom.feishu.cn/wiki/VyK0wfq56ivuvjklMKJcmHQknGt
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
from typing import List, Optional, Set
|
||||
|
||||
from pydantic import BaseModel
|
||||
from tenacity import retry, stop_after_attempt, wait_random_exponential
|
||||
|
||||
from metagpt.actions import Action
|
||||
from metagpt.config2 import config
|
||||
from metagpt.const import GRAPH_REPO_FILE_REPO
|
||||
from metagpt.logs import logger
|
||||
from metagpt.utils.common import aread, list_files
|
||||
from metagpt.repo_parser import CodeBlockInfo, DotClassInfo
|
||||
from metagpt.schema import UMLClassView
|
||||
from metagpt.utils.common import (
|
||||
add_affix,
|
||||
aread,
|
||||
auto_namespace,
|
||||
concat_namespace,
|
||||
general_after_log,
|
||||
list_files,
|
||||
parse_json_code_block,
|
||||
read_file_block,
|
||||
split_namespace,
|
||||
)
|
||||
from metagpt.utils.di_graph_repository import DiGraphRepository
|
||||
from metagpt.utils.graph_repository import GraphKeyword
|
||||
from metagpt.utils.graph_repository import SPO, GraphKeyword, GraphRepository
|
||||
|
||||
|
||||
class ReverseUseCase(BaseModel):
|
||||
"""
|
||||
Represents a reverse engineered use case.
|
||||
|
||||
Attributes:
|
||||
description (str): A description of the reverse use case.
|
||||
inputs (List[str]): List of inputs for the reverse use case.
|
||||
outputs (List[str]): List of outputs for the reverse use case.
|
||||
actors (List[str]): List of actors involved in the reverse use case.
|
||||
steps (List[str]): List of steps for the reverse use case.
|
||||
reason (str): The reason behind the reverse use case.
|
||||
"""
|
||||
|
||||
description: str
|
||||
inputs: List[str]
|
||||
outputs: List[str]
|
||||
actors: List[str]
|
||||
steps: List[str]
|
||||
reason: str
|
||||
|
||||
|
||||
class ReverseUseCaseDetails(BaseModel):
|
||||
"""
|
||||
Represents details of a reverse engineered use case.
|
||||
|
||||
Attributes:
|
||||
description (str): A description of the reverse use case details.
|
||||
use_cases (List[ReverseUseCase]): List of reverse use cases.
|
||||
relationship (List[str]): List of relationships associated with the reverse use case details.
|
||||
"""
|
||||
|
||||
description: str
|
||||
use_cases: List[ReverseUseCase]
|
||||
relationship: List[str]
|
||||
|
||||
|
||||
class RebuildSequenceView(Action):
|
||||
async def run(self, with_messages=None, format=config.prompt_schema):
|
||||
graph_repo_pathname = self.context.git_repo.workdir / GRAPH_REPO_FILE_REPO / self.context.git_repo.workdir.name
|
||||
graph_db = await DiGraphRepository.load_from(str(graph_repo_pathname.with_suffix(".json")))
|
||||
entries = await RebuildSequenceView._search_main_entry(graph_db)
|
||||
for entry in entries:
|
||||
await self._rebuild_sequence_view(entry, graph_db)
|
||||
await graph_db.save()
|
||||
"""
|
||||
Represents an action to reconstruct sequence view through reverse engineering.
|
||||
|
||||
@staticmethod
|
||||
async def _search_main_entry(graph_db) -> List:
|
||||
rows = await graph_db.select(predicate=GraphKeyword.HAS_PAGE_INFO)
|
||||
Attributes:
|
||||
graph_db (Optional[GraphRepository]): An optional instance of GraphRepository for graph database operations.
|
||||
"""
|
||||
|
||||
graph_db: Optional[GraphRepository] = None
|
||||
|
||||
async def run(self, with_messages=None, format=config.prompt_schema):
|
||||
"""
|
||||
Implementation of `Action`'s `run` method.
|
||||
|
||||
Args:
|
||||
with_messages (Optional[Type]): An optional argument specifying messages to react to.
|
||||
format (str): The format for the prompt schema.
|
||||
"""
|
||||
graph_repo_pathname = self.context.git_repo.workdir / GRAPH_REPO_FILE_REPO / self.context.git_repo.workdir.name
|
||||
self.graph_db = await DiGraphRepository.load_from(str(graph_repo_pathname.with_suffix(".json")))
|
||||
if not self.i_context:
|
||||
entries = await self._search_main_entry()
|
||||
else:
|
||||
entries = [SPO(subject=self.i_context, predicate="", object_="")]
|
||||
for entry in entries:
|
||||
await self._rebuild_main_sequence_view(entry)
|
||||
while await self._merge_sequence_view(entry):
|
||||
pass
|
||||
await self.graph_db.save()
|
||||
|
||||
@retry(
|
||||
wait=wait_random_exponential(min=1, max=20),
|
||||
stop=stop_after_attempt(6),
|
||||
after=general_after_log(logger),
|
||||
)
|
||||
async def _rebuild_main_sequence_view(self, entry: SPO):
|
||||
"""
|
||||
Reconstruct the sequence diagram for the __main__ entry of the source code through reverse engineering.
|
||||
|
||||
Args:
|
||||
entry (SPO): The SPO (Subject, Predicate, Object) object in the graph database that is related to the
|
||||
subject `__name__:__main__`.
|
||||
"""
|
||||
filename = entry.subject.split(":", 1)[0]
|
||||
rows = await self.graph_db.select(predicate=GraphKeyword.IS, object_=GraphKeyword.CLASS)
|
||||
classes = []
|
||||
prefix = filename + ":"
|
||||
for r in rows:
|
||||
if prefix in r.subject:
|
||||
classes.append(r)
|
||||
await self._rebuild_use_case(r.subject)
|
||||
participants = await self._search_participants(split_namespace(entry.subject)[0])
|
||||
class_details = []
|
||||
class_views = []
|
||||
for c in classes:
|
||||
detail = await self._get_class_detail(c.subject)
|
||||
if not detail:
|
||||
continue
|
||||
class_details.append(detail)
|
||||
view = await self._get_uml_class_view(c.subject)
|
||||
if view:
|
||||
class_views.append(view)
|
||||
|
||||
actors = await self._get_participants(c.subject)
|
||||
participants.update(set(actors))
|
||||
|
||||
use_case_blocks = []
|
||||
for c in classes:
|
||||
use_cases = await self._get_class_use_cases(c.subject)
|
||||
use_case_blocks.append(use_cases)
|
||||
prompt_blocks = ["## Use Cases\n" + "\n".join(use_case_blocks)]
|
||||
block = "## Participants\n"
|
||||
for p in participants:
|
||||
block += f"- {p}\n"
|
||||
prompt_blocks.append(block)
|
||||
block = "## Mermaid Class Views\n```mermaid\n"
|
||||
block += "\n\n".join([c.get_mermaid() for c in class_views])
|
||||
block += "\n```\n"
|
||||
prompt_blocks.append(block)
|
||||
block = "## Source Code\n```python\n"
|
||||
block += await self._get_source_code(filename)
|
||||
block += "\n```\n"
|
||||
prompt_blocks.append(block)
|
||||
prompt = "\n---\n".join(prompt_blocks)
|
||||
|
||||
rsp = await self.llm.aask(
|
||||
msg=prompt,
|
||||
system_msgs=[
|
||||
"You are a python code to Mermaid Sequence Diagram translator in function detail.",
|
||||
"Translate the given markdown text to a Mermaid Sequence Diagram.",
|
||||
"Return the merged Mermaid sequence diagram in a markdown code block format.",
|
||||
],
|
||||
stream=False,
|
||||
)
|
||||
sequence_view = rsp.removeprefix("```mermaid").removesuffix("```")
|
||||
rows = await self.graph_db.select(subject=entry.subject, predicate=GraphKeyword.HAS_SEQUENCE_VIEW)
|
||||
for r in rows:
|
||||
if r.predicate == GraphKeyword.HAS_SEQUENCE_VIEW:
|
||||
await self.graph_db.delete(subject=r.subject, predicate=r.predicate, object_=r.object_)
|
||||
await self.graph_db.insert(
|
||||
subject=entry.subject, predicate=GraphKeyword.HAS_SEQUENCE_VIEW, object_=sequence_view
|
||||
)
|
||||
await self.graph_db.insert(
|
||||
subject=entry.subject,
|
||||
predicate=GraphKeyword.HAS_SEQUENCE_VIEW_VER,
|
||||
object_=concat_namespace(datetime.now().strftime("%Y%m%d%H%M%S%f")[:-3], add_affix(sequence_view)),
|
||||
)
|
||||
for c in classes:
|
||||
await self.graph_db.insert(
|
||||
subject=entry.subject, predicate=GraphKeyword.HAS_PARTICIPANT, object_=auto_namespace(c.subject)
|
||||
)
|
||||
await self._save_sequence_view(subject=entry.subject, content=sequence_view)
|
||||
|
||||
async def _merge_sequence_view(self, entry: SPO) -> bool:
|
||||
"""
|
||||
Augments additional information to the provided SPO (Subject, Predicate, Object) entry in the sequence diagram.
|
||||
|
||||
Args:
|
||||
entry (SPO): The SPO object representing the relationship in the graph database.
|
||||
|
||||
Returns:
|
||||
bool: True if additional information has been augmented, otherwise False.
|
||||
"""
|
||||
new_participant = await self._search_new_participant(entry)
|
||||
if not new_participant:
|
||||
return False
|
||||
|
||||
await self._merge_participant(entry, new_participant)
|
||||
return True
|
||||
|
||||
async def _search_main_entry(self) -> List:
|
||||
"""
|
||||
Asynchronously searches for the SPO object that is related to `__name__:__main__`.
|
||||
|
||||
Returns:
|
||||
List: A list containing information about the main entry in the sequence diagram.
|
||||
"""
|
||||
rows = await self.graph_db.select(predicate=GraphKeyword.HAS_PAGE_INFO)
|
||||
tag = "__name__:__main__"
|
||||
entries = []
|
||||
for r in rows:
|
||||
|
|
@ -39,24 +219,395 @@ class RebuildSequenceView(Action):
|
|||
entries.append(r)
|
||||
return entries
|
||||
|
||||
async def _rebuild_sequence_view(self, entry, graph_db):
|
||||
filename = entry.subject.split(":", 1)[0]
|
||||
src_filename = RebuildSequenceView._get_full_filename(root=self.i_context, pathname=filename)
|
||||
if not src_filename:
|
||||
@retry(
|
||||
wait=wait_random_exponential(min=1, max=20),
|
||||
stop=stop_after_attempt(6),
|
||||
after=general_after_log(logger),
|
||||
)
|
||||
async def _rebuild_use_case(self, ns_class_name: str):
|
||||
"""
|
||||
Asynchronously reconstructs the use case for the provided namespace-prefixed class name.
|
||||
|
||||
Args:
|
||||
ns_class_name (str): The namespace-prefixed class name for which the use case is to be reconstructed.
|
||||
"""
|
||||
rows = await self.graph_db.select(subject=ns_class_name, predicate=GraphKeyword.HAS_CLASS_USE_CASE)
|
||||
if rows:
|
||||
return
|
||||
content = await aread(filename=src_filename, encoding="utf-8")
|
||||
content = f"```python\n{content}\n```\n\n---\nTranslate the code above into Mermaid Sequence Diagram."
|
||||
data = await self.llm.aask(
|
||||
msg=content, system_msgs=["You are a python code to Mermaid Sequence Diagram translator in function detail"]
|
||||
|
||||
detail = await self._get_class_detail(ns_class_name)
|
||||
if not detail:
|
||||
return
|
||||
participants = set()
|
||||
participants.update(set(detail.compositions))
|
||||
participants.update(set(detail.aggregations))
|
||||
class_view = await self._get_uml_class_view(ns_class_name)
|
||||
source_code = await self._get_source_code(ns_class_name)
|
||||
|
||||
# prompt_blocks = [
|
||||
# "## Instruction\n"
|
||||
# "You are a python code to UML 2.0 Use Case translator.\n"
|
||||
# 'The generated UML 2.0 Use Case must include the roles or entities listed in "Participants".\n'
|
||||
# "The functional descriptions of Actors and Use Cases in the generated UML 2.0 Use Case must not "
|
||||
# 'conflict with the information in "Mermaid Class Views".\n'
|
||||
# 'The section under `if __name__ == "__main__":` of "Source Code" contains information about external '
|
||||
# "system interactions with the internal system.\n"
|
||||
# ]
|
||||
prompt_blocks = []
|
||||
block = "## Participants\n"
|
||||
for p in participants:
|
||||
block += f"- {p}\n"
|
||||
prompt_blocks.append(block)
|
||||
block = "## Mermaid Class Views\n```mermaid\n"
|
||||
block += class_view.get_mermaid()
|
||||
block += "\n```\n"
|
||||
prompt_blocks.append(block)
|
||||
block = "## Source Code\n```python\n"
|
||||
block += source_code
|
||||
block += "\n```\n"
|
||||
prompt_blocks.append(block)
|
||||
prompt = "\n---\n".join(prompt_blocks)
|
||||
|
||||
rsp = await self.llm.aask(
|
||||
msg=prompt,
|
||||
system_msgs=[
|
||||
"You are a python code to UML 2.0 Use Case translator.",
|
||||
'The generated UML 2.0 Use Case must include the roles or entities listed in "Participants".',
|
||||
"The functional descriptions of Actors and Use Cases in the generated UML 2.0 Use Case must not "
|
||||
'conflict with the information in "Mermaid Class Views".',
|
||||
'The section under `if __name__ == "__main__":` of "Source Code" contains information about external '
|
||||
"system interactions with the internal system.",
|
||||
"Return a markdown JSON object with:\n"
|
||||
'- a "description" key to explain what the whole source code want to do;\n'
|
||||
'- a "use_cases" key list all use cases, each use case in the list should including a `description` '
|
||||
"key describes about what the use case to do, a `inputs` key lists the input names of the use case "
|
||||
"from external sources, a `outputs` key lists the output names of the use case to external sources, "
|
||||
"a `actors` key lists the participant actors of the use case, a `steps` key lists the steps about how "
|
||||
"the use case works step by step, a `reason` key explaining under what circumstances would the "
|
||||
"external system execute this use case.\n"
|
||||
'- a "relationship" key lists all the descriptions of relationship among these use cases.\n',
|
||||
],
|
||||
stream=False,
|
||||
)
|
||||
|
||||
code_blocks = parse_json_code_block(rsp)
|
||||
for block in code_blocks:
|
||||
detail = ReverseUseCaseDetails.model_validate_json(block)
|
||||
await self.graph_db.insert(
|
||||
subject=ns_class_name, predicate=GraphKeyword.HAS_CLASS_USE_CASE, object_=detail.model_dump_json()
|
||||
)
|
||||
|
||||
@retry(
|
||||
wait=wait_random_exponential(min=1, max=20),
|
||||
stop=stop_after_attempt(6),
|
||||
after=general_after_log(logger),
|
||||
)
|
||||
async def _rebuild_sequence_view(self, ns_class_name: str):
|
||||
"""
|
||||
Asynchronously reconstructs the sequence diagram for the provided namespace-prefixed class name.
|
||||
|
||||
Args:
|
||||
ns_class_name (str): The namespace-prefixed class name for which the sequence diagram is to be reconstructed.
|
||||
"""
|
||||
await self._rebuild_use_case(ns_class_name)
|
||||
|
||||
prompts_blocks = []
|
||||
use_case_markdown = await self._get_class_use_cases(ns_class_name)
|
||||
if not use_case_markdown: # external class
|
||||
await self.graph_db.insert(subject=ns_class_name, predicate=GraphKeyword.HAS_SEQUENCE_VIEW, object_="")
|
||||
return
|
||||
block = f"## Use Cases\n{use_case_markdown}"
|
||||
prompts_blocks.append(block)
|
||||
|
||||
participants = await self._get_participants(ns_class_name)
|
||||
block = "## Participants\n" + "\n".join([f"- {s}" for s in participants])
|
||||
prompts_blocks.append(block)
|
||||
|
||||
view = await self._get_uml_class_view(ns_class_name)
|
||||
block = "## Mermaid Class Views\n```mermaid\n"
|
||||
block += view.get_mermaid()
|
||||
block += "\n```\n"
|
||||
prompts_blocks.append(block)
|
||||
|
||||
block = "## Source Code\n```python\n"
|
||||
block += await self._get_source_code(ns_class_name)
|
||||
block += "\n```\n"
|
||||
prompts_blocks.append(block)
|
||||
prompt = "\n---\n".join(prompts_blocks)
|
||||
|
||||
rsp = await self.llm.aask(
|
||||
prompt,
|
||||
system_msgs=[
|
||||
"You are a Mermaid Sequence Diagram translator in function detail.",
|
||||
"Translate the markdown text to a Mermaid Sequence Diagram.",
|
||||
"Return a markdown mermaid code block.",
|
||||
],
|
||||
stream=False,
|
||||
)
|
||||
|
||||
sequence_view = rsp.removeprefix("```mermaid").removesuffix("```")
|
||||
await self.graph_db.insert(
|
||||
subject=ns_class_name, predicate=GraphKeyword.HAS_SEQUENCE_VIEW, object_=sequence_view
|
||||
)
|
||||
|
||||
async def _get_participants(self, ns_class_name: str) -> List[str]:
|
||||
"""
|
||||
Asynchronously returns the participants list of the sequence diagram for the provided namespace-prefixed SPO
|
||||
object.
|
||||
|
||||
Args:
|
||||
ns_class_name (str): The namespace-prefixed class name for which to retrieve the participants list.
|
||||
|
||||
Returns:
|
||||
List[str]: A list of participants in the sequence diagram.
|
||||
"""
|
||||
participants = set()
|
||||
detail = await self._get_class_detail(ns_class_name)
|
||||
if not detail:
|
||||
return []
|
||||
participants.update(set(detail.compositions))
|
||||
participants.update(set(detail.aggregations))
|
||||
return list(participants)
|
||||
|
||||
async def _get_class_use_cases(self, ns_class_name: str) -> str:
|
||||
"""
|
||||
Asynchronously assembles the context about the use case information of the namespace-prefixed SPO object.
|
||||
|
||||
Args:
|
||||
ns_class_name (str): The namespace-prefixed class name for which to retrieve use case information.
|
||||
|
||||
Returns:
|
||||
str: A string containing the assembled context about the use case information.
|
||||
"""
|
||||
block = ""
|
||||
rows = await self.graph_db.select(subject=ns_class_name, predicate=GraphKeyword.HAS_CLASS_USE_CASE)
|
||||
for i, r in enumerate(rows):
|
||||
detail = ReverseUseCaseDetails.model_validate_json(r.object_)
|
||||
block += f"\n### {i + 1}. {detail.description}"
|
||||
for j, use_case in enumerate(detail.use_cases):
|
||||
block += f"\n#### {i + 1}.{j + 1}. {use_case.description}\n"
|
||||
block += "\n##### Inputs\n" + "\n".join([f"- {s}" for s in use_case.inputs])
|
||||
block += "\n##### Outputs\n" + "\n".join([f"- {s}" for s in use_case.outputs])
|
||||
block += "\n##### Actors\n" + "\n".join([f"- {s}" for s in use_case.actors])
|
||||
block += "\n##### Steps\n" + "\n".join([f"- {s}" for s in use_case.steps])
|
||||
block += "\n#### Use Case Relationship\n" + "\n".join([f"- {s}" for s in detail.relationship])
|
||||
return block + "\n"
|
||||
|
||||
async def _get_class_detail(self, ns_class_name: str) -> DotClassInfo | None:
|
||||
"""
|
||||
Asynchronously retrieves the dot format class details of the namespace-prefixed SPO object.
|
||||
|
||||
Args:
|
||||
ns_class_name (str): The namespace-prefixed class name for which to retrieve class details.
|
||||
|
||||
Returns:
|
||||
Union[DotClassInfo, None]: A DotClassInfo object representing the dot format class details,
|
||||
or None if the details are not available.
|
||||
"""
|
||||
rows = await self.graph_db.select(subject=ns_class_name, predicate=GraphKeyword.HAS_DETAIL)
|
||||
if not rows:
|
||||
return None
|
||||
dot_class_info = DotClassInfo.model_validate_json(rows[0].object_)
|
||||
return dot_class_info
|
||||
|
||||
async def _get_uml_class_view(self, ns_class_name: str) -> UMLClassView | None:
|
||||
"""
|
||||
Asynchronously retrieves the UML 2.0 format class details of the namespace-prefixed SPO object.
|
||||
|
||||
Args:
|
||||
ns_class_name (str): The namespace-prefixed class name for which to retrieve UML class details.
|
||||
|
||||
Returns:
|
||||
Union[UMLClassView, None]: A UMLClassView object representing the UML 2.0 format class details,
|
||||
or None if the details are not available.
|
||||
"""
|
||||
rows = await self.graph_db.select(subject=ns_class_name, predicate=GraphKeyword.HAS_CLASS_VIEW)
|
||||
if not rows:
|
||||
return None
|
||||
class_view = UMLClassView.model_validate_json(rows[0].object_)
|
||||
return class_view
|
||||
|
||||
async def _get_source_code(self, ns_class_name: str) -> str:
|
||||
"""
|
||||
Asynchronously retrieves the source code of the namespace-prefixed SPO object.
|
||||
|
||||
Args:
|
||||
ns_class_name (str): The namespace-prefixed class name for which to retrieve the source code.
|
||||
|
||||
Returns:
|
||||
str: A string containing the source code of the specified namespace-prefixed class.
|
||||
"""
|
||||
rows = await self.graph_db.select(subject=ns_class_name, predicate=GraphKeyword.HAS_PAGE_INFO)
|
||||
filename = split_namespace(ns_class_name=ns_class_name)[0]
|
||||
if not rows:
|
||||
src_filename = RebuildSequenceView._get_full_filename(root=self.i_context, pathname=filename)
|
||||
if not src_filename:
|
||||
return ""
|
||||
return await aread(filename=src_filename, encoding="utf-8")
|
||||
code_block_info = CodeBlockInfo.model_validate_json(rows[0].object_)
|
||||
return await read_file_block(
|
||||
filename=filename, lineno=code_block_info.lineno, end_lineno=code_block_info.end_lineno
|
||||
)
|
||||
await graph_db.insert(subject=filename, predicate=GraphKeyword.HAS_SEQUENCE_VIEW, object_=data)
|
||||
logger.info(data)
|
||||
|
||||
@staticmethod
|
||||
def _get_full_filename(root: str | Path, pathname: str | Path) -> Path | None:
|
||||
"""
|
||||
Convert package name to the full path of the module.
|
||||
|
||||
Args:
|
||||
root (Union[str, Path]): The root path or string representing the package.
|
||||
pathname (Union[str, Path]): The pathname or string representing the module.
|
||||
|
||||
Returns:
|
||||
Union[Path, None]: The full path of the module, or None if the path cannot be determined.
|
||||
|
||||
Examples:
|
||||
If `root`(workdir) is "/User/xxx/github/MetaGPT/metagpt", and the `pathname` is
|
||||
"metagpt/management/skill_manager.py", then the returned value will be
|
||||
"/User/xxx/github/MetaGPT/metagpt/management/skill_manager.py"
|
||||
"""
|
||||
if re.match(r"^/.+", pathname):
|
||||
return pathname
|
||||
files = list_files(root=root)
|
||||
postfix = "/" + str(pathname)
|
||||
for i in files:
|
||||
if str(i).endswith(postfix):
|
||||
return i
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def parse_participant(mermaid_sequence_diagram: str) -> List[str]:
|
||||
"""
|
||||
Parses the provided Mermaid sequence diagram and returns the list of participants.
|
||||
|
||||
Args:
|
||||
mermaid_sequence_diagram (str): The Mermaid sequence diagram string to be parsed.
|
||||
|
||||
Returns:
|
||||
List[str]: A list of participants extracted from the sequence diagram.
|
||||
"""
|
||||
pattern = r"participant ([a-zA-Z\.0-9_]+)"
|
||||
matches = re.findall(pattern, mermaid_sequence_diagram)
|
||||
matches = [re.sub(r"[\\/'\"]+", "", i) for i in matches]
|
||||
return matches
|
||||
|
||||
async def _search_new_participant(self, entry: SPO) -> str | None:
|
||||
"""
|
||||
Asynchronously retrieves a participant whose sequence diagram has not been augmented.
|
||||
|
||||
Args:
|
||||
entry (SPO): The SPO object representing the relationship in the graph database.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: A participant whose sequence diagram has not been augmented, or None if not found.
|
||||
"""
|
||||
rows = await self.graph_db.select(subject=entry.subject, predicate=GraphKeyword.HAS_SEQUENCE_VIEW)
|
||||
if not rows:
|
||||
return None
|
||||
sequence_view = rows[0].object_
|
||||
rows = await self.graph_db.select(subject=entry.subject, predicate=GraphKeyword.HAS_PARTICIPANT)
|
||||
merged_participants = []
|
||||
for r in rows:
|
||||
name = split_namespace(r.object_)[-1]
|
||||
merged_participants.append(name)
|
||||
participants = self.parse_participant(sequence_view)
|
||||
for p in participants:
|
||||
if p in merged_participants:
|
||||
continue
|
||||
return p
|
||||
return None
|
||||
|
||||
@retry(
|
||||
wait=wait_random_exponential(min=1, max=20),
|
||||
stop=stop_after_attempt(6),
|
||||
after=general_after_log(logger),
|
||||
)
|
||||
async def _merge_participant(self, entry: SPO, class_name: str):
|
||||
"""
|
||||
Augments the sequence diagram of `class_name` to the sequence diagram of `entry`.
|
||||
|
||||
Args:
|
||||
entry (SPO): The SPO object representing the base sequence diagram.
|
||||
class_name (str): The class name whose sequence diagram is to be augmented.
|
||||
"""
|
||||
rows = await self.graph_db.select(predicate=GraphKeyword.IS, object_=GraphKeyword.CLASS)
|
||||
participants = []
|
||||
for r in rows:
|
||||
name = split_namespace(r.subject)[-1]
|
||||
if name == class_name:
|
||||
participants.append(r)
|
||||
if len(participants) == 0: # external participants
|
||||
await self.graph_db.insert(
|
||||
subject=entry.subject, predicate=GraphKeyword.HAS_PARTICIPANT, object_=concat_namespace("?", class_name)
|
||||
)
|
||||
return
|
||||
if len(participants) > 1:
|
||||
for r in participants:
|
||||
await self.graph_db.insert(
|
||||
subject=entry.subject, predicate=GraphKeyword.HAS_PARTICIPANT, object_=auto_namespace(r.subject)
|
||||
)
|
||||
return
|
||||
|
||||
participant = participants[0]
|
||||
await self._rebuild_sequence_view(participant.subject)
|
||||
sequence_views = await self.graph_db.select(
|
||||
subject=participant.subject, predicate=GraphKeyword.HAS_SEQUENCE_VIEW
|
||||
)
|
||||
if not sequence_views: # external class
|
||||
return
|
||||
rows = await self.graph_db.select(subject=entry.subject, predicate=GraphKeyword.HAS_SEQUENCE_VIEW)
|
||||
prompt = f"```mermaid\n{sequence_views[0].object_}\n```\n---\n```mermaid\n{rows[0].object_}\n```"
|
||||
|
||||
rsp = await self.llm.aask(
|
||||
prompt,
|
||||
system_msgs=[
|
||||
"You are a tool to merge sequence diagrams into one.",
|
||||
"Participants with the same name are considered identical.",
|
||||
"Return the merged Mermaid sequence diagram in a markdown code block format.",
|
||||
],
|
||||
stream=False,
|
||||
)
|
||||
|
||||
sequence_view = rsp.removeprefix("```mermaid").removesuffix("```")
|
||||
rows = await self.graph_db.select(subject=entry.subject, predicate=GraphKeyword.HAS_SEQUENCE_VIEW)
|
||||
for r in rows:
|
||||
await self.graph_db.delete(subject=r.subject, predicate=r.predicate, object_=r.object_)
|
||||
await self.graph_db.insert(
|
||||
subject=entry.subject, predicate=GraphKeyword.HAS_SEQUENCE_VIEW, object_=sequence_view
|
||||
)
|
||||
await self.graph_db.insert(
|
||||
subject=entry.subject,
|
||||
predicate=GraphKeyword.HAS_SEQUENCE_VIEW_VER,
|
||||
object_=concat_namespace(datetime.now().strftime("%Y%m%d%H%M%S%f")[:-3], add_affix(sequence_view)),
|
||||
)
|
||||
await self.graph_db.insert(
|
||||
subject=entry.subject, predicate=GraphKeyword.HAS_PARTICIPANT, object_=auto_namespace(participant.subject)
|
||||
)
|
||||
await self._save_sequence_view(subject=entry.subject, content=sequence_view)
|
||||
|
||||
async def _save_sequence_view(self, subject: str, content: str):
|
||||
pattern = re.compile(r"[^a-zA-Z0-9]")
|
||||
name = re.sub(pattern, "_", subject)
|
||||
filename = Path(name).with_suffix(".sequence_diagram.mmd")
|
||||
await self.context.repo.resources.data_api_design.save(filename=str(filename), content=content)
|
||||
|
||||
async def _search_participants(self, filename: str) -> Set:
|
||||
content = await self._get_source_code(filename)
|
||||
|
||||
rsp = await self.llm.aask(
|
||||
msg=content,
|
||||
system_msgs=[
|
||||
"You are a tool for listing all class names used in a source file.",
|
||||
"Return a markdown JSON object with: "
|
||||
'- a "class_names" key containing the list of class names used in the file; '
|
||||
'- a "reasons" key lists all reason objects, each object containing a "class_name" key for class name, a "reference" key explaining the line where the class has been used.',
|
||||
],
|
||||
)
|
||||
|
||||
class _Data(BaseModel):
|
||||
class_names: List[str]
|
||||
reasons: List
|
||||
|
||||
json_blocks = parse_json_code_block(rsp)
|
||||
data = _Data.model_validate_json(json_blocks[0])
|
||||
return set(data.class_names)
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Time : 2024/1/4
|
||||
@Author : mashenquan
|
||||
@File : rebuild_sequence_view_an.py
|
||||
"""
|
||||
from metagpt.actions.action_node import ActionNode
|
||||
from metagpt.utils.mermaid import MMC2
|
||||
|
||||
CODE_2_MERMAID_SEQUENCE_DIAGRAM = ActionNode(
|
||||
key="Program call flow",
|
||||
expected_type=str,
|
||||
instruction='Translate the "context" content into "format example" format.',
|
||||
example=MMC2,
|
||||
)
|
||||
|
|
@ -50,6 +50,7 @@ class ArgumentsParingAction(Action):
|
|||
rsp = await self.llm.aask(
|
||||
msg=prompt,
|
||||
system_msgs=["You are a function parser.", "You can convert spoken words into function parameters."],
|
||||
stream=False,
|
||||
)
|
||||
logger.debug(f"SKILL:{prompt}\n, RESULT:{rsp}")
|
||||
self.args = ArgumentsParingAction.parse_arguments(skill_name=self.skill.name, txt=rsp)
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ class TalkAction(Action):
|
|||
|
||||
async def run(self, with_message=None, **kwargs) -> Message:
|
||||
msg, format_msgs, system_msgs = self.aask_args
|
||||
rsp = await self.llm.aask(msg=msg, format_msgs=format_msgs, system_msgs=system_msgs)
|
||||
rsp = await self.llm.aask(msg=msg, format_msgs=format_msgs, system_msgs=system_msgs, stream=False)
|
||||
self.rsp = Message(content=rsp, role="assistant", cause_by=self)
|
||||
return self.rsp
|
||||
|
||||
|
|
|
|||
|
|
@ -23,11 +23,7 @@ from tenacity import retry, stop_after_attempt, wait_random_exponential
|
|||
from metagpt.actions.action import Action
|
||||
from metagpt.actions.project_management_an import REFINED_TASK_LIST, TASK_LIST
|
||||
from metagpt.actions.write_code_plan_and_change_an import REFINED_TEMPLATE
|
||||
from metagpt.const import (
|
||||
BUGFIX_FILENAME,
|
||||
CODE_PLAN_AND_CHANGE_FILENAME,
|
||||
REQUIREMENT_FILENAME,
|
||||
)
|
||||
from metagpt.const import BUGFIX_FILENAME, REQUIREMENT_FILENAME
|
||||
from metagpt.logs import logger
|
||||
from metagpt.schema import CodingContext, Document, RunCodeResult
|
||||
from metagpt.utils.common import CodeParser
|
||||
|
|
@ -98,8 +94,6 @@ class WriteCode(Action):
|
|||
bug_feedback = await self.repo.docs.get(filename=BUGFIX_FILENAME)
|
||||
coding_context = CodingContext.loads(self.i_context.content)
|
||||
test_doc = await self.repo.test_outputs.get(filename="test_" + coding_context.filename + ".json")
|
||||
code_plan_and_change_doc = await self.repo.docs.code_plan_and_change.get(filename=CODE_PLAN_AND_CHANGE_FILENAME)
|
||||
code_plan_and_change = code_plan_and_change_doc.content if code_plan_and_change_doc else ""
|
||||
requirement_doc = await self.repo.docs.get(filename=REQUIREMENT_FILENAME)
|
||||
summary_doc = None
|
||||
if coding_context.design_doc and coding_context.design_doc.filename:
|
||||
|
|
@ -111,7 +105,7 @@ class WriteCode(Action):
|
|||
|
||||
if bug_feedback:
|
||||
code_context = coding_context.code_doc.content
|
||||
elif code_plan_and_change:
|
||||
elif self.config.inc:
|
||||
code_context = await self.get_codes(
|
||||
coding_context.task_doc, exclude=self.i_context.filename, project_repo=self.repo, use_inc=True
|
||||
)
|
||||
|
|
@ -122,10 +116,10 @@ class WriteCode(Action):
|
|||
project_repo=self.repo.with_src_path(self.context.src_workspace),
|
||||
)
|
||||
|
||||
if code_plan_and_change:
|
||||
if self.config.inc:
|
||||
prompt = REFINED_TEMPLATE.format(
|
||||
user_requirement=requirement_doc.content if requirement_doc else "",
|
||||
code_plan_and_change=code_plan_and_change,
|
||||
code_plan_and_change=str(coding_context.code_plan_and_change_doc),
|
||||
design=coding_context.design_doc.content if coding_context.design_doc else "",
|
||||
task=coding_context.task_doc.content if coding_context.task_doc else "",
|
||||
code=code_context,
|
||||
|
|
|
|||
|
|
@ -6,30 +6,44 @@
|
|||
@File : write_code_plan_and_change_an.py
|
||||
"""
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from metagpt.actions.action import Action
|
||||
from metagpt.actions.action_node import ActionNode
|
||||
from metagpt.logs import logger
|
||||
from metagpt.schema import CodePlanAndChangeContext
|
||||
|
||||
CODE_PLAN_AND_CHANGE = ActionNode(
|
||||
key="Code Plan And Change",
|
||||
expected_type=str,
|
||||
instruction="Developing comprehensive and step-by-step incremental development plan, and write Incremental "
|
||||
"Change by making a code draft that how to implement incremental development including detailed steps based on the "
|
||||
"context. Note: Track incremental changes using mark of '+' or '-' for add/modify/delete code, and conforms to the "
|
||||
"output format of git diff",
|
||||
example="""
|
||||
1. Plan for calculator.py: Enhance the functionality of `calculator.py` by extending it to incorporate methods for subtraction, multiplication, and division. Additionally, implement robust error handling for the division operation to mitigate potential issues related to division by zero.
|
||||
```python
|
||||
DEVELOPMENT_PLAN = ActionNode(
|
||||
key="Development Plan",
|
||||
expected_type=List[str],
|
||||
instruction="Develop a comprehensive and step-by-step incremental development plan, providing the detail "
|
||||
"changes to be implemented at each step based on the order of 'Task List'",
|
||||
example=[
|
||||
"Enhance the functionality of `calculator.py` by extending it to incorporate methods for subtraction, ...",
|
||||
"Update the existing codebase in main.py to incorporate new API endpoints for subtraction, ...",
|
||||
],
|
||||
)
|
||||
|
||||
INCREMENTAL_CHANGE = ActionNode(
|
||||
key="Incremental Change",
|
||||
expected_type=List[str],
|
||||
instruction="Write Incremental Change by making a code draft that how to implement incremental development "
|
||||
"including detailed steps based on the context. Note: Track incremental changes using the marks `+` and `-` to "
|
||||
"indicate additions and deletions, and ensure compliance with the output format of `git diff`",
|
||||
example=[
|
||||
'''```diff
|
||||
--- Old/calculator.py
|
||||
+++ New/calculator.py
|
||||
|
||||
class Calculator:
|
||||
self.result = number1 + number2
|
||||
return self.result
|
||||
|
||||
- def sub(self, number1, number2) -> float:
|
||||
+ def subtract(self, number1: float, number2: float) -> float:
|
||||
+ '''
|
||||
+ """
|
||||
+ Subtracts the second number from the first and returns the result.
|
||||
+
|
||||
+ Args:
|
||||
|
|
@ -38,13 +52,13 @@ class Calculator:
|
|||
+
|
||||
+ Returns:
|
||||
+ float: The difference of number1 and number2.
|
||||
+ '''
|
||||
+ """
|
||||
+ self.result = number1 - number2
|
||||
+ return self.result
|
||||
+
|
||||
def multiply(self, number1: float, number2: float) -> float:
|
||||
- pass
|
||||
+ '''
|
||||
+ """
|
||||
+ Multiplies two numbers and returns the result.
|
||||
+
|
||||
+ Args:
|
||||
|
|
@ -53,15 +67,15 @@ class Calculator:
|
|||
+
|
||||
+ Returns:
|
||||
+ float: The product of number1 and number2.
|
||||
+ '''
|
||||
+ """
|
||||
+ self.result = number1 * number2
|
||||
+ return self.result
|
||||
+
|
||||
def divide(self, number1: float, number2: float) -> float:
|
||||
- pass
|
||||
+ '''
|
||||
+ """
|
||||
+ ValueError: If the second number is zero.
|
||||
+ '''
|
||||
+ """
|
||||
+ if number2 == 0:
|
||||
+ raise ValueError('Cannot divide by zero')
|
||||
+ self.result = number1 / number2
|
||||
|
|
@ -75,10 +89,11 @@ class Calculator:
|
|||
+ print("Result is already zero, no need to clear.")
|
||||
+
|
||||
self.result = 0.0
|
||||
```
|
||||
```''',
|
||||
"""```diff
|
||||
--- Old/main.py
|
||||
+++ New/main.py
|
||||
|
||||
2. Plan for main.py: Integrate new API endpoints for subtraction, multiplication, and division into the existing codebase of `main.py`. Then, ensure seamless integration with the overall application architecture and maintain consistency with coding standards.
|
||||
```python
|
||||
def add_numbers():
|
||||
result = calculator.add_numbers(num1, num2)
|
||||
return jsonify({'result': result}), 200
|
||||
|
|
@ -106,6 +121,7 @@ def add_numbers():
|
|||
if __name__ == '__main__':
|
||||
app.run()
|
||||
```""",
|
||||
],
|
||||
)
|
||||
|
||||
CODE_PLAN_AND_CHANGE_CONTEXT = """
|
||||
|
|
@ -172,14 +188,16 @@ Role: You are a professional engineer; The main goal is to complete incremental
|
|||
2. COMPLETE CODE: Your code will be part of the entire project, so please implement complete, reliable, reusable code snippets.
|
||||
3. Set default value: If there is any setting, ALWAYS SET A DEFAULT VALUE, ALWAYS USE STRONG TYPE AND EXPLICIT VARIABLE. AVOID circular import.
|
||||
4. Follow design: YOU MUST FOLLOW "Data structures and interfaces". DONT CHANGE ANY DESIGN. Do not use public member functions that do not exist in your design.
|
||||
5. Follow Code Plan And Change: If there is any Incremental Change that is marked by the git diff format using '+' and '-' for add/modify/delete code, or Legacy Code files contain "{filename} to be rewritten", you must merge it into the code file according to the plan.
|
||||
5. Follow Code Plan And Change: If there is any "Incremental Change" that is marked by the git diff format with '+' and '-' symbols, or Legacy Code files contain "{filename} to be rewritten", you must merge it into the code file according to the "Development Plan".
|
||||
6. CAREFULLY CHECK THAT YOU DONT MISS ANY NECESSARY CLASS/FUNCTION IN THIS FILE.
|
||||
7. Before using a external variable/module, make sure you import it first.
|
||||
8. Write out EVERY CODE DETAIL, DON'T LEAVE TODO.
|
||||
9. Attention: Retain details that are not related to incremental development but are important for maintaining the consistency and clarity of the old code.
|
||||
"""
|
||||
|
||||
WRITE_CODE_PLAN_AND_CHANGE_NODE = ActionNode.from_children("WriteCodePlanAndChange", [CODE_PLAN_AND_CHANGE])
|
||||
CODE_PLAN_AND_CHANGE = [DEVELOPMENT_PLAN, INCREMENTAL_CHANGE]
|
||||
|
||||
WRITE_CODE_PLAN_AND_CHANGE_NODE = ActionNode.from_children("WriteCodePlanAndChange", CODE_PLAN_AND_CHANGE)
|
||||
|
||||
|
||||
class WriteCodePlanAndChange(Action):
|
||||
|
|
@ -192,14 +210,14 @@ class WriteCodePlanAndChange(Action):
|
|||
prd_doc = await self.repo.docs.prd.get(filename=self.i_context.prd_filename)
|
||||
design_doc = await self.repo.docs.system_design.get(filename=self.i_context.design_filename)
|
||||
task_doc = await self.repo.docs.task.get(filename=self.i_context.task_filename)
|
||||
code_text = await self.get_old_codes()
|
||||
context = CODE_PLAN_AND_CHANGE_CONTEXT.format(
|
||||
requirement=self.i_context.requirement,
|
||||
prd=prd_doc.content,
|
||||
design=design_doc.content,
|
||||
task=task_doc.content,
|
||||
code=code_text,
|
||||
code=await self.get_old_codes(),
|
||||
)
|
||||
logger.info("Writing code plan and change..")
|
||||
return await WRITE_CODE_PLAN_AND_CHANGE_NODE.fill(context=context, llm=self.llm, schema="json")
|
||||
|
||||
async def get_old_codes(self) -> str:
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from tenacity import retry, stop_after_attempt, wait_random_exponential
|
|||
|
||||
from metagpt.actions import WriteCode
|
||||
from metagpt.actions.action import Action
|
||||
from metagpt.const import CODE_PLAN_AND_CHANGE_FILENAME, REQUIREMENT_FILENAME
|
||||
from metagpt.const import REQUIREMENT_FILENAME
|
||||
from metagpt.logs import logger
|
||||
from metagpt.schema import CodingContext
|
||||
from metagpt.utils.common import CodeParser
|
||||
|
|
@ -149,29 +149,21 @@ class WriteCodeReview(Action):
|
|||
use_inc=self.config.inc,
|
||||
)
|
||||
|
||||
if not self.config.inc:
|
||||
context = "\n".join(
|
||||
[
|
||||
"## System Design\n" + str(self.i_context.design_doc) + "\n",
|
||||
"## Task\n" + task_content + "\n",
|
||||
"## Code Files\n" + code_context + "\n",
|
||||
]
|
||||
)
|
||||
else:
|
||||
ctx_list = [
|
||||
"## System Design\n" + str(self.i_context.design_doc) + "\n",
|
||||
"## Task\n" + task_content + "\n",
|
||||
"## Code Files\n" + code_context + "\n",
|
||||
]
|
||||
if self.config.inc:
|
||||
requirement_doc = await self.repo.docs.get(filename=REQUIREMENT_FILENAME)
|
||||
code_plan_and_change_doc = await self.repo.get(filename=CODE_PLAN_AND_CHANGE_FILENAME)
|
||||
context = "\n".join(
|
||||
[
|
||||
"## User New Requirements\n" + str(requirement_doc) + "\n",
|
||||
"## Code Plan And Change\n" + str(code_plan_and_change_doc) + "\n",
|
||||
"## System Design\n" + str(self.i_context.design_doc) + "\n",
|
||||
"## Task\n" + task_content + "\n",
|
||||
"## Code Files\n" + code_context + "\n",
|
||||
]
|
||||
)
|
||||
insert_ctx_list = [
|
||||
"## User New Requirements\n" + str(requirement_doc) + "\n",
|
||||
"## Code Plan And Change\n" + str(self.i_context.code_plan_and_change_doc) + "\n",
|
||||
]
|
||||
ctx_list = insert_ctx_list + ctx_list
|
||||
|
||||
context_prompt = PROMPT_TEMPLATE.format(
|
||||
context=context,
|
||||
context="\n".join(ctx_list),
|
||||
code=iterative_code,
|
||||
filename=self.i_context.code_doc.filename,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ REFINED_PRODUCT_GOALS = ActionNode(
|
|||
key="Refined Product Goals",
|
||||
expected_type=List[str],
|
||||
instruction="Update and expand the original product goals to reflect the evolving needs due to incremental "
|
||||
"development.Ensure that the refined goals align with the current project direction and contribute to its success.",
|
||||
"development. Ensure that the refined goals align with the current project direction and contribute to its success.",
|
||||
example=[
|
||||
"Enhance user engagement through new features",
|
||||
"Optimize performance for scalability",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue