mirror of
https://github.com/FoundationAgents/MetaGPT.git
synced 2026-06-08 15:05:17 +02:00
feat: RFC 135
This commit is contained in:
parent
27c731d11a
commit
2bf8ef8c6a
16 changed files with 416 additions and 82 deletions
|
|
@ -11,8 +11,9 @@ from typing import List
|
|||
|
||||
from metagpt.actions import Action, ActionOutput
|
||||
from metagpt.config import CONFIG
|
||||
from metagpt.const import WORKSPACE_ROOT
|
||||
from metagpt.const import PRDS_FILE_REPO, SYS_DESIGN_FILE_REPO, WORKSPACE_ROOT
|
||||
from metagpt.logs import logger
|
||||
from metagpt.schema import Document, Documents
|
||||
from metagpt.utils.common import CodeParser
|
||||
from metagpt.utils.get_template import get_template
|
||||
from metagpt.utils.json_to_markdown import json_to_markdown
|
||||
|
|
@ -202,7 +203,44 @@ class WriteDesign(Action):
|
|||
await self._save_prd(docs_path, resources_path, context)
|
||||
await self._save_system_design(docs_path, resources_path, system_design)
|
||||
|
||||
async def run(self, context, format=CONFIG.prompt_format):
|
||||
async def run(self, with_messages, format=CONFIG.prompt_format):
|
||||
# 通过git diff来识别docs/prds下哪些PRD文档发生了变动
|
||||
prds_file_repo = CONFIG.git_repo.new_file_repository(PRDS_FILE_REPO)
|
||||
changed_prds = prds_file_repo.changed_files
|
||||
# 通过git diff来识别docs/system_designs下那些设计文档发生了变动;
|
||||
system_design_file_repo = CONFIG.git_repo.new_file_repository(SYS_DESIGN_FILE_REPO)
|
||||
changed_system_designs = system_design_file_repo.changed_files
|
||||
|
||||
# 对于那些发生变动的PRD和设计文档,重新生成设计内容;
|
||||
changed_files = Documents()
|
||||
for filename in changed_prds.keys():
|
||||
prd = await prds_file_repo.get(filename)
|
||||
old_system_design_doc = await system_design_file_repo.get(filename)
|
||||
if not old_system_design_doc:
|
||||
system_design = await self._run(context=prd.content)
|
||||
doc = Document(
|
||||
root_path=SYS_DESIGN_FILE_REPO, filename=filename, content=system_design.instruct_content.json()
|
||||
)
|
||||
else:
|
||||
doc = await self._merge(prd_doc=prd, system_design_doc=old_system_design_doc)
|
||||
await system_design_file_repo.save(
|
||||
filename=filename, content=doc.content, dependencies={prd.root_relative_path}
|
||||
)
|
||||
changed_files.docs[filename] = doc
|
||||
|
||||
for filename in changed_system_designs.keys():
|
||||
if filename in changed_files.docs:
|
||||
continue
|
||||
prd_doc = await prds_file_repo.get(filename=filename)
|
||||
old_system_design_doc = await system_design_file_repo.get(filename)
|
||||
new_system_design_doc = await self._merge(prd_doc, old_system_design_doc)
|
||||
await system_design_file_repo.save(filename=filename, content=new_system_design_doc.content)
|
||||
changed_files.docs[filename] = new_system_design_doc
|
||||
|
||||
# 等docs/system_designs/下所有文件都处理完才发publish message,给后续做全局优化留空间。
|
||||
return ActionOutput(content=changed_files.json(), instruct_content=changed_files)
|
||||
|
||||
async def _run(self, context, format=CONFIG.prompt_format):
|
||||
prompt_template, format_example = get_template(templates, format)
|
||||
prompt = prompt_template.format(context=context, format_example=format_example)
|
||||
# system_design = await self._aask(prompt)
|
||||
|
|
@ -213,5 +251,8 @@ class WriteDesign(Action):
|
|||
"Python package name",
|
||||
system_design.instruct_content.dict()["Python package name"].strip().strip("'").strip('"'),
|
||||
)
|
||||
await self._save(context, system_design)
|
||||
# await self._save(context, system_design)
|
||||
return system_design
|
||||
|
||||
async def _merge(self, prd_doc, system_design_doc):
|
||||
return system_design_doc
|
||||
|
|
|
|||
|
|
@ -7,19 +7,37 @@
|
|||
@Desc: PrepareDocuments Action: initialize project folder and add new requirements to docs/requirements.txt.
|
||||
RFC 135 2.2.3.5.1.
|
||||
"""
|
||||
from metagpt.actions import Action
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from metagpt.actions import Action, ActionOutput
|
||||
from metagpt.config import CONFIG
|
||||
from metagpt.const import DOCS_FILE_REPO, REQUIREMENT_FILENAME, WORKSPACE_ROOT
|
||||
from metagpt.schema import Document
|
||||
from metagpt.utils.file_repository import FileRepository
|
||||
from metagpt.utils.git_repository import GitRepository
|
||||
|
||||
|
||||
class PrepareDocuments(Action):
|
||||
def __init__(self, name="", context=None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
|
||||
async def run(self, with_message, **kwargs):
|
||||
parent = self.context.get("parent")
|
||||
if not parent:
|
||||
raise ValueError("Invalid owner")
|
||||
env = parent.get_env()
|
||||
if env.git_repository:
|
||||
return
|
||||
env.git_repository = GitRepository()
|
||||
env.git_repository.open(WORKS)
|
||||
async def run(self, with_messages, **kwargs):
|
||||
if CONFIG.git_repo:
|
||||
docs_repo = CONFIG.git_repo.new_file_repository(DOCS_FILE_REPO)
|
||||
doc = await docs_repo.get(REQUIREMENT_FILENAME)
|
||||
return ActionOutput(content=doc.json(exclue="content"), instruct_content=doc)
|
||||
|
||||
# Create and initialize the workspace folder, initialize the Git environment.
|
||||
CONFIG.git_repo = GitRepository()
|
||||
workdir = Path(CONFIG.WORKDIR) if CONFIG.WORKDIR else WORKSPACE_ROOT / FileRepository.new_file_name()
|
||||
CONFIG.git_repo.open(local_path=workdir, auto_init=True)
|
||||
|
||||
# Write the newly added requirements from the main parameter idea to `docs/requirement.txt`.
|
||||
docs_file_repository = CONFIG.git_repo.new_file_repository(DOCS_FILE_REPO)
|
||||
doc = Document(root_path=DOCS_FILE_REPO, filename=REQUIREMENT_FILENAME, content=with_messages[0].content)
|
||||
await docs_file_repository.save(REQUIREMENT_FILENAME, content=doc.content)
|
||||
|
||||
# Send a Message notification to the WritePRD action, instructing it to process requirements using
|
||||
# `docs/requirement.txt` and `docs/prds/`.
|
||||
return ActionOutput(content=doc.content, instruct_content=doc)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,10 @@ from typing import List
|
|||
from metagpt.actions import Action, ActionOutput
|
||||
from metagpt.actions.search_and_summarize import SearchAndSummarize
|
||||
from metagpt.config import CONFIG
|
||||
from metagpt.const import DOCS_FILE_REPO, PRDS_FILE_REPO, REQUIREMENT_FILENAME
|
||||
from metagpt.logs import logger
|
||||
from metagpt.schema import Document, Documents
|
||||
from metagpt.utils.file_repository import FileRepository
|
||||
from metagpt.utils.get_template import get_template
|
||||
|
||||
templates = {
|
||||
|
|
@ -222,7 +225,34 @@ class WritePRD(Action):
|
|||
def __init__(self, name="", context=None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
|
||||
async def run(self, requirements, format=CONFIG.prompt_format, *args, **kwargs) -> ActionOutput:
|
||||
async def run(self, with_messages, format=CONFIG.prompt_format, *args, **kwargs) -> ActionOutput:
|
||||
# 判断哪些需求文档需要重写:调LLM判断新增需求与prd是否相关,若相关就rewrite prd
|
||||
docs_file_repo = CONFIG.git_repo.new_file_repository(DOCS_FILE_REPO)
|
||||
requirement_doc = await docs_file_repo.get(REQUIREMENT_FILENAME)
|
||||
prds_file_repo = CONFIG.git_repo.new_file_repository(PRDS_FILE_REPO)
|
||||
prd_docs = await prds_file_repo.get_all()
|
||||
change_files = Documents()
|
||||
for prd_doc in prd_docs:
|
||||
if await self._is_relative_to(requirement_doc, prd_doc):
|
||||
prd_doc = await self._merge(requirement_doc, prd_doc)
|
||||
await prds_file_repo.save(filename=prd_doc.filename, content=prd_doc.content)
|
||||
change_files.docs[prd_doc.filename] = prd_doc
|
||||
# 如果没有任何PRD,就使用docs/requirement.txt生成一个prd
|
||||
if not change_files.docs:
|
||||
prd = await self._run_new_requirement(
|
||||
requirements=[requirement_doc.content], format=format, *args, **kwargs
|
||||
)
|
||||
doc = Document(
|
||||
root_path=PRDS_FILE_REPO,
|
||||
filename=FileRepository.new_file_name() + ".json",
|
||||
content=prd.instruct_content.json(),
|
||||
)
|
||||
await prds_file_repo.save(filename=doc.filename, content=doc.content)
|
||||
change_files.docs[doc.filename] = doc
|
||||
# 等docs/prds/下所有文件都与新增需求对比完后,再触发publish message让工作流跳转到下一环节。如此设计是为了给后续做全局优化留空间。
|
||||
return ActionOutput(content=change_files.json(), instruct_content=change_files)
|
||||
|
||||
async def _run_new_requirement(self, requirements, format=CONFIG.prompt_format, *args, **kwargs) -> ActionOutput:
|
||||
sas = SearchAndSummarize()
|
||||
# rsp = await sas.run(context=requirements, system_text=SEARCH_AND_SUMMARIZE_SYSTEM_EN_US)
|
||||
rsp = ""
|
||||
|
|
@ -239,3 +269,9 @@ class WritePRD(Action):
|
|||
# prd = await self._aask_v1(prompt, "prd", OUTPUT_MAPPING)
|
||||
prd = await self._aask_v1(prompt, "prd", OUTPUT_MAPPING, format=format)
|
||||
return prd
|
||||
|
||||
async def _is_relative_to(self, doc1, doc2) -> bool:
|
||||
return False
|
||||
|
||||
async def _merge(self, doc1, doc2) -> Document:
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class Config(metaclass=Singleton):
|
|||
self.openai_api_key = self._get("OPENAI_API_KEY")
|
||||
self.anthropic_api_key = self._get("Anthropic_API_KEY")
|
||||
if (not self.openai_api_key or "YOUR_API_KEY" == self.openai_api_key) and (
|
||||
not self.anthropic_api_key or "YOUR_API_KEY" == self.anthropic_api_key
|
||||
not self.anthropic_api_key or "YOUR_API_KEY" == self.anthropic_api_key
|
||||
):
|
||||
raise NotConfiguredException("Set OPENAI_API_KEY or Anthropic_API_KEY first")
|
||||
self.openai_api_base = self._get("OPENAI_API_BASE")
|
||||
|
|
@ -93,6 +93,7 @@ class Config(metaclass=Singleton):
|
|||
self.pyppeteer_executable_path = self._get("PYPPETEER_EXECUTABLE_PATH", "")
|
||||
|
||||
self.prompt_format = self._get("PROMPT_FORMAT", "markdown")
|
||||
self.git_repo = None
|
||||
|
||||
def _init_with_config_files_and_env(self, configs: dict, yaml_file):
|
||||
"""Load from config/key.yaml, config/config.yaml, and env in decreasing order of priority"""
|
||||
|
|
|
|||
|
|
@ -49,3 +49,8 @@ MESSAGE_ROUTE_TO = "send_to"
|
|||
MESSAGE_ROUTE_CAUSE_BY = "cause_by"
|
||||
MESSAGE_META_ROLE = "role"
|
||||
MESSAGE_ROUTE_TO_ALL = "<all>"
|
||||
|
||||
REQUIREMENT_FILENAME = "requirement.txt"
|
||||
DOCS_FILE_REPO = "docs"
|
||||
PRDS_FILE_REPO = "docs/prds"
|
||||
SYS_DESIGN_FILE_REPO = "docs/system_design"
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
functionality is to be consolidated into the `Environment` class.
|
||||
"""
|
||||
import asyncio
|
||||
from typing import Iterable, Optional, Set
|
||||
from typing import Iterable, Set
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
|
@ -20,7 +20,6 @@ from metagpt.logs import logger
|
|||
from metagpt.roles import Role
|
||||
from metagpt.schema import Message
|
||||
from metagpt.utils.common import is_subscribed
|
||||
from metagpt.utils.git_repository import GitRepository
|
||||
|
||||
|
||||
class Environment(BaseModel):
|
||||
|
|
@ -32,7 +31,6 @@ class Environment(BaseModel):
|
|||
roles: dict[str, Role] = Field(default_factory=dict)
|
||||
consumers: dict[Role, Set] = Field(default_factory=dict)
|
||||
history: str = Field(default="") # For debug
|
||||
git_repository: Optional[GitRepository] = None
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
|
@ -113,8 +111,3 @@ class Environment(BaseModel):
|
|||
def set_subscription(self, obj, tags):
|
||||
"""Set the labels for message to be consumed by the object"""
|
||||
self.consumers[obj] = tags
|
||||
|
||||
def dict(self, *args, **kwargs):
|
||||
"""Generate a dictionary representation of the model, optionally specifying which fields to include or
|
||||
exclude."""
|
||||
return super(Environment, self).dict(exclude={"git_repository"})
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
"""
|
||||
from metagpt.actions import BossRequirement, WritePRD
|
||||
from metagpt.actions.prepare_documents import PrepareDocuments
|
||||
from metagpt.config import CONFIG
|
||||
from metagpt.roles import Role
|
||||
|
||||
|
||||
|
|
@ -38,12 +39,12 @@ class ProductManager(Role):
|
|||
constraints (str): Constraints or limitations for the product manager.
|
||||
"""
|
||||
super().__init__(name, profile, goal, constraints)
|
||||
self._init_actions([PrepareDocuments(context={"parent": self}), WritePRD])
|
||||
self._watch([BossRequirement])
|
||||
self._init_actions([PrepareDocuments, WritePRD])
|
||||
self._watch([BossRequirement, PrepareDocuments])
|
||||
|
||||
async def _think(self) -> None:
|
||||
"""Decide what to do"""
|
||||
if self._rc.env.git_repository:
|
||||
if CONFIG.git_repo:
|
||||
self._set_state(1)
|
||||
else:
|
||||
self._set_state(0)
|
||||
|
|
|
|||
|
|
@ -6,14 +6,16 @@
|
|||
@File : schema.py
|
||||
@Modified By: mashenquan, 2023-10-31. According to Chapter 2.2.1 of RFC 116:
|
||||
Replanned the distribution of responsibilities and functional positioning of `Message` class attributes.
|
||||
@Modified By: mashenquan, 2023/11/22. Add `Document` and `Documents` for `FileRepository` in Section 2.2.3.4 of RFC 135.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os.path
|
||||
from asyncio import Queue, QueueEmpty, wait_for
|
||||
from json import JSONDecodeError
|
||||
from typing import List, Set, TypedDict
|
||||
from typing import Dict, List, Optional, Set, TypedDict
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
|
@ -32,6 +34,42 @@ class RawMessage(TypedDict):
|
|||
role: str
|
||||
|
||||
|
||||
class Document(BaseModel):
|
||||
"""
|
||||
Represents a document.
|
||||
"""
|
||||
|
||||
root_path: str
|
||||
filename: str
|
||||
content: Optional[str] = None
|
||||
|
||||
def get_meta(self) -> Document:
|
||||
"""Get metadata of the document.
|
||||
|
||||
:return: A new Document instance with the same root path and filename.
|
||||
"""
|
||||
|
||||
return Document(root_path=self.root_path, filename=self.filename)
|
||||
|
||||
@property
|
||||
def root_relative_path(self):
|
||||
"""Get relative path from root of git repository.
|
||||
|
||||
:return: relative path from root of git repository.
|
||||
"""
|
||||
return os.path.join(self.root_path, self.filename)
|
||||
|
||||
|
||||
class Documents(BaseModel):
|
||||
"""A class representing a collection of documents.
|
||||
|
||||
Attributes:
|
||||
docs (Dict[str, Document]): A dictionary mapping document names to Document instances.
|
||||
"""
|
||||
|
||||
docs: Dict[str, Document] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class Message(BaseModel):
|
||||
"""list[<role>: <content>]"""
|
||||
|
||||
|
|
|
|||
83
metagpt/utils/dependency_file.py
Normal file
83
metagpt/utils/dependency_file.py
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Time : 2023/11/22
|
||||
@Author : mashenquan
|
||||
@File : dependency_file.py
|
||||
@Desc: Implementation of the dependency file described in Section 2.2.3.2 of RFC 135.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Set
|
||||
|
||||
import aiofiles
|
||||
|
||||
from metagpt.logs import logger
|
||||
|
||||
|
||||
class DependencyFile:
|
||||
def __init__(self, workdir: Path | str):
|
||||
self._dependencies = {}
|
||||
self._filename = Path(workdir) / ".dependencies.json"
|
||||
|
||||
async def load(self):
|
||||
if not self._filename.exists():
|
||||
return
|
||||
try:
|
||||
async with aiofiles.open(str(self._filename), mode="r") as reader:
|
||||
data = await reader.read()
|
||||
self._dependencies = json.loads(data)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load {str(self._filename)}, error:{e}")
|
||||
|
||||
async def save(self):
|
||||
try:
|
||||
data = json.dumps(self._dependencies)
|
||||
async with aiofiles.open(str(self._filename), mode="w") as writer:
|
||||
await writer.write(data)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save {str(self._filename)}, error:{e}")
|
||||
|
||||
async def update(self, filename: Path | str, dependencies: Set[Path | str], persist=True):
|
||||
if persist:
|
||||
await self.load()
|
||||
|
||||
root = self._filename.parent
|
||||
try:
|
||||
key = Path(filename).relative_to(root)
|
||||
except ValueError:
|
||||
key = filename
|
||||
|
||||
if dependencies:
|
||||
relative_paths = []
|
||||
for i in dependencies:
|
||||
try:
|
||||
relative_paths.append(str(Path(i).relative_to(root)))
|
||||
except ValueError:
|
||||
relative_paths.append(str(i))
|
||||
self._dependencies[str(key)] = relative_paths
|
||||
elif str(key) in self._dependencies:
|
||||
del self._dependencies[str(key)]
|
||||
|
||||
if persist:
|
||||
await self.save()
|
||||
|
||||
async def get(self, filename: Path | str, persist=False):
|
||||
if persist:
|
||||
await self.load()
|
||||
|
||||
root = self._filename.parent
|
||||
try:
|
||||
key = Path(filename).relative_to(root)
|
||||
except ValueError:
|
||||
key = filename
|
||||
return set(self._dependencies.get(str(key), {}))
|
||||
|
||||
def delete_file(self):
|
||||
self._filename.unlink(missing_ok=True)
|
||||
|
||||
@property
|
||||
def exists(self):
|
||||
return self._filename.exists()
|
||||
|
|
@ -8,16 +8,29 @@
|
|||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
from typing import Dict, List, Set
|
||||
|
||||
import aiofiles
|
||||
|
||||
from metagpt.logs import logger
|
||||
from metagpt.schema import Document
|
||||
|
||||
|
||||
class FileRepository:
|
||||
"""A class representing a FileRepository associated with a Git repository.
|
||||
|
||||
:param git_repo: The associated GitRepository instance.
|
||||
:param relative_path: The relative path within the Git repository.
|
||||
|
||||
Attributes:
|
||||
_relative_path (Path): The relative path within the Git repository.
|
||||
_git_repo (GitRepository): The associated GitRepository instance.
|
||||
"""
|
||||
|
||||
def __init__(self, git_repo, relative_path: Path = Path(".")):
|
||||
"""Initialize a FileRepository instance.
|
||||
|
||||
|
|
@ -26,16 +39,9 @@ class FileRepository:
|
|||
"""
|
||||
self._relative_path = relative_path
|
||||
self._git_repo = git_repo
|
||||
self._dependencies: Dict[str, List[str]] = {}
|
||||
|
||||
# Initializing
|
||||
self.workdir.mkdir(parents=True, exist_ok=True)
|
||||
if self.dependency_path_name.exists():
|
||||
try:
|
||||
with open(str(self.dependency_path_name), mode="r") as reader:
|
||||
self._dependencies = json.load(reader)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load {str(self.dependency_path_name)}, error:{e}")
|
||||
|
||||
async def save(self, filename: Path | str, content, dependencies: List[str] = None):
|
||||
"""Save content to a file and update its dependencies.
|
||||
|
|
@ -44,59 +50,68 @@ class FileRepository:
|
|||
:param content: The content to be saved.
|
||||
:param dependencies: List of dependency filenames or paths.
|
||||
"""
|
||||
path_name = self.workdir / filename
|
||||
path_name.parent.mkdir(parents=True, exist_ok=True)
|
||||
async with aiofiles.open(str(path_name), mode="w") as writer:
|
||||
pathname = self.workdir / filename
|
||||
pathname.parent.mkdir(parents=True, exist_ok=True)
|
||||
async with aiofiles.open(str(pathname), mode="w") as writer:
|
||||
await writer.write(content)
|
||||
logger.info(f"save to: {str(pathname)}")
|
||||
|
||||
if dependencies is not None:
|
||||
await self.update_dependency(filename, dependencies)
|
||||
dependency_file = await self._git_repo.get_dependency()
|
||||
await dependency_file.update(pathname, set(dependencies))
|
||||
logger.info(f"update dependency: {str(pathname)}:{dependencies}")
|
||||
|
||||
async def get(self, filename: Path | str):
|
||||
"""Read the content of a file.
|
||||
|
||||
:param filename: The filename or path within the repository.
|
||||
:return: The content of the file.
|
||||
"""
|
||||
path_name = self.workdir / filename
|
||||
async with aiofiles.open(str(path_name), mode="r") as reader:
|
||||
return await reader.read()
|
||||
|
||||
def get_dependency(self, filename: Path | str) -> List:
|
||||
async def get_dependency(self, filename: Path | str) -> Set[str]:
|
||||
"""Get the dependencies of a file.
|
||||
|
||||
:param filename: The filename or path within the repository.
|
||||
:return: List of dependency filenames or paths.
|
||||
:return: Set of dependency filenames or paths.
|
||||
"""
|
||||
key = str(filename)
|
||||
return self._dependencies.get(key, [])
|
||||
pathname = self.workdir / filename
|
||||
dependency_file = await self._git_repo.get_dependency()
|
||||
return await dependency_file.get(pathname)
|
||||
|
||||
def get_changed_dependency(self, filename: Path | str) -> List:
|
||||
async def get_changed_dependency(self, filename: Path | str) -> Set[str]:
|
||||
"""Get the dependencies of a file that have changed.
|
||||
|
||||
:param filename: The filename or path within the repository.
|
||||
:return: List of changed dependency filenames or paths.
|
||||
"""
|
||||
dependencies = self.get_dependency(filename=filename)
|
||||
dependencies = await self.get_dependency(filename=filename)
|
||||
changed_files = self.changed_files
|
||||
changed_dependent_files = []
|
||||
changed_dependent_files = set()
|
||||
for df in dependencies:
|
||||
if df in changed_files.keys():
|
||||
changed_dependent_files.append(df)
|
||||
changed_dependent_files.add(df)
|
||||
return changed_dependent_files
|
||||
|
||||
async def update_dependency(self, filename, dependencies: List[str]):
|
||||
"""Update the dependencies of a file.
|
||||
async def get(self, filename: Path | str) -> Document | None:
|
||||
"""Read the content of a file.
|
||||
|
||||
:param filename: The filename or path within the repository.
|
||||
:param dependencies: List of dependency filenames or paths.
|
||||
:return: The content of the file.
|
||||
"""
|
||||
self._dependencies[str(filename)] = dependencies
|
||||
doc = Document(root_path=str(self.root_path), filename=str(filename))
|
||||
path_name = self.workdir / filename
|
||||
if not path_name.exists():
|
||||
return None
|
||||
async with aiofiles.open(str(path_name), mode="r") as reader:
|
||||
doc.content = await reader.read()
|
||||
return doc
|
||||
|
||||
async def save_dependency(self):
|
||||
"""Save the dependencies to a file."""
|
||||
data = json.dumps(self._dependencies)
|
||||
with aiofiles.open(str(self.dependency_path_name), mode="w") as writer:
|
||||
await writer.write(data)
|
||||
async def get_all(self) -> List[Document]:
|
||||
"""Get the content of all files in the repository.
|
||||
|
||||
:return: List of Document instances representing files.
|
||||
"""
|
||||
docs = []
|
||||
for root, dirs, files in os.walk(str(self.workdir)):
|
||||
for file in files:
|
||||
file_path = Path(root) / file
|
||||
relative_path = file_path.relative_to(self.workdir)
|
||||
doc = await self.get(relative_path)
|
||||
docs.append(doc)
|
||||
return docs
|
||||
|
||||
@property
|
||||
def workdir(self):
|
||||
|
|
@ -107,14 +122,9 @@ class FileRepository:
|
|||
return self._git_repo.workdir / self._relative_path
|
||||
|
||||
@property
|
||||
def dependency_path_name(self):
|
||||
"""Return the absolute path to the dependency file.
|
||||
|
||||
:return: The absolute path to the dependency file.
|
||||
"""
|
||||
filename = ".dependencies.json"
|
||||
path_name = self.workdir / filename
|
||||
return path_name
|
||||
def root_path(self):
|
||||
"""Return the relative path from git repository root"""
|
||||
return self._relative_path
|
||||
|
||||
@property
|
||||
def changed_files(self) -> Dict[str, str]:
|
||||
|
|
@ -147,3 +157,13 @@ class FileRepository:
|
|||
continue
|
||||
children.append(str(f))
|
||||
return children
|
||||
|
||||
@staticmethod
|
||||
def new_file_name():
|
||||
"""Generate a new filename based on the current timestamp and a UUID suffix.
|
||||
|
||||
:return: A new filename string.
|
||||
"""
|
||||
current_time = datetime.now().strftime("%Y%m%d%H%M%S")
|
||||
guid_suffix = str(uuid.uuid4())[:8]
|
||||
return f"{current_time}t{guid_suffix}"
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ from git.repo import Repo
|
|||
from git.repo.fun import is_git_dir
|
||||
|
||||
from metagpt.const import WORKSPACE_ROOT
|
||||
from metagpt.utils.dependency_file import DependencyFile
|
||||
from metagpt.utils.file_repository import FileRepository
|
||||
|
||||
|
||||
|
|
@ -47,6 +48,7 @@ class GitRepository:
|
|||
:param auto_init: If True, automatically initializes a new Git repository if the provided path is not a Git repository.
|
||||
"""
|
||||
self._repository = None
|
||||
self._dependency = None
|
||||
if local_path:
|
||||
self.open(local_path=local_path, auto_init=auto_init)
|
||||
|
||||
|
|
@ -113,7 +115,7 @@ class GitRepository:
|
|||
:param local_path: The local path to check.
|
||||
:return: True if the directory is a Git repository, False otherwise.
|
||||
"""
|
||||
git_dir = local_path / ".git"
|
||||
git_dir = Path(local_path) / ".git"
|
||||
if git_dir.exists() and is_git_dir(git_dir):
|
||||
return True
|
||||
return False
|
||||
|
|
@ -151,7 +153,7 @@ class GitRepository:
|
|||
self.add_change(self.changed_files)
|
||||
self.commit(comments)
|
||||
|
||||
def new_file_repository(self, relative_path: Path | str) -> FileRepository:
|
||||
def new_file_repository(self, relative_path: Path | str = ".") -> FileRepository:
|
||||
"""Create a new instance of FileRepository associated with this Git repository.
|
||||
|
||||
:param relative_path: The relative path to the file repository within the Git repository.
|
||||
|
|
@ -159,6 +161,15 @@ class GitRepository:
|
|||
"""
|
||||
return FileRepository(git_repo=self, relative_path=Path(relative_path))
|
||||
|
||||
async def get_dependency(self) -> DependencyFile:
|
||||
"""Get the dependency file associated with the Git repository.
|
||||
|
||||
:return: An instance of DependencyFile.
|
||||
"""
|
||||
if not self._dependency:
|
||||
self._dependency = DependencyFile(workdir=self.workdir)
|
||||
return self._dependency
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
path = WORKSPACE_ROOT / "git"
|
||||
|
|
|
|||
|
|
@ -44,4 +44,4 @@ ta==0.10.2
|
|||
semantic-kernel==0.3.13.dev0
|
||||
wrapt==1.15.0
|
||||
websocket-client==0.58.0
|
||||
|
||||
aiofiles==23.2.1
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import asyncio
|
|||
|
||||
import fire
|
||||
|
||||
from metagpt.config import CONFIG
|
||||
from metagpt.roles import (
|
||||
Architect,
|
||||
Engineer,
|
||||
|
|
@ -54,6 +55,7 @@ def main(
|
|||
code_review: bool = True,
|
||||
run_tests: bool = False,
|
||||
implement: bool = True,
|
||||
project_path: str = None,
|
||||
):
|
||||
"""
|
||||
We are a software startup comprised of AI. By investing in us,
|
||||
|
|
@ -63,8 +65,12 @@ def main(
|
|||
a certain dollar amount to this AI company.
|
||||
:param n_round:
|
||||
:param code_review: Whether to use code review.
|
||||
:param run_tests: Whether run unit tests.
|
||||
:param implement: Whether to write codes.
|
||||
:param project_path: The path of the old version project to improve.
|
||||
:return:
|
||||
"""
|
||||
CONFIG.WORKDIR = project_path
|
||||
asyncio.run(startup(idea, investment, n_round, code_review, run_tests, implement))
|
||||
|
||||
|
||||
|
|
|
|||
64
tests/metagpt/utils/test_dependency_file.py
Normal file
64
tests/metagpt/utils/test_dependency_file.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Time : 2023/11/22
|
||||
@Author : mashenquan
|
||||
@File : test_dependency_file.py
|
||||
@Desc: Unit tests for dependency_file.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional, Set, Union
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel
|
||||
|
||||
from metagpt.utils.dependency_file import DependencyFile
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dependency_file():
|
||||
class Input(BaseModel):
|
||||
x: Union[Path, str]
|
||||
deps: Optional[Set[Union[Path, str]]]
|
||||
key: Optional[Union[Path, str]]
|
||||
want: Set[str]
|
||||
|
||||
inputs = [
|
||||
Input(x="a/b.txt", deps={"c/e.txt", Path(__file__).parent / "d.txt"}, want={"c/e.txt", "d.txt"}),
|
||||
Input(
|
||||
x=Path(__file__).parent / "x/b.txt",
|
||||
deps={"s/e.txt", Path(__file__).parent / "d.txt"},
|
||||
key="x/b.txt",
|
||||
want={"s/e.txt", "d.txt"},
|
||||
),
|
||||
Input(x="f.txt", deps=None, want=set()),
|
||||
Input(x="a/b.txt", deps=None, want=set()),
|
||||
]
|
||||
|
||||
file = DependencyFile(workdir=Path(__file__).parent)
|
||||
|
||||
for i in inputs:
|
||||
await file.update(filename=i.x, dependencies=i.deps)
|
||||
assert await file.get(filename=i.key or i.x) == i.want
|
||||
|
||||
file2 = DependencyFile(workdir=Path(__file__).parent)
|
||||
file2.delete_file()
|
||||
assert not file.exists
|
||||
await file2.update(filename="a/b.txt", dependencies={"c/e.txt", Path(__file__).parent / "d.txt"}, persist=False)
|
||||
assert not file.exists
|
||||
await file2.save()
|
||||
assert file2.exists
|
||||
|
||||
file1 = DependencyFile(workdir=Path(__file__).parent)
|
||||
assert file1.exists
|
||||
assert await file1.get("a/b.txt") == set()
|
||||
await file1.load()
|
||||
assert await file1.get("a/b.txt") == {"c/e.txt", "d.txt"}
|
||||
file1.delete_file()
|
||||
assert not file.exists
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-s"])
|
||||
|
|
@ -34,11 +34,13 @@ async def test_file_repo():
|
|||
assert file_repo.workdir.exists()
|
||||
await file_repo.save("a.txt", "AAA")
|
||||
await file_repo.save("b.txt", "BBB", ["a.txt"])
|
||||
assert "AAA" == await file_repo.get("a.txt")
|
||||
assert "BBB" == await file_repo.get("b.txt")
|
||||
assert ["a.txt"] == file_repo.get_dependency("b.txt")
|
||||
doc = await file_repo.get("a.txt")
|
||||
assert "AAA" == doc.content
|
||||
doc = await file_repo.get("b.txt")
|
||||
assert "BBB" == doc.content
|
||||
assert {"a.txt"} == await file_repo.get_dependency("b.txt")
|
||||
assert {"a.txt": ChangeType.UNTRACTED, "b.txt": ChangeType.UNTRACTED} == file_repo.changed_files
|
||||
assert ["a.txt"] == file_repo.get_changed_dependency("b.txt")
|
||||
assert {"a.txt"} == await file_repo.get_changed_dependency("b.txt")
|
||||
await file_repo.save("d/e.txt", "EEE")
|
||||
assert ["d/e.txt"] == file_repo.get_change_dir_files("d")
|
||||
|
||||
|
|
|
|||
|
|
@ -77,5 +77,20 @@ async def test_git1():
|
|||
assert not local_path.exists()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dependency_file():
|
||||
local_path = Path(__file__).parent / "git2"
|
||||
repo, subdir = await mock_repo(local_path)
|
||||
|
||||
dependancy_file = await repo.get_dependency()
|
||||
assert not dependancy_file.exists
|
||||
|
||||
await dependancy_file.update(filename="a/b.txt", dependencies={"c/d.txt", "e/f.txt"})
|
||||
assert dependancy_file.exists
|
||||
|
||||
repo.delete_repository()
|
||||
assert not dependancy_file.exists
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-s"])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue